给 ijkplayer 增加了 cmake 构建方式,支持在 android studio 中进行 c 代码断点调试。
直接上手请前往 https://github.com/befovy/ijkplayer#build-android-via-cmake
如果想了解一下整个过程请继续阅读本文。
ijkplayer 简介
ijkplayer 是由 B 站开源的一款移动端播放器。基于 ffmpeg 中 ffplay 开发,并添加 android MediaCodec、iOS VideotTolbox 视频硬解码支持以及 opengles、NativeWindow(android) 渲染,在当前的移动直播热潮中,被大量使用。 但使用 ijkplayer 进行 android 播放器开发的过程中,调试起来稍微有些麻烦,官方提供了ndk 调试的几个patch 文件,并且有一段描述,是基于 ndk-build android.mk 工具链构建的。说实话由于没有做成开箱即用,我也没认真看过这部分内容。 由于我对 cmake 的了解更多一些,加之 google 官方也逐渐将 ndk 的工具链由 ndk-build 转向 cmake,并在 android studio 中对 cmake 提供了更好的支持。同时 cmake 也是跨平台的构建工具,方便后续将 ijkplayer 扩展至其它平台,所以我用 cmake 重新实现了 ijkplayer 的构建过程。
只要不是修改 ffmpeg 的源代码,其它内容包括 ijkplayer 的 c 代码修改, java 层修改,都可以在 android studio 中一键运行、调试。
关于 ijkplayer 内部核心代码的分析,多个线程的工作内容,解码部分,音视频同步控制等部分的更深入的介绍,可以参考金山云视频的分析文章 https://www.jianshu.com/p/daf0a61cc1e0 , 我最开始也是从这一篇文章入手逐渐读懂 ijkplayer 源代码的。
题外话:ijkplayer 在 github 上的提交记录自 k0.8.8 版本之后就再也没有了,好多人评论说项目已经放弃维护了,但是细心寻找会发现在另一个项目 Bilibili/ffmpeg 中,还不断有提交记录。说明项目还是在不断开发,只是重心放在实现新的 ffmpeg protocol 上,没有及时维护 ijkplayer 项目上的 issues。
下面具体说说一些具体的修改内容。
android 工程结构调整
ijkplayer 的项目本身具有多个 android module。
ijkplayer-arm64
ijkplayer-armv5
ijkplayer-armv7a
ijkplayer-x86
ijkplayer-x86_64 # 上面几个都是一个module 壳,shell 命令编译好 so 库放在指定目录中
ijkplayer-example # 示例APP项目
ijkplayer-exo # 对 google exoplayer 的一层封装适,配合ijkplayer 定义的 mediaplyer 接口
ijkplayer-java # ijkplayer java 层代码,对接 native 层代码,以及硬解码器的选择
项目中为不同的 cpu 架构都单独设置了 module,这几个 module 最大的区别就是在 Application.mk 中 APP_ABI
和 APP_PLATFORM
的值不同,其中 APP_PLATFORM
取值和 build.gradle 中定义的 minSdkVersion 相同。
ijkplayer 最低竟然支持 sdk version 9,确实感谢 B 站项目组初期在这方面的辛苦付出。
这种 module 确实方便采用 ndk-build 工具的项目管理,但是我在使用过程中还是觉得有些不太方便之处:
- 在 android studio 中搜索 c 代码,搜索结果中重复出现多次
- android studio 中的 c 代码没有语法高亮。
对于问题1,我找到的解决方案是:
//NOTE:: run `./gradlew ideaModule` to apply exclude dirs
apply plugin: 'idea'
idea.module {
excludeDirs += file('ijkplayer-armv7a/src/')
excludeDirs += file('ijkplayer-arm64/src/')
excludeDirs += file('ijkplayer-armv5/src/')
excludeDirs += file('ijkplayer-x86/src/')
excludeDirs += file('ijkplayer-x86_64/src/')
}
在工程根 build.gradle 中增加这样的配置,并执行 ./gradlew ideaModule
, 就会在搜索结果中排除来自这几个文件夹的内容。
对于问题2,我当然是用 cmake 解决了,新建一个 module, 取名 fijkplayer-full。换个名字方便和原项目进行区分,同时 f 也表示full
, 我在这个 module 中囊括了原来 6 个 module 提供的内容。 f 还有别的含义,有机会在其它地方会提到。
在 fijkplayer-full 的 build.gradel 中增加 cmake 的配置以及引入 ijkplayer-java module 的 java 源代码。
android {
.......
.......
sourceSets.main {
java.srcDirs = ["$rootProject.rootDir/ijkplayer-java/src/main/java/"]
jni.srcDirs = [] // This prevents the auto generation of Android.mk
}
externalNativeBuild {
cmake {
path 'src/main/CMakeLists.txt'
}
}
}
修改后,fijkplayer-full 就成了 “六神合体” 了。
对于不同cpu 架构区分不同的 minSdkVersion 可以通过 productFlavors 实现,这一块我还没有具体考虑充分,目前是统一设置了大家用得最多的 minSdkVersion 16。
此部分修改的完整内容点我查看, github.com/befovy/ijkplayer
CMakeLists.txt
编写 CMakeLists.txt 完全是个翻译工作,将 Android.mk 中的规则翻译过来就行。完成后通过 CMake 方式最终生成的 aar 和原始方式生成的aar 文件大小几乎没什么差别,见下图。 fijkplayer-full 只编译了 armv7a 架构,和 ijkplayer-armv7a 模块编译结果的对比。 fijkplayer-full 中 classes.jar 较大是因为包含了 ijkplayer-java module 中的内容。 还可以调整CMake 参数进一步减小生成库的大小。
转到 CMake 工具链的过程还遇到下面几个问题。
libffmpeg.so 找不到
libffmpeg.so 是事先通过 shell 命令编译成的,在 ffmpeg build 文件夹中,并且几乎不会改变,除非是要修改 ffmpeg。 在 CMake 中通过
add_library(ijkffmpeg SHARED IMPORTED)
set_target_properties( # Specifies the target library.
ijkffmpeg
PROPERTIES
IMPORTED_LOCATION ${FFMPAG_SHARED_DIR}/libijkffmpeg.so
)
引入预编译好的 ijkffmpeg,在编译 ijkplayer 的过程可以顺利完成链接。但是打包运行找不到 libijkffmpeg.so。分析最终的 apk 以及 fijkplayer-full.aar ,发现其中都没有 libijkffmpeg.so 文件,所以是打包 aar 的时候没有将这个文件打包进去。查看工程目录结构,发现编译好的 libijkplayer.so 和 libijksdl.so 都在文件夹 /build/intermediates/cmake/debug/obj/armeabi-v7a
中,所以还需要修改 CMakeLists.txt 在每次编译的时候将 libijkffmpeg.so 复制过去。最后的解决方案是
add_library(ijkffmpeg SHARED IMPORTED)
set_target_properties( # Specifies the target library.
ijkffmpeg
PROPERTIES
IMPORTED_LOCATION ${FFMPAG_SHARED_DIR}/libijkffmpeg.so
)
add_custom_target(cpffmpeg
COMMAND mkdir -p ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}
COMMAND cp ${FFMPAG_SHARED_DIR}/libijkffmpeg.so ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}
)
add_dependencies(ijkffmpeg cpffmpeg)
没有找到一条命令能够解决的,所以麻烦了一点多写了点,用两个cmake 命令解决。
cpufeatures 编译出错
原项目中用到了 ndk 中的 cpufeatures,我在 CMake 文件中也引入并编译了 cpufeatures,但在编译 x86 以及 x86_64 架构的时候却出现了编译错误,具体是一处未识别的汇编指令。 最后发现是 ndk r15c 版本中 文件 $ANDROID_NDK/sources/android/cpufeatures/cpu-features.c 中少了几个换行符,补上之后(下面内容)编译就没问题了。
static __inline__ void x86_cpuid(int func, int values[4])
{
int a, b, c, d;
/* We need to preserve ebx since we're compiling PIC code */
/* this means we can't use "=b" for the second output register */
__asm__ __volatile__ ( \
"push %%ebx"
"cpuid\n" \
"mov %%ebx, %1\n"
"pop %%ebx\n"
: "=a" (a), "=r" (b), "=c" (c), "=d" (d) \
: "a" (func) \
);
values[0] = a;
values[1] = b;
values[2] = c;
values[3] = d;
}
意外的是我最终发现 ijkplayer 项目中没有用到 cpufeatures 的地方,可能是 gpl 协议的 android-ndk-profiler 部分用了吧,但项目中默认使用了一个假的 android-ndk-profiler,此部分没有去深究,性能分析使用 simpleperf 就足够了。 最后还是去掉 CMake 中 cpufeatures 的部分。
此部分 CMakeLists.txt 的完整内容请看
https://github.com/befovy/ijkplayer/blob/master/android/ijkplayer/fijkplayer-full/src/main/CMakeLists.txt
https://github.com/befovy/ijkplayer/blob/master/ijkmedia/CMakeLists.txt
https://github.com/befovy/ijkplayer/blob/master/ijkmedia/ijkplayer/CMakeLists.txt
https://github.com/befovy/ijkplayer/blob/master/ijkmedia/ijksdl/CMakeLists.txt
结束语
😄 附一张 NDK 断点调试的大图 👏
刚开始写博客,可能有些啰嗦,多多包涵。或是没有说明白的地方,请大方的前往原始地址进行评论。
最近对 Flutter 产生了兴趣,并打算用 ijkplayer 开发一个 Flutter 的播放器 Plugin, 祝我早日完成。
本文通过 mirror 和 hugo 生成,原始地址 https://github.com/befovy/blogback/issues/5
github.com/befovy/blogback/issues/5
vy/blogback/issues/5
tps://github.com/befovy/blogback/issues/5