FFmpeg与 Android NDK平台M1交叉编译记录

语言: CN / TW / HK

theme: mk-cute highlight: atelier-estuary-dark


FFmpeg与 Android NDK平台M1交叉编译记录

一.前言

交叉编译是在一个平台上生成另一个平台上的可执行代码

本文所描述的,就是在Mac OS M1平台下,交叉编译的记录,最终的目标是,让ffmpeg-release/5.0版本编译出适合Android ARM64-v8a运行的产物,让FFMpeg在Android上运行起来。

本文只做一些记录。整个过程中,其实和是不是M1没啥关系,只不过NDK24加入了对M1的支持:

image-20220517234730136.png

你需要以下的环境和工具:

  1. 基于M1芯片的MacOS;
  2. GitHub - FFmpeg/FFmpeg: Mirror of http://git.ffmpeg.org/ffmpeg.git
  3. NDK 下载 | Android NDK | Android Developers (google.cn)
  4. 一个用于测试的mp4文件

首先看看NDK,这玩意下载出来是一个dmg文件,其实我们需要的是其中的NDK和其文件夹下的东西,所以我们需要打开这个DMG文件,在里面的AndroidNDK821888应用图标上,右键显示包内容,将Contents中的NDK文件夹整个移动出来,整个便是我们需要的NDK工具。

image-20220517234625300.png

二. 环境预配置

我们接下来要做的,就是在FFmpeg文件夹下,指定编译器和编译平台,为Android编译可用的FFmpeg,在.zshrc或者.bashrc下,设置NDK_HOME变量:

export NDK_HOME=~/sdk/NDK/

完成后,source .zshrc一下,在终端中输入:

echo $NDK_HOME

如果正确输出了路径,那么就配置完成了。当然,你不配也行,在后面脚本中直接固定写NDK路径也是一样的。

接着,我们来到FFmpeg源代码文件夹,并且使用: git checkout release/5.0 切换到我们的目标分支。

三. 编写编译脚本

我们需要配置编译的平台、指定目标平台的一些架构信息。

在FFmpeg根目录下,新建一个build_android.sh文件,然后打开,键入:

```

!/bin/bash

NDK的路径 (在bash.rc、zshrc中设置的路径,其中的NDK_HOME需要配置环境变量,如果你不配,在这改成你自己的也可以。)

NDK_ROOT=$NDK_HOME

NDK_ROOT=你的NDK路径

TOOLCHAIN_PREFIX=$NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64 ​ echo "<<<<<< FFMPEG 交叉编译 <<<<<<" echo "<<<<<< 基于当前系统NDK地址: $NDK_HOME <<<<<<" ​ ​

编译的相关参数

目标平台的CPU指令类型 ARMv8

CPU=armv8-a

架构类型 : ARM

ARCH=arm64

操作系统

OS=android

平台

PLATFORM=aarch64-linux-android OPTIMIZE_CFLAGS="-march=$CPU" ​

指定输出路径

PREFIX=~/tools/outputs/ffmpeg/aarch64 ​

SYSROOT

SYSROOT=$TOOLCHAIN_PREFIX/sysroot ​

交叉编译工具链

CROSS_PREFIX=$TOOLCHAIN_PREFIX/bin/llvm- ​

Android交叉编译工具链的位置

ANDROID_CROSS_PREFIX=$TOOLCHAIN_PREFIX/bin/${PLATFORM}29 ​ echo ">>>>>> FFMPEG 开始编译 >>>>>>" ​ ./configure \ --prefix=$PREFIX \ --enable-shared \ --enable-gpl \ --enable-neon \ --enable-hwaccels \ --enable-postproc \ --enable-jni \ --enable-small \ --enable-mediacodec \ --enable-decoder=h264_mediacodec \ --enable-ffmpeg \ --disable-ffplay \ --disable-ffprobe \ --disable-ffplay \ --disable-avdevice \ --disable-debug \ --disable-static \ --disable-doc \ --disable-symver \ --cross-prefix=$CROSS_PREFIX \ --target-os=$OS \ --arch=$ARCH \ --cpu=$CPU \ --cc=${ANDROID_CROSS_PREFIX}-clang \ --cxx=${ANDROID_CROSS_PREFIX}-clang++ \ --enable-cross-compile \ --sysroot=$SYSROOT \ --extra-cflags="-Os -fPIC $OPTIMIZE_CFLAGS" \ --extra-ldflags="$ADDI_LDFLAGS" \ ​ ​ make clean ​

创建目标路径,如果不存在的话,最终产物存储在Prefix对应路径之下。

mkdir -p $PREFIX ​ sudo make -j8 ​ sudo make install ​ ​ echo "<<<<<< 编译完成,产物存储在:$PREFIX <<<<<<" ```

然后直接在终端执行该脚本即可开始编译,过程大概五到十分钟,如果权限不够,需要使用: chmod 777 build_android.shbuild_android.sh加上执行权限。

最终的产物,会存在上述脚本的.sh目录之下,我们需要的是libs文件夹中的一些.so文件:

image-20220518224602728.png

四. Android端测试

首先我们先新建一个Android Module:ffmpeg,然后将刚才生成的所有.so文件放入如下文件夹:ffmpeg/src/main/cpp/libs/arm64-v8a

