從事多媒體軟件開發的人幾乎沒有不知道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 等價。