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 在编码流程中的协作关系:

AVCodec、AVCodecContext、AVFrame 关系图 AVCodec、AVCodecContext、AVFrame 编码协作关系 AVFrame 原始视频帧 YUV420P 像素数据 AVCodecContext 编码器运行时配置 码率/分辨率/GOP AVCodec 编解码器算法 H.264 / AAC 实现 输入原始数据 调用编码 输出 AVPacket 编码流程说明: 1. 应用层填充 AVFrame(原始 YUV 数据) 2. 调用 avcodec_send_frame() 将 AVFrame 送入 AVCodecContext 3. AVCodecContext 使用 AVCodec 的编码算法进行压缩 4. 调用 avcodec_receive_packet() 取出编码后的 AVPacket

4.5 编码流程中的协作

理解了三个结构体的角色,我们来看一个完整的编码流程:

  1. 查找 AVCodec:通过 avcodec_find_encoder() 拿到编码器。
  2. 创建 AVCodecContext:avcodec_alloc_context3() 分配上下文,设置参数。
  3. 打开编码器:avcodec_open2() 绑定 AVCodecContext 和 AVCodec。
  4. 准备 AVFrame:分配帧,填充原始数据(YUV 或 PCM)。
  5. 送入编码:avcodec_send_frame() 把 AVFrame 交给 AVCodecContext。
  6. 取出结果: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 编解码的基石,搞懂了它们,后面的推流流程就顺理成章了。