FFmpeg与 Android NDK平台M1交叉编译记录
一.前言
交叉编译是在一个平台上生成另一个平台上的可执行代码。
本文所描述的,就是在Mac OS M1平台下,交叉编译的记录,最终的目标是,让ffmpeg-release/5.0版本编译出适合Android ARM64-v8a运行的产物,让FFMpeg在Android上运行起来。
本文只做一些记录。整个过程中,其实和是不是M1没啥关系,只不过NDK24加入了对M1的支持:
你需要以下的环境和工具:
- 基于M1芯片的MacOS;
- GitHub - FFmpeg/FFmpeg: Mirror of https://git.ffmpeg.org/ffmpeg.git
- NDK 下载 | Android NDK | Android Developers (google.cn)
- 一个用于测试的mp4文件
首先看看NDK,这玩意下载出来是一个dmg文件,其实我们需要的是其中的NDK和其文件夹下的东西,所以我们需要打开这个DMG文件,在里面的AndroidNDK821888应用图标上,右键显示包内容,将Contents中的NDK文件夹整个移动出来,整个便是我们需要的NDK工具。
二. 环境预配置
我们接下来要做的,就是在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文件:
四. Android端测试
首先我们先新建一个Android Module:ffmpeg,然后将刚才生成的所有.so文件放入如下文件夹:ffmpeg/src/main/cpp/libs/arm64-v8a:
然后编辑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读取文件生效了,读出的时长是正确的。
~ end