MacOS編譯Android端使用的FFmpeg
[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, 下載地址:http://ffmpeg.org/
- ndkVersion: "20.1.5948944", 下載地址:http://developer.android.google.cn/ndk/downloads/older_releases#ndk-20b-downloads
- 編譯器: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 ,搜索找到如下命令行:
```java 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 字段説明
(注:這僅為字段説明, " \ " 後不能有註釋,否則編譯報錯)
```java
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 文件沒有生成
錯誤信息:
bash
./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的相關功能。
```java 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:
```c
include
include "com_jni_FFmpeg.h"
include
include
include
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相當於二級指針*
```java (*env)->NewStringUTF(env, conf); // C下的調用方式
env->NewStringUTF(conf); //C++下的調用方式 ```
在app下的build.gradle下配置Cmake相關:
```sh
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:
```java
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中調用: ```java 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的配置信息。