录制用户的视频

Mat Scales

许多浏览器现在都可以访问用户的视频和音频输入。不过,根据浏览器的不同,这种体验可能是全动态的内嵌体验,也可能被委托给用户设备上的其他应用。

从简单、循序渐进开始

最简单的方法是直接要求用户提供预先录制的文件。为此,请创建一个简单的文件输入元素,然后添加 accept 过滤器(指示我们只能接受视频文件)和 capture 属性(指示我们想要直接从相机获取视频文件)。

<input type="file" accept="video/*" capture />

此方法适用于所有平台。在桌面设备上,它会提示用户从文件系统上传文件(忽略 capture 属性)。在 iOS 版 Safari 中,它会打开相机应用,让您录制视频并将其发送回网页;在 Android 上,它会让用户选择使用哪个应用来录制视频,然后再将其发送回网页。

许多移动设备都有多个摄像头。如果您有偏好,可以将 capture 属性设置为 user(如果要让摄像头朝向用户),或者 environment 属性(如果您希望摄像头朝外)。

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

请注意,这只是一个提示,如果浏览器不支持该选项,或您所需的相机类型不可用,则浏览器可能会选择其他相机。

用户完成录制并返回网站后,您需要以某种方式获取文件数据。将 onchange 事件附加到输入元素,然后读取事件对象的 files 属性,即可快速访问该事件。

<input type="file" accept="video/*" capture="camera" id="recorder" />
<video id="player" controls></video>
<script>
  var recorder = document.getElementById('recorder');
  var player = document.getElementById('player');

  recorder.addEventListener('change', function (e) {
    var file = e.target.files[0];
    // Do something with the video file.
    player.src = URL.createObjectURL(file);
  });
</script>

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

  • 将其直接附加到 <video> 元素,以便播放
  • 将其下载到用户的设备上
  • 通过附加到 XMLHttpRequest 将其上传到服务器
  • 在画布上绘制边框,然后对其应用滤镜

虽然使用 input 元素方法获取视频数据访问权限的情况普遍存在,但却是最没有吸引力的方案。我们非常希望能够直接在页面中使用相机并提供良好的体验

以交互方式使用相机

现代浏览器可以直接连接摄像头,这让我们能够打造与网页完全集成的体验,让用户永远都不需要离开浏览器。

获得相机使用权限

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

如果成功,该 API 将返回一个 Stream,其中包含来自相机或麦克风的数据,然后我们可以将其附加到 <video> 元素、将其附加到 WebRTC 流或使用 MediaRecorder API 保存。

为了从相机获取数据,我们只需在传递给 getUserMedia() API 的约束对象中设置 video: true

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

  var handleSuccess = function (stream) {
    player.srcObject = stream;
  };

  navigator.mediaDevices
    .getUserMedia({audio: true, video: true})
    .then(handleSuccess);
</script>

如果您要选择特定的摄像头,可以先枚举可用的摄像头。

navigator.mediaDevices.enumerateDevices().then((devices) => {
  devices = devices.filter((d) => d.kind === 'videoinput');
});

然后,您可以在调用 getUserMedia 时传递要使用的 deviceId。

navigator.mediaDevices.getUserMedia({
  audio: true,
  video: {
    deviceId: devices[0].deviceId,
  },
});

这本身并不实用。我们所能做的就是获取视频数据并进行播放。

访问相机中的原始数据

如需从相机访问原始视频数据,您可以将每一帧绘制到 <canvas> 中,并直接操控像素。

对于 2D 画布,您可以使用上下文的 drawImage 方法将 <video> 元素的当前帧绘制到画布中。

context.drawImage(myVideoElement, 0, 0);

在 WebGL 画布中,您可以使用 <video> 元素作为纹理的来源。

gl.texImage2D(
  gl.TEXTURE_2D,
  0,
  gl.RGBA,
  gl.RGBA,
  gl.UNSIGNED_BYTE,
  myVideoElement,
);

请注意,无论是哪种情况,都会使用正在播放的视频的当前帧。如需处理多个帧,您每次都需要将视频重新绘制到画布上。

如需了解详情,请参阅我们有关对图片和视频应用实时效果的文章。

保存相机中的数据

如需保存相机中的数据,最简单的方法是使用 MediaRecorder API。

MediaRecorder API 将获取 getUserMedia 创建的数据流,然后逐步将数据流中的数据保存到首选目标位置。

<a id="download">Download</a>
<button id="stop">Stop</button>
<script>
  let shouldStop = false;
  let stopped = false;
  const downloadLink = document.getElementById('download');
  const stopButton = document.getElementById('stop');

  stopButton.addEventListener('click', function() {
    shouldStop = true;
  })

  var handleSuccess = function(stream) {
    const options = {mimeType: 'video/webm'};
    const recordedChunks = [];
    const mediaRecorder = new MediaRecorder(stream, options);

    mediaRecorder.addEventListener('dataavailable', function(e) {
      if (e.data.size > 0) {
        recordedChunks.push(e.data);
      }

      if(shouldStop === true && stopped === false) {
        mediaRecorder.stop();
        stopped = true;
      }
    });

    mediaRecorder.addEventListener('stop', function() {
      downloadLink.href = URL.createObjectURL(new Blob(recordedChunks));
      downloadLink.download = 'acetest.webm';
    });

    mediaRecorder.start();
  };

  navigator.mediaDevices.getUserMedia({ audio: true, video: true })
      .then(handleSuccess);
</script>

在本例中,我们要将数据直接保存到数组中,稍后可以将其转换为 Blob,然后可将此数组保存到网络服务器或直接保存到用户设备上的存储空间中。

以负责任的方式请求相机使用权限

如果用户之前未向您的网站授予相机使用权限,那么您调用 getUserMedia 时,浏览器会立即提示用户向您的网站授予相机使用权限。

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

使用 Permissions API 检查您是否已拥有访问权限

使用 getUserMedia API 时,您无法得知自己是否已获得相机访问权限。这会给您带来一个问题:为了提供良好的界面来让用户向您授予对相机的访问权限,您必须请求对相机的访问权限。

在某些浏览器中,可以使用 Permission API 解决此问题。借助 navigator.permission API,您可以查询对特定 API 的访问权限的状态,而无需再次提示用户。

如需查询您是否有权访问用户的相机,您可以将 {name: 'camera'} 传入查询方法,该方法会返回以下两者之一:

  • granted - 用户之前已授予您对相机的访问权限;
  • prompt - 用户尚未授予您访问权限;当您调用 getUserMedia 时,系统将提示您;
  • denied - 系统或用户已明确阻止访问摄像头,您将无法访问摄像头。

现在,您可以进行快速检查,看看是否需要更改界面以适应用户需要执行的操作。

navigator.permissions.query({name: 'camera'}).then(function (result) {
  if (result.state == 'granted') {
  } else if (result.state == 'prompt') {
  } else if (result.state == 'denied') {
  }
  result.onchange = function () {};
});

反馈