共计 4935 个字符,预计需要花费 13 分钟才能阅读完成。
图片起源:https://bz.zzzmh.cn/
本文作者:王永亮
在网易云音乐 8.0 改版中,接到一个播放中的视频能够点击「小窗」按钮收起到 mini 播放条中持续播放的需要,刚接到这个需要时心田是解体的,要晓得网易云音乐的 mini 播放条是一个可能会呈现在 App 中的任何 Activity 上的 View,在不同 Activity 之间跳转时,如何能保障视频能够从一个 Activity“无缝”转移到另一个 Activity 呢?
MediaPlayer 换绑
个别简略的视频播放性能我会应用零碎自带的 VideoView,只需几行代码就能够让视频播放起来,零碎自带的 VideoView 继承自 SurfaceView,并且将 MediaPlayer 的具体调用,包含 Surface 和 MediaPlayer 的绑定封装在外面,这样封装的劣势是简略易用,然而也存在一些问题,SurfaceView 和 MediaPlayer 齐全绑定在一起,一个 MediaPlayer 只能对应一个 SurfaceView,而小窗播放想做到的是 MediaPlayer 和 SurfaceView 能够一对多,在页面切换时 MediaPlayer 能够绑定新的 SurfaceView,就像一台电脑对应多个显示器。咱们的视频播放框架很好的解决了这个问题,如下图所示:
因为 App 中有一些对视频做动画的场景,所以框架中应用的是 TextureView,TextureView 和 MediaPlayer 应用 AIDL 进行通信,如下图所示:
从下面两图能够看出,视频播放框架中把所有的 MediaPlayer 放到了一个独自的 Video 过程的缓存池中来治理,正在应用的放在 Active 的池子中,闲置在 idle 池子中,闲置的 MediaPlayer 超过下限时会被回收,启动新页面时 VideoView 能够从 Video 过程的池子中获取闲置的 MediaPlayer,其余过程中的 VideoView 通过 AIDL 同 Video 过程中的 MediaPlayer 通信。
这种架构使不同 Activity 中的 VideoView 能够很不便的替换其绑定的 MediaPlayer,因为播放能力都在 MediaPlayer 中,所以在 MediaPlayer 同 TextureView 解绑时并不会导致播放的中断,新页面启动时,只有将正在播放的 MediaPlayer 同 TextureView 从新绑定,新的页面就能立即展现播放中的画面了。实际上视频播放框架最早并不是服务于无缝播放的场景,设计最早是出于以下起因:
- 自研的 MediaPlayer 在上线晚期稳定性并没有那么好,应用多过程能够避免播放器异样影响主过程其余性能
- 一般 VideoView 只能反对一个 TextureView 一个 MediaPlayer,而视频播放优化须要额定的视频播放器实现视频预加载的能力
- 缩小主过程内存占用,防止视频播放对音频播放等重要业务产生影响
- 播放器复用缩小对象创立
所以综合以上需要咱们设计了这套 MediaPlayer 和 TextureView 隔离的计划,如果是比较简单场景也能够思考应用单例持有 MediaPlayer,因为这套计划曾经很好的将 MediaPlayer 和 TextureView 隔离,所以咱们只须要通过给 MediaPlayer 池子减少一些获取被复用播放器的办法就能够很容易的反对 VideoView 和 MediaPlayer 的换绑,从取得无缝播放的成果了。
具体的换绑 MediaPlayer 流程如下图:
在原有 Activity 中,如果播放器是要被复用的,咱们会将播放器的惟一 id 和正在播放的资源 id 保留在一个全局地位,以此作为播放器可复用的标记。在新页面启动时,新页面的 VideoView 被创立,在新页面中会调用 VideoView 的 setDataSource 设置要播放的内容,setDataSource 会依据以后播放的内容和保留的全局播放器 id 在播放器池子中从新找到原来正在播放的播放器,并将 Surface 通过 AIDL 发送给被复用的 MediaPlayer 从新绑定,这样在不打断以后播放的状况下,视频播放的画面就无缝被转移到新的 Activity 中了。其中要留神的一个知识点是 Surface 自身就是反对跨过程传递的:
public class Surface implements Parcelable
另外这个计划中应用 MediaPlayer 对象的 hashCode 作为播放器的惟一 id,如果应用这个计划,大家也能够联合本人的状况设计惟一 id。
换绑计划的外围是 MediaPlayer 同 VideoView 的从新绑定,从新绑定只须要做到上面两步:
- 应用以后页面中定义的 onPrepare、onPause 等回调设置给播放器替换原有回调
- 从新绑定 Surface,须要留神的是有时 SurfaceTexture 并不是立即就能筹备好,没筹备好时能够在 onSurfaceAvailable 中从新绑定 SurfaceTexture。
这个计划基本上能满足绝大部分的无缝播放需要,不过也并非没有毛病,这个计划次要有以下几个问题:
- 在视频暂停时 MediaPlayer 从新绑定 TextureView,Surface 上会没有内容,这种状况能够先应用视频封面笼罩 TextureView,等从新播放时再移除封面。
- 原来的方案设计中将 AudioFocus 的获取封装在了 VideoView 中,在新的页面应用 MediaPlayer 开始播放时,因为原来持有 MediaPlayer 页面还持续持有着 MediaPlayer 的援用,所以会因为 AudioFocus 抢占而调用 MediaPlayer 的 pause 办法,从而造成在新的页面播放也会暂停,解决方案是将 AudioFocus 的监听放在 Video 过程中的 MediaPlayer 中,大家在应用这个计划时也能够留神下有没有相似问题。
- 原来播放器应用实现会追随页面销毁而被回收,在复用场景是不能回收的,这时要留神防止播放器透露。
终实现成果如下图:
“假”页面切换计划
在换绑计划之外,网易云音乐中也有一些其余的无缝播放计划实现,首先介绍一种实现比较简单,也是在网易云中比拟早应用的一种计划,“假”页面切换计划,由名字能够晓得,这种计划不是真正的在 Activity 之间进行跳转,而是利用 TextureView 能够像一般 View 一样挪动、做动画的个性,利用过渡动画,让成果看起来像是从一个页面跳转到了另一个页面,成果如下图所示:
在网易云音乐的视频 Feed 流中,视频播放时,点击热区能够在不暂停播放的状况下“开展”到播放详情页,具体的实现办法是将视频播放的 View 放在 Fragment 中,Fragment 的 Container 放在整个 ViewTree 的最顶层,点击播放时,将视频播放 Fragment 挪动到须要展现视频的地位并开始播放,须要点击进入详情页时,只须要对视频播放的 Fragment 做平移和缩放动画,在视频播放的 Fragment 下方再增加评论等其余的 Fragment。这里能够参考 Android 原生的 VideoView 的封装思维来实现:
public class VideoView extends SurfaceView
implements MediaPlayerControl, SubtitleController.Anchor {
参考 VideoView 源码能够将 SurfaceView 替换为 TextureView,再对应解决下 onSurfaceTextureAvailable 等回调即可。这种计划利用还是比拟宽泛的,比方京东、淘宝等的商品详情的介绍视频。这种计划尽管简略然而局限也比拟大,只能解决在同一个 Activity 中的场景,如果需要是在不同 Activity 中无缝播放切换这个计划就无奈满足了。
不同页面间播放器 Seek 计划
实现跨 Activity 场景无缝播放的另一个计划是关上新的页面时,在新的页面中应用新播放器从新关上资源,并依据原来保留的进度从新 seek 后再续播,这种计划其实并不能保障真正的“无缝”播放,毕竟 Activity 启动也要耗费一两百毫秒的工夫,不过这个计划最大的劣势是一些老逻辑进行很少的更改就能够反对无缝播放性能,比方在一些不是很重要的页面中,视频播放性能可能曾经存在并且播放逻辑耦合了很重的业务逻辑,这时 seek 计划就比拟适合了。
这个计划尽管简略然而也有一些须要留神的中央:
- 缓存复用晋升体验。播放在线视频时能够应用播放缓存复用来晋升用户体验,视频播放缓存能够采纳 url 代理下载的形式实现,个别做法是启动 Local Http Server 将视频播放的申请代理到本地 server,在本地 server 中将视频文件存储到指定的地位,这也是视频播放中比拟罕用的计划,缓存性能的能够参考 AndroidVideoCache。
- 零碎提供的 MediaPlayer 只能 Seek 到关键帧的问题。应用零碎播放器从新关上视频资源 seek 时,有时会无奈 seek 到原来播放的地位,甚至会间接跳转到视频播放的开始或者完结地位,视频越短压缩率越高就会越显著,这就是关键帧的问题。关键帧问题的解决方案是能够是基于 MediaCodec 本人实现播放器,能够参考或间接应用 Google 开源的 ExoPlayer。
关键帧被称为 I 帧,能够被看做是一帧没有压缩过的画面,解码的时候无需依赖其余帧,关键帧之间还存在 B 帧和 P 帧这样的压缩帧,须要依赖其余帧能力解码出残缺的画面,两个关键帧之间的距离被称为一个 GOP,在 GOP 内的帧零碎播放器是没方法间接 seek 的。
4. View 跨 Activity 复用计划
View 跨 Activity 复用是指手动应用 ApplicationContext 创立须要被复用的 View,并且应用单例 Manager 持有该 View,增加删除可复用 View 能够对立在 Activity 生命周期函数中实现,示例代码如下:
object Manager : ActivityLifecycleCallbacks
override fun onActivityStarted(activity: Activity) {
...
removePlayerBarFromWindow(activity)
addPlayerBarToWindow(activity)
}
override fun onActivityPaused(activity: Activity) {
...
if (activity.isFinishing && getMiniPlayerBarParentContext() == activity) {removePlayerBarFromWindow(activity, true)
}
}
private fun getPlayerBar(activityBase: Activity): MiniPlayerBar {synchronized(this) {if (miniPlayerBar == null) {miniPlayerBar = MiniPlayerBar(activityBase.applicationContext)
}
...
return miniPlayerBar!!
}
}
实践上这是一种更加灵便的计划,应用 Application 作为 View 的 Context 也不必放心透露问题,不过因为在这次小窗的需要中波及到老的页面和新页面的播放器复用,在很多场景下并不是一个对立的播放 View,所以没有采纳这种计划,不过这个计划在网易云音乐的音街 App 的 mini 播放条上曾经被应用,有趣味的小伙伴也能够尝试下。
总结
以上是网易云音乐中一些无缝播放的计划的总结,次要介绍了一下网易云音乐中几种无缝播放能力的实现思路,给大家计划选型做参考,如果有其余的计划也欢送交换。网易云音乐中的计划是从简略到简单逐步演进而来,随着需要一直迭代变成明天的样子,集体了解设计方案时不必过分的谋求大而全,适宜以后场景的才是最好的,好的架构不仅要靠好的设计,也要靠一直的改良优化。
本文公布自 网易云音乐大前端团队,文章未经受权禁止任何模式的转载。咱们长年招收前端、iOS、Android,如果你筹备换工作,又恰好喜爱云音乐,那就退出咱们 grp.music-fe(at)corp.netease.com!