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.
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.
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 minimum | 0 (pure HTML) |
| Custom UI needed | Yes (video, buttons, canvas) | No (browser-native dialog) |
| Permission handling | Manual (try/catch, error states) | Automatic (browser handles it) |
| Stream lifecycle | Manual stop (leak risk) | Automatic |
| Form integration | Manual (DataTransfer hack) | Native (file input, FormData) |
| Works without JS | No | Yes |
| Cross-browser (mobile) | Partial (ImageCapture: Chrome-only) | Safari, Chrome, Firefox |
| Cross-browser (desktop) | getUserMedia: broad; ImageCapture: Chrome-only | No desktop browser yet; Chrome: this CL |
// 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;
}
}
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:
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;
}
}
<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. -->
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 -->
<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
}
-->
| Browser | Mobile | Desktop |
|---|---|---|
| Chrome | Yes (Android) | This CL adds support |
| Safari | Yes (iOS) | No |
| Firefox | Yes (Android) | No |
| ImageCapture API | Chrome-only | Chrome-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.
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.
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.