捕获用户的图像

大多数浏览器都可访问用户的摄像头。

许多浏览器现在都可以访问用户的视频和音频输入。不过,根据浏览器的不同,这种体验可能是全动态的内嵌体验,也可能被委托给用户设备上的其他应用。此外,有些设备甚至没有摄像头。那么,如何打造一种使用用户生成的图片且在任何地方都能正常运行的体验?

从简单做起,循序渐进

如果您想逐步提升用户体验,就需要先从适用于所有平台的功能入手。最简易的做法是直接要求用户提供预先录制的文件。

请求提供网址

这是最受支持但最不令人满意的选项。让用户为您提供一个网址,然后使用该网址。如果只是显示图片,这种方法在任何地方都适用。创建 img 元素,设置 src,即可完成。

不过,如果您想以任何方式操纵图片,就要稍微复杂一些。CORS 会阻止您访问实际像素,除非服务器设置了适当的标头,并且您将图片标记为跨源;唯一可行的解决方法是运行代理服务器。

文件输入

您还可以使用简单的文件输入元素,包括指示您只需要图片文件的 accept 过滤条件。

<input type="file" accept="image/*" />

此方法在所有平台上都有效。在桌面平台上,它会提示用户通过文件系统上传图像文件。在 iOS 和 Android 版 Chrome 和 Safari 中,用户可以选择使用哪个应用来拍摄图片,包括直接使用相机拍照或选择现有的图片文件。

一个 Android 菜单,其中包含两个选项:拍摄图片和文件 iOS 菜单,包含以下三个选项:拍照、照片库和 iCloud

然后可将数据附加到一个 <form>,或通过 JavaScript 操作数据:侦听 input 元素上的 onchange 事件,然后读取事件 targetfiles 属性。

<input type="file" accept="image/*" id="file-input" />
<script>
  const fileInput = document.getElementById('file-input');

  fileInput.addEventListener('change', (e) =>
    doSomethingWithFiles(e.target.files),
  );
</script>

files 属性是一个 FileList 对象,稍后我会详细介绍该对象。

您还可以选择向该元素添加 capture 属性,向浏览器表明您希望从相机获取图片。

<input type="file" accept="image/*" capture />
<input type="file" accept="image/*" capture="user" />
<input type="file" accept="image/*" capture="environment" />

如果添加 capture 属性而不指定值,浏览器会决定要使用哪个摄像头,而 "user""environment" 值则会指示浏览器分别首选前置摄像头和后置摄像头。

capture 属性适用于 Android 和 iOS,但在桌面设备上会被忽略。不过请注意,在 Android 设备上,这意味着用户将无法再选择现有图片。系统会直接启动系统相机应用。

拖放

如果您已经添加了上传文件的功能,可以通过几种简单的方法让用户体验更加丰富。

第一种方法是向您的网页添加一个拖放目标,以便用户从桌面或其他应用中拖放文件。

<div id="target">You can drag an image file here</div>
<script>
  const target = document.getElementById('target');

  target.addEventListener('drop', (e) => {
    e.stopPropagation();
    e.preventDefault();

    doSomethingWithFiles(e.dataTransfer.files);
  });

  target.addEventListener('dragover', (e) => {
    e.stopPropagation();
    e.preventDefault();

    e.dataTransfer.dropEffect = 'copy';
  });
</script>

与文件输入类似,您可以从 drop 事件的 dataTransfer.files 属性获取 FileList 对象;

借助 dragover 事件处理脚本,您可以使用 dropEffect 属性向用户发送信号,告知他们在拖放文件时会发生什么。

拖放操作已存在很长时间,并且受到主流浏览器的广泛支持。

从剪贴板粘贴

获取现有图片文件的最后一种方法是从剪贴板获取。实现此功能的代码非常简单,但要想提供良好的用户体验,则稍微有些难度。

<textarea id="target">Paste an image here</textarea>
<script>
  const target = document.getElementById('target');

  target.addEventListener('paste', (e) => {
    e.preventDefault();
    doSomethingWithFiles(e.clipboardData.files);
  });
</script>

e.clipboardData.files 是另一个 FileList 对象。)

剪贴板 API 的难点在于,为了实现全面的跨浏览器支持,目标元素需要同时可选择和可修改。<textarea><input type="text"> 都符合此要求,具有 contenteditable 属性的元素也是如此。但这些功能显然也适用于编辑文本。

如果您不希望用户能够输入文本,则很难顺利实现此操作。诸如使隐藏的输入在点击其他元素时会被选中之类的技巧,可能会加大维护无障碍功能的难度。

处理 FileList 对象

由于上述大多数方法都会生成 FileList,因此我应该先介绍一下它是什么。

FileList 类似于 Array。它具有数字键和 length 属性,但实际上并不是数组。没有 forEach()pop() 等数组方法,并且它不可迭代。当然,您也可以使用 Array.from(fileList) 获取真实的数组。

FileList 的条目是 File 对象。这些对象与 Blob 对象完全相同,但具有额外的 namelastModified 只读属性。

