Android FFmpeg 编译和集成(十四)

2021-01-17 14:20:11 浏览数 (1)

直接使用FFmpeg

因为FFmpeg是一套集录制、转换以及流化音视频的完整的跨平台解决方案,如果我们开发者想直接在自己开发的Android应用中使用ffmpeg的提供的功能,则需要引入so静态库,比如制作一些音视频编辑应用。

交叉编译生成,so动态库

编译工具链

对于C/C 的编译,通常有两个工具 GCC 和 CLANG 。

如果有用过c/c 的开发者应该都知道GCC,是一个编译工具,不仅可以编译C/C ,也可以编译Java,Object-C,Go等语言。

CLANG 则是更高效的C/C 编译工具,Google在ndk 17 以后,把 GCC 移除了,全面推行使用 CLANG 。 所以网上一些比较旧的ffmpeg编译教程没来得及更新,我们容易踩坑,导致ffmpeg源码编译失败。

使用CLANG编译FFmpeg

笔者的本文用的编译环境是:

编译机器: Mac OS Big Sur Version 11.1

NDK版本:android-ndk-r21d-darwin-x86_64.zip

测试机:华为Mate 30

FFmpeg版本:目前最新版本4.2.2

本文是使用目前最新的 NDK r21d 版本来编译。

NDK 下载地址:Android-NDK

NDK目录

代码语言:txt复制
编译工具链目录:
toolchains/llvm/prebuilt/darwin-x86_64/bin

交叉编译环境目录:
toolchains/llvm/prebuilt/darwin-x86_64/sysroot

如下图所示

因为笔者的测试机器是华为的Mate 30,所以选择 CPU 架构 aarch64,Android版本 29,我们可以按照自己的实际需求选择编译工具

代码语言:txt复制
aarch64-linux-androideabi21-clang
aarch64-linux-androideabi21-clang  
下载FFmpeg源码

FFmpeg官网下载,直接DownLoad即可。 本文使用的是目前最新的版本 ffmpeg-4.2.2。 下载解压源码后,进入根目录,找到congfigure 的文件,它是一个shell脚本,用于生成一些 FFmpeg 编译需要的配置文件。这个文件非常重要,FFmpeg 的编译配置是依赖它完成的。

修改 configure 脚本 (可以用Subline打开)

我们需要修改ffmpeg-4.2.2 根目录下的 configure 文件,实际上是因为Google 在新版ndk把 GCC 移除了,全面推行使用 CLANG,所以我们需要把编译工具配置进行修改。修改流程如下:

1.新增 cross_prefix_clang 参数

我们可以搜索 CMDLINE_SET ,可以找到以下代码,然后新增一个命令行选项:cross_prefix_clang

2.修改编译工具路径设置

我们可以搜索 ar_default="${cross_prefix}${ar_default}" , 找到以下代码:

代码语言:txt复制
ar_default="${cross_prefix}${ar_default}"

cc_default="${cross_prefix}${cc_default}"

cxx_default="${cross_prefix}${cxx_default}"

nm_default="${cross_prefix}${nm_default}"

pkg_config_default="${cross_prefix}${pkg_config_default}"

将中间两行修改为:

代码语言:txt复制
ar_default="${cross_prefix}${ar_default}"
#------------------------------------------------
cc_default="${cross_prefix_clang}${cc_default}"
cxx_default="${cross_prefix_clang}${cxx_default}"
#------------------------------------------------
nm_default="${cross_prefix}${nm_default}"
pkg_config_default="${cross_prefix}${pkg_config_default}"

3.新建编译配置脚本

在 ffmpeg-4.2.2 根目录下新建 shell 脚本,命名为: build_android_clang.sh,脚本代码如下:

代码语言:txt复制
#!/bin/bash
set -x
# 目标Android版本
API=29
ARCH=arm64
CPU=armv8-a
TOOL_CPU_NAME=aarch64
#so库输出目录
OUTPUT=/Users/pj1053/Downloads/ffmpeg_source/ffmpeg/android/$CPU
# NDK的路径,根据自己的NDK位置进行设置
NDK=/Users/pj1053/Downloads/android-ndk-r21d
# 编译工具链路径
TOOLCHAIN=$NDK/toolchains/llvm/prebuilt/darwin-x86_64
# 编译环境
SYSROOT=$TOOLCHAIN/sysroot

TOOL_PREFIX="$TOOLCHAIN/bin/$TOOL_CPU_NAME-linux-android"
 
