Access and manage a camera stream in the browser
How to open a webcam stream in the browser, pick the webcam microphone, and handle track lifecycle (stop, mute, device changes) for clean speech capture.
The short answer
Browsers expose the webcam and its microphone through the Media Capture API. You call navigator.mediaDevices.getUserMedia() with the media you want, the user grants permission, and you receive a MediaStream that holds separate tracks for video and audio. You manage each track individually. You can disable it temporarily, listen for it ending or going silent, or stop it to release the hardware. One condition applies: the page must run in a secure context, which means HTTPS or localhost. Without permission or a secure context, you get no stream.
For speech capture, the main practical point is that the audio track is a separate object from the video track. A webcam that includes a microphone usually appears to the browser as two devices, one camera and one audio input. Your code must select the microphone you want. It is not chosen automatically because a camera was chosen.
Requesting the stream
The simplest request asks for any camera and any microphone. The browser decides which devices to use, typically the system defaults or the user's earlier choices for the site. Mute the preview element so the speakers do not play the microphone back and cause feedback.
async function startCamera() {
const stream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true
});
const preview = document.querySelector("video");
preview.muted = true;
preview.srcObject = stream;
return stream;
}
Choosing the webcam's own microphone
navigator.mediaDevices.enumerateDevices() lists the available inputs and outputs. Each entry has a kind (videoinput, audioinput or audiooutput), a deviceId, a label and a groupId. Labels are generally blank until the user has granted media permission, so the usual pattern is to request a stream first and enumerate afterwards.
The specification intends groupId to be shared by devices that belong to the same physical product. In many cases, matching the camera's groupId therefore finds the webcam's built-in microphone. This depends on how the operating system and browser report the hardware, so it is not guaranteed. Fall back to the device label or to user selection when no match exists.
const devices = await navigator.mediaDevices.enumerateDevices();
const cam = devices.find(d => d.kind === "videoinput");
const webcamMic = devices.find(
d => d.kind === "audioinput" && cam && d.groupId === cam.groupId
);
const stream = await navigator.mediaDevices.getUserMedia({
video: { deviceId: { exact: cam.deviceId } },
audio: webcamMic ? { deviceId: { exact: webcamMic.deviceId } } : true
});
With exact, the request fails if that device is unavailable. With ideal, the browser treats your choice as a preference and may substitute another device. Use exact when the user explicitly picked a microphone. Use ideal when you only want a sensible default.
Track lifecycle: enabled, muted, ended
Each MediaStreamTrack has a few separate states. They are easy to confuse, and they behave differently for speech capture.
- enabled: a property your code sets. Setting it to false makes an audio track produce silence and a video track produce black frames. The track stays alive and can be re-enabled instantly. This is the usual way to build a mute button.
- muted: a read-only state the browser controls. It means the source is temporarily not delivering media, for example because the operating system or hardware has interrupted it. The track fires mute and unmute events as this changes.
- readyState: either live or ended. Once a track is ended, it cannot be restarted. You need a new getUserMedia call.
A disabled track generally keeps the device allocated, and whether the camera indicator light turns off varies by browser and hardware. To release the camera and microphone fully, call stop() on every track. If you cloned a track with clone(), the hardware is typically released only after the original and all clones are stopped.
function stopStream(stream) {
stream.getTracks().forEach(track => track.stop());
}
const [micTrack] = stream.getAudioTracks();
micTrack.addEventListener("ended", () => {
// Device unplugged, permission revoked, or source lost
});
micTrack.addEventListener("mute", () => {
// Source temporarily delivering no audio
});
The ended event fires when the track ends for reasons outside your code, such as the webcam being unplugged or permission being revoked. It does not fire when you call stop() yourself. Put your cleanup logic in one function and call it from both paths.
Handling device changes
navigator.mediaDevices fires a devicechange event when inputs are added or removed. Re-enumerate when it fires, update any device picker, and check whether your active tracks have ended. Switching microphones mid-session usually means requesting a new audio-only stream for the new deviceId and stopping the old audio track. On a WebRTC call, you can then swap the new track in with RTCRtpSender.replaceTrack(), which avoids renegotiating the connection.
async function switchMic(stream, sender, deviceId) {
const fresh = await navigator.mediaDevices.getUserMedia({
audio: { deviceId: { exact: deviceId } }
});
const [newTrack] = fresh.getAudioTracks();
const [oldTrack] = stream.getAudioTracks();
if (sender) await sender.replaceTrack(newTrack);
stream.removeTrack(oldTrack);
oldTrack.stop();
stream.addTrack(newTrack);
}
Speech processing settings
Browsers can apply three voice-oriented processing steps to microphone audio. Echo cancellation removes sound from your speakers that the microphone picks up again. Noise suppression reduces steady background noise. Automatic gain control adjusts the level so quiet and loud speakers end up closer together. Browsers commonly enable these by default for getUserMedia audio, which suits calls. When you want to judge what a webcam microphone actually captures, these steps hide its raw behaviour, so turn them off.
const stream = await navigator.mediaDevices.getUserMedia({
audio: {
deviceId: { exact: webcamMic.deviceId },
echoCancellation: false,
noiseSuppression: false,
autoGainControl: false
}
});
console.log(stream.getAudioTracks()[0].getSettings());
Support for these constraints and for others such as sampleRate and channelCount varies between browsers, and a browser may ignore a non-exact constraint. Check navigator.mediaDevices.getSupportedConstraints() before relying on one. After capture starts, read track.getSettings() to see what was actually applied. track.getCapabilities() reports what the device allows, where the browser supports it, and track.applyConstraints() changes settings on a live track.
Common errors
- NotAllowedError: the user or a browser policy denied permission. Also check that the page is served over HTTPS or localhost.
- NotFoundError: no device matches the request, for example no microphone is connected.
- NotReadableError: the device exists but could not be opened, often because another application is using it or the operating system blocked access.
- OverconstrainedError: an exact constraint, such as a specific deviceId, could not be met.
What this guide cannot tell you
No measurements, test results or browser version data were available for this guide. It does not say how well any particular webcam microphone captures speech, and it does not list which browser versions support each constraint. The behaviour described above follows the published Media Capture and WebRTC interfaces. Indicator lights, groupId matching and constraint support depend on the browser, operating system and hardware. Test them on the setups you need to support.