使用 canvas 操作视频
通过结合 video 元素和 canvas 的功能,您可以实时操作视频数据,为显示的视频添加各种视觉效果。本教程演示了如何使用 JavaScript 代码执行色度键控(也称为“绿色屏幕效果”)。
文档内容
下面显示了用于渲染此内容的 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>
从中可以提取的关键点是:
- 此文档设置了两个
canvas元素,ID 分别为c1和c2。Canvasc1用于显示原始视频的当前帧,而c2用于显示应用色度键控效果后的视频;c2预加载了将用于替换视频中绿色背景的静态图像。 - JavaScript 代码从名为
processor.js的脚本中导入。
JavaScript 代码
processor.js 中的 JavaScript 代码包含三个方法。
初始化色度键控播放器
当 HTML 文档初始加载时,会调用 doLoad() 方法。此方法负责准备色度键控处理代码所需的变量,并设置事件监听器,以便我们能够检测用户何时开始播放视频。
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”事件时)被调用,然后负责安排自己定期被调用,以便为每一帧启动键控效果。
processor.timerCallback = function timerCallback() {
if (this.video.paused || this.video.ended) {
return;
}
this.computeFrame();
setTimeout(() => {
this.timerCallback();
}, 0);
};
回调做的第一件事是检查视频是否正在播放;如果不是,回调将立即返回而不做任何事情。
然后它调用 computeFrame() 方法,该方法对当前视频帧执行色度键控效果。
回调做的最后一件事是调用 setTimeout() 来安排自己尽快再次被调用。在实际应用中,你可能会根据视频的帧率来安排这项工作。
操作视频帧数据
下面显示的 computeFrame() 方法负责实际获取帧数据并执行色度键控效果。
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 元素正在显示最新的视频数据帧,其外观如下:

该视频帧被复制到第一个 canvas 的图形上下文 ctx1 中,指定的高度和宽度是我们之前保存的用于以一半大小绘制帧的值。请注意,您可以将 video 元素传递到上下文的 drawImage() 方法中,将当前视频帧绘制到上下文中。结果如下:

对第一个上下文调用 getImageData() 方法会获取当前视频帧的原始图形数据的副本。这提供了 32 位像素图像原始数据,我们可以对其进行操作。然后,我们通过将帧图像数据的总大小除以四来计算图像中的像素数。
for 循环遍历帧的像素,提取每个像素的红色、绿色和蓝色值,并将这些值与预定数字进行比较,这些数字用于检测将被替换为从 foo.png 导入的静态背景图像的绿色屏幕。
在帧图像数据中找到的每个属于绿色屏幕参数范围内的像素,其 alpha 值将被替换为零,表示该像素完全透明。因此,最终图像的整个绿色屏幕区域将 100% 透明,当它使用 ctx2.putImageData 绘制到目标上下文时,结果将是叠加在静态背景上。
生成的图像看起来像这样:

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