使用画布操作视频
通过将 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" />
</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
。画布c1
用于显示原始视频的当前帧,而c2
用于显示执行色度键控效果后的视频;c2
预先加载了将用于替换视频中绿色背景的静态图像。 - JavaScript 代码从名为
processor.js
的脚本中导入。
JavaScript 代码
processor.js
中的 JavaScript 代码包含三个方法。
初始化色度键控播放器
doLoad()
方法在 HTML 文档首次加载时被调用。此方法的工作是准备色度键控处理代码所需的变量,并设置事件监听器,以便我们能够检测用户何时开始播放视频。
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()
方法来开始监视视频并计算视觉效果。
计时器回调
计时器回调在视频开始播放时(当“播放”事件发生时)最初被调用,然后负责定期建立自身,以便在视频开始播放时启动每个帧的键控效果。
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);
};
当调用此例程时,视频元素会显示最新的视频数据帧,如下所示
视频帧被复制到第一个画布的图形上下文 ctx1
中,指定我们之前保存的用于绘制帧的宽度和高度,以绘制半尺寸的帧。请注意,您可以将视频元素传递到上下文的 drawImage()
方法中,以将当前视频帧绘制到上下文中。结果如下
在第一个上下文中调用 getImageData()
方法会获取当前视频帧的原始图形数据的副本。这会提供原始的 32 位像素图像数据,然后我们可以对其进行操作。然后,我们通过将帧图像数据的总大小除以四来计算图像中的像素数量。
for
循环扫描帧的像素,提取每个像素的红色、绿色和蓝色值,并将这些值与预先确定的数字进行比较,这些数字用于检测将被从 foo.png
导入的静态背景图像替换的绿幕。
在帧图像数据中找到的每个被认为是绿幕一部分的像素,其 alpha 值将被替换为零,表示该像素完全透明。因此,最终图像的整个绿幕区域为 100% 透明,因此当使用 ctx2.putImageData
将其绘制到目标上下文中时,结果将是叠加到静态背景上的叠加图层。
生成的图像如下所示
此操作在视频播放时重复执行,因此会重复处理并显示带有色度键控效果的每一帧。