15、屏幕截图:Canvas捕获视频帧、生成截图、截图下载与分享

屏幕共享做完了,画面也投出去了,但有个需求经常被忽略——用户想「拍个照」。说白了,就是把当前屏幕上的某一帧截下来,保存成图片,甚至直接分享给别人。

我最早做这个功能时,以为很简单:拿个截图工具不就行了?后来发现,在WebRTC场景里,截图必须从视频流里「捞」出来,而且得保证清晰度、时效性,还得考虑用户怎么拿到这张图。嗯,这里面的坑,我踩过不少。

核心思路: 利用 Canvas 的 drawImage() 方法,把 video 元素的当前帧绘制到画布上,再通过 toDataURL() 或 toBlob() 导出为图片文件。

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.videoWidthvideo.videoHeight 是视频的真实分辨率,不是 CSS 显示尺寸。如果你用 CSS 把 video 缩放了,截图会按原始分辨率来,清晰度有保障。

我的习惯: 截图前先检查 video.readyState >= 2,确保视频帧已经加载完毕。否则 drawImage 可能画出一张黑图。

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');
}
注意: 在移动端 Safari 上,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 知识体系总览

下面这张图,把整个截图流程串起来了。从视频流到最终输出,每一步都有对应的技术点:

屏幕截图核心流程 视频流 MediaStream → video Canvas 捕获 drawImage(video, 0, 0) 导出图片 toDataURL / toBlob 导出方式对比 toDataURL() 输出 Base64 字符串 适合:直接展示、分享 toBlob() 输出 Blob 对象 适合:下载、上传 应用场景 下载到本地 | 复制到剪贴板 | 上传服务器 | 分享到聊天

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;
}
避坑指南: 我曾经在截图时忘了检查 video 的 readyState,结果用户刚打开共享就点截图,画出来一张黑图。后来我加了个判断:if (video.readyState < 2) return alert('请等待画面加载完成');,问题解决。

另外,如果你需要截取「整个屏幕」而不是某个 video 元素,可以用 navigator.mediaDevices.getDisplayMedia() 获取屏幕流,然后同样用 Canvas 捕获。但要注意,这种方式会弹出权限对话框,用户体验上不如直接截 video 流畅。

好了,屏幕截图这块的核心内容就这些。从 Canvas 捕获到导出,再到下载和分享,每一步都有对应的 API 和注意事项。你只要按照这个流程走,基本不会出大问题。


公众号:蓝海资料掘金营,微信deep3321