美文网首页
《Android音视频系列-6》FFmpeg视频文件推流

《Android音视频系列-6》FFmpeg视频文件推流

作者: 蓝师傅_Android | 来源:发表于2019-08-23 19:54 被阅读0次

    阅读本文需要一点JNI基础~

    通过本文可以学到如下知识:

    • JNI 回调封装
    • 视频推流大概流程

    如果还没搭建直播服务器,看上一篇文章
    搭建直播服务器Nginx+rtmp,如此简单(mac)

    上一篇直播服务器搭建好了,也测试推流拉流都是成功的,这一篇将在手机上将一个mp4文件推流到服务器,然后可以通过拉流软件看直播。

    1、cmake 配置

    # 需要引入我们头文件,以这个配置的目录为基准
    include_directories(src/main/jniLibs/include)
    
    # 添加共享库(so)搜索路径
    LINK_DIRECTORIES(${CMAKE_SOURCE_DIR}/src/main/jniLibs/armeabi)
    
    AUX_SOURCE_DIRECTORY(${CMAKE_SOURCE_DIR}/src/main/cpp/push PUSH_SRC_LIST)
    
    add_library(
            # 编译生成的库的名称叫 push_handle,对应System.loadLibrary("push_handle");
            push_handle
            # Sets the library as a shared library.
            SHARED
            # Provides a relative path to your source file(s).
            ${PUSH_SRC_LIST}
    )
    
    target_link_libraries(
            push_handle
            # 编解码(最重要的库)
            avcodec-57
            # 设备信息
            avdevice-57
            # 滤镜特效处理库
            avfilter-6
            # 封装格式处理库
            avformat-57
            # 工具库(大部分库都需要这个库的支持)
            avutil-55
            # 后期处理
            postproc-54
            # 音频采样数据格式转换库
            swresample-2
            # 视频像素数据格式转换
            swscale-4
            # 链接 android ndk 自带的一些库
            android
            # Links the target library to the log library
            # included in the NDK.
            # 链接 OpenSLES
            OpenSLES
            log)
    
    

    最终生成的so叫 libpush_handle.so

    FFmpeg的so跟include文件如下,跟之前的文章一样。


    2、Java层推流管理类

    public class PushHandle {
    
        static {
            System.loadLibrary("push_handle");
        }
    
        private PushCallback pushCallback;
    
        public void setCallback(PushCallback pushCallback) {
            this.pushCallback = pushCallback;
        }
    
    
        public native int nPushRtmpFile(String filePath, String rtmp_url);
    
        public native int nStopPush();
    
    
        /**native回调*/
        private void onInfo(long pts, long dts, long duration, long index) {
            if (pushCallback != null) {
                pushCallback.onInfo(pts, dts, duration, index);
            }
        }
    
        private void onError(int code, String msg) {
            if (pushCallback != null) {
                pushCallback.onError(code, msg);
            }
        }
    
        private void onPushComplete() {
            if (pushCallback != null) {
                pushCallback.onPushComplete();
            }
        }
    
    }
    

    public native int nPushRtmpFile(String filePath, String rtmp_url); 推流方法就传一个视频文件路径和推流地址,然后就等回调

    回调比较简单

    public interface PushCallback {
    
        //回调推流每一帧信息
        void onInfo(long pts, long dts, long duration, long index);
    
        //错误回调
        void onError(int code, String msg);
    
        //推流完成
        void onPushComplete();
    }
    

    3、Native层

    在/src/main/cpp/push目录下定义一个 PushHandle.cpp的c++文件,
    然后同步一下,鼠标依次放在PushHandle.java 的 两个native方法上,按 option+enter(win 是alt+enter),自动生成对应的native方法实现,然后就开始写c++代码了。

    3.1 JNI回调封装

    在当前文件目录新建一个c++ file,名字就叫 PushJniCall,
    然后会生成对应的PushJniCall.h头文件和PushJniCall.cpp文件

    PushJniCall.h
    #ifndef FFMPEGDEMO_PUSHJNICALL_H
    #define FFMPEGDEMO_PUSHJNICALL_H
    
    #include <jni.h>
    
    class PushJniCall {
    public:
        JNIEnv *jniEnv;
    
        //定义java的对象和方法id,在cpp构造函数赋值
        jobject jPushCallbackObj;
        jmethodID jOnErrorMid;
        jmethodID jOnInfoMid;
        jmethodID jOnPushCompleteMid;
    
    public:
        PushJniCall(JNIEnv *jniEnv,jobject jPushCallbackObj);
        ~PushJniCall();
    
    public:
    
        //定义回调方法,在cpp实现
        void callOnError(int code, char *msg);
    
        void callOnPushComplete();
    
        void callOnInfo(int64_t pts, int64_t dts, int64_t duration, long long index);
    };
    
    #endif //FFMPEGDEMO_PUSHJNICALL_H
    
    实现类 PushJniCall.cpp
    #include "PushJniCall.h"
    
    PushJniCall::PushJniCall(JNIEnv *jniEnv, jobject jPushCallbackObj) {
    
        this->jniEnv = jniEnv;
        //需要创建一个全局应用
        this->jPushCallbackObj = jniEnv->NewGlobalRef(jPushCallbackObj);
        jclass cls = jniEnv->GetObjectClass(jPushCallbackObj);
    
        //获取方法id
        jOnInfoMid = jniEnv->GetMethodID(cls, "onInfo", "(JJJJ)V");
        jOnErrorMid = jniEnv->GetMethodID(cls, "onError", "(ILjava/lang/String;)V");
        jOnPushCompleteMid = jniEnv->GetMethodID(cls, "onPushComplete", "()V");
    
    }
    
    PushJniCall::~PushJniCall() {
        jniEnv->DeleteGlobalRef(jPushCallbackObj);
    }
    
    void PushJniCall::callOnError(int code, char *msg) {
        //C中的char*转化为JNI中的jstring
        jniEnv->CallVoidMethod(jPushCallbackObj, jOnErrorMid, code, msg);
    
    }
    
    void PushJniCall::callOnPushComplete() {
        jniEnv->CallVoidMethod(jPushCallbackObj, jOnPushCompleteMid);
    
    }
    
    void PushJniCall::callOnInfo(int64_t pts, int64_t dts, int64_t duration, long long index) {
        jniEnv->CallVoidMethod(jPushCallbackObj, jOnInfoMid, (jlong) pts, (jlong) dts,
                               (jlong) duration, (jlong) index);
    
    }
    

    这些属于JNI基础,应该能看懂,看不懂就去找一篇JNI基础文章看下就行。
    [传送门]

    3.2 推流核心代码

    PushHandle.cpp

    推流代码参考文末链接,加了一些注释

    #include <jni.h>
    #include "PushJniCall.h"
    #include "PushStatus.h"
    #include "log.h"
    
    //ffmpeg 是c写的,要用c的include
    extern "C" {
    #include "libavcodec/avcodec.h"
    #include "libavformat/avformat.h"
    //引入时间
    #include "libavutil/time.h"
    };
    
    #include <iostream>
    
    using namespace std;
    
    //JNI回调处理
    PushJniCall *pushJniCall;
    //状态处理
    PushStatus *pushStatus;
    
    int avError(int errNum) {
        char buf[1024];
        //获取错误信息
        av_strerror(errNum, buf, sizeof(buf));
    
        LOGE("发生异常:%s",buf);
        if (pushJniCall != NULL) {
            pushJniCall->callOnError(errNum, buf);
        }
        return errNum;
    }
    
    
    extern "C"
    JNIEXPORT jint JNICALL
    Java_com_lanshifu_ffmpegdemo_push_PushHandle_nPushRtmpFile(JNIEnv *env, jobject instance,
                                                               jstring path_, jstring rtmp_url) {
    
        pushJniCall = new PushJniCall(env, instance);
        pushStatus = new PushStatus();
    
        const char *inUrl = env->GetStringUTFChars(path_, 0);
        const char *outUrl = env->GetStringUTFChars(rtmp_url, 0);
    
        LOGW("nPushRtmpFile,inUrl = %s", inUrl);
        LOGW("nPushRtmpFile,outUrl = %s", outUrl);
    
        int videoindex = -1;
        /// 1.使用FFmpeg之前要调用av_register_all和avformat_network_init
        //初始化所有的封装和解封装 flv mp4 mp3 mov。不包含编码和解码
        av_register_all();
        //初始化网络库
        avformat_network_init();
    
    
        //////////////////////////////////////////////////////////////////
        //                   输入流处理部分
        /////////////////////////////////////////////////////////////////
    
    
        //输入封装的上下文。包含所有的格式内容和所有的IO。如果是文件就是文件IO,网络就对应网络IO
        AVFormatContext *ictx = NULL;
        AVFormatContext *octx = NULL;
    
        AVPacket pkt;
        int ret = 0;
    
        try {
            ///2.打开文件,解封文件头
            ret = avformat_open_input(&ictx, inUrl, 0, NULL);
            if (ret < 0) {
                avError(ret);
                throw ret;
            }
            LOGD("avformat_open_input success!");
            ///3.获取音频视频的信息 .h264 flv 没有头信息
            ret = avformat_find_stream_info(ictx, 0);
            if (ret != 0) {
                avError(ret);
                throw ret;
            }
            LOGD("avformat_find_stream_info success!");
    
            av_dump_format(ictx, 0, inUrl, 0);
    
            //////////////////////////////////////////////////////////////////
            //                   输出流处理部分
            /////////////////////////////////////////////////////////////////
    
            /// 4.创建输出上下文,如果是输入文件 flv可以不传,可以从文件中判断。如果是流则必须传
            ret = avformat_alloc_output_context2(&octx, NULL, "flv", outUrl);
            if (ret < 0) {
                avError(ret);
                throw ret;
            }
            LOGD("avformat_alloc_output_context2 success!");
    
            ///遍历输入流列表,一般有音频流和视频流,为输出内容添加音视频流
            for (int i = 0; i < ictx->nb_streams; i++) {
                //获取输入视频流,可能是音频流,视频流?
                AVStream *in_stream = ictx->streams[i];
                ///为输出内容添加一个音视频流,格式什么的跟输入流保持一致。
                AVStream *out_stream = avformat_new_stream(octx, in_stream->codec->codec);
                LOGD("avformat_new_stream %d, success!", i);
                if (!out_stream) {
                    LOGE("未能成功添加音视频流 %d", i);
                    ret = AVERROR_UNKNOWN;
                    throw ret;
                }
                //这个不知道有什么用,注释掉吧,不影响结果
    //            if (octx->oformat->flags & AVFMT_GLOBALHEADER) {
    //                out_stream->codec->flags |= CODEC_FLAG_GLOBAL_HEADER;
    //            }
                /// 输入流参数拷贝到输出流
                ret = avcodec_parameters_copy(out_stream->codecpar, in_stream->codecpar);
                if (ret < 0) {
                    LOGD("copy 编解码器上下文失败\n");
                }
                out_stream->codecpar->codec_tag = 0;
    
                ///记录视频流的位置
                if (videoindex == -1 && ictx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
                    videoindex = i;
                    LOGD("找到视频流的位置 %d,", videoindex);
                }
            }
    
    
            //打印输出的格式信息
            av_dump_format(octx, 0, outUrl, 1);
            LOGD("准备推流...");
    
            //////////////////////////////////////////////////////////////////
            //                   准备推流
            /////////////////////////////////////////////////////////////////
    
            ///打开IO
            ret = avio_open(&octx->pb, outUrl, AVIO_FLAG_READ_WRITE);
            // todo :Linux服务器地址报错 IO error,本地服务器不会
            if (ret < 0) {
                avError(ret);
                throw ret;
            }
            LOGD("打开IO avio_open success!");
            ///写入头部信息
            ret = avformat_write_header(octx, 0);
            if (ret < 0) {
                avError(ret);
                throw ret;
            }
            LOGD("写入头部信息 avformat_write_header Success!");
            //推流每一帧数据
            //int64_t pts  [ pts*(num/den)  第几秒显示]
            //int64_t dts  解码时间 [P帧(相对于上一帧的变化) I帧(关键帧,完整的数据) B帧(上一帧和下一帧的变化)]  有了B帧压缩率更高。
            //获取当前的时间戳  微妙
            long long start_time = av_gettime();
            long long frame_index = 0;
            LOGD("开始推流 >>>>>>>>>>>>>>>");
            while (!pushStatus->isExit) {
                //输入输出视频流
                AVStream *in_stream, *out_stream;
                ///不断读取每一帧数据
                ret = av_read_frame(ictx, &pkt);
                if (ret < 0) {
                    //数据读完,播放完成
                    break;
                }
    
                /*
                PTS(Presentation Time Stamp)显示播放时间
                DTS(Decoding Time Stamp)解码时间
                */
                //没有显示时间(比如未解码的 H.264 )
                if (pkt.pts == AV_NOPTS_VALUE) {
                    //AVRational time_base:时基。通过该值可以把PTS,DTS转化为真正的时间。
                    AVRational time_base1 = ictx->streams[videoindex]->time_base;
    
                    //计算两帧之间的时间
                    /*
                    r_frame_rate 基流帧速率  (不是太懂,先知道流程就行)
                    av_q2d 转化为double类型
                    */
                    int64_t calc_duration = (double) AV_TIME_BASE / av_q2d(ictx->streams[videoindex]->r_frame_rate);
    
                    //配置参数
                    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);
                }
    
                ///通过睡眠的方式保持播放时间同步
                if (pkt.stream_index == videoindex) {
                    AVRational time_base = ictx->streams[videoindex]->time_base;
                    AVRational time_base_q = {1, AV_TIME_BASE};
                    //计算视频播放时间,比如在11s播放
                    int64_t pts_time = av_rescale_q(pkt.dts, time_base, time_base_q);
                    //计算实际视频的播放时间,比如已经播放了10s
                    int64_t now_time = av_gettime() - start_time;
                    if (pts_time > now_time) {
                        //睡眠一段时间(目的是让当前视频记录的播放时间与实际时间同步)
                        av_usleep((unsigned int) (pts_time - now_time));
                    }
                }
    
                //输入输出视频流赋值
                in_stream = ictx->streams[pkt.stream_index];
                out_stream = octx->streams[pkt.stream_index];
    
                //计算延时后,重新指定时间戳,调函数即可
                pkt.pts = av_rescale_q_rnd(pkt.pts, in_stream->time_base, out_stream->time_base,
                        (AVRounding) (AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
                pkt.dts = av_rescale_q_rnd(pkt.dts, in_stream->time_base, out_stream->time_base,
                        (AVRounding) (AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
                pkt.duration = (int) av_rescale_q(pkt.duration, in_stream->time_base,out_stream->time_base);
                //字节流的位置,-1 表示不知道字节流位置
                pkt.pos = -1;
    
                if (pkt.stream_index == videoindex) {
                    //当前视频帧数
                    frame_index++;
                }
                //回调数据
                if (pushJniCall != NULL) {
                    pushJniCall->callOnInfo(pkt.pts, pkt.dts, pkt.duration, frame_index);
                }
    
                ///把一帧数据写到输出流(推流)
                ret = av_interleaved_write_frame(octx, &pkt);
    
                if (ret < 0) {
                    LOGE("推流失败 ret=%d",ret);
                    break;
                }
    
            }
            ret = 0;
        } catch (int errNum) {
            if (pushJniCall != NULL) {
                pushJniCall->callOnError(errNum, const_cast<char *>("出错了"));
            }
        }
    
        if (pushJniCall != NULL) {
            pushJniCall->callOnPushComplete();
        }
    
        LOGD("推流结束》》》");
        //关闭资源
        if (octx != NULL){
            avio_close(octx->pb);
            octx = NULL;
        }
        //释放输出封装上下文
        if (octx != NULL) {
            avformat_free_context(octx);
        }
        //关闭输入上下文
        if (ictx != NULL) {
            avformat_close_input(&ictx);
            ictx = NULL;
        }
        //释放
        av_packet_unref(&pkt);
    
        env->ReleaseStringUTFChars(path_, outUrl);
        return ret;
    }
    
    extern "C"
    JNIEXPORT jint JNICALL
    Java_com_lanshifu_ffmpegdemo_push_PushHandle_nStopPush(JNIEnv *env, jobject instance) {
        LOGD("nStopPush");
        if (pushStatus != NULL){
            pushStatus->isExit = true;
        }
    }
    

    其中,日志封装了一个 log.h,如下

    #ifndef _LOG_H
    #define _LOG_H
    
    #define LOGN (void) 0
    
    #ifndef WIN32
    #include <android/log.h>
    
    #define LOG_VERBOSE     1
    #define LOG_DEBUG       2
    #define LOG_INFO        3
    #define LOG_WARNING     4
    #define LOG_ERROR       5
    #define LOG_FATAL       6
    #define LOG_SILENT      7
    
    #ifndef LOG_TAG
    //#define LOG_TAG __FILE__
    #define LOG_TAG "JNI_TAG"
    #endif
    
    #ifndef LOG_LEVEL
    #define LOG_LEVEL LOG_VERBOSE
    #endif
    
    #define LOGP(level, fmt, ...) \
            __android_log_print(level, LOG_TAG, "%s:" fmt, \
                __PRETTY_FUNCTION__, ##__VA_ARGS__)
    
    #if LOG_VERBOSE >= LOG_LEVEL
    #define LOGV(fmt, ...) \
            LOGP(ANDROID_LOG_VERBOSE, fmt, ##__VA_ARGS__)
    #else
    #define LOGV(...) LOGN
    #endif
    
    #if LOG_DEBUG >= LOG_LEVEL
    #define LOGD(fmt, ...) \
            LOGP(ANDROID_LOG_DEBUG, fmt, ##__VA_ARGS__)
    #else
    #define LOGD(...) LOGN
    #endif
    
    #if LOG_INFO >= LOG_LEVEL
    #define LOGI(fmt, ...) \
            LOGP(ANDROID_LOG_INFO, fmt, ##__VA_ARGS__)
    #else
    #define LOGI(...) LOGN
    #endif
    
    #if LOG_WARNING >= LOG_LEVEL
    #define LOGW(fmt, ...) \
            LOGP(ANDROID_LOG_WARN, fmt, ##__VA_ARGS__)
    #else
    #define LOGW(...) LOGN
    #endif
    
    #if LOG_ERROR >= LOG_LEVEL
    #define LOGE(fmt, ...) \
            LOGP(ANDROID_LOG_ERROR, fmt, ##__VA_ARGS__)
    #else
    #define LOGE(...) LOGN
    #endif
    
    #if LOG_FATAL >= LOG_LEVEL
    #define LOGF(fmt, ...) \
            LOGP(ANDROID_LOG_FATAL, fmt, ##__VA_ARGS__)
    #else
    #define LOGF(...) LOGN
    #endif
    
    #if LOG_FATAL >= LOG_LEVEL
    #define LOGA(condition, fmt, ...) \
        if (!(condition)) \
        { \
            __android_log_assert(condition, LOG_TAG, "(%s:%u) %s: error:%s " fmt, \
                __FILE__, __LINE__, __PRETTY_FUNCTION__, condition, ##__VA_ARGS__); \
        }
    #else
    #define LOGA(...) LOGN
    #endif
    
    #else
    #include <stdio.h>
    
    #define LOGP(fmt, ...) printf("%s line:%d " fmt, __FILE__, __LINE__, ##__VA_ARGS__)
    
    #define LOGV(fmt, ...) LOGP(fmt, ##__VA_ARGS__)
    
    #define LOGD(fmt, ...) LOGP(fmt, ##__VA_ARGS__)
    
    #define LOGI(fmt, ...) LOGP(fmt, ##__VA_ARGS__)
    
    #define LOGW(fmt, ...) LOGP(fmt, ##__VA_ARGS__)
    
    #define LOGE(fmt, ...) LOGP(fmt, ##__VA_ARGS__)
    
    #define LOGF(fmt, ...) LOGP(fmt, ##__VA_ARGS__)
    
    #define LOGA(...) LOGN
    
    #endif // ANDROID_PROJECT
    
    #endif // _LOG_H
    

    PushStatus 主要是管理状态,比如退出的时候要取消推流,将标志位改成true

    PushStatus.h

    #ifndef FFMPEGDEMO_PLAYERSTATUS_H
    #define FFMPEGDEMO_PLAYERSTATUS_H
    
    class PushStatus {
    public:
        /**
         * 是否退出,打算用这个变量来做退出(销毁)
         */
        bool isExit = false;
    
    };
    
    #endif //FFMPEGDEMO_PLAYERSTATUS_H
    

    PushStatus.cpp

    #include "PushStatus.h"
    

    最终Activity中调用

    点击推流按钮开个子线程执行

        private void filePushRunable() {
            mPushHandle = new PushHandle();
            mPushHandle.setCallback(new PushCallback() {
                @Override
                public void onInfo(final long pts, final long dts, final long duration, final long index) {
                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            StringBuilder sb = new StringBuilder();
                            sb.append("pts: ").append(pts).append("\n");
                            sb.append("dts: ").append(dts).append("\n");
                            sb.append("duration: ").append(duration).append("\n");
                            sb.append("index: ").append(index).append("\n");
                            tvPushInfo.setText(sb.toString());
                        }
                    });
    
                }
    
                @Override
                public void onError(final int code, final String msg) {
                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            tvPushInfo.setText("失败,code=" + code + ",msg=" + msg);
                        }
                    });
                }
    
                @Override
                public void onPushComplete() {
                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            tvPushInfo.setText("推流完成");
                        }
                    });
    
                }
            });
    
            String videoPath = "/sdcard/input1.mp4";
            mPushHandle.nPushRtmpFile(videoPath, "rtmp://192.168.43.144:1935/test/live");
        }
    

    打印日志


    可以看到推流是成功的,拉流试试

    嗯,是成功的,2019 AG超玩会回归了,中单老帅认识吧,哈哈。


    总结一下推流的大概流程:

    • 打开输入文件,解封文件头,avformat_open_input
    • 获取输入音视频文件的信息 avformat_find_stream_info
    • 创建输出流上下文 avformat_alloc_output_context2
    • 添加音视频流 avformat_new_stream
    • 输入流参数拷贝到输出流 avcodec_parameters_copy
    • 打开推流地址 avio_open
    • 写入头部信息 avformat_write_header
    • 不断读取输入视频文件的每一帧 av_read_frame
    • 没有显示时间的帧添加时间
    • 同步视频记录的时间和实际播放时间
    • 更正时间戳
    • 推流 av_interleaved_write_frame

    代码中可能有些不太好理解,没关系,只要这个流程清楚了就行,后面文章再深入一点理解FFmpeg。

    下一篇介绍摄像头推流,可以结合OpenGL,添加滤镜和水印,应该会比较有意思。


    参考
    https://www.jianshu.com/p/dcac5da8f1da

    相关文章

      网友评论

          本文标题:《Android音视频系列-6》FFmpeg视频文件推流

          本文链接:https://www.haomeiwen.com/subject/vahrsctx.html