• Android 音视频深入 十五 FFmpeg 推流mp4文件(附源码下载)


    源码地址
    https://github.com/979451341/Rtmp

    1.配置RTMP服务器

    这个我不多说贴两个博客分别是在mac和windows环境上的,大家跟着弄

    MAC搭建RTMP服务器
    https://www.jianshu.com/p/6fcec3b9d644
    这个是在windows上的,RTMP服务器搭建(crtmpserver和nginx)

    https://www.jianshu.com/p/c71cc39f72ec

    2.关于推流输出的ip地址我好好说说

    我这里是手机开启热点,电脑连接手机,这个RTMP服务器的推流地址有localhost,服务器在电脑上,对于电脑这个localhost是127.0.0.1,但是对于外界比如手机,你不能用localhost,而是用这个电脑的在这个热点也就是局域网的ip地址,不是127.0.0.1这个只代表本设备节点的ip地址,这个你需要去手机设置——》更多——》移动网络共享——》便携式WLAN热点——》管理设备列表,就可以看到电脑的局域网ip地址了

    3.说说代码

    注册组件,第二个如果不加的话就不能获取网络信息,比如类似url

        av_register_all();
        avformat_network_init();

    获取输入视频的信息,和创建输出url地址的环境

            av_dump_format(ictx, 0, inUrl, 0);
            ret = avformat_alloc_output_context2(&octx, NULL, "flv", outUrl);
            if (ret < 0) {
                avError(ret);
                throw ret;
            }

    将输入视频流放入刚才创建的输出流里

            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("未能成功添加音视频流
    ");
                    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 编解码器上下文失败
    ");
                }
                out_stream->codecpar->codec_tag = 0;
    //        out_stream->codec->codec_tag = 0;
            }

    打开输出url,并写入头部数据

            //打开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;
            }
    

    然后开始循环解码并推流数据

    首先获取一帧的数据

    ret = av_read_frame(ictx, &pkt);

    然后给这一帧的数据配置参数,如果原有配置没有时间就配置时间,我在这里再提两个概念

    DTS(解码时间戳)和PTS(显示时间戳)分别是解码器进行解码和显示帧时相对于SCR(系统参考)的时间戳。SCR可以理解为解码器应该开始从磁盘读取数据时的时间。

                if (pkt.pts == AV_NOPTS_VALUE) {
                    //AVRational time_base:时基。通过该值可以把PTS,DTS转化为真正的时间。
                    AVRational time_base1 = ictx->streams[videoindex]->time_base;
                    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);
                }

    调节播放时间,就是当初我们解码视频之前记录了一个当前时间,然后在循环推流的时候又获取一次当前时间,两者的差值是我们视频应该播放的时间,如果视频播放太快就进程休眠 pkt.dts减去实际播放的时间的差值

                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));
                    }
                }

    如果延时了,这一帧的配置所记录的时间就应该改变

                //计算延时后,重新指定时间戳
                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);

    回调这一帧的时间参数,这里在MainActivity里实例化了接口,显示播放时间

            int res = FFmpegHandle.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() {
                            if(pts == -1){
                                tvPushInfo.setText("播放结束");
                                return ;
                            }
                            tvPushInfo.setText("播放时间:"+dts/1000+"秒");
                        }
                    });
                }
            });

    然后段代码调用了c语言的setCallback函数,获取了接口的实例,和接口的videoCallback函数引用,这里还调用了一次这个函数初始化时间显示

        //转换为全局变量
        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);

    这个时候我们回到循环推流一帧帧数据的时候调用videoCallback函数

        env->CallVoidMethod(pushCallback, mid, (jlong) pts, (jlong) dts, (jlong) duration,
                            (jlong) index);

    然后就是向输出url输出数据,并释放这一帧的数据

                ret = av_interleaved_write_frame(octx, &pkt);
                av_packet_unref(&pkt);

    释放资源

        //关闭输出上下文,这个很关键。
        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);
        env->ReleaseStringUTFChars(outUrl_, outUrl);

    最后回调时间显示,说播放结束

        callback(env, -1, -1, -1, -1);

    4.关于接收推流数据


    我这里使用的是VLC,这个mac和windows都有版本,FILE——》OPEN NETWORK,输入之前的输出url就可以了。这里要注意首先在app上开启推流再使用VLC打开url才可以

    效果如下

    参考文章

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

    这个博主对于推流真的熟练,大家如果对推流还想输入了解可以看看他的博客

  • 相关阅读:
    多维数组和元组
    字符串
    列表
    JQuery事件的绑定
    JQuery设置缓慢下拉大行多次执行的解决办法,以及stop()函数的简单理解
    JQuery_AJAX简单笔记
    C#后台验证身份证号码的一个方法
    JQuery AJAX请求aspx后台方法
    网络编辑器插件ckeditor+ckfinder配置
    请编程实现:产生一个int数组,长度为100,并向其中随机插入1-100,并且不能重复(百度了一下,get一种高性能算法,非递归)
  • 原文地址:https://www.cnblogs.com/jianpanwuzhe/p/8480599.html
Copyright © 2020-2023  润新知