FFmpeg采集摄像头图像并推流至RTSP/RTMP

在很典型的一种应用场景:把流推送到RTSP、RTMP、HLS服务器,由服务器转发给其他用户观看。很多开发者也是调用FFmpeg API来实现推流的,用FFmpeg 做一个推流器很简单,调用流程跟输出文件的基本相同,稍微修改就可以做出一个采集+编码+推流的软件。这里,我先假设读者已经会用FFmpeg API保存或录制文件,但没有实现过推流功能,我将给大家说一下做推流跟录制文件的区别,还有说一下要注意的几个问题,希望能帮助大家在开发推流功能时减少一些问题的出现。

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

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 (int i = 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("can not new out stream");
		}
		res = avcodec_copy_context(out_stream->codec, in_stream->codec);
		if (res < 0)
		{
			string strError = "can not copy context, filepath: " + m_filePath + ",errcode:" + to_string(res) + ",err msg:" + 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))
	{	
		AVIOInterruptCB icb = {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)
		{
			string strError = "can not open output io, URL:" + m_outputUrl;
			TRACE("%s \n", strError.c_str());
			return FALSE;
		}
	}

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

static int interruptCallBack(void *ctx)
{
    FileStreamPushTask * pSession = (FileStreamPushTask*) ctx;
 
   //once your preferred time is out you can return 1 and exit from the loop
    if(pSession->CheckTimeOut(GetTickCount()))
    {
      return 1;
    }
 
   //continue 
   return 0;
 
}
 
BOOL   FileStreamPushTask::CheckTimeOut(DWORD dwCurrentTime)
{
	if(dwCurrentTime < m_dwLastSendFrameTime) //CPU时间回滚
	{
		return FALSE;
	}
 
	if(m_stop_status)
		return TRUE;
 
	if(m_bInited)
	{
		if(m_dwLastSendFrameTime > 0)
		{
			if((dwCurrentTime - m_dwLastSendFrameTime) > 15000) //发送过程中超时
			{
				return TRUE;
			}
		}
	}
	else
	{
		if((dwCurrentTime - m_dwStartConnectTime) > 5000) //连接超时
		{
			TRACE("Connect timeout! \n");
			m_stop_status = true;
			return TRUE;
		}
	}
	return FALSE;
}

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

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)
	{
		string strError = "can not write outputstream header, URL:" + m_outputUrl + ",errcode:" + to_string(res);
		TRACE("%s \n", strError.c_str());
		m_bInited = FALSE;
		return FALSE;
	}
 
	m_bInited = TRUE;

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

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

 if(in_stream->codec->codec_type == AVMEDIA_TYPE_VIDEO)  //视频
	{
		if(pkt.pts == AV_NOPTS_VALUE) //没有时间戳
		{
			AVRational time_base1 = out_stream->time_base; 
 
			//Duration between 2 frames (us) 
			int64_t calc_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++;
	}
 
	// write the compressed frame to the output format
	int nError = av_interleaved_write_frame(m_outputAVFormatCxt, &pkt);

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

int res = 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)
		{
		  int res = 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;
      }

  • 0
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值