NDK 编译(二)—— NDK 编译与集成 FFmpeg

发布于:2024-04-29 ⋅ 阅读:(25) ⋅ 点赞:(0)

NDK 编译系列文章共三篇,目录如下:

NDK 编译(一)—— Linux 知识汇总
NDK 编译(二)—— NDK 编译与集成 FFmpeg
NDK 编译(三)—— CMake 原生构建工具

在使用 NDK 进行音视频开发时,势必会用到 FFmpeg,因此我们要知道如何编译 FFmpeg 并将其集成到 Android 项目中。

1、准备工作

1.1 下载 FFmpeg

下载 Linux 版的 FFmpeg 4.0.6 版本并解压:

[root@frank AndroidNDK]# wget https://ffmpeg.org/releases/ffmpeg-4.0.6.tar.bz2
[root@frank AndroidNDK]# tar -xjf ffmpeg-4.0.6.tar.bz2

解压好了后将进入 FFmpeg 的目录,将用户手册导出到文件中方便查看:

[root@frank AndroidNDK]# cd ffmpeg-4.0.6/
[root@frank ffmpeg-4.3.6]# ./configure --help -> ffmpeg_help.txt

1.2 下载 NDK

下载 Linux 版本的 NDK r17c 版本并解压:

[root@frank AndroidNDK]# wget https://dl.google.com/android/repository/android-ndk-r17c-linux-x86_64.zip?hl=zh_cn
[root@frank AndroidNDK]# unzip android-ndk-r17c-linux-x86_64.zip

2、编译 FFmpeg

实际上在 FFmpeg 根目录下有一个编译脚本 configure.sh,运行该脚本就可以编译出完整的 FFmpeg 的库文件和头文件,但是这样编译出的库在 Android 上用不了。所以我们才需要通过 NDK 做交叉编译,编译出 Android 可以使用的静态库或动态库。由于编译 FFmpeg 的参数众多,因此我们把编译指令写入一个脚本文件中:

#!/bin/bash
# NDK 根目录
NDK_ROOT=/root/AndroidNDK/android-ndk-r17c
# 定义 NDK 交叉编译时使用的编译器(所在的目录)
TOOLCHAIN=$NDK_ROOT/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64
# 编译参数的 FLAGS 可以从 AS 的 app\.cxx\...\x86_64\build.ninja(不同版本路径不同)
# 与 4.0.2 版本相比去掉了 -fstackprotector-strong 参数,因为 4.0.6 不识别
FLAGS="-isystem $NDK_ROOT/sysroot/usr/include/arm-linux-androideabi -D__ANDROID_API__=21 -g -DANDROID -ffunction-sections -funwind-tables -no-canonical-prefixes -march=armv7-a -mfloat-abi=softfp -mfpu=vfpv3-d16 -mthumb -Wa,--noexecstack -Wformat -Werror=format-security -O0 -fPIC"
# 编译时所需的系统的头文件
INCLUDES=" -isystem $NDK_ROOT/sources/android/support/include"

# 执行 configure 脚本,用于生成 makefile,参数详解:
#--prefix: 安装目录
#--enable-small: 启用小型优化,以减小生成的库文件的大小
#--disable-programs:不编译 ffmpeg 自带的程序(命令行工具),我们是需要获得静态(动态)库
#--disable-avdevice:关闭 avdevice 模块(音视频设备支持),此模块在 android 中无用
#--disable-encoders:关闭所有编码器(播放不需要编码)
#--disable-muxers:关闭所有复用器(封装器),不需要生成 mp4 这样的文件,所以关闭
#--disable-filters:关闭视频滤镜
#--enable-cross-compile:开启交叉编译,用于在一个平台上为另一个平台生成可执行文件(ffmpeg 支持跨平台)
#--cross-prefix:指定交叉编译工具链的前缀,例如 gcc 的前缀 xxx/xxx/xxx-gcc 则给 xxx/xxx/xxx-
#--disable-shared 和 enable-static:禁用生成动态库和启用生成静态库。不写也可以,默认就是这样的
#--sysroot: 指定系统根目录,用于定位 Android 平台的头文件和库文件
#--extra-cflags: 会传给 gcc 的参数
#--arch:指定目标平台的体系结构,一般真机为 ARM,模拟器为 x86
#--target-os: 指定目标操作系统,这里为 Android

PREFIX=./output/android
./configure \
--prefix=$PREFIX \
--enable-small \
--disable-programs \
--disable-avdevice \
--disable-encoders \
--disable-muxers \
--disable-filters \
--enable-cross-compile \
--cross-prefix=$TOOLCHAIN/bin/arm-linux-androideabi- \
--disable-shared \
--enable-static \
--sysroot=$NDK_ROOT/platforms/android-21/arch-arm \
--extra-cflags="$FLAGS $INCLUDES" \
--extra-cflags="-isysroot $NDK_ROOT/sysroot/" \
--arch=arm \
--target-os=android

make clean
make install

