Android音视频-FFmpeg推流Mp4文件到电脑端查看

本文主要了解通过FFmpeg在Android端来进行推流,其中推流的主要代码参考自雷神,我主要先了解其中一个大体的过程,里面的推流c代码没有去细究。本文要了解掌握的的知识点:

  • FFmpeg在Android端推流一个视频文件
  • 搭建简单的流媒体服务器(srs)
  • 查看推出来的音视频数据流(使用VLC查看)

FFmpeg推流

本示例是和上一篇的整合一个FFmpeg so放在同一个module下面的,所以不用在引入FFmpeg文件了,直接专注和推流相关的事情。

编写推流底层C相关代码

在cpp文件下面新建一饿c文件streamer.c,这里是推流的关键代码:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <stdio.h>
#include <jni.h>
#include <time.h>

#include "include/libavcodec/avcodec.h"
#include "include/libavformat/avformat.h"
#include "include/libavutil/log.h"
#include "include/libavutil/time.h"

//Log
#ifdef ANDROID

#include <android/log.h>

#define LOGE(format, ...)  __android_log_print(ANDROID_LOG_ERROR, "(>_<)", format, ##__VA_ARGS__)
#else
#define LOGE(format, ...)  printf("(>_<) " format "\n", ##__VA_ARGS__)
#endif

/* Header for class com_lyman_ffmpeg_cmake_single_StreamerActivity */

#ifndef _Included_com_lyman_ffmpeg_cmake_single_StreamerActivity
#define _Included_com_lyman_ffmpeg_cmake_single_StreamerActivity
#ifdef __cplusplus
extern "C" {
#endif

void custom_log(void *ptr, int level, const char *fmt, va_list vl) {

    //To TXT file
    FILE *fp = fopen("/storage/emulated/0/av_log.txt", "a+");
    if (fp) {
        vfprintf(fp, fmt, vl);
        fflush(fp);
        fclose(fp);
    }

    //To Logcat
    //LOGE(fmt, vl);
}

/*
 * Class:     com_lyman_ffmpeg_cmake_single_StreamerActivity
 * Method:    stream
 * Signature: (Ljava/lang/String;Ljava/lang/String;)I
 */
JNIEXPORT jint JNICALL Java_com_lyman_ffmpeg_1cmake_1single_StreamerActivity_stream
        (JNIEnv *env, jobject obj, jstring input_jstr, jstring output_jstr) {
    AVOutputFormat *ofmt = NULL;
    AVFormatContext *ifmt_ctx = NULL, *ofmt_ctx = NULL;
    AVPacket pkt;

    int ret, i;
    char input_str[500] = {0};
    char output_str[500] = {0};
    char info[1000] = {0};
    sprintf(input_str, "%s", (*env)->GetStringUTFChars(env, input_jstr, NULL));
    sprintf(output_str, "%s", (*env)->GetStringUTFChars(env, output_jstr, NULL));

    //input_str  = "cuc_ieschool.flv";
    //output_str = "rtmp://localhost/publishlive/livestream";
    //output_str = "rtp://233.233.233.233:6666";

    //FFmpeg av_log() callback
    av_log_set_callback(custom_log);

    av_register_all();
    //Network
    avformat_network_init();

    //Input
    if ((ret = avformat_open_input(&ifmt_ctx, input_str, 0, 0)) < 0) {
        LOGE("Could not open input file.");
        goto end;
    }
    if ((ret = avformat_find_stream_info(ifmt_ctx, 0)) < 0) {
        LOGE("Failed to retrieve input stream information");
        goto end;
    }

    int videoindex = -1;
    for (i = 0; i < ifmt_ctx->nb_streams; i++)
        if (ifmt_ctx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO) {
            videoindex = i;
            break;
        }
    //Output
    avformat_alloc_output_context2(&ofmt_ctx, NULL, "flv", output_str); //RTMP
    //avformat_alloc_output_context2(&ofmt_ctx, NULL, "mpegts", output_str);//UDP

    if (!ofmt_ctx) {
        LOGE("Could not create output context\n");
        ret = AVERROR_UNKNOWN;
        goto end;
    }
    ofmt = ofmt_ctx->oformat;
    for (i = 0; i < ifmt_ctx->nb_streams; i++) {
        //Create output AVStream according to input AVStream
        AVStream *in_stream = ifmt_ctx->streams[i];
        AVStream *out_stream = avformat_new_stream(ofmt_ctx, in_stream->codec->codec);
        if (!out_stream) {
            LOGE("Failed allocating output stream\n");
            ret = AVERROR_UNKNOWN;
            goto end;
        }
        //Copy the settings of AVCodecContext
        ret = avcodec_copy_context(out_stream->codec, in_stream->codec);
        if (ret < 0) {
            LOGE("Failed to copy context from input to output stream codec context\n");
            goto end;
        }
        out_stream->codec->codec_tag = 0;
        if (ofmt_ctx->oformat->flags & AVFMT_GLOBALHEADER)
            out_stream->codec->flags |= CODEC_FLAG_GLOBAL_HEADER;
    }

    //Open output URL
    if (!(ofmt->flags & AVFMT_NOFILE)) {
        ret = avio_open(&ofmt_ctx->pb, output_str, AVIO_FLAG_WRITE);
        if (ret < 0) {
            LOGE("Could not open output URL '%s'", output_str);
            goto end;
        }
    }
    //Write file header
    ret = avformat_write_header(ofmt_ctx, NULL);
    if (ret < 0) {
        LOGE("Error occurred when opening output URL\n");
        goto end;
    }

    int frame_index = 0;

    int64_t start_time = av_gettime();
    while (1) {
        AVStream *in_stream, *out_stream;
        //Get an AVPacket
        ret = av_read_frame(ifmt_ctx, &pkt);
        if (ret < 0)
            break;
        //FIX:No PTS (Example: Raw H.264)
        //Simple Write PTS
        if (pkt.pts == AV_NOPTS_VALUE) {
            //Write PTS
            AVRational time_base1 = ifmt_ctx->streams[videoindex]->time_base;
            //Duration between 2 frames (us)
            int64_t calc_duration =
                    (double) AV_TIME_BASE / av_q2d(ifmt_ctx->streams[videoindex]->r_frame_rate);
            //Parameters
            pkt.pts = (double) (frame_index * calc_duration) /
                      (double) (av_q2d(time_base1) * AV_TIME_BASE);
            pkt.dts = pkt.pts;
            pkt.duration = (double) calc_duration / (double) (av_q2d(time_base1) * AV_TIME_BASE);
        }
        //Important:Delay
        if (pkt.stream_index == videoindex) {
            AVRational time_base = ifmt_ctx->streams[videoindex]->time_base;
            AVRational time_base_q = {1, AV_TIME_BASE};
            int64_t pts_time = av_rescale_q(pkt.dts, time_base, time_base_q);
            int64_t now_time = av_gettime() - start_time;
            if (pts_time > now_time)
                av_usleep(pts_time - now_time);

        }

        in_stream = ifmt_ctx->streams[pkt.stream_index];
        out_stream = ofmt_ctx->streams[pkt.stream_index];
        /* copy packet */
        //Convert PTS/DTS
        pkt.pts = av_rescale_q_rnd(pkt.pts, in_stream->time_base, out_stream->time_base,
                                   AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX);
        pkt.dts = av_rescale_q_rnd(pkt.dts, in_stream->time_base, out_stream->time_base,
                                   AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX);
        pkt.duration = av_rescale_q(pkt.duration, in_stream->time_base, out_stream->time_base);
        pkt.pos = -1;
        //Print to Screen
        if (pkt.stream_index == videoindex) {
            LOGE("Send %8d video frames to output URL\n", frame_index);
            frame_index++;
        }
        //ret = av_write_frame(ofmt_ctx, &pkt);
        ret = av_interleaved_write_frame(ofmt_ctx, &pkt);

        if (ret < 0) {
            LOGE("Error muxing packet\n");
            break;
        }
        av_free_packet(&pkt);

    }
    //Write file trailer
    av_write_trailer(ofmt_ctx);
    end:
    avformat_close_input(&ifmt_ctx);
    /* close output */
    if (ofmt_ctx && !(ofmt->flags & AVFMT_NOFILE))
        avio_close(ofmt_ctx->pb);
    avformat_free_context(ofmt_ctx);
    if (ret < 0 && ret != AVERROR_EOF) {
        LOGE("Error occurred.\n");
        return -1;
    }
    return 0;
}

#ifdef __cplusplus
}
#endif
#endif

