欢迎光临
我们一直在努力

《建议收藏》耗时一周,把 FFmpeg 音视频编解码彻底讲透了!从原理到实战,这一篇就够了!

📢 引言:为什么要死磕 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 的世界浩如烟海,我们今天剖析了:

  • 理论基础:I/P/B 帧,YUV 格式。
  • 核心流程:Demux -> Decode -> Encode -> Mux。
  • 代码实战:基于 C++ 的标准解码模版。
  • 避坑指南:PTS/DTS 同步与内存对齐。
  • 掌握了这些,你就可以尝试开发自己的播放器、视频剪辑工具,甚至是直播推流客户端。

    🎁 彩蛋:互动时间

    你正在使用 FFmpeg 解决什么具体问题?

    • A. 公司项目需要,做视频压缩。
    • B. 自己写播放器练手。
    • C. 做直播推流(RTMP/RTSP)。
    • D. 单纯觉得以前没看懂,今天想彻底搞懂。

    👉 欢迎在评论区留下你的答案(比如选 C),我会针对排名前三的需求,在下一篇博文中更新对应的《FFmpeg 实战代码库》!

    整理不易,如果这篇文章让你对 FFmpeg 有了新的理解,请点赞、收藏、关注三连支持一下!我们下期见!


    赞(0)
    未经允许不得转载:171主机测评 » 《建议收藏》耗时一周,把 FFmpeg 音视频编解码彻底讲透了!从原理到实战,这一篇就够了!
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址