ffmpeg中的AVFormatContext是一个复杂的结构,因为它上承文件读写,下承编码转换,新手刚入门,看了老版本的demo,难免一时间不知所措。好在3.x版本开始,ffmpeg开始分离写法,现在AVFormatContext的功能只剩下文件读写跟数据封装了。今天我就将新版本分离后,如何划分AVFormatContext的任务。同时提供一些抽象思路。
AVFormatContext,从名字可以看出,格式上下文。它集成了封装跟解封装的功能。很多新手一入门,都认为这个是个初始化步骤,从而只会一套组合流水线,不知道怎么单独使用。这不利于我们进行开发。
从功能上,我将它划分成为封装器跟解封装器。封装器就是将码流数据类似h264、aac打包成mp4、mkv、avi等格式的功能,而解封装器则是将mp4、mkv、avi等格式中从获取h264、aac码流数据。
由于很多接口并不是通用的,我将这两个功能进行单独封装。这一步封装有利于帮助开发者抽象数据结构,分离接口,进行生命周期管理。
解封装器我抽离了接口
class AVDeMux
{
public :
AVDeMux();
~AVDeMux();
//过滤掉流,在一些特殊的网络流中可以得到网速优化
void SkipStream(int index);
/****
作用:初始化解封装器
参数: name,需要初始化的文件名,可以是网络url
参数: ppDict,预设词典,封装结构提供,可以通过ffmpeg的解封装器找到需要的,一般不设置
****/
int InitDeMux(const char* name, AVDictionary** ppDict = NULL);
//获取流中的packet
int GetStreamPacket(AVPacket* packet);
AVErrorStatus m_eStatus;
//所有的数据流
std::vector<AVStream*> m_pStreams[AVMEDIA_TYPE_NB];
private:
AVFormatContext* m_pDeMuxFmt;
};
封装器接口
class AVMux
{
public:
AVMux();
~AVMux();
//初始化封装器
int InitMux(const char* name);
//添加流用
int AddStream(AVCodecParameters* param, const AVCodec* codec, AVRational timebase);
//写文件夹头
int WriteHead(AVDictionary** ppDict);
//写流数据
int WritePacket(AVPacket* packet);
//写文件夹尾
int WriteTrailer();
AVErrorStatus m_eStatus;
std::vector<AVStream*> m_pStreams[AVMEDIA_TYPE_NB];
private:
AVFormatContext* m_pMuxContext;
};
这样将上下文的操作划分为两个功能,使用起来就不会很头疼了。
先来讲讲解封装器,解封装器必要接口较少,基本上就初始化跟获取流数据而已。初始化有两个比较重要的api,avformat_open_input跟avformat_find_stream_info,这两个接口被我集成到了AVDeMux::InitDeMux中。
int iRet = 0;
do {
//打开文件,并且初始化解封装器
iRet=avformat_open_input(&m_pDeMuxFmt, name, NULL, ppDict);
if (iRet)
break;
//探测码流信息
iRet = avformat_find_stream_info(m_pDeMuxFmt, NULL);
if (iRet)
break;
//组装码流
for (size_t i = 0; i < m_pDeMuxFmt->nb_streams; i++)
{
AVMediaType type = m_pDeMuxFmt->streams[i]->codecpar->codec_type;
if (type != AVMEDIA_TYPE_UNKNOWN)
m_pStreams[type].push_back(m_pDeMuxFmt->streams[i]);
}
} while (0);
if (iRet)
{
m_eStatus.MakeErrorLog(iRet, __FUNCTION__);
if (m_pDeMuxFmt)
{
avformat_close_input(&m_pDeMuxFmt);
}
}
return iRet;
avformat_open_input,初始化context并且打开输入流,一步到位初始化。
avformat_find_stream_info,用于探测码流数据,需要在打开输入流后才可使用。能够将文件中的码流信息获取出来,部分格式会获取信息不正常。获取的信息存放到AVFormatContext的streams中。AVStream由AVFormatcontext进行管理,属于内容,不可手动创建,在解封装器中,通过这个方法进行建立。
通过以上两个api后,初始化工作已经完成,AVDeMux::GetStreamPacket是通过调用av_read_frame从解封装器中获取数据即可,以上三个api能够满足所有读取需求。av_read_frame获取的数据通过解码器解码即可获取到原始数据。由于篇幅问题,本篇不进行介绍。
int AVDeMux::GetStreamPacket(AVPacket * packet)
{
int iRet = 0;
//覆写之前清堆内数据
av_packet_unref(packet);
//获取数据包
iRet=av_read_frame(m_pDeMuxFmt, packet);
if (iRet)
{
m_eStatus.MakeErrorLog(iRet, __FUNCTION__);
}
return iRet;
}
而封装器,相对较为复杂,你也可以看到,我封装了好几个接口,因为它提供的diy功能实在是太多了,ffmpeg把大部分功能开放给了开发者。我们只需要关注几个常用的情况即可。
首先是初始化AVMux::InitMux,avformat_alloc_output_context2跟avio_open,这两个api,负责文件的初始化跟预设封装器生成。你甚至可以不依据这个api进行预设,手动预设。但是由于那样的难度不适合新手,通常使用avformat_alloc_output_context进行预设数据获取。对比手动初始化,它多了个AVOutputFormat的预设(iformat)。这个预设,是ffmpeg内部推荐的编码器,在转码时,可以参考预设数值进行编码器选取。不过由于我们只需要封装器重要的部分,暂时不需要了解iformat的数据怎么使用。
int AVMux::InitMux(const char * name)
{
int iRet=avformat_alloc_output_context2(&m_pMuxContext, NULL, NULL, name);
if (!iRet&&!(m_pMuxContext->oformat->flags&AVFMT_NOFILE))
iRet=avio_open(&m_pMuxContext->pb, name, AVIO_FLAG_WRITE);
if (iRet)
{
m_eStatus.MakeErrorLog(iRet, __FUNCTION__);
}
return iRet;
}
初始化完输出流,要开始初始化码流的全局数据了。通过AVMux::AddStream对内容进行了封装,首先我们需要码流的基本信息。这个信息由编码器生成,转封装中,我们已经有了输入流的编码器信息,直接进行拷贝即可。
int AVMux::AddStream(AVCodecParameters * param, const AVCodec* codec, AVRational timebase)
{
int iRet = 0;
do {
AVStream* pStream = avformat_new_stream(m_pMuxContext, codec);
if (!pStream)
{
iRet = AVERROR_INVALIDDATA;
break;
}
iRet=avcodec_parameters_copy(pStream->codecpar, param);
pStream->time_base = timebase;
m_pStreams[codec->type].push_back(pStream);
} while (0);
if (iRet)
{
m_eStatus.MakeErrorLog(iRet, __FUNCTION__);
}
return iRet;
}
我这里使用了3.x的接口,不再使用avcodec_context_xxx了,因为avcodeccontext是编码上下文了,我们不需要那么多的东西,只需要确保封装需要的东西即可。封装需要视频的宽高、编码器头、像素格式、时间轴,音频需要采样方式,采样率、声道数、声道分布、编码器头。
接着是写文件头,WriteHead,这里用于检查上述所有的初始化是否合理,如果牛头不对马嘴,这里才会告诉你初始化失败。比如你初始化h264码流信息到mp3封装里,这时候封装器不接受这个初始化结果,写头的时候会返回错误给你。到这里才是走完了所有的初始化
int AVMux::WriteHead(AVDictionary ** ppDict)
{
int iRet=avformat_write_header(m_pMuxContext, ppDict);
if (iRet)
{
m_eStatus.MakeErrorLog(iRet, __FUNCTION__);
}
return iRet;
}
WritePacket,用于写码流数据包,跟AVDemux::GetStreamPacket相对
int AVMux::WritePacket(AVPacket * packet)
{
int iRet = av_interleaved_write_frame(m_pMuxContext,packet);
if (iRet)
{
m_eStatus.MakeErrorLog(iRet, __FUNCTION__);
}
return iRet;
}
相比解封装,最后关闭封装器之前,还要写文件未才是真正的结束了文件的写出,最后关闭前调用WriteTrailer
int AVMux::WriteTrailer()
{
int iRet = av_write_trailer(m_pMuxContext);
if (iRet)
{
m_eStatus.MakeErrorLog(iRet, __FUNCTION__);
}
return iRet;
}
以上,所有的重要封装基本完成了。对于新手来说,单个api可能看不太懂。所以我将这些api的基础组合了起来,帮助大家了解整个流程。
int AVFormatConvert::Translate(AVErrorStatus & status, const char * inputname, const char* outputname, bool needvideo, bool needaudio)
{
AVDeMux DeMux;
AVMux Mux;
int iRet = 0;
do {
iRet = DeMux.InitDeMux(inputname, NULL);//初始化解封装器
if (iRet)
break;
iRet = Mux.InitMux(outputname);//初始化封装器
if (iRet)
break;
//考虑多流情况较为复杂,本demo不进行多流支持
if (needvideo&&DeMux.m_pStreams[AVMEDIA_TYPE_VIDEO].size())
{
//由于codecparam里没有,目前的初始化必须通过codec_id找编码器结构
const AVCodec* pCodec = avcodec_find_decoder(DeMux.m_pStreams[AVMEDIA_TYPE_VIDEO][0]->codecpar->codec_id);
//编码器信息拷贝
iRet=Mux.AddStream(DeMux.m_pStreams[AVMEDIA_TYPE_VIDEO][0]->codecpar, pCodec, DeMux.m_pStreams[AVMEDIA_TYPE_VIDEO][0]->time_base);
if (iRet)
break;
for (size_t i = 1; i < DeMux.m_pStreams[AVMEDIA_TYPE_VIDEO].size(); i++)
{
//加速用
DeMux.SkipStream(DeMux.m_pStreams[AVMEDIA_TYPE_VIDEO][i]->index);
}
}
if (needaudio&&DeMux.m_pStreams[AVMEDIA_TYPE_AUDIO].size())
{
const AVCodec* pCodec = avcodec_find_decoder(DeMux.m_pStreams[AVMEDIA_TYPE_AUDIO][0]->codecpar->codec_id);
//编码器信息拷贝
iRet = Mux.AddStream(DeMux.m_pStreams[AVMEDIA_TYPE_AUDIO][0]->codecpar, pCodec, DeMux.m_pStreams[AVMEDIA_TYPE_AUDIO][0]->time_base);
if (iRet)
break;
for (size_t i = 1; i < DeMux.m_pStreams[AVMEDIA_TYPE_AUDIO].size(); i++)
{
DeMux.SkipStream(DeMux.m_pStreams[AVMEDIA_TYPE_AUDIO][i]->index);
}
}
for (int i = AVMEDIA_TYPE_VIDEO; i < AVMEDIA_TYPE_NB; i++)
{
//已经过滤过了
if ((needvideo&&i==AVMEDIA_TYPE_VIDEO)||(needaudio&&i==AVMEDIA_TYPE_AUDIO))
continue;
for (auto it = DeMux.m_pStreams[i].begin(); it != DeMux.m_pStreams[i].end(); it++)
{
DeMux.SkipStream((*it)->index);
}
}
iRet=Mux.WriteHead(NULL);
if (iRet)
break;
AVPacket pkt;
av_init_packet(&pkt);
while(!iRet)
{
iRet = DeMux.GetStreamPacket(&pkt);//获取码流数据
if (iRet == AVERROR_EOF)
break;
AVStream* pInStream;
AVStream* pOutStream;
//切换到封装的index
if (needvideo&&pkt.stream_index == DeMux.m_pStreams[AVMEDIA_TYPE_VIDEO][0]->index)
{
pInStream = DeMux.m_pStreams[AVMEDIA_TYPE_VIDEO][0];
pOutStream = Mux.m_pStreams[AVMEDIA_TYPE_VIDEO][0];
}
else if (needaudio&&pkt.stream_index == DeMux.m_pStreams[AVMEDIA_TYPE_AUDIO][0]->index)
{
pInStream = DeMux.m_pStreams[AVMEDIA_TYPE_AUDIO][0];
pOutStream = Mux.m_pStreams[AVMEDIA_TYPE_AUDIO][0];
}
else
{
continue;
}
//转换时间轴
av_packet_rescale_ts(&pkt, pInStream->time_base, pOutStream->time_base);
iRet = Mux.WritePacket(&pkt);
}
if (!iRet||iRet==AVERROR_EOF)
iRet=Mux.WriteTrailer();
} while (0);
if (iRet&&iRet!=AVERROR_EOF)
{
if (DeMux.m_eStatus.m_iErrornum)
{
status = DeMux.m_eStatus;
}
else if(Mux.m_eStatus.m_iErrornum)
{
status = Mux.m_eStatus;
}
}
return iRet;
}
从解封装器到封装器的对接,就已经完成了。里面基本所有的信息都可以互相对应起来,出了时间基可能不行。因为历史原因,MP4的流time_base通常是1/90000而mkv是1/1000。这两种情况下,30帧的视频换算的节点是不行的,在90000的时间基中,每一帧是1/3000,而在1000的时间基中是33/1000。为了处理这种情况。我们需要自己设置,通过av_packet_rescale_ts(&pkt, pInStream->time_base, pOutStream->time_base);这个方法进行时间基的转换。不用担心会有问题,因为流的time_base通常情况都不会比codec_context的time_base小(意味着mkv不支持1000帧以上的视频)
我用soui3做了个界面,如果想要自己编译,需要下载soui3的框架,如果不想要ui界面,直接把项目中media文件夹内容拖出来也可以 https://gitee.com/setoutsoft/soui3?_from=gitee_search
提取出来的视频看起来是没问题的
代码已经上传到gitee了 需要自取 https://gitee.com/heweisheng/avformat-convert/tree/master