Android交叉编译OpenCV+FFmpeg+x264的艰难历程

847 阅读11分钟

前言

如果你没有兴趣看完本文,只想获得可编译的代码或编译后的产物,可以直接点击下面的链接,跟随步骤编译代码或直接下载我编译好的产物

注:编译顺序要按照 x264 -> FFmpeg -> OpenCV 这样来

x264

FFmpeg

OpenCV

起因

最近在做一个视频生成的app,使用OpenCV库实现,用的是C语言,一开始我是在mac_x86上书写代码,fourcc视频编码器选择的是mp4v,视频输出一切正常,然后当我将代码移植到Android上时发现,从OpenCV官网下载的so库它不支持编码mp4v格式,只能编码成mjpg格式,后缀名为avi,尴尬的是Android原生又不支持播放这种格式的视频,所以要想办法让OpenCV支持编码mp4vh264等格式

我在网上搜索了一下为什么OpenCV默认不支持h264格式,得知OpenCV默认使用FFmpeg做视频处理,FFmpeg使用的是LGPL协议,而x264使用的是GPL协议,GPL协议具有传染性,如果代码中使用了GPL协议的软件,则要求你的代码也必须开源。我猜测是因为这个原因,FFmpeg默认不使用GPL协议的软件,避免产生一些不必要的问题和纠纷,如果想要使用GPL协议的软件,则需要在编译的时候加上--enable-gpl选项

基于此上原因,我开启了我艰难的编译之路

声明

本篇文章只针对Linux系统编译,其他系统不保证可以编译通过

本篇文章使用的NDK版本为21.4.7075529,不同的版本可能会有些差别,需要自行调整

本人对c/c++编译这块并不是很了解,很多东西也是边学习边尝试的,如果有什么错误的话也恳请大佬们指正,谢谢

准备

准备一台Linux系统的电脑或使用虚拟机,安装一些最基本的编译工具(makecmake等),我使用的是Ubuntu系统,强烈建议在安装的时候选择完整安装,这样这些编译工具应该都会跟随系统自动安装好

Android交叉编译肯定是需要NDK的,我使用的是21.4.7075529版本,r19以上版本的NDK都是直接自带了工具链,而r19之前的版本则需要先生成工具链,具体可以参考独立工具链(已弃用)这篇文档

x264

既然需要依赖x264,那我们肯定是先要编译x264库,各位可以clone我准备好的tag

git clone -b v0.164_compilable https://github.com/dreamgyf/x264.git

这个版本是从原x264镜像仓库的stable分支切出的,版本为0.164。想知道x264版本的话,可以运行其目录下的version.sh脚本,它会输出三串数字,前面的164是在x264.h中定义的X264_BUILD,第二个3095+4表示master分支的提交数 + master分支到HEAD的提交数,最后的一串数字表示当前分支最新的commit id

在构建编译脚本之前,我们先要看看这个库提供了哪些编译选项,我们可以看到在x264根目录下有一个configure文件,这是一个脚本文件,大多数库都提供了这个脚本,用来负责生成Makefile,准备好构建环境,我们可以通过下面这个命令获取帮助文件

./configure --help > help.txt

可以看到,里面提供了一些编译选项及其描述,我们可以根据这些选项和描述构建编译脚本

先看一下我写好的脚本吧

# Linux 交叉编译 Android 库脚本
if [[ -z $ANDROID_NDK ]]; then
    echo 'Error: Can not find ANDROID_NDK path.'
    exit 1
fi

echo "ANDROID_NDK path: ${ANDROID_NDK}"

OUTPUT_DIR="_output_"

mkdir ${OUTPUT_DIR}
cd ${OUTPUT_DIR}

OUTPUT_PATH=`pwd`

API=21
TOOLCHAIN=$ANDROID_NDK/toolchains/llvm/prebuilt/linux-x86_64

