捕获用户的图像

大多数浏览器都可以访问用户的相机。

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

从简单开始,逐步深入

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

请求提供网址

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

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

文件输入

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

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

此方法适用于所有平台。在桌面设备上,系统会提示用户从文件系统上传图片文件。在 iOS 和 Android 设备上的 Chrome 和 Safari 中,此方法可让用户选择使用哪个应用拍摄图片,包括直接使用相机拍照或选择现有图片文件。

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

然后,您可以通过监听输入元素上的 onchange 事件,然后读取事件 targetfiles 属性,将数据附加到 <form> 或使用 JavaScript 对其进行操作。

<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 不同,Web 上没有专门用于视频的流处理 API,因此您必须使用一些黑客技巧才能从用户的相机捕获快照。

具体过程如下:

  1. 创建一个用于存放相机画面的画布对象
  2. 获取对摄像头画面的访问权限
  3. 将其附加到视频元素
  4. 如需捕获精确的帧,请使用 drawImage() 将视频元素中的数据添加到画布对象。
<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 规范变更和前缀差异的影响。

反馈