4、解码器上下文AVCodecContext:avcodec_alloc_context3()源码与初始化

解码器上下文,说白了就是解码器的“身份证”加“工作台”。
FFmpeg 里所有解码相关的状态、参数、配置,全塞在这个结构体里。
我刚开始看源码时,面对 AVCodecContext 那几百个字段,说实话有点懵。
但后来发现,真正核心的也就那么十几个,其他的要么是编解码器私有,要么是内部状态。

这一章,我们就从 avcodec_alloc_context3() 这个入口函数开始,看看上下文是怎么出生的,以及那些关键字段到底在干什么。

4.1 avcodec_alloc_context3() 源码分析

这个函数是分配上下文的唯一官方入口。原型很简单:

AVCodecContext *avcodec_alloc_context3(const AVCodec *codec);

参数是一个 AVCodec 指针,传 NULL 也行,但很多字段不会自动填充。
我个人习惯是:只要知道用哪个解码器,就一定传进去,省得后面手动配。

我们来看核心实现(简化版,去掉了一些调试和锁逻辑):

AVCodecContext *avcodec_alloc_context3(const AVCodec *codec)
{
    AVCodecContext *avctx = av_mallocz(sizeof(AVCodecContext));
    if (!avctx)
        return NULL;

    avctx->av_class = &av_codec_context_class;
    avctx->codec_type = AVMEDIA_TYPE_UNKNOWN;
    avctx->codec_id   = AV_CODEC_ID_NONE;
    avctx->codec      = NULL;

    // 如果传入了 codec,就根据 codec 类型设置 codec_type
    if (codec) {
        avctx->codec_type = codec->type;
        avctx->codec_id   = codec->id;
        avctx->codec      = codec;
    }

    // 初始化内部锁
    ff_mutex_init(&avctx->internal->mutex, NULL);

    return avctx;
}

注意几个关键点:

  • av_mallocz:分配内存并清零,所有字段初始为 0 或 NULL。
  • av_class:指向 AVClass 结构体,用于日志和选项系统。没有它,av_log() 就不知道打印什么前缀。
  • codec_type / codec_id / codec:如果传入了 codec,这三个字段会同步设置。否则全是 UNKNOWN / NONE。
小技巧: 如果你用 avcodec_find_decoder() 找到了解码器,记得把结果传给 avcodec_alloc_context3()。这样能省掉手动设置 codec_id 和 codec_type 的步骤,减少出错概率。

4.2 上下文初始化流程

分配完上下文只是第一步。真正的初始化,其实是在 avcodec_open2() 里完成的。
但分配阶段已经埋下了很多默认值。我们来看一张流程图:

AVCodecContext 初始化流程 avcodec_alloc_context3() av_mallocz 分配并清零 设置 av_class / 默认值 codec != NULL? 否 是 codec_type=UNKNOWN 同步 codec_type/codec_id 分配完成后,还需调用 avcodec_open2() 完成解码器内部资源初始化

从流程图可以看出,分配阶段只做了三件事:

  1. 分配内存:av_mallocz 保证所有字段初始为 0。
  2. 设置 AVClass:让日志和选项系统能工作。
  3. 根据 codec 同步基础字段:如果传入了解码器,就把 type、id、codec 指针填上。

嗯,这里要注意:avcodec_alloc_context3() 并不会打开解码器,也不会分配内部缓冲区。
真正的“重活”在 avcodec_open2() 里。但分配阶段如果没做好,后面 open 的时候就会出各种奇怪的问题。

4.3 关键字段详解

AVCodecContext 的字段太多了,我挑几个最常用的来讲。其他的,用到的时候再查文档就行。

