3、FFmpeg核心数据结构:AVFormatContext、AVCodecContext、AVPacket、AVFrame的深入理解与使用

说实话,FFmpeg 这四个核心数据结构,我当年刚接触时也绕了不少弯路。你想想看,一个视频文件从磁盘读到内存,再到解码出图像,中间要经过多少层?每一层都有对应的结构体在管事儿。今天我就带你把这四个"大管家"彻底捋清楚。

一句话总结:AVFormatContext 管"容器",AVCodecContext 管"编解码器",AVPacket 管"压缩数据",AVFrame 管"原始数据"。四者配合,完成音视频的完整处理流程。

FFmpeg 核心数据结构关系图 输入文件 .mp4 / .flv / .ts AVFormatContext 容器层:解封装 获取流信息、元数据 AVCodecContext 编解码器上下文 参数配置、编解码状态 AVPacket 压缩后的数据包 H.264 / AAC 裸流 AVFrame 解码后的原始数据 YUV / PCM 数据 解封装 → 读取 解码 → 输出

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 四者配合的完整流程

这四个结构体不是孤立的,它们串起来就是一条完整的处理流水线:

  1. avformat_open_input → 创建 AVFormatContext,打开容器
  2. avformat_find_stream_info → 获取流信息,填充 streams 数组
  3. avcodec_find_decoder + avcodec_alloc_context3 → 创建 AVCodecContext
  4. avcodec_parameters_to_context → 把流参数同步到编解码上下文
  5. av_read_frame → 读取 AVPacket(压缩数据)
  6. avcodec_send_packet → 把 AVPacket 送入解码器
  7. avcodec_receive_frame → 从解码器取出 AVFrame(原始数据)
  8. 处理 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 调用图,写代码时心里就有底了。

公众号:蓝海资料掘金营,微信deep3321