上一节我们讲到了通过自定义的NDK交叉编译链来编译第三方库,还不了解自定义交叉编译工具链的同学,可以看下上一节的内容,本节我们讲一下如何使用NDK交叉编译链来编译我们经常用到的ffmpeg这个库。
编译环境
操作系统:mac os 10.13.5
ndk版本:android-ndk-r17b
编译器:clang
ffmpeg版本:4.0.2
armeabi工具链
首先,这里我们需要使用到的是armeabi的交叉编译工具链,关于工具链的自定义,如果不知道如何自定义,我们可以参考我前面的文章自定义NDK交叉编译链,本节中我要采用armeabi这个平台下的交叉编译链来编译ffmpeg库。
关于ffmpeg
ffmpeg,我想只要做音视频相关工作的人,大概可能有至少50%的时间需要与这个开源库打交道,关于他的强大之处,请自行google,这里不做更多的叙述。需要说明的一点是,由于这个库的强大之处,在编译的时候提供了很多的选项来定制这个库的功能,比如音视频编解码格式的支持,openssl的支持等等非常多的功能,这里的定制功能,我们不做过多的讨论,在后续的文章中我会特别讲一些关于定制方面的内容。
编译环境脚本
我们这里整理了一个编译环境的脚本ffmpeg-toolchain-env.sh
#!/bin/sh
# Android cross-compile environment setup script
# Author : eggsy
# Date : 2018-08-28
# Version : 1.0
# Android NDK sources and standalone toolchain is put here
export DEV=${HOME}/Library/Android/sdk
export CHAIN_ENV=${HOME}/Chain/android-toolchain
# All the built binaries, libs and their header will be installed here
export PREFIX=${HOME}/Chain
# static or share libs dir
export OUT_PUT=${PREFIX}/android-output
# Don't mix up .pc files from your host and build target
export PKG_CONFIG_PATH=${PREFIX}/lib/pkgconfig
# The building system we are using (Linux x86_64)
export BUILD_SYS=arm-linux-gnu
# Set Android target API level
export ANDROID_API=21
# Set Android target arch
export ANDROID_ARCH=arm
# Set Android target name, according to Table 2 in
# https://developer.android.com/ndk/guides/standalone_toolchain.html
export ANDROID_TARGET=armv5te-none-linux-androideabi
# The cross-compile toolchain we use
export TOOLCHAIN=arm-linux-androideabi
# The path of standalone NDK toolchain
# Refer to https://developer.android.com/ndk/guides/standalone_toolchain.html
export NDK_TOOLCHAIN=${CHAIN_ENV}/${ANDROID_ARCH}
# export toolchain path
export PATH=${NDK_TOOLCHAIN}/bin:$PATH
# Set Android Sysroot according to API and arch
export SYSROOT=${NDK_TOOLCHAIN}/sysroot
# this one is the absolute, prebuilt path
# Binutils path
export CROSS_PREFIX=${NDK_TOOLCHAIN}/bin/${TOOLCHAIN}
# this one is the absolute, prebuilt path
# Non-exhaustive lists of compiler + binutils
export AR=${CROSS_PREFIX}-ar
export AS=${CROSS_PREFIX}-as
export LD=${CROSS_PREFIX}-ld
export NM=${CROSS_PREFIX}-nm
export CC=${CROSS_PREFIX}-gcc
export CXX=${CROSS_PREFIX}-g++
export CPP=${CROSS_PREFIX}-cpp
export CXXCPP=${CROSS_PREFIX}-cpp
export STRIP=${CROSS_PREFIX}-strip
export RANLIB=${CROSS_PREFIX}-ranlib
export STRINGS=${CROSS_PREFIX}-strings
# Set build flags
# Refer to https://developer.android.com/ndk/guides/standalone_toolchain.html
export PATH=$PATH:${PREFIX}/bin:${PREFIX}/lib
export CFLAGS="--sysroot=${SYSROOT} -I${SYSROOT}/usr/include -I${PREFIX}/include -fPIE -DANDROID -Wno-multichar"
export CXXFLAGS=${CFLAGS}
export CPPFLAGS="--sysroot=${SYSROOT} -I${SYSROOT}/usr/include -I${NDK_TOOLCHAIN}/include/c++/ -DANDROID -DNO_XMALLOC -mandroid"
export LIBS="-lgcc"
export LDFLAGS="-Wl,-rpath-link=-I${SYSROOT}/usr/lib -L${SYSROOT}/usr/lib -L${PREFIX}/lib -L${NDK_TOOLCHAIN}/lib"
编译ffmpeg
设置编译环境
source ffmpeg-toolchain-env.sh
配置与编译
./configure --enable-static --disable-shared --disable-doc --disable-ffmpeg --disable-ffplay --disable-ffprobe --disable-avdevice --disable-doc --disable-symver --prefix=${OUT_PUT}/ffmpeg/${ANDROID_ARCH} --target-os=android --arch=arm --enable-cross-compile --sysroot=${SYSROOT} --cross-prefix=${TOOLCHAIN}- --extra-cflags="${CFLAGS}" --extra-ldflags="${LDFLAGS}" --extra-libs="${LIBS}" --extra-cxxflags="${CXXFLAGS}"
make
make install
编译结果
我们看到这里有
include
、
lib
、
share
目录,其中
include
是相关的头文件,
lib
中是边界结果的静态库
我们将lib和include拷贝到我们的工程中,就可以开始进行ffmpeg的相关开发了。这里看我们生成的是静态库,因为我们上面configure中我们使用了
--enable-static --disable-shared
参数,如果要生成动态库,这里可以反过来配置
--disable-static --enable-shared
,后面执行
make clean
./configure --enable-static --disable-shared --disable-doc --disable-ffmpeg --disable-ffplay --disable-ffprobe --disable-avdevice --disable-doc --disable-symver --prefix=${OUT_PUT}/ffmpeg/${ANDROID_ARCH} --target-os=android --arch=arm --enable-cross-compile --sysroot=${SYSROOT} --cross-prefix=${TOOLCHAIN}- --extra-cflags="${CFLAGS}" --extra-ldflags="${LDFLAGS}" --extra-libs="${LIBS}" --extra-cxxflags="${CXXFLAGS}"
make
make install
这时候得到的编译结果与上面类似,不过lib
中获得的是动态库
其中
libavcodec.so
与
libavcodec.so.58
都是软连接指向
libavcodec.so.58.18.100
,其他动态库以此类推,这时候我们可以删除软连接并且将
libavcodec.so.58.18.100
重命名为
libavcodec.so
,与
include
中的头文件一同拷贝到android工程中使用,其他动态库以此类推。
上面的这种操作其实也有不方便的地方,就是每次都要重命名文件,然后拷贝,能不能直接生成我们想要的动态库呢?替换原有配置
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_MAJOR='$(SLIBPREF)$(FULLNAME)-$(LIBMAJOR)$(SLIBSUF)'
LIB_INSTALL_EXTRA_CMD='$$(RANLIB)"$(LIBDIR)/$(LIBNAME)"'
SLIB_INSTALL_NAME='$(SLIBNAME)'
SLIB_INSTALL_LINKS='$(SLIBNAME_WITH_MAJOR)'
然后重新执行configure
、make
、make install
同样,复制我们的libXXX.so文件和
include
目录下的头文件到android工程,就可以在android上开始我们ffmpeg开发之旅了。
编译遇到错误
在这个ffmpeg版本编译的时候可能会遇到几处错误
错误1
libavcodec/aaccoder.c: In function 'search_for_ms':
libavcodec/aaccoder.c:803:25: error: expected identifier or '(' before numeric constant
int B0 = 0, B1 = 0;
^
libavcodec/aaccoder.c:865:28: error: lvalue required as left operand of assignment
B0 += b1+b2;
^
libavcodec/aaccoder.c:866:25: error: 'B1' undeclared (first use in this function)
B1 += b3+b4;
^
libavcodec/aaccoder.c:866:25: note: each undeclared identifier is reported only once for each function it appears in
make: *** [libavcodec/aaccoder.o] Error 1
解决:将libavcodec/aaccoder.c文件B0变量替换成b0
错误2
libavcodec/hevc_mvs.c: In function 'derive_spatial_merge_candidates':
libavcodec/hevc_mvs.c:208:15: error: 'y0000000' undeclared (first use in this function)
((y ## v) >> s->ps.sps->log2_min_pu_size))
^
libavcodec/hevc_mvs.c:204:14: note: in definition of macro 'TAB_MVF'
tab_mvf[(y) * min_pu_width + x]
^
libavcodec/hevc_mvs.c:274:16: note: in expansion of macro 'TAB_MVF_PU'
(cand && !(TAB_MVF_PU(v).pred_flag == PF_INTRA))
^
libavcodec/hevc_mvs.c:368:23: note: in expansion of macro 'AVAILABLE'
is_available_b0 = AVAILABLE(cand_up_right, B0) &&
^
libavcodec/hevc_mvs.c:208:15: note: each undeclared identifier is reported only once for each function it appears in
((y ## v) >> s->ps.sps->log2_min_pu_size))
^
libavcodec/hevc_mvs.c:204:14: note: in definition of macro 'TAB_MVF'
tab_mvf[(y) * min_pu_width + x]
^
libavcodec/hevc_mvs.c:274:16: note: in expansion of macro 'TAB_MVF_PU'
(cand && !(TAB_MVF_PU(v).pred_flag == PF_INTRA))
^
libavcodec/hevc_mvs.c:368:23: note: in expansion of macro 'AVAILABLE'
is_available_b0 = AVAILABLE(cand_up_right, B0) &&
^
libavcodec/hevc_mvs.c:207:15: error: 'x0000000' undeclared (first use in this function)
TAB_MVF(((x ## v) >> s->ps.sps->log2_min_pu_size), \
^
libavcodec/hevc_mvs.c:204:34: note: in definition of macro 'TAB_MVF'
tab_mvf[(y) * min_pu_width + x]
^
libavcodec/hevc_mvs.c:274:16: note: in expansion of macro 'TAB_MVF_PU'
(cand && !(TAB_MVF_PU(v).pred_flag == PF_INTRA))
^
libavcodec/hevc_mvs.c:368:23: note: in expansion of macro 'AVAILABLE'
is_available_b0 = AVAILABLE(cand_up_right, B0) &&
^
libavcodec/hevc_mvs.c: In function 'ff_hevc_luma_mv_mvp_mode':
libavcodec/hevc_mvs.c:208:15: error: 'y0000000' undeclared (first use in this function)
((y ## v) >> s->ps.sps->log2_min_pu_size))
^
libavcodec/hevc_mvs.c:204:14: note: in definition of macro 'TAB_MVF'
tab_mvf[(y) * min_pu_width + x]
^
libavcodec/hevc_mvs.c:274:16: note: in expansion of macro 'TAB_MVF_PU'
(cand && !(TAB_MVF_PU(v).pred_flag == PF_INTRA))
^
libavcodec/hevc_mvs.c:683:24: note: in expansion of macro 'AVAILABLE'
is_available_b0 = AVAILABLE(cand_up_right, B0) &&
^
libavcodec/hevc_mvs.c:207:15: error: 'x0000000' undeclared (first use in this function)
TAB_MVF(((x ## v) >> s->ps.sps->log2_min_pu_size), \
^
libavcodec/hevc_mvs.c:204:34: note: in definition of macro 'TAB_MVF'
tab_mvf[(y) * min_pu_width + x]
^
libavcodec/hevc_mvs.c:274:16: note: in expansion of macro 'TAB_MVF_PU'
(cand && !(TAB_MVF_PU(v).pred_flag == PF_INTRA))
^
libavcodec/hevc_mvs.c:683:24: note: in expansion of macro 'AVAILABLE'
is_available_b0 = AVAILABLE(cand_up_right, B0) &&
^
make: *** [libavcodec/hevc_mvs.o] Error 1
解决:将libavcodec/hevc_mvs.c文件的变量B0改成b0,xB0改成xb0,yB0改成yb0
错误3
libavcodec/opus_pvq.c: In function 'quant_band_template':
libavcodec/opus_pvq.c:498:9: error: expected identifier or '(' before numeric constant
int B0 = blocks;
^
libavcodec/opus_pvq.c:559:12: error: lvalue required as left operand of assignment
B0 = blocks;
^
make: *** [libavcodec/opus_pvq.o] Error 1
解决:将libavcodec/opus_pvq.c文件的变量B0改成b0
打包成ffmpeg.so
上面我们编译出了libXXX.a或者libXXX.so库,当然直接拷贝到android工程引用是一种方式,还有一种就是将静态库打成一个动态库,这样android程序启动的时候只要加载一个动态库即可,理论上效率更高。
同样的,先加载环境变量、打包、压缩优化
# 先重置环境变量
source ffmpeg-toolchain-env.sh
# 执行打包命令
arm-linux-androideabi-ld -rpath-link=$SYSROOT/usr/lib -L$SYSROOT/usr/lib -L${OUT_PUT}/ffmpeg/${ANDROID_ARCH}/lib -soname libffmpeg.so -shared -nostdlib -Bsymbolic --whole-archive --no-undefined -o ${OUT_PUT}/ffmpeg/${ANDROID_ARCH}/libffmpeg.so libavcodec.a libavfilter.a libavformat.a libavutil.a libswresample.a libswscale.a -lc -lm -lz -ldl -llog --dynamic-linker=/system/bin/linker $CHAIN_ENV/${ANDROID_ARCH}/lib/gcc/arm-linux-androideabi/4.9.x/libgcc.a
会生成一个ffmpeg.so
最后对包的大小进行优化压缩
arm-linux-androideabi-strip ${OUT_PUT}/ffmpeg/${ANDROID_ARCH}/libffmpeg.so
这样,我们的ffmpeg.so动态库就生成了,拷贝到android工程中使用吧,当然也别忘记了拷贝头文件哦~~~
遇到问题
使用android-ndk-r17b这个版本的ndk,可能会遇到以下错误
libavfilter/vf_psnr.c:387: error: undefined reference to 'stdout'
libavfilter/vf_psnr.c:258: error: undefined reference to 'stdout'
libavfilter/vf_ssim.c:478: error: undefined reference to 'stdout'
libavfilter/vf_ssim.c:356: error: undefined reference to 'stdout'
libavutil/log.c:138: error: undefined reference to 'stderr'
libavutil/log.c:217: error: undefined reference to 'stderr'
libavutil/log.c:217: error: undefined reference to 'stderr'
这时候切换到android-ndk-r14b这个版本,重新制作工具链,重新configure
、make
、make install
后重新打包,压缩优化,即可生成
总结
上面我们讲解了通过android的arm平台架构的自定义交叉编译链来编译ffmpeg的动态或者静态库,不知道对你是否有用处,后面开始会陆续基于ffmpeg进行android上播放器的开发,欢迎留言交流~~