javaee论坛

普通会员

225648

帖子

324

回复

338

积分

楼主
发表于 2019-11-03 12:49:56 | 查看: 689 | 回复: 0

我的一篇博文《如何用FFmpegAPI采集摄像头视频和麦克风音频。。。》已经介绍了如何从视音频采集设备获取数据,并且编码、保存文件到本地。但是,有些应用并不是把流保存成文件,而是需要发送到网络的,比如现在很典型的一种应用场景:把流推送到RTSP、RTMP、HLS服务器,由服务器转发给其他用户观看。很多开发者也是调用FFmpegAPI来实现推流的,用FFmpeg做一个推流器很简单,调用流程跟输出文件的基本相同,基于前面博文的例子稍微修改就可以做出一个采集+编码+推流的软件。这里,我先假设读者已经会用FFmpegAPI保存或录制文件,但没有实现过推流功能,我将给大家说一下做推流跟录制文件的区别,还有说一下要注意的几个问题,希望能帮助大家在开发推流功能时减少一些问题的出现。

首先,做推流和录制文件都需要调用到封装器对象的接口,我们需要定义一个封装器(或叫混合器):

AVFormatContext*m_outputAVFormatCxt;

创建封装器对象,根据输入的协议类型生成对应的封装器。

比如,对于RTSP,我们生成如下的推流封装器:

 res=avformat_alloc_output_context2(&m_outputAVFormatCxt,NULL,"rtsp",m_outputUrl.c_str());

对于RTMP,生成封装器的代码如下:

  res=avformat_alloc_output_context2(&m_outputAVFormatCxt,NULL,"flv",m_outputUrl.c_str());

其中,上面的m_outputUrl是推流地址。

然后,向封装器添加要发送的流(视频、音频),设置每个流的属性。假如我们要推送的流来源于一个文件,那就要先把文件的流枚举出来,获得每个流的信息,然后把这几个流“插入”到封装器里面,这样封装器才能识别这些流的格式。下面是从文件提取流的信息并添加到封装器的代码:

AVOutputFormat*fmt=m_outputAVFormatCxt->oformat;//fmt->video_codec=AV_CODEC_ID_H264;//fmt->audio_codec=AV_CODEC_ID_AAC;for(inti=0;i<m_inputAVFormatCxt->nb_streams;i++){AVStream*in_stream=m_inputAVFormatCxt->streams[i];if(in_stream->codec->codec_type!=AVMEDIA_TYPE_VIDEO&&in_stream->codec->codec_type!=AVMEDIA_TYPE_AUDIO)//忽略掉不是视频和音频的流{continue;}AVStream*out_stream=avformat_new_stream(m_outputAVFormatCxt,in_stream->codec->codec);if(!out_stream){TRACE("cannotnewoutstream");}res=avcodec_copy_context(out_stream->codec,in_stream->codec);if(res<0){stringstrError="cannotcopycontext,filepath:"+m_filePath+",errcode:"+to_string(res)+",errmsg:"+av_make_error_string(m_tmpErrString,AV_ERROR_MAX_STRING_SIZE,res);TRACE("%s\n",strError.c_str());}out_stream->codec->codec_tag=0;if(m_outputAVFormatCxt->oformat->flags&AVFMT_GLOBALHEADER){out_stream->codec->flags|=CODEC_FLAG_GLOBAL_HEADER;}}

我们调用avformat_new_stream生成一个新的流,然后调用avcodec_copy_context将文件的视频流或音频流的上下文属性拷贝到新流的目标上下文中,这样新的流就具有了和输入流同样的属性了(编码格式、分辨率、采样率等)。

发送数据到网络可能因为网络阻塞而发送超时,超时有可能发生在连接或中间数据传输的时候,如果连接超时,用户程序就会很久阻塞在连接函数里。解决这个问题的方法是:我们设置一个回调函数,FFmpeg在发送数据或连接超时会调用到该回调,如果超过某个时间,我们在回调里返回某个标志,让IO函数马上返回,用户的程序就不会一直卡在IO函数里。设置回调函数的代码如下:

