📢 引言:为什么要死磕 FFmpeg?
你好,我是你们的技术老友。
在音视频开发领域,流传着这样一句话:“音视频开发只有两种,一种是基于 FFmpeg 的,一种是自己造轮子的。”
FFmpeg 是什么?它是音视频领域的瑞士军刀,是 VLC、PotPlayer、甚至 YouTube 和 Bilibili 都在使用的底层核心。但是,它的学习曲线之陡峭,堪比徒手攀岩。
- 官方文档晦涩难懂,由于历史悠久,API 废弃和更迭极快(ffmpeg 3.x, 4.x, 5.x, 6.x 差异巨大)。
- 涉及大量的音视频理论(YUV、PCM、PTS/DTS、I/P/B 帧)。
- C 语言的内存管理稍有不慎就是内存泄漏(Memory Leak)。
为了帮大家跨过这道坎,我耗时一周,翻阅了数千行源码,整理了这篇万字长文。不讲虚的,只讲干货。我们将从最基础的音视频原理讲起,拆解 FFmpeg 架构,最后手把手带你用 C++ 写一个视频解码器。
建议先收藏,再阅读,防止迷路!
🛠️ 第一部分:音视频基础知识(必修课)
在碰代码之前,如果不懂基础理论,你看着 FFmpeg 的 API 就像看天书。
1.1 视频不仅仅是图片
很多新手认为视频就是连续播放的 JPG。错!如果这样存,1小时的电影可能需要几百 GB。 视频的核心在于压缩(Coding)。
- 帧内压缩(Intra-frame):类似 JPEG,只压缩当前画面,去掉人眼不敏感的信息。
- 帧间压缩(Inter-frame):这是视频压缩的神技。比如一个新闻主播在播报,背景是不动的,只有嘴巴在动。我们只需要记录变化的部分。
这就引出了视频编码中最重要的概念:I 帧、P 帧、B 帧。
- I 帧 (Intra Picture):关键帧。完整画面,自给自足,不参考别人。文件最大,解码最快。
- P 帧 (Predictive):前向预测帧。参考前面的 I 帧或 P 帧,只存差异。
- B 帧 (Bi-directional):双向预测帧。既参考前面,也参考后面。压缩率最高,但解码最耗时。
注意:由于 B 帧的存在,解码顺序和显示顺序是不一样的!这导致了 DTS 和 PTS 的区别(后文细讲)。
1.2 颜色模型:YUV vs RGB
屏幕显示用 RGB,但视频存储通常用 YUV。
- Y:亮度(Luma),占用的频带最宽,人眼最敏感。
- U/V:色度(Chroma),人眼不敏感。 这意味着我们可以疯狂压缩 UV 分量而不影响观感。常见的 YUV4:2:0 格式,每 4 个 Y 像素共用一组 UV,数据量直接砍掉一半。
1.3 封装格式 vs 编码格式
这是最容易混淆的概念。
- 封装格式 (Container):.mp4, .mkv, .avi。它只是一个盒子,用来装视频流、音频流、字幕流。
- 编码格式 (Codec):H.264 (AVC), H.265 (HEVC), AAC, MP3。这才是真正压缩数据的算法。
比喻:MP4 是一个快递箱,H.264 是箱子里的茅台酒。你不能说“我要喝 MP4”,而应该说“我要喝 H.264”。
🏗️ 第二部分:FFmpeg 核心架构与流程
FFmpeg 的强大在于其模块化设计。要驾驭它,你必须熟悉它的“八大金刚”库。
2.1 核心库介绍
| libavcodec | 编解码核心库 | 包含所有原生编解码器,FFmpeg 的灵魂 |
| libavformat | 封装格式处理 | 负责解封装(Demux)和封装(Mux),如打开 MP4 文件 |
| libavutil | 工具库 | 内存管理、数学运算、日志系统 |
| libswscale | 图像转换 | YUV 转 RGB,改变分辨率 |
| libswresample | 音频重采样 | 改变采样率、声道数(如 44.1k 转 48k) |
| libavfilter | 滤镜库 | 加水印、去噪、特效处理 |
| libavdevice | 设备输入输出 | 读取摄像头、麦克风、屏幕录制 |
2.2 万能的处理流程(The Pipeline)
无论你是做播放器、转码器还是推流器,FFmpeg 的生命周期都逃不开下面这张图。我特意绘制了这张 Mermaid 流程图,请务必刻在脑子里:
#mermaid-svg-jwgkSWeVWSCVUzcR{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-jwgkSWeVWSCVUzcR .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-jwgkSWeVWSCVUzcR .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-jwgkSWeVWSCVUzcR .error-icon{fill:#552222;}#mermaid-svg-jwgkSWeVWSCVUzcR .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-jwgkSWeVWSCVUzcR .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-jwgkSWeVWSCVUzcR .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-jwgkSWeVWSCVUzcR .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-jwgkSWeVWSCVUzcR .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-jwgkSWeVWSCVUzcR .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-jwgkSWeVWSCVUzcR .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-jwgkSWeVWSCVUzcR .marker{fill:#333333;stroke:#333333;}#mermaid-svg-jwgkSWeVWSCVUzcR .marker.cross{stroke:#333333;}#mermaid-svg-jwgkSWeVWSCVUzcR svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-jwgkSWeVWSCVUzcR p{margin:0;}#mermaid-svg-jwgkSWeVWSCVUzcR .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-jwgkSWeVWSCVUzcR .cluster-label text{fill:#333;}#mermaid-svg-jwgkSWeVWSCVUzcR .cluster-label span{color:#333;}#mermaid-svg-jwgkSWeVWSCVUzcR .cluster-label span p{background-color:transparent;}#mermaid-svg-jwgkSWeVWSCVUzcR .label text,#mermaid-svg-jwgkSWeVWSCVUzcR span{fill:#333;color:#333;}#mermaid-svg-jwgkSWeVWSCVUzcR .node rect,#mermaid-svg-jwgkSWeVWSCVUzcR .node circle,#mermaid-svg-jwgkSWeVWSCVUzcR .node ellipse,#mermaid-svg-jwgkSWeVWSCVUzcR .node polygon,#mermaid-svg-jwgkSWeVWSCVUzcR .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-jwgkSWeVWSCVUzcR .rough-node .label text,#mermaid-svg-jwgkSWeVWSCVUzcR .node .label text,#mermaid-svg-jwgkSWeVWSCVUzcR .image-shape .label,#mermaid-svg-jwgkSWeVWSCVUzcR .icon-shape .label{text-anchor:middle;}#mermaid-svg-jwgkSWeVWSCVUzcR .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-jwgkSWeVWSCVUzcR .rough-node .label,#mermaid-svg-jwgkSWeVWSCVUzcR .node .label,#mermaid-svg-jwgkSWeVWSCVUzcR .image-shape .label,#mermaid-svg-jwgkSWeVWSCVUzcR .icon-shape .label{text-align:center;}#mermaid-svg-jwgkSWeVWSCVUzcR .node.clickable{cursor:pointer;}#mermaid-svg-jwgkSWeVWSCVUzcR .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-jwgkSWeVWSCVUzcR .arrowheadPath{fill:#333333;}#mermaid-svg-jwgkSWeVWSCVUzcR .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-jwgkSWeVWSCVUzcR .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-jwgkSWeVWSCVUzcR .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-jwgkSWeVWSCVUzcR .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-jwgkSWeVWSCVUzcR .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-jwgkSWeVWSCVUzcR .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-jwgkSWeVWSCVUzcR .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-jwgkSWeVWSCVUzcR .cluster text{fill:#333;}#mermaid-svg-jwgkSWeVWSCVUzcR .cluster span{color:#333;}#mermaid-svg-jwgkSWeVWSCVUzcR div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-jwgkSWeVWSCVUzcR .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-jwgkSWeVWSCVUzcR rect.text{fill:none;stroke-width:0;}#mermaid-svg-jwgkSWeVWSCVUzcR .icon-shape,#mermaid-svg-jwgkSWeVWSCVUzcR .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-jwgkSWeVWSCVUzcR .icon-shape p,#mermaid-svg-jwgkSWeVWSCVUzcR .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-jwgkSWeVWSCVUzcR .icon-shape rect,#mermaid-svg-jwgkSWeVWSCVUzcR .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-jwgkSWeVWSCVUzcR .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-jwgkSWeVWSCVUzcR .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-jwgkSWeVWSCVUzcR :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
avformat_open_input
av_read_frame
avcodec_send_packet
avcodec_receive_frame
处理后 AVFrame
avcodec_send_frame
avcodec_receive_packet
av_interleaved_write_frame
输入文件 Input File
解复用器 Demuxer
编码数据包 AVPacket
解码器 Decoder
原始帧 AVFrame
滤镜 Filter Graph
处理后 AVFrame
编码器 Encoder
编码数据包 AVPacket
复用器 Muxer
输出文件 Output File
核心数据结构解析:
- AVFormatContext:统领全局,包含输入/输出文件的所有信息。
- AVCodecContext:编解码器的上下文,存着宽高、码率等信息。
- AVPacket:存放编码后的数据(如 H.264 的 NALU),它是压缩的。
- AVFrame:存放解码后的数据(如 YUV 像素或 PCM 音频),它是巨大的。
核心动作: Packet (压缩) -> Decode -> Frame (原始) -> Encode -> Packet (压缩)
💻 第三部分:C++ 代码实战(解码流程)
Talk is cheap, show me the code. 我们将基于 FFmpeg 4.x/5.x/6.x 通用 API(旧版 API 如 avcodec_decode_video2 已废弃,请勿再学),编写一个简单的视频解码器,将 MP4 视频解码并保存为 YUV 文件。
3.1 环境准备
确保你的开发环境链接了 ffmpeg 的 include 和 lib。
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>
#include <libavutil/imgutils.h>
}
#include <iostream>
3.2 核心代码实现
为了方便阅读,我将代码拆解为几个核心步骤,并附带详细注释。
步骤一:打开文件与查找流
// 1. 分配上下文
AVFormatContext* pFormatCtx = avformat_alloc_context();
// 2. 打开视频文件
if (avformat_open_input(&pFormatCtx, "input.mp4", NULL, NULL) != 0) {
printf("无法打开文件\\n");
return –1;
}
// 3. 查找流信息(必须步骤,否则可能拿不到宽高)
if (avformat_find_stream_info(pFormatCtx, NULL) < 0) {
printf("无法获取流信息\\n");
return –1;
}
// 4. 找到视频流的索引
int videoStreamIndex = –1;
for (int i = 0; i < pFormatCtx->nb_streams; i++) {
if (pFormatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
videoStreamIndex = i;
break;
}
}
步骤二:配置解码器
注意:从 FFmpeg 3.0 开始,我们不再直接使用 stream->codec,而是通过 codecpar 来复制参数。
// 1. 获取解码器参数
AVCodecParameters* pCodecPar = pFormatCtx->streams[videoStreamIndex]->codecpar;
// 2. 查找解码器 (如 H.264)
const AVCodec* pCodec = avcodec_find_decoder(pCodecPar->codec_id);
if (!pCodec) {
printf("找不到解码器\\n");
return –1;
}
// 3. 创建解码器上下文
AVCodecContext* pCodecCtx = avcodec_alloc_context3(pCodec);
// 4. 将流参数复制到解码器上下文
avcodec_parameters_to_context(pCodecCtx, pCodecPar);
// 5. 打开解码器
if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0) {
printf("无法打开解码器\\n");
return –1;
}
步骤三:解码循环(核心中的核心)
这里使用新版 API:avcodec_send_packet 和 avcodec_receive_frame。这是一种异步的思想:你往解码器里塞几个包,解码器可能吐出一个帧,也可能需要更多包。
AVPacket* pPacket = av_packet_alloc();
AVFrame* pFrame = av_frame_alloc();
while (av_read_frame(pFormatCtx, pPacket) >= 0) {
// 只处理视频流
if (pPacket->stream_index == videoStreamIndex) {
// — 发送 Packet 到解码器 —
int ret = avcodec_send_packet(pCodecCtx, pPacket);
if (ret < 0) {
printf("发送 Packet 错误\\n");
break;
}
// — 从解码器接收 Frame —
while (ret >= 0) {
ret = avcodec_receive_frame(pCodecCtx, pFrame);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
// EAGAIN: 需要更多 Packet 才能产出 Frame
// EOF: 文件结束
break;
} else if (ret < 0) {
printf("解码错误\\n");
break;
}
// — 成功拿到一帧画面 (pFrame) —
printf("解码第 %d 帧, 分辨率: %dx%d, 格式: %d\\n",
pCodecCtx->frame_number, pFrame->width, pFrame->height, pFrame->format);
// TODO: 在这里进行 YUV 保存、Swscale 转换或 OpenGL 渲染
}
}
// 重要:每次循环结束必须释放 Packet 引用,否则内存爆炸
av_packet_unref(pPacket);
}
步骤四:资源释放
C/C++ 开发者的自我修养。
av_frame_free(&pFrame);
av_packet_free(&pPacket);
avcodec_free_context(&pCodecCtx);
avformat_close_input(&pFormatCtx);
⏳ 第四部分:进阶 – 那些让你头秃的细节
如果说上面的代码能让你运行起来,那么下面的内容能救你的命。
4.1 PTS 与 DTS 的爱恨情仇
在处理 H.264 视频时,你一定会遇到音视频不同步或者画面抖动的问题。根源通常在 PTS (Presentation Time Stamp) 和 DTS (Decoding Time Stamp)。
- DTS:告诉解码器什么时候解码这一帧。
- PTS:告诉播放器什么时候显示这一帧。
对于没有 B 帧的视频,PTS = DTS。 但是一旦有了 B 帧(B 帧需要参考后面的 P 帧,所以后面的 P 帧必须先解码),顺序就变了:
- 写入/解码顺序 (DTS): I0, P3, B1, B2
- 显示顺序 (PTS): I0, B1, B2, P3
实战坑点: 如果你手动进行转封装(Remuxing),必须正确换算 TimeBase。
4.2 内存对齐 (Memory Alignment)
FFmpeg 默认生成的 Frame 数据,为了利用 CPU 的 SIMD 指令集加速(SSE/AVX),通常是内存对齐的。 这意味着:pFrame->linesize[0] (这一行的跨度) 不一定等于 pFrame->width。 切记:复制 YUV 数据时,千万不要直接 memcpy(dst, src, width * height)!必须逐行复制:
for (int i = 0; i < height; i++) {
memcpy(dst_y + i * dst_stride,
src_y + i * src_stride, // src_stride 即 pFrame->linesize[0]
width);
}
❓ 第五部分:常见问题 QA
Q1: 为什么我的解码器一开始会丢失几帧? A: 可能是解码器缓冲区未刷新。在文件读取结束后,需要传入 NULL Packet 调用一次 avcodec_send_packet,进入 Draining Mode,把解码器里缓存的最后几帧“冲”出来。
Q2: 为什么 av_read_frame 读出来的包大小都不一样? A: 因为视频是变码率(VBR)压缩的,画面复杂时 Packet 大,静止时 Packet 小。
Q3: 如何进行硬件解码(CUDA/QuickSync)? A: 初始化时需要通过 av_hwdevice_ctx_create 创建硬件设备上下文,并将其绑定到 AVCodecContext。流程比软解复杂一倍,如果反响热烈,我下一篇专门讲硬解!
🎯 总结与展望
看懂了这篇文章,你已经迈过了音视频开发最高的门槛。
FFmpeg 的世界浩如烟海,我们今天剖析了:
掌握了这些,你就可以尝试开发自己的播放器、视频剪辑工具,甚至是直播推流客户端。
🎁 彩蛋:互动时间
你正在使用 FFmpeg 解决什么具体问题?
- A. 公司项目需要,做视频压缩。
- B. 自己写播放器练手。
- C. 做直播推流(RTMP/RTSP)。
- D. 单纯觉得以前没看懂,今天想彻底搞懂。
👉 欢迎在评论区留下你的答案(比如选 C),我会针对排名前三的需求,在下一篇博文中更新对应的《FFmpeg 实战代码库》!
整理不易,如果这篇文章让你对 FFmpeg 有了新的理解,请点赞、收藏、关注三连支持一下!我们下期见!



