我的ffmpeg开源项目地址Viktor_ffmpeg
该项目主要以学习ffmpeg为主,代码中将ffplay摘抄出来(主要去除了sdl,使用c++的std),用自己的方式实现(面向对象)。
从该项目中你可以学到
1.如何使用ffmpeg解码视频并播放
2.如何多个视频串行播放
该项目是以剪映的功能为目标
我的编译项目地址ffmpeg编译
该篇基于我前面翻译的fmpeg-android-maker文章基础上,实现对ffmpeg4.3.1版本编译。没有看过该篇文章的一定要先看下,内容不多,主要是要把编译shell代码clone下来。或者clone我的ffmpeg编译项目(主要加了点echo输出日志方便排查问题、以及更改了ffmpeg目录下build.sh文件的部分编译条件以符合自己的编译需求)
本文编译产出共享库(.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下来的编译代码基本结构
结构说明
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_HOME和ANDROID_NDK_HOME。
可以自己查看系统.bash_profile中是否定义了这两个值,只要保证check-host-machine.sh和.bash_profile文件中定义的sdk、ndk变量同名即可(如果保持和.bash_profile文件中同名,记得全局搜索ANDROID_SDK_HOME、ANDROID_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就知道答案了)
看下我编译完成后项目结构情况
最后
总结下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文件能够定位解决很多问题,如果还有问题欢迎留言讨论!