🎬 FFmpeg 视频解码入门:H264 软解码器简单示例
📅 更新时间:2026 年1月2日
🏷️ 标签:FFmpeg | H264 解码 | 音视频编程 | C/C++ | YUV
文章目录
- [📖 前言](#📖 前言)
- [🔄 解码流程概述](#🔄 解码流程概述)
- [💻 完整代码](#💻 完整代码)
- [🎯 重点代码解析](#🎯 重点代码解析)
-
- [1️⃣ 头文件引入与 extern "C"](#1️⃣ 头文件引入与 extern "C")
- [2️⃣ 错误处理函数](#2️⃣ 错误处理函数)
- [3️⃣ 查找并配置解码器](#3️⃣ 查找并配置解码器)
- [4️⃣ 解码循环核心逻辑](#4️⃣ 解码循环核心逻辑)
- [5️⃣ YUV 数据写入文件](#5️⃣ YUV 数据写入文件)
- [6️⃣ 刷新解码器缓冲区](#6️⃣ 刷新解码器缓冲区)
- [7️⃣ 资源释放](#7️⃣ 资源释放)
- [🎥 验证解码结果](#🎥 验证解码结果)
-
- [使用 ffplay 播放 YUV 文件](#使用 ffplay 播放 YUV 文件)
- [📋 总结](#📋 总结)
-
- [核心 API 回顾](#核心 API 回顾)
- 内存管理规则
📖 前言
在音视频开发中,视频解码 是一个非常重要的环节。本文将通过一个简单完整 的实例,介绍如何使用 FFmpeg 调用 H264 软解码器对 MP4 视频文件进行解码 ,并将解码后的 YUV 原始数据保存到文件中 (只解码视频流!!!)
本文的核心任务:
打开MP4文件 → 查找视频流 → 配置H264解码器 → 解码循环 → 保存YUV数据
🔄 解码流程概述
整体流程图
┌─────────────────────┐
│ avformat_open_input│ ← 打开视频文件
└──────────┬──────────┘
↓
┌─────────────────────┐
│ av_find_best_stream │ ← 查找视频流
└──────────┬──────────┘
↓
┌─────────────────────┐
│ avcodec_find_decoder│ ← 查找H264解码器
└──────────┬──────────┘
↓
┌─────────────────────┐
│ avcodec_alloc_context3 │
│ avcodec_parameters_to_context │ ← 配置解码器
│ avcodec_open2 │
└──────────┬──────────┘
↓
┌────────┐
┌──│ 解码循环│──┐
│ └────────┘ │
│ ↓ │
│ av_read_frame → avcodec_send_packet → avcodec_receive_frame
│ ↓ │
│ 写入YUV文件 │
│ ↓ │
└──────────────┘
↓
┌─────────────────────┐
│ 刷新解码器缓冲区 │ ← 发送空packet获取剩余帧
└──────────┬──────────┘
↓
┌─────────────────────┐
│ 资源释放 │ ← 各种free函数
└─────────────────────┘
💻 完整代码
cpp
#include <iostream>
extern "C"
{
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/avutil.h>
}
//打印错误原因
void log_error(int error,std::string tmp)
{
char errbuf[256];
av_strerror(error, errbuf, sizeof(errbuf));
std::cout << tmp<<"," << errbuf << std::endl;
}
int main()
{
//配置
AVFormatContext *avformat_context = nullptr;
AVCodecContext *avcodec_context = nullptr;
AVStream *video_stream = nullptr;
AVPacket *packet = nullptr;
AVFrame *frame = nullptr;
const AVCodec *decode = nullptr;
const char* file_url = "D:/桌面/视频录制/500001652967108-1-192.mp4";
int result = 0;
int video_index = 0;
FILE *file_yuv = fopen("output_h264.yuv", "wb");
if(!file_yuv)
{
std::cout << "无法创建输出文件" << std::endl;
}
else
{
std::cout << "成功创建输出文件" << std::endl;
}
//打开视频
result = avformat_open_input(&avformat_context, file_url, nullptr, nullptr);
if(result==0)
{
std::cout << "成功打开mp4视频文件" << std::endl;
}
else
{
log_error(result, "视频文件打开失败");
}
//寻找视频流
result = av_find_best_stream(avformat_context, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0);
if(result>=0)
{
std::cout << "视频流索引:" <<result <<std::endl;
video_index = result;
}
else
{
log_error(result, "未找到视频流索引");
}
video_stream = avformat_context->streams[result];
//创建解码器
decode=avcodec_find_decoder(video_stream->codecpar->codec_id);
if(decode)
{
std::cout << "解码器:" << decode->name << std::endl;
}
else
{
std::cout << "未找到对应解码器" << std::endl;
}
avcodec_context = avcodec_alloc_context3(decode);
avcodec_parameters_to_context(avcodec_context, video_stream->codecpar);
result = avcodec_open2(avcodec_context, decode, nullptr);
if(result==0)
{
std::cout << "解码器信息配置成功" << std::endl;
}
else
{
log_error(result,"解码器信息配置失败");
}
//分配packet frame
packet = av_packet_alloc();
frame = av_frame_alloc();
if(!packet||!frame)
{
std::cout << "packet/frame 分配失败" << std::endl;
}
//解码循环
while(av_read_frame(avformat_context,packet)>=0)
{
//只处理视频流
if(packet->stream_index==video_index)
{
result = avcodec_send_packet(avcodec_context, packet);
if(result<0)
{
log_error(result, "发送packet给解码器失败");
continue;
}
while(1)
{
result = avcodec_receive_frame(avcodec_context, frame);
if(result==0)//成功
{
//将解码后的每一帧写入.yuv文件中
for (int i = 0; i < frame->height;i++)
{
fwrite(frame->data[0] + i * frame->linesize[0], 1, frame->width, file_yuv);
}
for (int i = 0; i < frame->height / 2;i++)
{
fwrite(frame->data[1] + i * frame->linesize[1], 1, frame->width / 2,file_yuv);
}
for (int i = 0; i < frame->height / 2; i++)
{
fwrite(frame->data[2] + i * frame->linesize[2], 1, frame->width / 2, file_yuv);
}
}
else if(result==AVERROR(EAGAIN))//继续下一个packet
{
break;
}
else if(result==AVERROR_EOF)//解码结束
{
break;
}
else
{
log_error(result, "receive_frame 其他错误,失败");
break;
}
av_frame_unref(frame);
}
}
av_packet_unref(packet);
}
//解码器缓冲区内部frame
result = avcodec_send_packet(avcodec_context, nullptr);
while (1)
{
result = avcodec_receive_frame(avcodec_context, frame);
if (result == 0) // 成功
{
// 将解码后的每一帧写入.yuv文件中
for (int i = 0; i < frame->height; i++)
{
fwrite(frame->data[0] + i * frame->linesize[0], 1, frame->width, file_yuv);
}
for (int i = 0; i < frame->height / 2; i++)
{
fwrite(frame->data[1] + i * frame->linesize[1], 1, frame->width / 2, file_yuv);
}
for (int i = 0; i < frame->height / 2; i++)
{
fwrite(frame->data[2] + i * frame->linesize[2], 1, frame->width / 2, file_yuv);
}
}
else if (result == AVERROR(EAGAIN)) // 继续下一个packet
{
break;
}
else if (result == AVERROR_EOF) // 解码结束
{
break;
}
else
{
log_error(result, "receive_frame 其他错误,失败");
break;
}
av_frame_unref(frame);
}
//资源回收
fclose(file_yuv);
av_packet_free(&packet);
av_frame_free(&frame);
avcodec_free_context(&avcodec_context);
avformat_close_input(&avformat_context);
return 0;
}
🎯 重点代码解析
1️⃣ 头文件引入与 extern "C"
cpp
extern "C"
{
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/avutil.h>
}
为什么需要 extern "C"?
FFmpeg 是用 C 语言编写的库,而我们的代码是 C++。C++ 编译器会对函数名进行名称修饰(name mangling),导致链接时找不到 FFmpeg 的函数。使用 extern "C" 告诉编译器按 C 语言的方式处理这些头文件。
2️⃣ 错误处理函数
cpp
void log_error(int error, std::string tmp)
{
char errbuf[256];
av_strerror(error, errbuf, sizeof(errbuf));
std::cout << tmp << "," << errbuf << std::endl;
}
av_strerror() 是 FFmpeg 提供的错误码转换函数,可以将数字错误码转换为可读的错误信息,对调试非常有帮助。
3️⃣ 查找并配置解码器
cpp
// 根据视频流的codec_id查找对应的解码器
decode = avcodec_find_decoder(video_stream->codecpar->codec_id);
// 分配解码器上下文
avcodec_context = avcodec_alloc_context3(decode);
// 将流参数复制到解码器上下文
avcodec_parameters_to_context(avcodec_context, video_stream->codecpar);
// 打开解码器
result = avcodec_open2(avcodec_context, decode, nullptr);
关键步骤说明:
| 函数 | 作用 |
|---|---|
avcodec_find_decoder() |
根据 codec_id 查找对应的解码器(如 h264) |
avcodec_alloc_context3() |
分配解码器上下文内存 |
avcodec_parameters_to_context() |
将视频流的参数(分辨率、像素格式等)复制到解码器上下文 |
avcodec_open2() |
初始化并打开解码器 |
4️⃣ 解码循环核心逻辑
cpp
while(av_read_frame(avformat_context, packet) >= 0)
{
if(packet->stream_index == video_index)
{
result = avcodec_send_packet(avcodec_context, packet);
while(1)
{
result = avcodec_receive_frame(avcodec_context, frame);
if(result == 0)
{
// 处理解码后的帧...
}
else if(result == AVERROR(EAGAIN))
{
break; // 需要更多packet
}
else if(result == AVERROR_EOF)
{
break; // 解码结束
}
}
av_frame_unref(frame);
}
av_packet_unref(packet);
}
解码流程说明:
av_read_frame()- 从文件读取一个压缩数据包(AVPacket)avcodec_send_packet()- 将数据包发送给解码器avcodec_receive_frame()- 从解码器接收解码后的帧(AVFrame)
重要概念 :解码是异步的!
- 发送一个 packet 后,可能解码出 0 个、1 个或多个 frame
AVERROR(EAGAIN)表示需要发送更多 packet 才能输出 frame- 所以
avcodec_receive_frame()需要循环调用
5️⃣ YUV 数据写入文件
cpp
// Y分量
for (int i = 0; i < frame->height; i++)
{
fwrite(frame->data[0] + i * frame->linesize[0], 1, frame->width, file_yuv);
}
// U分量
for (int i = 0; i < frame->height / 2; i++)
{
fwrite(frame->data[1] + i * frame->linesize[1], 1, frame->width / 2, file_yuv);
}
// V分量
for (int i = 0; i < frame->height / 2; i++)
{
fwrite(frame->data[2] + i * frame->linesize[2], 1, frame->width / 2, file_yuv);
}
YUV420P 格式说明:
┌────────────────────┐
│ │
│ Y │ height行,每行width字节
│ │
├────────────────────┤
│ U │ height/2行,每行width/2字节
├────────────────────┤
│ V │ height/2行,每行width/2字节
└────────────────────┘
为什么要逐行写入?
由于内存对齐的原因,linesize(每行实际存储的字节数)可能大于 width(图像实际宽度)。如果直接按 linesize 写入,会包含填充数据,导致 YUV 文件无法正确播放。
6️⃣ 刷新解码器缓冲区
cpp
// 发送空packet,通知解码器输入结束
result = avcodec_send_packet(avcodec_context, nullptr);
// 循环获取解码器缓冲区中剩余的帧
while (1)
{
result = avcodec_receive_frame(avcodec_context, frame);
if (result == 0)
{
// 处理帧...
}
else if (result == AVERROR_EOF)
{
break; // 所有帧都已输出
}
}
为什么需要刷新?
解码器内部有缓冲区,可能缓存了一些帧还没有输出。发送 nullptr 作为 packet,告诉解码器"输入已经结束",让它把缓冲区中剩余的帧全部输出。
7️⃣ 资源释放
cpp
fclose(file_yuv);
av_packet_free(&packet);
av_frame_free(&frame);
avcodec_free_context(&avcodec_context);
avformat_close_input(&avformat_context);
释放顺序:遵循"后创建先释放"的原则,避免悬空指针。
🎥 验证解码结果
解码完成后,会生成 output_h264.yuv 文件。我们可以使用 ffplay 来验证解码是否成功。
使用 ffplay 播放 YUV 文件
bash
ffplay -f rawvideo -video_size 宽x高 -pixel_format yuv420p output_h264.yuv
📋 总结
核心 API 回顾
| API | 功能 | 调用时机 |
|---|---|---|
avformat_open_input() |
打开视频文件 | 程序开始 |
av_find_best_stream() |
查找视频流 | 打开文件后 |
avcodec_find_decoder() |
查找解码器 | 找到视频流后 |
avcodec_open2() |
打开解码器 | 配置解码器后 |
av_read_frame() |
读取压缩数据包 | 解码循环中 |
avcodec_send_packet() |
发送数据包给解码器 | 读取到 packet 后 |
avcodec_receive_frame() |
接收解码后的帧 | 发送 packet 后循环调用 |
内存管理规则
av_packet_alloc()/av_frame_alloc()- 循环外分配一次av_packet_unref()/av_frame_unref()- 每次使用后释放引用av_packet_free()/av_frame_free()- 程序结束前释放
如果您觉得这篇文章对您有帮助,不妨点赞 + 收藏 + 关注,更多 FFmpeg 系列教程将持续更新 🔥!