canvas 是一个可以使用 Javascript 来绘制内容的 HTML 元素。相信大多数人都对 canvas 有一些了解,我们经常用它开发一些动画,也有人用它来开发游戏、画板等应用,或者用来做一些简单的图像处理。

除了这些场景外,还可以用它来进行视频处理,视频就是图像集合按照时间顺序排列起来的,canvas 既然能处理图像,那肯定也能处理视频。

canvas 的基本用法

要在 canvas 中绘制内容,我们得先有一个 canvas,然后在 js 中获取 canvas 的上下文并使用上下文提供的 API 进行绘制

<canvas id="canvas" width="1280" height="720" style="width: 640px; height: 360px; border: 1px solid #ddd">
很遗憾,你的浏览器不支持 canvas
</canvas>

<script>
    const canvas = document.getElementById('canvas');
    const ctx = canvas.getContext('2d'); // 获取上下文
    ctx.scale(2, 2);
    
    ctx.fillStyle = 'red'; // 设置填充颜色
    ctx.beginPath(); // 开始一个路径
    ctx.arc(50, 50, 20, 0, Math.PI * 2); // 圈出一个圆形区域
    ctx.fill(); // 填充这个圆形
    ctx.closePath(); // 关闭路径
</script>

上面的代码在 (50, 50) 的位置绘制了一个半径为 20 ,填充颜色为红色的圆 而如果要绘制图片的话,需要使用 ctx.drawImage() 方法

const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
ctx.scale(2, 2)

const image = new Image();
image.crossOrigin = 'anonymous';
image.src = './demo.jpg';
image.onload = () => {
    ctx.drawImage(image, 0, 0);
}

ctx.drawImage 方法第一个参数可以是 Image 对象,也可以是 <img> 标签,或者是 **<video>** 标签,而我们今天的主角就是 <video> 标签。

代码中出现了两个宽高以及 ctx.scale(2, 2) ,这部分代码的目的是为了解决 canvas 绘制模糊的问题,更详细的信息可以查阅相关资料。

绘制视频

了解了 canvas 的基本用法,我们来尝试绘制一个视频

const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
ctx.scale(2, 2)

const video = document.createElement('video');
video.crossOrigin = 'anonymous';
video.src = './demo-video.mp4';
video.autoplay = true;
function draw() {
    ctx.drawImage(video, 0, 0);
    requestAnimationFrame(draw);
}
requestAnimationFrame(draw)

从代码上看,和绘制图片是没有太大差别的,都是使用 drawImage 方法进行绘制,不同点在于需要绘制每一帧,因此使用到了 requestAnimationFrame ,我们完全可以通过控制 video 标签来实现视频播放的控制,因为如果视频是暂停状态,canvas 绘制出来的每一帧也都是相同的暂停状态。

成功将视频绘制出来后,我们可以做的事情就很多了,完全可以使用图像处理的套路来给视频增加一些效果,比如添加滤镜、边框、视频拼接等。

要想实现这些效果,我们需要先获取到每一帧视频的图像数据,上面的代码中是直接将 <video> 标签作为参数传递给了 drawImage 方法来绘制的画面,我们没有直接得到图像数据,<video> 标签也没有提供相关的 API 来获取图像数据。不过 canvas 提供了一个 getImageData 方法来获取画布中指定区域的图像数据,我们可以通过这个 API 来间接的拿到每一帧视频。

对上面的代码进行简单的修改

const offlineCanvas = canvas.cloneNode();
const offlineCtx = offlineCanvas.getContext('2d');
function draw() {
  offlineCtx.drawImage(video, 0, 0);
  const frame = offlineCtx.getImageData(0, 0, canvas.width, canvas.height);

  for (let i = 0; i < frame.data.length; i += 4) {
    const r = frame.data[i];
    const g = frame.data[i + 1];
    const b = frame.data[i + 2];
    const gray = (r + g + b) / 3;
    frame.data[i] = frame.data[i + 1] = frame.data[i + 2] = gray;
  }

  ctx.putImageData(frame, 0, 0);

  requestAnimationFrame(draw);
}

