用 CMake 为 ijkplayer 增加 android native debug 支持

给 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_ABIAPP_PLATFORM 的值不同,其中 APP_PLATFORM 取值和 build.gradle 中定义的 minSdkVersion 相同。 ijkplayer 最低竟然支持 sdk version 9,确实感谢 B 站项目组初期在这方面的辛苦付出。

这种 module 确实方便采用 ndk-build 工具的项目管理,但是我在使用过程中还是觉得有些不太方便之处:

  1. 在 android studio 中搜索 c 代码,搜索结果中重复出现多次
  2. 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