news 2026/9/23 23:56:59

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
FFmpeg 视频解码入门:H264 软解码器简单示例

🎬 FFmpeg 视频解码入门:H264 软解码器简单示例
📅 更新时间:2026 年1月2日
🏷️ 标签:FFmpeg | H264 解码 | 音视频编程 | C/C++ | YUV

文章目录

  • 📖 前言
  • 🔄 解码流程概述
    • 整体流程图
  • 💻 完整代码
  • 🎯 重点代码解析
    • 1️⃣ 头文件引入与 extern "C"
    • 2️⃣ 错误处理函数
    • 3️⃣ 查找并配置解码器
    • 4️⃣ 解码循环核心逻辑
    • 5️⃣ YUV 数据写入文件
    • 6️⃣ 刷新解码器缓冲区
    • 7️⃣ 资源释放
  • 🎥 验证解码结果
    • 使用 ffplay 播放 YUV 文件
  • 📋 总结
    • 核心 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函数 └─────────────────────┘

💻 完整代码

#include<iostream>extern"C"{#include<libavcodec/avcodec.h>#include<libavformat/avformat.h>#include<libavutil/avutil.h>}//打印错误原因voidlog_error(interror,std::string tmp){charerrbuf[256];av_strerror(error,errbuf,sizeof(errbuf));std::cout<<tmp<<","<<errbuf<<std::endl;}intmain(){//配置AVFormatContext*avformat_context=nullptr;AVCodecContext*avcodec_context=nullptr;AVStream*video_stream=nullptr;AVPacket*packet=nullptr;AVFrame*frame=nullptr;constAVCodec*decode=nullptr;constchar*file_url="D:/桌面/视频录制/500001652967108-1-192.mp4";intresult=0;intvideo_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 framepacket=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(inti=0;i<frame->height;i++){fwrite(frame->data[0]+i*frame->linesize[0],1,frame->width,file_yuv);}for(inti=0;i<frame->height/2;i++){fwrite(frame->data[1]+i*frame->linesize[1],1,frame->width/2,file_yuv);}for(inti=0;i<frame->height/2;i++){fwrite(frame->data[2]+i*frame->linesize[2],1,frame->width/2,file_yuv);}}elseif(result==AVERROR(EAGAIN))//继续下一个packet{break;}elseif(result==AVERROR_EOF)//解码结束{break;}else{log_error(result,"receive_frame 其他错误,失败");break;}av_frame_unref(frame);}}av_packet_unref(packet);}//解码器缓冲区内部frameresult=avcodec_send_packet(avcodec_context,nullptr);while(1){result=avcodec_receive_frame(avcodec_context,frame);if(result==0)// 成功{// 将解码后的每一帧写入.yuv文件中for(inti=0;i<frame->height;i++){fwrite(frame->data[0]+i*frame->linesize[0],1,frame->width,file_yuv);}for(inti=0;i<frame->height/2;i++){fwrite(frame->data[1]+i*frame->linesize[1],1,frame->width/2,file_yuv);}for(inti=0;i<frame->height/2;i++){fwrite(frame->data[2]+i*frame->linesize[2],1,frame->width/2,file_yuv);}}elseif(result==AVERROR(EAGAIN))// 继续下一个packet{break;}elseif(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);return0;}

🎯 重点代码解析

1️⃣ 头文件引入与 extern “C”

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️⃣ 错误处理函数

voidlog_error(interror,std::string tmp){charerrbuf[256];av_strerror(error,errbuf,sizeof(errbuf));std::cout<<tmp<<","<<errbuf<<std::endl;}

av_strerror()是 FFmpeg 提供的错误码转换函数,可以将数字错误码转换为可读的错误信息,对调试非常有帮助。


3️⃣ 查找并配置解码器

