getUserMedia in Production: Constraints, Permission UX, and the Device Quirks Nobody Documents
The real-world guide to camera capture in the browser — constraint negotiation, permission flows that don't scare users, and the hardware quirks that break on specific devices.
getUserMedia() looks like a five-line demo. In production it’s a negotiation layer between your constraints, the browser’s policy, the OS’s permissions, and whatever the hardware actually supports — and every layer has edge cases that bite.
The Constraint API Is a Negotiation, Not a Command
// Constraints are hints, not orders. Browser picks closest match.
const stream = await navigator.mediaDevices.getUserMedia({
video: {
width: { ideal: 1280, max: 1920 },
height: { ideal: 720 },
frameRate: { ideal: 30, max: 30 },
facingMode: 'user'
},
audio: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true
}
});
Use ideal for preferred values and exact/max only when failure is acceptable — an exact constraint on an unsupported resolution throws OverconstrainedError instead of falling back.
The Permission UX That Doesn’t Scare Users
| Pattern | User Reaction | Recommendation |
|---|---|---|
| Request on page load | “Why does this site want my camera?” — deny, leave | Don’t |
| Request on first use with context | “OK, it’s asking because I clicked record” | Do |
| Graceful deny + re-prompt path | User can reconsider without a page refresh | Required |
Always explain why before the browser prompt appears — a one-line “we need your camera to record” converts dramatically better than a cold permission request.
The Quirks That Bite
- iOS Safari requires a user gesture —
getUserMediacalled from a non-user-initiated context throws silently on older versions. Trigger from a click handler. - Android Chrome on low-end devices lies about supported resolutions —
getCapabilities()reports 1080p but the camera delivers 720p with heavy frame drops. Checktrack.getSettings()after capture starts. - Firefox’s
video constraintspersist differently — Firefox remembers camera choices per-origin more aggressively; a denied permission may needpermissions.revoke()handling. - Privacy indicators stay on — the OS-level “camera in use” LED doesn’t clear until all tracks are stopped, not just paused. Call
track.stop()not justvideoElement.srcObject = null.
“The demo works on your MacBook. Production means the $80 Android with a bottom-mounted selfie camera, the corporate laptop with a privacy shutter, and the user who clicks ‘Block’ by reflex.”
Our device-quirk test matrix, permission state machines, and the retry-UX patterns are in the getUserMedia production guide.