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 赋值反了。
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 去逐行拷贝,结果图像边缘出现了奇怪的条纹。
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帧一个关键帧
3.4 三者的协作关系
它们怎么配合工作的?我给你画个流程:
- 解码场景: 读取文件 → AVPacket(压缩数据)→ avcodec_send_packet() → avcodec_receive_frame() → AVFrame(原始数据)
- 编码场景: 采集设备 → AVFrame(原始数据)→ avcodec_send_frame() → avcodec_receive_packet() → AVPacket(压缩数据)
AVCodecContext 全程都在后台控制着编解码器的行为。你想想看,没有它,解码器连视频的分辨率都不知道,怎么解出正确的图像?
嗯,这里要注意一个细节:AVPacket 和 AVFrame 的生命周期管理。它们都使用了引用计数机制。你用 av_packet_alloc() 分配一个包,用完后要 av_packet_free()。但如果你只是从解码器里取出来的包,用 av_packet_unref() 释放引用即可,不用 free。
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 是分拣规则。下一章咱们聊聊怎么用它们搭一个完整的解码管道。