4. 核心数据结构(下):AVCodecContext、AVFrame、AVCodec 的作用与关系
上一节我们把 AVFormatContext 和 AVPacket 聊透了。这一节继续啃剩下的三块硬骨头:AVCodecContext、AVFrame 和 AVCodec。
这三兄弟的关系,说白了就是「编解码器、编解码参数、音视频帧数据」的三角恋。我刚开始用 FFmpeg 时,经常把 AVCodec 和 AVCodecContext 搞混,以为它们是一个东西。后来踩了坑才明白——一个是工厂,一个是生产线。
一句话总结:AVCodec 是编解码器(算法实现),AVCodecContext 是编解码器的运行时配置(参数 + 状态),AVFrame 是解码后的原始数据(或编码前的原始数据)。
4.1 AVCodec:编解码器的「身份证」
AVCodec 结构体描述了一个编解码器的能力。它不保存任何运行时状态,只告诉你:我叫什么名字、我能编解码什么格式、我支持哪些像素格式。
我个人习惯把 AVCodec 看作「编解码器的静态描述」。你不需要自己创建它,而是通过 avcodec_find_encoder() 或 avcodec_find_decoder() 去查找。
// 查找 H.264 编码器
const AVCodec *codec = avcodec_find_encoder(AV_CODEC_ID_H264);
if (!codec) {
fprintf(stderr, "H.264 编码器未找到\n");
exit(1);
}
printf("编码器名称: %s\n", codec->name); // 输出: libx264
printf("编码器长名: %s\n", codec->long_name); // 输出: H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10
| 字段 | 含义 | 典型值 |
|---|---|---|
name |
编解码器短名称 | libx264, aac |
long_name |
编解码器描述 | H.264 / AVC |
id |
编解码器 ID | AV_CODEC_ID_H264 |
type |
媒体类型 | AVMEDIA_TYPE_VIDEO |
pix_fmts |
支持的像素格式列表 | AV_PIX_FMT_YUV420P |
supported_samplerates |
支持的采样率列表(音频) | 44100, 48000 |
避坑指南:我曾经在项目里直接修改 AVCodec 的字段,结果程序崩溃。记住,AVCodec 是只读的,你只能读,不能写。要改参数?那是 AVCodecContext 的事。
4.2 AVCodecContext:编解码器的「运行环境」
AVCodecContext 才是你真正要操作的对象。它包含了编解码器的所有运行时参数:码率、分辨率、帧率、像素格式、GOP 大小……
你想想看,同一个 H.264 编码器,可以输出 1080p 也可以输出 720p,可以跑 2Mbps 也可以跑 10Mbps。这些参数不可能写在 AVCodec 里,所以 FFmpeg 把它们抽到了 AVCodecContext 中。
// 创建编码器上下文
AVCodecContext *codec_ctx = avcodec_alloc_context3(codec);
if (!codec_ctx) {
fprintf(stderr, "无法分配编码器上下文\n");
exit(1);
}
// 设置编码参数
codec_ctx->width = 1920;
codec_ctx->height = 1080;
codec_ctx->time_base = (AVRational){1, 25}; // 25fps
codec_ctx->bit_rate = 2000000; // 2Mbps
codec_ctx->pix_fmt = AV_PIX_FMT_YUV420P;
codec_ctx->gop_size = 12; // 每12帧一个关键帧
codec_ctx->max_b_frames = 2; // 最多2个B帧
// 打开编码器
if (avcodec_open2(codec_ctx, codec, NULL) < 0) {
fprintf(stderr, "无法打开编码器\n");
exit(1);
}
这里有个关键点:avcodec_open2() 会把 AVCodecContext 和 AVCodec 绑定起来。绑定之后,AVCodecContext 内部会分配编码器所需的私有数据。
| 字段 | 含义 | 设置时机 |
|---|---|---|
width / height |
视频宽高 | 编码前设置 |
time_base |
时间基 | 编码前设置 |
bit_rate |
目标码率 | 编码前设置 |
pix_fmt |
像素格式 | 编码前设置 |
gop_size |
关键帧间隔 | 编码前设置 |
max_b_frames |
最大 B 帧数 | 编码前设置 |
codec |
绑定的编解码器 | avcodec_open2 自动设置 |
refcounted_frames |
是否启用引用计数 | 解码前设置(推荐 1) |
注意:avcodec_alloc_context3() 只是分配了内存,avcodec_open2() 才是真正的初始化。我见过有人忘记调用 avcodec_open2(),结果编码出来的全是花屏。嗯,别问我怎么知道的。
4.3 AVFrame:原始音视频数据的「容器」
AVFrame 存放的是解码后的原始数据。对于视频,它是 YUV 或 RGB 像素数据;对于音频,它是 PCM 采样数据。
说白了,AVFrame 就是一张「未压缩的图像」或「一段未压缩的音频」。它不包含编码信息,只包含原始采样值。
// 分配一个视频帧
AVFrame *frame = av_frame_alloc();
if (!frame) {
fprintf(stderr, "无法分配 AVFrame\n");
exit(1);
}
// 设置帧参数
frame->format = AV_PIX_FMT_YUV420P;
frame->width = 1920;
frame->height = 1080;
// 为帧分配数据缓冲区
av_frame_get_buffer(frame, 32); // 32 字节对齐
// 填充数据(这里只演示结构,实际需要填充 Y/U/V 平面)
// frame->data[0] -> Y 平面
// frame->data[1] -> U 平面
// frame->data[2] -> V 平面
// frame->linesize[0] -> Y 平面行字节数
AVFrame 的几个关键字段:
- data[]:数据指针数组。视频通常用 data[0]~data[2](Y/U/V),音频用 data[0]~data[7](多声道)。
- linesize[]:每行数据的字节数。注意,linesize 可能大于 width * bpp,因为有对齐要求。
- pts:显示时间戳。这是帧在原始时间基下的时间点。
- key_frame:是否为关键帧(解码时由 FFmpeg 设置)。
- sample_rate / nb_samples / channels:音频专用,表示采样率、样本数、声道数。
个人经验:我在做推流项目时,经常需要把摄像头采集的 RGB 数据转成 YUV420P。这时候 AVFrame 的 linesize 就特别重要——不能直接用 width * 3 去算 RGB 的行字节数,因为 linesize 可能包含填充字节。老老实实用 linesize 来遍历,否则你会得到歪斜的图像。
4.4 三者的关系:一张图说清楚
下面这张 SVG 图展示了 AVCodec、AVCodecContext、AVFrame 在编码流程中的协作关系:
4.5 编码流程中的协作
理解了三个结构体的角色,我们来看一个完整的编码流程:
- 查找 AVCodec:通过
avcodec_find_encoder()拿到编码器。 - 创建 AVCodecContext:
avcodec_alloc_context3()分配上下文,设置参数。 - 打开编码器:
avcodec_open2()绑定 AVCodecContext 和 AVCodec。 - 准备 AVFrame:分配帧,填充原始数据(YUV 或 PCM)。
- 送入编码:
avcodec_send_frame()把 AVFrame 交给 AVCodecContext。 - 取出结果:
avcodec_receive_packet()拿到编码后的 AVPacket。
// 完整编码循环示例
AVFrame *frame = /* 从采集或读取获取 */;
AVPacket *pkt = av_packet_alloc();
// 发送帧到编码器
int ret = avcodec_send_frame(codec_ctx, frame);
if (ret < 0) {
// 处理错误
}
// 循环取出编码后的包
while (ret >= 0) {
ret = avcodec_receive_packet(codec_ctx, pkt);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
break;
} else if (ret < 0) {
// 处理错误
break;
}
// 这里 pkt 就是编码后的数据,可以写入输出文件或推流
// av_interleaved_write_frame(fmt_ctx, pkt);
av_packet_unref(pkt);
}
重要提醒:avcodec_send_frame() 和 avcodec_receive_packet() 是异步的。你送一帧进去,可能拿不到包(EAGAIN),也可能一次拿到多个包。我曾经在项目中没处理 EAGAIN,结果丢帧严重。记住,一定要用 while 循环去收包。
4.6 解码流程中的协作
解码是编码的逆过程,但结构体的角色不变:
- AVCodec:解码器(如 H.264 解码器)。
- AVCodecContext:解码器上下文,保存解码参数和状态。
- AVFrame:解码后的原始帧(YUV 数据)。
解码时,你送 AVPacket 进去,取出 AVFrame。流程是:avcodec_send_packet() → avcodec_receive_frame()。
| 操作 | 编码 | 解码 |
|---|---|---|
| 输入 | AVFrame(原始数据) | AVPacket(压缩数据) |
| 输出 | AVPacket(压缩数据) | AVFrame(原始数据) |
| 发送函数 | avcodec_send_frame() |
avcodec_send_packet() |
| 接收函数 | avcodec_receive_packet() |
avcodec_receive_frame() |
4.7 内存管理要点
这三个结构体的内存管理,是新手最容易翻车的地方。我总结几条铁律:
- AVCodec:不需要你分配和释放,FFmpeg 内部管理。
- AVCodecContext:用
avcodec_alloc_context3()分配,用avcodec_free_context()释放。 - AVFrame:用
av_frame_alloc()分配,用av_frame_free()释放。数据缓冲区用av_frame_get_buffer()分配。 - 引用计数:解码时建议设置
codec_ctx->refcounted_frames = 1,这样 AVFrame 的数据会通过引用计数管理,避免内存拷贝。
核心记忆法:AVCodec 是「菜谱」,AVCodecContext 是「灶台参数」,AVFrame 是「食材」。菜谱告诉你做什么菜,灶台参数决定火候和调料,食材就是你要加工的东西。三者缺一不可。
嗯,这一节的内容就到这里。这三个结构体是 FFmpeg 编解码的基石,搞懂了它们,后面的推流流程就顺理成章了。