实现用FFmpeg接收RTSP,把H264视频和AAC音频录制成MP4文件
FFmpeg支持Rtsp接收功能,并且相关的协议实现已经很完善了,另外它也支持保存文件的功能,这里我就向大家介绍怎么用它的API来实现这两个功能。
我把接收RTSP和录制文件的逻辑都用一个类RtspStreamMuxTask来处理,下面给出这个类的头文件和源文件。
RtspStreamMuxTask.h文件:
#ifndef RtspStreamMuxTask_H
#define RtspStreamMuxTask_H
class RtspStreamMuxTask
{
public:
RtspStreamMuxTask();
virtual ~RtspStreamMuxTask();
void SetInputUrl(string rtspUrl);
void SetOutputPath(string outputPath);
void StartRecvStream();
void StopRecvStream();
void GetVideoSize(long & width, long & height) //获取视频分辨率
{
width = coded_width;
height = coded_height;
}
private:
void run();
int OpenInputStream();
void CloseInputStream();
void readAndMux();
static DWORD WINAPI ReadingThrd(void * pParam);
int openOutputStream();
void closeOutputStream();
void ReleaseCodecs();
private:
string m_inputUrl;
string m_outputFile;
AVFormatContext* m_inputAVFormatCxt;
AVBitStreamFilterContext* m_bsfcAAC;
AVBitStreamFilterContext* m_bsfcH264;
int m_videoStreamIndex;
int m_audioStreamIndex;
AVFormatContext* m_outputAVFormatCxt;
char m_tmpErrString[64];
bool m_stop_status;
HANDLE m_hReadThread;
BOOL m_bInputInited;
BOOL m_bOutputInited;
int coded_width, coded_height;
int m_frame_rate;
};
#endif // RtspStreamMuxTask_H
RtspStreamMuxTask.cpp文件:
#include "stdafx.h"
#include "RtspStreamMuxTask.h"
#include <sstream>
string to_string(int n)
{
std::ostringstream stm;
string str;
stm << n;
str = stm.str();
//std::cout << str << std::endl;
return str;
}
//
RtspStreamMuxTask::RtspStreamMuxTask()
{
m_stop_status = false;
m_inputAVFormatCxt = nullptr;
m_bsfcAAC = nullptr;
m_bsfcH264 = nullptr;
m_videoStreamIndex = -1;
m_audioStreamIndex = -1;
m_outputAVFormatCxt = nullptr;
m_hReadThread = NULL;
m_bInputInited = FALSE;
m_bOutputInited = FALSE;
coded_width = coded_height = 0;
m_frame_rate = 25;
}
RtspStreamMuxTask::~RtspStreamMuxTask()
{
StopRecvStream();
}
void RtspStreamMuxTask::SetInputUrl(string rtspUrl)
{
m_inputUrl = rtspUrl;
}
void RtspStreamMuxTask::SetOutputPath(string outputPath)
{
m_outputFile = outputPath;
}
void RtspStreamMuxTask::StartRecvStream()
{
if(m_inputUrl.empty())
return;
m_videoStreamIndex = -1;
m_audioStreamIndex = -1;
m_bInputInited = FALSE;
m_bOutputInited = FALSE;
coded_width = coded_height = 0;
DWORD threadID = 0;
m_hReadThread = CreateThread(NULL, 0, ReadingThrd, this, 0, &threadID);
}
void RtspStreamMuxTask::StopRecvStream()
{
m_stop_status = true;
if (m_hReadThread != NULL)
{
WaitForSingleObject(m_hReadThread, INFINITE);
CloseHandle(m_hReadThread);
m_hReadThread = NULL;
}
CloseInputStream();
}
DWORD WINAPI RtspStreamMuxTask::ReadingThrd(void * pParam)
{
RtspStreamMuxTask * pTask = (RtspStreamMuxTask *) pParam;
pTask->run();
OutputDebugString("ReadingThrd exited\n");
return 0;
}
void RtspStreamMuxTask::run()
{
try
{
m_stop_status = false;
OpenInputStream();
openOutputStream();
m_stop_status = false;
readAndMux();
CloseInputStream();
closeOutputStream();
}
catch(std::exception& e)
{
TRACE("%s \n", e.what());
CloseInputStream();
}
}
int RtspStreamMuxTask::OpenInputStream()
{
if (m_inputAVFormatCxt)
{
string strError = ("already has input avformat");
TRACE("%s \n", strError.c_str());
return -1;
}
m_bsfcAAC = av_bitstream_filter_init("aac_adtstoasc");
if(!m_bsfcAAC)
{
string strError = ("can not create aac_adtstoasc filter");
TRACE("%s \n", strError.c_str());
return -1;
}
m_bsfcH264 = av_bitstream_filter_init("h264_mp4toannexb");
if(!m_bsfcH264)
{
string strError = ("can not create h264_mp4toannexb filter");
TRACE("%s \n", strError.c_str());
return -1;
}
///
int res = 0;
res = avformat_open_input(&m_inputAVFormatCxt, m_inputUrl.c_str(), 0, NULL);
if(res < 0)
{
string strError = ("can not open file:" + m_inputUrl + ",errcode:" + to_string(res) + ",err msg:" + av_make_error_string(m_tmpErrString, AV_ERROR_MAX_STRING_SIZE, res));
TRACE("%s \n", strError.c_str());
return -1;
}
if (avformat_find_stream_info(m_inputAVFormatCxt, 0) < 0)
{
string strError = ("can not find stream info");
TRACE("%s \n", strError.c_str());
return -1;
}
av_dump_format(m_inputAVFormatCxt, 0, m_inputUrl.c_str(), 0);
for (int i = 0; i < m_inputAVFormatCxt->nb_streams; i++)
{
AVStream *in_stream = m_inputAVFormatCxt->streams[i];
TRACE("codec id: %d, URL: %s \n", in_stream->codec->codec_id, m_inputUrl.c_str());
if (in_stream->codec->codec_type == AVMEDIA_TYPE_VIDEO)
{
m_videoStreamIndex = i;
coded_width = in_stream->codec->width;
coded_height = in_stream->codec->height;
if(in_stream->avg_frame_rate.den != 0 && in_stream->avg_frame_rate.num != 0)
{
m_frame_rate = in_stream->avg_frame_rate.num/in_stream->avg_frame_rate.den;//每秒多少帧
}
TRACE("video stream index: %d, width: %d, height: %d, FrameRate: %d\n", m_videoStreamIndex, in_stream->codec->width, in_stream->codec->height, m_frame_rate);
}
else if (in_stream->codec->codec_type == AVMEDIA_TYPE_AUDIO)
{
m_audioStreamIndex = i;
}
}
m_bInputInited = TRUE;
return 0;
}
void RtspStreamMuxTask::CloseInputStream()
{
if (m_inputAVFormatCxt)
{
avformat_close_input(&m_inputAVFormatCxt);
}
if(m_bsfcAAC)
{
av_bitstream_filter_close(m_bsfcAAC);
m_bsfcAAC = nullptr;
}
if(m_bsfcH264)
{
av_bitstream_filter_close(m_bsfcH264);
m_bsfcH264 = nullptr;
}
m_bInputInited = FALSE;
}
int RtspStreamMuxTask::openOutputStream()
{
if (m_outputAVFormatCxt)
{
TRACE("already has rtmp avformat \n");
return -1;
}
int res = 0;
if(!m_outputFile.empty())
{
res = avformat_alloc_output_context2(&m_outputAVFormatCxt, NULL, "mp4", m_outputFile.c_str());
if (m_outputAVFormatCxt == NULL)
{
TRACE("can not alloc output context \n");
return -1;
}
AVOutputFormat* fmt = m_outputAVFormatCxt->oformat;
// fmt->audio_codec = AV_CODEC_ID_AAC;
// fmt->video_codec = AV_CODEC_ID_H264;
for (int i = 0; i < m_inputAVFormatCxt->nb_streams; i++)
{
AVStream *in_stream = m_inputAVFormatCxt->streams[i];
AVStream *out_stream = avformat_new_stream(m_outputAVFormatCxt, in_stream->codec->codec);
if (!out_stream)
{
TRACE("can not new out stream");
return -1;
}
res = avcodec_copy_context(out_stream->codec, in_stream->codec);
if (res < 0)
{
string strError = "can not copy context, url: " + m_inputUrl + ",errcode:" + to_string(res) + ",err msg:" + av_make_error_string(m_tmpErrString, AV_ERROR_MAX_STRING_SIZE, res);
TRACE("%s \n", strError.c_str());
return -1;
}
if (m_outputAVFormatCxt->oformat->flags & AVFMT_GLOBALHEADER)
{
out_stream->codec->flags |= CODEC_FLAG_GLOBAL_HEADER;
}
}
av_dump_format(m_outputAVFormatCxt, 0, m_outputFile.c_str(), 1);
if (!(fmt->flags & AVFMT_NOFILE))
{
res = avio_open(&m_outputAVFormatCxt->pb, m_outputFile.c_str(), AVIO_FLAG_WRITE);
if (res < 0)
{
string strError = "can not open output io, file:" + m_outputFile + ",errcode:" + to_string(res) + ", err msg:" + av_make_error_string(m_tmpErrString, AV_ERROR_MAX_STRING_SIZE, res);
TRACE("%s \n", strError.c_str());
return -1;
}
}
res = avformat_write_header(m_outputAVFormatCxt, NULL);
if (res < 0)
{
string strError = "can not write outputstream header, URL:" + m_outputFile + ",errcode:" + to_string(res) + ", err msg:" + av_make_error_string(m_tmpErrString, AV_ERROR_MAX_STRING_SIZE, res);
TRACE("%s \n", strError.c_str());
m_bOutputInited = FALSE;
return -1;
}
m_bOutputInited = TRUE;
}
return 0;
}
void RtspStreamMuxTask::closeOutputStream()
{
if (m_outputAVFormatCxt)
{
if(m_bOutputInited)
{
int res = av_write_trailer(m_outputAVFormatCxt);
}
if (!(m_outputAVFormatCxt->oformat->flags & AVFMT_NOFILE))
{
if(m_outputAVFormatCxt->pb)
{
avio_close(m_outputAVFormatCxt->pb);
}
}
avformat_free_context(m_outputAVFormatCxt);
m_outputAVFormatCxt = nullptr;
}
m_bOutputInited = FALSE;
}
void RtspStreamMuxTask::readAndMux()
{
int nVideoFramesNum = 0;
int64_t first_pts_time = 0;
DWORD start_time = GetTickCount();
AVPacket pkt;
av_init_packet(&pkt);
while(1)
{
if(m_stop_status == true)
{
break;
}
int res;
res = av_read_frame(m_inputAVFormatCxt, &pkt);
if (res < 0) //读取错误或流结束
{
if(AVERROR_EOF == res)
{
TRACE("End of file \n");
}
else
{
TRACE("av_read_frame() got error: %d \n", res);
}
break;
}
AVStream *in_stream = m_inputAVFormatCxt->streams[pkt.stream_index];
AVStream *out_stream = m_outputAVFormatCxt->streams[pkt.stream_index];
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;
if(in_stream->codec->codec_type != AVMEDIA_TYPE_VIDEO && in_stream->codec->codec_type != AVMEDIA_TYPE_AUDIO)
{
continue;
}
if(in_stream->codec->codec_type == AVMEDIA_TYPE_VIDEO) //视频
{
nVideoFramesNum++;
// write the compressed frame to the output format
int nError = av_interleaved_write_frame(m_outputAVFormatCxt, &pkt);
if (nError != 0)
{
char tmpErrString[AV_ERROR_MAX_STRING_SIZE] = {0};
av_make_error_string(tmpErrString, AV_ERROR_MAX_STRING_SIZE, nError);
TRACE("Error: %d while writing video frame, %s\n", nError, tmpErrString);
}
//int nSecs = pkt.pts*in_stream->time_base.num/in_stream->time_base.den;
//TRACE("Frame time: %02d:%02d \n", nSecs/60, nSecs%60);
}
else if(in_stream->codec->codec_type == AVMEDIA_TYPE_AUDIO) //音频
{
// write the compressed frame to the output format
int nError = av_interleaved_write_frame(m_outputAVFormatCxt, &pkt);
if (nError != 0)
{
char tmpErrString[AV_ERROR_MAX_STRING_SIZE] = {0};
av_make_error_string(tmpErrString, AV_ERROR_MAX_STRING_SIZE, nError);
TRACE("Error: %d while writing audio frame, %s\n", nError, tmpErrString);
}
}
if((in_stream->codec->codec_type == AVMEDIA_TYPE_VIDEO) )
{
if(first_pts_time == 0)
first_pts_time = pkt.pts;
int64_t pts_time = (pkt.pts - first_pts_time)*1000*in_stream->time_base.num/in_stream->time_base.den; //转成毫秒
int64_t now_time = GetTickCount() - start_time;
//if(pts_time > now_time + 10 && pts_time < now_time + 3000)
//{
// Sleep(pts_time-now_time);
//}
//else if(pts_time == 0 && nVideoFramesNum > 1)
//{
// Sleep(20);
//}
}
av_free_packet(&pkt);
}//while
TRACE("Reading ended, read %d video frames \n", nVideoFramesNum);
}
下面对代码的一些关键流程进行说明:
1. 传入URL和设置保存文件路径
void SetInputUrl(string rtspUrl);
void SetOutputPath(string outputPath);
SetInputUrl函数用于设置要接收的Rtsp地址,而SetOutputPath函数用于定义录制的文件名称,文件必须是以.MP4为后缀。接收RTSP流时,程序会将收到的视频(H264)和音频(AAC)会封装到目标文件容器(MP4)里面。
2. StartRecvStream函数用于开始接收流,这个函数先判断输入URL是否为空,如果为空则不做接收就返回了。如果URL合法,则初始化类的成员变量,接着创建线程,负责连接RTSP服务器并开始接收数据。
void RtspStreamMuxTask::StartRecvStream()
{
if(m_inputUrl.empty())
return;
m_videoStreamIndex = -1;
m_audioStreamIndex = -1;
m_bInputInited = FALSE;
m_bOutputInited = FALSE;
coded_width = coded_height = 0;
DWORD threadID = 0;
m_hReadThread = CreateThread(NULL, 0, ReadingThrd, this, 0, &threadID);
}
3. 线程函数的实现代码如下:
DWORD WINAPI RtspStreamMuxTask::ReadingThrd(void * pParam)
{
RtspStreamMuxTask * pTask = (RtspStreamMuxTask *) pParam;
pTask->run();
OutputDebugString("ReadingThrd exited\n");
return 0;
}
void RtspStreamMuxTask::run()
{
try
{
m_stop_status = false;
OpenInputStream();
openOutputStream();
m_stop_status = false;
readAndMux();
CloseInputStream();
closeOutputStream();
}
catch(std::exception& e)
{
TRACE("%s \n", e.what());
CloseInputStream();
}
}
它会调用OpenInputStream函数通过传入的URL连接RTSP服务器,并获取流的信息;然后,调用OpenOutputStream函数初始化输出的容器和编码格式,生成目标文件;接着,就调用readAndMux函数做接收处理,这个函数里面不停地调用FFmpeg的API av_read_frame接收数据包,数据包类型分视频和音频,如果av_read_frame返回-1表示断开连接或流结束了,要退出线程。对于MP4容器,对混合进去的视频和音频的编码格式是有要求的,视频可以是MPEG4,H264,音频一般是AAC。接收到的数据通过av_interleaved_write_frame写到文件。
void RtspStreamMuxTask::readAndMux()
{
int nVideoFramesNum = 0;
int64_t first_pts_time = 0;
DWORD start_time = GetTickCount();
AVPacket pkt;
av_init_packet(&pkt);
while(1)
{
if(m_stop_status == true)
{
break;
}
int res;
res = av_read_frame(m_inputAVFormatCxt, &pkt);
if (res < 0) //读取错误或流结束
{
if(AVERROR_EOF == res)
{
TRACE("End of file \n");
}
else
{
TRACE("av_read_frame() got error: %d \n", res);
}
break;
}
AVStream *in_stream = m_inputAVFormatCxt->streams[pkt.stream_index];
AVStream *out_stream = m_outputAVFormatCxt->streams[pkt.stream_index];
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;
if(in_stream->codec->codec_type != AVMEDIA_TYPE_VIDEO && in_stream->codec->codec_type != AVMEDIA_TYPE_AUDIO)
{
continue;
}
if(in_stream->codec->codec_type == AVMEDIA_TYPE_VIDEO) //视频
{
nVideoFramesNum++;
// write the compressed frame to the output format
int nError = av_interleaved_write_frame(m_outputAVFormatCxt, &pkt);
if (nError != 0)
{
char tmpErrString[AV_ERROR_MAX_STRING_SIZE] = {0};
av_make_error_string(tmpErrString, AV_ERROR_MAX_STRING_SIZE, nError);
TRACE("Error: %d while writing video frame, %s\n", nError, tmpErrString);
}
//int nSecs = pkt.pts*in_stream->time_base.num/in_stream->time_base.den;
//TRACE("Frame time: %02d:%02d \n", nSecs/60, nSecs%60);
}
else if(in_stream->codec->codec_type == AVMEDIA_TYPE_AUDIO) //音频
{
// write the compressed frame to the output format
int nError = av_interleaved_write_frame(m_outputAVFormatCxt, &pkt);
if (nError != 0)
{
char tmpErrString[AV_ERROR_MAX_STRING_SIZE] = {0};
av_make_error_string(tmpErrString, AV_ERROR_MAX_STRING_SIZE, nError);
TRACE("Error: %d while writing audio frame, %s\n", nError, tmpErrString);
}
}
if((in_stream->codec->codec_type == AVMEDIA_TYPE_VIDEO) )
{
if(first_pts_time == 0)
first_pts_time = pkt.pts;
int64_t pts_time = (pkt.pts - first_pts_time)*1000*in_stream->time_base.num/in_stream->time_base.den; //转成毫秒
int64_t now_time = GetTickCount() - start_time;
//if(pts_time > now_time + 10 && pts_time < now_time + 3000)
//{
// Sleep(pts_time-now_time);
//}
//else if(pts_time == 0 && nVideoFramesNum > 1)
//{
// Sleep(20);
//}
}
av_free_packet(&pkt);
}//while
TRACE("Reading ended, read %d video frames \n", nVideoFramesNum);
}
但注意写文件前必须对时间戳修改一下,因为收到的RTP包的时间戳基线跟FFmpeg录制容器中的时间戳基线是不一样的,这个时间戳基线也叫做时钟频率。我们要通过av_rescale_q_rnd、av_rescale_q转换时基,下面是转换时间戳的代码:
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);
4. 接收的循环退出后,我们要关闭输入和输出,释放相关的变量。
void RtspStreamMuxTask::CloseInputStream()
{
if (m_inputAVFormatCxt)
{
avformat_close_input(&m_inputAVFormatCxt);
}
if(m_bsfcAAC)
{
av_bitstream_filter_close(m_bsfcAAC);
m_bsfcAAC = nullptr;
}
if(m_bsfcH264)
{
av_bitstream_filter_close(m_bsfcH264);
m_bsfcH264 = nullptr;
}
m_bInputInited = FALSE;
}
void RtspStreamMuxTask::closeOutputStream()
{
if (m_outputAVFormatCxt)
{
if(m_bOutputInited)
{
int res = av_write_trailer(m_outputAVFormatCxt);
}
if (!(m_outputAVFormatCxt->oformat->flags & AVFMT_NOFILE))
{
if(m_outputAVFormatCxt->pb)
{
avio_close(m_outputAVFormatCxt->pb);
}
}
avformat_free_context(m_outputAVFormatCxt);
m_outputAVFormatCxt = nullptr;
}
m_bOutputInited = FALSE;
}
整个类的代码就这么简单。但是,还有些地方有优化空间的。
比如,上述代码存在一个问题:当RTSP地址连接不上的时候会一直卡在avformat_open_input函数。这个问题是因为avformat_open_input函数是阻塞的,它会等服务器连接上或等到超时(10秒以上)才返回,所以,服务器连接不上的时候就卡很久。解决这个问题的办法是设置异常回调,事实上FFmpeg已经为我们考虑到这个问题,我们需要做的是增加一个异常回调函数,在里面做一些判断。下面补充这个问题的解决方法和步骤:
1. 先定义几个变量:
DWORD m_dwStartConnectTime; //开始接收的时间
DWORD m_dwLastRecvFrameTime; //上一次收到帧数据的时间,单位:毫秒
DWORD m_nMaxRecvTimeOut; //网络接收数据的超时时间,单位:秒
DWORD m_nMaxConnectTimeOut; //连接超时,单位:秒
2. 修改avformat_open_input调用的参数,指定Callback函数
//Initialize format context
m_inputAVFormatCxt = avformat_alloc_context();
//Initialize intrrupt callback
AVIOInterruptCB icb = {interruptCallBack,this};
m_inputAVFormatCxt->interrupt_callback = icb;
m_nMaxConnectTimeOut = 8;
m_nMaxRecvTimeOut = 10;
m_dwLastRecvFrameTime = 0;
m_dwStartConnectTime = GetTickCount();
//m_inputAVFormatCxt->flags |= AVFMT_FLAG_NONBLOCK;
AVDictionary* options = nullptr;
//av_dict_set(&options, "rtsp_transport", "tcp", 0);
//av_dict_set(&options, "stimeout", "3000000", 0); //设置超时断开连接时间,好像没有效
res = avformat_open_input(&m_inputAVFormatCxt, m_InputUrl.c_str(), 0, &options);
上面的代码指定了intrrupt callback函数为interruptCallBack,而这个函数代码如下,大概思路是:如果超过自己设定的超时就返回1,表示不继续等待,这样avformat_open_input就会马上返回。注意:该回调函数不光是连接时调用,在接收数据的时候也会被调用,所以也可以通过在回调函数里对接收超时时间做处理。
static int interruptCallBack(void *ctx)
{
RtspStreamMuxTask * pSession = (RtspStreamMuxTask*) 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;
}
而CheckTimeOut成员函数的实现如下,
BOOL RtspStreamMuxTask::CheckTimeOut(DWORD dwCurrentTime)
{
if(m_stop_status) //是否退出程序
return TRUE;
if(m_dwLastRecvFrameTime > 0)
{
if((dwCurrentTime - m_dwLastRecvFrameTime)/1000 > m_nMaxRecvTimeOut) //接收过程中超时
{
return TRUE;
}
}
else
{
if((dwCurrentTime - m_dwStartConnectTime)/1000 > m_nMaxConnectTimeOut) //连接超时
{
return TRUE;
}
}
return FALSE;
}