视频解码实战:把 H.264 码流变成 YUV
前面我们学习了解封装(从 MP4 提取数据包),现在到了最关键的一步——**解码**!本篇用最通俗的语言,带你从"压缩的 H.264 码流"到"可显示的 YUV 像素",彻底搞懂 FFmpeg 的 send/receive 解码循环
07. 视频解码实战:把 H.264 码流变成 YUV
专栏导读:前面我们学习了解封装(从 MP4 提取数据包),现在到了最关键的一步——解码!本篇用最通俗的语言,带你从"压缩的 H.264 码流"到"可显示的 YUV 像素",彻底搞懂 FFmpeg 的 send/receive 解码循环。
🎬 开场:解码器是个"智能快递分拣机"
想象你在一个快递分拣中心工作:
输入: 一堆压缩的快递包裹(H.264 压缩包 AVPacket)
- 包裹上贴着标签: DTS(处理顺序)和 PTS(送达顺序)
分拣机: 解码器(AVCodecContext)
- 自动拆包、识别内容
- 内部有个仓库(缓冲区),攒够材料才出货
输出: 一张张整理好的照片(YUV 像素帧 AVFrame)
- 可以直接挂墙上展示(渲染到屏幕)
关键问题:
- 包裹什么时候拆?→ 送进去(send)的时机
- 照片什么时候拿?→ 取出来(receive)的时机
- 顺序会乱吗?→ DTS 和 PTS 的区别
让我们一步步揭秘!
📦 什么是 H.264 压缩包(AVPacket)?
定义:从解封装器(Demuxer)读取的压缩数据,还没解码。
typedef struct AVPacket {
uint8_t *data; // 压缩数据(H.264/HEVC 码流)
int size; // 数据大小(字节)
int64_t pts; // 显示时间戳(Presentation Time Stamp)
int64_t dts; // 解码时间戳(Decode Time Stamp)
int stream_index; // 属于哪个流(0=视频, 1=音频)
int flags; // 标志位(是否为关键帧)
} AVPacket;
实际大小:
1080p H.264 视频(30fps):
I 帧 (关键帧): 50 KB ~ 150 KB (完整画面)
P 帧 (预测帧): 5 KB ~ 20 KB (只存差异)
B 帧 (双向帧): 2 KB ~ 10 KB (参考前后帧)
平均:
每个 AVPacket ≈ 10-30 KB
每秒 30 个包 ≈ 300 KB ~ 1 MB

🖼️ 什么是 YUV 帧(AVFrame)?
定义:解码器输出的原始像素数据,可以直接渲染。
typedef struct AVFrame {
uint8_t *data[AV_NUM_DATA_POINTERS]; // 像素数据(YUV 三个平面)
int linesize[AV_NUM_DATA_POINTERS]; // 每行字节数(可能有对齐)
int width, height; // 分辨率
int format; // 像素格式(YUV420P/NV12 等)
int64_t pts; // 显示时间戳
int key_frame; // 是否为关键帧
} AVFrame;
YUV420P 格式:
Y 平面(亮度): width × height 字节
U 平面(色度): (width/2) × (height/2) 字节
V 平面(色度): (width/2) × (height/2) 字节
总大小 = width × height × 1.5 字节
示例(1920×1080):
Y: 1920 × 1080 = 2,073,600 字节
U: 960 × 540 = 518,400 字节
V: 960 × 540 = 518,400 字节
总计: 3,110,400 字节 ≈ 3 MB
对比:
压缩前(AVPacket): 10 KB ~ 30 KB
解压后(AVFrame): 3 MB(1080p)
压缩比: 100:1 ~ 300:1 🎉
📊 YUV420P 内存布局图

