FFmpeg 核心 API 系列:av_read_frame / avcodec_send_packet / avcodec_receive_frame - 指南

FFmpeg 核心 API 系列:av_read_frame / avcodec_send_packet / avcodec_receive_frame 全解析
更新时间:2025年10月5日
️ 标签:FFmpeg | 多媒体处理 | 音视频编程 | C/C++ | 流媒体


前言

回顾前两个阶段,我们已经能够:

  • 阶段一:打开文件 → 得到 AVFormatContextAVStream
  • 阶段二:查找解码器 → 配置并打开 AVCodecContext

现在解码器已经准备好了,但还没有真正开始解码!就像你买了一台榨汁机(解码器),但还没有往里面放水果(压缩数据)

本阶段的核心任务

从文件读取压缩数据(AVPacket)→ 发送给解码器 → 接收解码后的帧(AVFrame)

这就是真正的解码过程


三个核心API详解

API 1️⃣:av_read_frame - 读取压缩数据包

函数原型

int av_read_frame(AVFormatContext *s, AVPacket *pkt);

参数说明

参数说明
s格式上下文(已打开的文件)
pkt数据包指针(用于接收读取的数据)

返回值

  • >= 0:成功,返回0
  • < 0:失败或文件结束(AVERROR_EOF

作用

从文件中读取一个压缩数据包(AVPacket)

重要概念:Packet 与 Frame 的关系

一个 Packet ≠ 一个 Frame,它们的关系是:

情况说明举例
1个packet = 1个frame最常见的情况视频的I帧、P帧
1个packet = 多个frame音频常见一个AAC packet包含1024个采样(多个音频帧)
多个packet = 1个frame大帧被分片超大关键帧被切分存储
1个packet < 1个frame不完整的帧数据帧数据跨packet边界

解码器的缓冲机制

  • 发送packet后,如果数据不足以组成完整的frame,解码器会缓存数据
  • 继续发送更多packet,直到能解码出完整frame
  • 这就是为什么avcodec_receive_frame可能返回AVERROR(EAGAIN)——需要更多输入数据

关键要点

  1. 读取的是压缩数据:不是解码后的图像,是H.264/AAC等编码格式的数据
  2. 可能是任意流:需要通过packet->stream_index判断是哪个流
  3. 需要提前分配AVPacket:使用av_packet_alloc()
  4. 使用后必须释放引用:调用av_packet_unref()

基本用法

AVPacket* packet = av_packet_alloc();
while (av_read_frame(avfc, packet) >= 0)
{
qDebug() << "读取到一包数据,大小:" << packet->size << "字节";
  qDebug() << "所属流索引:" << packet->stream_index;
    qDebug() << "时间戳PTS:" << packet->pts;
      // 使用完必须释放引用
      av_packet_unref(packet);
      }
      // 最后释放packet
      av_packet_free(&packet);

练习小Demo

#include "mainwindow.h"
#include <QDebug>
  #include <QApplication>
    extern "C" {
    #include <libavformat/avformat.h>
      #include <libavcodec/avcodec.h>
        }
        int main(int argc, char *argv[])
        {
        QApplication a(argc, argv);
        QString path = "video.mp4";
        AVFormatContext* avfc = nullptr;
        // 打开文件
        if (avformat_open_input(&avfc, path.toUtf8().data(), nullptr, nullptr) < 0) {
        qDebug() << "打开文件失败";
        return -1;
        }
        qDebug() << "开始读取数据包...";
        AVPacket* packet = av_packet_alloc();
        int count = 0;
        // 读取前10个packet
        while (av_read_frame(avfc, packet) >= 0 && count < 10)
        {
        count++;
        qDebug() << "=== 第" << count << "个packet ===";
        qDebug() << "大小:" << packet->size << "字节";
          qDebug() << "流索引:" << packet->stream_index;
            av_packet_unref(packet);
            }
            av_packet_free(&packet);
            avformat_close_input(&avfc);
            return 0;
            }

输出结果示例

开始读取数据包...
=== 第1个packet ===
大小: 45628 字节
流索引: 0
=== 第2个packet ===
大小: 1024 字节
流索引: 1
...

API 2️⃣:avcodec_send_packet - 发送数据包给解码器

函数原型

int avcodec_send_packet(AVCodecContext *avctx, const AVPacket *avpkt);

参数说明

参数说明
avctx解码器上下文(已打开的解码器)
avpkt要解码的数据包

返回值

  • 0成功
  • AVERROR(EAGAIN):需要先读取输出帧(调用avcodec_receive_frame
  • AVERROR_EOF:解码器已刷新完毕
  • < 0:其他错误

作用

将压缩数据包送入解码器进行解码


关键要点

  1. 只是发送,不立即返回结果解码是异步的!!!
  2. 会创建内部拷贝:发送后可以立即unref packet
  3. 可能需要多次发送:一个packet可能解码出多个frame
  4. 需要判断流索引:只发送需要的流的packet

基本用法

// 只发送视频流的packet
if (packet->stream_index == video_stream_index)
{
int ret = avcodec_send_packet(decoder_ctx, packet);
if (ret == 0) {
qDebug() << "数据包发送成功";
} else if (ret == AVERROR(EAGAIN)) {
qDebug() << "解码器缓冲区满,需要先接收帧";
} else {
qDebug() << "发送失败,错误码:" << ret;
}
}

API 3️⃣:avcodec_receive_frame - 接收解码后的帧

函数原型

int avcodec_receive_frame(AVCodecContext *avctx, AVFrame *frame);

参数说明

参数说明
avctx解码器上下文
frame帧指针(用于接收解码后的数据)

返回值

  • 0成功,frame包含解码后的数据
  • AVERROR(EAGAIN):需要发送更多packet
  • AVERROR_EOF:解码器已刷新完毕
  • < 0:其他错误

作用

从解码器获取解码后的帧数据


关键要点

  1. 需要循环调用:一个packet可能解码出多个frame(特别是音频)
  2. frame需要提前分配:使用av_frame_alloc()
  3. 使用后必须释放引用:调用av_frame_unref()
  4. 返回EAGAIN是正常的:表示解码器缓冲区数据不足以组成完整frame,需要发送更多packet

为什么会返回 AVERROR(EAGAIN)?

原因说明
数据不足当前packet只包含半帧数据,解码器缓存等待更多数据
B帧延迟视频中的B帧需要后续帧才能解码
解码器预读解码器需要缓冲几个packet才开始输出

典型流程

发送packet1 → receive返回EAGAIN(数据不足)
发送packet2 → receive返回EAGAIN(还不够)
发送packet3 → receive成功,输出frame1
            → receive成功,输出frame2(一个packet产生多帧)
            → receive返回EAGAIN(需要更多输入)

基本用法

AVFrame* frame = av_frame_alloc();
// 循环接收所有解码后的帧
while (avcodec_receive_frame(decoder_ctx, frame) == 0)
{
qDebug() << "成功解码一帧";
qDebug() << "分辨率:" << frame->width << "x" << frame->height;
  qDebug() << "像素格式:" << frame->format;
    // 使用完必须释放引用
    av_frame_unref(frame);
    }
    // 最后释放frame
    av_frame_free(&frame);

辅助API:内存管理

AVPacket 内存管理

av_packet_alloc - 分配数据包

AVPacket* av_packet_alloc(void);
  • 作用:分配一个AVPacket结构体
  • 返回:AVPacket指针,失败返回NULL

av_packet_unref - 释放数据包引用

void av_packet_unref(AVPacket *pkt);
  • 作用:释放packet内部的数据缓冲区,但不释放packet本身
  • 使用场景:每次av_read_frame后必须调用

av_packet_free - 释放数据包

void av_packet_free(AVPacket **pkt);
  • 作用:释放packet本身的内存
  • 使用场景:程序结束前调用

AVFrame 内存管理

av_frame_alloc - 分配帧

AVFrame* av_frame_alloc(void);
  • 作用:分配一个AVFrame结构体
  • 返回:AVFrame指针,失败返回NULL

av_frame_unref - 释放帧引用

void av_frame_unref(AVFrame *frame);
  • 作用:释放frame内部的数据缓冲区,但不释放frame本身
  • 使用场景:每次avcodec_receive_frame后必须调用

av_frame_free - 释放帧

void av_frame_free(AVFrame **frame);
  • 作用:释放frame本身的内存
  • 使用场景:程序结束前调用

内存管理对比

结构体分配释放引用释放结构体
AVPacketav_packet_alloc()av_packet_unref()av_packet_free()
AVFrameav_frame_alloc()av_frame_unref()av_frame_free()

重要规则

  • alloc只调用一次(循环外)
  • unref每次使用后都要调用(循环内)
  • free在程序结束前调用(清理阶段)

关键数据结构

AVPacket(压缩数据包)

AVPacket* packet;
// 核心字段
packet->data;           // 压缩数据指针
packet->size;           // 数据大小(字节)
packet->pts;            // 显示时间戳(Presentation Time Stamp)
packet->dts;            // 解码时间戳(Decode Time Stamp)
packet->stream_index;   // 所属流的索引
packet->duration;       // 该包的持续时间
packet->flags;          // 标志位(如关键帧:AV_PKT_FLAG_KEY)

PTS 和 DTS 的区别

时间戳全称含义使用场景
PTSPresentation Time Stamp显示时间戳决定这一帧何时显示
DTSDecode Time Stamp解码时间戳决定这一帧何时解码

为什么需要两个时间戳?

  • 视频中有B帧(双向预测帧),解码顺序和显示顺序不同
  • 例如:显示顺序 I-B-P,解码顺序 I-P-B
  • DTS:I(0) → P(1) → B(2)
  • PTS:I(0) → B(1) → P(2)

AVFrame(解码后的帧)

AVFrame* frame;
// 视频帧核心字段
frame->data[8];         // 数据平面指针数组(YUV通常是3个平面)
frame->linesize[8];     // 每行的字节数(包含对齐)
frame->width;           // 宽度
frame->height;          // 高度
frame->format;          // 像素格式(如AV_PIX_FMT_YUV420P)
frame->pts;             // 时间戳
frame->key_frame;       // 是否是关键帧
// 音频帧核心字段
frame->nb_samples;      // 采样数
frame->sample_rate;     // 采样率
frame->channels;        // 声道数
frame->channel_layout;  // 声道布局

视频帧的数据布局(以YUV420P为例)

YUV420P格式:
data[0] → Y平面(亮度)  宽×高
data[1] → U平面(色度)  宽/2 × 高/2
data[2] → V平面(色度)  宽/2 × 高/2
linesize[0] → Y平面每行字节数
linesize[1] → U平面每行字节数
linesize[2] → V平面每行字节数

完整解码流程

标准流程图

┌─────────────────────┐
│  av_packet_alloc()  │  ← 分配packet(循环外)
└──────────┬──────────┘
           ↓
┌─────────────────────┐
│  av_frame_alloc()   │  ← 分配frame(循环外)
└──────────┬──────────┘
           ↓
      ┌────────┐
   ┌──│  循环  │──┐
   │  └────────┘  │
   │      ↓       │
   │ ┌─────────────────────┐
   │ │  av_read_frame()    │  ← 读取packet
   │ └──────────┬──────────┘
   │            ↓
   │ ┌─────────────────────┐
   │ │ 判断stream_index    │  ← 是否是目标流?
   │ └──────────┬──────────┘
   │            ↓ 是
   │ ┌─────────────────────┐
   │ │ avcodec_send_packet │  ← 发送给解码器
   │ └──────────┬──────────┘
   │            ↓
   │       ┌────────┐
   │    ┌──│ 内循环 │──┐
   │    │  └────────┘  │
   │    │      ↓       │
   │    │ ┌─────────────────────┐
   │    │ │avcodec_receive_frame│  ← 接收解码后的帧
   │    │ └──────────┬──────────┘
   │    │            ↓
   │    │ ┌─────────────────────┐
   │    │ │  处理frame数据      │  ← 显示、保存等
   │    │ └──────────┬──────────┘
   │    │            ↓
   │    │ ┌─────────────────────┐
   │    │ │  av_frame_unref()   │  ← 释放frame引用
   │    │ └──────────┬──────────┘
   │    │            ↓
   │    └────────────┘
   │            ↓
   │ ┌─────────────────────┐
   │ │  av_packet_unref()  │  ← 释放packet引用
   │ └──────────┬──────────┘
   │            ↓
   └────────────┘
           ↓
┌─────────────────────┐
│  av_frame_free()    │  ← 释放frame(循环外)
└──────────┬──────────┘
           ↓
┌─────────────────────┐
│  av_packet_free()   │  ← 释放packet(循环外)
└─────────────────────┘

完整小Demo

目标:打开视频文件,解码前10帧视频,显示帧信息

#include "mainwindow.h"
#include <QDebug>
  #include <QApplication>
    extern "C" {
    #include <libavformat/avformat.h>
      #include <libavcodec/avcodec.h>
        #include <libavutil/avutil.h>
          }
          int main(int argc, char *argv[])
          {
          QApplication a(argc, argv);
          QString path = "E:/video.mp4";
          AVFormatContext* avfc = nullptr;
          // ===== 阶段一:打开文件 =====
          int result = avformat_open_input(&avfc, path.toUtf8().data(), nullptr, nullptr);
          if (result < 0) {
          qDebug() << "打开文件失败";
          return -1;
          }
          qDebug() << "打开文件成功";
          // ===== 阶段二:查找并打开解码器 =====
          result = av_find_best_stream(avfc, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
          if (result < 0) {
          qDebug() << "未找到视频流";
          return -1;
          }
          int video_index = result;
          qDebug() << "找到视频流,索引ID为:" << video_index;
          AVStream* stream = avfc->streams[video_index];
          // 查找解码器
          const AVCodec* decoder = avcodec_find_decoder(stream->codecpar->codec_id);
          if (!decoder) {
          qDebug() << "未找到解码器";
          return -1;
          }
          qDebug() << "找到解码器:" << decoder->name;
            // 分配解码器上下文
            AVCodecContext* decoder_ctx = avcodec_alloc_context3(decoder);
            // 复制参数
            if (avcodec_parameters_to_context(decoder_ctx, stream->codecpar) < 0) {
            qDebug() << "复制参数失败";
            return -1;
            }
            // 打开解码器
            if (avcodec_open2(decoder_ctx, decoder, nullptr) < 0) {
            qDebug() << "打开解码器失败";
            return -1;
            }
            qDebug() << "解码器打开成功";
            qDebug() << "";
            // ===== 阶段三:读取并解码 =====
            AVPacket* packet = av_packet_alloc();
            AVFrame* frame = av_frame_alloc();
            int frame_count = 0;
            qDebug() << "========== 开始解码视频 ==========";
            while (av_read_frame(avfc, packet) >= 0 && frame_count < 10)
            {
            // 只处理视频流的packet
            if (packet->stream_index != video_index) {
            av_packet_unref(packet);
            continue;
            }
            // 发送packet给解码器
            if (avcodec_send_packet(decoder_ctx, packet) == 0)
            {
            // 接收解码后的frame(可能有多个)
            while (avcodec_receive_frame(decoder_ctx, frame) == 0 && frame_count < 10)
            {
            frame_count++;
            qDebug() << "=== 第" << frame_count << "帧 ===";
            qDebug() << "分辨率:" << frame->width << "x" << frame->height;
              qDebug() << "像素格式:" << frame->format;
                qDebug() << "PTS:" << frame->pts;
                  qDebug() << "是否关键帧:" << (frame->key_frame ? "是" : "否");
                    qDebug() << "";
                    av_frame_unref(frame);
                    }
                    }
                    av_packet_unref(packet);
                    }
                    qDebug() << "解码完成,总共解码了" << frame_count << "帧";
                    // ===== 清理资源 =====
                    av_frame_free(&frame);
                    av_packet_free(&packet);
                    avcodec_free_context(&decoder_ctx);
                    avformat_close_input(&avfc);
                    qDebug() << "程序结束";
                    return 0;
                    }

输出结果

打开文件成功
找到视频流,索引ID为: 0
找到解码器: h264
解码器打开成功
========== 开始解码视频 ==========
=== 第1帧 ===
分辨率: 1280 x 720
像素格式: 0
PTS: 0
是否关键帧: 是
=== 第2帧 ===
分辨率: 1280 x 720
像素格式: 0
PTS: 512
是否关键帧: 否
=== 第3帧 ===
分辨率: 1280 x 720
像素格式: 0
PTS: 1024
是否关键帧: 否
...(省略中间帧)...
=== 第10帧 ===
分辨率: 1280 x 720
像素格式: 0
PTS: 4608
是否关键帧: 否
解码完成,总共解码了 10 帧
程序结束

⚠️ 常见错误与注意事项

错误1:忘记释放packet引用

// ❌ 错误:内存泄漏
while (av_read_frame(avfc, packet) >= 0) {
// 处理packet
// 忘记调用av_packet_unref(packet);
}
// ✅ 正确:每次使用后必须unref
while (av_read_frame(avfc, packet) >= 0) {
// 处理packet
av_packet_unref(packet);  // 必须调用
}

错误2:在循环内重复分配frame

// ❌ 错误:每次都创建新的frame,导致内存泄漏
while (av_read_frame(avfc, packet) >= 0) {
AVFrame* frame = av_frame_alloc();  // 错误!
avcodec_receive_frame(decoder_ctx, frame);
av_frame_unref(frame);
}
// ✅ 正确:循环外分配一次,循环内复用
AVFrame* frame = av_frame_alloc();
while (av_read_frame(avfc, packet) >= 0) {
avcodec_receive_frame(decoder_ctx, frame);
av_frame_unref(frame);  // 只释放引用,不释放frame本身
}
av_frame_free(&frame);  // 循环结束后释放

错误3:忘记判断stream_index

// ❌ 错误:把所有流的packet都发给视频解码器
while (av_read_frame(avfc, packet) >= 0) {
avcodec_send_packet(video_decoder, packet);  // 可能发送了音频packet
}
// ✅ 正确:只发送视频流的packet
while (av_read_frame(avfc, packet) >= 0) {
if (packet->stream_index == video_stream_index) {
avcodec_send_packet(video_decoder, packet);
}
av_packet_unref(packet);
}

错误4:receive_frame没有循环调用

// ❌ 错误:一个packet可能产生多个frame
if (avcodec_send_packet(decoder_ctx, packet) == 0) {
avcodec_receive_frame(decoder_ctx, frame);  // 只调用一次
}
// ✅ 正确:循环接收所有frame
if (avcodec_send_packet(decoder_ctx, packet) == 0) {
while (avcodec_receive_frame(decoder_ctx, frame) == 0) {
// 处理frame
av_frame_unref(frame);
}
}

错误5:packet发送后没有unref

// ❌ 错误:packet引用没有释放
if (packet->stream_index == video_index) {
avcodec_send_packet(decoder_ctx, packet);
// 忘记unref
}
// ✅ 正确:发送后立即unref
if (packet->stream_index == video_index) {
avcodec_send_packet(decoder_ctx, packet);
}
av_packet_unref(packet);  // 无论是否发送,都要unref

错误6:误以为一个packet必定对应一个frame

// ❌ 错误:假设1个packet = 1个frame
while (av_read_frame(avfc, packet) >= 0) {
if (packet->stream_index == video_index) {
avcodec_send_packet(decoder_ctx, packet);
avcodec_receive_frame(decoder_ctx, frame);  // 可能EAGAIN或漏掉多余的frame
// 处理frame
av_frame_unref(frame);
}
av_packet_unref(packet);
}
// ✅ 正确:理解packet和frame的复杂关系
while (av_read_frame(avfc, packet) >= 0) {
if (packet->stream_index == video_index) {
avcodec_send_packet(decoder_ctx, packet);
// 循环接收,因为可能有多个frame
while (avcodec_receive_frame(decoder_ctx, frame) == 0) {
// 处理frame
av_frame_unref(frame);
}
// 返回EAGAIN是正常的,说明需要更多packet
}
av_packet_unref(packet);
}

关键理解

  • ❌ 不要假设 1 packet = 1 frame
  • ✅ packet是存储单元,frame是逻辑单元
  • ✅ 它们是多对多的关系
  • ✅ 解码器内部有缓冲区,会根据需要输出frame

总结

核心流程回顾

1. av_packet_alloc() / av_frame_alloc()  ← 分配内存(一次)
2. while (av_read_frame() >= 0)          ← 循环读取packet
3.     判断 stream_index                  ← 筛选目标流
4.     avcodec_send_packet()              ← 发送给解码器
5.     while (avcodec_receive_frame())    ← 循环接收frame
6.         处理frame数据                  ← 显示、保存等
7.         av_frame_unref()               ← 释放frame引用
8.     av_packet_unref()                  ← 释放packet引用
9. av_frame_free() / av_packet_free()    ← 释放内存(一次)

三个API对比

API功能调用时机返回值
av_read_frame读取压缩数据包循环调用0成功,<0失败/EOF
avcodec_send_packet发送给解码器每个需要解码的packet0成功,EAGAIN需接收
avcodec_receive_frame接收解码后的帧循环调用(一个packet可能多个frame)0成功,EAGAIN需发送

如果您觉得这篇文章对您有帮助,不妨点赞 + 收藏 + 关注,更多 FFmpeg 系列教程将持续更新 !

posted on 2025-11-06 16:33  wgwyanfs  阅读(113)  评论(0)    收藏  举报

导航