从事多媒体软件开发的人几乎没有不知道FFmpeg的,很多视频播放器都是基于FFmpeg开发的。如今最火的智能手机操作系统Android上的很多第三方视频播放器也是基于FFmpeg实现全格式支持。由于Android通常跑在ARM处理器上,而且Android使用了自己的libc库(即bionic),因此要在Android上编译和使用FFmpeg需要做一些移植工作,好在FFmpeg本身用C写成,很好地支持跨平台移植,实现这个目的并不难,事实上已经有很多前辈做过这方面的工作并公开了他们的成果。
介绍如何使用 Android NDK(r7) 设置 Android 本地代码编译工具链,如何根据 Makefile 编写 Android.mk,并以 ffmpeg(0.8.5) 为例子介绍如何使用此工具链移植。使用编译出来的库文件,可以通过本地 C/C++ 程序调用 ffmpeg 解码库;也可以另外编写 JNI 接口,使用 Java 程序调用 ffmepg。
我们都知道编译软件的一般步骤为:
./configure
make
make install
当然还可以增加参数做些自定义,但大概的流程是这样。要移植一个已有的库到 Android 当中却有很大的不同,首先需要搭建一个交叉编译环境去运行 configure 脚本以便生成配置文件,然后还需要编写 Android.mk 才能编译。
拿 ffmpeg 作例子,运行 configure 会生成 config.mak、config.h 和 libavutil/avconfig.h 这几个文件,里面决定了 ffmpeg 编译哪些模块、是否开启某些特性等。当然如果足够熟悉的话也可以手动修改这几个文件,但是其中的依赖关系复杂,较容易出错。接着根据原来的 Makefile 手动编写 Android.mk 文件,就能编译了。以下是详细流程。
注意:不能直接在宿主系统上运行 configure 脚本,因为环境和目标系统(Android)是不同的,这需要建立交叉编译环境。
1. 设定编译工具链
这一步没有实质作用,只是为了说明下一步。有两种等价的方法,手动指定工具链或者使用 NDK 自动生成工具链,详细文档在 NDK 目录下的 docs/STANDALONE-TOOLCHAIN.html。
手动指定工具链
就是手动指定交叉编译工具链的位置,其中 $NDK 为 NDK 所在目录。此方法是比较麻烦的一种方法,以下是手动指定 gcc,并编译 foo.c:
SYSROOT=$NDK/platforms/android-8/arch-arm #可更改 API 版本,8 对应 Android 2.2
export CC=“$NDK/toolchains/arm-linux-androideabi-4.4.3/prebuilt/linux-x86/bin/arm-linux-androideabi-gcc --sysroot=$SYSROOT“
$CC -o foo.o -c foo.c
2. 设定编译参数
运行 configure 脚本的时候有很多选项,根据自己的需要以及目标系统进行自定义,运行 ./configure --help 了解所有选项。重点需要了解的有:
需要的模块与功能:ffmpeg 有很多组件,根据需要裁减。比如说 codec 有很多,如果只需要其中几个的话可以把不需要的屏蔽,减小代码体积。-> 查看 ffmpeg/doc 目录下的帮助,了解每个模块的作用。
根据目标 CPU 开启某些指令集:需要了解目标 CPU 架构,一般是 ARM11 或 Cortex-A8;以及是否支持 VFP、NEON 这些扩展指令。-> 查阅 CPU 供应商提供的芯片资料。
指定交叉编译工具链:设置交叉编译工具位置,设置一些必要的 cflags、ldflags。-> 查看 GNU Make 帮助,了解必要选项。了解 Android NDK,了解其支持什么指令集、提供哪些库。
我现在的目标 CPU 是高通的 8255,架构是 Cortex-A8 支持 VFPv3 以及 NEON 指令集;目标系统是 Android 2.3。希望编译一个支持文件解析与解码的库,不需要其他组件。以下是符合我的需求的配置(手动指定工具链):
NDK=你的NDK所在目录
SYSROOT=$NDK/platforms/android-9/arch-arm
PREBUILT=$NDK/toolchains/arm-linux-androideabi-4.4.3/prebuilt/linux-x86
./configure --disable-ffmpeg --disable-ffplay --disable-ffserver \ # 屏蔽与解码无关组件,运行时请删除行末注释
--disable-ffprobe --disable-swscale --disable-postproc \
--disable-bsfs --disable-filters \
--disable-avdevice --disable-network --disable-devices \
--disable-encoders --disable-muxers \ # 屏蔽编码相关组件
--disable-protocols --enable-protocol=file \ # 只保留本地文件协议
--enable-cross-compile --target-os=linux \
--arch=arm --cpu=armv7-a \
--enable-shared \ # 直接用 make 编译时加上
--sysroot=$SYSROOT \
--cc=$PREBUILT/bin/arm-linux-androideabi-gcc \
--enable-memalign-hack \
--extra-cflags=“-march=armv7-a -mfloat-abi=softfp -mfpu=neon“ # CPU 特性
运行成功的话会显示详细的报告,说明开启了那些功能与选项,检查这些选项看看与自己设定的是否一致。检查 config.h 或 config.mak 也可以确认所有选项的取值情况,检查 config.log 进一步了解有些选项为什么检查不通过。其中对性能有重大影响的是 CPU 特性,因为有很多算法都用到了汇编语言优化,检查以下 config.mak 变量:
HAVE_ARMV5TE=yes
HAVE_ARMV6=yes
HAVE_ARMV6T2=yes
HAVE_ARMVFP=yes
HAVE_NEON=yes
HAVE_VFPV3=yes
注意:configure 脚本中检测某项目标平台特性是通过调用编译器编译某些源码实现的,例如检测编译器是否支持 NEON 指令集就是检测是否能够成功编译汇编指令 "vadd.i16 q0, q0, q0"。
注意:如果 CPU 是 ARM11 系列(例如高通 MSM7227),则上面配置相应改为 “--arch=arm --cpu=armv6 \”,--extra-cflags 部分可以去掉。
NEON 指令集加速效果还是很明显的,因为 ffmpeg 里面不少算法都使用对此进行了优化,可以充分发挥 NEON 单指令多数据(SIMD)的特性。例如在 1.0GHz 的高通 MSM8255 上播放级别为 insane 的 APE 音乐,CPU 占有率从 80% 以上下降到 30% 左右;AVC 解码时 CPU 占有率也稍微下降了一些。
3. 编译库文件
注意:其实也可以根本不写 Android.mk 而直接使用原有的 Makefile,只要设置时有 ”--enable-shared“ 就可以运行完 configure 后直接 make 编译。如果想使用 Android.mk 的话请继续看下去,否则本文已完结。
编写 Android.mk
以 libavcodec 模块为例子,我们打开 libavcodec/Makefile 看看,重点需要参考的是 OBJS 开头的语句,摘抄如下:
# parts needed for many different codecs
OBJS-$(CONFIG_AANDCT) += aandcttab.o # 意思是如果 config.mak 中 CONFIG_AANDCT = yes,则添加到变量 OBJS
OBJS-$(CONFIG_AC3DSP) += ac3dsp.o
OBJS-$(CONFIG_CRYSTALHD) += crystalhd.o
OBJS-$(CONFIG_ENCODERS) += faandct.o jfdctfst.o jfdctint.o
OBJS-$(CONFIG_DCT) += dct.o dct32_fixed.o dct32_float.o
OBJS-$(CONFIG_DWT) += dwt.o
OBJS-$(CONFIG_DXVA2) += dxva2.o
这是根据上一步生成的 config.mak 决定哪些文件将会被编译,非常重要,我们需要将其添加到 Android.mk 当中。新建一个 libavcodec/Android.mk,复制 libavcodec/Makefile 有关 OBJS 的语句过来,把其中所有的 *.o 替换为 *.c,也就是指定需要编译的源代码文件。
#libavcodec/Android.mk
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
include $(LOCAL_PATH)/../config.mak
OBJS = allcodecs.c \
audioconvert.c \
avpacket.c \
bitstream.c \
bitstream_filter.c \
dsputil.c \
faanidct.c \
fmtconvert.c \
imgconvert.c \
jrevdct.c \
options.c \
parser.c \
raw.c \
rawdec.c \
resample.c \
resample2.c \
simple_idct.c \
apedec.c \
utils.c \
# parts needed for many different codecs
OBJS-$(CONFIG_AANDCT) += aandcttab.c
OBJS-$(CONFIG_AC3DSP) += ac3dsp.c
OBJS-$(CONFIG_CRYSTALHD) += crystalhd.c
OBJS-$(CONFIG_ENCODERS) += faandct.c jfdctfst.c jfdctint.c
OBJS-$(CONFIG_DCT) += dct.c dct32_fixed.c dct32_float.c
OBJS-$(CONFIG_DWT) += dwt.c
OBJS-$(CONFIG_DXVA2) += dxva2.c
### 中间省略类似的
include $(LOCAL_PATH)/arm/Android.mk # 添加 ARM 相关源文件,下面会讲到
LOCAL_CFLAGS := $(CFLAGS) # 添加 config.mak 中的编译选项
LOCAL_CPPFLAGS := $(CPPFLAGS)
LOCAL_SRC_FILES = $(sort $(OBJS) $(OBJS-yes)) # 添加所需源文件,使用 sort 防止多次添加
LOCAL_MODULE := libavcodec
LOCAL_MODULE_TAGS := optional
LOCAL_PRELINK_MODULE := false
LOCAL_SHARED_LIBRARIES += libavutil libz # 依赖 libavutil.so
LOCAL_C_INCLUDES := $(LOCAL_PATH) \ # 头文件位置
$(LOCAL_PATH)/arm \
$(LOCAL_PATH)/../ \
external/zlib \
include $(BUILD_SHARED_LIBRARY) # 编译成动态库
另外 libavcodec 当中有很多汇编优化的文件,例如我们目标平台是 arm,需要加入 libavcodec/arm 目录下的源文件。同样是根据 libavcodec/arm/Makefile 写成的,摘录如下:
#libavcodec/arm/Android.mk
OBJS-$(CONFIG_AC3DSP) += arm/ac3dsp_init_arm.c \
arm/ac3dsp_arm.c
OBJS-$(CONFIG_DCA_DECODER) += arm/dcadsp_init_arm.c \
# 中间省略,根据 ffmpeg 版本不同,文件可能有不同。。。
OBJS-$(HAVE_NEON) += arm/dsputil_init_neon.c \
arm/dsputil_neon.c \
arm/fmtconvert_neon.c \
arm/int_neon.c \
arm/mpegvideo_neon.c \
arm/simple_idct_neon.c \
$(NEON-OBJS-yes)
在每个需要编译的目录下都这样写 Android.mk。最后还要在编译的顶层目录写一个调用子目录的 Android.mk 文件(下一节介绍)。这里我们只编译其中的三个解码必须模块,libavutil libavcodec libavformat。其中 libavutil 依赖 libz,libavcodec 依赖 libavutil,libavformat 依赖前两者。
编译
使用 ndk-build 命令可编译,但要注意目录布局,假设当前目录是 $PROJECT,必须把 ffmpeg 源码目录以及顶层 Android.mk 放在 $PROJECT/jni 目录下,然后在 $PROJECT 目录运行 ndk-build。
#$PROJECT/jni/Android.mk,与 ffmpeg 目录同级
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
include $(LOCAL_PATH)/ffmpeg/config.mak
LOCAL_SHARED_LIBRARIES := libz
include $(LOCAL_PATH)/ffmpeg/libavutil/Android.mk \
$(LOCAL_PATH)/ffmpeg/libavcodec/Android.mk \
$(LOCAL_PATH)/ffmpeg/libavformat/Android.mk
另外可以使用 Android 源码提供的编译命令 mmm 编译,需要把顶层 Android.mk 放在 ffmpeg 目录下并修改里面的文件路径。我使用的是此方法,编译出来的库使用自制播放器可以正常解码,应该与 ndk-build 等价。