Capturing video from a user's device

Browsers increasingly ship with APIs for accessing a user's camera and microphone. The way those capabilities are exposed varies: some offer a fully in-page experience, while others hand off to a native app on the device.

Start with a file input

The most broadly compatible approach is to request a pre-recorded file through a standard file input. An accept filter restricts the picker to video, and the capture attribute hints that the file should come straight from the camera:

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

This works on every platform. On desktop the browser shows a normal file picker and ignores capture. On iOS Safari it launches the camera app and returns the recorded clip to the page; on Android the user picks which app to record with before the file comes back.

On devices with more than one camera, you can express a preference through the capture value: user requests the front-facing camera, environment the rear. This is only a hint — the browser may fall back to another camera if the requested type is unsupported or unavailable.

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

Once recording finishes and the user is back on the page, an onchange handler on the input reads the resulting file from event.target.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>

The resulting File can be used like any other file: played back in a <video> element, downloaded locally, uploaded to a server via XMLHttpRequest, or drawn frame-by-frame into a canvas for filtering.

The file-input route is universal but clunky. A better experience keeps the user inside the page.

Going live with getUserMedia

Connecting to the camera

The WebRTC getUserMedia() API provides a direct line to the device's microphones and cameras. After prompting the user, it hands back a MediaStream that you can attach to a video element, feed into a WebRTC stream, or persist with MediaRecorder.

Requesting camera data is a simple constraints object:

<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>

To select a specific camera, enumerate the available ones first:

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

Then pass the chosen deviceId into the getUserMedia() call:

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

Manipulating the raw frames

Playing a live preview in a <video> element is the easy part. To process the underlying pixels, draw each frame into a canvas. With a 2D context, drawImage() copies the video's current frame:

context.drawImage(myVideoElement, 0, 0);

For a WebGL canvas, the same video element can serve directly as a texture source:

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

Both techniques operate on the video's current frame, so handling multiple frames means redrawing each time a new one is ready.

Recording the stream

For durable output, the MediaRecorder API consumes the stream from getUserMedia() and writes its data to a destination of your choosing. Here the chunks are buffered in an array for later assembly into a Blob, which can then be uploaded or saved locally:

<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>

Handling camera permission gracefully

The first call to getUserMedia() on a site triggers a native permission prompt. Users often dismiss or block requests they don't understand. The prompting it automatically carries out means you should call getUserMedia() only when the user's action actually needs the camera. Once access is granted, it persists, but a rejection is hard to recover from — the denial isn't something an in-page UI can easily reverse.

getUserMedia() itself won't tell you whether permission was previously granted. To build the right UI before calling it, check the Permission API's navigator.permission.query(). Querying {name: 'camera'} returns one of three states:

  • granted — prior access, no prompt will appear
  • prompt — no decision yet; getUserMedia() will trigger the prompt
  • denied — blocked by the user or system; no further access is possible
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 () {};
});

Prompting the user only when it's meaningful — and shaping the page to reflect the possible outcomes — keeps the experience from feeling like an unwelcome interruption.