摘要:前一段时间熟悉了下FFmpeg主流程源码实现,对FFmpeg的整体框架有了个大概的认识,因此在此做一个笔记,希望以比较容易理解的文字描述FFmpeg本身的结构,加深对FFmpeg的框架进行梳理加深理解,如果文章中有纰漏或者错误欢迎指出。本文描述了FFmpeg编解码框架的工程结构,基本构成以及大体的调用流程。因为FFmpeg的滤镜是相对独立的一个模块,因此在此不会进行描述。
关键字:FFmpeg,Framework
阅读须知:阅读本文前,你首先需要了解最基本的音视频处理相关的知识,对于这些知识你至少需要最基本的了解,比如知道什么是容器,什么是编解码器,以及大概的工作流程即可。
FFmepg是一个用C语言实现的多媒体封装、解封转、编解码开源框架,支持了多种IO协议操作,媒体封装格式的封装与解封装以及编解码格式编解码器(包括硬解和软解)。任何软件都可以在FFmpeg的License范围内合理地基于FFmpeg进行开发。FFmpeg有两种开源协议:
- GPL,该协议是具有传染性的,如果使用了GPL部分的代码(FFmpeg可以配置是否开关这部分代码)对应的软件也必须开源否则有法律风险;
- LGPL,允许以动态发布的形式使用,即将FFmpeg编译为动态库使用,但是修改到了FFmpeg部分的代码,修改的部分也需要开源,一般商业软件都会采用这种方式来进行商业软件的开发。
FFmpeg is the leading multimedia framework, able to decode, encode, transcode, mux, demux, stream, filter and play pretty much anything that humans and machines have created. It supports the most obscure ancient formats up to the cutting edge. No matter if they were designed by some standards committee, the community or a corporation. It is also highly portable: FFmpeg compiles, runs, and passes our testing infrastructure FATE across Linux, Mac OS X, Microsoft Windows, the BSDs, Solaris, etc. under a wide variety of build environments, machine architectures, and configurations.
1 FFmpeg工程
本小节简单描述下FFmpeg的工程结构相关的内容,以期对FFmpeg工程本身的基本构成有一个基本的认识。
1.1 FFmpeg工程结构
FFmpeg本身的目录结构比较清晰,我们从目录名称中基本就能看出该目录下可能包含哪些文件具体用来干什么。
.:当前目录下存储的是一些编译和项目相关的配置文件,比如Makefile,License等;compat:兼容文件;doc:文档,以及一些FFmpeg使用的示例,如果学习FFmpeg的话强烈建议阅读示例;ffbuild:编译相关的一些文件,比如依赖选项等等;fftools:可以编译成可执行文件的一些工具实现,比如ffplay,ffmpeg,ffprobe等工具;libavcodec:编解码核心,编解码相关的文件都存放在这里,比如h264dec.c等;libavdevice:设备相关,比如DShow等;libavfilter:滤镜特效处理;libavformat:IO操作以及封装格式的封装和转封装等处理;libavutil:工具库,比如一些基本的字符串操作,图像操作等;libavpostproc:一些效果后处理相关的内容,一般通过filter处理;libswresample:音频重采样处理;libswscale:视频缩放、颜色空间转换以及色调映射等;presets:编解码器的配置文件,参考FFmpeg-Present-filestests:测试示例;tools:一些简单的工具。
2 FFmpeg架构
2.1 FFmpeg的总体架构

