MacOS编译Android端使用的FFmpeg

2,514 阅读4分钟

[toc]

MacOS编译Android端使用的FFmpeg

1. 背景

编译ffmpeg工程,需要NDK。 早些年都是使用GCC来编译。从r18b开始,NDK正式移除gcc,google官方放弃了gcc交叉编译工具链,转而使用clang编译工具链,因为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 ,搜索找到如下命令行:

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 字段说明

(注:这仅为字段说明, " \ " 后不能有注释,否则编译报错)


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 文件没有生成

错误信息:

./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的相关功能。

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:

#include <android/log.h>
#include "com_jni_FFmpeg.h"
#include <stdlib.h>
#include <stdbool.h>
#include <stdio.h>
#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相当于二级指针

(*env)->NewStringUTF(env, conf); // C下的调用方式

env->NewStringUTF(conf); //C++下的调用方式

在app下的build.gradle下配置Cmake相关:


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:


# 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中调用:

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