image.png 然后编辑ffmpeg模块下的build.gradle:

android {    compileSdk 32 ​    defaultConfig {        ……        ndk {            abiFilters "arm64-v8a"       }   }    ……

因为是测试,我们只保留arm64-v8a一种型号的设备,有需要可以自己按照前面的步骤去编译x86或者是armeabi-v7a等等。

编辑CMakeLists.txt,删掉最后一行的target_link_libraries的调用,并在最后追加:

``` set(JNI_LIBS_DIR ${CMAKE_CURRENT_SOURCE_DIR}) ​

引入一些头文件,这里填你FFmpeg源码存在的路径,主要是引入一些头文件,编译时要使用。

include_directories(~/tools/FFmpeg) ​ add_library(avutil       SHARED       IMPORTED) set_target_properties(avutil       PROPERTIES IMPORTED_LOCATION        ${JNI_LIBS_DIR}/libs/${ANDROID_ABI}/libavutil.so) ​ add_library(swresample       SHARED       IMPORTED) set_target_properties(swresample       PROPERTIES IMPORTED_LOCATION        ${JNI_LIBS_DIR}/libs/${ANDROID_ABI}/libswresample.so) ​ add_library(swscale       SHARED       IMPORTED) set_target_properties(swscale       PROPERTIES IMPORTED_LOCATION        ${JNI_LIBS_DIR}/libs/${ANDROID_ABI}/libswscale.so) ​ add_library(avcodec       SHARED       IMPORTED) set_target_properties(avcodec       PROPERTIES IMPORTED_LOCATION        ${JNI_LIBS_DIR}/libs/${ANDROID_ABI}/libavcodec.so) ​ add_library(avformat       SHARED       IMPORTED) set_target_properties(avformat       PROPERTIES IMPORTED_LOCATION        ${JNI_LIBS_DIR}/libs/${ANDROID_ABI}/libavformat.so) ​ add_library(avfilter       SHARED       IMPORTED) set_target_properties(avfilter       PROPERTIES IMPORTED_LOCATION        ${JNI_LIBS_DIR}/libs/${ANDROID_ABI}/libavfilter.so) ​ add_library(postproc       SHARED       IMPORTED) set_target_properties(postproc       PROPERTIES IMPORTED_LOCATION        ${JNI_LIBS_DIR}/libs/${ANDROID_ABI}/libpostproc.so) ​ target_link_libraries(ffmpeg       avutil swresample swscale avcodec avformat avfilter postproc        ${log-lib} ${android-lib} ${log}) ```

然后在自带的NativeLib.java中,编写一个方法:

// 定义一个方法获取Path对应视频的FFmpegContext external fun getFFmpegContext(path:String):String

然后按住options+enter,在ffmpeg.cpp中自动生成对应的cpp方法,编辑ffmpeg.cpp:

```

include

include

include

include

include

include

​ ​ extern "C" {

include

include

include

include

include

​ extern "C" JNIEXPORT jstring JNICALL Java_com_example_ffmpeg_NativeLib_stringFromJNI(        JNIEnv env,        jobject / this /) {    std::string hello = "Hello from C++";    return env->NewStringUTF(hello.c_str()); } ​ ​ extern "C" JNIEXPORT jstring JNICALL Java_com_example_ffmpeg_NativeLib_getFFmpegContext(JNIEnv env, jobject thiz, jstring path) {    AVFormatContext fmt_ctx = avformat_alloc_context(); ​    const char str = env->GetStringUTFChars(path, 0);    int ret = avformat_open_input(&fmt_ctx, str, nullptr, nullptr);    if (ret < 0) {        __android_log_print(ANDROID_LOG_ERROR, "FFmpeg Error", "Cannot open %s ,cause %s\n",                            str, av_err2str(ret));        goto __finally;   } ​    __finally:        avformat_close_input(&fmt_ctx);        env->ReleaseStringUTFChars(path, str); ​    return env->NewStringUTF("complete!"); } } ```

因为是一个测试,所以我们直接在app的MainActivity中调用该FFMpeg方法,其中的hftw.mp4是一个视频文件,你需要将它手动放到/data/user/0/com.example.ffmpegcrosscomplieproj/cache/hftw.mp4文件夹下,你可以使用Android Studio界面边缘的Device File Explorer上传一个MP4视频文件。

这个路径你可以自定义,然后传给JNI。只要保证App能够取到即可。

class MainActivity : AppCompatActivity() {    override fun onCreate(savedInstanceState: Bundle?) {        super.onCreate(savedInstanceState)        setContentView(R.layout.activity_main)        val path = applicationContext.cacheDir.absolutePath + "/hftw.mp4"        Log.i("Storage Environment", path)        NativeLib().getFFmpegContext(path)   } }

最后,我们在app模块的build.gradle引入对我们自建的ffmpeg module的引用:

implementation project(":ffmpeg")

完成后,我们在ffmpeg.cpp的

if (ret < 0) {

这一行打上断点,程序以Debug模式启动之后,在该位置停下,我们查看fmt_ctx的内容,如果有数据,就说明FFmpeg读取文件生效了,读出的时长是正确的。

image-20220518234640621.png

~ end