2、FFmpeg核心数据结构:AVFormatContext、AVInputFormat、AVOutputFormat 深度解析
做封装解析,说白了就是跟这三个结构体打交道。我刚开始接触FFmpeg时,看着AVFormatContext那几百个字段,说实话有点懵。后来踩了几个坑才明白——核心就三个结构体,搞懂了它们,封装层基本就通了。
2.1 AVFormatContext:封装层的“总控台”
AVFormatContext是整个封装格式层的上下文。你可以把它想象成一个总控台,所有跟容器格式相关的信息都挂在这里。不管是读文件还是写文件,第一步都是创建它。
核心定位:AVFormatContext 是 I/O 层与编解码层之间的桥梁。它管理着流信息、元数据、封装格式的输入输出状态。
2.1.1 关键字段一览
我挑几个最常用的字段说说。其他的你用到时再查文档就行,不用死记。
| 字段名 | 类型 | 作用 | 我的备注 |
|---|---|---|---|
iformat |
AVInputFormat* |
输入封装格式指针 | 打开文件时自动匹配 |
oformat |
AVOutputFormat* |
输出封装格式指针 | 写文件时需指定 |
nb_streams |
unsigned int |
流数量 | 视频、音频、字幕都算 |
streams |
AVStream** |
流指针数组 | 每个流对应一个AVStream |
duration |
int64_t |
时长(微秒) | 有些格式不准确 |
bit_rate |
int64_t |
总码率 | 估算值,别太当真 |
metadata |
AVDictionary* |
元数据字典 | 标题、作者等信息 |
pb |
AVIOContext* |
I/O上下文 | 读写字节的通道 |
2.1.2 生命周期管理
创建和销毁有固定套路。我见过有人直接malloc,结果内存泄漏得一塌糊涂。
// 读模式:avformat_open_input 内部会创建
AVFormatContext *fmt_ctx = NULL;
avformat_open_input(&fmt_ctx, "input.mp4", NULL, NULL);
// ... 使用 ...
avformat_close_input(&fmt_ctx); // 自动释放内部资源
// 写模式:先手动分配
AVFormatContext *ofmt_ctx = NULL;
avformat_alloc_output_context2(&ofmt_ctx, NULL, "mp4", "output.mp4");
// ... 使用 ...
avformat_free_context(ofmt_ctx); // 手动释放
注意:avformat_open_input 和 avformat_alloc_output_context2 的释放方式不同。前者用 avformat_close_input,后者用 avformat_free_context。混用会导致 double free 或内存泄漏。我曾经在这个问题上排查了整整一下午。
2.2 AVInputFormat:解封装器的“身份证”
AVInputFormat 描述的是“怎么读”某种容器格式。每个容器格式——MP4、FLV、TS、MKV——都有一个对应的 AVInputFormat 实例。它不存储数据,只存储行为。
2.2.1 核心成员
这个结构体是只读的,你不需要自己创建。FFmpeg 内部已经注册好了所有支持的格式。
| 成员 | 说明 |
|---|---|
name |
格式名称,如 "mp4"、"flv" |
long_name |
描述性名称,如 "QuickTime / MOV" |
extensions |
文件扩展名列表,逗号分隔 |
read_probe |
探测函数,判断文件是否匹配本格式 |
read_header |
读取文件头,解析流信息 |
read_packet |
读取一个数据包 |
read_close |
关闭解封装器 |
2.2.2 格式探测机制
你想想看,当你给 FFmpeg 一个文件时,它怎么知道该用哪个解封装器?靠的就是 av_probe_input_format 函数。它会读取文件开头的一小段数据,然后跟所有注册的 AVInputFormat 的 read_probe 函数比对得分。
// 手动探测格式的示例
AVProbeData pd = {0};
pd.buf = buf; // 文件头数据
pd.buf_size = size; // 数据大小
pd.filename = "unknown.bin";
AVInputFormat *fmt = av_probe_input_format(&pd, 1);
if (fmt) {
printf("检测到格式: %s\n", fmt->long_name);
}
小技巧:如果你确定文件格式,可以直接传 AVInputFormat 指针给 avformat_open_input,跳过探测步骤,能省几十毫秒。我在做批量转码工具时就这么干的。
2.3 AVOutputFormat:封装器的“施工图”
AVOutputFormat 跟 AVInputFormat 是对称的。它描述的是“怎么写”某种容器格式。写 MP4 和写 FLV 的流程不同,但都遵循同一套接口。
2.3.1 关键成员对比
| AVInputFormat | AVOutputFormat | 说明 |
|---|---|---|
read_header |
write_header |
读/写文件头 |
read_packet |
write_packet |
读/写数据包 |
read_close |
write_trailer |
关闭/写文件尾 |
read_probe |
无 | 输出格式不需要探测 |
| 无 | audio_codec |
默认音频编码器ID |
| 无 | video_codec |
默认视频编码器ID |
2.3.2 输出格式的匹配方式
输出格式通常通过文件名后缀或显式指定。avformat_alloc_output_context2 会帮你自动匹配。
// 方式一:通过文件名后缀自动匹配
avformat_alloc_output_context2(&ctx, NULL, NULL, "output.mp4");
// 方式二:显式指定格式名
avformat_alloc_output_context2(&ctx, NULL, "flv", "output.flv");
// 方式三:直接传 AVOutputFormat 指针
AVOutputFormat *fmt = av_guess_format("mpegts", NULL, NULL);
avformat_alloc_output_context2(&ctx, fmt, NULL, "output.ts");
2.4 三者协作关系
这三个结构体不是孤立的。它们通过 AVFormatContext 串联起来,形成完整的封装处理链路。我画了张图,你看一眼就明白了。
2.5 实战中的常见陷阱
嗯,这里要注意几个坑。我这些年没少在这些地方栽跟头。
2.5.1 流索引的稳定性
AVStream 在 AVFormatContext 中的索引位置,在读取过程中是稳定的。但如果你手动添加或移除流,索引就会变。我做过一个项目,在写 MP4 时动态添加字幕流,结果忘了更新索引,导致音视频不同步。
2.5.2 元数据的生命周期
AVDictionary 类型的 metadata 字段,在 avformat_close_input 时会自动释放。但如果你把 metadata 指针复制到其他地方使用,记得先 av_dict_ref 增加引用计数,否则会变成悬空指针。
2.5.3 自定义 I/O 时的 pb 处理
如果你用 avio_alloc_context 创建了自定义的 AVIOContext,记得设置 AVFormatContext 的 pb 字段,并且要自己管理 AVIOContext 的释放。avformat_close_input 不会释放你手动分配的 pb。
// 自定义 I/O 的正确释放顺序
avformat_close_input(&fmt_ctx); // 先关格式上下文
avio_context_free(&custom_pb); // 再释放自定义 I/O
警告:顺序反了会导致 use-after-free。avformat_close_input 内部会读写 pb,如果你先释放了 pb,程序直接崩溃。我曾经在生产环境遇到过这个问题,排查时发现 core dump 堆栈指向了 avio_read。
2.6 小结
AVFormatContext 是总控台,AVInputFormat 是读格式的行为描述,AVOutputFormat 是写格式的行为描述。三者通过指针关联,构成了 FFmpeg 封装层的核心骨架。搞懂它们,你就掌握了 FFmpeg 封装层的钥匙。
我个人习惯在写工具时,先把这三个结构体的生命周期画出来,再动手编码。这样能避免很多低级错误。你也不妨试试。