function build {
    ABI=$1

    if [[ $ABI == "armeabi-v7a" ]]; then
        ARCH="arm"
        TRIPLE="armv7a-linux-androideabi"
    elif [[ $ABI == "arm64-v8a" ]]; then
        ARCH="arm64"
        TRIPLE="aarch64-linux-android"
    elif [[ $ABI == "x86" ]]; then
        ARCH="x86"
        TRIPLE="i686-linux-android"
    elif [[ $ABI == "x86-64" ]]; then
        ARCH="x86_64"
        TRIPLE="x86_64-linux-android"
    else
        echo "Unsupported ABI ${ABI}!"
        exit 1
    fi

    echo "Build ABI ${ABI}..."

    rm -rf ${ABI}
    mkdir ${ABI} && cd ${ABI}

    PREFIX=${OUTPUT_PATH}/product/$ABI

    export CC=$TOOLCHAIN/bin/${TRIPLE}${API}-clang
    export CFLAGS="-g -DANDROID -fdata-sections -ffunction-sections -funwind-tables -fstack-protector-strong -no-canonical-prefixes -D_FORTIFY_SOURCE=2 -Wformat -Werror=format-security  -O0 -DNDEBUG  -fPIC --gcc-toolchain=$TOOLCHAIN --target=${TRIPLE}${API}"

    ../../configure \
        --host=${TRIPLE} \
        --prefix=$PREFIX \
        --enable-static \
        --enable-shared \
        --enable-pic \
        --disable-lavf \
        --sysroot=$TOOLCHAIN/sysroot

    make clean && make -j`nproc` && make install

    cd ..
}

echo "Select arch:"
select arch in "armeabi-v7a" "arm64-v8a" "x86" "x86-64"
do
    build $arch
    break
done

这也是我其他库编译脚本的基本结构,首先需要ANDROID_NDK环境变量用来确定NDK的位置

OUTPUT_DIR为编译的输出路径,我这里命名为_output_,防止和源码本身的目录重名

API为最低支持的Android API版本,我这里写的21,也就是Android 5.0

TOOLCHAIN为交叉编译工具链的路径,对于r19之前的NDK,需要将其改为你生成出来的工具链的路径,r19之后不需要改动

我这里定义了一个build函数,通过输入的ABI编译出对应架构的产物。ABI总共有四种:armeabi-v7aarm64-v8ax86x86-64,这个决定你的App能在哪些平台架构上运行

这里,我通过不同的ABI定义了不同的TRIPLE变量,这是遵循了NDK工具链的命名规则,可以在 将 NDK 与其他构建系统配合使用 这篇文档中找到

TRIPLE

$TOOLCHAIN/bin目录下,我们也能发现这种命名方式

TRIPLE

我们需要根据其命名规则,指定相应的编译器,设置相应的hosttarget

关于buildhosttarget的含义可以参阅 Cross-Compilation 这篇文档

  • build: 编译该库所使用的平台,不设置的话,编译器会自动推测所在平台

  • host: 编译出的库要运行在哪个平台上,不设置的话,默认为build值,但这样也就不再是交叉编译了

  • target: 该库所处理的目标平台,不设置的话,默认为host

多数UNIX平台会通过CC调用C语言编译器,而CFLAGS则是C语言编译器的编译选项,根据我们上文所说的命名规则可以发现,工具链中C语言编译器的命名规则为${TRIPLE}${API}-clang,假设我们要编译arm64-v8a ABIAPI 21的库,则需要指定CCaarch64-linux-android21-clang

至于CFLAGS这里就不多说了,可以自行查阅 Clang编译器参数手册 ,这里需要注意的是,必须要指定--gcc-toolchain--target,否则编译会报错

然后就是configure的选项了,这里必须指定--host--sysrootsysroot表示使用这个值作为编译的头文件和库文件的查找目录,该目录结构如下

sysroot
└── usr
    ├── include
    └── lib
        ├── aarch64-linux-android
        ├── arm-linux-androideabi
        ├── i686-linux-android
        └── x86_64-linux-android

--prefix为编译后的安装路径,也就是编译产物的输出路径

--enable-static--enable-shared选项表示生成静态库和动态库,大家可以根据情况自行选择

nprocLinux下的一个命令,表示当前进程可用的CPU核数,一般make使用线程数为CPU核数就可以了,如果编译产生问题,可以尝试调小这个值

到这里基本上整个构建脚本就分析完了,大家调整完编译选项后保存,就可以执行命令./build.sh开始编译了

