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

news/2024/7/5 5:49:22

源码地址
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("未能成功添加音视频流\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;
}

打开输出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

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

转载于:https://blog.51cto.com/13591594/2073664


http://www.niftyadmin.cn/n/4606979.html

相关文章

基于PelicanDT实现dubbo断网验证

具体介绍 Dubbo-example&#xff0c;是基于PelicanDT实现dubbo环境准备&#xff0c;禁止端口网络访问&#xff0c;执行接口调用验证端口是否禁用示例 前期准备 本示例程序是基于阿里云ECS或远程Linux服务器完成&#xff0c;只需购买阿里云机器&#xff0c;或者选定已准备好的远…

Launcher解析

&#xfeff;&#xfeff;首先来说说我为什么写这篇文章&#xff0c;最近公司要我负责搞Launcher&#xff0c;网上一查这方面的资料比较少&#xff0c;并且不全&#xff0c;研究起来相当困难&#xff0c;所以就写了这篇文章&#xff0c;希望对大家有帮助。这篇文章是相当长的&a…

你遵守开源协议GPL/LGPL吗?

一款Linux 手机操作系统&#xff0c;至少要有40&#xff5e;50个开源项目的支援。这些开源项目拿来后如何用&#xff1f;直接修改&#xff0c;当然要修改。修改后如何发布&#xff1f;还是闭源算了&#xff01;这个问题我也考虑过&#xff0c;有时又不想考虑。 下面是网友的关于…

滴滴工程师带你深入理解 TCP 握手分手全过程

本文作者&#xff1a;饶全成&#xff0c;中科院计算所硕士&#xff0c;滴滴出行后端研发工程师。个人主页&#xff1a;https://zhihu.com/people/raoquancheng记得刚毕业找工作面试的时候&#xff0c;经常会被问到&#xff1a;你知道“3次握手&#xff0c;4次挥手”吗&#xff…

Linux手机开发中,尽量不要用多线程。

在Linux手机操作系统中&#xff0c;一般不提倡用多线程&#xff0c;为什么呢&#xff1f; 1 难调试&#xff1b; 2 难同步。 所以&#xff0c;一个进程中就搞一个线程。不要在进程中搞一堆线程&#xff0c;否则调试起来很痛苦。 不过也有一些比较特殊的程序&#xff0c;比如…

精辟的山寨。

有人为山寨机编了这么一段话&#xff1a; “一定得选最好的硬件芯片&#xff0c;雇法国设计师&#xff0c;做就得做最高档的手机&#xff1b; 平台直接用MTK&#xff0c;屏幕最小也得3.0的&#xff0c;什么智能呀、电视功能呀、双卡同时待机呀、能给他装的全给他装上&#xf…

Android4.0 Launcher拖拽原理分析(1)

&#xfeff;&#xfeff;Android4.0 Launcher拖拽原理分析&#xff08;一&#xff09;在Android4.0源码自带的Launcher中&#xff0c;拖拽是由DragController进行控制的。 基本流程是相应的View在检测到用户操作后进行判断&#xff0c;若可以触发拖拽&#xff0c;则设置自身的…

LiMo阶段性成果:又添7款新机器,同时成员扩到50个。

根据LiMo基金会透露&#xff0c;有7款新手机刚刚通过LiMo的认证过程&#xff0c;现在&#xff0c;采用该平台的手机产品再次获得了增加。 通过认证的7款手机分属于摩托罗拉、松下、NEC等等&#xff0c;全部采用最新的LiMo Release 1版本。其中松下和NEC的手机将通过NTT DoCoMo…