使用 NDK 编译 FFmpeg4.1

编译环境

操作系统: Mac OS 10.13.6
FFMpeg 版本: 4.1
NDK 版本: android-ndk-r18b
编译器: clang

构建 ndk 编译链

执行 ndk 自带的脚本构建编译链,会生成编译环境

1
$NDK/build/tools/make_standalone_toolchain.py --arch arm --api 21 --install-dir /tmp/my-android-toolchain

构建 ffmpeg 编译环境

修改 ffmpeg 项目根目录下 configure 文件将这几个环境变量替换成如下

1
2
3
4
SLIBNAME_WITH_MAJOR='$(SLIBPREF)$(FULLNAME)-$(LIBMAJOR)$(SLIBSUF)'
LIB_INSTALL_EXTRA_CMD='$$(RANLIB)"$(LIBDIR)/$(LIBNAME)"'
SLIB_INSTALL_NAME='$(SLIBNAME)'
SLIB_INSTALL_LINKS='$(SLIBNAME_WITH_MAJOR)'

准备 ffmpeg-toolchain-env.sh 脚本,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#!/bin/sh
# Android cross-compile environment setup script
# Author : binea
# Date : 2019-01-14
# Version : 1.0
export HOME=根目录
# Android NDK sources and standalone toolchain is put here
export DEV=${HOME}/dev/android/sdk/adt-bundle-mac-x86_64-20140702/sdk
export NDK=${DEV}/android-ndk-r18b
export CHAIN_ENV=${NDK}/android-21

# All the built binaries, libs and their header will be installed here
# export PREFIX=${HOME}/Chain
export PREFIX=$(pwd)/android/$CPU

# 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
export CLANGXX=${CROSS_PREFIX}-clang++

# 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 -android"
export LIBS="-lgcc"
export LDFLAGS="-Wl,-rpath-link=-I${SYSROOT}/usr/lib -L${SYSROOT}/usr/lib -L${PREFIX}/lib -L${NDK_TOOLCHAIN}/lib"

这个脚本应该是通用脚本,网上很多,但是有些脚本提供是错的。-android 写成了 -mandroid 这里需要注意

执行以下命令

1
2
3
4
5
./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

然后 ffmpeg 就开始编译了。编译成功后会生成如下文件件

ffmpeg_output

编译过程中可能遇到的错误

错误 1

1
2
3
4
5
6
7
8
9
10
11
12
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
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

1
2
3
4
5
6
7
8
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 程序启动的时候只要加载一个动态库即可,理论上效率更高。同样的,先加载环境变量、打包、压缩优化。

1
2
3
4
# 先重置环境变量
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

对 so 文件进行裁剪压缩以下

1
arm-linux-androideabi-strip  ${OUT_PUT}/ffmpeg/${ANDROID_ARCH}/libffmpeg.so

这样打包出来的 so 库大小

至此 ffmpg 的编译已经完成了。

Android Studio 使用 ffmpeg.so

main 下面建立 cpp 或者 jni 然后将 libffmpeg.so 拷到如下目录

cpp 或者 jni 根目录下创建 CMakeLists.txt 文件,在 build.gradle 文件中加入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
defaultConfig {
externalNativeBuild {
cmake {
arguments '-DANDROID_STL=c++_static'
}
}

ndk {
abiFilters "armeabi-v7a"
}
}

sourceSets {
main {
jniLibs.srcDirs = ['src/main/cpp/ffmpeg/lib']
}
}

CMakeFileLists 脚本中加入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
cmake_minimum_required(VERSION 3.4.1)

set(CURRENT_DIR ${CMAKE_SOURCE_DIR})

message("CURRENT_DIR:" ${CMAKE_SOURCE_DIR})

add_library(player SHARED ${CMAKE_SOURCE_DIR}/ffmpeg/player.c)

add_library(myffmpeg SHARED IMPORTED)
set_target_properties(myffmpeg PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/ffmpeg/lib/armeabi-v7a/libffmpeg.so)

include_directories(${CMAKE_SOURCE_DIR}/ffmpeg/include)

# Include libraries needed for player lib
target_link_libraries(player myffmpeg android log)

在 kotlin 中载入动态库

1
2
System.loadLibrary("ffmpeg")
System.loadLibrary("player")

然后 build, 这个时候去看 apk

至此 ffmpeg.so 成功打包到了 apk当中。

打包的过程中可能会报找不到 so 文件,这个时候需要检查一下 so 文件与 eabi 是否与 gradle 的配置一致。so 文件的目录不能单纯的使用 armeabi,这样会导致 so 无法打包到 apk