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

2,493 阅读2分钟

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 https://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.sh

build_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 <jni.h>
#include <string>
#include <android/native_window.h>
#include <android/native_window_jni.h>
#include <string>
#include <unistd.h>
​
​
extern "C" {
#include <libavutil/log.h>
#include <libavutil/error.h>
#include <libavformat/avio.h>
#include <libavformat/avformat.h>
#include <android/log.h>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