使用画布操作视频

通过将 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" />
    </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。画布 c1 用于显示原始视频的当前帧,而 c2 用于显示执行色度键控效果后的视频;c2 预先加载了将用于替换视频中绿色背景的静态图像。
  2. JavaScript 代码从名为 processor.js 的脚本中导入。

JavaScript 代码

processor.js 中的 JavaScript 代码包含三个方法。

初始化色度键控播放器

doLoad() 方法在 HTML 文档首次加载时被调用。此方法的工作是准备色度键控处理代码所需的变量,并设置事件监听器,以便我们能够检测用户何时开始播放视频。

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();
    },
    false,
  );
};

此代码获取对 HTML 文档中我们特别感兴趣的元素的引用,即 video 元素和两个 canvas 元素。它还获取对两个画布的图形上下文的引用。这些将在我们实际执行色度键控效果时使用。

然后调用 addEventListener() 来开始监视 video 元素,以便我们在用户按下视频上的播放按钮时收到通知。响应用户开始播放,此代码获取视频的宽度和高度,并将每个值减半(我们在执行色度键控效果时会将视频的大小减半),然后调用 timerCallback() 方法来开始监视视频并计算视觉效果。

计时器回调

计时器回调在视频开始播放时(当“播放”事件发生时)最初被调用,然后负责定期建立自身,以便在视频开始播放时启动每个帧的键控效果。

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);
};

当调用此例程时,视频元素会显示最新的视频数据帧,如下所示

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

视频帧被复制到第一个画布的图形上下文 ctx1 中,指定我们之前保存的用于绘制帧的宽度和高度,以绘制半尺寸的帧。请注意,您可以将视频元素传递到上下文的 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.

此操作在视频播放时重复执行,因此会重复处理并显示带有色度键控效果的每一帧。

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

另请参阅