字段名 类型 说明 设置时机
codec const AVCodec * 指向当前使用的解码器实例 alloc 或 open 时
codec_id enum AVCodecID 编码格式标识,如 AV_CODEC_ID_H264 alloc 或手动设置
codec_type enum AVMediaType 媒体类型:视频/音频/字幕等 alloc 时自动推断
pix_fmt enum AVPixelFormat 视频解码输出的像素格式 open 后由解码器决定
width / height int 视频帧的宽高(像素) open 后或从流中读取
sample_fmt enum AVSampleFormat 音频采样格式 open 后由解码器决定
sample_rate int 音频采样率(Hz) open 后或从流中读取
ch_layout AVChannelLayout 音频声道布局 open 后或从流中读取
time_base AVRational 时间基,用于 PTS/DTS 换算 从流中拷贝或手动设置
extradata / extradata_size uint8_t* / int 编解码器私有数据(如 H.264 SPS/PPS) 从流中拷贝

4.3.1 codec 与 codec_id

这两个字段是上下文的“灵魂”。
codec 指向具体的解码器实现(比如 H.264 解码器),codec_id 则标识编码格式。
为什么要有两个?因为 codec_id 可以在没有找到解码器的情况下单独存在,用于描述“这个流是什么格式”。

我在项目中遇到过一个问题:用 avcodec_find_decoder() 找到了解码器,但忘了传给 avcodec_alloc_context3(),结果 codec 字段是 NULL。
后来调用 avcodec_open2() 时传了 NULL 的 codec,函数内部又根据 codec_id 重新查找了一次,虽然能工作,但多了一次查找开销。
所以我的建议是:分配时就把 codec 传进去,一步到位。

4.3.2 pix_fmt —— 像素格式

对于视频解码器,pix_fmt 是输出图像的像素格式。
常见的值有 AV_PIX_FMT_YUV420P、AV_PIX_FMT_NV12 等。
这个字段在 avcodec_open2() 之后才会被真正确定,解码器会根据码流中的信息选择最合适的格式。

注意: 不要手动设置 pix_fmt!除非你明确知道自己在做什么。我曾经为了省一次格式转换,强行把 pix_fmt 设为 NV12,结果解码器内部用的是 YUV420P,导致输出画面颜色完全错乱。解码器自己选的格式才是最靠谱的。

4.3.3 extradata —— 编解码器私有数据

这个字段很关键,但容易被忽略。
对于 H.264/H.265,extradata 里存的是 SPS/PPS 等参数集。
对于 AAC,存的是 ADTS 头或 ASC 信息。
如果没有正确设置 extradata,解码器可能无法正确解析码流。

我记得有一次调试一个 MP4 文件,解码出来全是花屏。查了半天,发现是 extradata 没有从 AVStream 拷贝到 AVCodecContext。
后来加了一行 avcodec_parameters_to_context(),问题就解决了。

4.4 上下文与 AVStream 的关系

在实际开发中,我们通常从 AVStream 中获取解码器参数,然后拷贝到 AVCodecContext。
FFmpeg 提供了一个便捷函数:

int avcodec_parameters_to_context(AVCodecContext *codec_ctx,
                                  const AVCodecParameters *par);

这个函数会把 AVCodecParameters 里的字段(包括 width、height、pix_fmt、extradata 等)批量拷贝到上下文中。
我个人强烈建议:不要手动逐个赋值,直接用这个函数。它内部处理了很多边界情况,比如 extradata 的深拷贝。

最佳实践:
  1. 用 avformat_find_stream_info() 获取流信息。
  2. 从 AVStream->codecpar 拿到参数。
  3. 调用 avcodec_alloc_context3(codec) 分配上下文。
  4. 调用 avcodec_parameters_to_context() 拷贝参数。
  5. 最后 avcodec_open2() 打开解码器。

4.5 避坑指南

最后分享几个我踩过的坑:

  • 不要重复分配上下文:每次 avcodec_alloc_context3() 都要对应一次 avcodec_free_context()。我见过有人在一个循环里反复分配不释放,内存泄漏到怀疑人生。
  • extradata 的生命周期:avcodec_parameters_to_context() 会深拷贝 extradata,所以原数据释放后上下文里的还能用。但如果你手动赋值,一定要确保 extradata 在解码期间有效。
  • time_base 不要乱设:视频的 time_base 通常从流中拷贝,音频的 time_base 是 1/sample_rate。手动设置错误会导致 PTS 换算全乱。
  • pix_fmt 是输出格式:解码器输出的像素格式,不是你输入的。输入格式由码流决定,你改不了。