MY WEBCAM TEST GUIDE

How to Stop a WebRTC Video Stream and Release the Camera

Stop a WebRTC video stream properly: end every camera track with stop(), clear the video element, handle peer connections, and confirm the browser released the camera.

The short answer

To stop a WebRTC video stream and free the camera, call stop() on every track in the MediaStream you got from getUserMedia(). Removing the stream from a video element, closing a peer connection, or setting a track's enabled property to false does not release the camera on its own. Only stopping the track ends capture from the device.

function stopStream(stream) {
  if (!stream) return;
  stream.getTracks().forEach((track) => track.stop());
}

A MediaStream is a container for tracks. A MediaStreamTrack is a single audio or video source, such as your camera. When you call stop(), the track's readyState changes to "ended" permanently and the browser can release the hardware. You cannot restart an ended track. To show video again, request a new stream with getUserMedia().

Decide which "stop" you need

"Stop the video" can mean three different things. Each one has a different effect on the camera.

Goal | Method | Camera released? | Reversible?
Turn the camera off completely | track.stop() | Yes, once no other live track uses it | No, request a new stream
Mute video temporarily, keep camera ready | track.enabled = false | No, the device stays in use | Yes, set enabled = true
Stop sending video to the other side only | sender.replaceTrack(null) | No, unless you also stop the track | Yes, call replaceTrack with a track again

Setting enabled to false makes the track send black frames, or silence for audio. The device can stay active, so the camera indicator light may stay on. If the reader's concern is privacy or freeing the camera for another app, use stop().

Step-by-step: fully stop video and clean up

  • 1. Stop every track in every stream that came from the camera, including the video track and, if you want, the microphone track.
  • 2. Stop any cloned tracks. Each track created with clone() is separate and must be stopped on its own. The camera stays in use while any copy is still live.
  • 3. Detach the stream from the page by setting the video element's srcObject to null. This clears the last frame from the display. It does not stop capture.
  • 4. If the video was being sent over an RTCPeerConnection, either replace the outgoing track with null or remove the sender. If the call is over, close the connection.
  • 5. Drop your references to the old stream and tracks so they are not reused by mistake.
async function endVideo({ stream, videoEl, pc }) {
  // 1–2. End capture on every track (clones need their own stop call)
  stream?.getTracks().forEach((track) => track.stop());

  // 3. Clear the preview
  if (videoEl) videoEl.srcObject = null;

  // 4. Stop sending, or end the call entirely
  if (pc) {
    for (const sender of pc.getSenders()) {
      if (sender.track?.kind === "video") {
        await sender.replaceTrack(null);
      }
    }
    // pc.close(); // only if the whole call is finished
  }
}

Closing a peer connection does not stop the local camera tracks you passed into it. If you call pc.close() and skip track.stop(), the camera can stay on even though the call has ended.

Stop only the video and keep the microphone

To turn off the camera but stay on the call with audio, stop only the video tracks:

stream.getVideoTracks().forEach((track) => track.stop());

To turn the camera back on later, call getUserMedia() again with video only. Then pass the new video track to the existing sender with replaceTrack(). This usually avoids renegotiating the connection.

Check that it worked

  • Check the track state: every stopped track should report readyState === "ended".
  • Check the camera indicator: the browser's recording icon and any hardware light should turn off after all tracks that use the camera have stopped.
  • Check from another app: open a different camera app. If it can use the camera, your page has released it.
  • Check the remote side: if you used replaceTrack(null) or stopped the track, the other participant should stop receiving new frames.

Keep these checks separate. A track that reports "ended" means capture has stopped in your code. An indicator light that stays on means some other track, tab, or app is still using the camera. The remote view is a third question, decided by what the peer connection is sending.

Common reasons the camera stays on

  • Only the video element was cleared, and track.stop() was never called.
  • enabled = false was used, which mutes the track but keeps the device open.
  • A cloned track, or a second getUserMedia() call made elsewhere in the code, is still live.
  • Another tab or application is using the same camera.
  • The reference to the original stream was overwritten before its tracks were stopped, so nothing can reach them to stop them.

Stopping is not the same as revoking permission

Stopping a track ends capture. It does not remove the site's permission to use the camera. Depending on the browser and the user's earlier choice, a later getUserMedia() call may start the camera again without a new prompt. To revoke access, the user changes it in the browser's site settings or the operating system's privacy settings. A page cannot reliably do this through WebRTC.

Limitations of this guide

This guide follows the standard behavior of the WebRTC and Media Capture APIs. No browser-specific test results were available, so it does not claim exact timings for when indicators turn off or how each browser handles permission persistence. If behavior differs in a particular browser, check that browser's own documentation and test with the verification steps above.