探秘ijkplayer——编译篇

  • ijkplayer是什么
  • 准备工作
  • 编译过程
  • 运行demo
  • 后记
  • 总结

最近有机会又重新捡起了视频播放器这个难啃的骨头,回顾一下,发现忘了好多。。。借着这次机会,希望可以把ijkplayer整体梳理一遍,也可以帮助新手快速入门ijkplayer这个神秘的殿堂

ijkplayer是什么

ijkplayer是bilibili开源的视频播放器,底层基于FFMPEG,经过jni层封装,在应用层提供与Android原生MediaPlayer相同的适配接口。

总体上看,在应用层使用IjkPlayer与原生的MediaPlayer基本无差别;但IjkPlayer的优势在于,它不再依赖Android系统支持的编码格式,(参见Android原生支持的编码格式)而是基于FFMPEG自己维护了视频编解码,可以说覆盖了Android大部分的机型和系统,这一点对于饱受碎片化扎心的Android开发者来说是非常好的体验(当然这是要增加包大小的

介绍完成,下边开始从pull代码编译开始逐步扒开ijkplayer的面纱

准备工作

首先从github上pull代码(github传送门
按照github内的步骤,安装homebrew、git、yasm(已安装过的忽略)

准备工作其实就这些,接下来就要开始进行编译了

编译过程

操作流程请按照github上的步骤进行,本部分仅是对编译过程进行详细拆分和解读
编译一共分为以下几步

  1. 配置ijkplayer支持的音视频编解码格式
  2. 下载参与编译的库(FFmpeg、libyuv、soundtouch)
  3. 编译FFmpeg
  4. 编译ijkplayer

第一步——配置支持的编码格式

# config/module-lite-hevc.sh# 可以看到一些ffmpeg配置的编码格式
export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --disable-decoders"
export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-decoder=aac"
export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-decoder=aac_latm"
export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-decoder=flv"
export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-decoder=h263"
export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-decoder=h263i"
export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-decoder=h263p"
export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-decoder=h264"
export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-decoder=mp3*"
export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-decoder=vp6"
export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-decoder=vp6a"
export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-decoder=vp6f"
export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-decoder=hevc"

第二步——下载参与编译的必要库

# ./init-android.sh# 下载ffmpeg到extra目录
echo "== pull ffmpeg base =="
sh $TOOLS/pull-repo-base.sh $IJK_FFMPEG_UPSTREAM $IJK_FFMPEG_LOCAL_REPO# ....省略部分内容# 下载libyuv
./init-android-libyuv.sh
# 下载soundtouch
./init-android-soundtouch.sh

第三步——编译ffmpeg
编译ffmpeg的步骤稍显复杂,需要详细拆解一下

# android/contrib/compile-ffmpeg.shcase "$FF_TARGET" in
#....省略all|all64)echo_archs $FF_ACT_ARCHS_64for ARCH in $FF_ACT_ARCHS_64do# 可以看到调用了 tools/do-compile-ffmpeg.shsh tools/do-compile-ffmpeg.sh $ARCH $FF_TARGET_EXTRAdoneecho_nextstep_help;;
# android/contrib/tools/do-compile-ffmpeg.sh# ...省略
# 初始化编译的一些环境变量
. ./tools/do-detect-env.sh# ...# 初始化对应arm版本的编译目录和产物输出目录
elif [ "$FF_ARCH" = "armv5" ]; thenFF_BUILD_NAME=ffmpeg-armv5FF_BUILD_NAME_OPENSSL=openssl-armv5FF_BUILD_NAME_LIBSOXR=libsoxr-armv5FF_SOURCE=$FF_BUILD_ROOT/$FF_BUILD_NAMEFF_CROSS_PREFIX=arm-linux-androideabiFF_TOOLCHAIN_NAME=${FF_CROSS_PREFIX}-${FF_GCC_VER}FF_CFG_FLAGS="$FF_CFG_FLAGS --arch=arm"FF_EXTRA_CFLAGS="$FF_EXTRA_CFLAGS -march=armv5te -mtune=arm9tdmi -msoft-float"FF_EXTRA_LDFLAGS="$FF_EXTRA_LDFLAGS"FF_ASSEMBLER_SUB_DIRS="arm"# ...# 使用独立的交叉编译工具链进行编译
FF_TOOLCHAIN_TOUCH="$FF_TOOLCHAIN_PATH/touch"
if [ ! -f "$FF_TOOLCHAIN_TOUCH" ]; then$ANDROID_NDK/build/tools/make-standalone-toolchain.sh \$FF_MAKE_TOOLCHAIN_FLAGS \--platform=$FF_ANDROID_PLATFORM \--toolchain=$FF_TOOLCHAIN_NAMEtouch $FF_TOOLCHAIN_TOUCH;
fi# ...# ffmpeg的编译参数detected
FF_CFLAGS="-O3 -Wall -pipe \-std=c99 \-ffast-math \-fstrict-aliasing -Werror=strict-aliasing \-Wno-psabi -Wa,--noexecstack \-DANDROID -DNDEBUG"# 看到了第一步熟悉的编解码支持格式
export COMMON_FF_CFG_FLAGS=
. $FF_BUILD_ROOT/../../config/module.sh
FF_CFG_FLAGS="$FF_CFG_FLAGS $COMMON_FF_CFG_FLAGS"# 这些配置暂时还不太明白,不过可以看出是ffmpeg的配置
# Standard options:
FF_CFG_FLAGS="$FF_CFG_FLAGS --prefix=$FF_PREFIX"# Advanced options (experts only):
FF_CFG_FLAGS="$FF_CFG_FLAGS --cross-prefix=${FF_CROSS_PREFIX}-"
FF_CFG_FLAGS="$FF_CFG_FLAGS --enable-cross-compile"
FF_CFG_FLAGS="$FF_CFG_FLAGS --target-os=linux"
FF_CFG_FLAGS="$FF_CFG_FLAGS --enable-pic"
# FF_CFG_FLAGS="$FF_CFG_FLAGS --disable-symver"# 此处生成config.h(依据之前配置的flag,使用ffmpeg的configure脚本生成)
echo ""
echo "--------------------"
echo "[*] configurate ffmpeg"
echo "--------------------"
cd $FF_SOURCE
if [ -f "./config.h" ]; thenecho 'reuse configure'
elsewhich $CC./configure $FF_CFG_FLAGS \--extra-cflags="$FF_CFLAGS $FF_EXTRA_CFLAGS" \--extra-ldflags="$FF_DEP_LIBS $FF_EXTRA_LDFLAGS"make clean
fi# 此处进行编译
echo ""
echo "--------------------"
echo "[*] compile ffmpeg"
echo "--------------------"
cp config.* $FF_PREFIX
make $FF_MAKE_FLAGS > /dev/null
# 执行的是android/contrib/ffmpeg-armv5/Makefile
make install
mkdir -p $FF_PREFIX/include/libffmpeg
cp -f config.h $FF_PREFIX/include/libffmpeg/config.h# 链接,生成libijkffmpeg.so(这个so文件会打进apk里)
$CC -lm -lz -shared --sysroot=$FF_SYSROOT -Wl,--no-undefined -Wl,-z,noexecstack $FF_EXTRA_LDFLAGS \-Wl,-soname,libijkffmpeg.so \$FF_C_OBJ_FILES \$FF_ASM_OBJ_FILES \$FF_DEP_LIBS \-o $FF_PREFIX/libijkffmpeg.so# 最后一步,拷贝生成的文件(不再贴代码了)

第四步——编译ijkplayer
相比较上一步编译ijkplayer,这一步就简单多了,主要是依赖上一步编译的产物,对jni层剩余的部分进行编译链接,并生成so文件

# android/compile-ijk.sh# 对全部armeabi进行编译
case "$REQUEST_TARGET" inall|all64)for ABI in $ACT_ABI_64dodo_ndk_build "$ABI" $REQUEST_SUB_CMD;done;;do_ndk_build () {PARAM_TARGET=$1PARAM_SUB_CMD=$2case "$PARAM_TARGET" inarmv5|armv7a)cd "ijkplayer/ijkplayer-$PARAM_TARGET/src/main/jni"do_sub_cmd $PARAM_SUB_CMDcd -;;esac
}do_sub_cmd () {SUB_CMD=$1if [ "$PARAM_SUB_CMD" = 'prof' ]; thenecho 'profiler build: YES';ln -s ../../../../../../ijkprof/android-ndk-profiler/jni android-ndk-profelseecho 'profiler build: NO';ln -s ../../../../../../ijkprof/android-ndk-profiler-dummy/jni android-ndk-profficase $SUB_CMD in*)# 此处的编译路径为android/ijkplayer/⁨ijkplayer-armv5⁩/ ⁨src/⁨main/jni/Android.mk⁩$ANDROID_NDK/ndk-build $FF_MAKEFLAGS;;esac
}

