K 歌移动客户端19年在直播间中上线了视频礼物资源动画能力,使用特制的视频资源加通道导出和混合 (基于企鹅电竞vapx方案),支持了细腻的视频动画素材播放渲染,同时解决了直接播放视频背景无法透明的问题。
在随后的新 pc 主播端项目中我们对直播工具进行重构 (主界面 UI 基于 web 完成),礼物动画部分由于当时没有 web 版本的 sdk,为了复用线上已有的动画资源以及和移动端保持对齐的效果,web 端通过 video canvas/webgl 实现进行了支持。
此文回顾整理一下之前的实现流程与细节。
0. 业务流程
首先基于线上方案,上架一个动画资源的整体的流程为以下几步:
- 将多个不同视频样本上传到配置平台,同时填写配置 (类型/方向/尺寸等);
- 后台根据配置生成生成礼物编号入库,将视频发到 CDN 上架;
- 前端通过后台接口可拉到礼物资源所对应的视频地址与配置参数;
- 前端触发播放礼物动画。

下面主要讲解渲染播放方面的实现。
1. 实现逻辑
从方案和动画资源来看,为了解决背景透明的问题,视频文件都包含了两个部分:原动画部分以及单独导出的 alpha 通道。只是尺寸和方向不同。
因此逐帧将两个部分的 rgb 分别取出,进行通道混合,就能实现透明背景的画面。 具体来讲,假设资源在某一帧某一点的 rgb 分别为: 原片部分: rgb(R, G, B) alpha部分: rgb(A, A, A) 混合后的动画相应位置就是: rgba(R, G, B, A)
2. 最简方案
首先视频一般用 <video> 播放。结合上面这个角度讲,自然先想到了使用 canvas:让 video 隐藏播放,同时在播放过程中逐帧 drawImage 到画布,读取 ImageData,按照位置取出两部分,混合后重新 putImageData 显示。
共使用到两个 canvas 画布,一个用来离屏读写 imageData, 计算后放到另一个真实看到的画布。
这样第一版就快速实现了。单个 demo 来看是 Ok 的。
但是接下来仔细测试,还有不少优化空间:
3. 加载问题
首先尝试多个动画同时渲染,调低网速,会发现动画跟随缓冲而卡顿。(这里为了方便实验关闭了缓存)

从 network 来看,同时加载播放多个线上视频,并行占用带宽,播放缓冲会导致 video 暂停,实际结果就是 fps下降了。礼物动画这种场景本身不应该出现播放中的等待。因此需要支持加载完整个视频后再本地播放。
这里改为使用 xhr2 将视频完全下载后转为 blob 再放到 video 让其能够一次顺畅播完。
修改后的效果。整体首次播放比刚刚要顺畅了。
但也有代价,就是增加了加载准备时间。后续可以通过离线缓存和空闲时预加载来弥补和提升。
视频动画资源通常很大,单个在2-5m左右甚至更多,一些高频礼物如果实时下载延迟会比较大,没有缓存反复下载也会导致带宽消耗浪费。因此也加上了 service worker 进行资源的持久化。策略使用为 CacheFirst (基于workbox)。冷启动空闲时也可以手动预加载部分资源。
4. CPU消耗
这时继续再多增加同屏个数来测试,下面翻一倍增加到 8 个,同时反复多次循环重复播放,发现性能大幅下降了,非常卡顿。
重复播放时资源都有了,这次肯定不是加载问题。这时打开 performance monitor,发现 cpu 消耗非常高,基本都是 100%。
于是通过录制 performance 来分析,发现瓶颈主要在 canvas 的 getImageData / drawImage 以及 pixelData 的遍历计算上。这里对 CPU 的消耗太高了。 
这里 demo 单个视频是 1440x1152,等于每一帧要 get 出 6635520 个 pixelData (pixel * rgba)。遍历计算 1658880 次结果色值。n个动画再乘以n,计算量非常大,导致高负载,fps也相应降低。 
另外这里高频的绘图场景,直觉上应该是 GPU 的长项才对。但通过系统监控看到GPU在打开前后负载没太大的变化 (在20-30%间波动)。能否想办法发挥 GPU 的能力?
因此重新思考方案,看能否找到其他合适的方案可以代替 ImageData 操作和计算。
5. 更换 WebGL
按照前面的设想 (尝试将消耗转移和利用 GPU),于是考虑使用 WebGL 来看看能否实现。
理论上就是每帧两个部分的对应区域叠加混合。刚开始凭直觉找了一圈 Blend 和 composite 的方案不合适。后来想起 ImageData、 <image /> 这些是可以作为 texture 纹理在 WebGL 中使用的。
那 <video /> 能否当做纹理?查阅文档果然也可以。然后思路就来了:我们知道纹理是可以互相叠加的,在渲染过程中着色器可以清楚的表达如何去处理最后的色值。那理论上我们就可以直接把整个 video 作为纹理,取不同的区域去参与渲染计算和叠加。
根据这个逻辑,梳理一下代码实现。首先创建程序和挂载着色器:
顶点着色器 (上面的Shaders.vertex) 里把坐标和变量声明:

创建两个坐标变量 AlphaCoord 和 ColorCoord,分别代表两个区域的位置 (gl很啰嗦,已省略部分非关键代码):
再来看看片段着色器 (前面传入的Shaders.fragment) 。根据前文的逻辑,带入坐标,分别从两个区域各取出 rgb 和 alpha,合成新的color:
三行就搞定了。就和我们自己计算 rgb 一样,只不过是手动 CPU 计算变成了编译到 GPU 运算。
最后逐帧使用 video 创建纹理并渲染:

经过编码和调试,成功跑起来后,再次打开 performance,cpu 峰值和均值都下降了(90-100% 到 20-30%):
fps也提升了3-4倍(4-5 到 20左右)。证明思路是ok的。
对比此时的系统负载 GPU 比原先增加15%(从30%到45%)。CPU从60%左右下降到20-30%。

再降到同屏 4-5 个的情况下,可以稳定在60fps,足够承载业务场景。
6. 总结
打开了 WebGL 的宝盒,到此后续还有没有更多优化空间?比如冷启动预缓冲时间的缩短;移动端的适配,卡顿检测等等。另外还有没有比 video 纹理叠加更高效率的方式,或者更大胆的想法,能否 MSE 或 WASM 跳过 video 直接到 WebGL?更多细节还有待后续研究。
腾讯音乐全民k歌招聘客户端、web前端、后台开发,点击查看原文投递简历!或邮箱联系: godjliu@tencent.com