音视频开发系列的第一篇,我想从 FFmpeg 的编译说起。这不是没来由的选择——我见过不少朋友在后面的开发中被库带崩、被链接错误卡住,实际原因都在编译阶段埋下了雷。FFmpeg 编译这事儿,理解清楚之后其实就两个动作:configure 和 make,但这两个动作背后牵连着依赖查找、功能裁剪、交叉编译思路好几条线。如果你正准备入门音视频开发,或者已经做了几年业务开发但一直只用别人编好的库,这篇内容应该能帮你把整条线串起来。
我原本也以为编译 FFmpeg 就是把官方源码拉下来跑三条命令的事,真到自己动手才发现,编译的目的不同,configure 的开关组合完全不同。有人是为了一台机器上做视频转码,有人是为了做成 Android 端 SDK 的解码内核,还有人只是为了调试某个滤镜算法。这些场景对 FFmpeg 的要求不一样,盲目全量编译不仅有踩坑风险,产出物也不一定适合你的项目。这篇就把我的实操过程、每个参数的选择逻辑、踩过的坑和修复方法完整写下来。
1. 为什么第一篇文章就从 FFmpeg 编译开始
1.1 现成的库为什么不够用
很多新手会有一个疑问:FFmpeg 官网上不是有编译好的包吗?直接用不就行了。这话在多数场景下成立,但在音视频开发里完全依赖现成库,非常容易遇到三件麻烦事。
第一是功能缺失。官方默认编译出来的库通常只包含基础能力,像 H.264/H.265 的编码器、MP3 编码器、字幕处理这类强依赖第三方库的功能默认是关掉的。你拿着官方静态包去做推流或者录屏,到了编码环节发现libx264根本不存在,整个管道就断了。第二是没法裁剪。移动端安装包体积敏感,一份完整的 FFmpeg 光 .so 加起来就有几十 MB,但你实际用到解码器的也许就一两个。默认包帮不了你做减法。第三是平台适配。Android 的 NDK 工具链和桌面 Linux 的 ABI 不一致,桌面平台编译出来的库放到 Android 上完全跑不了,必须自己用交叉编译来生成目标平台的库。
所以自己做一次编译,不是在重复造轮子,是在把控制权拿回到自己手里。你用哪些编解码器、要哪些滤镜、不要哪些协议层、产物多大,都由 configure 阶段的一条条参数决定。
1.2 看懂 FFmpeg 的功能骨架
在动手编译之前,有必要先知道 FFmpeg 不止是一个软件,它是一组库和一组可执行程序的结合。编译完之后你会得到类似下面的产物:
- 可执行文件:
ffmpeg(转码)、ffprobe(探测媒体信息)、ffplay(播放器) - 库文件:
libavcodec(编解码)、libavformat(封装解析)、libavfilter(滤镜)、libavutil(公共工具)、libswscale(图像缩放转换)、libswresample(音频重采样)
其中libavcodec和libavformat是核心中的核心,几乎所有音视频工程都用这两个库。编译时这些库彼此有依赖关系,比如libavfilter依赖libavcodec和libavformat,这中间的顺序和开关在 configure 阶段自动处理,我们不用手动管理,但心里要有个谱。
有一个类比我觉得很贴切:FFmpeg 就像一个集成厨房,libavcodec是大厨,负责把原料变成可食用的半成品;libavformat是传菜员,负责把半成品装进符合规矩的餐盒(容器格式);libavfilter是摆盘师傅,负责加滤镜做处理。编译 FFmpeg 就相当于决定这个厨房配备哪几位师傅、每个师傅带什么工具。
1.3 哪些人强烈建议自己编译
不是所有人都需要自己编译 FFmpeg,但下面三类人我建议把编译能力掌握起来:第一类是做播放器、编辑器、音视频 SDK 的客户端/服务端工程师,因为业务库迟早要跟 FFmpeg 深度耦合;第二类是做跨平台功能的移动端工程师,你们需要为不同 CPU 架构生成对应库;第三类是算法工程师,调试自定义滤镜时需要修改 FFmpeg 源码,改完必然要重新编译。
这篇文章里我会先从最基础的本地编译讲起,再把 Android 交叉编译的思路单独拎出来说,因为这两块是音视频开发日常使用频率最高的两块。
2. 动手前的环境体检清单
2.1 把最小工具链备齐
编译 FFmpeg 本质上是把 C 源码变成二进制产物,所以第一件事是确认编译工具链完整。以 Ubuntu/Debian 系统为例,我一般会先跑这么一段:
sudo apt update sudo apt install -y build-essential nasm pkg-config git这里build-essential包含 gcc、g++、make 等基础工具,nasm是汇编器,FFmpeg 里很多 CPU 优化代码(比如 x86 平台的 SIMD 加速)依赖它来编译。pkg-config用来查找第三方依赖库的头文件和链接参数,后面找 x264、mp3lame 都靠它。
nasm经常被忽略,少了它 configure 阶段会直接提示nasm not found。旧版 FFmpeg 还能退回到yasm,但现在主流版本更偏好 nasm,所以这一步装好能省不少事。
macOS 上则要确保 Xcode Command Line Tools 已经装好,然后通过 Homebrew 安装 pkg-config、nasm 等工具。Windows 上我会推荐使用 MSYS2 环境,它提供类 Linux 的终端和包管理工具,整体体验比在 Visual Studio 里折腾要顺滑得多。
2.2 获取源码的两种方式
获取 FFmpeg 源码有两条路,看你当时的需求。
想跟踪最新特性和修复,用 git clone:
git clone https://git.ffmpeg.org/ffmpeg.git ffmpeg cd ffmpeg想保证版本稳定、方便以后复现构建,就去官网下载 tar 包,比如ffmpeg-6.1.tar.xz,解压进入目录。我实际做工程时更倾向于下载 release 版本而非 git 主干,因为主干上的 API 变化快,今天编出来的接口细节过两周可能就变了,商业项目里这种不可控因素不太好接受。
注意源码目录不要放到有空格或中文的路径下,configure 脚本对这类路径处理不友好,会引发一些莫名奇妙的路径拼接错误。这个我踩过,白折腾了半个小时才发现是目录名问题。
2.3 pkg-config 在编译里的角色
编译 FFmpeg 时用到第三方库,最常见的就是 libx264、libmp3lame、libfdk-aac。FFmpeg 的 configure 怎么知道这些库装没装、头文件在哪?靠 pkg-config。
pkg-config 会搜索.pc文件,这些文件内部记录了某个库的安装路径、头文件路径、链接参数。系统默认搜索/usr/lib/pkgconfig、/usr/lib/x86_64-linux-gnu/pkgconfig这类目录。如果你把第三方库装到了自定义前缀目录,就需要手动告诉 pkg-config 去哪里找:
export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig:$PKG_CONFIG_PATH这个环境变量的设置非常关键,我遇到过三次ERROR: libx264 not found,前两次以为是 x264 没装成功,后来才发现 x264 已经装到了/usr/local,但 pkg-config 的搜索路径根本没覆盖那个目录。把PKG_CONFIG_PATH指对之后,问题立刻解决。
如果你用了 Linux 发行版自带的 libx264-dev 包,一般不需要手动设置路径;但如果你是自己源码编译安装的 x264,那PKG_CONFIG_PATH几乎是必设的。
3. configure 参数全解析:这些开关到底在干什么
3.1 默认行为先搞清楚
拿到源码后,直接跑./configure并不是完全不能用,但默认配置是“全量开启”的,意味着 FFmpeg 会尝试编译几乎所有它能支持的特性,包括大量你永远用不到的编码器、滤镜、协议层。这样做的结果有两点让人难受:编译时间特别久,编译产物特别大。
我当年第一次编译时没加任何参数,机器比较旧,跑了将近四十分钟,产出的库文件体积也大得离谱。后来学会用--disable-everything配合按需开启,编译加裁剪一次搞定,速度还快不少。
所以记住一个原则:全量开关是为了兼容性,按需开关才是为了实际工程。configure 支持--disable-everything先把所有功能关掉,然后单个开启你真正需要的模块。这种做法的收益在移动端尤其明显,一个裁剪得当的 FFmpeg 能比全量版本小好几倍。
3.2 从“全要”到“够用”:enable 与 disable 的取舍
--disable-everything之后,你最常开的是编解码器和滤镜。FFmpeg 的编解码器开关格式是--enable-encoder=名字和--enable-decoder=名字,滤镜是--enable-filter=名字。
我整理了一个常用的最小配置供参考,这段配置适合做本地转码工具的场景:
./configure \ --prefix=/usr/local/ffmpeg \ --enable-gpl \ --enable-libx264 \ --enable-encoder=h264 \ --enable-decoder=h264 \ --enable-decoder=aac \ --enable-encoder=aac \ --enable-filter=scale \ --enable-filter=yadif \ --disable-doc \ --disable-debug \ --disable-ffplay \ --disable-ffprobe这里我保留了ffmpeg主程序,用它来做转码测试;禁掉了ffplay和ffprobe,因为开发和调试阶段我用不到它们,后续需要再单独开。--disable-doc和--disable-debug能明显缩短编译时间,前者跳过文档生成,后者去掉调试符号减少产物体积,代价是出问题时不能直接用 gdb 看内部变量,这点对开发阶段不太友好,所以我只在发布构建时才用--disable-debug,平时开发构建保留调试符号。
再强调一次:--disable-everything是裁剪的大前提。如果没有它,你手动开的一两个开关并不能抵消默认全量编译带来的体积膨胀。
3.3 第三方编码器怎么开:GPL、nonfree 与依赖库绑定
FFmpeg 要启用第三方库,必须先把对应依赖编译好,然后在 configure 参数中加上对应的--enable-libxxx。这里有几个许可证概念需要明白,因为它直接决定你能不能商用。
--enable-gpl允许启用 GPL 协议的库,最常用的 libx264 就在这一类。如果你用了 GPL 库,整个产品的代码开源义务需要评估好,这是法律边界问题,别踩线。--enable-nonfree对应 libfdk-aac 这类不是自由使用的编码器,两者不要混淆。
以启用 libx264 为例,需要先编译装好 x264,再回到 FFmpeg 源码目录执行:
./configure --enable-gpl --enable-libx264如果你要音频编码走 libmp3lame:
./configure --enable-libmp3lamelibfdk-aac 则必须加--enable-nonfree --enable-libfdk-aac。这里有个经验:libfdk-aac 的编码质量在 AAT 编码器里确实排第一梯队,但它是 nonfree 协议,如果只是为了系数 AAC,直接用 FFmpeg 内置的aac编码器就够,省去不少法律上的麻烦。
这些第三方库的编译顺序是:先编译第三方库本身,再编译 FFmpeg。顺序反了会提示找不到对应库。如果你编译第三方库时用了自定义安装目录,那么还要把它的 pkgconfig 路径导出到PKG_CONFIG_PATH,上一节已经展开过。
3.4 prefix 和 strip:分隔“编译环境”与“发布产物”
--prefix指定安装路径,比如/usr/local/ffmpeg。很多人不设置它,直接把 FFmpeg 装进系统默认目录,这在你只是自用的情况问题不大,但如果以后要打包发布或者切换版本,自定义前缀会让管理更干净。
我的习惯是先建立一个工作根目录,比如~/dev/ffmpeg_build,把所有第三方库和 FFmpeg 都安装到~/dev/ffmpeg_build/ffmpeg下,这样整个构建根目录可以随时删掉重来,不会污染系统。
安装前还可以加--enable-stripping,它类似于strip命令,移除符号表信息,进一步减小动态库体积。这个开关和--disable-debug有相似之处,但控制点不一样。发布构建建议两者都开,调试构建都关。
另外要注意--enable-pic这个开关。如果你要把 FFmpeg 的静态库链接到其他动态库里,编译时必须生成位置无关代码,也就是开 PIC。Android 交叉编译时这个开关几乎是必开的,否则链接阶段会报重定位错误。本地编译生成可执行文件时,PIC 不是刚需,但如果你同时生成静态库和动态库,最好统一开启。
4. make 与安装:完整编译一次 FFmpeg
4.1 最常用的本地编译组合
我直接在命令行里把常用的本地编译流程完整走一遍。这里假设你已经进入 FFmpeg 源码目录,并且已经安装好上一节提到的工具链。
第一步,先检查环境:
pkg-config --version nasm -v gcc --version三个命令都正常输出,环境基本没问题。接着执行 configure:
./configure \ --prefix=/usr/local/ffmpeg \ --enable-gpl \ --enable-libx264 \ --enable-static \ --disable-shared这里我选择了--enable-static --disable-shared,也就是只生成静态库。静态库的好处是链接后不依赖运行时动态库,部署简单;坏处是产物体积会增大,而且如果多个模块都链了同一个静态库,内存占用会上升。桌面端做工具类软件,静态链接最省心。
如果你需要给别人做二次开发库,一般会开--enable-shared,生成.so或.dylib。注意动态库模式下 FFmpeg 的库之间存在依赖顺序,安装后运行 ffmpeg 可能提示error while loading shared libraries,原因是系统没找到刚安装的libavcodec.so,这种问题解法在后面问题排查里细说。
4.2 make 阶段的并行编译与时长控制
configure 成功之后,就是最直观的编译阶段:
make -j$(nproc)-j参数允许并行编译,.nproc自动读取 CPU 逻辑核心数。以八核十六线程的机器为例,全量编 FFmpeg 大约在八到十五分钟,裁剪后的配置会快不少;如果你没加-j,默认单线程会把时间拖长到四十分钟以上,这是新手最容易忽略的点。
编译过程中如果某个模块报错,make 会自动停在出错文件处,不会继续浪费时间。看到Error 1时不要急着重跑整个 make,先看错误信息,九成是依赖缺失或路径问题。需要重新配置的时候,最好执行一次make distclean清除之前生成的配置文件,避免旧配置残留影响新参数生效。这一点官方文档没强调,但我在改 configure 参数后直接重新 make 时踩过坑,提示的报错信息完全对不上当前配置。
make 完成后:
make installffmpeg 相关的可执行文件、静态库、头文件就被安装到--prefix指定的目录。你可以用ls /usr/local/ffmpeg/bin查看产物是否齐全。
4.3 编译产物清单:二进制、静态库和头文件各有什么用
安装完成后看一眼目录结构:
/usr/local/ffmpeg/ ├── bin/ │ ├── ffmpeg ├── lib/ │ ├── libavcodec.a │ ├── libavformat.a │ ├── libavfilter.a │ ├── libavutil.a │ ├── libswscale.a │ └── libswresample.a └── include/ ├── libavcodec/ ├── libavformat/ ├── libavfilter/ ├── libavutil/ ├── libswscale/ └── libswresample/bin/ffmpeg是转码命令行工具,验证功能最直接。lib/下的.a文件是链接时要用到的静态库;include/下的头文件是写代码时必须引用的 API 声明。这三块刚好对应音视频工程集成的三个环节:工具验证、链接库、编译头文件路径。
验证编译产物是否正常,跑一下版本回显:
/usr/local/ffmpeg/bin/ffmpeg -version如果你开了 libx264,还会在输出里看到--enable-libx264,这说明配置生效了。接着测试一个最简单的转码命令,把输入文件转成 H.264 编码的 MP4:
/usr/local/ffmpeg/bin/ffmpeg -i input.mp4 -c:v libx264 -preset fast output_h264.mp4这一步能过,说明你的 FFmpeg 具备实际转码能力,不只是“编译成功了”而是“编译对了”。
5. 交叉编译到 Android 平台:音视频 SDK 的典型场景
5.1 交叉编译需要哪些额外参数
移动端集成 FFmpeg 时,绝大多数是 Android 场景,因为 iOS 有系统级的 AVFoundation,不少情况可以绕开 FFmpeg,但 Android 上你要处理复杂容器格式或者第三方编码器,FFmpeg 依然是主流方案。
交叉编译和本地编译的本质区别是:编译器的目标架构不再是当前机器的架构。比如你在 x86_64 的开发机上生成 arm64-v8a 的库,就需要使用 NDK 中对应的工具链。
拿 Android 的 arm64-v8a 为例,configure 的参数是这个形态:
./configure \ --target-os=android \ --arch=aarch64 \ --cpu=armv8-a \ --cross-prefix=$NDK_TOOLCHAIN/bin/aarch64-linux-android- \ --sysroot=$NDK_TOOLCHAIN/sysroot \ --enable-cross-compile \ --enable-pic \ --enable-static \ --disable-shared \ --disable-programs \ --disable-doc \ --disable-debug关键参数逐个拆开讲。--cross-prefix必须指向 NDK 工具链中带前缀的编译器,比如aarch64-linux-android-,FFmpeg 会把它拼上gcc或clang来调用实际编译器。--sysroot指定 Android 平台的头文件和标准库路径,NDK 每个 API level 都有独立 sysroot,不要混用。
--enable-cross-compile是交叉编译的总开关,没开的话 configure 会认为你忘装了某个编译器并直接报错。--disable-programs在移动端很有用,因为 Android 库里我们不需要ffmpeg、ffprobe可执行文件,只保留库文件本身。这样可以避免 NDK 环境下链接 glibc 相关组件引发错误,也减小产物体积。
还有一个隐藏关键点:--extra-cflags和--extra-ldflags。如果你在用 FFmpeg 时还要链接其他第三方库(比如 openssl、x264),需要手动把它们的头文件和库路径加进去:
--extra-cflags="-I$OTHER_LIB_PATH/include" --extra-ldflags="-L$OTHER_LIB_PATH/lib"不然就算依赖库已经编译成.a,FFmpeg 也找不到它。
5.2 静态库和动态库的发布策略
Android 场景我推荐静态库方案,也就是只编出.a文件。理由有两条:一是 Android 自身的系统库列表里没有 FFmpeg 的 .so,你如果提供的是 .so,得自己管理libavcodec.so、libavformat.so等一众文件的加载顺序和依赖,很容易出UnsatisfiedLinkError;二是多个 .so 文件在 Android 安装包里比较零散,合并成一个 .so 反而省体积省加载时间。
具体做法是编译出 FFmpeg 的静态库后,再用 Android NDK 提供的编译脚本,把它们和你的业务代码一起链接成一个总体的libyour_sdk.so。这个总 .so 可以暴露 JNI 接口给上层调用,FFmpeg 的静态库在链接时会被整体吸收进去。
如果你的应用里确实需要动态更新编解码器,或者模块间要解耦,那才考虑--enable-shared编 .so。但动态库方案在 Android 上要特别注意 RTTI 和异常开关的一致性,否则运行时会直接 crash,排查非常痛苦。
5.3 交叉编译最容易踩的几个坑
第一个坑是 NDK 版本与 FFmpeg 的匹配。老版本的 FFmpeg 对 NDK r21 以上的新工具链支持不完整,可能编译到一半报unknown model之类的错误。通用的解法是查一下你手里的 FFmpeg 版本对应的 NDK 兼容范围,实在不行就分架构编译再合并,别硬碰。
第二个坑是 CPU 指令集优化参数。不同 Android 设备的 CPU 能力差异大,如果你在 configure 里指定了过高的--cpu优化等级,产出的库在小核设备上可能直接 illegal instruction。我的习惯是只指定--arch和--cpu的保守值(如 armv8-a),不做激进优化。
第三个坑是 sysroot 下缺少某些系统库。比如libpostproc在某些配置下需要额外的头文件,找不到时 configure 会提示检查错误日志。遇到这种提示先看config.log,里面最后一段会有具体失败原因,比猜效率高得多。
第四个坑是 clang 与 gcc 的混用。NDK 从 r18 开始默认使用 clang,如果你之前习惯性把--cc=gcc写进配置,会出现找不到编译器或者链接失败。NDK 新版本建议不要指定--cc,让 FFmpeg 自己选。
6. 编译错误排查手册:遇到问题别慌
6.1 configure 阶段高频报错
configure 报错有个特点:它通常会明确写出ERROR: xxx,比如nasm not found、libx264 not found,所以第一步永远是读错误关键字,然后对症下药。
nasm not found直接安装 nasm,libx264 not found则先确认 x264 是否编译安装,其次确认PKG_CONFIG_PATH是否正确指向 x264 的 pc 文件目录。我当时犯过的错误是把 x264 编译安装到了/usr/local,却没有导出PKG_CONFIG_PATH=/usr/local/lib/pkgconfig,configure 一直找不到。加上之后就通了。
还有一种报错形式是ERROR: x265 not found, use --enable-libx265,但你已经加了--enable-libx265还报错,这种多半是 x265 的 pc 文件路径没配置好,跟上面的解法相同。configure 阶段最万能的自查方式是:
grep -n "ERROR" config.logconfig.log会记录详细的检测过程,搜索 ERROR 关键字找到最末位的失败原因,比看脚本输出更准确。
6.2 make 阶段高频报错
make 阶段的报错常见的有两类。第一类是编译某个 C 文件时提示头文件缺失,例如libavcodec/*.h: No such file or directory。这种一般是你在 configure 时手动指定了第三方库路径,但实际路径不对或路径下有特殊字符。重新检查--extra-cflags的-I参数。
第二类是汇编文件编译失败,通常和 nasm 版本有关。FFmpeg 新版本对汇编指令的优化要求较高,老版本的 nasm 容易报error: instruction expected。解决方法是把 nasm 升级到较新版本,不要用系统自带的老版本。
如果 make 中途失败后再改配置,务必先运行make distclean再重新 configure。不清理旧配置的话,新参数经常融合不进去,产生的报错还莫名其妙。这是我在本地和交叉编译中都踩过的坑。
6.3 链接阶段:头文件和库对不上
这种问题多半发生在你用自己的代码引用 FFmpeg 库时。现象是编译期正常,链接时报一堆undefined reference to avcodec_xxx。
首选检查你链接的所有 FFmpeg 库名字是否齐全,顺序是否正确。静态库链接时有依赖顺序要求,比如libavfilter.a依赖libavcodec.a,那你链接时要把libavfilter放在libavcodec前面。更省事的办法是使用pkg-config获取链接参数:
export PKG_CONFIG_PATH=/usr/local/ffmpeg/lib/pkgconfig pkg-config --libs libavcodec libavformat libavutil它会自动输出正确的-L和-l列表,直接拼接在你的编译命令后面,比自己手写省心。
另外要注意链接的是静态库还是动态库的混合场景。FFmpeg 静态库开启 PIC 后才可以被动态库引用,如果你发现cannot find -lavcodec或者重定位错误,先确认编译 FFmpeg 时是否开了--enable-pic。
6.4 运行时找不到动态库的三种解法
如果编译的是动态库版本,安装后运行ffmpeg常看到这种错误:
ffmpeg: error while loading shared libraries: libavdevice.so.60: cannot open shared object file: No such file or directory原因是系统动态链接器并不知道你安装目录下的新库。解法有三种。
第一种是临时导出库路径再运行:
export LD_LIBRARY_PATH=/usr/local/ffmpeg/lib:$LD_LIBRARY_PATH ffmpeg -version第二种是把库目录写入系统缓存,执行ldconfig。注意这需要把库路径放到/etc/ld.so.conf.d/或系统默认库目录里,否则不生效。
第三种是将相对路径嵌入可执行文件,用--prefix配合 RPATH。不过这条链路比较复杂,不是普通用户的首选。我日常调试直接用第一种,打包发布时用第二种,真正做商业应用时基本不依赖 FFmpeg 动态库,直接静态链接最省事。
7. 一些小经验,顺手分享
最后再分享几个我在多次编译 FFmpeg 后沉淀下来的小习惯,不算系统教程,但挺管用。
我现在每个项目都会维护一个 build 脚本,比如build_android.sh,把 configure 的参数、PKG_CONFIG_PATH的导出、make 和安装路径全部固化下来,每次改编版本只改变量,不重复敲命令。这样做的好处是,几个月后回来看当时编译的是什么参数,脚本一目了然;遇到 CI 重建也更稳定,不会因为你换了机器少装某个依赖而导致构建失败。
还有一点,我习惯在 make 成功和 install 成功后各打一个时间戳记录耗时,比如文字输出里带time命令的包裹。别小看这个细节,它能帮你快速判断机器性能和配置是否发生了突变。有一回我发现凭空多编了十分钟,查了一圈才知道是 configure 时忘了加--disable-doc,白生成了一堆文档文件。
对刚接触 FFmpeg 编译的朋友,我的建议很简单:先按这篇文章里的本地编译流程跑通一次,确认 FFmpeg 可执行文件能正常转码,再考虑裁剪和交叉编译。编译本身不是目的,能根据自己的业务需求定制一个可用、可控的 FFmpeg 才是目的。后面我会在这个系列里继续聊 FFmpeg 的 API 调用、滤镜链设计、编码参数调优等内容,第一条路已经铺好了,剩下的就是一步步踩过去。