再看一下jni的Android.mk文件

# 依赖了之前编译完的ffmpeg的相关内容
ifeq ($(TARGET_ARCH_ABI),armeabi)
MY_APP_FFMPEG_OUTPUT_PATH := $(realpath $(MY_APP_ANDROID_ROOT)/contrib/build/ffmpeg-armv5/output)
MY_APP_FFMPEG_INCLUDE_PATH := $(realpath $(MY_APP_FFMPEG_OUTPUT_PATH)/include)

运行demo

执行完编译链接之后,可以看到,在demo工程的对应位置,生成了so文件,此时运行demo工程,即可看到令人热血沸腾的app页面
生成so文件后的工程目录截图

后记

由于上述流程都是直接生成了so文件,从而导致了以下几个问题

  1. 无法在Android Studio中使用ndk一键运行
  2. 无法使用Android Studio查看源码(函数调用之类的)
  3. 无法debug调试

所以在此基础上,小帽还是尝试了下可以更加美好美丽的进行后边的探秘工作,最终找到了解决上述三个问题的方法

  1. 首先,下载ndk并在Android Studio里配置ndk的路径(可以执行完编译流程,ndk应该都下载过了,配置方式请自行百度)
  2. 由于ijkplayer使用的gradle版本较低,我们需要升级下gradle wrapper和gradle插件的版本
