首先,这一系列文章均基于本身的理解和实践,可能有不对的地方,欢迎你们指正。
其次,这是一个入门系列,涉及的知识也仅限于够用,深刻的知识网上也有许许多多的博文供你们学习了。
最后,写文章过程当中,会借鉴参考其余人分享的文章,会在文章最后列出,感谢这些做者的分享。html
码字不易,转载请注明出处!linux
教程代码:【Github传送门】 |
---|
使用
GCC
或CLANG
交叉编译出Android平台可使用的FFmpeg so库。为了很好的迈出FFmpeg
开发的第一步,不只要知其然,更要知其因此然。不只要知道怎么样能成功编译,更要知道为何能成功编译。在开始动手以前,建议先通读整篇文章,相信本文定可让你有所感悟。android
网上其实已经有不少的关于FFmpeg so库编译的分享,可是大部分都是直接把配置文件的内容贴出来。我想大部分去搜索 「如何编译FFmpeg so库」的人,对交叉编译这个东东都是比较陌生的。git
特别对于移动端开发者来讲,大部分人大多数时候都是在Java层作开发,不多接触到NDK层的东西。若是直接去看一份交叉编译的配置,估计会很上头。github
一般状况下,在一篇FFmpeg编译的文章下面都会有不少的相似「为何按照楼主的配置仍是没法编译成功?」的评论,那为何人家能够编译成功,咱们copy下来却不能够呢?shell
缘由有很是多,大部分其实集中在如下几个方面:bash
1. 无脑copy,祈求有一个傻瓜式的配置能够成功编译;
2. FFmpeg版本和NDK版本不少,每个版本均可能须要不同的配置;
3. 不了解每一个配置项的意义,即便好运配置对了, 可是稍微一修改,又没法正常编译了。
复制代码
为何FFmpeg让人以为很难搞?架构
我想主要是由于迈出第一步就很困难,连so库都编译不出来,后面的都是扯淡了。app
引自百度百科的定义:交叉编译,是在一个平台上生成另外一个平台上的可执行代码。cors
什么意思呢?说白了,就是在一个机器上生成一个程序,这个程序能够跑在另一个机器上。举栗:在PC上编译一个apk,这个apk能够跑在Android手机上,这其实就是一个交叉编译的过程。
咱们知道,PC上的软件是直接在PC上编译生成的,那为何Android上的软件不能在Android上本身编译生成呢?
理论上是能够,可是Android手机上的资源有限啊,在PC上编译一个apk都要那么久,你能够想象在Android手机上编译一个apk要多久吗?或者你能想象在手机上敲代码的情景吗?
那咱们会想既然PC上资源那么丰富,那可不能够利用PC来编译出在手机上能够运行的软件呢?
因而,交叉编译出现。
咱们知道PC上的环境和手机上的运行环境是绝然不一样的,若是使用PC上的环境直接编译的话,能够想象这个编译出来的App,分分钟就会挂掉。
因此,交叉编译最重要的是,要配置好编译过程当中使用到的相关的环境,而这个环境其实就是目标机器(好比Android手机)正在运行的环境。
对于C/C++的编译,一般有两个工具 GCC
和 CLANG
。
GCC
可能你们都有据说过,这是一个老牌的编译工具,不只能够编译C/C++,也能够编译Java,Object-C,Go等语言。
CLANG
则是一个效率更高的C/C++编译工具,而且兼容GCC,Google在很早之前就开始建议使用clang进行编译,而且在 ndk 17
之后,把 GCC
移除了,全面推行使用 CLANG
。
鼎鼎大名的FFmpeg,不说在音视频界如雷贯耳,就算一个不开发音视频的开发者也都是略有耳闻。
官方简介
A complete, cross-platform solution to record, convert and stream audio and video.
翻译过来就是:FFmpeg是一套集录制、转换以及流化音视频的完整的跨平台解决方案。
从这段简介能够看到FFmpeg有如下特色:
从前面的介绍,基本上能够总结出FFmepg编译的基本流程:
流程就是这么简单,接下来就来详细看看,如何经过 CLANG
和 GCC
两种方式来编译。
注:本文编译平台为Mac,建议使用Mac或者Linux进行编译,听说Windows有不少坑。
Android 的 NDK
已经迭代了不少版本,在 r17c
之后,Google正式移除 GCC
,再也不支持 GCC
,新版本的 NDK
都是使用 CLANG
进行编译。
这里就使用目前最新的 NDK r20b
版原本编译。
NDK
下载地址:Android-NDK
最主要的就是这两个路径:
编译工具链目录:
toolchains/llvm/prebuilt/darwin-x86_64/bin
交叉编译环境目录:
toolchains/llvm/prebuilt/darwin-x86_64/sysroot
复制代码
根据不一样的CPU架构区和不一样的Android版本,区分了不一样的clang工具,根据本身须要选择就行了。
本文选择 CPU 架构 armv7a
,Android版本 21
:
armv7a-linux-androideabi21-clang
armv7a-linux-androideabi21-clang++
复制代码
在 toolchains/llvm/prebuilt/darwin-x86_64/sysroot
目录下,包含了两个目录: usr/include
,usr/lib
,分别对应了 头文件
和 库文件
。
FFmpeg官网下载,直接DownLoad便可。
本文使用的是目前最新的版本 ffmpeg-4.2.2
。
下载好源码后,进入根目录,找到一个名为 congfigure
的文件,这是一个shell脚本,用于生成一些 FFmpeg
编译须要的配置文件。
这个文件很是重要,
FFmpeg
的编译配置就是靠它完成的。 后面咱们将对其中一些重要的内容进行分析,这是理解FFmpeg
编译配置的关键。
有了以上基础之后,就能够对FFmpeg进行编译了。
cross_prefix_clang
参数打开(注:不是双击运行) ffmpeg-4.2.2
根目录下的 configure
文件,搜索 CMDLINE_SET
,能够找到如下代码,而后新增一个命令行选项:cross_prefix_clang
CMDLINE_SET=" $PATHS_LIST ar arch as assert_level build_suffix cc objcc cpu cross_prefix # 新增命令行参数 cross_prefix_clang custom_allocator cxx dep_cc # 省略其余..... "
复制代码
搜索 ar_default="${cross_prefix}${ar_default}"
, 找到如下代码
ar_default="${cross_prefix}${ar_default}"
cc_default="${cross_prefix}${cc_default}"
cxx_default="${cross_prefix}${cxx_default}"
nm_default="${cross_prefix}${nm_default}"
pkg_config_default="${cross_prefix}${pkg_config_default}"
复制代码
将中间两行修改成
ar_default="${cross_prefix}${ar_default}"
#------------------------------------------------
cc_default="${cross_prefix_clang}${cc_default}"
cxx_default="${cross_prefix_clang}${cxx_default}"
#------------------------------------------------
nm_default="${cross_prefix}${nm_default}"
pkg_config_default="${cross_prefix}${pkg_config_default}"
复制代码
至于为何这么修改,将在后面的 configure
分析中详细讲解
在 ffmpeg-4.2.2
根目录下新建 shell
脚本,命名为: build_android_clang.sh
#!/bin/bash
set -x
# 目标Android版本
API=21
CPU=armv7-a
#so库输出目录
OUTPUT=/Users/cxp/Desktop/FFmpeg/ffmpeg-4.2.2/android/$CPU
# NDK的路径,根据本身的NDK位置进行设置
NDK=/Users/cxp/Desktop/FFmpeg/android-ndk-r20b
# 编译工具链路径
TOOLCHAIN=$NDK/toolchains/llvm/prebuilt/darwin-x86_64
# 编译环境
SYSROOT=$TOOLCHAIN/sysroot
function build
{
./configure \
--prefix=$OUTPUT \
--target-os=android \
--arch=arm \
--cpu=armv7-a \
--enable-asm \
--enable-neon \
--enable-cross-compile \
--enable-shared \
--disable-static \
--disable-doc \
--disable-ffplay \
--disable-ffprobe \
--disable-symver \
--disable-ffmpeg \
--sysroot=$SYSROOT \
--cross-prefix=$TOOLCHAIN/bin/arm-linux-androideabi- \
--cross-prefix-clang=$TOOLCHAIN/bin/armv7a-linux-androideabi$API- \
--extra-cflags="-fPIC"
make clean all
# 这里是定义用几个CPU编译
make -j12
make install
}
build
复制代码
这个shell脚本,大致上其实仍是很容易懂的,好比
--disabble-static
--enable-shared
分别用于禁止输出静态库,以及输出动态库;
--arch
--cpu
用于配置输出的so库是什么架构的;
--prefix
用于配置输出的so库的存放路径。
接下来重点来说一下几个选项:
--target-os=android
:在旧版本的 FFmpeg
中,对Android平台的支持并非很完善,并无 android
这个target,因此在一些比较老的文章中都会提到,编译Android平台的so库,须要对 configure
作如下修改,不然会按照 linux
标准的方式输出so库,其命名方式和Android的so不同,Android是没法加载的。
SLIBNAME_WITH_VERSION='$(SLIBNAME).$(LIBVERSION)'
SLIBNAME_WITH_MAJOR='$(SLIBNAME).$(LIBMAJOR)'
LIB_INSTALL_EXTRA_CMD='$$(RANLIB) "$(LIBDIR)/$(LIBNAME)"'
SLIB_INSTALL_NAME='$(SLIBNAME_WITH_VERSION)'
SLIB_INSTALL_LINKS='$(SLIBNAME_WITH_MAJOR) $(SLIBNAME)'
修改成:
SLIBNAME_WITH_VERSION='$(SLIBNAME).$(LIBVERSION)'
SLIBNAME_WITH_MAJOR='$(SLIBPREF)$(FULLNAME)-$(LIBMAJOR)$(SLIBSUF)'
LIB_INSTALL_EXTRA_CMD='$$(RANLIB)"$(LIBDIR)/$(LIBNAME)"'
SLIB_INSTALL_NAME='$(SLIBNAME_WITH_MAJOR)'
SLIB_INSTALL_LINKS='$(SLIBNAME)'
复制代码
可是在新版本的FFmpeg中,这个问题终于被解决了,FFmpeg加入了 android
这个 target
。因此咱们不再须要手动去修改了
。
--sysroot=$SYSROOT
: 用于配置交叉编译环境的 根路径
,编译的时候会默认从这个路径下去寻找 usr/include
usr/lib
这两个路径,进而找到相关的头文件和库文件。
r20b
版本的 NDK
系统的头文件和库文件就是在 $SYSYROOT/usr/include
和 $SYSYROOT/usr/lib
中。
基本上不少新手在编译的时候都会出现找不到各类头文件,致使编译失败。因此当编译出现找不到头文件的时候,首先要检查的就是这个路径。
一点疑问
在使用最新的
ndk r20b
版本进行编译的时候发现,即便不配置sysroot
也能够正常编译,怀疑 Android 的clang
工具是否通过了处理,会自动去寻找对应的路径。 目前没有从configure
文件中找到缘由。
若有知情者的,还望告知呀~。
说到 sysroot
就不得不提到另一个参数 -isysyroot
,这个参数也让我困惑了好久,由于不多文章会提到这个两个参数的联系和区别,然而这个参数也很致使让人很莫名奇妙的编译失败。
介绍 -isysroot
以前,先看看这个 extra-cflags
选项。
这个选项的做用是,给编译器指定除了 sysroot
以外的头文件搜索路径。好比:
--extra-cflags="-I$SYSROOT/usr/include"
# 其中 -I 用于区分不一样的路径
复制代码
而 -isysroot
是这个选项的一个配置。好比
--extra-cflags="-isysroot $SYSROOT"
复制代码
-isysroot
的做用就是,把后面的路径设置为默认的头文件搜索路径,这时候,前面 sysroot
配置路径就再也不做为 头文件
默认的搜索路径了,不过依然是 库文件
默认的搜索路径。
能够看到,这两个配置从某种程度上说是同样的:
--extra-cflags="-I$SYSROOT/usr/include"
约等于
--extra-cflags="-isysroot $SYSROOT"
复制代码
这个和上面的 extra-cflags
做用是相似的,不过是用于配置额外的 库文件
搜索路径,如
--extra-ldflags="-L$SYSROOT/usr/lib"
# 其中 -L 用于区分不一样的路径
复制代码
能够看到 extra-cflags
extra-ldflags
结合起来能够替代 sysroot
。
这个选项直译为 交叉编译前缀
,指的是交叉编译工具的前缀。
这个选项常常和另一个选项 cc
一块儿出现搭配使用。
这是什么意思呢?网上有的文章对于 cc
这个选项常常出现两种配置方式:
一种是只配置 cross-prefix
,没有配置 cc
,好比本文。
另外一种是既配置 cross-prefix
,又配置 cc
。
好比:
--cc=$TOOLCHAIN/bin/arm-linux-androideabi-gcc \
--cross-prefix=$TOOLCHAIN/bin/arm-linux-androideabi- \
复制代码
这是两种彻底不一样的配置方式,可是很神奇的是有时候他们都能成功编译,有时候又会出现找不到编译链工具的错误。
为了搞明白 cross-prefix
cc
这两个选项的配置到底有什么影响,到底应该怎么使用这两个配置,我特意仔细的去看了 FFmpeg
根目录下的 configure
配置脚本,找到了一些蛛丝马迹。
注:如下分析基于ffmpeg-4.2.2版本,其余版本可能有所不一样,掌握基本原理便可。
打开(注:不是双击运行)configure
shell脚本,首先来看看 configure 是如何获取用户配置的编译选项的。
搜索 for opt do
,能够找到如下代码
for opt do
optval="${opt#*=}"
case "$opt" in
--extra-ldflags=*)
add_ldflags $optval
;;
--extra-ldexeflags=*)
add_ldexeflags $optval
;;
--extra-ldsoflags=*)
add_ldsoflags $optval
;;
--extra-ldlibflags=*)
warn "The --extra-ldlibflags option is only provided for compatibility and will be\n"\
"removed in the future. Use --extra-ldsoflags instead."
add_ldsoflags $optval
;;
--extra-libs=*)
add_extralibs $optval
;;
--disable-devices)
disable $INDEV_LIST $OUTDEV_LIST
;;
--enable-debug=*)
debuglevel="$optval"
;;
# 省略中间一些代码...
*)
optname="${opt%%=*}"
optname="${optname#--}"
optname=$(echo "$optname" | sed 's/-/_/g')
if is_in $optname $CMDLINE_SET; then
eval $optname='$optval'
elif is_in $optname $CMDLINE_APPEND; then
append $optname "$optval"
else
die_unknown $opt
fi
;;
esac
done
复制代码
这个shell脚本的代码有不少特有的语法,也不用钻牛角尖,能大概看明白就能够了。
for循环的首行 经过分割 =
获取到用户设置的选项值 optval
。
下面除了一些特殊的选项,咱们看看最后的通配符 *)
,这段代码的目的,其实就是把用户配置的选项和值关联起来。
好比 --cpu=armv7-a
,前面三行就是把 cpu
分割出来,赋值给 optname
,再把 optval
赋值给 cpu
,说白了就是初始化了 cpu
这个变量为 armv7-a
。
搜索 android
关键字,能够找到如下代码
# ffmpeg-4.2.2/configure
if test "$target_os" = android; then
cc_default="clang"
fi
ar_default="${cross_prefix}${ar_default}"
cc_default="${cross_prefix}${cc_default}"
cxx_default="${cross_prefix}${cxx_default}"
nm_default="${cross_prefix}${nm_default}"
pkg_config_default="${cross_prefix}${pkg_config_default}"
复制代码
当你配置了 --target-os=android
的时候,FFmpeg默认的编译工具为 clang
。
cc_default
其实就是配置项 cc
的默认值,能够看到 cc_default
在这里和 cross_prefix
作了拼接。这里就是为何说 cross_prefix
是交叉编译工具前缀。
拼接完是这样的:
cc_defalut=$TOOLCHAIN/bin/arm-linux-androideabi-$cc
复制代码
看下 ar_default
cc_default
cxx_default
这些默认值是什么。
搜索 cc_default
能够找到如下代码
# ffmpeg-4.2.2/configure
ar_default="ar"
cc_default="gcc"
cxx_default="g++"
host_cc_default="gcc"
复制代码
能够看到,FFmpeg 默认的编译工具是 GCC
。
当你编译 Android 平台的库时,因为 configure
强制设置 cc_default="clang"
,因此:
当你使用 GCC
做为编译工具时,必须配置 cc
选项,或修改 configure
中的 cc_default="clang"
为 cc_default="gcc"
;
当你使用 CLANG
做为编译工具时,能够不配置 cc
选项。
仔细想一想会发现,为何当 cc
配置为下边的值时,也能够正常编译呢?
--cc=$TOOLCHAIN/bin/arm-linux-androideabi-gcc
复制代码
这时 cc_defalut
不就等于
cc_defalut=$TOOLCHAIN/bin/arm-linux-androideabi-$TOOLCHAIN/bin/arm-linux-androideabi-gcc
复制代码
这个路径确定是错的啊!
这就要来看到底 cc_default
是怎么使用的了。
搜索 set_default arch
,能够看到如下代码,在这里 configure
从新设置了 cc
的默认值。
set_default arch cc cxx doxygen pkg_config ranlib strip sysinclude \
target_exec x86asmexe nvcc
复制代码
这里调用了一个叫 set_default
的函数,来看看这个函数的实现
set_default(){
for opt; do
eval : \${$opt:=\$${opt}_default}
done
}
复制代码
这也是一个看不太懂的shell语法,大概的意思就是:for循环获取全部的输入参数变量,而后给这个变量赋值。
好比 set_default cc
,意思就是 cc=cc_default
,不过有一点要注意的是中间这个符号 :=
。
这个符号相似Java中的三目运算符:
opt != null? opt:opt_defalut
复制代码
也就是说,若是参数为空,将 xx_default
赋值给 xx
。
这就能够解释上面的疑问了。
--cc=$TOOLCHAIN/bin/arm-linux-androideabi-gcc
复制代码
set_default cc
等于没有用了。由于通过 for
循环获取了用户的配置之后, cc
不为空。 set_default
后,cc
的值是不会改变的。
当 cc
不配置的时候,FFmpeg 根据默认的拼接方式,把拼接好的路径设置给 cc
。
可是,不能配置 cc=gcc
这种,这样,最后 cc
的值就只有 gcc
,确定是不能正确找到编译工具的。
corss-prefix-clang
这个选项如今能够来解释为何前面须要修改 configure
配置脚本了。
原始的配置是这样的
ar_default="${cross_prefix}${ar_default}"
cc_default="${cross_prefix}${cc_default}"
cxx_default="${cross_prefix}${cxx_default}"
nm_default="${cross_prefix}${nm_default}"
pkg_config_default="${cross_prefix}${pkg_config_default}"
复制代码
也就是说,默认的 cc
ar
nm
路径前缀是同样的,可是 Android NDK
的路径倒是这样的
看到了不?ar
/nm
和 cc
的前缀是不同的,前者是 arm-linux-androideabi-
, 后者是 armv7a-linux-androideabi16-
。
所以,须要对 cc
和 cxx
两个前缀进行修改,为此新加了 cross_prefix_clange
来进行单独配置。
这里只是针对 NDK r20b 的状况,不一样的 NDK 版本可能有所不一样,根据这个原理去设置便可。
综上,解释了一些编译 FFmpeg 经常使用的配置选项,而且从原理上弄明白为什么要这样配置,基本上搞清楚了这些,想要组合两个不一样版本的FFmpeg和NDK来编译,都会比较容易实现。
打开cmd终端,cd 到 FFmpeg 所在目录
输入 ./build_android_clang.sh
等待编译完成,将会在 ffmpeg/android/armv7-a目录下获得 include
和 lib
两个目录,分别是 头文件
和 so库文件
目前大部分网上的文章都是使用 GCC
来编译 FFmpeg
的,下面就来看看如何配置 GCC
的编译参数。
前面就说过,NDK r17c 之后,Googole 就移除了 GCC,因此要使用 GCC 只能下载 r17c 及之前的版本,本文使用 r17c 来编译。
根据本身编译平台选择对应的版本:NDK r17c
本文选择的是 Mac 版本:Mac OS X。
和 NDK r20b
相比,NDK r17c
的目录稍微有些变化。
# 库文件路径
android-ndk-r17c/platforms/android-21/arch-arm/usr/lib
复制代码
# 头文件路径
android-ndk-r17c/sysroot/usr/include
复制代码
android-ndk-r17c/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64/bin
复制代码
能够看到,Google 将 头文件
和 库文件
分离了,这也是不少新手在编译的时候一直没有配对路径,致使编译失败的缘由。
FFmpeg 的版本依然是使用上面的 ffmpeg-4.2.2
, 固然,此次不须要修改 configure
了。
根据前面介绍的知识,很容易就能写出编译配置了
在 ffmpeg-4.2.2
根目录新建脚本: build_android_gcc.sh
#!/bin/bash
set -x
API=21
CPU=armv7-a
#so库输出目录
OUTPUT=/Users/cxp/Desktop/FFmpeg/ffmpeg-4.2.2/android/$CPU
# NDK的路径,根据本身的安装位置进行设置
NDK=/Users/cxp/Desktop/FFmpeg/android-ndk-r17c
# 库文件
SYSROOT=$NDK/platforms/android-$API/arch-arm
# 头文件
ISYSROOT=$NDK/sysroot/usr/include
# 汇编头文件
ASM=$ISYSROOT/arm-linux-androideabi
TOOLCHAIN=$NDK/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64
function build
{
./configure \
--prefix=$OUTPUT \
--target-os=android \
--arch=arm \
--cpu=armv7-a \
--enable-asm \
--enable-cross-compile \
--enable-shared \
--disable-static \
--disable-doc \
--disable-ffplay \
--disable-ffprobe \
--disable-symver \
--disable-ffmpeg \
--sysroot=$SYSROOT \
--cc=$TOOLCHAIN/bin/arm-linux-androideabi-gcc \
--cross-prefix=$TOOLCHAIN/bin/arm-linux-androideabi- \
--extra-cflags="-I$ISYSROOT -I$ASM -fPIC"
make clean all
# 这里是定义用几个CPU编译
make -j12
make
make install
}
build
复制代码
能够看到,在基本上配置和使用 CLANG
进行编译差很少。
有如下不一样:
cc
配置。由于若是不配置 cc
默认为 clang
(参考前文的分析);extra-cflags
的配置,由于 SYSROOT
中只包含了 库文件
,须要额外配置 头文件
的搜索路径;汇编头文件
的路径也不在 SYSROOT
中,也须要额外配置 ASM
。打开 cmd 终端,cd 到 ffmpeg-4.2.2 目录
执行 ./build_android_gcc.sh
经过对 configure
的分析,可让咱们更加清晰的理解每一个参数配置项的意义,以及如何搭配使用这些配置。只要清楚了各个配置的含义,不管版本怎么变化,均可很快的写出编译脚本。
当了,本文只是介绍了最基础的配置方案,你还能够经过更多的 --disable-xxx
选项实现对 FFmpeg
的裁剪,或者经过 --enable-xxx
选项,开启一些高级功能。