<img id="output" />
<script>
  const output = document.getElementById('output');

  function doSomethingWithFiles(fileList) {
    let file = null;

    for (let i = 0; i < fileList.length; i++) {
      if (fileList[i].type.match(/^image\//)) {
        file = fileList[i];
        break;
      }
    }

    if (file !== null) {
      output.src = URL.createObjectURL(file);
    }
  }
</script>

此示例会查找具有图片 MIME 类型的第一个文件,但它也可以处理一次选择/粘贴/拖放多个图片的情况。

获得对该文件的访问权限后,您可以对其执行任何操作。例如,您可以:

  • 将其绘制到 <canvas> 元素中,以便对其进行操作
  • 将其下载至用户的设备
  • 使用 fetch() 将其上传到服务器

以交互方式使用相机

既然您已经覆盖了基础,是时候逐步提升技能了!

现代浏览器可直接访问摄像头,您可以借此打造与网页完全集成的体验,让用户永远都不需要离开浏览器。

获取对摄像头的访问权限

您可以使用 WebRTC 规范中名为 getUserMedia() 的 API 直接访问摄像头和麦克风。系统会提示用户授予其连接的麦克风和摄像头的访问权限。

getUserMedia() 的支持非常不错,但尚未全面支持。具体而言,Safari 10 或更低版本(在撰写本文时仍是最新的稳定版本)不支持此功能。不过,Apple 已宣布,该功能将在 Safari 11 中推出。

不过,检测支持情况非常简单。

const supported = 'mediaDevices' in navigator;

调用 getUserMedia() 时,您需要传入一个对象来描述您想要的媒体类型。这些选择称为限制条件。有多种可能的限制,包括您偏好前置摄像头还是后置摄像头、您是否需要音频,以及视频流的首选分辨率等。

不过,如需从摄像头获取数据,您只需一个约束条件,即 video: true

如果授权成功,该 API 将返回一个 MediaStream,其中包含来自摄像头的数据,然后您可以将数据附加到一个 <video> 元素、播放它以显示实时预览或将其附加到一个 <canvas> 以获取快照。

<video id="player" controls playsinline autoplay></video>
<script>
  const player = document.getElementById('player');

  const constraints = {
    video: true,
  };

  navigator.mediaDevices.getUserMedia(constraints).then((stream) => {
    player.srcObject = stream;
  });
</script>

这段代码本身的用处并不大。我们所能做的就是获取视频数据并进行播放。如果您想获取图片,则必须执行一些额外的工作。

截取快照

如需获取图片,您最好使用受支持的选项,即将视频中的帧绘制到画布上。

与 Web Audio API 不同,没有专门用于处理网络视频的流处理 API,因此您需要稍微用点歪招才能从用户的摄像头采集快照。

具体过程如下:

  1. 创建一个 canvas 对象,用来容纳来自摄像头的图帧
  2. 获取对摄像头数据流的访问权限
  3. 将其附加到视频元素
  4. 如果想精确地采集某一帧,可以利用 drawImage() 将 video 元素中的数据添加到 canvas 对象。
<video id="player" controls playsinline autoplay></video>
<button id="capture">Capture</button>
<canvas id="canvas" width="320" height="240"></canvas>
<script>
  const player = document.getElementById('player');
  const canvas = document.getElementById('canvas');
  const context = canvas.getContext('2d');
  const captureButton = document.getElementById('capture');

  const constraints = {
    video: true,
  };

  captureButton.addEventListener('click', () => {
    // Draw the video frame to the canvas.
    context.drawImage(player, 0, 0, canvas.width, canvas.height);
  });

  // Attach the video stream to the video element and autoplay.
  navigator.mediaDevices.getUserMedia(constraints).then((stream) => {
    player.srcObject = stream;
  });
</script>

将来自摄像头的数据存储在画布对象中后,就可以对其进行多种处理。您可以执行以下操作:

  • 将其直接上传到服务器
  • 将其存储在本地
  • 为图片应用时尚特效

提示

不需要时停止通过摄像头进行直播

最好在不再需要时停止使用摄像头。这不仅能节省电池电量和处理能力,还能增强用户对应用的信心。

如需停止访问摄像头,只需在 getUserMedia() 返回的视频流的每个视频轨上调用 stop()

<video id="player" controls playsinline autoplay></video>
<button id="capture">Capture</button>
<canvas id="canvas" width="320" height="240"></canvas>
<script>
  const player = document.getElementById('player');
  const canvas = document.getElementById('canvas');
  const context = canvas.getContext('2d');
  const captureButton = document.getElementById('capture');

  const constraints = {
    video: true,
  };

  captureButton.addEventListener('click', () => {
    context.drawImage(player, 0, 0, canvas.width, canvas.height);

    // Stop all video streams.
    player.srcObject.getVideoTracks().forEach(track => track.stop());
  });

  navigator.mediaDevices.getUserMedia(constraints).then((stream) => {
    // Attach the video stream to the video element and autoplay.
    player.srcObject = stream;
  });
</script>

以负责任的方式请求摄像头使用权限

如果用户之前未授予网站对摄像头的访问权,则调用 getUserMedia() 时浏览器会立即提示用户授予网站对摄像头的访问权。

用户讨厌在其机器上收到索要功能强大设备访问权限的提示,他们经常会屏蔽请求,如果他们不了解提示的创建上下文,则会忽略请求。最佳做法是在首次需要权限时只请求访问摄像头。用户授予访问权限后,系统就不会再次询问。但如果用户拒绝授权,您就无法再次获得访问权,除非他们手动更改摄像头权限设置。

兼容性

有关移动和桌面浏览器实现的更多信息:

我们还建议使用 adapter.js shim 来防止应用受到 WebRTC 规范变更和前缀差异的影响。

反馈