3、基础数据结构:AVPacket、AVFrame、AVCodecContext 结构体详解

好,咱们今天来啃三块硬骨头。做音视频开发,说白了就是跟这三个结构体打交道。AVPacket、AVFrame、AVCodecContext,你要是能把它们搞明白,整个管道就通了一半。

我个人习惯把这三兄弟比作「快递系统」:AVCodecContext 是快递分拣中心的规则手册,AVPacket 是还没拆封的包裹,AVFrame 是拆开包装后的实物。嗯,这个比喻你记着,后面会反复用到。

3.1 AVPacket:压缩后的数据包

AVPacket 存的是什么?说白了就是编码器吐出来的东西,或者解码器要吃进去的东西。它里面装的是压缩后的数据——H.264 的 NALU、AAC 的 ADTS 帧,都在这里面。

来看它的核心字段:

typedef struct AVPacket {
    AVBufferRef *buf;        // 引用计数缓冲区
    int64_t pts;             // 显示时间戳
    int64_t dts;             // 解码时间戳
    uint8_t *data;           // 压缩数据
    int   size;              // 数据大小
    int   stream_index;      // 所属流索引
    AVPacketSideData *side_data; // 附加数据
    int64_t duration;        // 该包的持续时间
} AVPacket;

这里我要重点说下 pts 和 dts。很多新手在这里栽跟头。pts 是给显示器看的,dts 是给解码器看的。对于视频来说,B 帧的存在会导致 pts 和 dts 不一样。我曾经在项目里遇到一个诡异的花屏问题,排查了两天,最后发现是 pts 和 dts 赋值反了。

⚠️ 避坑指南: 千万不要直接操作 AVPacket 的 data 指针去修改数据。它内部有引用计数机制,你改了 data,可能把其他引用这个 buffer 的包也改了。要用 av_packet_make_writable() 先做拷贝。

3.2 AVFrame:解码后的原始帧

AVFrame 就亲切多了。它存的是解码后的原始数据——YUV 像素、PCM 采样点。你可以直接拿它去渲染或者播放。

核心字段长这样:

typedef struct AVFrame {
    uint8_t *data[AV_NUM_DATA_POINTERS];  // 数据指针数组
    int linesize[AV_NUM_DATA_POINTERS];   // 每行字节数
    int width, height;                    // 视频宽高
    int nb_samples;                       // 音频采样数
    int format;                           // 像素格式/采样格式
    int64_t pts;                          // 显示时间戳
    int key_frame;                        // 是否为关键帧
    int sample_rate;                      // 音频采样率
    uint64_t channel_layout;              // 声道布局
} AVFrame;

这里有个容易混淆的点——linesize。它不是简单的 width * bytes_per_pixel。因为内存对齐的原因,linesize 往往比实际宽度大。我在做图像缩放时踩过这个坑,直接按 width 去逐行拷贝,结果图像边缘出现了奇怪的条纹。

💡 小技巧: 遍历 AVFrame 的像素数据时,一定要用 linesize 作为步长,而不是 width。对于 YUV420P 格式,Y 分量的 linesize 是 width 对齐到 32 或 64 的结果,UV 分量是它的一半。

3.3 AVCodecContext:编解码器的配置中心

AVCodecContext 是整个编解码过程的「大脑」。它告诉编码器用什么参数压缩,告诉解码器怎么理解码流。

我把它常用的配置项分成了三类:

类别 关键字段 说明
基础参数 codec_type, codec_id 指定是音频还是视频,用什么编码标准
视频参数 width, height, pix_fmt, framerate 分辨率、像素格式、帧率
音频参数 sample_rate, channels, sample_fmt 采样率、声道数、采样格式
码率控制 bit_rate, rc_min_rate, rc_max_rate 目标码率、码率控制范围
编码预设 profile, level, preset 编码档次、级别、速度预设

举个例子,你要编码 H.264 视频:

AVCodecContext *ctx = avcodec_alloc_context3(codec);
ctx->width = 1920;
ctx->height = 1080;
ctx->pix_fmt = AV_PIX_FMT_YUV420P;
ctx->time_base = (AVRational){1, 30};  // 30fps
ctx->bit_rate = 4000000;               // 4Mbps
ctx->profile = FF_PROFILE_H264_HIGH;
ctx->level = 41;                       // Level 4.1
ctx->gop_size = 30;                    // 每30帧一个关键帧
🔑 关键点: time_base 和 framerate 是两个不同的概念。time_base 是时间戳的刻度单位,framerate 是帧率。编码时用 time_base,显示时用 framerate。我见过有人把这两个搞混,导致生成的视频时长计算错误。

3.4 三者的协作关系

它们怎么配合工作的?我给你画个流程:

  1. 解码场景: 读取文件 → AVPacket(压缩数据)→ avcodec_send_packet() → avcodec_receive_frame() → AVFrame(原始数据)
  2. 编码场景: 采集设备 → AVFrame(原始数据)→ avcodec_send_frame() → avcodec_receive_packet() → AVPacket(压缩数据)

AVCodecContext 全程都在后台控制着编解码器的行为。你想想看,没有它,解码器连视频的分辨率都不知道,怎么解出正确的图像?

嗯,这里要注意一个细节:AVPacket 和 AVFrame 的生命周期管理。它们都使用了引用计数机制。你用 av_packet_alloc() 分配一个包,用完后要 av_packet_free()。但如果你只是从解码器里取出来的包,用 av_packet_unref() 释放引用即可,不用 free。

⚠️ 内存泄漏重灾区: 很多人在循环解码时,忘记调用 av_frame_unref() 释放上一帧的引用。循环跑个几千帧,内存就爆了。我的习惯是:每次从解码器拿到 AVFrame 后,用完立刻 unref,绝不拖延。

3.5 实战中的常见陷阱

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

  • AVPacket 的 duration 字段: 对于视频,它表示这一帧的持续时间。对于音频,它表示这个包包含的采样数对应的时长。不要混用。
  • AVFrame 的 pts 需要重新计算: 解码器返回的 pts 是基于码流时间基的。你要用 av_rescale_q() 把它转换到输出上下文的时间基上。
  • AVCodecContext 的 extradata: 某些编码器(如 H.264)需要额外的配置数据(SPS/PPS)。这些数据在 avcodec_open2() 之前就要设置好。

我记得有一次做直播推流,编码出来的视频在 VLC 上能播,但在浏览器上就是黑屏。查了半天,发现是 extradata 没有正确传递。浏览器端的解码器拿不到 SPS/PPS,自然解不出来。从那以后,我对 extradata 的处理就格外小心。

好了,这三个结构体就讲到这里。你只要记住:AVPacket 是快递包裹,AVFrame 是拆开的实物,AVCodecContext 是分拣规则。下一章咱们聊聊怎么用它们搭一个完整的解码管道。