CC="$TOOL_PREFIX$API-clang"
CXX="$TOOL_PREFIX$API-clang  "
OPTIMIZE_CFLAGS="-march=$CPU"
function build
{
  ./configure 
  --prefix=$OUTPUT 
  --target-os=android 
  --arch=$ARCH  
  --cpu=$CPU 
  --disable-asm 
  --enable-neon 
  --enable-cross-compile 
  --enable-shared 
  --disable-static 
  --disable-doc 
  --disable-ffplay 
  --disable-ffprobe 
  --disable-symver 
  --disable-ffmpeg 
  --cc=$CC 
  --cxx=$CXX 
  --sysroot=$SYSROOT 
  --extra-cflags="-Os -fpic $OPTIMIZE_CFLAGS" 

  make clean all
  # 这里是定义用几个CPU编译
  make -j8
  make install
}
build

其中有两个地方要修改成自己的ffmpeg源码项目和NDK编译工具的本地路径,如下图:

4.添加脚本权限

编写完脚本文件,需要添加权限。

代码语言:txt复制
chmod  x  build_android_clang.sh

5.脚本执行

添加脚本权限之后,我们可以直接运行脚本。

代码语言:txt复制
 ./build_android_clang.sh

等待编译完成,将会在 当前文件夹的/android/armv8-a目录下得到 include 和 lib 两个目录,分别是 头文件 和 so库文件,就是我们需要编译生成的ffmpeg静态库文件和头文件。

使用FFmpeg so动态库

1.使用Android Studio 创建Native C 工程

新建项目的时候有一个选项是选择Native C 的模板

点击next,配置项目的信息

点击next,选择使用哪种C 标准,选择Toolchain Default会使用默认的CMake设置即可

点击finish即可完成工程的创建。

2.工程结构

这时候主工程目录下会有cpp文件夹

cpp文件夹:存放C/C 代码文件,native-lib.cpp文件默认生成的;

cpp文件夹下有两个文件,一个是native-lib.cpp文件,一个是CMakeLists.txt文件。CMakeLists.txt文件是cmake脚本配置文件,cmake会根据该脚本文件中的指令去编译相关的C/C 源文件,并将编译后产物生成共享库或静态块,然后Gradle将其打包到APK中。

3.Java调用native层c/c 代码

在MainActivity.java,static{}语句中使用了加载so库,在类加载中只执行一次。

代码语言:txt复制
 static {
        System.loadLibrary("native-lib");
    }

然后,编写了原生的函数,函数名中要带有native。

代码语言:txt复制
public native String stringFromJNI();

最后,编写相对应的c函数,注意函数名的构成Java_com_pengjie0668_demo_myapplication_MainActivity_stringFromJNI加上包名、类型、方法名的下划线连成一起。

注意:要按照jni的规范定义方法(Java包名类名native方法名,其中包名中的点用代替)

native-lib.cpp文件

代码语言:txt复制
#include <jni.h>
#include <string>

extern "C" JNIEXPORT jstring JNICALL
Java_com_pengjie0668_demo_myapplication_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */) {
    std::string hello = "Hello from C  ";
    return env->NewStringUTF(hello.c_str());
}

4.引入 FFmpeg so库

  • 添加ffmpeg so库文件

首先,在 app/src/main/ 目录下,新建文件夹,并命名为 jniLibs ,接着,在 jniLibs 目录下,新建 arm64-v8a 目录,

最后把 FFmpeg 编译得到的所有 so 库粘贴到 arm64-v8a 目录。如图:

  • 添加 FFmpeg so库的头文件

在 cpp 目录下,新建 ffmpeg 目录,然后把编译时生成的 include 文件粘贴进来。

  • 配置CMakeLists.txt

上面已经把 so 和 头文件 放置到对应的目录中了,但是编译器是不会把它们编译、链接、并打包到 Apk 中的,我们还需要在 CMakeLists.txt 中显性的把相关的 so 添加和链接起来。完整的 CMakeLists.txt 如下:

代码语言:javascript复制
cmake_minimum_required(VERSION 3.10.2)

# 支持gnu  11
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=gnu  11")

# 1. 定义so库和头文件所在目录,方面后面使用
set(ffmpeg_lib_dir ${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI})
set(ffmpeg_head_dir ${CMAKE_SOURCE_DIR})

# 2. 添加头文件目录
include_directories(${ffmpeg_head_dir}/include)

# 3. 添加ffmpeg相关的so库
add_library( avutil
        SHARED
        IMPORTED )
