Stop browser camera tracks with navigator.mediaDevices and MediaStreamTrack.stop()
How to release a webcam and its built-in microphone in the browser: stop every MediaStreamTrack, why navigator.mediaDevices has no stop method, and how stopping differs from muting.
The short answer
navigator.mediaDevices has no method that stops the camera. It hands you a MediaStream through getUserMedia(), and you release the hardware by calling stop() on each MediaStreamTrack in that stream. A webcam request that includes audio produces at least two tracks: one video track for the camera and one audio track for the microphone. Stopping only the video track leaves the microphone open, and stopping only the audio track leaves the camera running. To fully release the device, stop every track, including any copies you made with clone().
// Acquire camera and microphone
const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
// Later: release both devices
stream.getTracks().forEach((track) => track.stop());
Why the microphone matters when you stop a webcam
Many webcams include a built-in microphone, and browsers expose it as a separate audio input. If your app captures speech from that microphone, it holds an audio track that is independent of the video track. If you stop only the video, the camera may appear to shut off while the page can still hear the room. For a speech-capture or conferencing tool, stopping the audio track is as important as stopping the video track.
To stop one kind of capture and keep the other, select tracks by kind:
// Stop only the microphone, keep the camera running
stream.getAudioTracks().forEach((track) => track.stop());
// Stop only the camera, keep the microphone running
stream.getVideoTracks().forEach((track) => track.stop());
What stop() does and does not do
- After stop(), the track's readyState becomes "ended" and the track delivers no more audio or video.
- Stopping is permanent for that track. You cannot restart it. To capture again, call getUserMedia() again and use the new stream.
- Calling stop() does not fire the track's own "ended" event. That event fires when the source ends for other reasons, such as the device being unplugged or permission being revoked. Do not rely on it to confirm your own stop() call.
- Stopping a track does not revoke the site's camera or microphone permission. A later getUserMedia() call may succeed without a new prompt, depending on the browser and the user's permission settings.
- The browser releases a device only after every track using it has stopped. Clones, and tracks from separate getUserMedia() calls, all count.
Common reasons the camera or microphone stays on
- Clearing the preview element (for example, setting video.srcObject = null) only detaches the stream from the element. The tracks keep running until you stop them.
- The app called getUserMedia() more than once, for example on each button press or component re-render, and stopped only the most recent stream.
- A track was cloned for a recorder, an audio analyser or a peer connection, and the clone was never stopped.
- Only the video tracks were stopped, so the webcam's microphone is still capturing.
Keeping a reference to every stream you acquire, and stopping all of their tracks in one cleanup function, avoids most of these problems.
function releaseStream(stream, videoElement) {
if (!stream) return;
stream.getTracks().forEach((track) => track.stop());
if (videoElement) videoElement.srcObject = null;
}
Stopping versus muting
Setting track.enabled = false is not the same as stopping. A disabled audio track produces silence and a disabled video track produces black frames, but the device stays open and the track can be turned back on instantly. That suits a temporary mute button in a call. stop() fully releases the hardware but requires a new getUserMedia() call to resume. Use enabled for mute and unmute, and stop() when the user leaves the call, closes the capture view, or switches to a different microphone or camera.
| Action | Device stays open | Can resume on the same track | Typical use |
| track.enabled = false | Yes | Yes | Temporary mute during a call |
| track.stop() | No, once all tracks using the device are stopped | No; request a new stream | Ending capture or switching devices |
| video.srcObject = null | Yes | Not applicable; only the preview is detached | Clearing the preview, combined with stop() |
Switching to a different microphone or camera
When a user picks another audio input, such as moving from the webcam's built-in microphone to a headset, first stop the old audio track, then request the new device by its deviceId. If the audio is being sent over an RTCPeerConnection, replaceTrack() on the relevant sender swaps in the new track without renegotiating the connection. Stop the previous track afterwards so the old microphone is released.
async function switchMicrophone(stream, deviceId) {
stream.getAudioTracks().forEach((track) => track.stop());
const newStream = await navigator.mediaDevices.getUserMedia({
audio: { deviceId: { exact: deviceId } }
});
return newStream.getAudioTracks()[0];
}
Limits of this guide
The behavior described here follows the documented Media Capture and Streams model. No measured evidence was available on how quickly specific browsers release a device after stop(), or on whether a given webcam's activity light turns off at the same moment. Hardware indicator behavior depends on the device and its driver. Browsers also differ in how they show and remember permissions. If exact timing or indicator behavior matters for your product, test it on the browsers and webcams your users actually have.