FFmpeg 视频解码入门:H264 软解码器简单示例

🎬 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 文件)
  • [📋 总结](#📋 总结)

📖 前言

在音视频开发中,视频解码 是一个非常重要的环节。本文将通过一个简单完整 的实例,介绍如何使用 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);
}

解码流程说明

  1. av_read_frame() - 从文件读取一个压缩数据包(AVPacket)
  2. avcodec_send_packet() - 将数据包发送给解码器
  3. 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 系列教程将持续更新 🔥!

相关推荐
却道天凉_好个秋19 小时前
音视频学习(八十六):宏块
音视频·hevc·宏块·ctu
小咖自动剪辑19 小时前
AI 智能视频无损放大工具:支持超分辨率与智能补帧
人工智能·音视频·智能电视
AI周红伟20 小时前
周红伟:2026年视频大模型第一篇,Sora 2 技术原理和技术架构,Sora2核心技术代码首次深度解析
音视频
阿甘编程点滴21 小时前
自媒体视频配音方案怎么选:从脚本到稳定输出
音视频·媒体
冬奇Lab1 天前
一天一个开源项目(第2篇):Remotion - 用 React 程序化创建视频
react.js·开源·音视频
一招定胜负1 天前
opencv视频处理
人工智能·opencv·音视频
行业探路者1 天前
音频二维码让音频分享变得更简单快捷
学习·音视频·语音识别·二维码·设备巡检
EasyNVR1 天前
docker版EasyNVR如何使用同步插件教程(包含网盘挂载,路径映射等)
docker·容器·音视频
IT陈图图2 天前
Flutter × OpenHarmony 跨端实践:从零构建一个轻量级视频播放器
flutter·音视频·鸿蒙·openharmony
深圳市友昊天创科技有限公司2 天前
友昊天创推出8K ,4K 120Hz 100米延长器方案
音视频·实时音视频·视频编解码