2、解码器注册机制:avcodec_register_all() 的源码分析
好,咱们正式开始啃 FFmpeg 的解码器注册机制。这一节我带你看看 avcodec_register_all() 到底干了什么,编解码器链表怎么组织的,以及 FFmpeg 怎么找到你要的解码器。
说实话,我第一次看这段代码时也有点懵。一堆宏定义,一堆链表操作。但后来我在项目中调试一个自定义解码器时,发现注册顺序居然会影响查找结果——嗯,从那以后我就老老实实把这块啃透了。
2.1 avcodec_register_all() 做了什么
先看入口函数。在 libavcodec/allcodecs.c 里:
void avcodec_register_all(void)
{
static AVOnce control = AV_ONCE_INIT;
ff_thread_once(&control, avcodec_register_all_once);
}
有意思吧?它用了 AVOnce 保证只执行一次。多线程环境下,多个线程同时调这个函数也不会出问题。我建议你记住这个模式——FFmpeg 里很多初始化都这么干。
真正的注册逻辑在 avcodec_register_all_once() 里。它本质上就是调用一堆 REGISTER_DECODER 和 REGISTER_ENCODER 宏:
static void avcodec_register_all_once(void)
{
REGISTER_DECODER(AAC, aac);
REGISTER_DECODER(AAC_LATM, aac_latm);
REGISTER_DECODER(AC3, ac3);
REGISTER_ENCODER(AC3, ac3);
// ... 几百行类似的调用
}
每个宏展开后,就是把一个 AVCodec 结构体挂到全局链表上。说白了,这就是个「登记造册」的过程。
核心要点:avcodec_register_all() 只执行一次,把所有内置编解码器注册到全局链表。你如果自己写了解码器,也得手动调 avcodec_register() 挂上去。
2.2 编解码器链表结构
FFmpeg 用了一个很朴素的单向链表来管理所有编解码器。每个 AVCodec 结构体里都有个 next 指针:
typedef struct AVCodec {
const char *name;
const char *long_name;
enum AVMediaType type;
enum AVCodecID id;
int capabilities;
const AVRational *supported_framerates;
const enum AVPixelFormat *pix_fmts;
const int *supported_samplerates;
const enum AVSampleFormat *sample_fmts;
const uint64_t *channel_layouts;
uint8_t max_lowres;
const AVClass *priv_class;
const AVProfile *profiles;
int priv_data_size;
struct AVCodec *next; // <-- 链表指针
// ... 回调函数指针
} AVCodec;
全局链表头在 libavcodec/utils.c 里定义:
static AVCodec *first_avcodec = NULL;
注册时,新编解码器被插入到链表头部。我画了张图帮你理解:
小技巧:调试时可以用 av_codec_iterate() 遍历所有已注册编解码器。我写过一个工具脚本,把链表里所有解码器按 id 排序打印出来,排查重复注册问题特别好用。
2.3 如何查找解码器
FFmpeg 提供了几种查找方式,咱们一个个看。
2.3.1 按 ID 查找:avcodec_find_decoder()
这是最常用的方式。从 AVCodecID 枚举值找对应的解码器:
AVCodec *avcodec_find_decoder(enum AVCodecID id)
{
return find_codec(id, av_codec_is_decoder);
}
static AVCodec *find_codec(enum AVCodecID id, int (*x)(const AVCodec *))
{
const AVCodec *p, *experimental = NULL;
void *i = 0;
while ((p = av_codec_iterate(&i))) {
if (!x(p))
continue;
if (p->id == id) {
if (p->capabilities & AV_CODEC_CAP_EXPERIMENTAL && !experimental)
experimental = p;
else
return p;
}
}
return experimental;
}
逻辑很直白:遍历链表,匹配 id。有个细节——实验性解码器会被特殊处理。如果同时有稳定版和实验版,优先返回稳定版。我记得有一次调试一个实验性解码器,死活找不到,就是因为这个逻辑。
2.3.2 按名称查找:avcodec_find_decoder_by_name()
有时候你知道解码器名字,比如 "h264" 或 "aac":
AVCodec *avcodec_find_decoder_by_name(const char *name)
{
const AVCodec *p;
void *i = 0;
while ((p = av_codec_iterate(&i))) {
if (av_codec_is_decoder(p) && strcmp(p->name, name) == 0)
return p;
}
return NULL;
}
这里用的是 strcmp 精确匹配。你传 "H264" 大写就找不到了。我在项目中踩过这个坑——配置文件名大小写不一致,查了半天才发现是大小写问题。
2.3.3 遍历所有解码器:av_codec_iterate()
这个函数是 FFmpeg 4.0 之后推荐的遍历方式。它隐藏了链表的具体实现:
const AVCodec *av_codec_iterate(void **opaque)
{
uintptr_t i = (uintptr_t)*opaque;
const AVCodec *c = NULL;
if (i < av_codec_init_static_data_size)
c = &av_codec_static_data[i];
if (!c)
c = av_codec_first;
if (c) {
*opaque = (void*)(i + 1);
return c;
}
return NULL;
}
你想想看,为什么要有 av_codec_init_static_data?这是 FFmpeg 做的一个优化——把最常用的编解码器放在静态数组里,避免链表遍历的开销。不过大多数场景下,你直接用 avcodec_find_decoder() 就够了。
注意:av_codec_iterate() 返回的指针是只读的,不要尝试修改里面的字段。我曾经手贱改了一个解码器的 name 字段,结果导致后续查找全乱套了。
2.4 注册与查找的完整流程
我把整个过程串起来,你感受一下:
| 步骤 | 函数 | 说明 |
|---|---|---|
| 1 | avcodec_register_all() | 初始化全局链表,注册所有内置编解码器 |
| 2 | avcodec_find_decoder(AV_CODEC_ID_H264) | 遍历链表,匹配 id 字段 |
| 3 | avcodec_alloc_context3(codec) | 分配 AVCodecContext,关联解码器 |
| 4 | avcodec_open2(ctx, codec, NULL) | 初始化解码器内部状态,准备解码 |
这个流程我建议你背下来。面试常考,实际开发也天天用。
2.5 避坑指南
最后分享几个我踩过的坑:
- 忘记调 avcodec_register_all():新手上路常见错误。不注册的话,链表是空的,啥解码器都找不到。
- 重复注册自定义解码器:avcodec_register() 不会检查重复。如果你注册了两次同名解码器,查找时返回的是先找到的那个(链表头部的)。
- 多线程环境下注册:avcodec_register_all() 本身是线程安全的,但如果你在子线程里调 avcodec_register() 注册自定义解码器,需要自己加锁。
- 静态链接 vs 动态链接:静态链接时,有些解码器可能没被编译进来。用
avcodec_find_decoder()返回 NULL 时,先确认一下你的编译配置。
总结一下:avcodec_register_all() 是 FFmpeg 解码器体系的入口,它构建了全局编解码器链表。avcodec_find_decoder() 通过遍历这个链表来匹配你需要的解码器。理解了这个机制,后面看 avcodec_open2() 的初始化流程就轻松多了。