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() 里完成的。
但分配阶段已经埋下了很多默认值。我们来看一张流程图:
从流程图可以看出,分配阶段只做了三件事:
- 分配内存:av_mallocz 保证所有字段初始为 0。
- 设置 AVClass:让日志和选项系统能工作。
- 根据 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() 之后才会被真正确定,解码器会根据码流中的信息选择最合适的格式。
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 的深拷贝。
- 用
avformat_find_stream_info()获取流信息。 - 从
AVStream->codecpar拿到参数。 - 调用
avcodec_alloc_context3(codec)分配上下文。 - 调用
avcodec_parameters_to_context()拷贝参数。 - 最后
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 是输出格式:解码器输出的像素格式,不是你输入的。输入格式由码流决定,你改不了。