Capture API Comparison

getUserMedia + ImageCapture vs <input capture> crbug.com/40291635 Sampler page

At a Glance

Both approaches let a user take a photo from their camera and use it on a web page. The difference is how much work the developer has to do and how it integrates with forms.

getUserMedia + ImageCapture

~50 lines JS

Developer must: request permission, create stream, build preview UI, wire up capture button, convert Blob to File, clean up stream, handle errors.

Result: an in-memory Blob. Needs extra work to put into a form submission.

<input type="file" capture>

1 line HTML, 0 lines JS

One HTML attribute. Browser handles permission, camera UI, preview, and capture. Result lands directly in the file input, ready for form submission.

Works on Android/iOS today. This CL adds desktop support.

getUserMedia + ImageCapture <input capture>
JS required~50 LoC minimum0 (pure HTML)
Custom UI neededYes (video, buttons, canvas)No (browser-native dialog)
Permission handlingManual (try/catch, error states)Automatic (browser handles it)
Stream lifecycleManual stop (leak risk)Automatic
Form integrationManual (DataTransfer hack)Native (file input, FormData)
Works without JSNoYes
Cross-browser (mobile)Partial (ImageCapture: Chrome-only)Safari, Chrome, Firefox
Cross-browser (desktop)getUserMedia: broad; ImageCapture: Chrome-onlyNo desktop browser yet; Chrome: this CL

Live Demo

Step-by-step (developer must build all of this)

Complex path -- 4 steps
Camera preview (developer builds this):
Click "Get User Media" to start
Captured photo:
Click "Take Photo" after starting camera
Waiting...

Complete implementation (this is literally it)

Simple path -- 1 step
Click the file button -- browser handles everything.

Code & Form Submission

Camera + capture code (~50 lines)

// HTML: <video id="preview" autoplay></video>
//       <canvas id="canvas"></canvas>
//       <button onclick="start()">Start</button>
//       <button onclick="capture()">Capture</button>
//       <form><input type="file" id="fileInput"></form>

let stream, imageCapture;

async function start() {
  try {
    stream = await navigator.mediaDevices.getUserMedia({video: true});
    document.getElementById('preview').srcObject = stream;
    const track = stream.getVideoTracks()[0];
    imageCapture = new ImageCapture(track);
  } catch (err) {
    // Developer must handle: NotAllowedError, NotFoundError,
    // NotReadableError, OverconstrainedError, AbortError...
    alert('Camera error: ' + err.message);
  }
}

async function capture() {
  try {
    const blob = await imageCapture.takePhoto();
    const bmp = await createImageBitmap(blob);
    const canvas = document.getElementById('canvas');
    canvas.width = bmp.width;
    canvas.height = bmp.height;
    canvas.getContext('2d').drawImage(bmp, 0, 0);

    // Convert Blob to File and inject into form input
    const file = new File([blob], 'capture.jpg', {type: blob.type});
    const dt = new DataTransfer();
    dt.items.add(file);
    document.getElementById('fileInput').files = dt.files;
  } catch (err) {
    alert('Capture error: ' + err.message);
  }
}

function stop() {
  // Developer MUST remember this or camera LED stays on
  if (stream) {
    stream.getTracks().forEach(t => t.stop());
    stream = null;
  }
}

Form submission wiring

Manual wiring

After capturing, the developer must convert the Blob and inject it:

// Convert blob to File
const file = new File([blob], 'photo.jpg', {type: 'image/jpeg'});

// Hack: use DataTransfer to set input.files
const dt = new DataTransfer();
dt.items.add(file);
input.files = dt.files;

// OR: skip the input entirely and use fetch
const fd = new FormData();
fd.append('photo', blob, 'photo.jpg');
await fetch('/upload', {method: 'POST', body: fd});

Hidden input for form:

No file injected yet

Error handling (developer handles all errors)

try {
  stream = await navigator.mediaDevices
    .getUserMedia({video: true});
} catch (err) {
  switch (err.name) {
    case 'NotAllowedError':
      // User denied permission
      break;
    case 'NotFoundError':
      // No camera available
      break;
    case 'NotReadableError':
      // Camera in use by another app
      break;
    case 'OverconstrainedError':
      // Constraints can't be satisfied
      break;
    case 'AbortError':
      // Unknown hardware error
      break;
  }
}

Complete code (1 line)

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

<!-- That's it. No JS needed.
     Browser handles: permission, camera UI, preview,
     capture, file creation, and form integration. -->

Form submission

Just works

File is already in the input. Standard form submission works:

<form action="/upload" method="post"
      enctype="multipart/form-data">
  <input type="file" accept="image/*"
         capture="user" name="photo">
  <button type="submit">Upload</button>
</form>

<!-- No JS needed for the entire flow -->
Click file button to capture

Error handling

Browser handles errors
<input type="file" accept="image/*" capture>

<!-- Browser shows native UI for all error states:
     - Permission denied: shown in dialog
     - No camera: graceful fallback
     - Camera busy: handled by OS
     - User cancels: input unchanged

     Developer can optionally check:
     if (!input.files.length) {
       // user cancelled
     }
-->

Cross-browser: <input capture>

BrowserMobileDesktop
ChromeYes (Android)This CL adds support
SafariYes (iOS)No
FirefoxYes (Android)No
ImageCapture APIChrome-onlyChrome-only

No browser currently supports <input capture> on desktop. This CL would make Chrome the first desktop browser to implement it, closing WPT coverage gaps and leading cross-platform parity.

Real-world Use Cases

Where <input capture> is the right tool

KYC / Identity verification: <input type="file" accept="image/*" capture="environment"> -- User photographs their ID document. No app JS needed.

Receipt upload: Quick snap of a receipt in an expense form. Works even with JS disabled.

Support tickets: "Attach a photo of the issue" -- single button, zero dev overhead.

Progressive enhancement: On browsers without capture support, falls back to normal file picker. No breakage.

Where getUserMedia + ImageCapture is the right tool

Video conferencing: Long-running stream, mute/unmute, device switching.

AR/ML processing: Frame-by-frame access via grabFrame(), real-time canvas manipulation.

Custom camera UX: Filters, overlays, cropping, multi-shot sequences.

These are NOT what <input capture> targets. The two APIs serve different developer needs.

Spec References

W3C HTML Media Capture (current spec)

MDN: ImageCapture API

Chrome ImageCapture sample (grab-frame-take-photo)

Design Doc