MacOS編譯Android端使用的FFmpeg

語言: CN / TW / HK

[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關鍵字代表編譯完成。

屏幕快照 2022-04-19 15.52.35.png

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下生成相應模塊的頭文件

屏幕快照 2022-04-19 15.56.19.png

3. JNI實現調用動態庫

此時編譯出來的so是不能在Android上直接使用的, 需要通過JNI的方式調用。 將lib下的so拷貝出來, 放到工程的libs的armeabi-v7a目錄下, 同時將將include 目錄也拷貝到libs下,結構如下:

屏幕快照 2022-04-19 16.04.16.png

新建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, 回車。

屏幕快照 2022-04-19 16.14.43.png

這樣會在當前目錄下生成一個 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下。

屏幕快照 2022-04-19 16.46.16.png

最後,在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的配置信息。

屏幕快照 2022-04-19 16.53.19.png