🎬 FFmpeg 核心 API 系列:av_read_frame / avcodec_send_packet / avcodec_receive_frame 全解析
📅 更新时间:2025年10月5日
🏷️ 标签:FFmpeg | 多媒体处理 | 音视频编程 | C/C++ | 流媒体
文章目录
- [📖 前言](#📖 前言)
- [🎯 三个核心API详解](#🎯 三个核心API详解)
-
- [API 1️⃣:`av_read_frame` - 读取压缩数据包](#API 1️⃣:
av_read_frame
- 读取压缩数据包) - [API 2️⃣:`avcodec_send_packet` - 发送数据包给解码器](#API 2️⃣:
avcodec_send_packet
- 发送数据包给解码器) - [API 3️⃣:`avcodec_receive_frame` - 接收解码后的帧](#API 3️⃣:
avcodec_receive_frame
- 接收解码后的帧)
- [API 1️⃣:`av_read_frame` - 读取压缩数据包](#API 1️⃣:
- [🔧 辅助API:内存管理](#🔧 辅助API:内存管理)
-
- [AVPacket 内存管理](#AVPacket 内存管理)
-
- [`av_packet_alloc` - 分配数据包](#
av_packet_alloc
- 分配数据包) - [`av_packet_unref` - 释放数据包引用](#
av_packet_unref
- 释放数据包引用) - [`av_packet_free` - 释放数据包](#
av_packet_free
- 释放数据包)
- [`av_packet_alloc` - 分配数据包](#
- [AVFrame 内存管理](#AVFrame 内存管理)
-
- [`av_frame_alloc` - 分配帧](#
av_frame_alloc
- 分配帧) - [`av_frame_unref` - 释放帧引用](#
av_frame_unref
- 释放帧引用) - [`av_frame_free` - 释放帧](#
av_frame_free
- 释放帧)
- [`av_frame_alloc` - 分配帧](#
- 内存管理对比
- [🔑 关键数据结构](#🔑 关键数据结构)
-
- AVPacket(压缩数据包)
-
- [PTS 和 DTS 的区别](#PTS 和 DTS 的区别)
- AVFrame(解码后的帧)
- [🔄 完整解码流程](#🔄 完整解码流程)
- [💻 完整小Demo](#💻 完整小Demo)
- [⚠️ 常见错误与注意事项](#⚠️ 常见错误与注意事项)
- [📋 总结](#📋 总结)
📖 前言
回顾前两个阶段,我们已经能够:
- 阶段一 :打开文件 → 得到
AVFormatContext
和AVStream
- 阶段二 :查找解码器 → 配置并打开
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)
------需要更多输入数据
关键要点
- 读取的是压缩数据:不是解码后的图像,是H.264/AAC等编码格式的数据
- 可能是任意流 :需要通过
packet->stream_index
判断是哪个流 - 需要提前分配AVPacket :使用
av_packet_alloc()
- 使用后必须释放引用 :调用
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
:其他错误
作用
将压缩数据包送入解码器进行解码
关键要点
- 只是发送,不立即返回结果 :解码是异步的!!!
- 会创建内部拷贝 :发送后可以立即
unref
packet - 可能需要多次发送:一个packet可能解码出多个frame
- 需要判断流索引:只发送需要的流的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)
:需要发送更多packetAVERROR_EOF
:解码器已刷新完毕< 0
:其他错误
作用
从解码器获取解码后的帧数据
关键要点
- 需要循环调用:一个packet可能解码出多个frame(特别是音频)
- frame需要提前分配 :使用
av_frame_alloc()
- 使用后必须释放引用 :调用
av_frame_unref()
- 返回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 系列教程将持续更新 🔥!