FFmpeg编译4.3.1-ndk(21)

该篇基于我前面翻译的fmpeg-android-maker文章基础上,实现对ffmpeg4.3.1最新版本编译。没有看过该篇文章的一定要先看下,内容不多,主要是要把编译shell代码clone下来。

本文编译产出共享库(.so文件)以及头文件(.h文件),主要针对android平台,使用的ndk是21.3.6528147版本。

之前也是从网上搜各种文章去编译ffmpeg,到最后不是编译不过,就是编译了之后在其他手机运行就崩溃,又或者需修改ffmpeg源码configure文件。
该篇文章基于ffmpeg官网给的git仓库去修改编译的(不是修改configure文件,是修改clone下来的shell文件,以符合自己的需求)

编译环境基本的要求这里就不啰嗦了。
主要是选择编译的系统使用mac或者linux
ndk使用android studio开发工具下载即可,后面会说到编译ffmpeg需要使用的基本配置。

clone下来的都是shell脚本写的代码,这里我用的开发工具是vs code

如果以后要专攻ffmpeg编译,shell脚本学习少不了,想系统学习shell脚本看这里
这里介绍一点基础的shell脚步知识:
A.export 变量=值--->定义环境变量,这样就可以在其他文件中访问该值了,类似全局变量,就好比你可以在命令行中执行echo $PATH,访问.bash_profile定义的变量。
B.$0,$1,$2,$3...$n脚本传递的参数的顺序,$0比较特殊代表脚步文件的名称。
C.$?代表执行的结果,shell中非0即为假,0代表真,例如echo $PATH 执行完后再执行echo $?输出0,说明echo $PATH这个命令执行结果为真。
D.$*,以一个单字符串显示所有向脚本传递的参数
E.$@,与$*相同,但是使用时加引号,并在引号中返回每个参数,一般用在for循环
F.if [[...]] ;then 或者if test ... ;then 都是基本的判断条件是否成功,具体可以网上搜一搜

先看下fmpeg-android-maker文章中clone下来的编译代码基本结构

image.png

结构说明

1.入口文件ffmpeg-android-maker.sh
2.scripts目录下包括:
------>ffmpeg,三方库目录:打开都是build.sh和download.sh
------>check-host-machine.sh、export-host-variables.sh、parse-arguments.sh等文件。
当运行ffmpeg-android-maker.sh文件后:
1.会自动调起scripts下的sh文件:例如check-host-machine.sh文件检查android需要sdk,ndk的配置问题等等
2.然后再编译对应ffmpeg,三方库目录下的download.sh下载源码,再执行各自的build.sh文件

下面依据入口文件ffmpeg-android-maker.sh来分析对ffmpeg的整个编译过程,最后完成编译输出.so和.h文件

入口文件ffmpeg-android-maker.sh

建议看该篇文章结合clone的源码看
下面是整个ffmpeg-android-maker.sh文件的代码,删去了注释。
这里会按照下面的注释“-----分块-----”来逐步解释

#!/usr/bin/env bash

#--------------------分块1-------start---------------
export BASE_DIR="$( cd "$( dirname "$0" )" && pwd )"

export SOURCES_DIR=${BASE_DIR}/sources

export STATS_DIR=${BASE_DIR}/stats

export SCRIPTS_DIR=${BASE_DIR}/scripts

export OUTPUT_DIR=${BASE_DIR}/output
${SCRIPTS_DIR}/check-host-machine.sh || exit 1
BUILD_DIR=${BASE_DIR}/build
export BUILD_DIR_FFMPEG=$BUILD_DIR/ffmpeg
export BUILD_DIR_EXTERNAL=$BUILD_DIR/external