FFmpeg

然后我们开始编译FFmpeg

git clone -b v5.0_compilable https://github.com/dreamgyf/FFmpeg.git

这个版本是从原FFmpeg镜像仓库的n5.0分支切出的,版本为5.0。其实我一开始用的是5.1版本,但当我解决了各种问题编译OpenCV到一半时,提示我FFmpeg的一些符号找不到,然后我去查了一下OpenCV的 Change Log ,发现它的最新版本4.6.0刚刚支持FFmpeg 5.0版本,无奈切到5.0重新编译

还是一样,先看编译脚本

# Linux 交叉编译 Android 库脚本
if [[ -z $ANDROID_NDK ]]; then
    echo 'Error: Can not find ANDROID_NDK path.'
    exit 1
fi

echo "ANDROID_NDK path: ${ANDROID_NDK}"

ROOT_PATH=`pwd`

OUTPUT_DIR="_output_"

mkdir ${OUTPUT_DIR}
cd ${OUTPUT_DIR}

OUTPUT_PATH=`pwd`

API=21
TOOLCHAIN=$ANDROID_NDK/toolchains/llvm/prebuilt/linux-x86_64
# 编译出的x264库地址
X264_ANDROID_DIR=/home/dreamgyf/compile/x264/_output_/product

EXTRA_CONFIGURATIONS="--disable-stripping \
    --disable-ffmpeg \
    --disable-doc \
    --disable-appkit \
    --disable-avfoundation \
    --disable-coreimage \
    --disable-amf \
    --disable-audiotoolbox \
    --disable-cuda-llvm \
    --disable-cuvid \
    --disable-d3d11va \
    --disable-dxva2 \
    --disable-ffnvcodec \
    --disable-nvdec \
    --disable-nvenc \
    --disable-vdpau \
    --disable-videotoolbox"

function build {
    ABI=$1

    if [[ $ABI == "armeabi-v7a" ]]; then
        ARCH="arm"
        TRIPLE="armv7a-linux-androideabi"
    elif [[ $ABI == "arm64-v8a" ]]; then
        ARCH="arm64"
        TRIPLE="aarch64-linux-android"
    elif [[ $ABI == "x86" ]]; then
        ARCH="x86"
        TRIPLE="i686-linux-android"
    elif [[ $ABI == "x86-64" ]]; then
        ARCH="x86_64"
        TRIPLE="x86_64-linux-android"
    else
        echo "Unsupported ABI ${ABI}!"
        exit 1
    fi

    echo "Build ABI ${ABI}..."

    rm -rf ${ABI}
    mkdir ${ABI} && cd ${ABI}

    PREFIX=${OUTPUT_PATH}/product/$ABI

    export CC=$TOOLCHAIN/bin/${TRIPLE}${API}-clang
    export CFLAGS="-g -DANDROID -fdata-sections -ffunction-sections -funwind-tables -fstack-protector-strong -no-canonical-prefixes -D_FORTIFY_SOURCE=2 -Wformat -Werror=format-security  -O0 -DNDEBUG  -fPIC --gcc-toolchain=$TOOLCHAIN --target=${TRIPLE}${API}"

    ../../configure \
        --prefix=$PREFIX \
        --enable-cross-compile \
        --sysroot=$TOOLCHAIN/sysroot \
        --cc=$CC \
        --enable-static \
        --enable-shared \
        --disable-asm \
        --enable-gpl \
        --enable-libx264 \
        --extra-cflags="-I${X264_ANDROID_DIR}/${ABI}/include" \
        --extra-ldflags="-L${X264_ANDROID_DIR}/${ABI}/lib" \
        $EXTRA_CONFIGURATIONS

    make clean && make -j`nproc` && make install

    cd $PREFIX
    `$ROOT_PATH/ffmpeg-config-gen.sh ${X264_ANDROID_DIR}/${ABI}/lib/libx264.a`
    cd $OUTPUT_PATH
}

echo "Select arch:"
select arch in "armeabi-v7a" "arm64-v8a" "x86" "x86-64"
do
    build $arch
    break
done

