使用 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.

这会随着视频的播放反复进行,从而使一帧接一帧地处理并以色度键控效果显示。

查看此示例的完整源代码.

另见