// 工程build.gradle
dependencies {// 升级到3.2.1版本classpath 'com.android.tools.build:gradle:3.2.1'//        classpath 'com.github.dcendents:android-maven-gradle-plugin:1.4.1'
//        classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.7'// NOTE: Do not place your application dependencies here; they belong// in the individual module build.gradle files}
// gradle/wrapper/gradle-wrapper.properties// 升级到4.10.1
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip
  1. jni编译参数配置
// ijkplayer-armv5/build.gradledefaultConfig {minSdkVersion 9targetSdkVersion rootProject.ext.targetSdkVersion// 增加abi配置externalNativeBuild {ndkBuild {abiFilters "armeabi"}}}// 增加makefile配置externalNativeBuild{ndkBuild{path "src/main/jni/Android.mk"}}
  1. 根据编译情况,可能还需要增加flavorDimensions,去掉manifest最低版本限制等等,这个看各位的编译情况

经过以上配置,再次点击运行,即可以debug jni源码,同时,也可以看方法调用链了,美滋滋

总结

本文主要介绍了ijkplayer编译的整体流程,并且总结了小帽遇到的一些坑和解决的方法,希望可以帮助到大家;后边会继续挖掘ijkplayer的源码,敬请期待(才疏学浅,望各位多多担待)

(瞎写一点)近期在看魏晋南北朝史,看到一句话深有感触,分享出来给大家细品【国有任臣则安,有重臣则乱。树国本根不深,无干辅之固,则所谓任臣者,化而为重臣矣】

smallhatkai
原创文章 1获赞 0访问量 8
关注私信
展开阅读全文