代码中通过 canvas.cloneNode() 来创建了一个离屏 canvas ,用它作为中间媒体来获取视频帧图像,经过处理之后再绘制到页面的 canvas 中。当然,我们也可以就在一个 canvas 中操作。

getImageData() 获取到的是一个 ImageData 对象,它有 data , width , height 三个属性,data 中存储了每个像素的 R、G、B、A 四个通道的值,为 Uint8ClampedArray 类型,数组总长度是 width height 4。上面的代码中对像素点进行遍历,使用平均值算法将图像转为灰度图,可以看一下效果

类似的,可以通过特定算法来实现各种各样的滤镜效果。

我们用到了 drawImage() , getImageData() , putImageData() 3个方法,其中 getImageData()putImageData() 不会收到 ctx.scale() 的影响,因此当我们需要使用这组 API 的时候,需要对应到 canvas 的上下文宽高,即上面的 1280 * 720。

而是用 drawImage() 的时候,如果不指定结束位置参数,则会按照默认比例来进行绘制。

一些场景

掌握了视频处理的基本方法之后,我们可以动手尝试几个实际场景。

视频拼接

通过将视屏绘制到画布中的不同区域,可以达到画中画、左右/上下分屏的效果。

function draw() {
  ctx.drawImage(video, 0, 0, canvas.width / 2, canvas.height / 2);
  const frame = ctx.getImageData(canvas.width / 2, 0, canvas.width, canvas.height);
  grayScale(frame);

  ctx.putImageData(frame, canvas.width / 2, 0);

  requestAnimationFrame(draw);
}

基于前面的例子,我们将视频的右边部分进行灰度处理,再绘制回原来的位置,就得到了左边全彩,右边灰度的对比效果。

绿幕背景

通过一些预设参数来检测画面中特定的像素,再进行替换就能够做到绿幕的效果,比如下面的代码,检测 green 中绿色的像素,并将其替换成相同位置的视频源的像素,就完成了绿幕的抠图

function cutGreen(green, source) {
  for (let i = 0; i < green.data.length; i += 4) {
    const r = green.data[i];
    const g = green.data[i + 1];
    const b = green.data[i + 2];
    if (r < 100 && g > 150 && b < 100) {
      green.data[i] = source.data[i];
      green.data[i + 1] = source.data[i + 1];
      green.data[i + 2] = source.data[i + 2];
      green.data[i + 3] = source.data[i + 3];
    }
  }
}

function draw() {
  ctx.drawImage(greenVideo, 0, 0, canvas.width / 2, canvas.height / 2);
  const greenFrame = ctx.getImageData(0, 0, canvas.width, canvas.height);
  ctx.drawImage(video, 0, 0, canvas.width / 2, canvas.height / 2);
  const sourceFrame = ctx.getImageData(0, 0, canvas.width, canvas.height);

  cutGreen(greenFrame, sourceFrame);

  ctx.putImageData(greenFrame, 0, 0);

  requestAnimationFrame(draw);
}

更多场景

上面简单展示了两个示例,除此之外,还可以做更多丰富的场景,可以用 canvas 来播放带有透明度的视频、可以用在 WebRTC 中进行特效加持,甚至能够结合交互,实现一些可交互视频、涂鸦等效果。

总结

canvas 对视频的处理还是挺有趣的,简单的代码可以实现一些意想不到的效果。还有更多更深入的内容等着我们去探索。

以上我们处理的仅是视频的画面,视频处理画面,还包含了音频,而 js 也是可以用来处理音频的,有着更丰富更高级的 API,有关更多音频的知识可以看我其它分享:Web Audio 概览让你喜欢的音乐动起来🎵

扩展

我们既然用 canvas 实现了一些视频的处理,但是仅仅是把它播放出来了,能不能将处理后的视频再导出来呢?

答案是可以的,感兴趣的小伙伴可以去尝试一下~