WEB 视频开发(5)高性能的弹幕
弾幕(だんまく/danmaku)、barrage 是显示在影片上的评论,大量吐槽评论从屏幕飘过时效果看上去像是飞行射击游戏里的弹幕。弹幕视频系统源自日本弹幕视频分享网站(niconico动画),国内首先引进为 AcFun 以及后来的 bilibili。
这篇文章将介绍 3 种实现方法,并找出可以兼容多个浏览器并且流畅播放的方案。
[[TOC]]
思路
视频弹幕可以分为两种,一种是静止显示在视频的顶部或底部,另一种是从右到左滚动显示。静止弹幕比较简单,这里主要介绍滚动弹幕。
视频播放时会有很大量弹幕从右边移动到左边,为了方便观看会限制单次导入的弹幕数量,并且让弹幕之间不发生重叠。如果用户开启无限弹幕模式,那么就无需限制弹幕数量和弹幕之间是否重叠。
可以将视频分割成一行行的隧道,每次插入弹幕时选择当前最短的一个隧道插入。
速度
因为每个弹幕的显示时间固定,所以长弹幕的速度比短弹幕的速度快。但是长弹幕的速度也不能太快,这样会让它覆盖到前面比较短的弹幕。
为了让弹幕显示时间不要太长,那么就需要后一个弹幕到达最左端时,它前一个弹幕刚好消失。也就是后一个弹幕的速度比前一快,但是又不能太快,它的速度可以让它到达最左端时才追上前一个弹幕。
为了计算后一个弹幕的速度,我们可以设后一个弹幕的速度为 s,经过时间 t 后,后一个弹幕追上前一个弹幕。那么 t 就等于前一个弹幕要走的路程除以前一个弹幕的速度。知道了时间 t 那么 s 就等于后一个弹幕的要走的路程除以时间 t。
s = \frac {current.x} {({prev.x} + {prev.width}) \div prev.speed}实现
requestAnimationFrame 和 transform
使用 js 控制弹幕的 transform 来实现滚动弹幕
requestAnimationFrame 和 canvas
与第一种相似,但是不使用的 css,而是用 canvas 渲染
transform 和 transition
不自己控制动画,而是用 css3 transition 属性实现
这三种方法在 chrome 上都非常流畅。一般最先想到的方法就是第一种方法,它实现起来非常的简单,但是第一种方法在 IE 上有明显卡顿。
由于第一和第二种实现方式差不多,只是最终渲染的时候一个是操作 DOM,一个是操作 canvas,所以这里就只演示比较流畅的 canvas 版本。
通用接口
这篇文章只关注弹幕的实现,一些通用代码就用接口表示了。
下面是一条弹幕
播放器对象
canvas 版本
canvas 版本,实现比较直接。这里使用两个类 Danmaku 控制所有弹幕,Bullet 滚动的单个弹幕。
我们使用 requestAnimationFrame 来循环执行 draw 方法。
draw 方法在没一帧中执行下面 4 个步骤:
加载当前时间点的弹幕
把加载的弹幕填入到合适的隧道中
更新弹幕位置
清理超出界限的弹幕
其中比较复杂是 fill 方法。它会找出当前最短的隧道,然后判断是否可以插入弹幕。
对于每个弹幕 Item,都会有个 Bullet 与它对应,下面是 Bullet 类完整代码。
下面是 Danmaku 的完整代码。
性能
下面使用 chrome 开发者工具检测的截图,可以看到弹幕可以稳定到 60 fps。
canvas 实现起来比较简单,在现代浏览器上也比较流畅。
如果这么简单就找到了这么流畅的方法,那也太小瞧 IE 了,在 IE Edge 浏览器上这个版本会有一点卡顿,虽然没有第一种方法那么严重,但还是影响观影。
为了在 IE 上也能流畅的发射弹幕,就需要使用下面这个方法。
完整代码 @rplayer/danmaku
transform 和 transition 版本
这个版本很接近纯 CSS 的方式,将弹幕的滚动交给 transition。这个版本与 canvas 版本有比较大的区别,而且比 canvas 版本复杂的多。
原理
这个版本并不是利用 requestAnimationFrame,而是使用 video 元素的 timeupdate 事件,在该事件的回调函数中执行与 canvas 版本相同的步骤。
每个弹幕都有一个开始时间和结束时间,是播放视频的具体时间点。当视频播放到弹幕的开始时间时,就给弹幕设置 transition 属性,时长等于弹幕的结束时间减去开始时间。这样浏览器就自动帮我们执行弹幕滚动动画。并且监听弹幕的 transitionend 事件,当它触发时回收弹幕。
使用这种方法也让弹幕与视频时间轴绑定在一起。
实现
首先来看看 timeupdate 的回调函数,它与 canvas 的 requestAnimationFrame 非常类型。
为了弹幕之间不重叠,弹幕的结束时间要根据它前一个弹幕的结束时间来计算。通过上面的提到的公式,弹幕的时长的就等于
为了知道下一个弹幕的开始时间,我们需要知道弹幕完全展示的时间点,也就是弹幕的右侧刚好与播放器的右侧接触的时间点。只有前一个弹幕完全展示出来,后一个弹幕才能开始。
弹幕再加个 showTime 属性代表它完全展示的时间。
通过 showTime 就可以知道下一个弹幕的开始时间,后面加 0.2 秒,为了让每个弹幕之间有点间隙。
getShortestTunnel 获取 showTime 最短的一个隧道,这里设置只要这个隧道在未来两秒可以完全展示,就可以新增弹幕在这个隧道。
了解了 insert 方法下面来看,弹幕的 update 方法。
如果还没到开始时间或已经在运行,就直接返回,否则就设置 transition 和 transform 来发射弹幕。
上面的代码就已经可以顺利运行弹幕了。但是如果视频暂停了呢?这个版本并不可以取消定时器来暂停动画。
在 Danmaku 类中监听暂停时间,并执行所有运行弹幕的 pause 方法,在 pause 方法中我们需要计算当前的弹幕已经走了对少距离,并设置 transform,然后把 transition 设置为 0 来停止动画。
当视频恢复播放时会触发 timeupdate 事件,所以无需监听 play 事件。
性能
如果直接用上面代码在 IE 浏览器中运行,会发现有非常明显的卡顿现象,这是因为少了几个 CSS 属性。
will-change: transform; 告知浏览器该元素会有哪些变化的方法,这样浏览器可以在元素属性真正发生变化之前提前做好对应的优化准备工作。因为弹幕一出生就会设置 transform 属性,所以这个属性非常适合它。
为了启用硬件加速设置 transform 时,设置 x,y,z 三个值。而不是 translateX 这样并不能启用硬件加速。我们需要设置成 translateX(${player.width + this.width}px) translateY(0) translateZ(0)。还可以设置 perspective 和 backface-visibility 防止可能出现的动画闪动。
下面是 chrome 开发者工具的截图
这种方法可以在 IE Edge 上流畅的运行,但是到了 firefox 上有时可能会出现一点点小卡顿,影响并不大。canvas 版本在 firefox 上表现要比这种方法好一点点。
完整代码 @rplayer/danmaku
比较
canvas 版本
实现简单,可以流畅在主流的现代浏览器。缺点就是在 IE Edge 上有点卡顿
transform 和 transition 版本
实现复杂,可以流畅大部分浏览器包括 IE Edge,但在 firefox 有时会不如 canvas 版本流畅
transform, transition 版本是比 canvas 更好的选择,它在大部分浏览器上都可以流畅运行,而且对于弹幕种添加图片或一些其他特效的情况下 css 实现也更加简单。
总结
这里介绍了三种方法,第一种方法虽然有 canvas 版本的实现简单和 css 灵活性,但是非常卡顿。如果不嫌麻烦的话 canvas 版本和 transform,transition 版本也可以同时实现,firefox 中使用 canvas 版本,否则使用 transform,transition 版本。
参考
Last updated