⚙️ 解码器(AVCodecContext)的工作原理
1. 解码器是个"状态机"
2. 内部缓冲区(Buffer)
解码器内部有个仓库:
输入仓库(发送缓冲):
┌─────────────────────────┐
│ Packet 1 │ Packet 2 │... │ ← avcodec_send_packet() 放进来
└─────────────────────────┘
参考帧缓存(Reference Frame Buffer):
┌─────────────────────────┐
│ I 帧 │ P 帧 │ 用于预测 │ ← 解码 B 帧时需要参考
└─────────────────────────┘
输出仓库(接收缓冲):
┌─────────────────────────┐
│ Frame 1 │ Frame 2 │... │ ← avcodec_receive_frame() 取出来
└─────────────────────────┘
关键规律:
- 送包(send)可能立即返回,也可能返回
EAGAIN(仓库满了,先取帧) - 取帧(receive)可能立即返回,也可能返回
EAGAIN(还没准备好,继续送包)
🔄 核心循环:send_packet & receive_frame
标准模式(推荐)
代码模板(伪代码)
// 打开解码器
AVCodecContext *ctx = avcodec_alloc_context3(codec);
avcodec_parameters_to_context(ctx, stream->codecpar);
avcodec_open2(ctx, codec, nullptr);
AVPacket *packet = av_packet_alloc();
AVFrame *frame = av_frame_alloc();
// 主循环
while (av_read_frame(fmt_ctx, packet) >= 0) {
if (packet->stream_index != video_stream_index) {
av_packet_unref(packet);
continue;
}
// 1️⃣ 送包
int ret = avcodec_send_packet(ctx, packet);
if (ret == AVERROR(EAGAIN)) {
// 解码器缓冲满了,先取帧
} else if (ret < 0) {
// 错误处理
break;
}
// 2️⃣ 取帧(循环)
while (true) {
ret = avcodec_receive_frame(ctx, frame);
if (ret == 0) {
// ✅ 成功取到一帧
process_frame(frame); // 处理(渲染/保存)
av_frame_unref(frame);
} else if (ret == AVERROR(EAGAIN)) {
// 需要更多包,退出内层循环
break;
} else if (ret == AVERROR_EOF) {
// 解码器已冲刷完毕
goto end;
} else {
// 错误
break;
}
}
av_packet_unref(packet);
}
// 3️⃣ 冲刷解码器(送空包)
avcodec_send_packet(ctx, nullptr);
while (avcodec_receive_frame(ctx, frame) == 0) {
process_frame(frame);
av_frame_unref(frame);
}
end:
av_frame_free(&frame);
av_packet_free(&packet);
avcodec_free_context(&ctx);
⏱️ 时间戳详解:DTS 和 PTS 的区别
场景 1:只有 I 帧和 P 帧(无 B 帧)
解码顺序 = 显示顺序
DTS = PTS
时间轴:
I₀ → P₁ → P₂ → P₃
↓ ↓ ↓ ↓
显示顺序也是 I₀ P₁ P₂ P₃
场景 2:有 B 帧(需要重排)
解码顺序(DTS): I₀ P₃ B₁ B₂ P₆ B₄ B₅
显示顺序(PTS): I₀ B₁ B₂ P₃ B₄ B₅ P₆
解释:
- B₁ 需要参考 I₀ 和 P₃,所以必须先解码 P₃
- 解码器内部会缓存 I₀ 和 P₃,然后才能解码 B₁ 和 B₂
关键规律:
对于 I 帧和 P 帧:
DTS = PTS
对于 B 帧:
DTS > PTS (先解码,后显示)
播放器同步:
永远以 PTS 为准!