m_outputAVFormatCxt->flags|=AVFMT_FLAG_NONBLOCK;av_dump_format(m_outputAVFormatCxt,0,m_outputUrl.c_str(),1);if(!(fmt->flags&AVFMT_NOFILE)){AVIOInterruptCBicb={interruptCallBack,this};m_dwStartConnectTime=GetTickCount();//res=avio_open(&m_outputAVFormatCxt->pb,m_outputUrl.c_str(),AVIO_FLAG_WRITE);res=avio_open2(&m_outputAVFormatCxt->pb,m_outputUrl.c_str(),AVIO_FLAG_WRITE,&icb,NULL);if(res<0){stringstrError="cannotopenoutputio,URL:"+m_outputUrl;TRACE("%s\n",strError.c_str());returnFALSE;}}

用户定义的回调函数如下所示:

staticintinterruptCallBack(void*ctx){FileStreamPushTask*pSession=(FileStreamPushTask*)ctx;//onceyourpreferredtimeisoutyoucanreturn1andexitfromtheloopif(pSession->CheckTimeOut(GetTickCount())){return1;}//continuereturn0;}BOOLFileStreamPushTask::CheckTimeOut(DWORDdwCurrentTime){if(dwCurrentTime<m_dwLastSendFrameTime)//CPU时间回滚{returnFALSE;}if(m_stop_status)returnTRUE;if(m_bInited){if(m_dwLastSendFrameTime>0){if((dwCurrentTime-m_dwLastSendFrameTime)>15000)//发送过程中超时{returnTRUE;}}}else{if((dwCurrentTime-m_dwStartConnectTime)>5000)//连接超时{TRACE("Connecttimeout!\n");m_stop_status=true;returnTRUE;}}returnFALSE;}

接着,就开始连接服务器,并与服务器进行握手交互。

AVDictionary*options=NULL;if(bIsRTSP)av_dict_set(&options,"rtsp_transport","tcp",0);av_dict_set(&options,"stimeout","8000000",0);//设置超时时间res=avformat_write_header(m_outputAVFormatCxt,&options);TRACE("avformat_write_header()return:%d\n",res);if(res<0){stringstrError="cannotwriteoutputstreamheader,URL:"+m_outputUrl+",errcode:"+to_string(res);TRACE("%s\n",strError.c_str());m_bInited=FALSE;returnFALSE;}m_bInited=TRUE;

如果执行到这一步而没有错误,表示已经成功连接服务器,并可以向服务器推流了。

而发送数据就简单了,流程跟输出文件的一样。我们调用av_interleaved_write_frame函数往封装器写入一个帧,让封装器打包数据和发送到服务器。但是,有个地方要注意,就是从文件读出来的帧(输入帧)的时间戳要转换为封装器对应输出流的时基。比如对于视频流,我们需要这样处理:

if(in_stream->codec->codec_type==AVMEDIA_TYPE_VIDEO)//视频{if(pkt.pts==AV_NOPTS_VALUE)//没有时间戳{AVRationaltime_base1=out_stream->time_base;//Durationbetween2frames(us)int64_tcalc_duration=(double)AV_TIME_BASE/av_q2d(in_stream->r_frame_rate);pkt.pts=(double)(nVideoFramesNum*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);}else{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=av_rescale_q(pkt.duration,in_stream->time_base,out_stream->time_base);pkt.pos=-1;}nVideoFramesNum++;}//writethecompressedframetotheoutputformatintnError=av_interleaved_write_frame(m_outputAVFormatCxt,&pkt);

推流完毕,我们要关闭推流器,其实是关闭封装器对象,我们一般会这样关闭:

intres=av_write_trailer(m_outputAVFormatCxt);if(!(m_outputAVFormatCxt->oformat->flags&AVFMT_NOFILE)){if(m_outputAVFormatCxt->pb){avio_close(m_outputAVFormatCxt->pb);m_outputAVFormatCxt->pb=nullptr;}}

但是这样写还不完善。经测试,如果前面的连接握手失败了,则强行调用av_write_trailer会出错的。所以,还需要加个标志变量判断一下连接是否已经初始化成功(我用布尔变量m_bInited表示),修改后的代码如下:

if(m_outputAVFormatCxt){if(m_bInited){intres=av_write_trailer(m_outputAVFormatCxt);}if(!(m_outputAVFormatCxt->oformat->flags&AVFMT_NOFILE)){if(m_outputAVFormatCxt->pb){avio_close(m_outputAVFormatCxt->pb);m_outputAVFormatCxt->pb=nullptr;}}avformat_free_context(m_outputAVFormatCxt);m_outputAVFormatCxt=nullptr;}

 


您需要登录后才可以回帖 登录 | 立即注册

触屏版| 电脑版

技术支持 历史网 V2.0 © 2016-2017