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;

注册时,新编解码器被插入到链表头部。我画了张图帮你理解:

FFmpeg 编解码器全局链表结构 first_avcodec AVCodec: libx264 id=AV_CODEC_ID_H264 AVCodec: aac id=AV_CODEC_ID_AAC AVCodec: ac3 id=AV_CODEC_ID_AC3 NULL 链表特点: • 单向链表,只增不减(注册后不会卸载) • 新节点插入头部,所以后注册的编解码器先被遍历到 • 每个节点包含 name、id、type 等关键字段用于匹配 • 解码器和编码器混在同一个链表里,通过 type 字段区分 • 自定义编解码器调用 avcodec_register() 后也会挂到这里

小技巧:调试时可以用 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() 的初始化流程就轻松多了。