[FFMPEG开发总结]1. 了解AVFormatContext(附带api封装跟转封装demo)

  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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值