前言
NDK
全称 Native Development Kit
,也就是原生开发工具包 ,官网对它有详细的 中文介绍 。可能一说到 NDK
或 JNI
,大家脑子里第一反应就是集成 C/C
。其实 JNI
的含义是 Java Native Interface
,这种接口允许 Java
和其他语言进行交互的,包括但不限于 C/C
。目前 Rust
也可以通过 JNI
来和 Java
交互,虽然不太成熟。
其实 NDK
更像一个桥梁,来连通 Java
和其他语言,它是一系列工具的集合。既然作为工具, NDK
并非必须在 Android
项目中才能用。本文我们来通过 NDK
对 FFmpeg 5.0
进行编译,生成动态链接库 so
。
注:本文的 Java
泛指 JVM
语言,不要拿 Kotlin
抬杠,本质太大的区别 。本文测试项目源码地址【TolyFFmpeg】
一、环境准备
想要编译 FFmpeg
应用 Android
中的动态链接库,我们要准备两个东西:一者是 FFmpeg
的源码;二者是 NDK
的工具包。这两者都可以通过简单的下载获得。
1、FFmpeg 源码下载:5.0.1
作为一个开源项目,想得到源码还是非常简单的。可以在官网直接下载源码,也可以通过 git
来下载,或者点击More releases
来选择某个版本进行下载。 https://ffmpeg.org/download.html
源码解压如下,里面的 doc
文件夹有些文档和案例,还是比较有用的。其余的东西暂时对我们来说并没有什么太大的意义,现在我们的目的是通过这个源码通过 NDK
来编译成在 Android
中可以使用的动态链接库 so
文件。
可能会有人疑惑,那就是 so
库嘛,下载别人的用不就完事了吗?原因很简单,自己编译 FFmpeg
可以手动设置需要的功能,如果直接别人编译好的,就没有设置的机会。而且自己编译也能掌握版本,也就是说,自己动手丰衣足食。
2、下载 NDK :r24
可以在如下网站中下载 NDK
的工具包,不过在 macOS
中更推荐用 Android SDK
管理器来下载,如下在 AndroidStudio
中选择 NDK
点击 OK
下载即可。这里下载的是最新版 r24 (24.0.8215888)
。
https://developer.android.google.cn/ndk/downloads/index.html
下载过后 你的AndroidSDK/ndk/24.0.8215888
会议相关文件,说明 NDK
环境准备就绪。
二、编译 FFmpeg
编译 FFmpeg
,只要是使用 ndk
中的编译根据,在 $ndkPath/toolchains/llvm/prebuilt/
下,不同平台的文件名不同,比如 macOS
中是 darwin-x86_64
。
1.编译脚本
编译脚本参考: 《使用Android Studio开发FFmpeg的正确姿势》
亲测该脚本在 r24
5.0.1
是可用的,使用时注意 tag1
和 tag2
处。
#!/bin/bash
# 用于编译android平台的脚本
# NDK所在目录
NDK_PATH=/Users/mac/Coder/SDK/AndroidSDK/ndk/24.0.8215888/ # tag1
# macOS 平台编译,其他平台看一下 $NDK_PATH/toolchains/llvm/prebuilt/ 下的文件夹名称
HOST_PLATFORM=darwin-x86_64 #tag1
# minSdkVersion
API=23
TOOLCHAINS="$NDK_PATH/toolchains/llvm/prebuilt/$HOST_PLATFORM"
SYSROOT="$NDK_PATH/toolchains/llvm/prebuilt/$HOST_PLATFORM/sysroot"
# 生成 -fpic 与位置无关的代码
CFLAG="-D__ANDROID_API__=$API -Os -fPIC -DANDROID "
LDFLAG="-lc -lm -ldl -llog "
# 输出目录
PREFIX=`pwd`/android-build
# 日志输出目录
CONFIG_LOG_PATH=${PREFIX}/log
# 公共配置
COMMON_OPTIONS=
# 交叉配置
CONFIGURATION=
build() {
APP_ABI=$1
echo "======== > Start build $APP_ABI"
case ${APP_ABI} in
armeabi-v7a)
ARCH="arm"
CPU="armv7-a"
MARCH="armv7-a"
TARGET=armv7a-linux-androideabi
CC="$TOOLCHAINS/bin/$TARGET$API-clang"
CXX="$TOOLCHAINS/bin/$TARGET$API-clang "
LD="$TOOLCHAINS/bin/$TARGET$API-clang"
# 交叉编译工具前缀
CROSS_PREFIX="$TOOLCHAINS/bin/arm-linux-androideabi-"
EXTRA_CFLAGS="$CFLAG -mfloat-abi=softfp -mfpu=vfp -marm -march=$MARCH "
EXTRA_LDFLAGS="$LDFLAG"
EXTRA_OPTIONS="--enable-neon --cpu=$CPU "
;;
arm64-v8a)
ARCH="aarch64"
TARGET=$ARCH-linux-android
CC="$TOOLCHAINS/bin/$TARGET$API-clang"
CXX="$TOOLCHAINS/bin/$TARGET$API-clang "
LD="$TOOLCHAINS/bin/$TARGET$API-clang"
CROSS_PREFIX="$TOOLCHAINS/bin/$TARGET-"
EXTRA_CFLAGS="$CFLAG"
EXTRA_LDFLAGS="$LDFLAG"
EXTRA_OPTIONS=""
;;
x86)
ARCH="x86"
CPU="i686"
MARCH="i686"
TARGET=i686-linux-android
CC="$TOOLCHAINS/bin/$TARGET$API-clang"
CXX="$TOOLCHAINS/bin/$TARGET$API-clang "
LD="$TOOLCHAINS/bin/$TARGET$API-clang"
CROSS_PREFIX="$TOOLCHAINS/bin/$TARGET-"
#EXTRA_CFLAGS="$CFLAG -march=$MARCH -mtune=intel -mssse3 -mfpmath=sse -m32"
EXTRA_CFLAGS="$CFLAG -march=$MARCH -mssse3 -mfpmath=sse -m32"
EXTRA_LDFLAGS="$LDFLAG"
EXTRA_OPTIONS="--cpu=$CPU "
;;
x86_64)
ARCH="x86_64"
CPU="x86-64"
MARCH="x86_64"
TARGET=$ARCH-linux-android
CC="$TOOLCHAINS/bin/$TARGET$API-clang"
CXX="$TOOLCHAINS/bin/$TARGET$API-clang "
LD="$TOOLCHAINS/bin/$TARGET$API-clang"
CROSS_PREFIX="$TOOLCHAINS/bin/$TARGET-"
#EXTRA_CFLAGS="$CFLAG -march=$CPU -mtune=intel -msse4.2 -mpopcnt -m64"
EXTRA_CFLAGS="$CFLAG -march=$CPU -msse4.2 -mpopcnt -m64"
EXTRA_LDFLAGS="$LDFLAG"
EXTRA_OPTIONS="--cpu=$CPU "
;;
esac
echo "-------- > Start clean workspace"
make clean
echo "-------- > Start build configuration"
CONFIGURATION="$COMMON_OPTIONS"
CONFIGURATION="$CONFIGURATION --logfile=$CONFIG_LOG_PATH/config_$APP_ABI.log"
CONFIGURATION="$CONFIGURATION --prefix=$PREFIX"
CONFIGURATION="$CONFIGURATION --libdir=$PREFIX/libs/$APP_ABI"
CONFIGURATION="$CONFIGURATION --incdir=$PREFIX/includes/$APP_ABI"
CONFIGURATION="$CONFIGURATION --pkgconfigdir=$PREFIX/pkgconfig/$APP_ABI"
CONFIGURATION="$CONFIGURATION --cross-prefix=$CROSS_PREFIX"
CONFIGURATION="$CONFIGURATION --arch=$ARCH"
CONFIGURATION="$CONFIGURATION --sysroot=$SYSROOT"
CONFIGURATION="$CONFIGURATION --cc=$CC"
CONFIGURATION="$CONFIGURATION --cxx=$CXX"
CONFIGURATION="$CONFIGURATION --ld=$LD"
# nm 和 strip
CONFIGURATION="$CONFIGURATION --nm=$TOOLCHAINS/bin/llvm-nm"
CONFIGURATION="$CONFIGURATION --strip=$TOOLCHAINS/bin/llvm-strip"
CONFIGURATION="$CONFIGURATION $EXTRA_OPTIONS"
echo "-------- > Start config makefile with $CONFIGURATION --extra-cflags=${EXTRA_CFLAGS} --extra-ldflags=${EXTRA_LDFLAGS}"
./configure ${CONFIGURATION}
--extra-cflags="$EXTRA_CFLAGS"
--extra-ldflags="$EXTRA_LDFLAGS"
echo "-------- > Start make $APP_ABI with -j1"
make -j1
echo "-------- > Start install $APP_ABI"
make install
echo " > make and install $APP_ABI complete."
}
build_all() {
#配置开源协议声明
COMMON_OPTIONS="$COMMON_OPTIONS --enable-gpl"
#目标android平台
COMMON_OPTIONS="$COMMON_OPTIONS --target-os=android"
#取消默认的静态库
COMMON_OPTIONS="$COMMON_OPTIONS --disable-static"
COMMON_OPTIONS="$COMMON_OPTIONS --enable-shared"
COMMON_OPTIONS="$COMMON_OPTIONS --enable-protocols"
#开启交叉编译
COMMON_OPTIONS="$COMMON_OPTIONS --enable-cross-compile"
COMMON_OPTIONS="$COMMON_OPTIONS --enable-optimizations"
COMMON_OPTIONS="$COMMON_OPTIONS --disable-debug"
#尽可能小
COMMON_OPTIONS="$COMMON_OPTIONS --enable-small"
COMMON_OPTIONS="$COMMON_OPTIONS --disable-doc"
#不要命令(执行文件)
COMMON_OPTIONS="$COMMON_OPTIONS --disable-programs" # do not build command line programs
COMMON_OPTIONS="$COMMON_OPTIONS --disable-ffmpeg" # disable ffmpeg build
COMMON_OPTIONS="$COMMON_OPTIONS --disable-ffplay" # disable ffplay build
COMMON_OPTIONS="$COMMON_OPTIONS --disable-ffprobe" # disable ffprobe build
COMMON_OPTIONS="$COMMON_OPTIONS --disable-symver"
COMMON_OPTIONS="$COMMON_OPTIONS --disable-network"
COMMON_OPTIONS="$COMMON_OPTIONS --disable-x86asm"
COMMON_OPTIONS="$COMMON_OPTIONS --disable-asm"
#启用
COMMON_OPTIONS="$COMMON_OPTIONS --enable-pthreads"
COMMON_OPTIONS="$COMMON_OPTIONS --enable-mediacodec"
COMMON_OPTIONS="$COMMON_OPTIONS --enable-jni"
COMMON_OPTIONS="$COMMON_OPTIONS --enable-zlib"
COMMON_OPTIONS="$COMMON_OPTIONS --enable-pic"
COMMON_OPTIONS="$COMMON_OPTIONS --enable-muxer=flv"
#COMMON_OPTIONS="$COMMON_OPTIONS --enable-avresample"
COMMON_OPTIONS="$COMMON_OPTIONS --enable-decoder=h264"
COMMON_OPTIONS="$COMMON_OPTIONS --enable-decoder=mpeg4"
COMMON_OPTIONS="$COMMON_OPTIONS --enable-decoder=mjpeg"
COMMON_OPTIONS="$COMMON_OPTIONS --enable-decoder=png"
COMMON_OPTIONS="$COMMON_OPTIONS --enable-decoder=vorbis"
COMMON_OPTIONS="$COMMON_OPTIONS --enable-decoder=opus"
COMMON_OPTIONS="$COMMON_OPTIONS --enable-decoder=flac"
echo "COMMON_OPTIONS=$COMMON_OPTIONS"
echo "PREFIX=$PREFIX"
echo "CONFIG_LOG_PATH=$CONFIG_LOG_PATH"
mkdir -p ${CONFIG_LOG_PATH}
build "armeabi-v7a"
build "arm64-v8a"
build "x86"
build "x86_64"
}
echo "-------- Start --------"
build_all
echo "-------- End --------"
2. 使用脚本
把上面的脚本写在 build_android.sh
中,放在 ffmpeg
源码根目录下。
进入到 ffmpeg
源码目录,执行如下命令,等待即可。
chmod x build_android.sh
./build_android.sh
如下在当前文件夹下会生成 android-build
文件夹,其中 libs
文件夹中盛放着各种架构的 so
库,includes
文件夹中盛放着各种架构的头文件。如果不想编译处某种架构的,在 build_android.sh
的末尾处注释即可。
mac@macdeMacBook-Pro android-build % tree -L 2
.
├── includes
│ ├── arm64-v8a
│ ├── armeabi-v7a
│ ├── x86
│ └── x86_64
├── libs
│ ├── arm64-v8a
│ ├── armeabi-v7a
│ ├── x86
│ └── x86_64
└── share
└── ffmpeg
从这里可以看出,把 FFmpeg
源码编译成 so
动态链接库,是 NDK
的功劳。其实在 Android
开发中,NDK
的作用也是如此,核心价值也是把其他语言编译成Android
平台可以访问的 so
而已。所以也不要觉得 NDK
有多么神秘,就是一个工具集而已。
三、Android 中集成 FFmpeg
在 AndroidStudio
中选择创建一个 Native C
的项目。其实这也不是必须的,普通项目也可以通过配置来支持 C
。
1. app 下的 build.gradle 修改建议
最好在 app/build.gradle
中指定 NDK
的版本,否则可能会下载其他版本的 NDK
而浪费时间。
android {
ndkVersion "24.0.8215888"
sourceSets {
main {
jniLibs.srcDirs = ['jniLibs']
}
}
//...
}
另外添加 jniLibs.srcDirs
的指向为了解决下面的异常,而且 jniLibs.srcDirs
指向什么目录都无所谓,但不加引入 so
时就会报错。官网说是 jniLibs
已经默认成为了目录,不需要指定 jniLibs.srcDirs
,但这里感觉莫名其妙,必须要指一下。
2. 项目结构
在 cpp
文件夹中处理 c
相关内容,jniLibs
文件夹放入文件编译的 so
库:
3. CMakeLists.txt 书写
CMakeLists
是构建的脚本,这里先使用 avcodec
打印一下配置信息,不过 ffmpeg 5.0
好像 avcodec
依赖了 swresample
和 avutil
模块。这里也需要添加一些,记得 4.2.7
的时候还不需要。
cmake_minimum_required(VERSION 3.18.1)
project("tolyffmpeg")
#引入头文件
include_directories(includes)
# 定义当前 so 库 - 在 java 代码中加载
add_library(tolyffmpeg SHARED native-lib.cpp)
# 添加 ffmpeg 的 avcodec、swresample、avutil 模块 start======
set(distribution_DIR ${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI})
add_library(avcodec SHARED IMPORTED)
set_target_properties(avcodec PROPERTIES IMPORTED_LOCATION ${distribution_DIR}/libavcodec.so)
add_library(swresample SHARED IMPORTED)
set_target_properties(swresample PROPERTIES IMPORTED_LOCATION ${distribution_DIR}/libswresample.so)
add_library(avutil SHARED IMPORTED)
set_target_properties(avutil PROPERTIES IMPORTED_LOCATION ${distribution_DIR}/libavutil.so)
# 添加 ffmpeg 的 avcodec、swresample、avutil 模块 end======
find_library(log-lib log)
target_link_libraries(
tolyffmpeg
avcodec
swresample
avutil
${log-lib})
4. 构建产物
点击小锤子,可以在 build
中看到一些构建产物,其中的 so
只会包含引入的相关模块:
默认情况下四种架构都会构建,可以在 app/build.gradle
中指定只构建哪些,比较支持的架构越多,应用体积越大。如下所示:
android {
defaultConfig {
externalNativeBuild {
cmake {
abiFilters 'armeabi-v7a', 'arm64-v8a'
}
}
}
5. C 代码修改和运行结果
如下代码,引入了 libavcodec/avcodec.h
头文件,使用其中的 avcodec_configuration
方法获取信息,进行返回。
---->[src/main/cpp/native-lib.cpp]----
#include <jni.h>
#include <string>
extern "C"{
#include <libavcodec/avcodec.h>
}
extern "C" JNIEXPORT jstring JNICALL
Java_com_toly1994_tolyffmpeg_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C ";
return env->NewStringUTF(avcodec_configuration());
}
如下就是编译时的配置信息,通过 C
获取,返回给 Java
端,进行显示。
四、小结
这就是最基本的利用 NDK
编译 FFmpeg
动态链接库。其实仔细想想,项目中的 C
文件也是被 NDK
编译成 libtolyffmpeg.so
库,才能被 Java
所调用。
最后用官网的几句话收尾:Android NDK 是一组使您能将 C 或 C (“原生代码”)嵌入到 Android 应用中的工具。 NDK 将 C 和 C 代码编译到原生库中,然后使用 Android Studio 的集成构建系统 Gradle 将原生库打包到 APK 中。Java 代码随后可以通过 Java 原生接口 (JNI) 框架调用原生库中的函数。
@张风捷特烈 2022.05.25 未允禁转
我的公众号:编程之王
我的 掘金主页
: 张风捷特烈我的 B站主页
: 张风捷特烈我的 github 主页
: toly1994328