编译 FFmpeg 过程:

  1. 定义两个变量,这两个变量在后续脚本中会被多次用到
    • NDK 的根目录:/root/AndroidNDK/android-ndk-r17c
    • 交叉编译的编译器所在的目录:TOOLCHAIN=$NDK_ROOT/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64
  2. 定义编译产物输出路径的变量:PREFIX=./output/android
  3. 定义 gcc 的编译参数,需要两个变量:
    • FLAGS:
      • -isystem <directory>:添加 directory 到编译器的系统包含路径中。我们要添加系统级别的头文件供编译器和系统使用,这里我们添加的路径是 $TOOLCHAIN/sysroot/usr/include/arm-linux-androideabi,它包含 iotcl.h 和 ioctls.h 等头文件
      • -D <macro>=<value>:-D 用来指定宏的值,这里我们指定 ANDROID_API__=33
      • -g:会生成源码级别的 debug 信息,
      • -DANDROID:-D 是一个预处理器定义的参数,用于定义预处理器宏。在编译过程中使用该参数会定义一个名为 ANDROID 的宏,并指定为 true,以便在代码中进行条件编译
      • -fdata-sections:GCC 的编译选项之一,用于将每个数据(变量)放置在独立的数据段(section)中
      • -ffunction-sections:GCC 编译选项之一,用于在生成目标文件时将每个函数放置在独立的代码段(section)中,作用是对生成的目标文件进行优化,以便在链接(linking)过程中更好地进行代码优化和减少最终可执行文件的大小
      • -funwind-tables:GCC 的编译选项之一,用于生成用于异常处理和堆栈展开(stack unwinding)的 unwind 表格(unwind tables)。该参数会为每个函数生成 unwind 表格,这些表格描述了函数中的异常处理逻辑和堆栈展开过程所需的信息
      • -fstack-protector-strong:GCC 的编译选项,用于启用堆栈保护机制,以防止栈溢出攻击
      • -no-canonical-prefixes:GCC 的编译选项之一,用于禁用规范化的前缀(canonical prefixes)
      • -D_FORTIFY_SOURCE=2:用于启用强化源代码(fortify source)的功能,以增强程序的安全性和防范某些类型的常见漏洞。具体而言,可以开启缓冲区溢出保护、安全字符串处理函数和编译时警告功能
      • -W:GCC 的编译选项之一,会启用指定的警告。-Wformat 就会启用格式字符串相关的警告,而 -Werror=format-security 会将格式字符串安全性警告视为错误
      • -fno-limit-debug-info:GCC 的编译选项之一,用于禁用调试信息的大小限制
      • -fPIC:是 GCC 的编译选项之一,用于生成位置无关代码(Position Independent Code,PIC)。位置无关代码是一种可执行代码或共享库的形式,其加载和执行的位置不依赖于内存的绝对地址。这种代码的特点是可以在内存中的任何位置加载并正确执行,因此适用于共享库的使用场景。使用位置无关代码可以使共享库在不同的进程地址空间中共享,并减少地址冲突的可能性。
    • INCLUDES:包含 gcc 编译时所需要的系统头文件
  4. 通过 ./configure 运行 FFmpeg 提供的编译脚本,其下面配置的参数主要分为两大类,一是对 FFmpeg 进行剪裁,因为 Android 开发用不到 FFmpeg 所有的功能,因此禁用掉不用的功能可以减小编译产物的大小;二是启用交叉编译,并配置编译相关的参数,具体详见脚本注释

编译完成之后会在我们指定的 /ffmpeg-4.0.6//output/android 目录下生成三个目录:include、lib、share:

2023-12-28.FFmpeg4.0.6编译产物

include 包含的是头文件,lib 是库文件,share 则是一些示例代码。

我们将它们打包准备从 Linux 服务器上下载到本地:

[root@frank output]# zip -r ffmpeg.zip ./*

3、为 AS 配置 FFmpeg

在本地解压上一步得到的 ffmpeg.zip,将 include 和 lib 文件夹内的文件拷贝到 Android Studio 的项目中,具体步骤如下:

  1. 将 include 文件夹拷贝到 /src/main/cpp 目录下

  2. 将 lib 文件夹内的 6 个静态库(.a 结尾)文件拷贝到 /src/main/cpp/armeabi-v7a 目录下(因为我们后续要在 CPU 架构为 armeabi-v7a 的真机上使用这些静态库)

  3. 配置 CMakeLists 文件:

    # 1、引入头文件
    include_directories(${CMAKE_SOURCE_DIR}/include)
    
    # 2、引入库文件
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -L${CMAKE_SOURCE_DIR}/${CMAKE_ANDROID_ARCH_ABI}")
    
    find_library(
            log-lib
            log)
    
    # 3、链接 FFmpeg 的静态库到目标库,注意顺序,比如 avformat 要在
    # avcodec 的前面
    target_link_libraries(
            ffmpeg
            ${log-lib}
            avformat avcodec avfilter avutil swresample swscale)
    

    配置 target_link_libraries 时要注意 6 个静态库的顺序,因为 avcodec 是依赖 avformat 的,被依赖的库要写在前面。如果想排除掉顺序影响,可以写成下面这样:

    target_link_libraries(
            ffmpeg
            ${log-lib}
            -Wl,--start-group
            avformat avcodec avfilter avutil swresample swscale
            -Wl,--end-group)
    
  4. 配置 build.gradle 添加 CPU 架构过滤:

    android {
        defaultConfig {
    		externalNativeBuild {
                cmake {
                    abiFilters 'armeabi-v7a'
                }
            }
            ndk {
                abiFilters 'armeabi-v7a'
            }
        }
    }
    
  5. 最后调用 avutil 库中的函数,输出 FFmpeg 的版本号,测试配置是否成功:

    // 因为 FFmpeg 是用纯 C 写的,所以要用 C 的方式编译
    extern "C" {
    #include <libavutil/avutil.h>
    }
    
    extern "C" JNIEXPORT jstring JNICALL
    Java_com_linux_ffmpeg_NativeLib_stringFromJNI(
            JNIEnv *env,
            jobject /* this */) {
        std::string hello = "FFmpeg 版本:";
        hello.append(av_version_info());
        return env->NewStringUTF(hello.c_str());
    }
    

    调用 JNI 函数,我写在了 Android 单元测试中:

    @RunWith(AndroidJUnit4::class)
    class ExampleInstrumentedTest {
        @Test
        fun test() {
            // Logcat 输出 test: FFmpeg 版本:4.0.6
            Log.d("Test", "test: ${NativeLib().stringFromJNI()}")
        }
    }