源码树一个库一个目录,编译完一个库变成一堆模块。 要不要碰源码,只取决于一件事:现成的构建能不能满足你。
1.3.2 讲的是编好的库,这一节讲源码到编译产物这条路。 1.4 开始讲这些库合起来能干什么。
一、源码树长什么样
解开源码包,第一层目录就是这套 SDK 的目录结构:
目录 编出哪个库 里面是什么
libavformat/ libavformat 各封装格式的读写代码 libavcodec/ libavcodec 各编解码器与通用框架 libavfilter/ libavfilter 每个滤镜一个文件 libavutil/ libavutil 内存、数学、公共结构 libswscale/ libswscale 像素格式转换 libswresample/ libswresample 音频重采样 libavdevice/ libavdevice 各系统的采集接口 fftools/ 三个 exe 三个工具的 main 函数
最后一行是很多人忽略的:ffmpeg、ffplay、ffprobe 的源码就在 fftools 目录里。 也就是说「命令行工具」本身也是这份源码的一部分,不是另外的东西。
想搞清楚某个参数怎么实现的,直接去 fftools 里搜参数名。 这比翻文档快得多,而且是唯一准确的答案。
二、configure 那一步在做什么
它是整个编译流程里唯一需要你拍板的一步,也是最容易失败的一步:
它在做三件事 结果
探测系统里有哪些库 决定哪些开关可以打开 按开关生成 config.h 决定哪些代码被编译进去 生成 Makefile 决定怎么编译、装到哪儿
所以 configure 通过以后,能力就定死了。 后面 make 只是把这个决定执行掉,不会再改变什么。
这也解释了为什么「装完发现缺功能」只能重编,不能补装。 很多新手想「再装一个包补上」,那在 FFmpeg 上是走不通的—— 它不是插件式架构,是编译期静态决定的。
三、编译产物的三个形态
同一份源码,能编出三种东西,用途完全不同:
形态 产物 给谁用
静态库 libav.a 链接进别人的程序 动态库 libav.so 或 .dll 运行时加载,可替换 可执行文件 ffmpeg 等三个 exe 直接敲命令
静态与动态的取舍在 1.1.4 讲过一半:LGPL 下动态链接才能闭源。 所以做商业产品时,这一行不是技术选择,是许可证选择。
还有第四种形态,本课的 Demo 一直在用:语言绑定。 它把动态库再包一层,让你在 Python、Java、C# 里直接调。 包一层的好处是省事,坏处是有些底层能力它没暴露出来。
四、什么时候才需要碰源码
四种情况,只有第一种是必须的:
情况 要不要碰源码
现成构建缺你想要的功能 要,重新 configure 想改某个滤镜的行为 要,改 C 代码 只是想把命令跑起来 不用,下静态包 想读懂某个参数怎么实现 可以只看不编
第二行要提醒一句:改滤镜是最容易踩的坑。 滤镜是并行处理的,看起来一次处理一帧,实际上下面的代码可能在多线程跑。 随手加个全局变量,就会出现「单线程正常、多线程花屏」。
第四行反而是最值得推荐的:源码就在那儿,读它不花钱。 遇到文档说不清的行为,去 fftools 或对应的 libav* 目录里搜关键字, 通常五分钟就能看到答案——而且这个答案不会过时。
五、动手 Demo:源码编完长什么样
沙箱里没有 FFmpeg 源码,但有编译好的 Python 绑定。 它能说明「一个库编出来会变成什么样」——形态是一样的,只是换了语言。
import os, av
d = os.path.dirname(av.__file__)
mods = []
for root, _, fs in os.walk(d):
for f in fs:
if f.endswith((u".pyd", u".so")):
p = os.path.join(root, f)
mods.append((os.path.relpath(p, d), os.path.getsize(p)))
print(u"模块数", len(mods))
for r, s in sorted(mods, key=lambda x: -x[1])[:5]:
print(u"%-26s %6.0f KB" % (r, s / 1024.0))
实跑输出:
--------------------------------------------------------------------
源码编完长什么样——49 个扩展模块,各管一块
--------------------------------------------------------------------
**① 源码树是一个库一个目录,编完是一个库一堆模块**
编译产物 大小(KB) 里面是什么
----------------------------------------------------------------------
video/frame.pyd 290 AVFrame 的实现,最重的一个
filter/context.pyd 132 滤镜实例,参数解析都在这儿
codec/context.pyd 120 AVCodecContext 的实现
container/core.pyd 94 AVFormatContext 的实现
error.pyd 92 错误码与异常映射
container/output.pyd 88 封装写出那一半
**一共 49 个扩展模块。它们在 C 里是七个库,在 Python 里被拆成按对象分的模块:**
**一个库里的东西太多,绑定时按「一个结构体一个模块」切开,用起来才顺手。**
**② 最大的那个是 AVFrame,不是巧合**
**AVFrame 要管像素平面、行宽对齐、时间戳、色彩信息、附属数据……**
**它是全流程里被摸次数最多的结构,所以实现也最厚。**
**这一点在下一节的流程图里会更明显:五个阶段里有四个都在动它。**
**③ 从源码到能用的 exe,四步**
步 做什么 产物
-----------------------------------------------------------------
1 ./configure 决定开哪些开关 config.h 与 Makefile
2 make 编译七个库 libav*.so 或 .dll
3 make 编译三个工具 ffmpeg、ffplay、ffprobe
4 make install 装到位 可执行文件加头文件
**第 1 步决定后面所有事。第 2、3 步只是把决定执行掉。**
**所以「我这个 ffmpeg 怎么没有某某功能」的答案,永远在第 1 步。**
第一张表挑出最大的六个模块,第一个是 AVFrame。 它不是巧合——AVFrame 要管像素平面、行宽对齐、时间戳、色彩信息、附属数据, 是全流程里被摸次数最多的结构。
第二张表说明一件事:源码里七个库,编出来是几十个模块。 因为一个库里的东西太多,绑定时按「一个结构体一个模块」切开,用起来才顺手。
第三张表把「从源码到能用的 exe」压成四步。 第 1 步决定后面所有事,第 2、3 步只是执行。
所以「我这个 ffmpeg 怎么没有某某功能」的答案,永远在第 1 步。 想补,只能回到第 1 步重来——FFmpeg 不是插件式的,装不上去。
30 秒实验
- 搜参数:下源码包,在
fftools/ffmpeg_opt.c里搜drawtext之类不会命中,要搜-vf的处理逻辑。源码里全是参数解析表,比文档全。 - 看滤镜:
libavfilter/下每个滤镜一个vf_*.c文件(视频)或af_*.c(音频)。想看 scale 怎么算的,就打开vf_scale.c。 - 验形态:找到你机器上的
libavcodec.so或avcodec-*.dll,看它多大。几百 KB 到几十 MB 都正常,取决于开了多少编解码器。
常见误区
- 以为缺功能可以「补装一个包」。FFmpeg 的能力在 configure 时就定死了,运行时不能加载新编解码器。要补只能重编。
- 以为命令行工具是另一份代码。它就在源码树的 fftools 目录里,和库是同一份源码编出来的。
- 以为改滤镜很简单。滤镜内部可能多线程执行,随手加全局状态就会导致单线程正常、多线程花屏。改之前先看它是怎么被调度的。
自测三问(点开对答案)
1. 源码树里 fftools 目录编出什么? —— 编出 ffmpeg、ffplay、ffprobe 三个可执行文件。它们只是库的调用者,本身不含编解码逻辑。 2. 为什么 configure 之后就不能再补功能了? —— 因为 configure 生成 config.h 决定哪些代码参与编译,Makefile 也按那个决定生成。没编进去的代码根本不在二进制里,运行期无从加载。 3. 静态库和动态库在许可证上有什么差别? —— LGPL 下动态链接通常不构成衍生作品,静态链接通常算。所以做闭源产品时,选动态库不是性能考虑,是合规考虑。- ☐ (本节完,下一节 1.4 FFmpeg 常用功能)