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