function prepareOutput() {
  OUTPUT_LIB=${OUTPUT_DIR}/lib/${ANDROID_ABI}
  mkdir -p ${OUTPUT_LIB}
  cp ${BUILD_DIR_FFMPEG}/${ANDROID_ABI}/lib/*.so ${OUTPUT_LIB}

  OUTPUT_HEADERS=${OUTPUT_DIR}/include/${ANDROID_ABI}
  mkdir -p ${OUTPUT_HEADERS}
  cp -r ${BUILD_DIR_FFMPEG}/${ANDROID_ABI}/include/* ${OUTPUT_HEADERS}
}

function checkTextRelocations() {
  TEXT_REL_STATS_FILE=${STATS_DIR}/text-relocations.txt
  ${FAM_READELF} --dynamic ${BUILD_DIR_FFMPEG}/${ANDROID_ABI}/lib/*.so | grep 'TEXTREL\|File' >> ${TEXT_REL_STATS_FILE}

  if grep -q TEXTREL ${TEXT_REL_STATS_FILE}; then
    echo "There are text relocations in output files:"
    cat ${TEXT_REL_STATS_FILE}
    exit 1
  fi
}

rm -rf ${BUILD_DIR}
rm -rf ${STATS_DIR}
rm -rf ${OUTPUT_DIR}
mkdir -p ${STATS_DIR}
mkdir -p ${OUTPUT_DIR}
#--------------------分块1-------end----------

#--------------------分块2-------start---------------
source ${SCRIPTS_DIR}/export-host-variables.sh
source ${SCRIPTS_DIR}/parse-arguments.sh

COMPONENTS_TO_BUILD=${EXTERNAL_LIBRARIES[@]}
COMPONENTS_TO_BUILD+=( "ffmpeg" )

for COMPONENT in ${COMPONENTS_TO_BUILD[@]}
do
  SOURCE_DIR_FOR_COMPONENT=${SOURCES_DIR}/${COMPONENT}
  mkdir -p ${SOURCE_DIR_FOR_COMPONENT}
  cd ${SOURCE_DIR_FOR_COMPONENT}

  source ${SCRIPTS_DIR}/${COMPONENT}/download.sh

  COMPONENT_SOURCES_DIR_VARIABLE=SOURCES_DIR_${COMPONENT}
  if [[ -z "${!COMPONENT_SOURCES_DIR_VARIABLE}" ]]; then
     export SOURCES_DIR_${COMPONENT}=${SOURCE_DIR_FOR_COMPONENT}
  fi

  cd ${BASE_DIR}
done

for ABI in ${FFMPEG_ABIS_TO_BUILD[@]}
do
  source ${SCRIPTS_DIR}/export-build-variables.sh ${ABI}

  for COMPONENT in ${COMPONENTS_TO_BUILD[@]}
  do
    echo "Building the component: ${COMPONENT}"
    COMPONENT_SOURCES_DIR_VARIABLE=SOURCES_DIR_${COMPONENT}
    cd ${!COMPONENT_SOURCES_DIR_VARIABLE}
    source ${SCRIPTS_DIR}/${COMPONENT}/build.sh || exit 1

  done
  checkTextRelocations || exit 1

  prepareOutput
done
#--------------------分块2-------end----------

1.---分块1---

主要是:
a.定义几个环境变量,即BASE_DIR,SOURCES_DIR等全局目录,没有就创建("---分块1---end---"最后几个rm -rf 和mdkir -p)
b.定义两个方法prepareOutput(将.so和.h文件拷贝到上面创建的OUTPUT_DIR目录中)、checkTextRelocations(检查重定向文件text-relocations.txt中是否能检查到TEXTREL,如果检查到说明整个编译结果失败,退出,具体看fmpeg-android-maker文章末尾介绍)
c.执行check-host-machine.sh文件,检查sdk、ndk环境配置

各个目录变量具体值可以自己输出查看,我的输出结果:

BASE_DIR:/Users/rain/ffmpeg/ffmpeg-android-custom
SOURCES_DIR:/Users/rain/ffmpeg/ffmpeg-android-custom/sources
STATS_DIR:/Users/rain/ffmpeg/ffmpeg-android-custom/stats
SCRIPTS_DIR:/Users/rain/ffmpeg/ffmpeg-android-custom/scripts
OUTPUT_DIR:/Users/rain/ffmpeg/ffmpeg-android-custom/output
BUILD_DIR_FFMPEG:/Users/rain/ffmpeg/ffmpeg-android-custom/build/ffmpeg
BUILD_DIR_EXTERNAL:/Users/rain/ffmpeg/ffmpeg-android-custom/build/external

BASE_DIR:clone下来的代码所在的位置,类似根目录
SOURCES_DIR:下载ffmpeg,三方库源码存放位置
STATS_DIR:主要用于输出文本重定位信息text-relocations.txt(具体看fmpeg-android-maker文章末尾介绍)
SCRIPTS_DIR:上面说过了,主要放置ffmpeg,三方库各自的download.sh,build.sh和公共sh文件(后面详细介绍这些文件)
OUTPUT_DIR:编译完成后输出的文件,共享库(.so文件)以及头文件(.h文件)
BUILD_DIR_FFMPEG/BUILD_DIR_EXTERNAL:整个编译过程中ffmpeg以及三方库的build和install所存放的文件

check-host-machine.sh检查sdk,ndk设置的环境变量

${SCRIPTS_DIR}/check-host-machine.sh || exit 1
文件执行成功接着往下走,如果失败直接退出(取check-host-machine.sh执行结果:$? || exit 1)

该文件主要是检查系统环境是否配置了ANDROID_SDK_HOMEANDROID_NDK_HOME
可以自己查看系统.bash_profile中是否定义了这两个值,只要保证check-host-machine.sh和.bash_profile文件中定义的sdk、ndk变量同名即可(如果保持和.bash_profile文件中同名,记得全局搜索ANDROID_SDK_HOMEANDROID_NDK_HOME,都要修改)。

2.---分块2---

重点都在这块了,这块代码决定了你能否成功编译出.so和.h文件。
核心是2个sh文件和2个for循环

第1个sh文件:export-host-variables.sh

source ${SCRIPTS_DIR}/export-host-variables.sh
#!/usr/bin/env bash

# Defining a toolchain directory's name according to the current OS.
# Assume that proper version of NDK is installed
# and is referenced by ANDROID_NDK_HOME environment variable
# 依据编译系统确定编译工具链的目录名字
#macOS  darwin-x86_64
#Linux  linux-x86_64
#32 位 Windows   windows
#64 位 Windows   windows-x86_64
case "$OSTYPE" in
  darwin*)  HOST_TAG="darwin-x86_64" ;;
  linux*)   HOST_TAG="linux-x86_64" ;;
  msys)
    case "$(uname -m)" in
      x86_64) HOST_TAG="windows-x86_64" ;;
      i686)   HOST_TAG="windows" ;;
    esac
  ;;
esac

#检查当前系统输出cup的个数,这就是你们经常网上看到的那种编译 make -j8,开启编译cpu线程数
#可以看下scripts/ffmpeg/build.sh最后用到了HOST_NPROC变量
if [[ $OSTYPE == "darwin"* ]]; then
  HOST_NPROC=$(sysctl -n hw.physicalcpu)
else
  HOST_NPROC=$(nproc)
fi

# The variable is used as a path segment of the toolchain path
export HOST_TAG=$HOST_TAG
# Number of physical cores in the system to facilitate parallel assembling
export HOST_NPROC=$HOST_NPROC

# Using CMake from the Android SDK
export CMAKE_EXECUTABLE=${ANDROID_SDK_HOME}/cmake/3.10.2.4988404/bin/cmake
# Using Build machine's Make, because Android NDK's Make (before r21) doesn't work properly in MSYS2 on Windows
export MAKE_EXECUTABLE=$(which make)
# Using Build machine's Ninja. It is used for libdav1d building. Needs to be installed
export NINJA_EXECUTABLE=$(which ninja)
# Meson is used for libdav1d building. Needs to be installed
export MESON_EXECUTABLE=$(which meson)
# Nasm is used for libdav1d and libx264 building. Needs to be installed
export NASM_EXECUTABLE=$(which nasm)
# A utility to properly pick shared libraries by FFmpeg's configure script. Needs to be installed
export PKG_CONFIG_EXECUTABLE=$(which pkg-config)

export-host-variables.sh文件是大的编译前提实现的决定性条件,先看下我的mac电脑上的输出:

HOST_TAG:darwin-x86_64
HOST_NPROC:2
CMAKE_EXECUTABLE:/Users/rain/Library/Android/sdk/cmake/3.10.2.4988404/bin/cmake
MAKE_EXECUTABLE:/usr/bin/make
NINJA_EXECUTABLE:/Library/Frameworks/Python.framework/Versions/3.8/bin/ninja
MESON_EXECUTABLE:/Library/Frameworks/Python.framework/Versions/3.8/bin/meson
NASM_EXECUTABLE:/usr/local/bin/nasm
PKG_CONFIG_EXECUTABLE:/usr/local/bin/pkg-config

这几个变量的含义:

CMAKE_EXECUTABLE:cmake的绝对路径,用的是sdk下的cmake,scripts/libaom/build.sh中用到了
MAKE_EXECUTABLE:make的绝对路径,所有scripts/目录/build.sh文件全用到了,make用来做啥就不用说了吧
NINJA_EXECUTABLE:ninja,主要构建三方库libdav1d用的
MESON_EXECUTABLE:meson,主要构建三方库libdav1d用的
NASM_EXECUTABLE:nasm,构建libdav1d和libx264用的
PKG_CONFIG_EXECUTABLE:编译ffmpeg时从configure脚本文件中选择共享库用的,很重要!
以上全局变量都可以全局搜索看看都在哪些build.sh文件中用到了,
命令行执行:which make/ninja/meson等查看是否安装了
最好按照网上教程全部安装!

回到ffmpeg-android-maker.sh文件继续分析
第2个sh文件:parse-arguments.sh

source ${SCRIPTS_DIR}/parse-arguments.sh

parse-arguments.sh文件代码比较多这里就不贴出来了,可以自己看源码。该文件就是解析你编译时输入的参数。
主要是给FFMPEG_ABIS_TO_BUILD、FFMPEG_SOURCE_TYPE、FFMPEG_SOURCE_VALUE、FFMPEG_EXTERNAL_LIBRARIES、DESIRED_ANDROID_API_LEVEL、DESIRED_BINUTILS
几个环境变量赋值。
我编译时输入的命令:./ffmpeg-android-maker.sh -abis=arm,arm64 -x264

再看下给上面几个变量赋值后的值
FFMPEG_ABIS_TO_BUILD:armeabi-v7a arm64-v8a
FFMPEG_SOURCE_TYPE:TAR
FFMPEG_SOURCE_VALUE:4.3.1
FFMPEG_EXTERNAL_LIBRARIES:libx264
DESIRED_ANDROID_API_LEVEL:21
DESIRED_BINUTILS:gnu
我要编译的就是armeabi-v7a arm64-v8a两个ABI,并且支持x264编码。
源码下载的方式我用的是TAR;ffmpeg编译的版本是4.3.1;
androi的API选择的是21,没有选择更高的版本是我遇到再其他手机运行报错:cannot locate symbol "iconv_close" referenced,可以自行网上搜索这类问题
DESIRED_ANDROID_API_LEVEL主要用来构建ffmpeg用到的交叉编译链中用到CC、CXX、AS、AR、NM等用到,具体可以看scripts/export-build-variables.sh文件解析;
DESIRED_BINUTILS代表使用哪个binutils(gnu或llvm),默认是gnu,看scripts/export-build-variables.sh文件中就能找到答案

回到ffmpeg-android-maker.sh文件,这时COMPONENTS_TO_BUILD的值是libx264 ffmpeg
并且ffmpeg是永远放在最后的。

第一个for循环

是对COMPONENTS_TO_BUILD进行遍历,这里是libx264 ffmpeg
核心功能就是下载每个模块的源码,然后将下载的源码路径赋值环境变量 SOURCES_DIR_${COMPONENT}
具体看代码注释

for COMPONENT in ${COMPONENTS_TO_BUILD[@]}
do
  #根据上面创建的SOURCES_DIR目录创建目录SOURCE_DIR_FOR_COMPONENT
  #例如ffmpeg:/Users/rain/ffmpeg/ffmpeg-android-custom/sources/ffmpeg
  SOURCE_DIR_FOR_COMPONENT=${SOURCES_DIR}/${COMPONENT}

  #创建SOURCE_DIR_FOR_COMPONENT目录,并且进入
  mkdir -p ${SOURCE_DIR_FOR_COMPONENT}
  cd ${SOURCE_DIR_FOR_COMPONENT}

  #上一步进入之后就执行对应模块的download.sh文件
  #/Users/rain/ffmpeg/ffmpeg-android-custom/scripts/ffmpeg/download.sh
  source ${SCRIPTS_DIR}/${COMPONENT}/download.sh

  #if判断COMPONENT_SOURCES_DIR_VARIABLE 赋值的SOURCES_DIR_${COMPONENT} 是否已经有值
  #没有的换就进入if 并且赋值环境变量SOURCES_DIR_${COMPONENT}
  #即SOURCES_DIR_ffmpeg=/Users/rain/ffmpeg/ffmpeg-android-custom/sources/ffmpeg
  #主要给下一个for循环使用
  COMPONENT_SOURCES_DIR_VARIABLE=SOURCES_DIR_${COMPONENT}
  if [[ -z "${!COMPONENT_SOURCES_DIR_VARIABLE}" ]]; then
     export SOURCES_DIR_${COMPONENT}=${SOURCE_DIR_FOR_COMPONENT}
  fi

  cd ${BASE_DIR}
done

第二个for循环是对在parse-arguments.sh中赋值的环境变量FFMPEG_ABIS_TO_BUILD:armeabi-v7a arm64-v8a
进行遍历操作,即选择的不同ABI进行编译。

这里插补下for循环中export-build-variables.sh文件的解析

source ${SCRIPTS_DIR}/export-build-variables.sh ${ABI}

运行export-build-variables.sh文件时传入了一个参数即:$1=ABI
该文件主要是根据传入的ABI和在parse-arguments.sh中定义的环境变量,
给TOOLCHAIN_PATH、SYSROOT_PATH,CROSS_PREFIX、FAM_CC、FAM_CXX等变量赋值,
主要是给每个模块build.sh时用的
该文件什么都不用改动,如果你感兴趣可以输出某些值看看,都是和编译ffmpeg需要的工具链相关的东西。

继续第二个for循环分析。经过参数ABI执行完export-build-variables.sh后
进入第二个for循环中的第二个循环,其实就是上面第一个循环执行不同的命令而已,即执行每个模块的build.sh文件
最后:第二个for循环执行完一个ABI之后,就执行上面说到的----分块1----中定义的两个方法checkTextRelocations,prepareOutput。
可以看到执行checkTextRelocations方法时,如果失败就退出当前ABI的for循环。
prepareOutput是将每个ABI的.so和.h从build/ffmpeg拷贝到output下:

根目录/build/ffmpeg/armeabi-v7a/lib/.so--->根目录/output/lib/armeabi-v7a
根目录/build/ffmpeg/armeabi-v7a/include/
--->根目录/output/include/armeabi-v7a

第二个for循环走完,整个编译就已经完成了,在根目录的output里面就能看到编译出的.so和.h。
这里scripts/ffmpeg/build.sh文件就不详细说了,和网上编译的几乎一样,不过ffmpeg git官网给的分支,不管是master还是what-the-codec都不是我要的
我是在what-the-codec分支基础上修改了我的需求,就是支持硬件编码,可以对比看下我的ffmpeg/build.sh内容:

./configure \
  --prefix=${BUILD_DIR_FFMPEG}/${ANDROID_ABI} \
  --enable-cross-compile \
  --target-os=android \
  --arch=${TARGET_TRIPLE_MACHINE_BINUTILS} \
  --sysroot=${SYSROOT_PATH} \
  --cc=${FAM_CC} \
  --cxx=${FAM_CXX} \
  --ld=${FAM_LD} \
  --ar=${FAM_AR} \
  --as=${FAM_CC} \
  --nm=${FAM_NM} \
  --ranlib=${FAM_RANLIB} \
  --strip=${FAM_STRIP} \
  --extra-cflags="-O3 -fPIC $DEP_CFLAGS" \
  --extra-ldflags="$DEP_LD_FLAGS" \
  --enable-shared \
  --disable-static \
  --pkg-config=${PKG_CONFIG_EXECUTABLE} \
  ${EXTRA_BUILD_CONFIGURATION_FLAGS} \
  --disable-runtime-cpudetect \
  --disable-programs \
  --disable-avdevice \
  --disable-postproc \
  --disable-doc \
  --disable-debug \
  --disable-network \
  --disable-bsfs \
  --enable-pthreads \
  --enable-asm \
  --disable-neon \
  --enable-jni \
  --enable-mediacodec \
  --enable-decoder=h264_mediacodec \
  $ADDITIONAL_COMPONENTS || exit 1

记住,开启mediacodec,一定要开jni,开启jni也要一定开启pthreads(如果开启jni,不开pthreads,会报错:ERROR: jni not found,可以在ffmpeg源码configure中全局搜索jni not found就知道答案了)

看下我编译完成后项目结构情况


image.png

最后

总结下clone下来代码之后需要修改的.sh文件有哪些,如果你完全看懂了每个.sh文件就可以随意修改了

1.check-host-machine.sh文件,保持文件中ANDROID_SDK_HOME、ANDROID_NDK_HOME两个变量和系统.bash_profile同名即可,
可以全局搜索就能知道哪里用到了这两个环境变量了(如果保持和.bash_profile文件中同名,记得全局搜索ANDROID_SDK_HOME、ANDROID_NDK_HOME,都要修改)
2.export-host-variables.sh文件,什么都不需要修改,唯一要注意的就是检查是否安装了make、ninja、meson、nasm、which pkg-config这些编译时要用到的工具,可以在命令行中执行要查看的工具,类似which make等查看即可
3.parse-arguments.sh文件,我只修改了API_LEVEL=21,其他的SOURCE_TYPE、SOURCE_VALUE根据自己需求修改
4.export-build-variables.sh文件,不做任何修改
5.common-functions.sh文件,不做任何修改。该文件是每个模块的download.sh中会调用,就是下载各个模块源码时使用

上面文件修改完成后,执行./ffmpeg-android-maker.sh -abis=arm,arm64 -x264就开始编译了(更多参数传递看这里)

编译过程中如果失败中断,一般都会有错误日志输出,看看哪个模块失败了,然后再到对应模块源码下查看config.log文件,
例如:
ffmpeg对应sources/ffmpeg/ffmpeg-4.3.1/ffbuild/config.log文件
libx264对应sources/libx264/x264-cde9adf/config.log文件
一般根据config.log文件能够定位解决很多问题,如果还有问题欢迎留言讨论!

推荐阅读更多精彩内容