该篇文章翻译于A Story about FFmpeg on Android. Part I: Compilation
可能通过一种或其他的方式,许多人都知道FFmpeg。这是用于视频/音频处理的很好用的工具。但是,使其在Android上运行绝非易事。编译、Android NDK和操作系统等等的限制很令人发狂,我想告诉你一个有关为Android烹饪FFmpeg的故事。我将这个故事奉献给所有花了大量时间去搞编译的人。
首先让我们定一个任务
假设我们必须创建一个显示视频文件基本信息的应用程序:容器和视频编解码器信息以及帧大小。或者我们也想实际显示视频中的帧,我们也可以显示有关音频和字幕的信息,但这里我们仅关注视频。
如何用FFmpeg解决呢?有两个选择:
- 使用已经存在的Java库来包装预建的FFmpeg二进制文件。如果您有时间限制,这是一个不错的选择,因为您的Java / Kotlin代码库可以使用所有内容。而且您不必处理所有这些编译工作。
但是,这种简单性带来了代价:你无法控制该库的内容,你只能接受预先构建或未预先构建的内容。此外,此类库甚至可能会错过目前需要的强制性的64位支持。 - 你可以自己构建FFmpeg。因此,您将能够精准打包所需的东西,并确保您支持所有必需的ABI。您也可以根据需要添加外部库。这种方式还简化了向FFmpeg的较新版本的迁移。
同样也有代价:这种方式更耗时,因为您必须自己编译FFmpeg并将其native调用绑定到JVM
我想创建的是一个最小的Android应用程序,该应用程序当下可以实现我的任务,并在将来实现其他目标。这意味着我需要对FFmpeg的构建过程进行足够的控制。考虑到这一点,我选择第二个选项。
开始构建
现在值得提一下,FFmpeg到底是什么?其实它就是一组C库:
1.libavformat主要作用于音视频文件容器,可以读写媒体流;
2.libavcodec负责媒体流的解码和编码;
3.libswscale原始视频格式转换,用于视频场景比例缩放、色彩映射转换;图像颜色空间或格式转换,如 rgb565、rgb888 等与 yuv420 等之间转换;
4.libavutil包含一些公共的工具函数的使用库,包括算数运算 字符操作
5.libswresample用于执行高度优化的音频重采样,重新矩阵化和采样格式转换操作;
6.libavfilter是一个包含媒体过滤器的库;
7.libavdevice库提供了一个通用框架,用于从许多常见的多媒体输入/输出设备抓取和渲染,并支持多个输入和输出设备,包括 Video4Linux2,VfW,DShow和ALSA。
另外有3个命令行工具:ffmpeg、ffprobe和ffplay,3个命令行其工作作为所有上述库的外观。您实际上也可以编译它们,并以某种Runtime.getRuntime().exec(...)方式在Android上执行它们。
FFmpeg还有什么?还有它自己编译的方式。有一个configure程序脚本,它是configure && make && make install构建软件方法的一部分,它位于FFmpeg源代码的根目录中。它检查可用的构建工具,并准备内部模块进行编译。尽管它是一个Shell脚本,但它也可以在Windows上执行。
FFmpeg本身是高度可配置的。该配置脚本可以接受许多不同的参数,这些参数会影响整个构建过程的输出。使FFmpeg在Android上工作最困难的部分就是传递适当的参数。
那么……我们需要传递给该脚本什么?这取决于FFmpeg的版本。幸运的是,随着时间的流逝,FFmpeg变得对Android更友好,并且配置变得更加简单。本文基于FFmpeg 4.2。该版本不需要更改任何源代码即可进行编译。
让我们将其分为两个部分:
1.构建配置
FFmpeg是由C和汇编代码组成的。为了编译源代码,我们需要一个编译器。
这里是第一个复杂的地方:
我们实际上需要产生的二进制文件有4种类型的处理器:ARM和x86分别对应的32位和64位版本。
我们不再考虑mips,因为其支持已从Android NDK中完全删除。
假设我们的工作站仅基于x86,我们需要一个交叉编译器-该交叉编译器可以为其他类型的处理器生成二进制文件。
还需要一个链接器和一些其他工具(该工具子集称为binutils)来构建过程。
最终组件-sysroot目录。它表示带有必需库和头文件的Android 操作系统的根目录。
简而言之:编译器+ binutils + sysroot = toolchain(工具链)。
那么,我们在哪里可以获得工具链?它位于Android NDK内-一个开发工具包,允许开发Android native层应用程序。NDK的版本也很重要,因为工具链结不是永久固定的。GCC编译器被删除(从r18版本开始),因此现在只有Clang了。同样,单个工具链的各个部分也可以拆分到NDK内部的不同目录中。例如,sysroot的标头和库可以位于完全不同的位置。从NDK r19开始,工具链的所有部分最终都可以很方便的传递到FFmpeg的configure脚本中去构建使用。
让我们逐行查看特定示例:
./configure \
--prefix=${BUILD_DIR}/${ABI} \
--enable-cross-compile \
--target-os=android \
--arch=${TARGET_TRIPLE_MACHINE_BINUTILS} \
--sysroot=${SYSROOT} \
--cross-prefix=${CROSS_PREFIX} \
--cc=${CC} \
--extra-cflags="-O3 -fPIC" \
--enable-shared \
--disable-static \
${EXTRA_BUILD_CONFIGURATION_FLAGS} \
--prefix=${BUILD_DIR} /${ABI} –放置输出头文件和二进制文件的路径。
它们将分别放置在include和lib目录中。如果您希望构建诸如ffprobe之类的命令行工具,也可以有一个bin目录。
--target-OS=android- 我们的目标操作系统就是Android。
configure脚本会为您进行某些编译配置,例如,它添加了-fPIE编译器标志
--arch=${TARGET_TRIPLE_MACHINE_BINUTILS} – 此变量中有4个值:
arm,aarch64,i686和x86_64。
这里有2件重要的事情:
1)这些值是其他变量创建的一部分
2)configure脚本支持同一体系结构的别名。
例如,脚本将aarch64和arm64视为相同。有关别名的完整列表,请参考configure脚本
--sysroot=${SYSROOT} – 所需的sysroot目录现在直接位于工具链的目录内:
$TOOLCHAIN_PATH/sysroot。
顺便说一下,工具链路径本身就是$NDK_ROOT/toolchains/llvm/prebuilt/$HOST_TAG。
HOST_TAG取决于构建NDK的操作系统:
macOS--->darwin-x86_64
Linux--->linux-x86_64
32 位 Windows--->windows
64 位 Windows--->windows-x86_64
--cross-prefix=${CROSS_PREFIX} – 这是告诉所有binutils的一种方式FFmpeg的配置脚本的工具。
例如,前缀将附加到ld以获取链接器。前缀的实际值在这里:
armeabi-v7a:$TOOLCHAIN_PATH/bin/arm-linux-androideabi-
arm64-v8a:$TOOLCHAIN_PATH/bin/aarch64-linux-android-
x86:$TOOLCHAIN_PATH/bin/i686-linux-android-
x86_64:$TOOLCHAIN_PATH/bin/x86_64-linux-android-
请注意,armeabi-v7a ABI的前缀的最后一个单词不是android而是androideabi.
另外请注意,此前缀不取决于Android API级别。
--cc=${CC} – C编译器的传递必须稍有不同。首先让我向您展示实际值:
armeabi-v7a:$TOOLCHAIN_PATH/bin/armv7a-linux-androideabi16-clang
arm64-v8a:$TOOLCHAIN_PATH/bin/aarch64-linux-android21-clang
x86:$TOOLCHAIN_PATH/bin/i686-linux-android16-clang
x86_64:$TOOLCHAIN_PATH/ bin/x86_64-linux-android21-clang
1.这些不是前缀,都是实际文件的路径,都以clang结尾;
2.这些不是二进制文件,而是shell脚本。他们只是将所有参数重定向到$TOOLCHAIN_PATH/bin/clang二进制,并添加了更多参数。
只需查看其中一个文件即可。这里还有一件事:32位和64位版本的其他参数集不同;
3.每个文件名中都有一个Android API级别。我们只需要根据我们应用的minSdkVersion选择它即可。
在NDK r19中,此类clang版本的最低版本为16。对于64位版本,它以21开头,因为Lollipop是第一个首先获得64位支持的Android版本。
4.请注意,对于armeabi-v7a ABI,--cross-prefix和--cc的第一个单词是不同的:分别对应的是arm和armv7a。
--extra-cflags=”-O3 -fPIC” –这是我们如何为C编译器传递附加参数的方法。
-O3 是优化级别。
-fPIC是必要的,如果我们是构建Android so 库文件
1.-fPIC 作用于编译阶段,告诉编译器产生与位置无关代码(Position-Independent Code),
则产生的代码中,没有绝对地址,全部使用相对地址,故而代码可以被
加载器加载到内存的任意位置,都可以正确的执行。这正是共享库所要
求的,共享库被加载时,在内存的位置不是固定的。
如果不加-fPIC,则加载.so文件的代码段时,代码段引用的数据对象需要重
定位, 重定位会修改代码段的内容,这就造成每个使用这个.so文件代码段
的进程在内核里都会生成这个.so文件代码段的copy.每个copy都不一样,
取决于 这个.so文件代码段和数据段内存映射的位置. 也就是
不加fPIC编译出来的so,是要再加载时根据加载到的位置再次重定位的.(因
为它里面的代码并不是位置无关代码)
如果被多个应用程序共同使用,那么它们必须每个程序维护一份.so的代码
副本了.(因为.so被每个程序加载的位置都不同,显然这些重定位后的代码
也不同,当然不能共享)
我们总是用fPIC来生成so,也从来不用fPIC来生成.a;
-mfpu=neon 浮点协处理器指令,使用硬件浮点的时候,我们需要给编译器传递一些参数,让编译器编译出硬件浮点单元处理器能识别的指令。参数-mfpu就是用来指定要产生哪种硬件浮点运算指令,常用的有vfp和neon等.
-mfloat-abi=soft使用这个参数时,其将调用软浮点库(softfloat lib)来持
对浮点的运算,GCC编译器已经有这个库了,一般在libgcc里面。这
时根本不会使用任何浮点指令,而是采用常用的指令来模拟浮点运算。
但使用的ARM芯片不支持硬浮点时,可以考虑使用这个参数。
-mfloat-abi=softfp
-mfloat-abi=hard
这两个参数都用来产生硬浮点指令,至于产生哪种类型的硬浮点指令,
需要由-mfpu=xxx参数来指令。这两个参数不同的地方是:
-mfloat-abi=softfp生成的代码采用兼容软浮点调用接口(即使用-mfloat-abi=soft时的调用接口),
这样带来的好处是:兼容性和灵活性。库可以采用-mfloat-abi=soft编
译,而关键的应用程序可以采用-mfloat-abi=softfp来编译。特别是在库
由第三方发布的情况下。
-mfloat-abi=hard生成的代码采用硬浮点(FPU)调用接口。这样要求所有
库和应用程序必须采用这同一个参数来编译,否则连接时会出现接口不
兼容错误。
--enable-shared和--disable-static–在这里,我们实际上是在告诉配置脚本,我们只对共享库感兴趣,而对静态库不感兴趣。
我们如何在静态库和共享库之间进行选择?
长话短说:共享库用作单独的文件。因此,实际上它们可以被一个接一个地导入,甚至只是现在实际需要的一个子集。
这样可以减少内存压力。这只是一个很小的优化。
静态库必须嵌入到其他共享库或可执行文件中。这样,您就要导入一个大的二进制文件(如果将所有内容合并在一起).
${EXTRA_BUILD_CONFIGURATION_FLAGS}给特殊的ABI添加额外信息:
-x86_64 adds --x86asmexe=${TOOLCHAIN_PATH}/bin/yasm 指定汇编编译器所在工具链路径下哪个内部目录
-86,是指定禁用汇编编译器以此达到优化目的(--disable-asm),
如果不这样做,最后的so库文件讲具有Text Relocations,这是从>=23版本开始是禁止的,这将导致某些性能下降。
FFmpeg并不会解决此问题,有关详细信息,请参见链接。
2.内容配置
如前所述,FFmpeg是高度可配置的,您可以裁剪不需要的某些部件。让我向你展示我的版本,以将脚本配置为具有尽可能小的二进制文件,并且仍然可以完成我的任务:
--disable-runtime-cpudetect \
--disable-programs \
--disable-muxers \
--disable-encoders \
--disable-avdevice \
--disable-postproc \
--disable-swresample \
--disable-avfilter \
--disable-doc \
--disable-debug \
--disable-pthreads \
--disable-network \
--disable-bsfs \
--disable-decoders \
${DECODERS_TO_ENABLE}
有关其他标志的完整列表,只需查看configure脚本本身,以查看哪些可以禁用以及哪些默认启用。
突出显示该列表的某些部分:
1.我的应用程序将仅从视频文件读取数据。因此,我只需要解复用器(demuxers)和解码器(decoders),而不需要复用器(muxers)和编码器(encoders)。查看此链接以了解更多术语。
2.该应用程序仅适用于视频解码器,因此我准备了FFmpeg中可用的视频解码器的特定列表,然后禁用了所有解码器(--disable-decoders),仅启用了特定于视频的解码器。因此,音频和字幕解码器没有包含。DECODERS_TO_ENABLE变量实际上是--enable-decoder=$video_decoder_name的序列。
3.有一个--enable-small标志可以生成更小的二进制文件,但我不得不忽略它,因为它会删除视频编解码器和容器的名称,而我的应用程序实际上想显示此信息。
4.对于我来说,其余的FFmpeg部分(例如网络,媒体流过滤器或命令行界面)完全没有必要。
需要强调的是,此内容配置特定于我的应用程序。只需查看您的要求并进行自己的配置即可。如果您想拥有尽可能小的二进制文件,则此部分是必需的。
让我们总结一下
我已经准备了一个包含单个脚本的存储库,该脚本可以完成上述所有操作:
它将为您下载FFmpeg的源代码。
它检查结果二进制文件中的文本重定位;
它集成了Travis CI,可以在云中执行脚本。
它可以在macOS,Linux和Windows上执行;