3、FFmpeg核心数据结构:AVFormatContext、AVCodecContext、AVPacket、AVFrame的深入理解与使用
说实话,FFmpeg 这四个核心数据结构,我当年刚接触时也绕了不少弯路。你想想看,一个视频文件从磁盘读到内存,再到解码出图像,中间要经过多少层?每一层都有对应的结构体在管事儿。今天我就带你把这四个"大管家"彻底捋清楚。
一句话总结:AVFormatContext 管"容器",AVCodecContext 管"编解码器",AVPacket 管"压缩数据",AVFrame 管"原始数据"。四者配合,完成音视频的完整处理流程。
3.1 AVFormatContext —— 容器的"总管家"
AVFormatContext 是 FFmpeg 里最外层的结构体。它代表一个多媒体文件的"容器格式"。说白了,它管的是文件层面的东西:文件里有多少路流?每路流是视频还是音频?元数据是什么?
我个人习惯把 AVFormatContext 比作一个"文件柜"。打开一个视频文件时,先创建这个文件柜,然后往里塞各种信息。
// 打开文件,获取 AVFormatContext
AVFormatContext *fmt_ctx = NULL;
int ret = avformat_open_input(&fmt_ctx, "input.mp4", NULL, NULL);
if (ret < 0) {
// 处理错误
}
// 读取流信息
ret = avformat_find_stream_info(fmt_ctx, NULL);
// 遍历所有流
for (int i = 0; i < fmt_ctx->nb_streams; i++) {
AVStream *stream = fmt_ctx->streams[i];
// 每个 stream 里藏着 codecpar 参数
}
我的经验:avformat_open_input 只打开容器头部,不会读取所有流信息。一定要调用 avformat_find_stream_info 才能拿到完整的流参数。我曾经踩过这个坑——没调这个函数就直接去解码,结果 codecpar 里的宽高全是 0。
3.2 AVCodecContext —— 编解码器的"控制面板"
AVCodecContext 是编解码器的上下文。它管理着编解码过程中的所有参数:编码器类型、码率、帧率、像素格式、GOP 大小等等。
你想想看,同样的 H.264 编码器,配置不同的参数,出来的码流天差地别。AVCodecContext 就是那个让你拧旋钮的地方。
// 从流中获取编解码器参数
AVCodecParameters *codecpar = stream->codecpar;
// 查找对应的解码器
const AVCodec *codec = avcodec_find_decoder(codecpar->codec_id);
// 创建 AVCodecContext
AVCodecContext *codec_ctx = avcodec_alloc_context3(codec);
// 关键一步:把参数复制到上下文
avcodec_parameters_to_context(codec_ctx, codecpar);
// 打开解码器
avcodec_open2(codec_ctx, codec, NULL);
注意:千万不要直接操作 codecpar 里的字段去解码!必须通过 avcodec_parameters_to_context 把参数同步到 codec_ctx。这是 FFmpeg 的设计规范,绕过去会出各种诡异问题。
3.3 AVPacket —— 压缩数据的"快递包裹"
AVPacket 存放的是编码后的数据。对于视频来说,就是 H.264/H.265 的 NALU;对于音频来说,就是 AAC 或 MP3 的帧数据。
它本质上是一个"快递包裹"——里面装着压缩后的二进制数据,外面贴着标签(pts、dts、duration、stream_index)。
AVPacket *pkt = av_packet_alloc();
// 循环读取数据包
while (av_read_frame(fmt_ctx, pkt) >= 0) {
// 判断是哪个流的数据
if (pkt->stream_index == video_stream_idx) {
// 送给视频解码器
avcodec_send_packet(codec_ctx, pkt);
} else if (pkt->stream_index == audio_stream_idx) {
// 送给音频解码器
avcodec_send_packet(audio_codec_ctx, pkt);
}
// 重要:用完必须解引用
av_packet_unref(pkt);
}
av_packet_free(&pkt);
关键点:AVPacket 内部的数据是引用计数的。av_packet_unref 会减少引用计数,当计数归零时自动释放数据。千万不要手动 free 里面的 data 指针!
3.4 AVFrame —— 解码后的"真面目"
AVFrame 是解码后的原始数据。视频帧是 YUV 或 RGB 像素数据,音频帧是 PCM 采样数据。
我记得第一次看到 AVFrame 里的 data[0]、data[1]、data[2] 时有点懵。对于 YUV420P 来说:
- data[0]:Y 分量(亮度)
- data[1]:U 分量(色度)
- data[2]:V 分量(色度)
- linesize[0]:Y 分量的行字节数
- linesize[1]/linesize[2]:UV 分量的行字节数
AVFrame *frame = av_frame_alloc();
// 从解码器接收解码后的帧
while (avcodec_receive_frame(codec_ctx, frame) >= 0) {
// 此时 frame 里就是解码后的 YUV 数据
int width = frame->width;
int height = frame->height;
// 访问 Y 分量
uint8_t *y_data = frame->data[0];
int y_linesize = frame->linesize[0];
// 处理完一帧后,必须 unref
av_frame_unref(frame);
}
av_frame_free(&frame);
避坑指南:我曾经在项目里犯过一个低级错误——直接拿 linesize 当 width 用。实际上 linesize 可能比 width 大,因为有内存对齐。正确的做法是:读取像素时用 linesize 做步进,但只取前 width 个像素。
3.5 四者配合的完整流程
这四个结构体不是孤立的,它们串起来就是一条完整的处理流水线:
- avformat_open_input → 创建 AVFormatContext,打开容器
- avformat_find_stream_info → 获取流信息,填充 streams 数组
- avcodec_find_decoder + avcodec_alloc_context3 → 创建 AVCodecContext
- avcodec_parameters_to_context → 把流参数同步到编解码上下文
- av_read_frame → 读取 AVPacket(压缩数据)
- avcodec_send_packet → 把 AVPacket 送入解码器
- avcodec_receive_frame → 从解码器取出 AVFrame(原始数据)
- 处理 AVFrame → 渲染、保存、转码等
// 完整示例:读取一帧视频
AVFormatContext *fmt_ctx = NULL;
avformat_open_input(&fmt_ctx, "video.mp4", NULL, NULL);
avformat_find_stream_info(fmt_ctx, NULL);
// 找到视频流
int video_idx = -1;
for (int i = 0; i < fmt_ctx->nb_streams; i++) {
if (fmt_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
video_idx = i;
break;
}
}
// 初始化解码器
AVCodecContext *dec_ctx = avcodec_alloc_context3(NULL);
avcodec_parameters_to_context(dec_ctx, fmt_ctx->streams[video_idx]->codecpar);
avcodec_open2(dec_ctx, avcodec_find_decoder(dec_ctx->codec_id), NULL);
// 读取并解码
AVPacket *pkt = av_packet_alloc();
AVFrame *frame = av_frame_alloc();
while (av_read_frame(fmt_ctx, pkt) >= 0) {
if (pkt->stream_index == video_idx) {
avcodec_send_packet(dec_ctx, pkt);
while (avcodec_receive_frame(dec_ctx, frame) >= 0) {
// 这里拿到了一帧 YUV 数据
// 可以渲染、保存或进一步处理
av_frame_unref(frame);
}
}
av_packet_unref(pkt);
}
// 清理
av_frame_free(&frame);
av_packet_free(&pkt);
avcodec_free_context(&dec_ctx);
avformat_close_input(&fmt_ctx);
重要提醒:avcodec_send_packet 和 avcodec_receive_frame 是配对使用的。有些编码器(如 H.264)存在 B 帧,会导致 send 一次可能 receive 出多帧,或者 send 多次才 receive 出一帧。所以要用 while 循环去 receive,直到返回 AVERROR(EAGAIN) 或 AVERROR_EOF。
3.6 内存管理要点
| 结构体 | 分配函数 | 释放函数 | 数据重置 |
|---|---|---|---|
| AVFormatContext | avformat_open_input | avformat_close_input | — |
| AVCodecContext | avcodec_alloc_context3 | avcodec_free_context | avcodec_flush_buffers |
| AVPacket | av_packet_alloc | av_packet_free | av_packet_unref |
| AVFrame | av_frame_alloc | av_frame_free | av_frame_unref |
嗯,这里要注意:AVPacket 和 AVFrame 内部都有引用计数的缓冲区。每次用完一包/一帧,必须调 unref 来减少引用计数。否则内存泄漏没跑。
我个人习惯在循环里每次都用 av_packet_unref 和 av_frame_unref,而不是等到循环结束再统一释放。这样内存峰值可控,跑长时间任务不会爆。
好了,这四个核心结构体就讲到这里。你只要记住它们的职责分工,再配合那几张 API 调用图,写代码时心里就有底了。