这个脚本和x264的编译脚本基本相同,由于我们需要依赖x264库,所以我们要使刚刚编译出来的x264产物参与FFmpeg的编译,为此,需要将X264_ANDROID_DIR改成自己编译出来的x264产物路径

configure选项中,我们需要--enable-cross-compile选项表示开启交叉编译,我们这里需要设置--cc选择C语言编译器,否则编译时会使用系统默认的编译器,--disable-asm选项我测试是必须要带上的,否则编译会报错,然后就是--enable-libx264开启x264依赖了,根据我在起因中说到的开源协议问题,所以--enable-gpl选项也要开启,最后需要指定x264的头文件和库文件目录,分别使用--extra-cflags--extra-ldflags加上对应的参数

这里提一下,编译器会优先从-I -L两个参数指定的目录中去查找头文件和库文件,如果没找到的话再会从sysroot目录中查找

最后,我还写了一个ffmpeg-config-gen.sh脚本,它的作用是生成ffmpeg-config.cmake文件,用来给OpenCV编译提供FFmpeg依赖查找,这个等我们后面讲到OpenCV依赖FFmpeg的处理时再说

x264一样,大家调整完编译选项后保存,就可以执行命令./build.sh开始编译了

OpenCV

最后,我们开始编译OpenCV

git clone -b v4.6.0_compilable https://github.com/dreamgyf/opencv.git

这个版本是从原OpenCV仓库的4.6.0分支切出的,版本为4.6.0,是目前的最新版本。其实前面两个库的编译都挺顺利的,最麻烦的问题都出在OpenCV这里

我们还是先看编译脚本

# Linux 交叉编译 Android 库脚本
if [[ -z $ANDROID_NDK ]]; then
    echo 'Error: Can not find ANDROID_NDK path.'
    exit 1
fi

echo "ANDROID_NDK path: ${ANDROID_NDK}"

OUTPUT_DIR="_output_"

mkdir ${OUTPUT_DIR}
cd ${OUTPUT_DIR}

OUTPUT_PATH=`pwd`

API=21
TOOLCHAIN=$ANDROID_NDK/toolchains/llvm/prebuilt/linux-x86_64
# 编译出的ffmpeg库地址
FFMPEG_ANDROID_DIR=/home/dreamgyf/compile/FFmpeg/_output_/product

EXTRA_ATTRS="-DWITH_CUDA=OFF \
    -DWITH_GTK=OFF \
    -DWITH_1394=OFF \
    -DWITH_GSTREAMER=OFF \
    -DWITH_LIBV4L=OFF \
    -DWITH_TIFF=OFF \
    -DBUILD_OPENEXR=OFF \
    -DWITH_OPENEXR=OFF \
    -DBUILD_opencv_ocl=OFF \
    -DWITH_OPENCL=OFF"

function build {
    ABI=$1

    if [[ $ABI == "armeabi-v7a" ]]; then
        ARCH="arm"
        TRIPLE="armv7a-linux-androideabi"
        TOOLCHAIN_NAME="arm-linux-androideabi"
    elif [[ $ABI == "arm64-v8a" ]]; then
        ARCH="arm64"
        TRIPLE="aarch64-linux-android"
        TOOLCHAIN_NAME="aarch64-linux-android"
    elif [[ $ABI == "x86" ]]; then
        ARCH="x86"
        TRIPLE="i686-linux-android"
        TOOLCHAIN_NAME="i686-linux-android"
    elif [[ $ABI == "x86-64" ]]; then
        ARCH="x86_64"
        TRIPLE="x86_64-linux-android"
        TOOLCHAIN_NAME="x86_64-linux-android"
    else
        echo "Unsupported ABI ${ABI}!"
        exit 1
    fi

    echo "Build ABI ${ABI}..."

    rm -rf ${ABI}
    mkdir ${ABI} && cd ${ABI}

    PREFIX=${OUTPUT_PATH}/product/$ABI

    cmake ../.. \
        -DCMAKE_INSTALL_PREFIX=$PREFIX \
        -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK/build/cmake/android.toolchain.cmake \
        -DANDROID_ABI=$ABI \
        -DANDROID_NDK=$ANDROID_NDK \
        -DANDROID_PLATFORM="android-${API}" \
        -DANDROID_LINKER_FLAGS="-Wl,-rpath-link=$TOOLCHAIN/sysroot/usr/lib/$TOOLCHAIN_NAME/$API" \
        -DBUILD_ANDROID_PROJECTS=OFF \
        -DBUILD_ANDROID_EXAMPLES=OFF \
        -DBUILD_SHARED_LIBS=$BUILD_SHARED_LIBS \
        -DWITH_FFMPEG=ON \
        -DOPENCV_GENERATE_PKGCONFIG=ON \
        -DOPENCV_FFMPEG_USE_FIND_PACKAGE=ON \
        -DFFMPEG_DIR=${FFMPEG_ANDROID_DIR}/${ABI} \
        $EXTRA_ATTRS

    make clean && make -j`nproc` && make install

    cd ..
}

