Android 音视频深入 十五 FFmpeg 实现基于Rtmp协议的推流(附源码下载)

丈夫欲遂平生志,一载寒窗一举汤。这篇文章主要讲述Android 音视频深入 十五 FFmpeg 实现基于Rtmp协议的推流(附源码下载)相关的知识,希望能为你提供帮助。
源码地址
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
【Android 音视频深入 十五 FFmpeg 实现基于Rtmp协议的推流(附源码下载)】这个博主对于推流真的熟练,大家如果对推流还想输入了解可以看看他的博客

    推荐阅读