[toc]
MacOS编译Android端使用的FFmpeg
1. 背景
编译ffmpeg工程,需要NDK。 早些年都是使用GCC来编译。从r18b开始,NDK正式移除gcc,google官方放弃了gcc交叉编译工具链,转而使用clang编译工具链,因为clang编译速度快、效率高。
- 系统:macos 10.14.3
- FFmpeg版本:ffmpeg 5.0, ffmpeg-snapshot.tar.bz2, 下载地址:ffmpeg.org/
- ndkVersion: "20.1.5948944", 下载地址:developer.android.google.cn/ndk/downloa…
- 编译器:clang
2. 编译ffmpeg源码
在FFmpeg源码目录有个configure配置脚本,使用./configure --help进行查看相关的编译选项, 通过 --enable-XX 和 --disable-XX 来开启和关闭 XX选项。
2.1 修改so后缀
默认编译出来的so库包括avcodec、avformat、avutil、avdevice、avfilter、swscale、avresample、swresample、postproc,编译出来so是个软链接,真正so名字后缀带有一长串主版本号与子版本号,形如:libavcodec.so.57, 这样的so名字在Adnroid平台无法识别。所以我们需要修改一下configure使其生成以 .so 结尾格式的动态库。
vim打开该文件, 冒号进入命令模式, 输入 /SLIBNAME_WITH_MAJOR ,搜索找到如下命令行:
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_WITH_MAJOR)'
SLIB_INSTALL_LINKS='$(SLIBNAME)'
2.2 编译脚本
修改FFmpeg编译选项, 可通过在FFmpeg根目录下通过 ./configure 命令进行设置。 但是为了方便记录与修改,都选择在FFmpeg根目录下建立一个脚本文件来运行 ./configure 命令。 创建一个名为build_ffmpeg.sh的shell脚本。
2.2.1 字段说明
(注:这仅为字段说明, " \ " 后不能有注释,否则编译报错)
if [ $archbit -eq 64 ];then
API=21 # 该架构所支持的最低API版本,向下兼容,运行的手机不能低于此版本
else
API=16
fi
export CC=$TOOLCHAIN/$PLATFORM-linux-$ANDROID$API-clang # clang编译器
export CXX=$TOOLCHAIN/$PLATFORM-linux-$ANDROID$API-clang++ # clang++编译器
./configure \
--prefix=$PREFIX \ #规定编译后的文件输出目录
--enable-cross-compile \ #启用交叉编译方式
--cross-prefix=$TOOLCHAIN/bin/arm-linux-androideabi- \ #交叉编译链路径
--target-os=android \ #目标系统
--arch=arm \ #目标平台架构
--sysroot=$SYSROOT \ #交叉编译环境, 使其在编译过程中能够引用到NDK提供的原生标头和共享库文件
--extra-cflags="" \ #编译时额外需要的flags
--extra-ldflags="" \ #链接库时额外需要的flags
--enable-shared \ #生成动态库(共享库)
--disable-static \ #禁止生成静态库
--disable-doc \ #禁用不需要的功能
--enable-macos-kperf #启用需要的功能
2.2.2 完整的脚本代码
#!/bin/bash
make clean
set -e
archbit=32
if [ $archbit -eq 64 ];then
echo "build for 64bit"
ARCH=aarch64
CPU=armv8-a
API=21
PLATFORM=aarch64
ANDROID=android
CFLAGS=""
LDFLAGS=""
else
echo "build for 32bit"
ARCH=arm
CPU=armv7-a
API=16
PLATFORM=armv7a
ANDROID=androideabi
CFLAGS="-mfloat-abi=softfp -march=$CPU"
LDFLAGS="-Wl,--fix-cortex-a8"
fi
export NDK=/Users/apple/Library/Android/android-ndk-r20b
export TOOLCHAIN=$NDK/toolchains/llvm/prebuilt/darwin-x86_64/bin
export SYSROOT=$NDK/toolchains/llvm/prebuilt/darwin-x86_64/sysroot
export CROSS_PREFIX=$TOOLCHAIN/$ARCH-linux-$ANDROID-
export CC=$TOOLCHAIN/$PLATFORM-linux-$ANDROID$API-clang
export CXX=$TOOLCHAIN/$PLATFORM-linux-$ANDROID$API-clang++
export PREFIX=../ffmpeg-android/$CPU
function build_android {
./configure \
--prefix=$PREFIX \
--cross-prefix=$CROSS_PREFIX \
--target-os=android \
--arch=$ARCH \
--cpu=$CPU \
--cc=$CC \
--cxx=$CXX \
--nm=$TOOLCHAIN/$ARCH-linux-$ANDROID-nm \
--strip=$TOOLCHAIN/$ARCH-linux-$ANDROID-strip \
--enable-cross-compile \
--sysroot=$SYSROOT \
--extra-cflags="$CFLAGS" \
--extra-ldflags="$LDFLAGS" \
--extra-ldexeflags=-pie \
--enable-runtime-cpudetect \
--disable-static \
--enable-shared \
--disable-ffprobe \
--disable-ffplay \
--disable-ffmpeg \
--disable-debug \
--disable-doc \
--enable-avfilter \
--enable-avresample \
--enable-decoders \
$ADDITIONAL_CONFIGURE_FLAG
make #执行make命令
make install #执行安装
}
build_android
按照上面shell脚本分为4段。
第一段make clean清除缓存,set -e设置编译出错后马上退出,archbit=xx指定cpu架构是32位还是64位。
第二段if...else...fi用来条件编译不同cpu架构对应字段的值。
第三段用export关键字声明宏定义,其中PREFIX是指定输出文件路径。
第四段是一个执行函数,按照ffmpeg的configure规范进行编写。函数里面的enable代表开启,disable代表关闭,也就是对ffmpeg进行剪裁,根据我们需要的功能进行enable。make命令是执行编译,make install命令是执行安装。最后的build_android是执行函数。
注:初次执行shell脚本,需要修改脚本权限,使用linux命令:chmod 777 build_ffmpeg.sh。
执行脚本只需要一行命令,即在命令行输入./build_ffmpeg.sh。 编译过程中,命令行会不断打印编译日志,等待命令行输出INSTALL xxx关键字代表编译完成。
2.2.2 编译问题分析
编译过程中,可能会出现这样那样的问题,比如ndk配置不对、脚本语法不对。但不用慌,编译输出窗口会描述出错原因,在ffbuild/config.log会告诉你问题的具体原因所在,顺着思路一般可以找到问题的答案。
2.2.2.1 xmakefile 文件没有生成
错误信息:
./build_ffmpeg.sh: line 36: --enable-shared: command not found
Makefile:2: ffbuild/config.mak: No such file or directory
Makefile:40: /tools/Makefile: No such file or directory
Makefile:41: /ffbuild/common.mak: No such file or directory
Makefile:91: /libavutil/Makefile: No such file or directory
Makefile:91: /ffbuild/library.mak: No such file or directory
Makefile:93: /fftools/Makefile: No such file or directory
Makefile:94: /doc/Makefile: No such file or directory
Makefile:95: /doc/examples/Makefile: No such file or directory
Makefile:160: /tests/Makefile: No such file or directory
make: *** No rule to make target `/tests/Makefile'. Stop.
解决:
执行*./configure --disable-x86asm* 生成config.mak文件
2.2.2.2 xxxxx No such file or directory
/build_ffmpeg.sh: line 32: xxxxx No such file or directory
./configure \ #XXX ...
原因: " \ " 后面不能有注释
解决: 删除" \ "后面的注释
2.2.3 输出结果
编译成功后,$PREFIX 目录下生成若干个文件夹,其中lib下生成相应模块的动态库,include下生成相应模块的头文件
3. JNI实现调用动态库
此时编译出来的so是不能在Android上直接使用的, 需要通过JNI的方式调用。 将lib下的so拷贝出来, 放到工程的libs的armeabi-v7a目录下, 同时将将include 目录也拷贝到libs下,结构如下:
新建java类FFmpeg,声明一个native方法run(), 并静态块中加载相应的动态库, 其中最下面的ffmpeg是将要编译生成的动态库,Android端通过这个so来调用FFmpeg的相关功能。
package com.jni;
public class FFmpeg {
static {
System.loadLibrary("avcodec");
System.loadLibrary("avformat");
System.loadLibrary("swscale");
System.loadLibrary("avutil");
System.loadLibrary("avfilter");
System.loadLibrary("avformat");
System.loadLibrary("swresample");
System.loadLibrary("avdevice");
System.loadLibrary("ffmpeg");
}
public static native String run();
}
生成相应的头文件:
在AS的Terminal下,先切换到app/main/java下, 然后输入 javah -classpath . com.jni.FFmpeg, 回车。
这样会在当前目录下生成一个 com_jni_FFmpeg.h, 然后在app下右键新建->Jni Folder,将头文件投入其中,并新建对应的c原文件 com_jni_FFmpeg.c:
#include <android/log.h>
#include "com_jni_FFmpeg.h"
#include <stdlib.h>
#include <stdbool.h>
#include <stdio.h>
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libavfilter/avfilter.h"
#include "../../../../../../../../../../Library/Android/sdk/ndk/20.1.5948944/toolchains/llvm/prebuilt/darwin-x86_64/sysroot/usr/include/jni.h"
//logcat打印并返回一个当前FFmpeg的配置的字符串。
JNIEXPORT jstring JNICALL Java_com_jni_FFmpeg_run(JNIEnv *env, jclass obj){
const char *conf = avcodec_configuration();
__android_log_print(ANDROID_LOG_INFO, "JNI", "avcodec_configuration: %s", conf);
return (*env)->NewStringUTF(env, conf);
}
这里需注意的JNIEnv *env,在C++和C下的区别: C下的env相当于二级指针
(*env)->NewStringUTF(env, conf); // C下的调用方式
env->NewStringUTF(conf); //C++下的调用方式
在app下的build.gradle下配置Cmake相关:
android {
compileSdkVersion 30
buildToolsVersion "30.0.3"
defaultConfig {
applicationId "com.milanac007.demo.ffmpeg_android_demo"
minSdkVersion 16
targetSdkVersion 28
versionCode 1
versionName "1.0"
externalNativeBuild {
cmake {
cppFlags ""
}
}
ndk {
abiFilters "armeabi-v7a" //, "arm64-v8a"
}
}
ndkVersion "20.1.5948944"
packagingOptions {
pickFirst '**/*.so'
}
sourceSets {
main {
jniLibs.srcDirs = ['libs']
jni.srcDirs = [] //disable automatic ndk-build call
}
}
externalNativeBuild {
cmake {
path "src/main/jni/CMakeLists.txt"
version "3.10.2"
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.2.0'
...
}
然后在当前的jni目录下新建CMakeLists.txt:
# Sets the minimum version of CMake required to build the native library.
cmake_minimum_required(VERSION 3.10.2)
# Declares and names the project.
project("ffmpeg_android_demo")
set(my_lib_path ${CMAKE_SOURCE_DIR}/../../../libs) #定义变量my_lib_path,${CMAKE_SOURCE_DIR}为当前CMakeList.txt所在路径
# 打印功能,在.cxx/cmake/debug/armeabi-v7a/cmake_build_output.txt里面会有显示。
MESSAGE(STATUS "Current Path: ${CMAKE_SOURCE_DIR}")
${CMAKE_ANDROID_ARCH_ABI}\nINCLUDE_PATH: ${my_lib_path}/include")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=gnu++11")
include_directories("${my_lib_path}/include"}) #包含的头文件路径
############################ 链接库 方式1 ########################################
link_directories("${my_lib_path}/armeabi-v7a")
link_libraries(
avcodec
avformat
swscale
avutil
avfilter
swresample
avdevice
)
############################ 链接库 方式2 ########################################
add_library(avcodec
SHARED
IMPORTED)
set_target_properties(avcodec
PROPERTIES IMPORTED_LOCATION
${my_lib_path}/armeabi-v7a/libavcodec.so)
add_library(avformat
SHARED
IMPORTED)
set_target_properties(avformat
PROPERTIES IMPORTED_LOCATION
${my_lib_path}/armeabi-v7a/libavformat.so)
add_library(swscale
SHARED
IMPORTED)
set_target_properties(swscale
PROPERTIES IMPORTED_LOCATION
${my_lib_path}/armeabi-v7a/libswscale.so)
add_library(avutil
SHARED
IMPORTED)
set_target_properties(avutil
PROPERTIES IMPORTED_LOCATION
${my_lib_path}/armeabi-v7a/libavutil.so)
add_library(avfilter
SHARED
IMPORTED)
set_target_properties(avfilter
PROPERTIES IMPORTED_LOCATION
${my_lib_path}/armeabi-v7a/libavfilter.so)
add_library(swresample
SHARED
IMPORTED)
set_target_properties(swresample
PROPERTIES IMPORTED_LOCATION
${my_lib_path}/armeabi-v7a/libswresample.so)
add_library(avdevice
SHARED
IMPORTED)
set_target_properties(avdevice
PROPERTIES IMPORTED_LOCATION
${my_lib_path}/armeabi-v7a/libavdevice.so)
############################ 链接库 end ########################################
find_library( # Sets the name of the path variable.
log-lib
log )
add_library(
ffmpeg
SHARED
com_jni_FFmpeg.c
)
target_link_libraries( # Specifies the target library.
ffmpeg
# Links the target library to the log library
# included in the NDK.
avcodec
avformat
swscale
avutil
avfilter
swresample
avdevice
${log-lib} )
注:链接库的两种方式,任取其一即可。第一种较为简洁。
Make工程,这样在app/build/intermediates/cmake/debug/obj下会生成 libffmpeg.so,运行程序,它会和app/libs下的其他so,一起被自动打包到APK的lib下。
最后,在MainActivity中调用:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView tv = findViewById(R.id.sample_text);
tv.setText(FFmpeg.run());
}
}
运行程序, 成功显示了FFmpeg的配置信息。