echo "Select arch:"
select arch in "armeabi-v7a" "arm64-v8a" "x86" "x86-64"
do
    echo "Select build static or shared libs:"
    select type in "static" "shared"
    do
        if [[ $type == "static" ]]; then
            BUILD_SHARED_LIBS=OFF
        elif [[ $type == "shared" ]]; then
            BUILD_SHARED_LIBS=ON
        else
            BUILD_SHARED_LIBS=OFF
        fi
        break
    done
    build $arch
    break
done

上面的准备工作和前面的几个脚本一样,不同的是,OpenCV并没有为我们准备configure脚本,所以这次我们使用cmake生成Makefile,再进行编译

既然使用cmake了,我们就可以不再像之前一样,自己指定编译器等工具链了,NDK为我们提供了交叉编译工具链cmake脚本$ANDROID_NDK/build/cmake/android.toolchain.cmake,我们只需要指定其为CMAKE_TOOLCHAIN_FILE,然后为其提供相关参数即可,具体的使用方式可以参考 CMake 这篇文档。我们这里只需要提供最低限度的几个参数ANDROID_ABIANDROID_NDKANDROID_PLATFORM即可

如果需要编译Android示例工程的话,还需要在环境变量中设置ANDROID_HOMEANDROID_SDK,我这里就直接使用-DBUILD_ANDROID_PROJECTS=OFF-DBUILD_ANDROID_EXAMPLES=OFF将其关闭了

然后就是如何让OpenCV依赖我们编译的FFmpeg的问题了,到这一步我们就需要去它的CMakeLists.txt中看看它是怎样声明FFmpeg的了

打开CMakeLists.txt文件,搜索FFMPEG关键字,我们可以找到这一段代码

if(WITH_FFMPEG OR HAVE_FFMPEG)
  if(OPENCV_FFMPEG_USE_FIND_PACKAGE)
    status("    FFMPEG:"       HAVE_FFMPEG         THEN "YES (find_package)"                       ELSE "NO (find_package)")
  elseif(WIN32)
    status("    FFMPEG:"       HAVE_FFMPEG         THEN "YES (prebuilt binaries)"                  ELSE NO)
  else()
    status("    FFMPEG:"       HAVE_FFMPEG         THEN YES ELSE NO)
  endif()
  status("      avcodec:"      FFMPEG_libavcodec_VERSION    THEN "YES (${FFMPEG_libavcodec_VERSION})"    ELSE NO)
  status("      avformat:"     FFMPEG_libavformat_VERSION   THEN "YES (${FFMPEG_libavformat_VERSION})"   ELSE NO)
  status("      avutil:"       FFMPEG_libavutil_VERSION     THEN "YES (${FFMPEG_libavutil_VERSION})"     ELSE NO)
  status("      swscale:"      FFMPEG_libswscale_VERSION    THEN "YES (${FFMPEG_libswscale_VERSION})"    ELSE NO)
  status("      avresample:"   FFMPEG_libavresample_VERSION THEN "YES (${FFMPEG_libavresample_VERSION})" ELSE NO)
endif()

我们可以发现,要想依赖FFmpeg,我们需要将HAVE_FFMPEG的值设为true,并且要指定FFmpeg libs的版本

我们再看到OPENCV_FFMPEG_USE_FIND_PACKAGE这个参数,表示通过find_package的方式寻找FFmpeg

这里,我们其实有两种办法依赖FFmpeg库,一是通过find_package,二是通过pkg-config,我两种方式都尝试了后,觉得还是使用find_package这种方式比较容易,侵入性较小,使用pkg-config需要手动修改OpenCV检测FFmpegcmake文件源码,不优雅

接着我们去看OpenCV是如何检测FFmpeg是否存在的,这里我们需要找到$OPENCV/modules/videoio/cmake/detect_ffmpeg.cmake这个文件,在开头第一段代码中,我们就可以发现

if(NOT HAVE_FFMPEG AND OPENCV_FFMPEG_USE_FIND_PACKAGE)
  if(OPENCV_FFMPEG_USE_FIND_PACKAGE STREQUAL "1" OR OPENCV_FFMPEG_USE_FIND_PACKAGE STREQUAL "ON")
    set(OPENCV_FFMPEG_USE_FIND_PACKAGE "FFMPEG")
  endif()
  find_package(${OPENCV_FFMPEG_USE_FIND_PACKAGE}) # Required components: AVCODEC AVFORMAT AVUTIL SWSCALE
  if(FFMPEG_FOUND OR FFmpeg_FOUND)
    set(HAVE_FFMPEG TRUE)
  endif()
endif()

如果OPENCV_FFMPEG_USE_FIND_PACKAGE选项被打开,则会使用find_package(FFMPEG)去查找这个库

find_package(<PackageName>)有两种模式,一种是Module模式,一种是Config模式

Module模式中,cmake需要找到一个名为Find<PackageName>.cmake的文件,这个文件负责找到库所在路径,引入头文件和库文件。cmake会在两个地方查找这个文件,先是我们手动指定的CMAKE_MODULE_PATH目录,搜索不到再搜索$CMAKE/share/cmake-<version>/Modules目录

如果Module模式没找到相应文件,则会转为Config模式,在这个模式下,cmake需要找到<lowercasePackageName>-config.cmake<PackageName>Config.cmake文件,通过这个文件找到库所在路径,引入头文件和库文件。cmake会优先在<PackageName>_DIR目录下搜索相应文件

关于find_package更详细的解释,可以去查看 官方文档

我这里选用了Config模式,再结合之前在CMakeLists.txtdetect_ffmpeg.cmake中的内容,我们可以得出结论:

我们需要在构建脚本中设置WITH_FFMPEG=ONOPENCV_FFMPEG_USE_FIND_PACKAGE=ONFFMPEG_DIR并且FFMPEG_DIR目录下需要有ffmpeg-config.cmakeFFMPEGConfig.cmake文件

这里就衔接了上文,我为什么要写一个ffmpeg-config-gen.sh脚本,脚本的内容很简单,我们直接看它生成出来的ffmpeg-config.cmake文件

set(FFMPEG_PATH "${CMAKE_CURRENT_LIST_DIR}")

set(FFMPEG_EXEC_DIR "${FFMPEG_PATH}/bin")
set(FFMPEG_LIBDIR "${FFMPEG_PATH}/lib")
set(FFMPEG_INCLUDE_DIRS "${FFMPEG_PATH}/include")

set(FFMPEG_LIBRARIES
    ${FFMPEG_LIBDIR}/libavformat.a
    ${FFMPEG_LIBDIR}/libavdevice.a
    ${FFMPEG_LIBDIR}/libavcodec.a
    ${FFMPEG_LIBDIR}/libavutil.a
    ${FFMPEG_LIBDIR}/libswscale.a
    ${FFMPEG_LIBDIR}/libswresample.a
    ${FFMPEG_LIBDIR}/libavfilter.a
    ${FFMPEG_LIBDIR}/libpostproc.a
    /home/dreamgyf/compile/x264/_output_/product/arm64-v8a/lib/libx264.a
    z
)

set(FFMPEG_libavformat_FOUND TRUE)
set(FFMPEG_libavdevice_FOUND TRUE)
set(FFMPEG_libavcodec_FOUND TRUE)
set(FFMPEG_libavutil_FOUND TRUE)
set(FFMPEG_libswscale_FOUND TRUE)
set(FFMPEG_libswresample_FOUND TRUE)
set(FFMPEG_libavfilter_FOUND TRUE)
set(FFMPEG_libpostproc_FOUND TRUE)

set(FFMPEG_libavcodec_VERSION 59.18.100)
set(FFMPEG_libavdevice_VERSION 59.4.100)
set(FFMPEG_libavfilter_VERSION 8.24.100)
set(FFMPEG_libavformat_VERSION 59.16.100)
set(FFMPEG_libavutil_VERSION 57.17.100)
set(FFMPEG_libpostproc_VERSION 56.3.100)
set(FFMPEG_libswresample_VERSION 4.3.100)
set(FFMPEG_libswscale_VERSION 6.4.100)

set(FFMPEG_FOUND TRUE)
set(FFMPEG_LIBS ${FFMPEG_LIBRARIES})

这里主要为cmake提供了三个变量

  • FFMPEG_INCLUDE_DIRS:提供头文件目录

  • FFMPEG_LIBRARIES:提供库文件链接

  • FFMPEG_FOUND:告诉cmake找到了FFmpeg

这里还有几个点要说,首先,cmake中的库文件链接顺序符合gcc链接顺序规则,所以说库的书写顺序也是有严格要求的,被依赖的库要放在依赖它的库的后面,正如这个文件,FFmpeg需要依赖x264,所以我需要将x264放在所有FFmpeg库的最后面

FFmpeg需要依赖zlib库,所以我在后面增加了一个z表示依赖zlib

FFmpeg这些库的版本定义是从$FFMPEG_PRODUCT/$ABI/lib/pkgconfig目录下各个文件读出来的

pkgconfig

version

ffmpeg-config.cmake文件写完,我们再回过头来看一下detect_ffmpeg.cmake

if(NOT HAVE_FFMPEG AND OPENCV_FFMPEG_USE_FIND_PACKAGE)
  if(OPENCV_FFMPEG_USE_FIND_PACKAGE STREQUAL "1" OR OPENCV_FFMPEG_USE_FIND_PACKAGE STREQUAL "ON")
    set(OPENCV_FFMPEG_USE_FIND_PACKAGE "FFMPEG")
  endif()
  find_package(${OPENCV_FFMPEG_USE_FIND_PACKAGE}) # Required components: AVCODEC AVFORMAT AVUTIL SWSCALE
  if(FFMPEG_FOUND OR FFmpeg_FOUND)
    set(HAVE_FFMPEG TRUE)
  endif()
endif()

可以看到最后的 if 中,如果FFMPEG_FOUNDtrue,则设置HAVE_FFMPEGtrue,正好对应了我们在ffmpeg-config.cmake中的行为,这下,CMakeLists.txt就可以找到我们的FFmpeg库了

这里还有一点,detect_ffmpeg.cmake中有一段用来测试的代码

if(HAVE_FFMPEG AND NOT HAVE_FFMPEG_WRAPPER AND NOT OPENCV_FFMPEG_SKIP_BUILD_CHECK)
  try_compile(__VALID_FFMPEG
      "${OpenCV_BINARY_DIR}"
      "${OpenCV_SOURCE_DIR}/cmake/checks/ffmpeg_test.cpp"
      CMAKE_FLAGS "-DINCLUDE_DIRECTORIES:STRING=${FFMPEG_INCLUDE_DIRS}"
                  "-DLINK_LIBRARIES:STRING=${FFMPEG_LIBRARIES}"
      OUTPUT_VARIABLE TRY_OUT
  )
  if(NOT __VALID_FFMPEG)
    message(FATAL_ERROR "FFMPEG: test check build log:\n${TRY_OUT}")
    message(STATUS "WARNING: Can't build ffmpeg test code")
    set(HAVE_FFMPEG FALSE)
  endif()
endif()

其中的message(FATAL_ERROR "FFMPEG: test check build log:\n${TRY_OUT}")原本是被注释了的,我强烈建议各位将其打开,这样如果哪里有误,一开始就可以报错并附带详细信息,免得到时候编到一半才报错,浪费时间

