使用 canvas 操作影片

透過結合 video 元素和 canvas 的功能,您可以即時操作影片資料,為顯示的影片新增各種視覺效果。本教程演示瞭如何使用 JavaScript 程式碼執行色度鍵控(也稱為“綠色螢幕效果”)。

文件內容

下面顯示了用於渲染此內容的 HTML 文件。

html
<!doctype html>
<html lang="en-US">
  <head>
    <meta charset="UTF-8" />
    <title>Video test page</title>
    <style>
      body {
        background: black;
        color: #cccccc;
      }
      #c2 {
        background-image: url("media/foo.png");
        background-repeat: no-repeat;
      }
      div {
        float: left;
        border: 1px solid #444444;
        padding: 10px;
        margin: 10px;
        background: #3b3b3b;
      }
    </style>
  </head>

  <body>
    <div>
      <video
        id="video"
        src="media/video.mp4"
        controls
        crossorigin="anonymous"></video>
    </div>
    <div>
      <canvas id="c1" width="160" height="96"></canvas>
      <canvas id="c2" width="160" height="96"></canvas>
    </div>
    <script src="processor.js"></script>
  </body>
</html>

從中可以提取的關鍵點是:

  1. 此文件設定了兩個 canvas 元素,ID 分別為 c1c2。Canvas c1 用於顯示原始影片的當前幀,而 c2 用於顯示應用色度鍵控效果後的影片;c2 預載入了將用於替換影片中綠色背景的靜態影像。
  2. JavaScript 程式碼從名為 processor.js 的指令碼中匯入。

JavaScript 程式碼

processor.js 中的 JavaScript 程式碼包含三個方法。

初始化色度鍵控播放器

當 HTML 文件初始載入時,會呼叫 doLoad() 方法。此方法負責準備色度鍵控處理程式碼所需的變數,並設定事件監聽器,以便我們能夠檢測使用者何時開始播放影片。

js
const processor = {};

processor.doLoad = function doLoad() {
  const video = document.getElementById("video");
  this.video = video;

  this.c1 = document.getElementById("c1");
  this.ctx1 = this.c1.getContext("2d");

  this.c2 = document.getElementById("c2");
  this.ctx2 = this.c2.getContext("2d");

  video.addEventListener("play", () => {
    this.width = video.videoWidth / 2;
    this.height = video.videoHeight / 2;
    this.timerCallback();
  });
};

此程式碼獲取 HTML 文件中特別感興趣的元素(即 video 元素和兩個 canvas 元素)的引用。它還獲取兩個 canvas 中每個 canvas 的圖形上下文的引用。這些將在我們實際執行色度鍵控效果時使用。

然後呼叫 addEventListener() 來開始監視 video 元素,以便在使用者按下影片播放按鈕時獲得通知。為響應使用者開始播放,此程式碼獲取影片的寬度和高度,並將每個值減半(我們將在執行色度鍵控效果時將影片大小減半),然後呼叫 timerCallback() 方法開始監視影片並計算視覺效果。

計時器回撥

計時器回撥最初在影片開始播放時(當發生“play”事件時)被呼叫,然後負責安排自己定期被呼叫,以便為每一幀啟動鍵控效果。

js
processor.timerCallback = function timerCallback() {
  if (this.video.paused || this.video.ended) {
    return;
  }
  this.computeFrame();
  setTimeout(() => {
    this.timerCallback();
  }, 0);
};

回撥做的第一件事是檢查影片是否正在播放;如果不是,回撥將立即返回而不做任何事情。

然後它呼叫 computeFrame() 方法,該方法對當前影片幀執行色度鍵控效果。

回撥做的最後一件事是呼叫 setTimeout() 來安排自己儘快再次被呼叫。在實際應用中,你可能會根據影片的幀率來安排這項工作。

操作影片幀資料

下面顯示的 computeFrame() 方法負責實際獲取幀資料並執行色度鍵控效果。

js
processor.computeFrame = function () {
  this.ctx1.drawImage(this.video, 0, 0, this.width, this.height);
  const frame = this.ctx1.getImageData(0, 0, this.width, this.height);
  const data = frame.data;

  for (let i = 0; i < data.length; i += 4) {
    const red = data[i + 0];
    const green = data[i + 1];
    const blue = data[i + 2];
    if (green > 100 && red > 100 && blue < 43) {
      data[i + 3] = 0;
    }
  }
  this.ctx2.putImageData(frame, 0, 0);
};

當呼叫此例程時,video 元素正在顯示最新的影片資料幀,其外觀如下:

A single frame of the video element. There is a person wearing a black t-shirt. The background-color is yellow.

該影片幀被複制到第一個 canvas 的圖形上下文 ctx1 中,指定的高度和寬度是我們之前儲存的用於以一半大小繪製幀的值。請注意,您可以將 video 元素傳遞到上下文的 drawImage() 方法中,將當前影片幀繪製到上下文中。結果如下:

A single frame of the video element. There is a person wearing a black t-shirt. The background-color is yellow. This is a smaller version of the picture above.

對第一個上下文呼叫 getImageData() 方法會獲取當前影片幀的原始圖形資料的副本。這提供了 32 位畫素影像原始資料,我們可以對其進行操作。然後,我們透過將幀影像資料的總大小除以四來計算影像中的畫素數。

for 迴圈遍歷幀的畫素,提取每個畫素的紅色、綠色和藍色值,並將這些值與預定數字進行比較,這些數字用於檢測將被替換為從 foo.png 匯入的靜態背景影像的綠色螢幕。

在幀影像資料中找到的每個屬於綠色螢幕引數範圍內的畫素,其 alpha 值將被替換為零,表示該畫素完全透明。因此,最終影像的整個綠色螢幕區域將 100% 透明,當它使用 ctx2.putImageData 繪製到目標上下文時,結果將是疊加在靜態背景上。

生成的影像看起來像這樣:

A single frame of the video element shows the same person wearing a black t-shirt as in the photos above. The background is different: it is the Firefox logo.

這會隨著影片的播放反覆進行,從而使一幀接一幀地處理並以色度鍵控效果顯示。

檢視此示例的完整原始碼.

另見