The definitive guide to getUserMedia()

By

Learn how to access cameras and microphones with getUserMedia(), choose devices, apply constraints, handle errors, and stop every media track.

~~~

The getUserMedia() method gives a web page access to a camera, microphone, or both.

Video calls are the obvious use case. You can also use it for audio recorders, QR scanners, profile photos, voice notes, and local video previews.

Camera and microphone access is sensitive. The browser asks the user for permission, shows an indicator while a device is active, and limits the API to secure contexts.

Let’s build a camera preview first, then look at constraints, devices, errors, and cleanup.

The smallest camera example

Add a video element and two buttons:

<video id="preview" autoplay playsinline muted></video>
<button id="start" type="button">Start camera</button>
<button id="stop" type="button" disabled>Stop camera</button>

Request a video track after the user clicks the Start button:

const preview = document.querySelector('#preview')
const startButton = document.querySelector('#start')
const stopButton = document.querySelector('#stop')

let stream

startButton.addEventListener('click', async () => {
  stream = await navigator.mediaDevices.getUserMedia({ video: true })
  preview.srcObject = stream

  startButton.disabled = true
  stopButton.disabled = false
})

getUserMedia() returns a promise. It resolves to a MediaStream containing one or more MediaStreamTrack objects.

Assign the stream to video.srcObject. Do not turn it into an object URL.

autoplay starts playback when the stream arrives. playsinline keeps the preview inline on mobile devices. The preview is muted so it cannot create audio feedback when you request a microphone too.

Request audio, video, or both

The argument is a constraints object.

Request only a microphone:

const stream = await navigator.mediaDevices.getUserMedia({
  audio: true,
})

Request audio and video:

const stream = await navigator.mediaDevices.getUserMedia({
  audio: true,
  video: true,
})

At least one of audio or video must be requested. If both are missing or false, the promise rejects.

The old callback-based navigator.getUserMedia() API is legacy. Use the promise-based method on navigator.mediaDevices.

getUserMedia() requires a secure context

Camera and microphone access is available only in a secure context. In practice, use HTTPS.

Browsers also treat http://localhost as potentially trustworthy for local development. A regular HTTP page on another hostname cannot use this API.

Check whether the API exists before showing controls that depend on it:

if (!navigator.mediaDevices?.getUserMedia) {
  throw new Error('Camera and microphone access is not available')
}

An embedded page can also be blocked by Permissions Policy. The top-level page controls whether an iframe may request camera or microphone access.

Ask only after a clear user action

Do not request a camera as soon as a page loads.

Explain why you need it, then let the user press a button. This gives the permission prompt context and avoids surprising them.

Permission is not permanent or predictable. A browser may remember a decision, make it temporary, or ask again. The operating system can also block access independently.

Build your interface around three outcomes:

  • access works
  • access is denied
  • no suitable device is available

Do not hide the entire feature behind a permission query. The request itself remains the important step.

Prefer ideal constraints first

Passing video: true lets the browser choose suitable settings. You can ask for a preferred resolution:

const stream = await navigator.mediaDevices.getUserMedia({
  video: {
    width: { ideal: 1280 },
    height: { ideal: 720 },
  },
})

ideal expresses a preference. The browser can return another size if necessary.

Use exact, min, or max only when the application truly requires them:

const stream = await navigator.mediaDevices.getUserMedia({
  video: {
    width: { min: 640, ideal: 1280, max: 1920 },
    aspectRatio: { ideal: 16 / 9 },
  },
})

Required constraints can cause an OverconstrainedError. My advice is to start loose, inspect what you received, and tighten only the setting that matters.

Common video constraints include:

  • width and height
  • aspectRatio
  • frameRate
  • facingMode
  • deviceId

Common audio constraints include:

  • echoCancellation
  • noiseSuppression
  • autoGainControl
  • channelCount

Support can vary by device. Check which names the browser recognizes:

const supported = navigator.mediaDevices.getSupportedConstraints()
console.log(supported)

This tells you whether a constraint name is understood. It does not promise that every device can satisfy every value.

Choose the front or rear camera

On phones, facingMode can express which direction you prefer.

Request the camera facing the user:

const stream = await navigator.mediaDevices.getUserMedia({
  video: { facingMode: 'user' },
})

Prefer the camera facing the environment:

const stream = await navigator.mediaDevices.getUserMedia({
  video: { facingMode: { ideal: 'environment' } },
})

Use ideal unless a rear camera is a hard requirement. Laptops often have only one camera.

Inspect the selected settings

Constraints describe what you requested. Settings describe what the browser selected.

const [videoTrack] = stream.getVideoTracks()
const settings = videoTrack.getSettings()

console.log(settings.width)
console.log(settings.height)
console.log(settings.frameRate)

This is useful when your layout, recorder, or processing pipeline depends on the actual dimensions.

You can also inspect getCapabilities() to see the ranges exposed by the source. Treat that information carefully because device capabilities can add to browser fingerprinting.

Change a track after it starts

Use applyConstraints() to change an active track:

const [videoTrack] = stream.getVideoTracks()

await videoTrack.applyConstraints({
  width: { ideal: 640 },
  height: { ideal: 360 },
})

This is useful when switching from a large preview to a smaller one. It does not create a new stream.

The call can reject when the new constraints cannot be satisfied. Keep the previous settings working and show a useful error.

Switch cameras safely

Switching cameras usually means stopping the current video track and requesting a new one.

async function switchCamera(deviceId) {
  for (const track of stream?.getVideoTracks() || []) {
    track.stop()
  }

  const nextStream = await navigator.mediaDevices.getUserMedia({
    audio: false,
    video: {
      deviceId: { exact: deviceId },
    },
  })

  stream = nextStream
  preview.srcObject = nextStream
}

If the application also has an audio track, do not throw it away by accident. Either request audio again or combine the existing audio track with the new video track.

const audioTracks = stream.getAudioTracks()
const nextVideo = await navigator.mediaDevices.getUserMedia({
  video: { deviceId: { exact: deviceId } },
})

stream = new MediaStream([
  ...audioTracks,
  ...nextVideo.getVideoTracks(),
])

Stop the old video tracks after the new request succeeds if you need to preserve the working preview when selection fails. The correct order depends on whether the device can run both cameras at once, so test mobile hardware.

Keep the selected deviceId as a preference, not a permanent identity. Device identifiers and availability can change.

List cameras and microphones

Use enumerateDevices():

const devices = await navigator.mediaDevices.enumerateDevices()

const cameras = devices.filter((device) => {
  return device.kind === 'videoinput'
})

const microphones = devices.filter((device) => {
  return device.kind === 'audioinput'
})

For privacy, labels and other device information may be limited until the page has permission or an active stream.

A common flow is:

  1. ask for the default camera or microphone
  2. enumerate devices after permission is granted
  3. show the available choices
  4. request the selected deviceId
const stream = await navigator.mediaDevices.getUserMedia({
  video: {
    deviceId: { exact: selectedDeviceId },
  },
})

Device lists can change when hardware is connected or removed. Listen for devicechange and rebuild the choices:

navigator.mediaDevices.addEventListener('devicechange', updateDeviceList)

Stop every track

Removing the preview element does not stop the camera. Clearing srcObject does not stop it either.

Call stop() on every track:

function stopMedia() {
  if (!stream) return

  for (const track of stream.getTracks()) {
    track.stop()
  }

  preview.srcObject = null
  stream = undefined

  startButton.disabled = false
  stopButton.disabled = true
}

stopButton.addEventListener('click', stopMedia)

This releases your page’s tracks. A physical device may remain active if another tab or application still uses it.

Stop old tracks before requesting a different camera. Otherwise you can leave more hardware active than intended.

Mute without stopping

Set a track’s enabled property to false when you want a temporary mute:

const [audioTrack] = stream.getAudioTracks()
audioTrack.enabled = false

Set it back to true to resume.

Muting and stopping are different. A disabled track can become active again. A stopped track has ended and cannot restart.

Handle errors by name

Always wrap the request in try and catch:

try {
  stream = await navigator.mediaDevices.getUserMedia({
    audio: true,
    video: true,
  })
} catch (error) {
  if (error.name === 'NotAllowedError') {
    showMessage('Camera or microphone access was not allowed')
  } else if (error.name === 'NotFoundError') {
    showMessage('No suitable camera or microphone was found')
  } else if (error.name === 'NotReadableError') {
    showMessage('The device could not be started')
  } else if (error.name === 'OverconstrainedError') {
    showMessage(`The ${error.constraint} requirement is not available`)
  } else {
    showMessage('Media access failed')
  }
}

NotAllowedError can mean the user denied access, the browser blocked it, the operating system denied it, the document is insecure, or a policy disallowed it. Avoid telling the user to “click Allow” as if denial were the only cause.

NotFoundError means no requested track matches. NotReadableError commonly means the browser found a device but could not use it.

Do not print raw error messages to users. Log details for debugging and give the user one actionable next step.

Design the preview for real devices

A camera preview needs more than srcObject.

Wait for video metadata before reading videoWidth or videoHeight:

preview.addEventListener('loadedmetadata', () => {
  console.log(preview.videoWidth, preview.videoHeight)
})

Those dimensions describe the video track, not the CSS box. Use object-fit: cover when the preview may crop to fill a fixed frame, or contain when every captured pixel must stay visible.

Mirroring a front-camera preview can feel natural:

.preview-user {
  transform: scaleX(-1);
}

Do not mirror the saved photo or transmitted stream unless that is truly the desired output. The transform only changes how the local video element is displayed.

If audio is part of the stream, keep the local preview muted. Playing microphone audio through nearby speakers creates feedback.

Handle page visibility deliberately. Hiding a tab does not mean your application no longer owns the stream. For a short capture flow, stop tracks when the user closes the dialog or leaves the step. For a call, show the current camera and microphone state when they return.

Test orientation changes, device rotation, incoming calls, unplugged hardware, and a second tab trying to use the same device. The permission happy path is only the start of a reliable media interface.

React when a track ends

A device can disappear or access can end outside your interface.

const [videoTrack] = stream.getVideoTracks()

videoTrack.addEventListener('ended', () => {
  preview.srcObject = null
  showMessage('The camera stopped')
})

Also handle page navigation and component cleanup. If this code lives inside a Custom Element or framework component, stop its tracks when the component is destroyed.

Capture a still image

Once the video has current data, draw a frame to a canvas:

const canvas = document.createElement('canvas')
const context = canvas.getContext('2d')

canvas.width = preview.videoWidth
canvas.height = preview.videoHeight

context.drawImage(preview, 0, 0)

const blob = await new Promise((resolve) => {
  canvas.toBlob(resolve, 'image/jpeg', 0.9)
})

The resulting Blob can be previewed, downloaded, or uploaded. See my Canvas API guide for drawing and sizing details.

Record the stream

MediaRecorder can turn the captured stream into chunks:

const chunks = []
const recorder = new MediaRecorder(stream)

recorder.addEventListener('dataavailable', (event) => {
  if (event.data.size > 0) {
    chunks.push(event.data)
  }
})

recorder.addEventListener('stop', () => {
  const recording = new Blob(chunks, {
    type: recorder.mimeType,
  })

  showRecording(recording)
})

recorder.start()

Recording does not stop capture. Call recorder.stop() to finish the recording, then stop every media track when the camera or microphone is no longer needed.

Check MediaRecorder.isTypeSupported() before requesting a particular container or codec. Browser and operating-system support varies.

Where getUserMedia() fits

getUserMedia() only captures local media. It does not record, upload, or send that media to another browser.

Use other APIs for the next step:

  • MediaRecorder records a stream into media chunks
  • Web Audio processes audio
  • Canvas reads or transforms video frames
  • WebRTC sends real-time media between peers

Keep those concerns separate. First acquire and clean up the stream correctly. Then pass it to the API that performs the next job.

When I would use it

I would use getUserMedia() for a camera preview, a QR scanner, or a short voice-note recorder where the browser can do the job without installing an application.

I would always provide an alternative when the media is not essential. A profile form should still accept an uploaded image. A support form should still accept typed text.

I would not leave a stream active while the user moves through unrelated screens. Camera and microphone access should be visible, deliberate, and easy to stop.

The Media Capture and Streams specification defines the current API, constraints model, and track lifecycle.

~~~

Related posts about platform: