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

2023-11-17

前言

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

注:编译顺序要按照 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语言编译的简直是折磨。因为我是在全部弄完后才开始写的文章,所以基本上坑都踩的差不多了,其中有些坑印象也没那么清楚了,我也没那么多精力再去复现出那些坑了,怎么说呢,能成功就万事大吉吧

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Android交叉编译OpenCV+FFmpeg+x264的艰难历程 的相关文章

  • 如何使用retrofit2进行GET请求?

    我有一个在本地主机上运行的安静的 Web 服务 我想在该剩余 URL 上发出 Retrofit2 GET 请求 MainActivity java private void requestData public static final S
  • 有没有办法将搜索栏添加到我的实际首选项屏幕?

    我一直看到有关添加您自己的搜索栏首选项的教程 但它不在我实际的 prefs xml 中 有什么方法可以在我的主偏好设置屏幕中添加一个 或者我必须将其分开 Google 似乎有 2 个滑块首选项 搜索栏首选项 https github com
  • 毕加索磁盘缓存

    我正在使用 Picasso 从 URL 加载图像 Picasso with getApplicationContext load product getImageUrl into imageView 据我所知 每次都会访问该网址 而不是缓存
  • 如何忽略 LeakCanary 中的某些类?

    有人能给我一个如何忽略 LeakCanary 中的某些类的有效示例吗 我正在查看这个示例 以忽略 LeakCanary 中第三方库中的某些类 但我不知道将其放在应用程序中的何处 我把它放在我的应用程序类中 但这些变量和方法有错误 isInA
  • 如何在 Android 应用程序中单击按钮时打开 Gmail Compose?

    当我的 Android 应用程序中单击按钮时 我尝试打开 Gmail 撰写屏幕 我需要 Google 提供的 API 密钥吗 或者我需要在按钮 onClickListener 中做什么 任何形式的见解都非常值得赞赏 正如 JeffC 指出的
  • Android 中有没有办法获取 SD 卡大小?

    欢迎大家 我已经在 Stackoverflow 和 google 上尝试过与此相关的每个问题 但没有一个有效 我已经尝试过类似下一个链接的操作 但它返回的内容与内部存储相同 如何获取外部存储 SD 卡的大小 带安装的 SD 卡 https
  • 我的 Android 设备需要安装哪个驱动程序才能运行我的应用程序?

    我购买了 intex mobile 来在真实设备中测试我的 Android 应用程序 然而 该设备不存在于 OEM USB 驱动程序列表中 android 提供的设备列表中 我检查了 intex 官方网站 但不确定到底需要安装哪个驱动程序
  • opencv水印周围的轮廓

    我想在图像中的水印周围画一个框 我已经提取了水印并找到了轮廓 但是 不会在水印周围绘制轮廓 轮廓是在我的整个图像上绘制的 请帮我提供正确的代码 轮廓坐标的输出为 array 0 0 0 634 450 634 450 0 dtype int
  • 在 Android 中始终以横向模式打开相机

    在我的 Android 应用程序中 单击按钮后我希望相机以横向模式打开 即使我将手机旋转为纵向模式 相机也应始终处于横向模式或纵向模式 使用此代码在横向模式下打开相机 Intent cameraIntent new Intent Media
  • android 多关键词搜索

    我的应用程序包含搜索功能 它将搜索数据库内的内容 我的搜索的弱点是 我只能使用一个标签进行搜索 例如我只能搜索 猫 它会返回我的数据库中包含 猫 一词的内容 因为我正在使用LIKE在 select 语句期间进行查询 如何使用多个标签进行搜索
  • Android - 多次实例化一个片段?

    我正在创建一个在 ListView 中显示数据的应用程序 数据分为两种类型 热门 收藏夹 我有一个活动和两个片段 片段根据类别显示项目列表 我为此使用了 ListView 然后我有两个fragment layouts 它们在设计上完全相同
  • 如何知道用户是否在 Android 应用程序中输入了错误的密码(锁定屏幕)

    我正在开发一个 Android 应用程序 如果用户在 Android 锁定屏幕中输入错误的密码 则必须完成其中一项活动 例如 如果用户输入错误的密码 则会发送电子邮件 我将不胜感激任何帮助 提前致谢 Kshitij 锁屏在完全沙箱环境中运行
  • 从扫描文档中提取行表 opencv python

    我想从扫描的表中提取信息并将其存储为 csv 现在我的表提取算法执行以下步骤 应用倾斜校正 应用高斯滤波器进行去噪 使用 Otsu 阈值进行二值化 进行形态学开局 Canny 边缘检测 进行霍夫变换以获得表格行 去除重复行 10像素范围内相
  • 如何在Android网格视图中设置单元格大小?

    我正在尝试为应用程序制作一个带有大图标的网格视图 但我找不到任何有关修改 Android 上网格布局上的单元格大小的教程 有人可以给我一个例子或相关链接吗 Thanks 就像另一个一样适配器视图 http developer android
  • 按名称获取 ArrayList

    这是正确的获取方式吗ArrayList
  • 如何更改 Android 12 启动屏幕中的图标形状?

    我想要矩形形状的启动屏幕图标 而不是 android 12 中的圆形形状 我不相信你可以 如果你看这里的第 3 点 https developer android com about versions 12 features splash
  • 在 android 中,第一次单击时按钮侦听器未注册

    因为我是 Android 新手 所以我遇到了按钮监听器的问题 我正在使用 OnClickListener 来处理胸像 但它第一次点击后不执行一旦我单击多个 它就会表现良好 但如何使其在第一次单击时成为可能 这是我的代码 public cla
  • Android 4.2 - Environment.getExternalStorageDirectory().getPath() 行为

    我一直在开发一个android应用程序 在上次更新到4 2之前 我使用 Environment getExternalStorageDirectory getPath 它返回了我 storage sdcard0 但自从更新后我现在得到了 s
  • 改造方法调用可能会产生“java.lang.NullPointerException”

    使用 Retrofit 2 3 0 我在 Android Studio 中收到以下消息 有关如何删除此 IDE 错误消息的任何建议 谢谢 来自Response文档 http square github io retrofit 2 x ret
  • 深度估计的准确性 - 立体视觉

    我正在研究立体视觉 我对这个问题的深度估计的准确性感兴趣 这取决于几个因素 例如 适当的立体校准 旋转 平移和失真提取 图像分辨率 相机和镜头质量 失真越小 色彩捕捉正确 两个图像之间的匹配特征 假设我们没有低成本的相机和镜头 没有廉价的网

随机推荐

  • C++ private,public,protected的访问范围解析

    第一 private public protected的访问范围 private 只能由该类中的函数 其友元函数访问 不能被任何其他访问 该类的对象也不能访问 protected 可以被该类中的函数 子类的函数 以及其友元函数访问 但不能被
  • BuilderException: Error parsing SQL Mapper Configuration. Cause: java.lang.NullPointerException

    集成mybatis plus sqlSessionFactory改为MybatisSqlSessionFactoryBean后 项目无法启动
  • 团队新人多,稳定性经验不足,研发质量怎么保障?|TakinTalks论道

    一分钟精华速览 在研发和稳定性保障过程中 人与设备 程序 组织的交互是一个复杂的过程 虽然人们极少会恶意犯错 但由于受特定情景下的实际条件影响 人为失误也时有发生 那么 如何尽可能减少这些失误的发生 如何保障研发质量和系统稳定 TakinT
  • 读《大数据时代》有感

    大数据时代 读后感 大数据时代 这本书主要描述的是大数据时代到临人们生活 工作与思维各方面所遇到的重大变革 文中清晰的阐述了大数据的基本概念和特点 并列出明确的观点 不管对于产业实践者 还是对于政府和公众机构 都非常具有价值 作者将本书分为
  • STM32系列(HAL库)——F103C8T6驱动WS2812全彩RGB模块(PWM+DMA方式)

    1 软件准备 1 编程平台 Keil5 2 CubeMX 2 硬件准备 1 某宝买的RGB模块 4个灯珠级联 2 F1的板子 本例使用经典F103C8T6 3 ST link 下载器 4 杜邦线若干 3 模块资料 1 模块简介 没什么比手册
  • 图像目标检测之cascade-rcnn实践

    最近一直在调试目标检测方面的模型 其中mmdetection中就集成了许多的目标检测模型 其中表现比较好的模型中有cascade rcnn 因此也趁这个机会具体了解一下这个模型的发展脉络 1 模型原理 在two stage模型中 常见都会预
  • 【网络云盘客户端】——项目简介

    项目简介 网络云盘客户端时基于QT C 框架实现了一个网络云盘客户端软件 主要功能包括用户的注册 登录 显示用户的个人文件列表 以及文件的上传 下载 删除 共享文件 登录界面 主窗口界面 文件属性对话框 文件展示列表 上传文件 详细实现参考
  • 【图像处理】 常用边缘检测算法对比分析

    边缘的定义 首先介绍什么是边缘 在数字图像中 边缘是指图像局部变化最显著的部分 边缘主要存在于目标与目标 目标与背景之间 是图像局部特性的不连续性 如灰度的突变 纹理结构的图标 颜色的图标等 尽管图像的边缘点产生的原因各不相同 但他们都是图
  • [操作系统] 分页存储管理中的页表项长度

    看到很多人有疑问 读到这里的时候我也有疑问的 在操作系统的分页存储管理方式中 写道 将页表始址与页号和页表项长度的乘积相加 便得到该表项在页表中的位置 于是可从中得到该页的物理块号 将之装入物理地址寄存器中 列出式子出来 页表始址 页号x页
  • Java excel poi 下载模板功能无法在Microsoft Excel 打开,但是可以WPS Excel打开

    问题 并且只显示前几个字段 后面的几个字段不显示 版本使用
  • 我的 Android 求职简历

    昨晚在Diycode的微信交流群里面 有位在校的童鞋想要找一份开发的实习工作 他把简历做好后在群上共享了一份 我看到后便下载了一份 看了看简历内容 我在想如果我是招聘单位看简历的 这份简历可以说是基本没戏的 因为内容基本和开发没有多大关系
  • Qt5Error:msvc-version.conf loaded but QMAKE_MSC_VER ins‘t set

    错误描述 msvc version conf loaded but QMAKE MSC VER ins t set 解决方案 这种情况一般都是修改了项目的配置之后出现的 也就是 qmake stash文件出错 删除这个文件再进行重新编译即可
  • 基于python的布尔盲注爆破脚本(sqli-libs第八关)

    写这个脚本的原因是因为布尔爆破步骤的繁琐 因此写下这个半自动化脚本来提升效率 只需输入url和标志词便可开始爆破 下面结合sqli libs第八关来详细说明 这一关是布尔盲注 布尔盲注用于页面没有回显的情况下 但是心细的同学会发现当我们注入
  • 使用Microsoft.Reporting. WebForms中ReportViewer报表查看器(Server方式)

    添加Web Reference http
  • Flex程序编译

    Makefile三要素 目标 依赖 命令 详解可见makefile 编写 周北 CSDN博客 makefile 编写 Makefile中常用函数和自动化变量 wildcard 扩展通配符 例 OBJECTS wildcard o 该找到目标
  • C++构造函数中不调用虚函数的原因

    今天在看网上一篇帖子的时候看到这个问题 试讲关于C 对象虚函数表和类型信息的 RTTI 的 正好看到了如下内容 这个是为什么在构造函数中不能调用虚函数的原因 因为任何时候在基类中的虚函数调用 都不可能到达子类的实现 因为子类的虚表初始化是在
  • [docker]搭建elasticsearch服务

    1 拉取镜像 docker pull elasticsearch 8 7 0 如果需要其他版本的话 访问 Dockerhttps hub docker com ela
  • QT从入门到入土(四)——文件的读写操作

    引言 文件的读写是很多应用程序具有的功能 甚至某些应用程序就是围绕着某一种格式文件的处理而开发的 所以文件读写是应用程序开发的一个基本功能 Qt 提供了两种读写纯文本文件的基本方法 用 QFile 类的 IODevice 读写功能直接进行读
  • uniapp中的分享功能实现(APP,小程序,公众号)

    uniapp中的分享功能实现 APP 小程序 公众号 1 APP端的分享 app端的分享可以直接使用uniapp封装的方法uni share uni app的App引擎已经封装了微信 QQ 微博的分享SDK 开发者可以直接调用相关功能 可以
  • Android交叉编译OpenCV+FFmpeg+x264的艰难历程

    前言 如果你没有兴趣看完本文 只想获得可编译的代码或编译后的产物 可以直接点击下面的链接 跟随步骤编译代码或直接下载我编译好的产物 注 编译顺序要按照 x264 gt FFmpeg gt OpenCV 这样来 x264 FFmpeg Ope