FFmpeg各个模块是互相独立的,都可以单独使用,比如解封装器只用来对媒体进行解封装或者封装拿到编码器的裸流,或者编解码器直接对裸流数据进行编解码,亦或者使用工具集对已经解码完的数据尽兴处理。
编解码模块支持多种不同编解码器,所有的编解码器所使用的参数和当前编解码器相关的Context都是使用AVCodecContext描述。而FFmpeg中每个具体的解码器都有一个静态的AVCodec描述当前解码器如何解码,这个是有一套统一的接口来定义的。上层拿到AVCodecContext和AVCodec就可以初始化解码器进行解码了,只不过使用FFmpeg提供的解码接口更加方便。FFmpeg并没有硬件解码器归类的AVCodec下面,而是在其下层另外规定了一套AVHWAccel,通过AVCodec来描述该硬件解码器。
封装和解封装支持多种不同的媒体文件类型,FFmpeg中讲一个文件抽象为AVFormatContext,而内部分别将输入流和输出流分别抽象为AVInputFormat,AVOutputFormat。AVInputFormat,AVOutputFormat用来描述当前媒体文件的相关参数以及对媒体文件进行封装和解封装,而具体的操作通过AVIO来进行。AVIO抽象了具体的文件IO操作,类似编解码器每种类型的输入流都有各自的描述,封装器和解封装器同理。
工具集也是独立的,只是一些工具函数的集合。
滤镜用来对裸数据进行一些特效上的处理。(本文不会过多讨论滤镜)
2.2 代码结构
FFmpeg内有一系列的基础组件,一部分是对一些native接口的封装来保证对上层的接口的一致性,一部分是为了方便内部的使用提供的基础接口。比如av_malloc,av_mallocz系列就是对内存分配接口的封装以保证内存对齐。
FFmpeg中有比较多的基础组件,有一些不了解也不影响我们使用FFmpeg,只需要在使用时去了解就可以,但是另一部分是必须了解的,比如AVOption,AVBuffer等。
AVOption
AVOption是FFmpeg中设置参数的一个基本抽象结构。因为FFmpeg是一个支持多种封装解封装器,编解码器的框架,而不同的外部库需要的参数各不相同,因此利用AVOption来封装一个基本的key-value结构来获取和设置对应模块的参数。AVOption本身就是一个key-value项,可以理解为C++中map中的项pair,而其中name就是key,default_val就是value。而在实际使用中所有的参数是存储在AVClass中的AVOption数组中,而需要设置参数的模块会在Context结构体开头设置一个AVClass的指针来表示当前模块的参数,FFmpeg通过搜索该数组来获取和设置对应模块的参数(该列表的搜索是线性搜索的,由于一般参数不会太多,即便几百个参数线性搜索也不会花费太多时间)。
typedef struct AVClass {
const char* class_name; //AVClass所属类的名称
const char* (*item_name)(void* ctx); //获取AVClass所属类名称的函数指针,有些实现会直接返回AVClass->class_name
const struct AVOption *option; //当前类的参数,没有就置为NULL
int version; //当前字段创建的版本,可用于版本控制,This is used to allow fields to be added without requiring major version bumps everywhere.
int log_level_offset_offset; //AVClass所属结构体中log_level_offset相对于其首地址的偏移,0表示没有该成员
int parent_log_context_offset; //当前Context中存储parent context的偏移量
void* (*child_next)(void *obj, void *prev); //AVOptions中下一个可用的参数
AVClassCategory category; //当前类的类别
AVClassCategory (*get_category)(void* ctx); //获取当前Context类别的函数指针
//查询对应选项的范围,虽然定义了但是FFmpeg源码中好像没有API用到
int (*query_ranges)(struct AVOptionRanges **, void *obj, const char *key, int flags);
/**
* Iterate over the AVClasses corresponding to potential AVOptions-enabled children.
* @param iter pointer to opaque iteration state. The caller must initialize *iter to NULL before the first call.
* @return AVClass for the next AVOptions-enabled child or NULL if there are no more such children.
* @note The difference between child_next and this is that child_next iterates over _already existing_ objects, while child_class_iterate iterates over _all possible_ children.
const struct AVClass* (*child_class_iterate)(void **iter);
} AVClass;
FFmpeg中Context中的一部分参数是自身持有的也可以通过AVOption来设置,其基本的原理就是通过对应成员的固定偏移来读写。
typedef struct AVFormatContext {
//A class for logging and @ref avoptions. Set by avformat_alloc_context(). Exports (de)muxer private options if they exist.
const AVClass *av_class;
//省略大部分代码
}AVFormatContext;
AVBuffer
AVBuffer,AVBufferPool是FFmpeg比较简单的一种基于引用技术实现的FIFO内存池
。AVBufferPool是一个以单链表形式实现的栈式内存池。其基本过程就是如果链表非空则出栈头结点,否则申请内存时就创建一个AVBufferRef返回给用户,用户释放时就会将节点入栈到头结点,并且申请和释放内存是线程安全的。AVBufferPool就是一个空闲链表栈,通过指定对应的AVBufferRef的释放函数为pool_release_buffer来对内存进行管理。
对于一个刚初始化的内存池,连续申请两个Buffer就是下面这种状态:

连续申请3个buffer,再释放2个就是下面这种状态(红色为链表的连接线):

其他一些需要详细注意的就是FFmpeg中存储数据的AVFrame和AVPacket,分别是用来存储裸数据和编码的数据流。
FFmpeg虽然是用C语言写的但是其基本的实现思想是按照OOP的思想实现的,每个具体的格式都有自己的Context和描述类然后通过函数指针来描述具体实例的实际实现,也就是上面描述的Context->Context->Context->....>Implementation这种形式,为了对当前处理的对象统一抽象就会有一个Context来描述。而每个Context都有一个AVClass和opaue来描述当前结构的参数和独有的一些数据,通过这种方式保持了接口的统一的同时,又能兼顾差异性。一般的Context接口如下:
struct ***Context{
const AVClass *av_class;
//省略部分可能的成员
void *private_data;
//省略部分可能的成员
}
FFmpeg虽然是用C语言写的但是其基本的实现思想是按照OOP的思想实现的,每个具体的格式都有自己的Context和描述类然后通过函数指针来描述具体实例的实际实现,也就是上面描述的Context->Context->Context->....>Implementation这种形式,为了对当前处理的对象统一抽象就会有一个Context来描述。而每个Context都有一个AVClass和opaue来描述当前结构的参数和独有的一些数据,通过这种方式保持了接口的统一的同时,又能兼顾差异性。

2.3 调用流程
FFmpeg的核心就是封装/解封装和解码那一套,下面的流程图是一个大概,有一部分调用被省略了。

3 Gif转码
上面大概描述了下FFmpeg的框架结构和基本的调用流程,但是介绍的比较粗糙,可能一个具体的例子更容易理解。因此下面会针对GIF图像的转码流程进行比较详细的流程跟踪FFmpeg的详细调用流程,以及数据处理。选择GIF的原因是GIF图像的格式和编解码相比其他格式相对比较简单,可以让我们更加关注主要的流程而不是具体某个格式的解封装或者解码。当然下面也会涉及的GIF的封装解封装,编解码过程,因此为了更加流畅的阅读,最好提前了解下GIF文件格式和GIF编解码。
3.1 大体流程
总体的调用流程如下,一般的转码的基本流程:

一个流媒体文件的转码基本上包含了FFmpeg的主要内容,从该过程入手我们能清晰的看到FFmpeg内部的实现逻辑。首先有一个流媒体文件,比如Mp4,MKV等等,我们期望是将其编码封装为另一种格式比如HEVC/MP4等等。
首先是一些环境的准备,比如打开媒体文件,这个时候FFmpeg会根据文件的流内容探测当前文件可能是什么格式,来确定使用哪种解封装器。然后打开解码器和编码器,解码器的参数是通过第一步探测到的,而编码器的参数需要根据你的需要设置。
文件和解码器已经打开就可以开始解码了。因为流信息是按照帧存储的,因此需要不断读取一帧一帧的压缩的流信息送给解码器进行解码。未压缩的数据存储在AVPacket中,而解压完的裸数据存储在AVFrame中。拿到裸数据后就可以将该数据发送给编码器进行编码,最后送到封装器进行封装存储就得到了一个完整的流媒体文件。
3.2 初始化Context
FFmpeg中一个AVFormatContext表示一个媒体文件的抽象,AVCodecContextt,AVCodec表示编解码参数和编解码器的抽象,因此分别初始过程需要初始化读和写文件的AVFormatContext,编码和解码的``AVCodecContext````,以及打开编解码器。
3.2.1 解封装AVFormatContext初始化
解封装的AVFormatContextFFmpeg内部会自动探测,不需要我们指定。该初始化过程主要涉及两个对外的API:avformat_open_input,avformat_find_stream_info,前者用来打开文件,后者用来进行流媒体信息探测。
3.2.1.1 avformat_open_input
avformat_open_input
avformat_open_input会打开文件句柄,探测当前文件的媒体格式,读取基本的流媒体格式信息。
avformat_open_input首先会在堆上分配一个AVFormatContext(下面称之为媒体句柄)并将用户自定义个一些options拷贝到该Context中。
此时的媒体句柄只是一个带有输入参数和文件路径的空壳,需要进一步的确认具体的媒体格式。之后会调用av_probe_input_format2(记住这个API,这里如果探测失败后续还会继续调用),实际上内部调用的是av_probe_input_format3对媒体文件探测检测。探测的方式比较粗暴就是遍历当前FFmpeg支持的所有媒体格式然后调用对应媒体格式的read_probe函数指针拿到一个分值,分值最高的那个就是当前媒体文件的格式。此时就会拿到对应文件的AVInputFormat赋值给媒体句柄中的iformat。伪代码如下:
int maxscore = 0;
AVInputFormat *tmp, *ret;
while(ret in [FFmpeg 支持的格式列表]){
int score = ret->read_probe();
if(score > maxscore){
tmp = ret;
maxscore = score;
}
}
return ret;
因为上面的probe是第一次调用还没有打开文件IO无法访问文件数据,因此大概率失败,那为什么还要在打开文件IO前调用?因为对于一些设置了
AVFMT_NONFILE的输入比如DShow等就不需要打开文件IO进行。
然后就是调用媒体句柄中的io_open函数指针打开流,该指针是在创建媒体句柄时设置的默认函数指针io_open_default。打开流是首先需要确认流的类型,基本过程和媒体探测流程差不多,根据文件名遍历FFmpeg支持的所有流格式拿到当前格式的URLProtocol,比如本地文件就是ff_file_protocol,确定流类型后就可以调用具体的函数指针url_open打开媒体文件了。对于本地文件的话就是posix那套文件操作,比如open,lseek,fstat等,之后文件读取也一样。打开文件后的文件句柄并不是URLProtocol的成员,而是存储在priv_data中,这也是FFmpeg中规避差异化的基本做法。
通过上述的操作我们只是拿到了URLContext,还需要拿到AVIOContext。创建AVIOContext的过程比较简单,就是堆上申请块儿对应的内存设置必要的参数然后返回。需要注意的是此时会申请一会儿缓冲区,存放在VIOContext供后续读写文件使用。
拿到AVIOContext后也就意味着IO已经成功打开,如果此时发现媒体句柄中没有iformat就会调用av_probe_input_buffer2再次探测。av_probe_input_buffer2内部会不断读取文件内容然后调用上面提到的APIav_probe_input_format2对文件内容进行探测,直到确定媒体文件格式或者达到最大的probesize为止。
GIF的read_probe比较简单,就是读取头部的标记确认是否为GIF文件。
static int gif_probe(const AVProbeData *p){
/* check magick */
if (memcmp(p->buf, gif87a_sig, 6) && memcmp(p->buf, gif89a_sig, 6))
return 0;
/* width or height contains zero? */
if (!AV_RL16(&p->buf[6]) || !AV_RL16(&p->buf[8]))
return 0;
return AVPROBE_SCORE_MAX;
}
到目前为止我们只是打开了IO,确认了媒体类型,但是媒体的基本信息比如宽高等还不清楚,剩下的工作就是调用iformat->read_header读取一些基本的信息写入到媒体句柄中。至此,媒体流打开的工作就已经结束了。
gif_read_header
下面通过详细的注释描述读取header的过程:
static int gif_read_header(AVFormatContext *s)
{
GIFDemuxContext *gdc = s->priv_data;
AVIOContext *pb = s->pb;
AVStream *st;
int type, width, height, ret, n, flags;
int64_t nb_frames = 0, duration = 0;
if ((ret = resync(pb)) < 0) //跳过开头89a和87a的标识符
return ret;
gdc->delay = gdc->default_delay;
width = avio_rl16(pb); //gif中宽高存储在开头,且分别占2个字节
height = avio_rl16(pb);
flags = avio_r8(pb); //读取标志位
avio_skip(pb, 1); //背景色索引,目前不需要就跳过
n = avio_r8(pb); //像素比
if (width == 0 || height == 0)
return AVERROR_INVALIDDATA;
st = avformat_new_stream(s, NULL); //动态图一定只有一个视频流,这里只需要创建一个即可
if (!st) return AVERROR(ENOMEM);
if (flags & 0x80) //跳过全局颜色表,全局颜色表只有在解码时有用
avio_skip(pb, 3 * (1 << (

&spm=1001.2101.3001.5002&articleId=132513704&d=1&t=3&u=9c75374f027e4ee499dab0a92c7775ea)
3318

被折叠的 条评论
为什么被折叠?



