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 - 读取压缩数据包

函数原型

cpp 复制代码
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()

基本用法

cpp 复制代码
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

cpp 复制代码
#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 - 发送数据包给解码器

函数原型

cpp 复制代码
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

基本用法

cpp 复制代码
// 只发送视频流的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 - 接收解码后的帧

函数原型

cpp 复制代码
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(需要更多输入)

基本用法

cpp 复制代码
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 - 分配数据包

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

av_packet_unref - 释放数据包引用

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

av_packet_free - 释放数据包

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

AVFrame 内存管理

av_frame_alloc - 分配帧

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

av_frame_unref - 释放帧引用

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

av_frame_free - 释放帧

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

内存管理对比

结构体 分配 释放引用 释放结构体
AVPacket av_packet_alloc() av_packet_unref() av_packet_free()
AVFrame av_frame_alloc() av_frame_unref() av_frame_free()

重要规则

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

🔑 关键数据结构

AVPacket(压缩数据包)

cpp 复制代码
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 的区别

时间戳 全称 含义 使用场景
PTS Presentation Time Stamp 显示时间戳 决定这一帧何时显示
DTS Decode Time Stamp 解码时间戳 决定这一帧何时解码

为什么需要两个时间戳?

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

AVFrame(解码后的帧)

cpp 复制代码
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帧视频,显示帧信息

cpp 复制代码
#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引用

cpp 复制代码
// ❌ 错误:内存泄漏
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

cpp 复制代码
// ❌ 错误:每次都创建新的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

cpp 复制代码
// ❌ 错误:把所有流的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没有循环调用

cpp 复制代码
// ❌ 错误:一个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

cpp 复制代码
// ❌ 错误: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

cpp 复制代码
// ❌ 错误:假设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 发送给解码器 每个需要解码的packet 0成功,EAGAIN需接收
avcodec_receive_frame 接收解码后的帧 循环调用(一个packet可能多个frame) 0成功,EAGAIN需发送

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

相关推荐
humors2211 天前
批量M3U8转MP4工具
ffmpeg·视频·mp4·多媒体·转换·m3u8
神洛华1 天前
FFmpeg 全面教程:从安装到高级应用
ffmpeg
筏.k2 天前
FFmpeg 核心 API 系列:avcodec_find_decoder / avcodec_alloc_context3 / avcodec_open2
ffmpeg
Everbrilliant892 天前
Xcode上编译调试ffmpeg
macos·ffmpeg·xcode·ffmpeg源码编译工具·xcode调试ffmpeg源码·ffmpeg工具环境变量配置
molihuan2 天前
开源 全平台 哔哩哔哩缓存视频合并 Github地址:https://github.com/molihuan/hlbmerge_flutter
android·flutter·缓存·ffmpeg·开源·github·音视频
爱吃牛肉的大老虎3 天前
FFmpeg和ZLMediaKit 实现本地视频推流
ffmpeg·音视频
liliangcsdn3 天前
基于ollama运行27b gemma3解决ffmpeg命令生成问题
人工智能·ffmpeg
Everbrilliant894 天前
音视频编解码全流程之用Extractor后Decodec
ffmpeg·视频编解码·mediacodec·音视频解码·ffmpeg编解码·decodec·ndkmediacodec
Industio_触觉智能5 天前
瑞芯微RK35XX系列FFmpeg硬件编解码实测,详细性能对比!
ffmpeg·rk3588·rk3568·编解码·rk3562·rk3576