「播放器」初识ExoPlayer

这只是一篇记录

又爱又恨的ijkplayer

刚开始准备改造视频播放器的时候,对比了ijkplayerExoPlayer,我们的用例是HLS源点播,因为ijkplayer中文资料更多,支持更多的格式,而且b站这么多的用户,”兼容性”应该更好,所以毫不犹豫选择了ijkplayer。实际使用起来也并不复杂,简单封装修改了IjkVideoView,根据需要修改编译配置,编译so库,API使用非常方便。但是在使用了大半年ijkplayer后,被几个问题一直困扰,导致现在不得不考虑更换。

遇到的问题:

  1. 经常出现加载失败,一直在加载
  2. 错误状态不清晰,比较难定位问题
  3. IjkMediaPlayer不保证复用安全
  4. 在某些4.4机型上频繁创建IjkMediaPlayer会崩溃

加载失败的问题最严重,虽然用户量不算大,但经常收到反馈,出现的情况比如播放时频繁加载、播放一段时间就卡住了,一直显示在加载,由于IjkMediaPlayer不保证复用安全,只能重新创建MediaPlayer。起初我以为只是用户的网络问题,或者是视频服务、CDN问题。但随着类似的反馈越来越多,问题肯定出在播放器上,我也尝试过各种设置,修改了各种参数,还有类似ijkhttphook等等,都无法解决。加载慢还带来了seek慢问题,seek之后需要等待很长时间才生效,当然这和视频源也有很大的关系,但在iOS、web端都正常。

错误状态不清晰也让我逐渐对她失去信心,视频源、播放设备都可能会出现问题,因为无法定位问题,浪费了很多时间联系用户,错误内容也没有直观的信息,每次收到出错日志都慌的一匹。

其他的如在某些4.4机型上频繁创建IjkMediaPlayer会崩溃,不断累积的native异常等,可靠性也不算好。不能确定是否是硬解软解与显示surface的问题,在Profiler里观察到播放效率(耗电量)明显偏高,内存占用也不少。

我并不擅长FFmpeg和c语言,没有信心在ijkplayer的基础上修改好,再加上一些安全性问题,需要修改之前的自定义协议方式,综合以上,在前段时间开始准备迁移到ExoPlayer。

迁移到ExoPlayer

首先想好做什么

目标很清晰,替换IjkMediaPlayer -> PlayerIjkVideoView -> PlayerView,因为并不打算继续使用ijkplayer,而且两个播放器API差异不算小,所以没有使用接口抽象继续兼容ijkplayer,播放器与业务相关的API改动不大,只需要替换player即可。除此之外我还需要加入部分解密算法,实现自定义协议、支持解密,native代码混淆。

研究原理

ExoPlayer资料非常丰富详细,包含官方文档、google io视频、丰富的github issue。通过这些方法解决了我遇到的所有问题,并且让我了解了很多相关知识,后面会介绍我涉及到的部分。解密的部分可以通过OpenSSL配合自定义DataSource实现,native代码混淆可以使用ollvm,他们的用法资料也非常多。

选择方案

得益于ExoPlayer清晰灵活的结构,很明确,ExoPlayer自定义数据源 + OpenSSL解密数据源 + ollvm native代码混淆。

初步实现

生产环境的大项目可不适合试错,打开配置好的模板demo,编写简单可播放sample的示例,自定义数据源参考ExoPlayer OkHttp extension

1
2
3
4
5
6
7
// 创建数据源
HttpDataSource
HttpDataSourceFactory

// 修改数据加载方法,处理请求和响应,满足自定义协议
@Override
public long open(DataSpec dataSpec) throws HttpDataSourceException {}

OpenSSL解密数据源,配置native开发环境,编译OpenSSL动态(静态)链接库,导入头文件,参考官方demo实现AES解密的相关jni方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
jbyte *key = (*env)->GetByteArrayElements(env, decryptKey, JNI_FALSE);
jbyte *iv = (*env)->GetByteArrayElements(env, encryptionIv, JNI_FALSE);

EVP_CIPHER_CTX *ctx;
/* Create and initialise the context */
if (!(ctx = EVP_CIPHER_CTX_new())) {
LOGE("initialise context failed -1");
return -1;
}

/*
* Initialise the decryption operation. IMPORTANT - ensure you use a key
* and IV size appropriate for your cipher
* In this example we are using 256 bit AES (i.e. a 256 bit key). The
* IV size for *most* modes is the same as the block size. For AES this
* is 128 bits
*/
if (1 != EVP_DecryptInit_ex(ctx, EVP_aes_128_cbc(), NULL,
(const unsigned char *) key,
(const unsigned char *) iv)) {
LOGE("DecryptInit failed -2");
return -2;
}

(*env)->ReleaseByteArrayElements(env, decryptKey, key, JNI_FALSE);
(*env)->ReleaseByteArrayElements(env, encryptionIv, iv, JNI_FALSE);

return (jlong) ctx;

使用FFmpeg准备好加密的HLS源,验证sample正确播放

1
2
3
4
ffmpeg -i 0_blv.mp4 \
-c copy -bsf:v h264_mp4toannexb -hls_time 5 -hls_list_size 0 \
-hls_key_info_file key.info \
encryptionM3U8/output.m3u8

ollvm native代码混淆,编译支持混淆的llvm,修改ndk toolchain,配置支持混淆的ndk路径,因为native代码并不多,支持的混淆方式都加上了,最后用IDA Pro查看是否混淆成功,一行简单的return代码被加上了几十层循环。

1
2
3
4
5
6
externalNativeBuild {
cmake {
// 添加各种混淆方式
cFlags "-mllvm -sub -mllvm -bcf -mllvm -fla -mllvm -sobf"
}
}

优化细节

在应用到项目之前,可以尝试优化一些细节,比如在线播放的时候加上CacheDataSource作为临时缓存,能极大提升拖动进度条的体验,减少流量消耗

1
SimpleCache(downloadContentDirectory, LeastRecentlyUsedCacheEvictor(CACHE_MAX_SIZE))

提前尝试好各种参数配置,如loadControlLoadErrorHandlingPolicy,整理好一些工具、辅助类方便处理MediaSessionPlayerNotification、截图、状态栏等等。清晰的错误报告这次不会错过了,处理好方法的异常,返回值,关键点日志,API,注释,混淆配置。

应用到项目

最后就需要耐心的接入到项目中了,发布SDK到maven仓库,细心的修改不同功能模块,细粒度的对比检查、提交代码。经过几周的测试,生产环境试用,最终优雅的解决了之前自定义协议带来的问题,网络加载问题得到了极大的改善,内存消耗减少,性能也显著提升。

通过使用ijkplayer了解到很多视频播放相关的知识,但每当我想一窥究竟的时候,里面的内容却是不熟悉的c代码。亲切的Java代码,加上ExoPlayer清晰的结构,把里面复杂的模块拆分展现出来,终于认识到我写的一串newSimpleInstance代码到底是什么了。

参考资料: