ExoPlayer 已经是封装过的视频播放框架了,为什么还要封装一把,这样不是过度封装?其实不然,要是哪天ExoPlayer 不维护了或者有更好的播放器了,又或者是有些视频exo不能播放其他的播放器可以播放,又或者ExoPlayer 的api大改,要是不封装直接提供给业务用,那业务要改的地方就多了,所以封装一把是很有必要的。
如何去封装,并能提供给compsoe 或 View 使用?
我们首先要来了解一下Exo 的基本使用。
ExoPlayer 的使用
ExoPlayer 大概的使用步骤和状态,如下。
- 初始化并获取播放器实例
我们使用Hilt 依赖注入,来获取ExoPlayer 内核
使用Builder配置一下缓存和回调相关的东西,然后得到播放器实例
缓存相关的的注入如下,我们暂且配置本地缓存的上限为 1g ,并且算法使用的是LRU
- 设置播放相关的参数、调用prepare方法
- 缓冲中
- 准备(可以播放)
- 播放中
- 暂停
- 停止,再次播放需要设置播放相关的参数并调用prepare方法
- 释放资源,需要构建新播放器才能播放
要获取ExoPlayer播放器相关的回调也很简单,只要设置一个Listener 就行了,如下
比如我们要获取播放的位置来更新进度相关的,可以在onEvents 回调中处理。
ExoPlayer 使用就讲这么多,怎么去封装并提供给业务使用才是本文的重点。
封装播放器
视频播放会占用主进程很多资源,所以我们还要把播放器相关的跨进程到子进程中去处理,这就涉及到跨进程通信相关的了,在安卓中可以用来跨进程通信的方案很多,比如LocalSocket,管道,EventFd,FIFO ,Binder 。Socket 基于流并且安全性较低而且使用都很麻烦,基于TCP的还有粘包问题。管道,FIFO,EventFd 这些都需要与文件打交道使用也很麻烦。从性能和安全使用上综合考虑,没有比binder 更适合播放器这种情况了,其实system_server进程把Surface 传递给app进程也是通过binder 来处理的。
定义两个接口,第一个用于播放器相关的,ExoPlayer播放器内核和我们提供给业务方的播放器门面类都是实现这个接口的,另一个用于 Binder 跨进程通信继承使用。
再定义两个AIDL接口,用于跨进程相关,一个用于播放控制,一个用于回调相关。
这些实现类我们待会再说。我们先来说下有限状态机。
播放器的整个流程用有限状态机FSM来控制,播放器初始化之后为 idle状态,release 为最终状态。
用事件来驱动整个状态机运行,并做出相应的操作。
比如准备事件,业务方通过调用prepare 方法并传递相关的参数,如在compose中使用
最终会在状态机内部执行,如下。
当然发送了事件也不能保证百分百就能切换到目标状态,比如我们发送playEvent 有时候播放器内部出错了导致视频播放失败,此时也不可能把状态机置于playing状态。
必须要播放成功才能切换。
多次发送同事件也可以进行过滤,当然状态机好处还不止这些。我们再来看看跨进程相关的。
Binder 是CS架构,这里使用service来进行传递binder,主进程要连接子进程,子进程要连接主进程。
我们定义两个Binder,用于连接并进行跨进程通信。
为什么IPlayerBinder 要继承IPlayer ?
因为这里我们可以使用 kotlin by 关键字,将跨进程播放器的真实调用委托给binder 进行调用,可以少写一部分代理的模板代码。
主进程的服务如下
如何扩展其他播放器内核
要添加一个新的播放器内核,只要实现IPlayer就行了,并在EntryPoint入口点进行注入,如下伪代码。
界面相关
处理seek
实现弹幕效果,弹幕移动使用graphicsLayer的translationX 来处理 ,并使用layout把弹幕放到最右边
开始播放时,启动用于执行动画的协程
再聊一下Compose
最后还想再聊一下Compose, 其实Compose 已经可以用于任何界面了,无论从哪方面来和View对比,都是碾压 View 那一套UI体系的,并能无缝兼容View 那一套体系。
和Flutter 对比,使用上面其实没什么可比的,Flutter 和 Compsoe都是声明式的框架,都是一家人,都是Google的出品的(不得不说Google开源的框架都是精品,EXO也是Google的),但是Flutter写出来的界面体验绝对比不过Compose,别问我为什么,因为触摸事件要传递到Flutter 引擎这一层JNI 调用所产生的性能损耗怎么都解决不了,但Flutter 也不是不能用,有些不注重体验的业务完全可以使用Flutter来写。