FFmpeg 源码解析:架构总览与 200 万行 C 代码的阅读指南
FFmpeg 是有史以来影响最深远的开源项目之一。几乎所有涉及多媒体的软件——VLC、Chrome、OBS、Handbrake、YouTube 后端、智能电视——都直接或间接地依赖 FFmpeg。整个项目约有 240 万行 C 代码,支持 400 多种编解码器、近 400 种容器格式,以及数十种 I/O 协议。尽管它无处不在,却很少有开发者真正读过它的公开 API 头文件之外的内容。
本文是六篇系列文章的第一篇,将带你深入 FFmpeg 源码内部。我们先从全局视角出发:项目包含哪些内容、各个部分如何协作,以及你在阅读代码时会反复遇到的设计模式。
FFmpeg 是什么:7 个库与 3 个工具
FFmpeg 不是一个单一的程序,而是一个平台。代码仓库提供了七个共享库和三个命令行工具,各自职责分明:
库
用途
libavutil
通用工具:数学运算、日志、像素格式、AVBuffer、AVFrame、AVClass
libswscale
图像缩放与像素格式转换
libswresample
音频重采样与采样格式转换
libavcodec
编解码器(支持 400+ 种)
libavformat
容器封装/解封装及 I/O 协议
libavfilter
音视频滤镜图引擎
libavdevice
平台相关的采集与播放设备
工具
用途
ffmpeg
核心转码工具——负责读取、解码、滤镜处理、编码和写入媒体
ffplay
基于 SDL2 的媒体播放器,用于预览
ffprobe
流/容器分析与元数据提取
这些库可以独立使用。如果你只需要解码功能,直接链接 libavcodec 和 libavutil 即可,无需引入整个项目。
目录结构一览
面对 240 万行代码,首先要搞清楚东西在哪里。以下是顶层目录结构:
目录
内容
libavutil/
约 300 个文件。共享基础组件:缓冲区、帧、像素/采样格式、数学、日志
libavcodec/
约 2700 个文件。所有编解码器实现及编解码框架
libavformat/
约 800 个文件。容器格式的解封装器/封装器及 I/O 协议
libavfilter/
约 600 个文件。音视频滤镜实现及滤镜图引擎
libswscale/
约 100 个文件。像素格式转换与缩放
libswresample/
约 50 个文件。音频采样率/格式转换
libavdevice/
约 50 个文件。操作系统相关的设备 I/O(ALSA、PulseAudio、V4L2 等)
fftools/
约 20 个文件。三个 CLI 工具及调度器/流水线引擎
ffbuild/
构建系统支持文件(common.mak、library.mak、version.sh)
doc/
文档,以及 doc/examples/ 中的 24 个独立示例程序
tests/
FATE 测试框架及测试数据
compat/
针对非 POSIX 平台的兼容性 shim
提示: libavcodec/ 目录一个人就占了整个代码库的一半以上。每个编解码器通常存放在一两个以编解码器命名的 .c 文件中(例如 libavcodec/h264dec.c、libavcodec/libx264.c)。
库依赖层次
七个库构成了严格的依赖层次。这不只是约定——Makefile 在链接阶段会强制执行。链接顺序定义于 Makefile#L33-L41:
# $(FFLIBS-yes) needs to be in linking order
FFLIBS-$(CONFIG_AVDEVICE) += avdevice
FFLIBS-$(CONFIG_AVFILTER) += avfilter
FFLIBS-$(CONFIG_AVFORMAT) += avformat
FFLIBS-$(CONFIG_AVCODEC) += avcodec
FFLIBS-$(CONFIG_SWRESAMPLE) += swresample
FFLIBS-$(CONFIG_SWSCALE) += swscale
FFLIBS := avutil
依赖关系自上而下流动——每个库只能使用位于其下方的库所提供的符号:
flowchart TD
avdevice["libavdevice"] --> avfilter["libavfilter"]
avdevice --> avformat["libavformat"]
avfilter --> avformat
avfilter --> avcodec["libavcodec"]
avformat --> avcodec
avcodec --> swresample["libswresample"]
avcodec --> swscale["libswscale"]
swresample --> avutil["libavutil"]
swscale --> avutil
avcodec --> avutil
这种分层是刻意为之的架构决策。libavutil 对其他任何 FFmpeg 库零依赖,是整个项目可靠的基础。同样,libavcodec 对容器格式一无所知——编解码器与容器之间的分离不只是概念上的,而是物理上的。
构建流程:configure → config.h → make
FFmpeg 使用一个自定义的 configure 脚本——不是 autotools,也不是 CMake——以单个 POSIX shell 脚本的形式编写。构建流程分三个阶段:
flowchart LR
A["./configure"] --> B["config.h\nconfig_components.h\nffbuild/config.mak"]
B --> C["make"]
C --> D["7 libraries + 3 tools"]
configure 脚本从 configure#L1-L55 处开始检测 shell 兼容性——如果当前 shell 有问题,它会尝试切换到备用 shell。这种务实的处理方式贯穿整个项目。
configure 中最值得关注的是 find_things_extern 函数,它负责在 configure#L4507-L4513 处发现可用组件:
find_things_extern(){
thing=$1
pattern=$2
file=$source_path/$3
out=${4:-$thing}
sed -n "s/^[^#]*extern.*$pattern *ff_\([^ ]*\)_$thing;/\1_$out/p" "$file"
}
这个函数用 sed 扫描源文件中与特定模式匹配的 extern 声明。举个例子,它会扫描 libavcodec/allcodecs.c 中形如 extern const FFCodec ff_h264_decoder; 的行,并提取出 h264_decoder。最终得到所有可用组件的列表,configure 再据此生成 config_components.h,写入形如 #define CONFIG_H264_DECODER 1 的宏定义。
在 Make 这一侧,Makefile#L123-L132 中的 DOSUBDIR 宏是驱动七个库统一构建的引擎:
define DOSUBDIR
$(foreach V,$(SUBDIR_VARS),$(eval $(call RESET,$(V))))
SUBDIR := $(1)/
include $(SRC_PATH)/$(1)/Makefile
-include $(SRC_PATH)/$(1)/$(ARCH)/Makefile
-include $(SRC_PATH)/$(1)/$(INTRINSICS)/Makefile
include $(SRC_PATH)/ffbuild/library.mak
endef
$(foreach D,$(FFLIBS),$(eval $(call DOSUBDIR,lib$(D))))
针对每个库,DOSUBDIR 重置所有变量,引入该库自己的 Makefile(其中设置了 OBJS、HEADERS 等),视情况引入用于 SIMD 代码的平台相关 Makefile,最后引入包含实际构建规则的 library.mak。这是 Make 元编程的一个简洁范例。
组件注册模式
FFmpeg 通过一套注册模式实现编译期的组件选择,将 configure 阶段的发现与运行时的遍历连接起来。这个模式在以下三个文件中以完全相同的形式出现:
libavcodec/allcodecs.c#L39-L80 — 编解码器
libavformat/allformats.c#L33-L80 — 容器格式
libavfilter/allfilters.c#L25-L60 — 滤镜
每个文件都为所有可能的组件声明 extern 符号:
extern const FFCodec ff_a64multi_encoder;
extern const FFCodec ff_aasc_decoder;
extern const FFCodec ff_h264_decoder;
// ... hundreds more
flowchart TD
subgraph Build Time
A["allcodecs.c declares\nextern FFCodec ff_h264_decoder"] --> B["configure scans with\nfind_things_extern"]
B --> C["Generates config_components.h\n#define CONFIG_H264_DECODER 1"]
end
subgraph Link Time
C --> D["Linker includes h264dec.o\nonly if CONFIG_H264_DECODER=1"]
end
subgraph Runtime
D --> E["av_codec_iterate() walks\nthe codec_list[] array"]
end
configure 脚本通过扫描这些 extern 声明来发现哪些组件存在。启用的组件会在 config_components.h 中获得对应的 #define CONFIG_XXX 1。库的 Makefile 只在配置标志被设置时才编译对应的实现文件。链接器只为启用的组件解析 extern 符号;未启用的组件根本不会被编译进去。
运行时,av_codec_iterate() 等函数遍历一个静态指针数组,该数组指向所有已启用组件的结构体,同样是根据配置标志生成的。这种设计意味着,添加一个新的编解码器只需做三件事:编写实现、在 allcodecs.c 中添加一行 extern 声明、在 Makefile 中添加一条构建规则。
命名约定与代码定位
FFmpeg 的代码库体量庞大,但命名约定出奇地一致。掌握这些规律之后,你就能预测文件名和符号名。
符号前缀:
av_ — 公开 API(可供外部使用,ABI 稳定)
ff_ — FFmpeg 内部(在库内各文件之间共享,但不对外公开)
无前缀 — 仅在单个文件内可见(static)
编解码器命名:
解码器:ff_{name}_decoder(例如 ff_h264_decoder)
编码器:ff_{name}_encoder(例如 ff_libx264_encoder)
文件名:通常为 {name}dec.c、{name}enc.c,外部封装库则为 lib{name}.c
格式命名:
解封装器:ff_{name}_demuxer(例如 ff_mov_demuxer)
封装器:ff_{name}_muxer(例如 ff_mp4_muxer)
滤镜命名:
视频滤镜:ff_vf_{name}(例如 ff_vf_scale)
音频滤镜:ff_af_{name}(例如 ff_af_aresample)
视频源:ff_vsrc_{name},音频源:ff_asrc_{name}
视频汇:ff_vsink_{name},音频汇:ff_asink_{name}
提示: 查找某个编解码器的实现时,在 allcodecs.c 中搜索其名称即可。extern 声明会告诉你符号名,而符号名与源文件路径之间有固定的对应关系。例如,ff_h264_decoder 的实现在 libavcodec/h264dec.c 中。
公开/私有结构体分离模式
这是 FFmpeg 中最重要的一个设计模式。理解了它,你就能在任何主要子系统中自如穿梭;不理解它,代码会让你感到一头雾水。
FFmpeg 中每个主要类型都有两个版本:一个是作为 ABI 一部分的公开结构体,另一个是将公开结构体作为第一个成员内嵌的私有结构体。私有结构体只在库内部使用;外部调用者只能看到公开结构体。
这个模式在 libavcodec/codec_internal.h#L127-L131 中的体现如下:
typedef struct FFCodec {
/**
* The public AVCodec. See codec.h for it.
*/
AVCodec p;
// ... private fields follow
} FFCodec;
C 语言保证,指向结构体的指针可以安全地转换为指向其第一个成员的指针(反之亦然)。因此,FFCodec* 可以安全地强制转换为 AVCodec* 并传递给外部代码。在内部,FFmpeg 通过一个简单的内联函数反向转换:
static av_always_inline const FFCodec *ffcodec(const AVCodec *codec)
{
return (const FFCodec*)codec;
}
classDiagram
class AVCodec {
+name: const char*
+long_name: const char*
+type: AVMediaType
+id: AVCodecID
+capabilities: int
}
class FFCodec {
+p: AVCodec
+caps_internal: unsigned
+cb_type: unsigned
+cb: union
+init(): int
+close(): int
}
class AVInputFormat {
+name: const char*
+long_name: const char*
+flags: int
}
class FFInputFormat {
+p: AVInputFormat
+read_probe(): int
+read_header(): int
+read_packet(): int
}
class AVFilter {
+name: const char*
+description: const char*
+inputs: AVFilterPad*
+outputs: AVFilterPad*
}
class FFFilter {
+p: AVFilter
+preinit(): int
+init(): int
+activate(): int
+uninit(): void
}
FFCodec *-- AVCodec : embeds as first member
FFInputFormat *-- AVInputFormat : embeds as first member
FFFilter *-- AVFilter : embeds as first member
这个模式遍布所有子系统:
公开(ABI 稳定)
私有(内部使用)
转换辅助函数
AVCodec
FFCodec
ffcodec()
AVInputFormat
FFInputFormat
ffinputformat()
AVOutputFormat
FFOutputFormat
ffoutputformat()
AVFilter
FFFilter
fffilter()
AVFilterContext
FFFilterContext
fffilterctx()
这种设计带来了 ABI 稳定性:公开结构体的布局永不改变(字段只能追加),而私有结构体则可以自由重排或扩展。这正是 FFmpeg 能够在内部快速迭代的同时跨版本维持二进制兼容性的根本原因。
接下来
有了这套心智模型——七个分层库、基于 configure 的构建系统、基于 extern 的组件注册机制,以及无处不在的公开/私有结构体分离模式——你已经具备了探索 FFmpeg 源码任意部分的能力。
第二篇文章将深入讲解贯穿整个生态系统的核心数据结构:用于引用计数的 AVBuffer、承载编码数据的 AVPacket、承载解码数据的 AVFrame,以及支撑结构化日志和运行时配置的 AVClass/AVOption 反射系统。