set_target_properties( avutil
        PROPERTIES IMPORTED_LOCATION
        ${ffmpeg_lib_dir}/libavutil.so )

add_library( swresample
        SHARED
        IMPORTED )
set_target_properties( swresample
        PROPERTIES IMPORTED_LOCATION
        ${ffmpeg_lib_dir}/libswresample.so )

add_library( avcodec
        SHARED
        IMPORTED )
set_target_properties( avcodec
        PROPERTIES IMPORTED_LOCATION
        ${ffmpeg_lib_dir}/libavcodec.so )

add_library( avfilter
        SHARED
        IMPORTED)
set_target_properties( avfilter
        PROPERTIES IMPORTED_LOCATION
        ${ffmpeg_lib_dir}/libavfilter.so )

add_library( swscale
        SHARED
        IMPORTED)
set_target_properties( swscale
        PROPERTIES IMPORTED_LOCATION
        ${ffmpeg_lib_dir}/libswscale.so )

add_library( avformat
        SHARED
        IMPORTED)
set_target_properties( avformat
        PROPERTIES IMPORTED_LOCATION
        ${ffmpeg_lib_dir}/libavformat.so )

add_library( avdevice
        SHARED
        IMPORTED)
set_target_properties( avdevice
        PROPERTIES IMPORTED_LOCATION
        ${ffmpeg_lib_dir}/libavdevice.so )

# 查找代码中使用到的系统库
find_library( # Sets the name of the path variable.
        log-lib

        # Specifies the name of the NDK library that
        # you want CMake to locate.
        log )

# 配置目标so库编译信息
add_library( # Sets the name of the library.
        native-lib

        # Sets the library as a shared library.
        SHARED

        # Provides a relative path to your source file(s).
        native-lib.cpp
        )

# 指定编译目标库时,cmake要链接的库
target_link_libraries(

        # 指定目标库,native-lib 是在上面 add_library 中配置的目标库
        native-lib

        # 4. 连接 FFmpeg 相关的库
        avutil
        swresample
        avcodec
        avfilter
        swscale
        avformat
        avdevice

        # Links the target library to the log library
        # included in the NDK.
        ${log-lib} )

5.验证使用 FFmpeg

要检查 FFmpeg 是否可以使用,可以通过获取 FFmpeg 基础信息来验证。

(1)在 native-lib.cpp 中添加对应的 JNI 层方法。

代码语言:javascript复制
#include <jni.h>
#include <string>
#include <unistd.h>

extern "C" {
    #include <libavcodec/avcodec.h>
    #include <libavformat/avformat.h>
    #include <libavfilter/avfilter.h>
    #include <libavcodec/jni.h>

    JNIEXPORT jstring JNICALL
    Java_com_cxp_learningvideo_FFmpegActivity_ffmpegInfo(JNIEnv *env, jobject  /* this */) {

        char info[40000] = {0};
        AVCodec *c_temp = av_codec_next(NULL);
        while (c_temp != NULL) {
            if (c_temp->decode != NULL) {
                sprintf(info, "%sdecode:", info);
            } else {
                sprintf(info, "%sencode:", info);
            }
            switch (c_temp->type) {
                case AVMEDIA_TYPE_VIDEO:
                    sprintf(info, "%s(video):", info);
                    break;
                case AVMEDIA_TYPE_AUDIO:
                    sprintf(info, "%s(audio):", info);
                    break;
                default:
                    sprintf(info, "%s(other):", info);
                    break;
            }
            sprintf(info, "%s[%s]n", info, c_temp->name);
            c_temp = c_temp->next;
        }
        
        return env->NewStringUTF(info);
    }
}

首先,我们看到代码被包裹在 extern "C" { } 当中,和前面的系统创建的稍微有些不同,通过这个大括号包裹,我们就不需要每个方法都添加单独的 extern "C" 开头了。 另外,由于 FFmpeg 是使用 C 语言编写的,所在 C 文件中引用 #include 的时候,也需要包裹在 extern "C" { },才能正确的编译。

(2)在 MainActivity 中添加一个外部方法 ffmpegInfo

代码语言:javascript复制
   public native String ffmpegInfo();

(3)如果一切正常,App运行后,就会显示出 FFmpeg 音视频编解码器的信息

小结:

使用Android NDK工具对ffmpeg 源码进行交叉编译动态库的原理比较简单,但是在实践操作过程中,需要主要编译工具中路径的设置,和编译脚本内参数的设置。

Github Demo下载链接

0 人点赞