一、简介
- 在音视频文件编解码中有三个重要的阶段,对于解码:先将文件读取到内存中,再解封装,再解码;对于编码过程也是一样的,这里边涉及三个结构体:
- AVIOContext主要是定义如何读取文件(从本地文件中,还是网络流)
- AVFormatContext主要是解封装(AVInputFormat)或者封装(AVOutputFormat)协议
- AVCodecContext则是解码、编码协议,包含了编解码器实体对象
- 本文对AVFormatContext、AVIOContext、AVStream三个结构体的做一介绍,主要涉及解封装过程,不涉及解码流程,因此暂时不涉及AVCodecContext
- AVFormatContext 是⼀个全局数据结构,几乎储存了所有的音视频处理所需信息。包括 :
- AVInputFormat或者AVOutputFormat:⼆者只能同时存在⼀个(解封装、封装)
- AVIOContext * pb:是的AVIOContext包含在AVFormatContext结构体中,随着后者一起初始化,用于读取待解码的文件到内存中,判断该文件的格式以确定demuxer类型等
- AVStream:代表该音频文件中的一个轨道,可能是视频、音频、字幕等,其中主要包括:
- AVCodecParameters *codecpar:储存了本轨道mov遍历box信息后的metedata信息
- AVCodecContext * codec:是的AVCodecContext包含在AVStream结构体中,代表了本轨道的编解码器上下文,但是他的AVCodec *成员(编解码器实体)需要使用单独的方法查找并分配
- AVIndexEntry *index_entries:遍历完box信息后,当前轨道每一帧的dts等信息
- 此外AVFormatContext还包含了本音视频文件的duration、bit_rate、probesize等其他信息
二、确定解封装器
1.ffmpeg解码流程简介
- 对于一个普通的ffmpeg解码流程,可用下图来概括:
- 可以看到,除了av_register_all()、avcodec_register_all()两个注册函数外(分别注册解封装器、解码器),avformat_open_input是所有ffmpeg工具使用过程中执行的第一个重要的功能函数
2.avformat_open_input确定demuxer流程
- avformat_open_input是所有ffmpeg工具使用过程中执行的第一个重要的函数,他的作用是打开一个输入流(视频流/音频流),并解析头文件。需要注意的是,这一步不涉及codec打开
- 首先要做的事是,是确定输入流的demuxer类型:
-
由上图可知,avformat_open_input()函数执行过程主要做了三件事:
- 调用
libavforamt/options.c#avformat_alloc_context()
函数,创建了AVFormatContext结构体。 这里就是简单的malloc对象,没什么可说的:
AVFormatContext *avformat_alloc_context(void){ AVFormatContext *ic; ic = av_malloc(sizeof(AVFormatContext)); if (!ic) return ic; avformat_get_context_defaults(ic); ic->internal = av_mallocz(sizeof(*ic->internal)); if (!ic->internal) { avformat_free_context(ic); return NULL; } ic->internal->offset = AV_NOPTS_VALUE; ic->internal->raw_packet_buffer_remaining_size = RAW_PACKET_BUFFER_SIZE; ic->internal->shortest_end = AV_NOPTS_VALUE; return ic; }
-
调用
libavforamt/utils.c#init_input()
函数,根据各种策略找到合适的demuxer解封装。这个函数执行过程涉及AVIOContext和AVInputFormat对象的创建和初始化:
- 第一步是执行av_probe_input_format2()/av_probe_input_format3()函数,根据路径判断需要的demuxer类型。但是由于此时
AVInputFormat *av_probe_input_format2(AVProbeData *pd, int is_opened, int *score_max)
的第二个参数传的是0,也就是文件并没有打开,导致在av_probe_input_format3()中执行while ((fmtl = av demuxer iteratel&il)
遍历已经注册的demuxer时,绝大多数情况会直接continue,因此除了极少数比较特殊的类型外,绝大多数常用的文件/输入流类型在这里都找不到合适的demuxer - 第二步是执行
AVFormatContext#io_open()
函数,该函数主要的工作就是,根据路径名,在已经注册的所有URLProtocol中,找到合适的URLProtocol。以本地文件file.c
为例,这个结构体中注册了文件的open、write、read、seek等方法,是后续读取本地文件等操作的实际执行函数。之后会创建URLContext对象,将上面找到的URLProtocol对象赋值给URLContext对象(前者是后者的成员变量)。之后会调用avio_alloc_context()函数创建AVIOContext对象,并将上面初始化好的URLContext对象赋值给AVIOContext对象(前者是后者的成员变量)。 - 第三步是执行av_probe_input_buffer2()函数,使用上述URLProtocol的.url_read()函数,读取文件流。同时调用av_probe_input_format3()函数,依次遍历已经注册的demuxer/AVInputFormat中的.read_probe()函数,解析读取到的文件流,创建正确的demuxer/AVInputFormat对象。
- 第一步是执行av_probe_input_format2()/av_probe_input_format3()函数,根据路径判断需要的demuxer类型。但是由于此时
-
调用
AVInputFormat#read_header()
函数,读取并解析文件头信息,创建AVStream对象。 对于mov文件,这一步就是遍历解析box信息。我们在下一篇文章中介绍这个过程。
- 调用
三、AVFormatContext、AVInputFormat与AVIOContext
- 上述分析avformat_open_input确定demuxer流程中,可以看到AVFormatContext、AVInputFormat与AVIOContext三个结构体都已经悉数出现,并各自承担了不同的职责。本节将我们逐个分析每个结构体的功能及用法
1.AVIOContext解析
-
先置顶雷神的博客:FFMPEG结构体分析:AVIOContext
-
根据AVIOContext的构造方法,可以得出比较重要的成员变量、函数:
AVIOContext *avio_alloc_context( unsigned char *buffer, int buffer_size, int write_flag, void *opaque, int (*read_packet)(void *opaque, uint8_t *buf, int buf_size), int (*write_packet)(void *opaque, uint8_t *buf, int buf_size), int64_t (*seek)(void *opaque, int64_t offset, int whence)) { AVIOContext *s = av_malloc(sizeof(AVIOContext)); if (!s) return NULL; ffio_init_context(s, buffer, buffer_size, write_flag, opaque, read_packet, write_packet, seek); return s; }
- unsigned char *buffer:缓存开始位置
- int buffer_size:缓存大小(默认32768)
- void *opaque:不透明指针,是 read_packet / write_packet 的第⼀个参数,用于传递给读写操作的回调函数:
- 在ffmpeg默认的aviobuf.c源码中串的是URLContext结构体
- 如果是自定义AVIOContext对象,就传用户自定义的数据,用的时候强转一下就行
- 甚至也可以直接传null,如果后面用不到的话
- int (*read_packet)(void *opaque, uint8_t *buf, int buf_size):AVIOContext真正实现读取输入流的地方,这个函数指针可以自己注册。比如我们可以自定义从网络流读取文件,则可以自定义.read_packet()函数,实现网络文件读取的逻辑
- int (*write_packet)(void *opaque, uint8_t *buf, int buf_size):用法同上
- int64_t (*seek)(void *opaque, int64_t offset, int whence)):用法同上
-
URLContext与URLProtocol解析
- URLContext是AVIOContext结构体的成员,而URLProtocol又是URLContext结构体的成员
- URLProtocol是预先注册好的输入流读写处理结构体,每一个预先注册的类型都要实现该协议中的方法。以本地文件读取为例子,其URLProtocol实现在
file.c
文件中,其中定义了文件的open、read、write、seek、close等必要功能的实现:
const URLProtocol ff_file_protocol = { .name = "file", .url_open = file_open, .url_read = file_read, .url_write = file_write, .url_seek = file_seek, .url_close = file_close, .url_get_file_handle = file_get_handle, .url_check = file_check, .url_delete = file_delete, .url_move = file_move, .priv_data_size = sizeof(FileContext), .priv_data_class = &file_class, .url_open_dir = file_open_dir, .url_read_dir = file_read_dir, .url_close_dir = file_close_dir, .default_whitelist = "file,crypto" };
-
URLProtocol查找过程比较简单粗暴,主要是根据文件路径名,遍历已经注册的所有URLProtocol去匹配:
static const struct URLProtocol *url_find_protocol(const char *filename) { const URLProtocol **protocols; char proto_str[128], proto_nested[128], *ptr; size_t proto_len = strspn(filename, URL_SCHEME_CHARS); int i; if (filename[proto_len] != ':' && (strncmp(filename, "subfile,", 8) || !strchr(filename + proto_len + 1, ':')) || is_dos_path(filename)) strcpy(proto_str, "file"); else av_strlcpy(proto_str, filename, FFMIN(proto_len + 1, sizeof(proto_str))); av_strlcpy(proto_nested, proto_str, sizeof(proto_nested)); if ((ptr = strchr(proto_nested, '+'))) *ptr = '\0'; protocols = ffurl_get_protocols(NULL, NULL); if (!protocols) return NULL; for (i = 0; protocols[i]; i++) { const URLProtocol *up = protocols[i]; if (!strcmp(proto_str, up->name)) { av_freep(&protocols); return up; } if (up->flags & URL_PROTOCOL_FLAG_NESTED_SCHEME && !strcmp(proto_nested, up->name)) { av_freep(&protocols); return up; } } av_freep(&protocols); return NULL; }
-
AVIOContext中的.read_packet()函数,是输入流的读取入口函数,其中可能会用到URLProtocol中定义的.url_open()、.url_read等函数,主要看.read_packet()的具体实现。如果我们自己重新定义了.read_packet()函数,并在该函数中直接实现了读取功能,那么也不一定非要用到URLProtocol中的读取函数了(参考这篇文章:FFmpeg 自定义 IO 操作之 AVIO 解析)
2.AVFormatContext、AVInputFormat解析
-
在使用FFMPEG进行开发的时候,AVFormatContext是一个贯穿始终的数据结构,很多函数都要用到它作为参数。它是FFMPEG解封装(flv,mp4,rmvb,avi)功能的结构体:
- AVIOContext *pb:输入数据的缓存
- AVInputFormat *iformat:解封装器/demuxer
- AVOutputFormat *oformat:封装器/muxer
- unsigned int nb_streams:视音频流的个数
- AVStream **streams:视音频流
- char filename[1024]:文件名
- int64_t duration:时长(单位:微秒us,转换为秒需要除以1000000)
- int bit_rate:比特率(单位bps,转换为kbps需要除以1000)
- AVDictionary *metadata:元数据
-
可以看到AVFormatContext中包含了AVIOContext、AVInputFormat、AVStream三个重要的结构体,其中AVInputFormat就是解封装器,在avformat_open_input()函数中主流程中,第二步init_input()函数的主要目的就是确定AVInputFormat对象的:
static int init_input(AVFormatContext *s, const char *filename, AVDictionary **options) { int ret; AVProbeData pd = { filename, NULL, 0 }; int score = AVPROBE_SCORE_RETRY; if (s->pb) { s->flags |= AVFMT_FLAG_CUSTOM_IO; if (!s->iformat) return av_probe_input_buffer2(s->pb, &s->iformat, filename, s, 0, s->format_probesize); else if (s->iformat->flags & AVFMT_NOFILE) av_log(s, AV_LOG_WARNING, "Custom AVIOContext makes no sense and " "will be ignored with AVFMT_NOFILE format.\n"); return 0; } if ((s->iformat && s->iformat->flags & AVFMT_NOFILE) || (!s->iformat && (s->iformat = av_probe_input_format2(&pd, 0, &score)))) return score; if ((ret = s->io_open(s, &s->pb, filename, AVIO_FLAG_READ | s->avio_flags, options)) < 0) return ret; if (s->iformat) return 0; return av_probe_input_buffer2(s->pb, &s->iformat, filename, s, 0, s->format_probesize); }
- 根据第二小节“avformat_open_input确定demuxer流程”中的分析可知,对于绝大多数文件类型,都是要在av_probe_input_buffer2函数中,遍历已经注册的所有demuxer,挨个调用各个demuxer的.read_probe()函数来判断,当前解封装器是否合适
- mov的解封装器类mov.c文件中,定义了一系列mov文件在解析过程中的关键函数,如mov_read_header、mov_read_packet、mov_read_seek等,我们将在下一章中详细解读mov文件的解析流程
AVInputFormat ff_mov_demuxer = { .name = "mov,mp4,m4a,3gp,3g2,mj2", .long_name = NULL_IF_CONFIG_SMALL("QuickTime / MOV"), .priv_class = &mov_class, .priv_data_size = sizeof(MOVContext), .extensions = "mov,mp4,m4a,3gp,3g2,mj2", .read_probe = mov_probe, .read_header = mov_read_header, .read_packet = mov_read_packet, .read_close = mov_read_close, .read_seek = mov_read_seek, .flags = AVFMT_NO_BYTE_SEEK, };