Canvas截图原理:从视频帧到图片的完整链路
做WebRTC录制和截图,绕不开Canvas。说白了,Canvas就是浏览器给我们的一块画板。你可以在上面画线、画圆、画图片,甚至把视频的每一帧都“印”上去。今天我们就来聊聊,怎么用Canvas把视频帧变成一张图片。
我个人习惯把Canvas截图理解成三步:准备画布 → 绘制视频帧 → 导出图片数据。听起来简单,但每一步都有坑。我踩过的坑,今天一并告诉你。
核心逻辑:Canvas截图本质上是把<video>元素的当前画面,通过drawImage()方法绘制到Canvas上,再通过toDataURL()或toBlob()导出为图片格式。
Canvas 2D API 基础
Canvas 2D API 是浏览器内置的绘图接口。你只需要一个 <canvas> 元素,然后通过 JavaScript 获取它的上下文(context),就能开始画了。
// 获取 Canvas 元素和 2D 上下文
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
// 设置画布尺寸(单位:像素)
canvas.width = 640;
canvas.height = 480;
// 画一个红色矩形
ctx.fillStyle = '#FF0000';
ctx.fillRect(10, 10, 100, 50);
嗯,这里要注意:canvas.width 和 canvas.height 是画布的实际像素尺寸。如果你用 CSS 设置了宽高,那只是拉伸显示,不会改变画布的分辨率。我刚开始做截图时,就因为这个原因导出了模糊的图片——CSS 宽高和画布宽高不一致。
我的习惯:在绘制视频帧之前,先把 Canvas 的宽高设置成视频的实际分辨率。比如视频是 1280×720,Canvas 就设成 1280×720。这样导出的图片才是清晰的。
drawImage 方法详解
drawImage() 是 Canvas 截图的核心方法。它有三种调用方式,我分别说一下。
| 调用方式 | 参数说明 | 适用场景 |
|---|---|---|
drawImage(img, x, y) |
在 (x, y) 位置绘制原图 | 简单绘制,不缩放 |
drawImage(img, x, y, w, h) |
绘制并缩放到宽 w、高 h | 需要调整画面大小 |
drawImage(img, sx, sy, sw, sh, dx, dy, dw, dh) |
从源图裁剪 (sx, sy, sw, sh) 区域,绘制到目标 (dx, dy, dw, dh) | 截取视频局部区域 |
第三种方式最强大。我在做人脸截图功能时,就用它来裁剪视频中的人脸区域。你想想看,如果视频里只有一个人脸在左上角,你不需要把整个画面都保存下来,只需要裁剪那一小块就行了。
// 示例:从视频中裁剪并绘制
ctx.drawImage(
videoElement, // 源:视频元素
100, 50, // 从源图 (100, 50) 开始裁剪
300, 200, // 裁剪宽 300、高 200
0, 0, // 绘制到 Canvas 的 (0, 0) 位置
640, 480 // 绘制宽 640、高 480(可缩放)
);
我曾经踩过的坑:如果视频还没有加载完成就调用 drawImage,Canvas 上会是一片空白。一定要确保视频的 loadeddata 事件已经触发,或者使用 requestAnimationFrame 在每一帧绘制时截图。
将视频帧绘制到 Canvas
把视频帧画到 Canvas 上,其实就一行代码。但前提是视频必须正在播放,或者至少已经加载了一帧数据。
// 假设 videoElement 是一个 <video> 元素,并且已经加载了视频
function captureFrame() {
const canvas = document.createElement('canvas');
canvas.width = videoElement.videoWidth;
canvas.height = videoElement.videoHeight;
const ctx = canvas.getContext('2d');
// 绘制当前视频帧
ctx.drawImage(videoElement, 0, 0);
// 现在 canvas 上就是当前视频帧的画面了
}
为什么会这样?因为 drawImage() 可以接受 HTMLVideoElement 作为源。浏览器会自动把视频的当前帧提取出来,当作一张图片绘制到 Canvas 上。这个操作是同步的,性能也很好。
我记得有一次做实时滤镜功能,需要在每一帧视频上叠加特效。我用 requestAnimationFrame 循环调用 drawImage,然后在绘制之前修改 Canvas 的像素数据。效果很流畅,但要注意性能——如果 Canvas 尺寸太大,每帧绘制可能会卡顿。
性能建议:如果只是截图,不需要每帧都绘制。用户点击截图按钮时,调用一次 drawImage 就够了。如果是做实时预览,再考虑用 requestAnimationFrame。
从 Canvas 导出图片数据
画好了,怎么导出?Canvas 提供了两个方法:toDataURL() 和 toBlob()。
toDataURL:得到 Base64 字符串
// 导出为 PNG 格式(默认)
const dataURL = canvas.toDataURL();
// 输出类似:data:image/png;base64,iVBORw0KGgo...
// 导出为 JPEG 格式,质量 0.8
const jpegURL = canvas.toDataURL('image/jpeg', 0.8);
toDataURL() 返回的是 Base64 编码的字符串。你可以直接把它赋值给 <img> 的 src 属性,或者通过 window.open() 在新标签页中预览。但 Base64 字符串比较长,如果图片很大,传输效率不高。
toBlob:得到二进制文件
// 导出为 Blob 对象
canvas.toBlob(function(blob) {
// blob 就是图片的二进制数据
// 可以用于上传、下载或创建 Object URL
const url = URL.createObjectURL(blob);
// 创建下载链接
const a = document.createElement('a');
a.href = url;
a.download = 'screenshot.png';
a.click();
URL.revokeObjectURL(url);
}, 'image/png');
我个人更推荐 toBlob()。为什么?因为 Blob 是二进制数据,体积更小,适合上传到服务器。而且你可以直接用 URL.createObjectURL() 生成一个临时 URL,用于预览或下载,用完记得释放内存。
注意:如果 Canvas 中绘制了跨域图片(比如来自不同域名的视频帧),导出时会抛出安全错误。解决办法是在 <video> 元素上设置 crossOrigin="anonymous",并且确保服务器返回了正确的 CORS 头。
完整示例:一键截图
把上面的知识点串起来,就是一个完整的截图功能。我写了一个最小可用的例子。
function takeScreenshot(videoElement) {
// 1. 创建 Canvas,尺寸与视频一致
const canvas = document.createElement('canvas');
canvas.width = videoElement.videoWidth;
canvas.height = videoElement.videoHeight;
// 2. 获取 2D 上下文,绘制视频帧
const ctx = canvas.getContext('2d');
ctx.drawImage(videoElement, 0, 0);
// 3. 导出为 Blob 并下载
canvas.toBlob(function(blob) {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `screenshot_${Date.now()}.png`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}, 'image/png');
}
// 使用方式:在视频播放时调用
// takeScreenshot(document.getElementById('myVideo'));
这个函数只有十几行,但涵盖了 Canvas 截图的全部流程。你可以在任何 WebRTC 项目中使用它,比如在视频通话中点击“截图”按钮,把对方的画面保存下来。
总结一下:Canvas 截图的核心就是 drawImage + toBlob。前者负责把视频帧“印”到画布上,后者负责把画布内容“存”成图片文件。中间注意跨域和尺寸问题,基本就不会出错了。
好了,Canvas 截图原理就讲到这里。下一章我们会深入 WebRTC 的 MediaStream 获取,看看怎么从摄像头拿到视频流,然后结合 Canvas 做实时截图。不过那是后话了,先把今天的内容消化掉。