15、屏幕截图:Canvas捕获视频帧、生成截图、截图下载与分享
屏幕共享做完了,画面也投出去了,但有个需求经常被忽略——用户想「拍个照」。说白了,就是把当前屏幕上的某一帧截下来,保存成图片,甚至直接分享给别人。
我最早做这个功能时,以为很简单:拿个截图工具不就行了?后来发现,在WebRTC场景里,截图必须从视频流里「捞」出来,而且得保证清晰度、时效性,还得考虑用户怎么拿到这张图。嗯,这里面的坑,我踩过不少。
15.1 从视频流里「抓」一帧
你想想看,屏幕共享的本质是 MediaStream,它最终会绑定到一个 <video> 元素上。我们要做的,就是把 video 当前显示的画面「拍」下来。
为什么用 Canvas?因为浏览器原生支持 Canvas 2D 上下文,drawImage() 可以直接接受 video 元素作为图像源。这比用 WebGL 或 OffscreenCanvas 简单得多。
// 获取 video 元素
const video = document.getElementById('screenVideo');
// 创建 Canvas
const canvas = document.createElement('canvas');
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
// 绘制当前帧
const ctx = canvas.getContext('2d');
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
// 导出为图片数据
const dataURL = canvas.toDataURL('image/png');
console.log('截图完成,数据长度:', dataURL.length);
这里有个细节:video.videoWidth 和 video.videoHeight 是视频的真实分辨率,不是 CSS 显示尺寸。如果你用 CSS 把 video 缩放了,截图会按原始分辨率来,清晰度有保障。
15.2 生成截图:两种导出方式
Canvas 提供了两种导出方式,各有适用场景。我列个表,你一看就明白:
| 方法 | 输出格式 | 适用场景 | 注意事项 |
|---|---|---|---|
| toDataURL() | Base64 字符串 | 直接展示、分享到聊天框 | 数据量大,PNG 格式可能几 MB |
| toBlob() | Blob 对象 | 下载文件、上传服务器 | 可指定质量参数(JPEG) |
我个人更推荐 toBlob(),尤其是做下载功能时。为什么?因为 Blob 可以直接传给 URL.createObjectURL() 生成下载链接,不需要先转成 Base64 再解码,内存占用更小。
// 使用 toBlob 生成截图
canvas.toBlob(function(blob) {
// blob 就是图片文件数据
const url = URL.createObjectURL(blob);
// 创建下载链接
const a = document.createElement('a');
a.href = url;
a.download = `screenshot-${Date.now()}.png`;
a.click();
// 释放内存
URL.revokeObjectURL(url);
}, 'image/png');
如果你想要 JPEG 格式,可以传第二个参数 'image/jpeg',第三个参数是质量(0~1)。比如 canvas.toBlob(callback, 'image/jpeg', 0.8)。我在项目中遇到过,JPEG 格式能把 2MB 的 PNG 压缩到 200KB,画质损失肉眼几乎看不出来。
15.3 截图下载:兼容多浏览器
下载功能看似简单,但不同浏览器对 download 属性的支持有差异。我踩过的坑是:Safari 不支持 a.download 跨域资源,而 Canvas 导出的 Blob URL 属于同源,所以没问题。但如果你用了 toDataURL() 生成的 Base64 链接,Safari 有时会直接打开新标签页而不是下载。
解决方案:统一用 toBlob() + URL.createObjectURL(),兼容性最好。
function downloadScreenshot(canvas, filename = 'screenshot.png') {
canvas.toBlob(function(blob) {
// 兼容 IE 和 Edge 旧版
if (window.navigator.msSaveBlob) {
window.navigator.msSaveBlob(blob, filename);
} else {
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(link.href);
}
}, 'image/png');
}
click() 触发下载可能被拦截。建议加一个用户手势(比如按钮点击事件)来触发截图流程,不要自动执行。
15.4 截图分享:复制到剪贴板
下载到本地还不够,用户可能想直接粘贴到聊天软件里。这时候就要用到 Clipboard API 了。
我记得 Chrome 76 之后才支持 navigator.clipboard.write(),而且要求页面在 HTTPS 环境下。如果你在本地开发(localhost),也是可以的。
async function copyScreenshotToClipboard(canvas) {
// 先把 Canvas 转为 Blob
const blob = await new Promise(resolve => {
canvas.toBlob(resolve, 'image/png');
});
// 构造 ClipboardItem
const item = new ClipboardItem({
'image/png': blob
});
try {
await navigator.clipboard.write([item]);
console.log('截图已复制到剪贴板');
} catch (err) {
console.error('复制失败:', err);
// 降级方案:提示用户手动复制
}
}
这里有个坑:ClipboardItem 的键名必须是 MIME 类型,比如 'image/png'。如果你传了 'image/jpeg',但实际数据是 PNG,粘贴时会失败。我建议统一用 PNG,兼容性最好。
15.5 知识体系总览
下面这张图,把整个截图流程串起来了。从视频流到最终输出,每一步都有对应的技术点:
15.6 完整示例:一键截图 + 下载 + 复制
把上面所有知识点整合成一个函数,你直接拿去用:
async function captureAndShare(videoElement) {
// 1. 创建 Canvas 并绘制
const canvas = document.createElement('canvas');
canvas.width = videoElement.videoWidth;
canvas.height = videoElement.videoHeight;
const ctx = canvas.getContext('2d');
ctx.drawImage(videoElement, 0, 0);
// 2. 生成 Blob
const blob = await new Promise(resolve => {
canvas.toBlob(resolve, 'image/png');
});
// 3. 下载
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `screenshot-${Date.now()}.png`;
a.click();
URL.revokeObjectURL(url);
// 4. 复制到剪贴板(可选)
try {
const item = new ClipboardItem({ 'image/png': blob });
await navigator.clipboard.write([item]);
console.log('截图已复制到剪贴板');
} catch (err) {
console.warn('剪贴板写入失败,用户可手动复制');
}
return blob;
}
if (video.readyState < 2) return alert('请等待画面加载完成');,问题解决。
另外,如果你需要截取「整个屏幕」而不是某个 video 元素,可以用 navigator.mediaDevices.getDisplayMedia() 获取屏幕流,然后同样用 Canvas 捕获。但要注意,这种方式会弹出权限对话框,用户体验上不如直接截 video 流畅。
好了,屏幕截图这块的核心内容就这些。从 Canvas 捕获到导出,再到下载和分享,每一步都有对应的 API 和注意事项。你只要按照这个流程走,基本不会出大问题。