• NDK 是什么 | FFmpeg 5.0 编译 so 库


    前言

    NDK 全称 Native Development Kit,也就是原生开发工具包 ,官网对它有详细的 中文介绍 。可能一说到 NDKJNI ,大家脑子里第一反应就是集成 C/C++ 。其实 JNI 的含义是 Java Native Interface ,这种接口允许 Java 和其他语言进行交互的,包括但不限于 C/C++ 。目前 Rust 也可以通过 JNI 来和 Java 交互,虽然不太成熟。

    其实 NDK 更像一个桥梁,来连通 Java 和其他语言,它是一系列工具的集合。既然作为工具, NDK 并非必须在 Android 项目中才能用。本文我们来通过 NDKFFmpeg 5.0 进行编译,生成动态链接库 so

    注:本文的 Java 泛指 JVM 语言,不要拿 Kotlin 抬杠,本质太大的区别 。本文测试项目源码地址【TolyFFmpeg】


    一、环境准备

    想要编译 FFmpeg 应用 Android 中的动态链接库,我们要准备两个东西:一者是 FFmpeg 的源码;二者是 NDK 的工具包。这两者都可以通过简单的下载获得。


    1、FFmpeg 源码下载:5.0.1

    作为一个开源项目,想得到源码还是非常简单的。可以在官网直接下载源码,也可以通过 git 来下载,或者点击More releases 来选择某个版本进行下载。 ffmpeg.org/download.ht…


    源码解压如下,里面的 doc 文件夹有些文档和案例,还是比较有用的。其余的东西暂时对我们来说并没有什么太大的意义,现在我们的目的是通过这个源码通过 NDK 来编译成在 Android 中可以使用的动态链接库 so 文件。

    可能会有人疑惑,那就是 so 库嘛,下载别人的用不就完事了吗?原因很简单,自己编译 FFmpeg 可以手动设置需要的功能,如果直接别人编译好的,就没有设置的机会。而且自己编译也能掌握版本,也就是说,自己动手丰衣足食。


    2、下载 NDK :r24

    可以在如下网站中下载 NDK 的工具包,不过在 macOS 中更推荐用 Android SDK 管理器来下载,如下在 AndroidStudio 中选择 NDK 点击 OK 下载即可。这里下载的是最新版 r24 (24.0.8215888)

    developer.android.google.cn/ndk/downloa…


    下载过后 你的AndroidSDK/ndk/24.0.8215888 会议相关文件,说明 NDK 环境准备就绪。


    二、编译 FFmpeg

    编译 FFmpeg ,只要是使用 ndk 中的编译根据,在 $ndkPath/toolchains/llvm/prebuilt/ 下,不同平台的文件名不同,比如 macOS 中是 darwin-x86_64


    1.编译脚本

    编译脚本参考: 《使用Android Studio开发FFmpeg的正确姿势》

    亲测该脚本在 r24 + 5.0.1 是可用的,使用时注意 tag1tag2 处。

    #!/bin/bash
    # 用于编译android平台的脚本
    ​
    # NDK所在目录
    NDK_PATH=/Users/mac/Coder/SDK/AndroidSDK/ndk/24.0.8215888/ # tag1
    # macOS 平台编译,其他平台看一下 $NDK_PATH/toolchains/llvm/prebuilt/ 下的文件夹名称
    HOST_PLATFORM=darwin-x86_64  #tag1
    # minSdkVersion
    API=23
    ​
    TOOLCHAINS="$NDK_PATH/toolchains/llvm/prebuilt/$HOST_PLATFORM"
    SYSROOT="$NDK_PATH/toolchains/llvm/prebuilt/$HOST_PLATFORM/sysroot"
    # 生成 -fpic 与位置无关的代码
    CFLAG="-D__ANDROID_API__=$API -Os -fPIC -DANDROID "
    LDFLAG="-lc -lm -ldl -llog "
    ​
    # 输出目录
    PREFIX=`pwd`/android-build
    # 日志输出目录
    CONFIG_LOG_PATH=${PREFIX}/log
    # 公共配置
    COMMON_OPTIONS=
    # 交叉配置
    CONFIGURATION=
    ​
    build() {
      APP_ABI=$1
      echo "======== > Start build $APP_ABI"
      case ${APP_ABI} in
      armeabi-v7a)
        ARCH="arm"
        CPU="armv7-a"
        MARCH="armv7-a"
        TARGET=armv7a-linux-androideabi
        CC="$TOOLCHAINS/bin/$TARGET$API-clang"
        CXX="$TOOLCHAINS/bin/$TARGET$API-clang++"
        LD="$TOOLCHAINS/bin/$TARGET$API-clang"
        # 交叉编译工具前缀
        CROSS_PREFIX="$TOOLCHAINS/bin/arm-linux-androideabi-"
        EXTRA_CFLAGS="$CFLAG -mfloat-abi=softfp -mfpu=vfp -marm -march=$MARCH "
        EXTRA_LDFLAGS="$LDFLAG"
        EXTRA_OPTIONS="--enable-neon --cpu=$CPU "
        ;;
      arm64-v8a)
        ARCH="aarch64"
        TARGET=$ARCH-linux-android
        CC="$TOOLCHAINS/bin/$TARGET$API-clang"
        CXX="$TOOLCHAINS/bin/$TARGET$API-clang++"
        LD="$TOOLCHAINS/bin/$TARGET$API-clang"
        CROSS_PREFIX="$TOOLCHAINS/bin/$TARGET-"
        EXTRA_CFLAGS="$CFLAG"
        EXTRA_LDFLAGS="$LDFLAG"
        EXTRA_OPTIONS=""
        ;;
      x86)
        ARCH="x86"
        CPU="i686"
        MARCH="i686"
        TARGET=i686-linux-android
        CC="$TOOLCHAINS/bin/$TARGET$API-clang"
        CXX="$TOOLCHAINS/bin/$TARGET$API-clang++"
        LD="$TOOLCHAINS/bin/$TARGET$API-clang"
        CROSS_PREFIX="$TOOLCHAINS/bin/$TARGET-"
        #EXTRA_CFLAGS="$CFLAG -march=$MARCH -mtune=intel -mssse3 -mfpmath=sse -m32"
        EXTRA_CFLAGS="$CFLAG -march=$MARCH  -mssse3 -mfpmath=sse -m32"
        EXTRA_LDFLAGS="$LDFLAG"
        EXTRA_OPTIONS="--cpu=$CPU "
        ;;
      x86_64)
        ARCH="x86_64"
        CPU="x86-64"
        MARCH="x86_64"
        TARGET=$ARCH-linux-android
        CC="$TOOLCHAINS/bin/$TARGET$API-clang"
        CXX="$TOOLCHAINS/bin/$TARGET$API-clang++"
        LD="$TOOLCHAINS/bin/$TARGET$API-clang"
        CROSS_PREFIX="$TOOLCHAINS/bin/$TARGET-"
        #EXTRA_CFLAGS="$CFLAG -march=$CPU -mtune=intel -msse4.2 -mpopcnt -m64"
        EXTRA_CFLAGS="$CFLAG -march=$CPU -msse4.2 -mpopcnt -m64"
        EXTRA_LDFLAGS="$LDFLAG"
        EXTRA_OPTIONS="--cpu=$CPU "
        ;;
      esac
    ​
      echo "-------- > Start clean workspace"
      make clean
    ​
      echo "-------- > Start build configuration"
      CONFIGURATION="$COMMON_OPTIONS"
      CONFIGURATION="$CONFIGURATION --logfile=$CONFIG_LOG_PATH/config_$APP_ABI.log"
      CONFIGURATION="$CONFIGURATION --prefix=$PREFIX"
      CONFIGURATION="$CONFIGURATION --libdir=$PREFIX/libs/$APP_ABI"
      CONFIGURATION="$CONFIGURATION --incdir=$PREFIX/includes/$APP_ABI"
      CONFIGURATION="$CONFIGURATION --pkgconfigdir=$PREFIX/pkgconfig/$APP_ABI"
      CONFIGURATION="$CONFIGURATION --cross-prefix=$CROSS_PREFIX"
      CONFIGURATION="$CONFIGURATION --arch=$ARCH"
      CONFIGURATION="$CONFIGURATION --sysroot=$SYSROOT"
      CONFIGURATION="$CONFIGURATION --cc=$CC"
      CONFIGURATION="$CONFIGURATION --cxx=$CXX"
      CONFIGURATION="$CONFIGURATION --ld=$LD"
      # nm 和 strip
      CONFIGURATION="$CONFIGURATION --nm=$TOOLCHAINS/bin/llvm-nm"
      CONFIGURATION="$CONFIGURATION --strip=$TOOLCHAINS/bin/llvm-strip"
      CONFIGURATION="$CONFIGURATION $EXTRA_OPTIONS"
    ​
      echo "-------- > Start config makefile with $CONFIGURATION --extra-cflags=${EXTRA_CFLAGS} --extra-ldflags=${EXTRA_LDFLAGS}"
      ./configure ${CONFIGURATION} \
      --extra-cflags="$EXTRA_CFLAGS" \
      --extra-ldflags="$EXTRA_LDFLAGS"
    ​
      echo "-------- > Start make $APP_ABI with -j1"
      make -j1
    ​
      echo "-------- > Start install $APP_ABI"
      make install
      echo "++++++++ > make and install $APP_ABI complete."
    ​
    }
    ​
    build_all() {
      #配置开源协议声明
      COMMON_OPTIONS="$COMMON_OPTIONS --enable-gpl"
      #目标android平台
      COMMON_OPTIONS="$COMMON_OPTIONS --target-os=android"
      #取消默认的静态库
      COMMON_OPTIONS="$COMMON_OPTIONS --disable-static"
      COMMON_OPTIONS="$COMMON_OPTIONS --enable-shared"
      COMMON_OPTIONS="$COMMON_OPTIONS --enable-protocols"
      #开启交叉编译
      COMMON_OPTIONS="$COMMON_OPTIONS --enable-cross-compile"
      COMMON_OPTIONS="$COMMON_OPTIONS --enable-optimizations"
      COMMON_OPTIONS="$COMMON_OPTIONS --disable-debug"
      #尽可能小
      COMMON_OPTIONS="$COMMON_OPTIONS --enable-small"
      COMMON_OPTIONS="$COMMON_OPTIONS --disable-doc"
      #不要命令(执行文件)
      COMMON_OPTIONS="$COMMON_OPTIONS --disable-programs"  # do not build command line programs
      COMMON_OPTIONS="$COMMON_OPTIONS --disable-ffmpeg"    # disable ffmpeg build
      COMMON_OPTIONS="$COMMON_OPTIONS --disable-ffplay"    # disable ffplay build
      COMMON_OPTIONS="$COMMON_OPTIONS --disable-ffprobe"   # disable ffprobe build
      COMMON_OPTIONS="$COMMON_OPTIONS --disable-symver"
      COMMON_OPTIONS="$COMMON_OPTIONS --disable-network"
      COMMON_OPTIONS="$COMMON_OPTIONS --disable-x86asm"
      COMMON_OPTIONS="$COMMON_OPTIONS --disable-asm"
      #启用
      COMMON_OPTIONS="$COMMON_OPTIONS --enable-pthreads"
      COMMON_OPTIONS="$COMMON_OPTIONS --enable-mediacodec"
      COMMON_OPTIONS="$COMMON_OPTIONS --enable-jni"
      COMMON_OPTIONS="$COMMON_OPTIONS --enable-zlib"
      COMMON_OPTIONS="$COMMON_OPTIONS --enable-pic"
      COMMON_OPTIONS="$COMMON_OPTIONS --enable-muxer=flv"
      #COMMON_OPTIONS="$COMMON_OPTIONS --enable-avresample"
      COMMON_OPTIONS="$COMMON_OPTIONS --enable-decoder=h264"
      COMMON_OPTIONS="$COMMON_OPTIONS --enable-decoder=mpeg4"
      COMMON_OPTIONS="$COMMON_OPTIONS --enable-decoder=mjpeg"
      COMMON_OPTIONS="$COMMON_OPTIONS --enable-decoder=png"
      COMMON_OPTIONS="$COMMON_OPTIONS --enable-decoder=vorbis"
      COMMON_OPTIONS="$COMMON_OPTIONS --enable-decoder=opus"
      COMMON_OPTIONS="$COMMON_OPTIONS --enable-decoder=flac"
      echo "COMMON_OPTIONS=$COMMON_OPTIONS"
      echo "PREFIX=$PREFIX"
      echo "CONFIG_LOG_PATH=$CONFIG_LOG_PATH"
      mkdir -p ${CONFIG_LOG_PATH}
      build "armeabi-v7a"
      build "arm64-v8a"
      build "x86"
      build "x86_64"
    }
    ​
    echo "-------- Start --------"
    build_all
    echo "-------- End --------"
    复制代码

    2. 使用脚本

    把上面的脚本写在 build_android.sh 中,放在 ffmpeg 源码根目录下。

    进入到 ffmpeg 源码目录,执行如下命令,等待即可。

    chmod +x build_android.sh
    ./build_android.sh
    复制代码

    如下在当前文件夹下会生成 android-build 文件夹,其中 libs 文件夹中盛放着各种架构的 so 库,includes 文件夹中盛放着各种架构的头文件。如果不想编译处某种架构的,在 build_android.sh 的末尾处注释即可。

    mac@macdeMacBook-Pro android-build % tree -L 2
    .
    ├── includes
    │   ├── arm64-v8a
    │   ├── armeabi-v7a
    │   ├── x86
    │   └── x86_64
    ├── libs
    │   ├── arm64-v8a
    │   ├── armeabi-v7a
    │   ├── x86
    │   └── x86_64
    └── share
        └── ffmpeg
    复制代码

    从这里可以看出,把 FFmpeg 源码编译成 so 动态链接库,是 NDK 的功劳。其实在 Android 开发中,NDK 的作用也是如此,核心价值也是把其他语言编译成Android 平台可以访问的 so 而已。所以也不要觉得 NDK 有多么神秘,就是一个工具集而已。


    三、Android 中集成 FFmpeg

    AndroidStudio 中选择创建一个 Native C++ 的项目。其实这也不是必须的,普通项目也可以通过配置来支持 C++


    1. app 下的 build.gradle 修改建议

    最好在 app/build.gradle 中指定 NDK 的版本,否则可能会下载其他版本的 NDK 而浪费时间。

    android {
      ndkVersion "24.0.8215888"
      
      sourceSets {
        main {
            jniLibs.srcDirs = ['jniLibs']
        }
      }
      //...  
    }
    复制代码

    另外添加 jniLibs.srcDirs 的指向为了解决下面的异常,而且 jniLibs.srcDirs 指向什么目录都无所谓,但不加引入 so 时就会报错。官网说是 jniLibs 已经默认成为了目录,不需要指定 jniLibs.srcDirs ,但这里感觉莫名其妙,必须要指一下。


    2. 项目结构

    cpp 文件夹中处理 c++ 相关内容,jniLibs 文件夹放入文件编译的 so 库:


    3. CMakeLists.txt 书写

    CMakeLists 是构建的脚本,这里先使用 avcodec 打印一下配置信息,不过 ffmpeg 5.0 好像 avcodec 依赖了 swresampleavutil 模块。这里也需要添加一些,记得 4.2.7 的时候还不需要。

    cmake_minimum_required(VERSION 3.18.1)
    ​
    project("tolyffmpeg")
    ​
    #引入头文件
    include_directories(includes)
    # 定义当前 so 库 - 在 java 代码中加载
    add_library(tolyffmpeg SHARED native-lib.cpp)
    ​
    # 添加 ffmpeg 的 avcodec、swresample、avutil 模块 start======
    set(distribution_DIR ${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI})
    ​
    add_library(avcodec SHARED IMPORTED)
    set_target_properties(avcodec PROPERTIES IMPORTED_LOCATION ${distribution_DIR}/libavcodec.so)
    ​
    add_library(swresample SHARED IMPORTED)
    set_target_properties(swresample PROPERTIES IMPORTED_LOCATION ${distribution_DIR}/libswresample.so)
    ​
    add_library(avutil SHARED IMPORTED)
    set_target_properties(avutil PROPERTIES IMPORTED_LOCATION ${distribution_DIR}/libavutil.so)
    # 添加 ffmpeg 的 avcodec、swresample、avutil 模块 end======
    ​
    find_library(log-lib log)
    target_link_libraries(
            tolyffmpeg
            avcodec
            swresample
            avutil
            ${log-lib})
    复制代码

    4. 构建产物

    点击小锤子,可以在 build 中看到一些构建产物,其中的 so 只会包含引入的相关模块:


    默认情况下四种架构都会构建,可以在 app/build.gradle 中指定只构建哪些,比较支持的架构越多,应用体积越大。如下所示:

    android {
        defaultConfig {
            externalNativeBuild {
                cmake {
                    abiFilters 'armeabi-v7a', 'arm64-v8a'
                }
            }
        }
    复制代码

    5. C++ 代码修改和运行结果

    如下代码,引入了 libavcodec/avcodec.h 头文件,使用其中的 avcodec_configuration 方法获取信息,进行返回。

    ---->[src/main/cpp/native-lib.cpp]----
    #include 
    #include 
    extern "C"{
    #include 
    }
    extern "C" JNIEXPORT jstring JNICALL
    Java_com_toly1994_tolyffmpeg_MainActivity_stringFromJNI(
            JNIEnv* env,
            jobject /* this */) {
        std::string hello = "Hello from C++";
        return env->NewStringUTF(avcodec_configuration());
    }
    复制代码

    如下就是编译时的配置信息,通过 C++ 获取,返回给 Java 端,进行显示。


    四、小结

    这就是最基本的利用 NDK 编译 FFmpeg 动态链接库。其实仔细想想,项目中的 C++ 文件也是被 NDK 编译成 libtolyffmpeg.so 库,才能被 Java 所调用。

    最后用官网的几句话收尾:Android NDK 是一组使您能将 C 或 C++(“原生代码”)嵌入到 Android 应用中的工具。 NDK 将 C 和 C++ 代码编译到原生库中,然后使用 Android Studio 的集成构建系统 Gradle 将原生库打包到 APK 中。Java 代码随后可以通过 Java 原生接口 (JNI) 框架调用原生库中的函数。

  • 相关阅读:
    【分布式】分布式锁解决方案介绍、DBMS级别乐观、悲观、redis的SETNX实现分布式锁
    学术论文写作
    二次型的概念
    微信小程序修改vant组件样式
    混凝土粉末
    lattice crosslink开发板mipi核心板csi测试dsi屏lif md6000 fpga
    (复刷) 面试题02.07.链表相交
    Redis应用案例之优惠券秒杀
    Zookeeper中的watch机制
    HTIML知识概述
  • 原文地址:https://blog.csdn.net/yinshipin007/article/details/128045455