🛠️ 实战 1:解码第一帧并保存为 YUV 文件
目标:读取 MP4 文件,解码第一帧视频,保存为原始 YUV 文件。
完整代码(minimal_decode.cpp)
// minimal_decode.cpp
// 编译: g++ minimal_decode.cpp -o minimal_decode $(pkg-config --cflags --libs libavformat libavcodec libavutil)
// 运行: ./minimal_decode input.mp4 output.yuv
extern "C" {
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libavutil/imgutils.h>
}
#include <cstdio>
int main(int argc, char** argv) {
if (argc < 3) {
printf("Usage: %s <input.mp4> <output.yuv>\n", argv[0]);
return 1;
}
const char* input_file = argv[1];
const char* output_file = argv[2];
// ========================================
// 步骤 1:打开输入文件(解封装)
// ========================================
AVFormatContext* fmt_ctx = nullptr;
if (avformat_open_input(&fmt_ctx, input_file, nullptr, nullptr) < 0) {
printf("❌ Failed to open input file\n");
return 1;
}
if (avformat_find_stream_info(fmt_ctx, nullptr) < 0) {
printf("❌ Failed to find stream info\n");
return 1;
}
// ========================================
// 步骤 2:查找视频流
// ========================================
int video_stream_idx = av_find_best_stream(fmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0);
if (video_stream_idx < 0) {
printf("❌ No video stream found\n");
return 1;
}
AVStream* video_stream = fmt_ctx->streams[video_stream_idx];
printf("✅ Found video stream: %dx%d, codec=%d\n",
video_stream->codecpar->width,
video_stream->codecpar->height,
video_stream->codecpar->codec_id);
// ========================================
// 步骤 3:打开解码器
// ========================================
const AVCodec* codec = avcodec_find_decoder(video_stream->codecpar->codec_id);
if (!codec) {
printf("❌ Codec not found\n");
return 1;
}
AVCodecContext* codec_ctx = avcodec_alloc_context3(codec);
if (!codec_ctx) {
printf("❌ Failed to allocate codec context\n");
return 1;
}
if (avcodec_parameters_to_context(codec_ctx, video_stream->codecpar) < 0) {
printf("❌ Failed to copy codec parameters\n");
return 1;
}
if (avcodec_open2(codec_ctx, codec, nullptr) < 0) {
printf("❌ Failed to open codec\n");
return 1;
}
printf("✅ Decoder opened: %s\n", codec->name);
// ========================================
// 步骤 4:分配 Packet 和 Frame
// ========================================
AVPacket* packet = av_packet_alloc();
AVFrame* frame = av_frame_alloc();
FILE* output_fp = fopen(output_file, "wb");
if (!packet || !frame || !output_fp) {
printf("❌ Allocation failed\n");
return 1;
}
// ========================================
// 步骤 5:解码循环(只解第一帧)
// ========================================
bool frame_decoded = false;
while (av_read_frame(fmt_ctx, packet) >= 0 && !frame_decoded) {
// 过滤非视频包
if (packet->stream_index != video_stream_idx) {
av_packet_unref(packet);
continue;
}
// 发送包到解码器
int ret = avcodec_send_packet(codec_ctx, packet);
if (ret < 0) {
printf("❌ Error sending packet to decoder\n");
break;
}
// 接收解码后的帧
while (ret >= 0) {
ret = avcodec_receive_frame(codec_ctx, frame);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
break; // 需要更多包或已结束
} else if (ret < 0) {
printf("❌ Error receiving frame from decoder\n");
break;
}
// ✅ 成功解码一帧!
printf("✅ Decoded frame: %dx%d, format=%d, pts=%ld\n",
frame->width, frame->height, frame->format, frame->pts);
// 保存 YUV420P 数据(假设格式是 YUV420P)
if (frame->format == AV_PIX_FMT_YUV420P) {
// 写 Y 平面
for (int y = 0; y < frame->height; y++) {
fwrite(frame->data[0] + y * frame->linesize[0], 1, frame->width, output_fp);
}
// 写 U 平面
for (int y = 0; y < frame->height / 2; y++) {
fwrite(frame->data[1] + y * frame->linesize[1], 1, frame->width / 2, output_fp);
}
// 写 V 平面
for (int y = 0; y < frame->height / 2; y++) {
fwrite(frame->data[2] + y * frame->linesize[2], 1, frame->width / 2, output_fp);
}
printf("✅ YUV data written to %s\n", output_file);
} else {
printf("⚠️ Pixel format is not YUV420P, got format=%d\n", frame->format);
}
frame_decoded = true;
break; // 只解第一帧
}
av_packet_unref(packet);
}
// ========================================
// 步骤 6:清理资源
// ========================================
fclose(output_fp);
av_frame_free(&frame);
av_packet_free(&packet);
avcodec_free_context(&codec_ctx);
avformat_close_input(&fmt_ctx);
if (frame_decoded) {
printf("🎉 Success! View with: ffplay -f rawvideo -pixel_format yuv420p -video_size %dx%d %s\n",
video_stream->codecpar->width,
video_stream->codecpar->height,
output_file);
}
return 0;
}
编译与运行
# Linux/macOS/WSL
g++ minimal_decode.cpp -o minimal_decode \
$(pkg-config --cflags --libs libavformat libavcodec libavutil)
# 运行
./minimal_decode input.mp4 output.yuv
# 预览 YUV 文件
ffplay -f rawvideo -pixel_format yuv420p -video_size 1920x1080 output.yuv
输出示例
✅ Found video stream: 1920x1080, codec=27
✅ Decoder opened: h264
✅ Decoded frame: 1920x1080, format=0, pts=0
✅ YUV data written to output.yuv
🎉 Success! View with: ffplay -f rawvideo -pixel_format yuv420p -video_size 1920x1080 output.yuv
🔍 关键 API 详解
1. avcodec_send_packet()
int avcodec_send_packet(AVCodecContext *avctx, const AVPacket *avpkt);
作用:向解码器发送压缩数据包。
返回值:
0:成功AVERROR(EAGAIN):解码器输入缓冲已满,需要先调用receive_frame取出一些帧AVERROR_EOF:解码器已进入冲刷模式,不再接受输入- 其他负值:错误
特殊用法:
// 冲刷解码器(Flush)
avcodec_send_packet(ctx, nullptr); // 发送空包
2. avcodec_receive_frame()
int avcodec_receive_frame(AVCodecContext *avctx, AVFrame *frame);
作用:从解码器接收解码后的帧。
返回值:
0:成功获取一帧AVERROR(EAGAIN):当前没有可用帧,需要先调用send_packet发送更多数据AVERROR_EOF:解码器已冲刷完毕,没有更多帧- 其他负值:错误
注意事项:
// ❌ 错误用法:只调用一次 receive
avcodec_send_packet(ctx, packet);
avcodec_receive_frame(ctx, frame); // 可能会漏掉多帧输出
// ✅ 正确用法:循环调用 receive
avcodec_send_packet(ctx, packet);
while (avcodec_receive_frame(ctx, frame) == 0) {
// 处理帧
}
3. av_frame_unref()
void av_frame_unref(AVFrame *frame);
作用:释放帧的引用计数,但不释放 AVFrame 结构体本身。
为什么需要:
AVFrame *frame = av_frame_alloc(); // 分配结构体
while (decode) {
avcodec_receive_frame(ctx, frame); // 填充数据
process(frame);
av_frame_unref(frame); // ⭐ 释放数据,但 frame 指针仍然可用
}
av_frame_free(&frame); // 最后释放结构体
🧠 思考题 1:为什么要循环调用 receive_frame?
点击查看答案原因:一个 AVPacket 可能对应多个 AVFrame!
场景 1:B 帧重排
送入: Packet P₃
输出: Frame B₁, Frame B₂, Frame P₃ (3 个帧!)
解释:
解码器内部缓存了 I₀ 和 P₃
现在可以同时输出 B₁, B₂, P₃
场景 2:解码延迟
送入: Packet 1, Packet 2, Packet 3
输出: (无)
送入: Packet 4
输出: Frame 1, Frame 2 (一次输出多帧)
正确做法:
avcodec_send_packet(ctx, packet);
// 循环取帧,直到返回 EAGAIN
while (avcodec_receive_frame(ctx, frame) == 0) {
printf("Got frame %ld\n", frame->pts);
}
🛠️ 实战 2:解码所有帧并统计(完整版)
目标:解码整个视频文件,统计总帧数、总耗时、平均码率、I/P/B 帧分布等信息。
完整代码(decode_stats.cpp)
// decode_stats.cpp
// 编译: g++ decode_stats.cpp -o decode_stats $(pkg-config --cflags --libs libavformat libavcodec libavutil)
// 运行: ./decode_stats input.mp4
extern "C" {
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libavutil/time.h>
}
#include <cstdio>
#include <chrono>
int main(int argc, char** argv) {
if (argc < 2) {
printf("Usage: %s <input.mp4>\n", argv[0]);
return 1;
}
const char* input_file = argv[1];
// ========================================
// 步骤 1:打开输入文件
// ========================================
AVFormatContext* fmt_ctx = nullptr;
if (avformat_open_input(&fmt_ctx, input_file, nullptr, nullptr) < 0) {
printf("❌ Failed to open input file\n");
return 1;
}
if (avformat_find_stream_info(fmt_ctx, nullptr) < 0) {
printf("❌ Failed to find stream info\n");
return 1;
}
// ========================================
// 步骤 2:查找视频流
// ========================================
int video_stream_idx = av_find_best_stream(fmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0);
if (video_stream_idx < 0) {
printf("❌ No video stream found\n");
return 1;
}
AVStream* video_stream = fmt_ctx->streams[video_stream_idx];
// 计算总时长(秒)
double duration_sec = video_stream->duration * av_q2d(video_stream->time_base);
if (duration_sec <= 0) {
duration_sec = fmt_ctx->duration / (double)AV_TIME_BASE;
}
printf("📹 Video Info:\n");
printf(" Resolution: %dx%d\n", video_stream->codecpar->width, video_stream->codecpar->height);
printf(" Codec: %s (ID=%d)\n", avcodec_get_name(video_stream->codecpar->codec_id), video_stream->codecpar->codec_id);
printf(" Duration: %.2f seconds\n", duration_sec);
printf(" Bitrate: %ld kbps\n", video_stream->codecpar->bit_rate / 1000);
printf("\n");
// ========================================
// 步骤 3:打开解码器
// ========================================
const AVCodec* codec = avcodec_find_decoder(video_stream->codecpar->codec_id);
if (!codec) {
printf("❌ Codec not found\n");
return 1;
}
AVCodecContext* codec_ctx = avcodec_alloc_context3(codec);
if (!codec_ctx) {
printf("❌ Failed to allocate codec context\n");
return 1;
}
if (avcodec_parameters_to_context(codec_ctx, video_stream->codecpar) < 0) {
printf("❌ Failed to copy codec parameters\n");
return 1;
}
// 🚀 性能优化:启用多线程解码
codec_ctx->thread_count = 4; // 使用 4 线程
codec_ctx->thread_type = FF_THREAD_FRAME; // 帧级并行
if (avcodec_open2(codec_ctx, codec, nullptr) < 0) {
printf("❌ Failed to open codec\n");
return 1;
}
printf("✅ Decoder opened: %s (threads=%d)\n\n", codec->name, codec_ctx->thread_count);
// ========================================
// 步骤 4:统计变量初始化
// ========================================
AVPacket* packet = av_packet_alloc();
AVFrame* frame = av_frame_alloc();
int total_frames = 0;
int i_frames = 0, p_frames = 0, b_frames = 0, other_frames = 0;
int64_t total_packet_size = 0;
auto start_time = std::chrono::high_resolution_clock::now();
// ========================================
// 步骤 5:解码主循环
// ========================================
printf("🎬 Decoding...\n");
while (av_read_frame(fmt_ctx, packet) >= 0) {
if (packet->stream_index != video_stream_idx) {
av_packet_unref(packet);
continue;
}
total_packet_size += packet->size;
// 发送包到解码器
int ret = avcodec_send_packet(codec_ctx, packet);
if (ret < 0) {
printf("❌ Error sending packet to decoder\n");
break;
}
// 接收解码后的帧(循环)
while (ret >= 0) {
ret = avcodec_receive_frame(codec_ctx, frame);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
break;
} else if (ret < 0) {
printf("❌ Error receiving frame from decoder\n");
break;
}
// 统计帧类型
total_frames++;
if (frame->key_frame) {
i_frames++;
} else if (frame->pict_type == AV_PICTURE_TYPE_P) {
p_frames++;
} else if (frame->pict_type == AV_PICTURE_TYPE_B) {
b_frames++;
} else {
other_frames++;
}
// 每 100 帧打印一次进度
if (total_frames % 100 == 0) {
printf(" Decoded %d frames (I=%d, P=%d, B=%d)...\r",
total_frames, i_frames, p_frames, b_frames);
fflush(stdout);
}
av_frame_unref(frame);
}
av_packet_unref(packet);
}
// ========================================
// 步骤 6:冲刷解码器
// ========================================
printf("\n🔄 Flushing decoder...\n");
avcodec_send_packet(codec_ctx, nullptr); // 发送空包
while (avcodec_receive_frame(codec_ctx, frame) == 0) {
total_frames++;
if (frame->key_frame) {
i_frames++;
} else if (frame->pict_type == AV_PICTURE_TYPE_P) {
p_frames++;
} else if (frame->pict_type == AV_PICTURE_TYPE_B) {
b_frames++;
} else {
other_frames++;
}
av_frame_unref(frame);
}
auto end_time = std::chrono::high_resolution_clock::now();
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time);
// ========================================
// 步骤 7:输出统计结果
// ========================================
printf("\n");
printf("📊 Decoding Statistics:\n");
printf(" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
printf(" Total Frames: %d\n", total_frames);
printf(" I-Frames (关键帧): %d (%.1f%%)\n", i_frames, i_frames * 100.0 / total_frames);
printf(" P-Frames (预测帧): %d (%.1f%%)\n", p_frames, p_frames * 100.0 / total_frames);
printf(" B-Frames (双向帧): %d (%.1f%%)\n", b_frames, b_frames * 100.0 / total_frames);
if (other_frames > 0) {
printf(" Other Frames: %d\n", other_frames);
}
printf(" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
printf(" Total Decode Time: %ld ms\n", elapsed.count());
printf(" Average FPS: %.2f\n", total_frames * 1000.0 / elapsed.count());
printf(" Time per Frame: %.2f ms\n", (double)elapsed.count() / total_frames);
printf(" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
printf(" Total Packet Size: %.2f MB\n", total_packet_size / (1024.0 * 1024.0));
printf(" Average Bitrate: %.2f Mbps\n", (total_packet_size * 8.0) / (duration_sec * 1000000.0));
printf(" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
// ========================================
// 步骤 8:清理资源
// ========================================
av_frame_free(&frame);
av_packet_free(&packet);
avcodec_free_context(&codec_ctx);
avformat_close_input(&fmt_ctx);
printf("\n✅ Done!\n");
return 0;
}
运行示例
$ ./decode_stats big_buck_bunny_1080p.mp4
📹 Video Info:
Resolution: 1920x1080
Codec: h264 (ID=27)
Duration: 596.46 seconds
Bitrate: 3481 kbps
✅ Decoder opened: h264 (threads=4)
🎬 Decoding...
Decoded 14315 frames (I=60, P=4761, B=9494)...
🔄 Flushing decoder...
📊 Decoding Statistics:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Total Frames: 14315
I-Frames (关键帧): 60 (0.4%)
P-Frames (预测帧): 4761 (33.3%)
B-Frames (双向帧): 9494 (66.3%)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Total Decode Time: 8234 ms
Average FPS: 1738.56
Time per Frame: 0.58 ms
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Total Packet Size: 251.23 MB
Average Bitrate: 3.37 Mbps
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✅ Done!
关键发现
从统计结果可以看出:
- B 帧占比高:66.3%,说明编码器使用了高效的双向预测
- I 帧稀疏:只有 0.4%,GOP(Group of Pictures)很大,有利于降低码率
- 解码速度快:1738 fps,远高于实时播放(24 fps),说明硬件性能充足
常见错误排查表
| 错误码 | 含义 | 可能原因 | 解决方法 |
|---|---|---|---|
EAGAIN |
需要更多数据 | 解码器缓冲区状态 | 正常,继续循环 |
EOF |
文件结束 | 已读取完所有数据 | 正常,结束解码 |
ENOMEM |
内存不足 | 系统内存耗尽 | 检查内存泄漏 |
EINVAL |
参数错误 | API 调用顺序错误 | 检查初始化流程 |
INVALIDDATA |
数据损坏 | 文件损坏或编码错误 | 跳过当前包 |
DECODER_NOT_FOUND |
解码器未找到 | 缺少编解码器支持 | 安装对应解码器 |
⚡ 性能优化技巧
1. 多线程解码
AVCodecContext* codec_ctx = avcodec_alloc_context3(codec);
// 🚀 启用多线程
codec_ctx->thread_count = 0; // 0 = 自动检测 CPU 核心数
codec_ctx->thread_type = FF_THREAD_FRAME; // 帧级并行(推荐)
// codec_ctx->thread_type = FF_THREAD_SLICE; // 切片级并行(适合高分辨率)
avcodec_open2(codec_ctx, codec, nullptr);
🧠 思考题 2:为什么要发送空包(nullptr)冲刷解码器?
点击查看答案原因:解码器内部有缓冲区,可能还有未输出的帧!
场景 1:B 帧延迟输出
输入: I₀ P₃ B₁ B₂ [EOF]
↓
解码器内部缓冲: B₁ B₂ 等待输出
如果不冲刷:
B₁ 和 B₂ 永远不会输出 ❌
视频末尾丢失几帧
冲刷后:
send(nullptr) → 解码器进入"排空"模式
receive() → 输出 B₁
receive() → 输出 B₂
receive() → 返回 EOF
场景 2:多线程解码
解码器内部有 4 个线程:
线程 1: 正在解码帧 100
线程 2: 正在解码帧 101
线程 3: 正在解码帧 102
线程 4: 正在解码帧 103
如果直接关闭:
4 个线程的结果都丢失 ❌
冲刷后:
等待所有线程完成 ✅
输出所有已解码的帧
正确流程:
// 1. 读取所有包
while (av_read_frame(fmt_ctx, packet) >= 0) {
avcodec_send_packet(ctx, packet);
while (avcodec_receive_frame(ctx, frame) == 0) {
process(frame);
}
}
// 2. ⭐ 冲刷解码器
avcodec_send_packet(ctx, nullptr); // 发送空包
while (avcodec_receive_frame(ctx, frame) == 0) {
process(frame); // 处理剩余帧
}
// 3. 清理
avcodec_free_context(&ctx);
🧠 思考题 3:如何判断一个 AVPacket 是否为关键帧?
点击查看答案方法 1:检查 flags 字段
if (packet->flags & AV_PKT_FLAG_KEY) {
printf("This is a key frame (I-frame)\n");
}
方法 2:解码后检查 AVFrame
avcodec_receive_frame(ctx, frame);
if (frame->key_frame) {
printf("This is a key frame\n");
}
// 或者检查帧类型
if (frame->pict_type == AV_PICTURE_TYPE_I) {
printf("This is an I-frame\n");
}
区别:
AVPacket->flags:
- 解码前就知道
- 适合 Seek 操作(跳转到关键帧)
AVFrame->key_frame:
- 解码后才知道
- 更准确(解码器确认)
实际应用:
// Seek 到最近的关键帧
int64_t target_pts = 5000; // 5 秒
av_seek_frame(fmt_ctx, video_stream_idx, target_pts, AVSEEK_FLAG_BACKWARD);
// 跳过非关键帧(快速播放)
while (av_read_frame(fmt_ctx, packet) >= 0) {
if (!(packet->flags & AV_PKT_FLAG_KEY)) {
av_packet_unref(packet);
continue; // 跳过 P/B 帧
}
decode_and_display(packet);
}
火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。
更多推荐
所有评论(0)