MediaStream API — the pragmatic standard
The MediaStream API (part of the WebRTC family) has been available in all mainstream browsers since 2015. It lets a web page — after explicit permission — access the webcam, smartphone camera, and microphone. By 2026 the stack has stabilized and is mainstream-ready for profile photo upload, QR scanning, live filters, AR effects, and document scanning.
The basic pattern
async function startCamera() {
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: {
facingMode: "user", // "environment" for the rear camera
width: { ideal: 1920 },
height: { ideal: 1080 }
},
audio: false
});
const video = document.querySelector("video");
video.srcObject = stream;
await video.play();
} catch (err) {
console.error("Camera access denied or unavailable:", err);
}
}Permissions and UX
getUserMedia() triggers a browser permission dialog. Key UX practices:
- Never ask on page load. Only after an explicit user action (a button click) — otherwise users deny out of hand.
- Explain why beforehand. A short note like "We use the camera for your profile photo" ahead of the browser prompt measurably increases the approval rate.
- Offer a fallback. If permission is denied, provide a classic file upload as plan B.
- Permission is remembered. Chrome stores the answer per site — on the second visit no dialog is shown.
Creating a snapshot in a canvas
Turning a running video stream into a static snapshot:
function takeSnapshot(video) {
const canvas = document.createElement("canvas");
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
const ctx = canvas.getContext("2d");
ctx.drawImage(video, 0, 0);
return canvas.toBlob(blob => {
const url = URL.createObjectURL(blob);
document.querySelector("img.preview").src = url;
}, "image/jpeg", 0.9);
}The blob can be uploaded directly as a profile photo, stored in IndexedDB, or processed further — for example with background removal.
Use case 1 — profile photo upload
The classic: the user takes a selfie for their profile. Best practice:
- Camera with
facingMode: "user"for the selfie cam. - Live preview with a soft crop box in the overlay (e.g. a 400×400 circle).
- Mirror the preview (CSS
transform: scaleX(-1)) — users see themselves as in a mirror. - On capture: save not mirrored.
- Strip EXIF on the server (see the EXIF post).
Use case 2 — QR code scanning
Browser QR scanning without a native app. Stack: getUserMedia() + canvas + the zxing-wasm library (a WebAssembly port of ZXing). Performance on mid-range smartphones: 5–10 FPS, enough for a stable scan.
Important: use the rear camera via facingMode: "environment". On smartphones autofocus is decisive — enable it via applyConstraints({ focusMode: "continuous" }).
Use case 3 — document scanning
Digitizing a passport photo, a contract, a receipt. Pipeline:
- Camera stream in fullscreen.
- Edge detection via OpenCV.js or the newer
shape-detector-api(Chrome). - Auto-crop to the detected document outline.
- Perspective correction (trapezoid → rectangle).
- Black-and-white conversion with adaptive thresholding.
- Export as PNG or PDF.
In 2026, browser-only document scanners are competitive with native apps like Scanner Pro or Adobe Scan — with the privacy advantage that nothing gets uploaded.
Use case 4 — AR preview
Furniture shoppers want to see how the chair looks in their own living room. Browser AR is production-ready in 2026 via:
- WebXR Device API: native AR sessions in Chrome on Android ARCore devices and Safari on iPhones (since visionOS 1.0).
- MediaStream + Three.js/AR.js: poor man's AR via marker tracking, works in any browser.
- Model-viewer web componentfrom Google: embeds a 3D model with a "View in AR" button that launches iOS Quick Look or Android Scene Viewer.
Use case 5 — live background effects
Processing the video stream live: background blur (like Zoom), green-screen replacement, beauty filters. Stack:
getUserMedia()stream.- Frame by frame via
requestVideoFrameCallback. - Segmentation with MediaPipe or BiRefNet (see the background removal post).
- Composite with the replacement background on a second canvas.
- Output stream via
captureStream()into a new video element.
Performance: on an M2 CPU with WebGPU, 30+ FPS at 720p. On mid-range Android with MediaPipe, ~15–20 FPS — acceptable.
Browser compatibility in 2026
- Chrome 110+: the full MediaStream API plus WebGPU-accelerated processing.
- Safari 17+: stable on macOS and iOS. iOS 17 finally brought PWA camera access without a Safari detour.
- Firefox 130+: full MediaStream, WebGPU stable since 2025.
- Edge: Chromium-based, identical to Chrome.
- Mobile in-app browsers (TikTok, Instagram): often restricted. If a user opens your link from inside an app, camera access may be missing.
Privacy in practice
- Always stop the stream when the feature is done:
stream.getTracks().forEach(t => t.stop()). Otherwise the camera keeps running and the LED stays on. - Never record unexpectedly. The
MediaRecorderAPI can record audio/video. If you use it, get explicit consent. - HTTPS is mandatory.
getUserMedia()only works over HTTPS or onlocalhost. Safe against MITM eavesdroppers.
HTML fallback without MediaStream
On mobile there is a nice classic route: <input type="file" accept="image/*" capture="user">opens the camera app directly on iOS and Android. No permission dialog, no stream management. Not as flexible as MediaStream, but for "take a photo and upload it" use cases it's often the simpler choice.
Tool recommendations
- The web API directly: for simple photo capture and profile photo upload.
- html5-qrcode or zxing-wasm: for QR scanning.
- MediaPipe: for face detection, segmentation, pose estimation.
- OpenCV.js: for classic image-processing algorithms.
- Google's model-viewer: for AR previews without your own 3D stack.
Sources
MDN — getUserMedia · W3C — Media Capture and Streams · Google MediaPipe · zxing-js library · OpenCV.js · W3C — WebXR Device API · model-viewer (Google) · web.dev — Capturing Images.