導讀:本文內容是筆者最近實現的 web 端彈幕元件—— Barrage UI 的一個延伸。在閱讀本文的例項和相關程式碼之前,不妨先瀏覽專案文件,對元件的使用方式和相關介面進行了解。
各位童鞋如果經常上 B 站(bilibili.com) ,應該對 蒙版彈幕 這個概念並不陌生。
蒙版彈幕 是由知名彈幕視訊網站 bilibili 於 2018 年中推出的一種彈幕渲染效果,可以有效減少彈幕文字對視訊主體資訊的干擾。
關於 B 站蒙版彈幕的實現原理,其實網上已經有很多細緻的討論和研究。個人總結了一下,大致要點如下:
- 基於使用者資料和一些機器學習的相關應用,可以提煉出視訊的關鍵主體
- 服務端預先對視訊進行處理,並生成相應的蒙版資料
- 客戶端播放視訊時,實時地載入對應資源
- 通過一些前端的技術手段,實現彈幕的蒙版處理
客戶端方面,由於 B 站彈幕是基於 div+css 的實現,因而採用了 svg 格式來傳輸向量蒙版(至少目前是這樣),通過 CSS 遮罩的方式實現渲染。
逼乎上有一篇關於這個方案的討論,感興趣的童鞋可以移步 這裡 進行了解。
Barrage UI
Barrage UI 是個人最近實現的一個前端彈幕元件,主要用於在前端頁面中掛載彈幕動畫。
元件提供了一系列的操作介面以方便使用者對彈幕的相關特性進行定製。你也可以在渲染層面對動畫中的每一幀影像進行處理,比如:
- 實時讀取視訊資訊
- 對每一幀視訊影像進行實時處理,計算出摳圖蒙版
- 將計算出的蒙版傳給彈幕元件,以實現實時的蒙版彈幕
下面是基於 Barrage UI 元件實現的蒙版彈幕效果:
由於文中不方便嵌入視訊,Demo 的實際效果請移步到 此處 檢視。
下面我們來介紹如何實現上圖的動畫效果。
色鍵(色度鍵控)
Demo 中使用了初音小姐姐跳舞的視訊。最主要的特點是除了人物外,視訊的背景是比較一致的純色。對於這種型別的影像,我們可以使用 色鍵 的方式進行摳圖(生成“蒙版”)。
色度鍵控,又稱色彩嵌空,是一種去背合成技術。Chroma 為純色之意,Key 則是抽離顏色之意。把被拍攝的人物或物體放置於綠幕的前面,並進行去背後,將其替換成其他的背景。此技術在電影、電視劇及遊戲製作中被大量使用,色鍵也是虛擬攝影棚(Virtual studio)與視覺效果(Visual effects)當中的一個重要環節。
下圖是色鍵技術的一個示例:在綠幕前穿著藍色衣服的小姐姐,左圖為去背前,右圖為去背後的新背景。
如何扣取視訊影像
在瀏覽器環境中,我們可以通過 canvas 畫布實時地繪製視訊的每一幀,並從畫布中讀取到影像中每個畫素的 RGBA 資訊,檢測每個點的 R(red)、G(green)、B(blue) 值是否滿足要求,最終將需要扣除的畫素的 A(alpha) 值置為 0,即可得到用於合成蒙版彈幕的蒙版影像。
注意:
Barrage UI 元件的蒙版功能是基於 Canvas 2D API 的 CanvasRenderingContext2D.globalCompositeOperation
屬性實現的(使用了 source-in
的混合模式),因而只需將不需要的畫素設定為透明(alpha=0)即可,並不需要改變影像的 RGB 色值。
下面介紹此案例的程式碼實現。
具體實現
安裝 Barrage UI 元件
直接使用 yarn 或 npm 安裝此元件:
yarn add barrage-ui
or npm install --save barrage-ui
HTML + CSS
準備一個 video
元素用於播放視訊,video
的父級元素用於掛載彈幕:
<div id="container">
<video id="video" src="videos/demo.mp4" controls></video>
</div>
根據視訊的實際尺寸(880×540)設定 #container
與 #video
的樣式:
html,
body {
font: 14px/18px Helvetica, Arial, `Microsoft Yahei`, Verdana, sans-serif;
width: 100%;
margin: 0;
padding: 0;
background: #eee;
overflow: hidden;
}
#container,
#video {
width: 880px;
height: 540px;
}
#container {
margin: 0 auto;
margin-top: 50vh;
margin-left: 50vw;
transform: translate(-50%, -50%);
background-color: #ddd;
}
建立彈幕
import Barrage from `barrage-ui`;
import data from `utils/mockData`;
// 獲取父級容器
const container = document.getElementById(`container`);
// 建立彈幕例項
const barrage = new Barrage({
container: container,
});
// 重置畫布高度,避免彈幕遮擋視訊播放控制元件
barrage.canvas.height = container.clientHeight - 80;
// 裝填彈幕資料
barrage.setData(data);
其中,mockData 是用於生成隨機彈幕資料的方法。
關於彈幕資料的內容與格式,詳見 Barrage UI 專案文件
實時獲取視訊影像
// 獲取 video 元素
const video = document.getElementById(`video`);
// 新建一個畫布來實時繪製視訊(純繪圖,不用新增進頁面)
const vCanvas = document.createElement(`canvas`);
vCanvas.width = video.clientWidth;
vCanvas.height = video.clientHeight;
const vContext = vCanvas.getContext(`2d`);
// 實時繪製視訊到畫布
barrage.afterRender = () => {
vContext.drawImage(video, 0, 0, vCanvas.width, vCanvas.height);
};
使用元件提供的渲染週期鉤子 .afterRender()
可以在彈幕動畫的每一幀影像渲染後,將視訊影像繪製到中間畫布 vCanvas
上。注意這裡的 vCanvas
畫布主要用於實時地獲取視訊影像,並不需要新增到頁面中。
實時計算蒙版資訊
// 渲染前讀取畫布 vCanvas 的資料,並處理為蒙版影像
barrage.beforeRender = () => {
// 讀取影像
const frame = vContext.getImageData(0, 0, vCanvas.width, vCanvas.height);
// 影像總畫素個數
const pxCount = frame.data.length / 4;
// 將 frame 構造成我們需要的蒙版影像
for (let i = 0; i < pxCount; i++) {
// 這裡不用 ES6 解構賦值的寫法,主要為了保證效能
// PS: 這裡如果用解構賦值語法將導致大量新物件的建立,是個很耗時的過程
const r = frame.data[i * 4 + 0];
const g = frame.data[i * 4 + 1];
const b = frame.data[i * 4 + 2];
// 將黑色區域以外的內容設為透明
if (r > 15 || g > 15 || b > 15) {
frame.data[4 * i + 3] = 0;
}
}
// 設定蒙版
barrage.setMask(frame);
};
使用元件提供的渲染週期鉤子 .beforeRender()
可以在彈幕動畫的每一幀影像渲染前計算出蒙版影像。其中,用於更新蒙版的介面為 .setMask()
。
視訊、彈幕的操作繫結
最後,為了讓彈幕的行為與視訊播放的操作協同,還需要進行一些繫結的操作:
// 繫結播放事件
video.addEventListener(
`play`,
() => {
barrage.play();
},
false
);
// 繫結暫停事件
video.addEventListener(
`pause`,
() => {
barrage.pause();
},
false
);
// 切換播放進度
video.addEventListener(
`seeked`,
() => {
barrage.goto(video.currentTime * 1000);
},
false
);
這裡分別用到 Brrage UI 元件的 .play()
.pause
.goto()
三個介面,分別用於播放、暫停和切換彈幕動畫的進度。需要注意的是,通過 video.currentTime
屬性獲取到的視訊播放進度是一個單位為 秒 的浮點數,需要轉換為 毫秒數 再傳給彈幕元件。
原始碼奉上
本文的案例已上傳 github,感興趣的童鞋可以點選 這裡 檢視原始碼細節。
關於 Barrage UI 元件如果有什麼建議和疑問,歡迎大家在專案中提 issue 給我,幫助我持續改進和迭代,更歡迎 star 和 PR。
感謝您能耐心讀到此處,如果覺得有趣或有用,不妨 點贊/評論/轉發 此文,再謝。