Request/add 调用 navigator.mediaDevices.getUserMedia() 或删除视频轨道后的网络摄像头

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

我创建了一个基本的 MediaStream,它在我的 react-app 中获取视频和音频轨道,如下所示:

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); 
  };

所以现在我希望用户能够禁用其网络摄像头。设置 track.enabled = false 将导致网络摄像头仍被网络应用程序使用,但将视频变为黑色,这不是我想要的行为。

相反,我希望网络应用程序不再使用网络摄像头。 我有一个网络摄像头,每次都会发出蓝光以表明摄像头正在录制。使用 track.enabled = false 我的网络摄像头显示它在技术上仍在记录。

如果我删除视频,track.stop() 将导致我想要的行为。网络摄像头不再使用,但是我如何将网络摄像头的视频轨道添加回 localStream?

track.stop()localStream 中删除轨道并从 MediaStream 中释放网络摄像头,但由于视频轨道不再存在,我如何请求网络摄像头的新视频轨道并附加它到 localStream 而不重新初始化 MediaStream?

以下解决方案使用 Vanilla Javascript 因为这是一个我相信将来会有很多人有兴趣解决的问题。

将此视为概念验证,可以适用于任何 Javascript 框架以支持 WebRTC 任务。

最后说明 - 这个例子只处理媒体流的获取、显示和合并。所有其他 WebRTC 内容,例如通过 RTCPeerConnection 发送流等,都必须使用此处创建的流来执行 - replacing/updating 这些超出了本示例的范围。

核心理念:

  1. 通过 getUserMedia() 获取流

  2. 将流分配给 HTMLMediaElement

  3. 使用 getVideoTracks() 仅停止视频轨道。

  4. 再次使用 getUserMedia() 获取没有音频的新流。

  5. 使用 MediaStream 构造函数创建新流,使用 - 来自新流的视频 + 来自现有流的音频,如下所示 -

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

  6. 根据需要使用新生成的 MediaStream(即替换为 RTCPeerConnection 等)。

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>