配置CmakeLists.txt

这里相当于要添加一个streamer-lib.c生成的so,注意这个so依赖ffmpeg so的配置,相关的配置如下:

add_library( # Sets the name of the library.
             streamer-lib
             # Sets the library as a shared library.
             SHARED
             # Provides a relative path to your source file(s).
             src/main/cpp/streamer-lib.c)

target_link_libraries(
                       streamer-lib
                       libffmpeg
                       ${log-lib})

Java层调用Native方法

我重新新建了一个StreamerActivity来调用实现,其中有一个第一次拷贝res下面的一个视频文件到手机存储目录上面,便于我们调用推流的时候路径的写入:

package com.lyman.ffmpeg_cmake_single;

import android.content.Context;
import android.os.Bundle;
import android.os.Environment;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.ProgressBar;
import android.widget.Toast;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;

public class StreamerActivity extends AppCompatActivity {
    private static final String TAG = "StreamerActivity";
    private ProgressBar mProgressBar;
    private static String mPushStreamPath = "rtmp://192.168.0.101/live/livestream";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_streamer);
        if (!getMoveFile().exists()) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    copyFilesFromRaw(StreamerActivity.this, R.raw.i_am_you,
                            "i_am_you.mp4",
                            getExternalFilesDir(Environment.DIRECTORY_MOVIES).getAbsolutePath());
                }
            }).start();
        }
        mProgressBar = findViewById(R.id.progressBar);
    }

    public void onClickPushStream(View View) {
        if (!getMoveFile().exists()) {
            Toast.makeText(this, "source file not exist", Toast.LENGTH_SHORT).show();
            return;
        }

        mProgressBar.setVisibility(View.VISIBLE);
        new Thread(new Runnable() {
            @Override
            public void run() {
                stream(getMoveFile().getAbsolutePath(), mPushStreamPath);
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        mProgressBar.setVisibility(0x00000008);
                    }
                });
            }
        }).start();
    }

    public native int stream(String inputurl, String outputurl);

    static {
        System.loadLibrary("streamer-lib");
    }


    private File getMoveFile() {
        File rootFile = getExternalFilesDir(Environment.DIRECTORY_MOVIES);
        // Create the storage directory if it does not exist
        if (!rootFile.exists()) {
            if (!rootFile.mkdirs()) {
                Log.d(TAG, "failed to create directory");
                return null;
            }
        }

        String fileName = "i_am_you.mp4";
        String path = rootFile.getAbsolutePath() + File.separator + fileName;
        return new File(path);
    }


    private void copyFilesFromRaw(Context context, int id, String fileName, String storagePath) {
        InputStream inputStream = context.getResources().openRawResource(id);
        File file = new File(storagePath);
        if (!file.exists()) {//如果文件夹不存在,则创建新的文件夹
            file.mkdirs();
        }
        readInputStream(storagePath + File.separator + fileName, inputStream);
    }

    /**
     * 读取输入流中的数据写入输出流
     *
     * @param storagePath 目标文件路径
     * @param inputStream 输入流
     */
    private void readInputStream(String storagePath, InputStream inputStream) {
        File file = new File(storagePath);
        try {
            if (!file.exists()) {
                // 1.建立通道对象
                FileOutputStream fos = new FileOutputStream(file);
                // 2.定义存储空间
                byte[] buffer = new byte[inputStream.available()];
                // 3.开始读文件
                int lenght = 0;
                while ((lenght = inputStream.read(buffer)) != -1) {// 循环从输入流读取buffer字节
                    // 将Buffer中的数据写到outputStream对象中
                    fos.write(buffer, 0, lenght);
                }
                fos.flush();// 刷新缓冲区
                // 4.关闭流
                fos.close();
                inputStream.close();
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

OK,现在build我们的项目,如果没有问题就可以在jniLibs下面看到生成的libstreamer-lib.so文件了。
现在运行项目肯定回看到这个错误:




很明显打不开那个RTMP的地址,这个地址我们到下面在仔细看。我们现在要在电脑上面搭建一个流媒体服务器,这个服务器的种类有很多,例如RED5,srs等,我下面选择的是srs

搭建SRS的RTMP服务器

我使用的系统是Mac(10.13.3),构建通过了。

下载编译SRS

下载SRS源码

srs的github地址为https://github.com/ossrs/srs,但是现在他们已经不在维护更新了,我下面使用的srs的源码是官方推荐的一个fork:https://github.com/smartdu/srs。第一个官网的代码我编译下来会有错误,于是没有用那个了

编译SRS源码

首先看看我们下载的SRS的项目包结构:




cd到trunk目录下面
指向如下命令:

./configure --osx --without-ssl --without-hls --without-hds --without-dvr --without-nginx --without-http-callback --without-http-server --without-stream-caster --without-http-api --without-ffmpeg --without-transcode --without-ingest --without-stat --without-librtmp

make

编译成功以后会生成srs/trunk/objs/srs这个可执行文件

运行SRS服务器

看到别人的博文写的Mac系统下在运行之前要修改一下srs/trunk/conf/src.conf文件里面的并发连接的个数。

max_connections     100;

开启运行srs服务器命令
cd到trunk目录下面:

./etc/init.d/srs start
./etc/init.d/srs stop
./etc/init.d/srs restart

上面的三个指令的意思想必一看就懂。服务器起来以后在终端会看到:




就代表服务器成功起来了
另外还有几个命令可以帮助我们看一些信息:

#跑起来后要留意下log,配置存在错误的话,srs进程会关闭掉
tail -f ./objs/srs.log
#检查下srs进程是否正常运行
ps aux | grep srs

推流和播放视频

上面我们已经把服务器成功起来了就可以进行这里的工作了,在上面我运行代码有一个错误,不能打开。。。。那其中有一个rtmp的地址信息,这个要稍微注意一下,那个地址是我的电脑的地址:




我的rtmp地址为:

rtmp://192.168.0.101/live/livestream

后面加了live/livestream这个具体的意义我没有去看,可以随便命名。把这个地址配置到我们项目的代码的推流地址上。
在VLC上面播放上面的rtmp流地址:





  • 当我们把srs服务器开着的时候,那么VLC就会到一个等待的界面:



这时我们在手机应用上面点击开始推流看看效果:




终于看到了效果QAQ。
但是有一个问题,我的视频文件是四分多钟的,但是我推出去在电脑上面看到的很短,并且AS打印推出的视频也只有几百帧。我想这是代码处理上面有问题,现在不深究了。

项目完整代码:查看

参考链接:
最简单的基于FFmpeg的移动端例子:Android 推流器
Build SRS on Mac

猜你喜欢

转载自blog.csdn.net/lyman_ye/article/details/79486095