// 根据视频流的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️⃣ 解码循环核心逻辑

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){// 处理解码后的帧...}elseif(result==AVERROR(EAGAIN)){break;// 需要更多packet}elseif(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 数据写入文件

// Y分量for(inti=0;i<frame->height;i++){fwrite(frame->data[0]+i*frame->linesize[0],1,frame->width,file_yuv);}// U分量for(inti=0;i<frame->height/2;i++){fwrite(frame->data[1]+i*frame->linesize[1],1,frame->width/2,file_yuv);}// V分量for(inti=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️⃣ 刷新解码器缓冲区

// 发送空packet,通知解码器输入结束result=avcodec_send_packet(avcodec_context,nullptr);// 循环获取解码器缓冲区中剩余的帧while(1){result=avcodec_receive_frame(avcodec_context,frame);if(result==0){// 处理帧...}elseif(result==AVERROR_EOF){break;// 所有帧都已输出}}

为什么需要刷新?

解码器内部有缓冲区,可能缓存了一些帧还没有输出。发送nullptr作为 packet,告诉解码器"输入已经结束",让它把缓冲区中剩余的帧全部输出。


7️⃣ 资源释放

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 文件

ffplay-frawvideo-video_size宽x高-pixel_formatyuv420p 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 系列教程将持续更新 🔥!

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/20 13:33:24

FFmpeg 视频解码进阶:H264_CUVID 硬解码器简单示例

&#x1f3ac; FFmpeg 视频解码入门&#xff1a;H264_CUVID 硬解码器简单示例 &#x1f4c5; 更新时间&#xff1a;2026 年1月2日 &#x1f3f7;️ 标签&#xff1a;FFmpeg | H264_CUVID | NVIDIA | 硬件解码 | CUDA | GPU 文章目录&#x1f4d6; 前言&#x1f504; 与软解码的…

作者头像 李华
网站建设 2026/9/20 13:33:24

2026开年12条重磅消息!机器人与AI正悄悄改变你的生活

2026年的第一个工作周&#xff0c;科技圈就扔出了不少“重磅炸弹”。如果你还觉得人工智能只是手机里的聊天软件&#xff0c;或者是离普通人很远的黑科技&#xff0c;那这几天的动态可能会让你大吃一惊。从家里能干活的人形机器人&#xff0c;到养老院里的黑科技&#xff0c;AI…

作者头像 李华
网站建设 2026/9/21 18:36:13

《利用混合整数规划优化航空旅行网络简介》

原文&#xff1a;towardsdatascience.com/introduction-0dd288ba9738?sourcecollection_archive---------7-----------------------#2024-02-18 如何设计一种算法&#xff0c;以最具成本效益的方式在网络中安排乘客需求 https://medium.com/rohan2465?sourcepost_page---byl…

作者头像 李华
网站建设 2026/9/20 13:34:31

从零实现有源蜂鸣器和无源区分功能测试

如何一眼看穿有源蜂鸣器和无源蜂鸣器&#xff1f;实战测试全解析 你有没有遇到过这种情况&#xff1a;手头有个蜂鸣器&#xff0c;没标签、没型号&#xff0c;连是“有源”还是“无源”都分不清。接上电吧&#xff0c;不响&#xff1b;换成PWM信号试试&#xff0c;又怕烧芯片……

作者头像 李华
网站建设 2026/9/20 13:34:31

motion_scale控制在1.0-1.1,避免Sonic动作僵硬或夸张

motion_scale控制在1.0–1.1&#xff0c;避免Sonic动作僵硬或夸张 在短视频内容爆炸式增长的今天&#xff0c;越来越多创作者和企业开始尝试用数字人替代真人出镜——无需布光、不需拍摄、不用剪辑&#xff0c;只需一张照片和一段音频&#xff0c;就能生成一个“会说话”的虚拟…

作者头像 李华
网站建设 2026/9/20 13:34:32

STM32CubeMX下载安装与驱动配置:手把手教学(含示例)

从零开始玩转STM32开发&#xff1a;CubeMX安装、驱动配置与代码生成实战指南 你是不是也经历过这样的场景&#xff1f;买了一块STM32开发板&#xff0c;兴冲冲地插上电脑&#xff0c;打开Keil准备烧录程序——结果设备管理器里却显示“未知设备”&#xff0c;调试器连不上&…

作者头像 李华