摘要:前一段时间熟悉了下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-files -
tests
:测试示例; -
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
初始化
解封装的AVFormatContext
FFmpeg内部会自动探测,不需要我们指定。该初始化过程主要涉及两个对外的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 << ((flags & 0x07) + 1)));
while ((type = avio_r8(pb)) != GIF_TRAILER) { //每个block都有各自的标识符,这里判断是否到达结尾
if (avio_feof(pb)) break;
if (type == GIF_EXTENSION_INTRODUCER) { //0x21
int subtype = avio_r8(pb);
if (subtype == GIF_COM_EXT_LABEL) { //Comment Extension
AVBPrint bp;
int block_size;
av_bprint_init(&bp, 0, AV_BPRINT_SIZE_UNLIMITED);
while ((block_size = avio_r8(pb)) != 0) {
avio_read_to_bprint(pb, &bp, block_size);
}
av_dict_set(&s->metadata, "comment", bp.str, 0);
av_bprint_finalize(&bp, NULL);
} else if (subtype == GIF_GCE_EXT_LABEL) { //Graphic Control Extension描述每一帧图像的内容
int block_size = avio_r8(pb);
if (block_size == 4) {
int delay;
avio_skip(pb, 1);
delay = avio_rl16(pb); //求delay总和得到gif的时长
if (delay < gdc->min_delay)
delay = gdc->default_delay;
delay = FFMIN(delay, gdc->max_delay);
duration += delay;
avio_skip(pb, 1);
} else {
avio_skip(pb, block_size);
}
gif_skip_subblocks(pb);
} else {
gif_skip_subblocks(pb);
}
} else if (type == GIF_IMAGE_SEPARATOR) { //Image Descriptor描述当前block的基本宽高等
avio_skip(pb, 8);
flags = avio_r8(pb);
if (flags & 0x80) //跳过局部颜色表
avio_skip(pb, 3 * (1 << ((flags & 0x07) + 1)));
avio_skip(pb, 1);
gif_skip_subblocks(pb);
nb_frames++; //统计帧的数量
} else {
break;
}
}
/* GIF format operates with time in "hundredths of second",
* therefore timebase is 1/100 */
avpriv_set_pts_info(st, 64, 1, 100);
st->codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
st->codecpar->codec_id = AV_CODEC_ID_GIF;
st->codecpar->width = width;
st->codecpar->height = height;
st->start_time = 0;
st->duration = duration;
st->nb_frames = nb_frames;
if (n) {//计算宽高比
st->codecpar->sample_aspect_ratio.num = n + 15;
st->codecpar->sample_aspect_ratio.den = 64;
}
/* jump to start because gif decoder needs header data too */
if (avio_seek(pb, 0, SEEK_SET) != 0)
return AVERROR(EIO);
return 0;
}
read_header
执行完就拿到了流的基本信息,下面最后一步就是校正一些参数,然后调用update_stream_avctx
将部分参数拷贝给解码器的Context等。
3.2.1.2 avformat_find_stream_info
avformat_open_input
之后能够拿到基本的流信息但是具体的流信息,但是媒体文件Header中存储的数据可能和帧中实际的信息不一致,因此需要通过解封装解码获取具体的帧信息来矫正。实际探测信息时回尝试解码一部分帧来获取信息,因此avformat_find_stream_info
可能比较耗时。
avformat_find_stream_info
通过解码流获取详细的流信息,但是到底探测多少内容?下面是FFmpeg中决定探测多少流内容的阈值,可以看到如果没有设置的话这里会使用一些经验值。阈值是探测流的时长,而不是文件大小,流越大解码耗时越久就会越慢。
这个函数非常长大概500行,在详细了解具体实现前,下面是大概调用流程的伪代码:
int avformat_find_stream_info(){
从已有的AVFormatContext和AVStream中获取和探测流相关的参数
for(int i = 0 -> number of streams){
初始化AVCodecParser
拷贝参数到AVCodecContext
find_decoder()
avcodec_open2()
设置一些解码相关的参数
}
for(;;){
for(int i = 0 -> number of streams){
分析出一些探测时长的参数
}
if(readsize > probesize) break;
read_frame_internal()
avpriv_packet_list_put();
if(has_extradata){
extract_extradata();
}
try_decode_frame();
}
if(flush_codecs) flush_codecs();
for(int i = 0 -> number of streams){
计算帧率
}
for(int i = 0 -> number of streams){
add_coded_side_data();
}
}
上面是基本的调用流程,下面一个一个流程详细说明:
探测参数设置
首先便是从已经创建的Context中获取解封装相关的参数,比如需要探测的码流时长等等。
int64_t max_analyze_duration = ic->max_analyze_duration;
max_stream_analyze_duration = max_analyze_duration;
max_subtitle_analyze_duration = max_analyze_duration;
if (!max_analyze_duration) {
max_stream_analyze_duration =
max_analyze_duration = 5*AV_TIME_BASE;
max_subtitle_analyze_duration = 30*AV_TIME_BASE;
if (!strcmp(ic->iformat->name, "flv"))
max_stream_analyze_duration = 90*AV_TIME_BASE;
if (!strcmp(ic->iformat->name, "mpeg") || !strcmp(ic->iformat->name, "mpegts"))
max_stream_analyze_duration = 7*AV_TIME_BASE;
}
尝试解码
探测流是因为需要部分解码因此FFmpeg需要初始化解码器对一部分帧解码从解码的帧中获取流数据。解码的过程就是FFmpeg的基本流程,先准备解码器,然后调用avcodec_open2
打开解码器。环境准备好之后调用read_frame_internal
逐帧读取压缩的数据AVPacket
然后调用try_decode_frame
送给解码器解码。然后根据当前解码的帧参数,更新当前解码器的AVCodecContext
以及AVStream
中的参数。
3.2.2 封装AVFormatContext
初始化
封装时会调用avformat_alloc_output_context2
初始化一个用于写文件的AVFormatContext
。创建AVFormatContext
主要是两部分,首先在堆上分配AVFormatContext
设置默认的参数,然后调用oformat = av_guess_format(NULL, filename, NULL);
获取写文件的AVOutpuFormat
(对比输入时需要AVInputFormat
)。av_guess_format
内部通过遍历FFmpeg支持的所有格式的AVOutputFormat
对比扩展名得到一个分数取分数最高的格式作为当前文件的格式。
while ((fmt = av_muxer_iterate(&i))) {
score = 0;
if (fmt->name && short_name && av_match_name(short_name, fmt->name))
score += 100;
if (fmt->mime_type && mime_type && !strcmp(fmt->mime_type, mime_type))
score += 10;
if (filename && fmt->extensions &&
av_match_ext(filename, fmt->extensions)) {
score += 5;
}
if (score > score_max) {
score_max = score;
fmt_found = fmt;
}
}
3.2.3 打开解码器
打开解码器的基本流程比较简单,初始化Paser,将流中的参数拷贝给AVCodecContext
,然后是根据解码器ID查找解码器,最后就是直接调用avcodec_open2
打开解码器了。
初始化paser就是遍历FFmpeg支持的paser的静态数组,找到后存储到FFStream中。GIF的paser就是ff_gif_parser
。
搜索解码器会调用avcodec_find_decoder
遍历当前FFmpeg中支持的解码器类型,直到找到相同解码器ID的AVCodec
解码器。
准备好解码器和Context就会打开解码器。打开解码器前为了保证线程安全会锁住解码器,锁解码器的锁是一个全局静态锁static AVMutex codec_mutex = AV_MUTEX_INITIALIZER
。打开解码器具体的内容就是设置解码器相关的参数,分配一些解码过程需要用到的内部变量比如AVCodecInternal
等。初始化codec时会创建一个AVCodecDescriptor
codec描述,这个也是从一个内部的全局表格codec_descriptors
中搜索得到的。之后会根据当前codec的类型分别调用ff_encode_preinit
和ff_decode_preinit
做一些基本的初始化,这里面也是对当前codec的一些基本参数设置和一些和codec本身相关的对象的创建。
线程初始化。ff_thread_init
用于初始化codec运行时的解码线程内部会创建多个线程的context并初始化,初始化最终调用的是pthread_***_init
接口进行初始化。解码线程的运行任务为frame_worker_thread
。
err = init_pthread(fctx, thread_ctx_offsets);
if (err < 0) {
free_pthread(fctx, thread_ctx_offsets);
av_freep(&avctx->internal->thread_ctx);
return err;
}
fctx->async_lock = 1;
fctx->delaying = 1;
if (codec->type == AVMEDIA_TYPE_VIDEO)
avctx->delay = src->thread_count - 1;
fctx->threads = av_mallocz_array(thread_count, sizeof(PerThreadContext));
if (!fctx->threads) {
err = AVERROR(ENOMEM);
goto error;
}
for (; i < thread_count; ) {
PerThreadContext *p = &fctx->threads[i];
int first = !i;
err = init_thread(p, &i, fctx, avctx, src, codec, first);
if (err < 0)
goto error;
}
最后调用AVCodec
的初始化函数指针初始化解码器,完成后解锁。下面是GIF初始化解码器的实现,主要就是设置当前解码的参数和分配解码器需要用到的缓存以及打开lzw解码器(其实就是分配一个LZWState
并且设置参数)。
static av_cold int gif_decode_init(AVCodecContext *avctx){
GifState *s = avctx->priv_data;
s->avctx = avctx;
avctx->pix_fmt = AV_PIX_FMT_RGB32;
s->frame = av_frame_alloc();
if (!s->frame)
return AVERROR(ENOMEM);
ff_lzw_decode_open(&s->lzw);
if (!s->lzw)
return AVERROR(ENOMEM);
return 0;
}
3.2.4 打开编码器
打开编码器和打开解码器都是调用avcodec_open2
基本流程差不多,区别是解码器的参数是通过探测得来的,而编码器的参数需要用户自己设置。编码器初始化时调用的函数指针为gif_encode_init
,线程初始化调用的是ff_frame_thread_encoder_init
,线程运行的任务是worker
。
gif_encode_init
只是创建内部使用的一些变量并做参数检查。
static av_cold int gif_encode_init(AVCodecContext *avctx){
GIFContext *s = avctx->priv_data;
if (avctx->width > 65535 || avctx->height > 65535) {
av_log(avctx, AV_LOG_ERROR, "GIF does not support resolutions above 65535x65535\n");
return AVERROR(EINVAL);
}
s->transparent_index = -1;
s->lzw = av_mallocz(ff_lzw_encode_state_size);
s->buf_size = avctx->width*avctx->height*2 + 1000;
s->buf = av_malloc(s->buf_size);
s->tmpl = av_malloc(avctx->width);
if (!s->tmpl || !s->buf || !s->lzw)
return AVERROR(ENOMEM);
if (avpriv_set_systematic_pal2(s->palette, avctx->pix_fmt) < 0)
av_assert0(avctx->pix_fmt == AV_PIX_FMT_PAL8);
return 0;
}
3.2 解封装
av_read_frame
用于从已经打开的文件中读取未经过解码的码流AVPacket
,对于视频帧就是一帧的压缩帧,对于音频帧如果音频是固定大小的话则可以是多帧,否则也是一帧。av_read_frame
内部读取码流时调用avpriv_packet_list_get
和av_read_frame_internal
。
avpriv_packet_list_get
比较简单就是从当前媒体的PackList中取出一帧。av_read_frame
的函数实现比较长,其大致流程为:
- 调用
ff_read_packet
读取一帧码流; - 如果1步骤失败则调用
parse_packet
刷新解析器,否则继续到步骤3; - 如果当前context需要更新解码器context,则将internal的解码器context更新到stream的解码器context;
- 如果成功拿到预期的帧则下一步,否则跳转到步骤1;
- 后续的工作就是解析元数据,计算需要丢弃的数据大小等。
ff_read_packet
会先检查缓冲区是否有帧没有的话就会调用s->iformat->read_packet
即对应个是的解析码流的函数进行解码。
GIF图解封装就是调用的gif_read_packet
,解封装首先就是跳过图像中的头信息,比如Image Descriptor等。然后不断遍历内部的流寻找一帧图像的Block,找到后根据当前Block的size读取数据组装一个AVPacket
,设置AVPacket
的参数,然后更新GIFDemuxContext
中存储的当前解封装读取到的位置,dt
等参数返回帧。
3.3 解码
avcodec_send_packet
首先是检查解码器的合法性以及数据是否为空,如果输入数据和Context符合要求就会删除AVcodecContext->internal->buffer_pkt
中缓存的一帧码流数据,将输入的Packet拷贝到该buffer上。av_bsf_send_packet
只是拷贝增加输入的Packet引用计数到AVBSFInternal->buffer_pkt
,最后如果缓存的buffer_frame
是空的就会调用decode_receive_frame_internal
解码帧,该过程根据配置项可谓同步也可为异步。
3.3.1 decode_receive_frame_internal
decode_receive_frame_internal
内就是真正的调用解码流程,如果解码器的receive_frame
函数指针不为空就直接调用解码器的receive_frame
进行解码该过程是同步的。否则就会调用decode_simple_receive_frame
进行解码。解码完成后需要根据解码的数据和当前解码器Context的一些pts相关的值计算当前帧的具体pts和dts,另外如果有指定FrameDecodeData
还会调用后处理流程fdd->post_process
进行解码。
3.3.2 decode_simple_receive_frame
decode_simple_receive_frame
主要是调用decode_simple_internal
进行解码。这里使用的Packet就是前面存储在AVBSFInternal
中的buffer_pkt
。然后就是实际调用解码的流程,如果没有配置解码线程就直接调用每个解码器对应的函数指针的avctx->codec->decode
直接同步拿到帧。否则就会调用ff_thread_decode_frame
进行多线程解码。
FFmpeg中每种格式,解码器等都有自己的描述结构,比如下面是gif的解码器描述。
static const AVClass decoder_class = {
.class_name = "gif decoder",
.item_name = av_default_item_name,
.option = options,
.version = LIBAVUTIL_VERSION_INT,
.category = AV_CLASS_CATEGORY_DECODER,
};
const AVCodec ff_gif_decoder = {
.name = "gif",
.long_name = NULL_IF_CONFIG_SMALL("GIF (Graphics Interchange Format)"),
.type = AVMEDIA_TYPE_VIDEO,
.id = AV_CODEC_ID_GIF,
.priv_data_size = sizeof(GifState),
.init = gif_decode_init,
.close = gif_decode_close,
.decode = gif_decode_frame,
.capabilities = AV_CODEC_CAP_DR1,
.caps_internal = FF_CODEC_CAP_INIT_THREADSAFE |
FF_CODEC_CAP_INIT_CLEANUP,
.priv_class = &decoder_class,
};
ff_thread_decode_frame
内都是通过锁和条件变量进行同步的。首先根据当前的状态获取一个解码线程的Context,然后将当前的Packet提交到该线程上,提交就是将一帧数据增加引用让解码Context的avpkt
也占用输入帧的引用计数,提交完成就会发送信号通知在等待的解码线程启动。
解码线程起始在avcodec_open2
的时候就已经创建好了,在wait数据。具体的执行函数就是frame_worker_thread
,该函数内就是调用codec->decode
进行解码解码完成后就会发送通知到ff_thread_decode_frame
中取解码完的帧。令条件if (!p->avctx->thread_safe_callbacks && ( p->avctx->get_format != avcodec_default_get_format || p->avctx->get_buffer2 != avcodec_default_get_buffer2))
为A,如果A为true则当前线程是会被阻塞的,完全就是同步运行,否则就是多线程的。
if (!p->avctx->thread_safe_callbacks && (
p->avctx->get_format != avcodec_default_get_format ||
p->avctx->get_buffer2 != avcodec_default_get_buffer2)) {
while (atomic_load(&p->state) != STATE_SETUP_FINISHED && atomic_load(&p->state) != STATE_INPUT_READY) {
int call_done = 1;
pthread_mutex_lock(&p->progress_mutex);
while (atomic_load(&p->state) == STATE_SETTING_UP)
pthread_cond_wait(&p->progress_cond, &p->progress_mutex);
switch (atomic_load_explicit(&p->state, memory_order_acquire)) {
case STATE_GET_BUFFER:
p->result = ff_get_buffer(p->avctx, p->requested_frame, p->requested_flags);
break;
case STATE_GET_FORMAT:
p->result_format = ff_get_format(p->avctx, p->available_formats);
break;
default:
call_done = 0;
break;
}
if (call_done) {
atomic_store(&p->state, STATE_SETTING_UP);
pthread_cond_signal(&p->progress_cond);
}
pthread_mutex_unlock(&p->progress_mutex);
}
}
3.3.2 avcodec_receive_frame
avcodec_receive_frame
比较简单先检查buffer_frame
有没有数据,有的话就直接返回,没有即调用decode_receive_frame_internal
进行解码。
3.3.3 gif_decode_frame
gif_decode_frame
中会将码流送给解码器进行解码然后将得到的数据填充到AVFrame
返回给上层。代码中前面一大段都是读取当前Block的图像信息比如Image Header这些,实际进行解码的是gif_parse_next_image
,其内部会根据当前block的类型调用具体的解码函数,比如图像就是调用gif_read_image
进行解码。
static int gif_parse_next_image(GifState *s, AVFrame *frame){
while (bytestream2_get_bytes_left(&s->gb) > 0) {
int code = bytestream2_get_byte(&s->gb);
int ret;
av_log(s->avctx, AV_LOG_DEBUG, "code=%02x '%c'\n", code, code);
switch (code) {
case GIF_IMAGE_SEPARATOR:
return gif_read_image(s, frame);
case GIF_EXTENSION_INTRODUCER:
if ((ret = gif_read_extension(s)) < 0)
return ret;
break;
case GIF_TRAILER:
/* end of image */
return AVERROR_EOF;
default:
/* erroneous block label */
return AVERROR_INVALIDDATA;
}
}
return AVERROR_EOF;
}
gif_read_image
解码过程中首先就是解析当前帧的局部颜色表以及GIF的存储模式,如果没有的话就使用全局的颜色表。参数解析完后直接调用ff_lzw_decode
解码读取到的LZW编码流,最后将索引映射根据颜色表映射会帧图像。
3.4 编码
avcodec_send_frame
用于在编码时将一帧raw数据发送给编码器,其基本的调用流程比较简单,主要工作就是将输入的数据ref到Internal Frame上。
avcodec_send_frame
首先检查当前的codec是不是编码器且是否打开,并且检查codec中的buffer是否有数据没有,有的话就意味着上一帧的数据还没处理完需要等待这一帧处理完才能继续发送。
if (!avcodec_is_open(avctx) || !av_codec_is_encoder(avctx->codec))
return AVERROR(EINVAL);
if (avci->draining)
return AVERROR_EOF;
if (avci->buffer_frame->data[0])
return AVERROR(EAGAIN);
然后是根据输入的frame是否为空来设置标志位,如果为空就表示是最后一帧数据后续的数据就无效了。能够看到在最后如果codec中的packet buffer是空的就会尝试获取一帧packet。
if (!frame) {
avci->draining = 1;
} else {
ret = encode_send_frame_internal(avctx, frame);
if (ret < 0)
return ret;
}
if (!avci->buffer_pkt->data && !avci->buffer_pkt->side_data) {
ret = encode_receive_packet_internal(avctx, avci->buffer_pkt);
if (ret < 0 && ret != AVERROR(EAGAIN) && ret != AVERROR_EOF)
return ret;
}
encode_send_frame_internal
比较简单,主要就是针对音频数据进行参数检查并对数据进行填充,最后调用av_frame_ref
将输入的数据的引用计数+1、
3.4.1 avcodec_receive_packet
3.4.1.1 基本流程
首先是检查当前codec是否为编码器并且是否打开,如果是就继续。然后检查codec中的packet buffer是否有数据有的话就直接返回了,不然就会调用encode_receive_packet_internal
。
int attribute_align_arg avcodec_receive_packet(AVCodecContext *avctx, AVPacket *avpkt){
AVCodecInternal *avci = avctx->internal;
int ret;
av_packet_unref(avpkt);
if (!avcodec_is_open(avctx) || !av_codec_is_encoder(avctx->codec))
return AVERROR(EINVAL);
if (avci->buffer_pkt->data || avci->buffer_pkt->side_data) {
av_packet_move_ref(avpkt, avci->buffer_pkt);
} else {
ret = encode_receive_packet_internal(avctx, avpkt);
if (ret < 0)
return ret;
}
return 0;
}
encode_receive_packet_internal
首先就是参数检查,然后根据codec的函数指针设置看调用哪个流程获取编码流。encode_simple_receive_packet
就是个while循环调用encode_simple_internal
直到获取编码数据或者出错为止。
if (avctx->codec->receive_packet) {
ret = avctx->codec->receive_packet(avctx, avpkt);
if (ret < 0)
av_packet_unref(avpkt);
else
// Encoders must always return ref-counted buffers.
// Side-data only packets have no data and can be not ref-counted.
av_assert0(!avpkt->data || avpkt->buf);
} else
ret = encode_simple_receive_packet(avctx, avpkt);
encode_simple_internal
除了前面一大坨参数检查,主要救赎下面这块儿,看是利用多线程编码还是利用codec的encode接口编码。
if (CONFIG_FRAME_THREAD_ENCODER &&
avci->frame_thread_encoder && (avctx->active_thread_type & FF_THREAD_FRAME))
/* This might modify frame, but it doesn't matter, because
* the frame properties used below are not used for video
* (due to the delay inherent in frame threaded encoding, it makes
* no sense to use the properties of the current frame anyway). */
ret = ff_thread_video_encode_frame(avctx, avpkt, frame, &got_packet);
else {
ret = avctx->codec->encode2(avctx, avpkt, frame, &got_packet);
if (avctx->codec->type == AVMEDIA_TYPE_VIDEO && !ret && got_packet &&
!(avctx->codec->capabilities & AV_CODEC_CAP_DELAY))
avpkt->pts = avpkt->dts = frame->pts;
}
3.4.1.2 多线程
编码的线程和解码的线程一样都是在avcodec_open2
时创建的,编码是调用ff_frame_thread_encoder_init
创建的,其中主要就是调用pthread的接口创建线程和相关的参数,可以看到其工作的函数为static void * attribute_align_arg worker(void *v)
,编码过程中有多个线程每个线程都运行一个worker任务,通过信号量来进行消息的同步。该任务中最终会调用avctx->codec->encode2
对数据进行编码。而所有的数据交互都是通过ThreadContext
进行的,无论是输入数还是输出的数据还是消息同步都是通过该Context进行的。
typedef struct{
AVCodecContext *parent_avctx;
pthread_mutex_t buffer_mutex;
pthread_mutex_t task_fifo_mutex; /* Used to guard (next_)task_index */
pthread_cond_t task_fifo_cond;
unsigned max_tasks;
Task tasks[BUFFER_SIZE];
pthread_mutex_t finished_task_mutex; /* Guards tasks[i].finished */
pthread_cond_t finished_task_cond;
unsigned next_task_index;
unsigned task_index;
unsigned finished_task_index;
pthread_t worker[MAX_THREADS];
atomic_int exit;
} ThreadContext;
当数据到达时主线程会先拷贝数据然后发送信号量signal给任务线程,任务线程拿到消息后编码完成后给主线程发信号finish,主线程取走数据。
3.4.2 gif_encode_frame
GIF编码调用的是gif_encode_frame
,内部实际调用的gif_image_write_image
。GIF编码和解码的流程基本相反,除了写Block的流信息外,先将当前图像的颜色映射根据颜色表映射到具体的索引,然后调用ff_lzw_encode
对流进行编码。
for (y = 0; y < height; y++) {
memcpy(s->tmpl, ptr, width);
for (x = 0; x < width; x++)
if (ref[x] == ptr[x])
s->tmpl[x] = trans;
len += ff_lzw_encode(s->lzw, s->tmpl, width);
ptr += linesize;
ref += ref_linesize;
3.5 封装
3.5.1 avformat_write_header
avformat_write_header
比较简单直接调用的对应格式的write_header
的函数指针。GIF的write_header
做的事情比较少。
static int gif_write_header(AVFormatContext *s){
if (s->nb_streams != 1 ||
s->streams[0]->codecpar->codec_type != AVMEDIA_TYPE_VIDEO ||
s->streams[0]->codecpar->codec_id != AV_CODEC_ID_GIF) {
av_log(s, AV_LOG_ERROR,
"GIF muxer supports only a single video GIF stream.\n");
return AVERROR(EINVAL);
}
avpriv_set_pts_info(s->streams[0], 64, 1, 100);
return 0;
}
3.5.2 av_interleaved_write_frame
首先就是根据输入数据是否为空选择调用的函数,如果为空就会调用interleaved_write_packet
刷新数据,否则调用write_packets_common
写数据。
write_packets_common
中,check_packet
检查输入的数据和期望写入的媒体流是否能够对上。prepare_input_packet
对输入数据进行修正,如果pts和dts其中之一为NOPTS则设置为对方的值,以及如果设置了is_intra_only则每一帧都会设置标志位AV_PKT_FLAG_KEY
。而check_bitstream
就是调用s->oformat->check_bitstream
检查流是否符合对应的格式。最后才是调用write_packet_common
进行写数据。如果有设置filter的话就调用write_packets_from_bsfs
处理。
write_packet_common
会根据输入的参数是否需要交织存储来调用具体的函数写packet。非交织的情况下就会调用write_packet
,该函数内部实际调用的s->oformat->write_packet
和s->oformat->write_uncoded_frame
写文件,后者处理裸流。
interleaved_write_packet
内,如果AVOuputFormat设置了对应的函数指针则直接调用s->oformat->interleave_packet
写文件,否则就用FFmpeg提供的ff_interleave_packet_per_dts
。我们重点看下这个函数实现。
3.5.2.1 ff_interleave_packet_per_dts
ff_interleave_packet_per_dts
只是针对当前的两个流的packet的时间戳进行比较避免在文件存储过程中距离太远导致解封转时要频繁seek文件。最终封装文件写入到磁盘还是需要write_packet
。该函数首先将送入的pkt插入到缓存队列中,然后在从当前缓存队列中选出一帧返回调用write_packet
进行写入。
在看ff_interleave_add_packet
函数的实现之前,我们先简单看下帧比较函数interleave_compare_dts
的实现,该函数用来比较两个packet的dts。如果非音频流就是调用的av_compare_ts
进行比较,否则会根据当前音频流是否有preload去除preload的偏移:
int preload = st ->codecpar->codec_type == AVMEDIA_TYPE_AUDIO;
int preload2 = st2->codecpar->codec_type == AVMEDIA_TYPE_AUDIO;
if (preload != preload2) {
int64_t ts, ts2;
preload *= s->audio_preload;
preload2 *= s->audio_preload;
//preload不同时需要减掉preload的偏移
ts = av_rescale_q(pkt ->dts, st ->time_base, AV_TIME_BASE_Q) - preload;
ts2= av_rescale_q(next->dts, st2->time_base, AV_TIME_BASE_Q) - preload2;
if (ts == ts2) {
ts = ((uint64_t)pkt ->dts*st ->time_base.num*AV_TIME_BASE - (uint64_t)preload *st ->time_base.den)*st2->time_base.den
- ((uint64_t)next->dts*st2->time_base.num*AV_TIME_BASE - (uint64_t)preload2*st2->time_base.den)*st ->time_base.den;
ts2 = 0;
}
comp = (ts2 > ts) - (ts2 < ts);
}
重点就是下面的代码,从当前buffer中找到当前帧的插入位置然后插入到packet的链表中。
if (st->internal->last_in_packet_buffer) {
next_point = &(st->internal->last_in_packet_buffer->next);
} else {
next_point = &s->internal->packet_buffer;
}
//省略部分代码.......
if (*next_point) {
if (chunked && !(pkt->flags & CHUNK_START))
goto next_non_null;
if (compare(s, &s->internal->packet_buffer_end->pkt, pkt)) {
while ( *next_point
&& ((chunked && !((*next_point)->pkt.flags&CHUNK_START))
|| !compare(s, &(*next_point)->pkt, pkt)))
next_point = &(*next_point)->next;
if (*next_point)
goto next_non_null;
} else {
next_point = &(s->internal->packet_buffer_end->next);
}
}
插入成功后回到ff_interleave_packet_per_dts
中,从当前的packet链表的头结点拿到一阵返回给write_packet
写入。
3.5.2.2 gif_write_packet
gif_write_packet
比较简单就是根据当前的流信息写GCE等基本的BLOCK信息。
/* "NETSCAPE EXTENSION" for looped animation GIF */
if (gif->loop >= 0) {
avio_w8(pb, GIF_EXTENSION_INTRODUCER); /* GIF Extension code */
avio_w8(pb, GIF_APP_EXT_LABEL); /* Application Extension Label */
avio_w8(pb, 0x0b); /* Length of Application Block */
avio_write(pb, "NETSCAPE2.0", sizeof("NETSCAPE2.0") - 1);
avio_w8(pb, 0x03); /* Length of Data Sub-Block */
avio_w8(pb, 0x01);
avio_wl16(pb, (uint16_t)gif->loop);
avio_w8(pb, 0x00); /* Data Sub-block Terminator */
}
delay_pos = gif_parse_packet(s, pkt->data + off, pkt->size - off);
if (delay_pos > 0 && delay_pos < pkt->size - off - 2) {
avio_write(pb, pkt->data + off, delay_pos);
avio_wl16(pb, gif_get_delay(gif, pkt, new_pkt));
avio_write(pb, pkt->data + off + delay_pos + 2, pkt->size - off - delay_pos - 2);
} else {
avio_write(pb, pkt->data + off, pkt->size - off);
}
3.5.3 av_write_trailer
av_write_trailer
就做了两件是刷新缓冲区和写尾。GIF写尾调用的static int gif_write_trailer(AVFormatContext *s)
。文章来源:https://www.toymoban.com/news/detail-690479.html
static int gif_write_trailer(AVFormatContext *s){
GIFContext *gif = s->priv_data;
AVIOContext *pb = s->pb;
if (!gif->prev_pkt)
return AVERROR(EINVAL);
gif_write_packet(s, NULL);
if (!gif->have_end)
avio_w8(pb, GIF_TRAILER);
av_packet_free(&gif->prev_pkt);
return 0;
}
3.6 销毁
完成任务后调用各自的现场清理函数。文章来源地址https://www.toymoban.com/news/detail-690479.html
到了这里,关于FFmpeg5.0源码阅读——FFmpeg大体框架(以GIF转码为示例)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!