到这里,我本以为万事大吉了,于是开始编译,这里我使用了BUILD_SHARED_LIBS=ON选项编译动态库,armeabi-v7a顺利编译通过,但当arm64-v8a编译到一半时突然报错,提示libz.so, needed by ../../lib/arm64-v8a/libopencv_core.so, not found (try using -rpath or -rpath-link)

rpath_error

我观察了一下NDK目录结构,发现libz.so动态库文件可以在$TOOLCHAIN/sysroot/usr/lib/$TOOLCHAIN_NAME/$API下找到,需要注意的是,这里的TOOLCHAIN_NAMETRIPLE很相似,但在armeabi-v7a情况下又有些细微的不同,所以我又新定义了这个变量

然后我开始尝试加入-rpath-link选项,首先,我尝试添加了一项cmake选项CMAKE_SHARED_LINKER_FLAGS="-Wl,-rpath-link=$TOOLCHAIN/sysroot/usr/lib/$TOOLCHAIN_NAME/$API",发现,虽然在编译开头的输出中可以看出,这个参数确实被加上生效了,但在编译到同样的地方时,仍然会报相同的错误,这里我不太清楚,难道参数的顺序也会对编译造成影响吗?

于是我去查看了android.toolchain.cmake文件,看他是怎么添加这些选项的,发现了这么一行

set(CMAKE_SHARED_LINKER_FLAGS "${ANDROID_LINKER_FLAGS} ${CMAKE_SHARED_LINKER_FLAGS}")

于是我在这行代码前加了这么一行

list(APPEND ANDROID_LINKER_FLAGS -Wl,-rpath-link=${ANDROID_TOOLCHAIN_ROOT}/sysroot/usr/lib/${ANDROID_TOOLCHAIN_NAME}/${ANDROID_PLATFORM_LEVEL})

-rpath-link这个选项提前一点,果不其然,编译顺利通过了,但这样做有点麻烦,还得改NDK里的配置,于是我在构建脚本里加了一个参数ANDROID_LINKER_FLAGS="-Wl,-rpath-link=$TOOLCHAIN/sysroot/usr/lib/$TOOLCHAIN_NAME/$API",这样的话,-rpath-link选项会被提到Linker flags的最前面,经过测试,这样也可以编译通过,于是OpenCV的编译脚本也就这么完成了

当然这里还剩一个疑点,为什么不加-rpath-link的时候,arm64-v8a编译报错但armeabi-v7a却编译通过,希望有大佬可以指点一下

FreeType

我的App中还用到了FreeType库渲染字体,在这里顺便也把它的编译方式放出来吧

直接去 FreeType 这里下载我编译好的版本或者源码,根据我写的步骤进行编译就可以了

在Android中使用

在Android中使用时需要注意,如果你使用静态库的方式的话,需要将OpenCV编译出来的第三方库也加入到链接中,放在OpenCV的后面,另外FFmpeg还需要mediandkzlib这两个依赖,具体可以参考下面的代码

target_link_libraries(
        textvideo

        freetype

        # opencv
        opencv_videoio
        opencv_photo
        opencv_highgui
        opencv_imgproc
        opencv_imgcodecs
        opencv_dnn
        opencv_core

        # ffmpeg
        ffmpeg_avformat
        ffmpeg_avdevice
        ffmpeg_avcodec
        ffmpeg_avutil
        ffmpeg_swscale
        ffmpeg_swresample
        ffmpeg_avfilter

        # ffmpeg依赖
        mediandk
        z

        # x264
        x264

        # opencv第三方支持库
        ade
        cpufeatures
        ittnotify
        libjpeg-turbo
        libopenjp2
        libpng
        libprotobuf
        libwebp
        quirc
        tegra_hal

        # android jni库
        jnigraphics
        android
        log)

总结

虽然我这篇文章写的看起来编译的过程很简单,根本不像标题所说的那么艰难,但实际上我前前后后弄了大概有一个多星期才真正完整编出可用版本,前前后后编译失败了不说一百次也有几十次,对我这种不懂c语言编译的简直是折磨。因为我是在全部弄完后才开始写的文章,所以基本上坑都踩的差不多了,其中有些坑印象也没那么清楚了,我也没那么多精力再去复现出那些坑了,怎么说呢,能成功就万事大吉吧 😭