[SOLVED] Request/add webcam after calling navigator.mediaDevices.getUserMedia() or removing video track

Issue

I’ve created a basic MediaStream which gets a video & audio track in my react-app like this:

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

setLocalStream(getLocalStream);
  const handleShowCamClick = async () => {
    if (!callContext.localStream) return;
    callContext.localStream.getVideoTracks().forEach((track: MediaStreamTrack) => track.enabled = true);
    callContext.setShowCam(true); 
  };

  const handleHideCamClick = () => {
    if (!callContext.localStream) return;
    // callContext.localStream.getVideoTracks().forEach((track: MediaStreamTrack) => track.enabled = false);
    callContext.localStream.getVideoTracks().forEach((track: MediaStreamTrack) => track.stop());
    callContext.setShowCam(false); 
  };

So now I want the user to be able to disable its webcam. Setting track.enabled = false will result in the webcam still being used by the webapp but turning the video to black, which is not the behaviour I want.

Instead I want the webcam to no longer be used by the webapp.
I have a webcam with a blue light shining every time to show that the cam is recording. With track.enabled = false my webcam shows me that its still technically recording.

If I remove the video, track.stop() will result in the behaviour I want. The webcam is no longer used, but I how do I add the video track of the webcam back to the localStream?

track.stop() removes the track from localStream and frees the webcam from the MediaStream, but since the video track is not there anymore, how can I request a new video track of the webcam and attach it to localStream without re-initializing a MediaStream?

Solution

The following solution uses Vanilla Javascript because this is a problem that I’m sure a lot of folks will be interested in solving in the future.

Treat this as a proof of concept that can be adapted to ANY Javascript framework to support WebRTC tasks.

Final Note – This example only deals with the fetching, displaying, and merging of Media Streams. All other WebRTC stuff like sending the stream across via RTCPeerConnection, etc. has to be performed using the streams created here – and replacing/updating those is beyond the scope of this example.

Core Idea:

  1. Fetch Stream via getUserMedia()

  2. Assign Stream to HTMLMediaElement

  3. Use getVideoTracks() to stop the video track only.

  4. Use getUserMedia() again to fetch a new stream without audio.

  5. Use MediaStream constructor to create a new stream using – the video from the new stream + audio from existing stream as follows –

    new MediaStream([…newStream.getVideoTracks(), …existingStream.getAudioTracks()]);

  6. Use newly generated MediaStream as required (i.e. replace in RTCPeerConnection, etc.).

CodePen Demo

let localStream = null;
let mediaWrapperDiv = document.getElementById('mediaWrapper');
let videoFeedElem = document.createElement('video');
videoFeedElem.id = 'videoFeed';
videoFeedElem.width = 640;
videoFeedElem.height = 360;
videoFeedElem.autoplay = true;
videoFeedElem.setAttribute('playsinline', true);

mediaWrapperDiv.appendChild(videoFeedElem);

let fetchStreamBtn = document.getElementById('fetchStream');
let killEverythingBtn = document.getElementById('killSwitch');
let killOnlyVideoBtn = document.getElementById('killOnlyVideo');
let reattachVideoBtn = document.getElementById('reattachVideo');

async function fetchStreamFn() {
  localStream = await navigator.mediaDevices.getUserMedia({
    video: true,
    audio: true
  });
  if (localStream) {
    await attachToDOM(localStream);
  }
}

async function killEverythingFn() {
  localStream.getTracks().map(track => track.stop());
  localStream = null;
}

async function killOnlyVideoFn() {
  localStream.getVideoTracks().map(track => track.stop());
}

async function reAttachVideoFn() {
  let existingStream = localStream;
  let newStream = await navigator.mediaDevices.getUserMedia({
    video: true,
    audio: false
  });
  localStream = new MediaStream([...newStream.getVideoTracks(), ...existingStream.getAudioTracks()]);
  if (localStream) {
    await attachToDOM(localStream);
  }
}

async function attachToDOM(stream) {
  videoFeedElem.srcObject = new MediaStream(stream.getTracks());
}

fetchStreamBtn.addEventListener('click', fetchStreamFn);
killOnlyVideoBtn.addEventListener('click', killOnlyVideoFn);
reattachVideoBtn.addEventListener('click', reAttachVideoFn);
killEverythingBtn.addEventListener('click', killEverythingFn);
div#mediaWrapper {
  margin: 0 auto;
  text-align: center;
}

div#mediaWrapper video {
  object-fit: cover;
}

div#mediaWrapper video#videoFeed {
  border: 2px solid blue;
}

div#btnWrapper {
  text-align: center;
  margin-top: 10px;
}

button {
  border-radius: 0.25rem;
  color: #ffffff;
  display: inline-block;
  font-size: 1rem;
  font-weight: 400;
  line-height: 1.6;
  padding: 0.375rem 0.75rem;
  text-align: center;
  cursor: pointer;
}

button.btn-blue {
  background-color: #007bff;
  border: 1px solid #007bff;
}

button.btn-red {
  background-color: #dc3545;
  border: 1px solid #dc3545;
}

button.btn-green {
  background-color: #28a745;
  border: 1px solid #28a745;
}
<h3>How to check if this actually works?
  <h3>
    <h4>Just keep speaking in an audibly loud volume, you'll hear your own audio being played from your device's speakers.<br> You should be able to hear yourself even after you "Kill Only Video" (i.e. Webcam light goes off)
    </h4>
    <div id="mediaWrapper"></div>
    <div id="btnWrapper">
      <button id="fetchStream" class="btn-blue" type="button" title="Fetch Stream (Allow Access)">Fetch Stream</button>
      <button id="killOnlyVideo" class="btn-red" type="button" title="Kill Only Video">Kill Only Video</button>
      <button id="reattachVideo" class="btn-green" type="button" title="Re-attach Video">Re-attach Video</button>
      <button id="killSwitch" class="btn-red" type="button" title="Kill Everything">Kill Everything</button>
    </div>

Answered By – BkiD

Answer Checked By – Jay B. (BugsFixing Admin)

Leave a Reply

Your email address will not be published. Required fields are marked *