【小案例】基於色鍵技術的純客戶端實時蒙版彈幕

導讀:本文內容是筆者最近實現的 web 端彈幕組件—— Barrage UI 的一個延伸。在閱讀本文的實例和相關代碼以前,不妨先瀏覽項目文檔,對組件的使用方式和相關接口進行了解。css

各位童鞋若是常常上 B 站(bilibili.com) ,應該對 蒙版彈幕 這個概念並不陌生。html

蒙版彈幕 是由知名彈幕視頻網站 bilibili 於 2018 年中推出的一種彈幕渲染效果,能夠有效減小彈幕文字對視頻主體信息的干擾。前端

關於 B 站蒙版彈幕的實現原理,其實網上已經有不少細緻的討論和研究。我的總結了一下,大體要點以下:git

  1. 基於用戶數據和一些機器學習的相關應用,能夠提煉出視頻的關鍵主體
  2. 服務端預先對視頻進行處理,並生成相應的蒙版數據
  3. 客戶端播放視頻時,實時地加載對應資源
  4. 經過一些前端的技術手段,實現彈幕的蒙版處理

客戶端方面,因爲 B 站彈幕是基於 div+css 的實現,於是採用了 svg 格式來傳輸矢量蒙版(至少目前是這樣),經過 CSS 遮罩的方式實現渲染。github

逼乎上有一篇關於這個方案的討論,感興趣的童鞋能夠移步 這裏 進行了解。web

Barrage UI

Barrage UI 是我的最近實現的一個前端彈幕組件,主要用於在前端頁面中掛載彈幕動畫。npm

組件提供了一系列的操做接口以方便用戶對彈幕的相關特性進行定製。你也能夠在渲染層面對動畫中的每一幀圖像進行處理,好比:canvas

  1. 實時讀取視頻信息
  2. 對每一幀視頻圖像進行實時處理,計算出摳圖蒙版
  3. 將計算出的蒙版傳給彈幕組件,以實現實時的蒙版彈幕

下面是基於 Barrage UI 組件實現的蒙版彈幕效果:瀏覽器

色鍵蒙版效果

因爲文中不方便嵌入視頻,Demo 的實際效果請移步到 此處 查看。markdown

下面咱們來介紹如何實現上圖的動畫效果。

色鍵(色度鍵控)

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。

感謝您能耐心讀到此處,若是以爲有趣或有用,不妨 點贊/評論/轉發 此文,再謝。

相關文章
相關標籤/搜索