美文网首页android音视频移动流媒体andriod
Android平台下使用FFmpeg进行RTMP推流(视频文件推

Android平台下使用FFmpeg进行RTMP推流(视频文件推

作者: 第八区 | 来源:发表于2017-11-02 16:50 被阅读2467次

    简介

    前面已经讲到如何在Linux环境下编译FFmpeg以及在Android项目中使用,这一节就开始真正的使用FFmpeg。在Android平台下用FFmepg解析视频文件并进行RTMP推流。如果对FFmpeg基础不熟或者不知道如何在Android项目中使用,请先阅读流媒体专栏里之前的文章。
    注意:这里的工程沿用Linux下FFmpeg编译以及Android平台下使用里的工程和结构。

    • 新增推流函数
    • 异常处理
    • 设置回调方法
    • 常见问题
    • 源码

    新增推流函数

    首先我们将所有FFmpeg的操作抽取到一个类里面,然后增加推流方法。

    public class FFmpegHandle {
        private static FFmpegHandle mInstance;
    
        public static void init(Context context) {
            mInstance = new FFmpegHandle();
        }
    
        public static FFmpegHandle getInstance() {
            if (mInstance == null) {
                throw new RuntimeException("FFmpegHandle must init fist");
            }
            return mInstance;
        }
    
        // Used to load the 'native-lib' library on application startup.
        static {
            System.loadLibrary("avutil-55");
            System.loadLibrary("swresample-2");
            System.loadLibrary("avcodec-57");
            System.loadLibrary("avformat-57");
            System.loadLibrary("swscale-4");
            System.loadLibrary("avfilter-6");
            System.loadLibrary("avdevice-57");
            System.loadLibrary("ffmpeg-handle");
        }
        public native int setCallback(PushCallback pushCallback);
    
        public native String getAvcodecConfiguration();
    
        public native int pushRtmpFile(String path);
    }
    

    我们先看到public native int pushRtmpFile(String path);方法,这里主要传入的参数是文件的路径。然后在cpp层的代码中也增加方法

    JNIEXPORT jint JNICALL
    Java_com_wangheart_rtmpfile_ffmpeg_FFmpegHandle_pushRtmpFile(JNIEnv *env, jobject instance,
                                                                 jstring path_) {
    ...省略代码
    }
    

    接下来就到了cpp层的开发,基本上和基于FFmpeg进行RTMP推流(二)中使用的代码一致,我们直接拷贝过来即可。至于FFmpeg的使用,这里就不重复讲了,不懂的可以看之前的文章。源码见末尾

    异常处理

    在我们之前的推流代码中,并没有做异常处理。这样在正式的使用中肯定不太好的。所以我们加上try catch。统一进行资源释放。源码见末尾

    设置回调方法

    为了方便我们查看推流的信息,我们新增一个回调类。

    • FFmpegHandle增加本地调用方法
    public native int setCallback(PushCallback pushCallback);
    
    • 同样cpp层也需要增加对应函数
    /**
     * 设置回到对象
     */
    extern "C"
    JNIEXPORT jint JNICALL
    Java_com_wangheart_rtmpfile_ffmpeg_FFmpegHandle_setCallback(JNIEnv *env, jobject instance,
                                                                jobject pushCallback1) {
        //转换为全局变量
        pushCallback = env->NewGlobalRef(pushCallback1);
        if (pushCallback == NULL) {
            return -3;
        }
        cls = env->GetObjectClass(pushCallback);
        if (cls == NULL) {
            return -1;
        }
        mid = env->GetMethodID(cls, "videoCallback", "(JJJJ)V");
        if (mid == NULL) {
            return -2;
        }
        env->CallVoidMethod(pushCallback, mid, (jlong) 0, (jlong) 0, (jlong) 0, (jlong) 0);
        return 0;
    }
    

    这里有个重点,就是对于传递进来的对象jobject pushCallback1是局部变量。而我们需要在推流的时候使用到这个对象,所以需要转化成全局变量

    pushCallback = env->NewGlobalRef(pushCallback1);
    

    同样也需要定义对应的全局变量

    jobject pushCallback = NULL;
    jclass cls = NULL;
    jmethodID mid = NULL;
    

    GetObjectClass、GetMethodID、CallVoidMethod这几个方法看名称也知道其功能。我们在设置回到对象时候就讲方法获取出来,后面就不需要每次去查找。

        cls = env->GetObjectClass(pushCallback);
        if (cls == NULL) {
            return -1;
        }
        mid = env->GetMethodID(cls, "videoCallback", "(JJJJ)V");
        if (mid == NULL) {
            return -2;
        }
    
    • 定义一个本地回调方法
    int callback(JNIEnv *env, int64_t pts, int64_t dts, int64_t duration, long long index) {
    //    logw("=================")
        if (pushCallback == NULL) {
            return -3;
        }
        if (cls == NULL) {
            return -1;
        }
        if (mid == NULL) {
            return -2;
        }
        env->CallVoidMethod(pushCallback, mid, (jlong) pts, (jlong) dts, (jlong) duration,
                            (jlong) index);
        return 0;
    }
    

    这样我们在推流的过程中就可以调用callback函数,将数据回调到java层。

    //回调数据
    callback(env, pkt.pts, pkt.dts, pkt.duration, frame_index);
    
    • java层设置回调对象
            int res = FFmpegHandle.getInstance().setCallback(new PushCallback() {
                @Override
                public void videoCallback(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());
                        }
                    });
                }
            });
    

    常见问题

    第二次推流出现Operation not permitted
    这是因为第一次推流完没有关闭输出上下文

        //关闭输出上下文,这个很关键。Linux下FFmpeg编译以及Android平台下使用
        if (octx != NULL)
            avio_close(octx->pb);
    

    加上即可。

    源码

    Github源码地址注意下载对应的版本

    9.png

    FFmpegHandle.java

    public class FFmpegHandle {
        private static FFmpegHandle mInstance;
    
        public static void init(Context context) {
            mInstance = new FFmpegHandle();
        }
    
        public static FFmpegHandle getInstance() {
            if (mInstance == null) {
                throw new RuntimeException("FFmpegHandle must init fist");
            }
            return mInstance;
        }
    
        // Used to load the 'native-lib' library on application startup.
        static {
            System.loadLibrary("avutil-55");
            System.loadLibrary("swresample-2");
            System.loadLibrary("avcodec-57");
            System.loadLibrary("avformat-57");
            System.loadLibrary("swscale-4");
            System.loadLibrary("avfilter-6");
            System.loadLibrary("avdevice-57");
            System.loadLibrary("ffmpeg-handle");
        }
        public native int setCallback(PushCallback pushCallback);
    
        public native String getAvcodecConfiguration();
    
        public native int pushRtmpFile(String path);
    }
    

    MainActivity.java

    package com.wangheart.rtmpfile;
    
    import android.app.Activity;
    import android.os.Bundle;
    import android.os.Environment;
    import android.util.Log;
    import android.view.View;
    import android.widget.TextView;
    
    import com.wangheart.rtmpfile.ffmpeg.FFmpegHandle;
    import com.wangheart.rtmpfile.ffmpeg.PushCallback;
    
    import java.io.File;
    
    public class MainActivity extends Activity {
        private TextView tvCodecInfo;
        private TextView tvPushInfo;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            initView();
            initData();
        }
    
        private void initView() {
            tvCodecInfo = findViewById(R.id.tv_codec_info);
            tvPushInfo = findViewById(R.id.tv_push_info);
        }
    
    
        private void initData() {
            FFmpegHandle.init(this);
            tvCodecInfo.setText(FFmpegHandle.getInstance().getAvcodecConfiguration());
            int res = FFmpegHandle.getInstance().setCallback(new PushCallback() {
                @Override
                public void videoCallback(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());
                        }
                    });
                }
            });
            log("result " + res);
        }
    
        public void btnPush(View view) {
            final String path = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "sample.flv";
            File file = new File(path);
            log(path + "  " + file.exists());
            new Thread() {
                @Override
                public void run() {
                    super.run();
                    int result = FFmpegHandle.getInstance().pushRtmpFile(path);
                    log("result " + result);
                }
            }.start();
        }
    
    
        public void log(String content) {
            Log.w("eric", content);
        }
    }
    

    ffmpeg_handle.cpp

    //
    // Created by eric on 2017/11/1.
    //
    #include <jni.h>
    #include <string>
    #include<android/log.h>
    #include <exception>
    
    //定义日志宏变量
    #define logw(content)   __android_log_write(ANDROID_LOG_WARN,"eric",content)
    #define loge(content)   __android_log_write(ANDROID_LOG_ERROR,"eric",content)
    #define logd(content)   __android_log_write(ANDROID_LOG_DEBUG,"eric",content)
    
    extern "C" {
    #include "libavcodec/avcodec.h"
    #include "libavformat/avformat.h"
    //引入时间
    #include "libavutil/time.h"
    }
    
    #include <iostream>
    
    using namespace std;
    
    jobject pushCallback = NULL;
    jclass cls = NULL;
    jmethodID mid = NULL;
    
    int callback(JNIEnv *env, int64_t pts, int64_t dts, int64_t duration, long long index) {
    //    logw("=================")
        if (pushCallback == NULL) {
            return -3;
        }
        if (cls == NULL) {
            return -1;
        }
        if (mid == NULL) {
            return -2;
        }
        env->CallVoidMethod(pushCallback, mid, (jlong) pts, (jlong) dts, (jlong) duration,
                            (jlong) index);
        return 0;
    }
    
    int avError(int errNum) {
        char buf[1024];
        //获取错误信息
        av_strerror(errNum, buf, sizeof(buf));
        loge(string().append("发生异常:").append(buf).c_str());
        return -1;
    }
    
    //获取FFmpeg相关信息
    extern "C"
    JNIEXPORT jstring JNICALL
    Java_com_wangheart_rtmpfile_ffmpeg_FFmpegHandle_getAvcodecConfiguration(JNIEnv *env,
                                                                            jobject instance) {
        char info[10000] = {0};
        sprintf(info, "%s\n", avcodec_configuration());
        return env->NewStringUTF(info);
    }
    
    /**
     * 设置回到对象
     */
    extern "C"
    JNIEXPORT jint JNICALL
    Java_com_wangheart_rtmpfile_ffmpeg_FFmpegHandle_setCallback(JNIEnv *env, jobject instance,
                                                                jobject pushCallback1) {
        //转换为全局变量
        pushCallback = env->NewGlobalRef(pushCallback1);
        if (pushCallback == NULL) {
            return -3;
        }
        cls = env->GetObjectClass(pushCallback);
        if (cls == NULL) {
            return -1;
        }
        mid = env->GetMethodID(cls, "videoCallback", "(JJJJ)V");
        if (mid == NULL) {
            return -2;
        }
        env->CallVoidMethod(pushCallback, mid, (jlong) 0, (jlong) 0, (jlong) 0, (jlong) 0);
        return 0;
    }
    
    extern "C"
    JNIEXPORT jint JNICALL
    Java_com_wangheart_rtmpfile_ffmpeg_FFmpegHandle_pushRtmpFile(JNIEnv *env, jobject instance,
                                                                 jstring path_) {
        const char *path = env->GetStringUTFChars(path_, 0);
        logw(path);
        int videoindex = -1;
        //所有代码执行之前要调用av_register_all和avformat_network_init
        //初始化所有的封装和解封装 flv mp4 mp3 mov。不包含编码和解码
        av_register_all();
    
        //初始化网络库
        avformat_network_init();
    
        const char *inUrl = path;
        //输出的地址
        const char *outUrl = "rtmp://192.168.31.127/live";
    
        //////////////////////////////////////////////////////////////////
        //                   输入流处理部分
        /////////////////////////////////////////////////////////////////
        //打开文件,解封装 avformat_open_input
        //AVFormatContext **ps  输入封装的上下文。包含所有的格式内容和所有的IO。如果是文件就是文件IO,网络就对应网络IO
        //const char *url  路径
        //AVInputFormt * fmt 封装器
        //AVDictionary ** options 参数设置
        AVFormatContext *ictx = NULL;
    
        AVFormatContext *octx = NULL;
    
        AVPacket pkt;
        int ret = 0;
        try {
            //打开文件,解封文件头
            ret = avformat_open_input(&ictx, inUrl, 0, NULL);
            if (ret < 0) {
                avError(ret);
                throw ret;
            }
            logd("avformat_open_input success!");
            //获取音频视频的信息 .h264 flv 没有头信息
            ret = avformat_find_stream_info(ictx, 0);
            if (ret != 0) {
                avError(ret);
                throw ret;
            }
            //打印视频视频信息
            //0打印所有  inUrl 打印时候显示,
            av_dump_format(ictx, 0, inUrl, 0);
    
            //////////////////////////////////////////////////////////////////
            //                   输出流处理部分
            /////////////////////////////////////////////////////////////////
            //如果是输入文件 flv可以不传,可以从文件中判断。如果是流则必须传
            //创建输出上下文
            ret = avformat_alloc_output_context2(&octx, NULL, "flv", outUrl);
            if (ret < 0) {
                avError(ret);
                throw ret;
            }
            logd("avformat_alloc_output_context2 success!");
    
            int i;
    
            for (i = 0; i < ictx->nb_streams; i++) {
    
                //获取输入视频流
                AVStream *in_stream = ictx->streams[i];
                //为输出上下文添加音视频流(初始化一个音视频流容器)
                AVStream *out_stream = avformat_new_stream(octx, in_stream->codec->codec);
                if (!out_stream) {
                    printf("未能成功添加音视频流\n");
                    ret = AVERROR_UNKNOWN;
                }
                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) {
                    printf("copy 编解码器上下文失败\n");
                }
                out_stream->codecpar->codec_tag = 0;
    //        out_stream->codec->codec_tag = 0;
            }
    
            //找到视频流的位置
            for (i = 0; i < ictx->nb_streams; i++) {
                if (ictx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO) {
                    videoindex = i;
                    break;
                }
            }
    
            av_dump_format(octx, 0, outUrl, 1);
            //////////////////////////////////////////////////////////////////
            //                   准备推流
            /////////////////////////////////////////////////////////////////
    
            //打开IO
            ret = avio_open(&octx->pb, outUrl, AVIO_FLAG_WRITE);
            if (ret < 0) {
                avError(ret);
                throw ret;
            }
            logd("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("start push >>>>>>>>>>>>>>>");
            while (1) {
                //输入输出视频流
                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};
                    //计算视频播放时间
                    int64_t pts_time = av_rescale_q(pkt.dts, time_base, time_base_q);
                    //计算实际视频的播放时间
                    int64_t now_time = av_gettime() - start_time;
    
                    AVRational avr = ictx->streams[videoindex]->time_base;
                    cout << avr.num << " " << avr.den << "  " << pkt.dts << "  " << pkt.pts << "   "
                         << pts_time << endl;
                    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);
    //        __android_log_print(ANDROID_LOG_WARN, "eric", "duration %d", pkt.duration);
                //字节流的位置,-1 表示不知道字节流位置
                pkt.pos = -1;
    
                if (pkt.stream_index == videoindex) {
                    printf("Send %8d video frames to output URL\n", frame_index);
                    frame_index++;
                }
                //回调数据
                callback(env, pkt.pts, pkt.dts, pkt.duration, frame_index);
                //向输出上下文发送(向地址推送)
                ret = av_interleaved_write_frame(octx, &pkt);
    
                if (ret < 0) {
                    printf("发送数据包出错\n");
                    break;
                }
                //释放
                av_packet_unref(&pkt);
            }
            ret = 0;
        } catch (int errNum) {
        }
        logd("finish===============");
        //关闭输出上下文,这个很关键。
        if (octx != NULL)
            avio_close(octx->pb);
        //释放输出封装上下文
        if (octx != NULL)
            avformat_free_context(octx);
        //关闭输入上下文
        if (ictx != NULL)
            avformat_close_input(&ictx);
        octx = NULL;
        ictx = NULL;
        env->ReleaseStringUTFChars(path_, path);
        return ret;
    }
    

    相关文章

      网友评论

        本文标题:Android平台下使用FFmpeg进行RTMP推流(视频文件推

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