4. 媒体流控制:开启/关闭麦克风、开启/关闭摄像头、切换前后摄像头、屏幕共享(getDisplayMedia)

这一章,我们来聊聊媒体流的实时控制。说白了,就是怎么让用户自己决定——什么时候说话、什么时候露脸、什么时候把手机转过来,或者干脆把屏幕分享出去。

我在做第一个WebRTC项目时,觉得这些控制就是调几个API,简单。结果呢?用户反馈说「我关了摄像头,怎么对方还能看到我最后一帧画面?」——嗯,这里面的坑,咱们一个个填。

4.1 获取媒体流的基础回顾

在动手控制之前,你得先拿到媒体流。我们通常用 getUserMedia 来获取音视频流:

const constraints = {
  audio: true,
  video: true
};

const stream = await navigator.mediaDevices.getUserMedia(constraints);

拿到 stream 之后,就可以对它进行各种操作了。我个人习惯把 stream 保存在一个全局变量里,方便后续控制。

4.2 开启/关闭麦克风

关闭麦克风,不是把 stream 停掉,而是把音频轨道禁用了。你想想看,如果直接停掉整个流,那视频也没了。

正确的做法是:

// 获取音频轨道
const audioTrack = stream.getAudioTracks()[0];

// 关闭麦克风
audioTrack.enabled = false;

// 开启麦克风
audioTrack.enabled = true;

enabled 属性控制轨道是否发送数据。设为 false 时,轨道还在,但不发送任何数据。对方会收到静音。

注意: 不要用 audioTrack.stop() 来关闭麦克风。stop() 会彻底销毁轨道,想再开启就得重新调用 getUserMedia,非常麻烦。

我曾经在项目里犯过这个错。用户点了一下静音,结果再点取消静音,麦克风没反应了。排查了半天,发现是 stop() 把轨道干掉了。从那以后,我记住了:静音用 enabled,不用 stop

4.3 开启/关闭摄像头

摄像头控制跟麦克风类似,也是操作 enabled 属性:

const videoTrack = stream.getVideoTracks()[0];

// 关闭摄像头
videoTrack.enabled = false;

// 开启摄像头
videoTrack.enabled = true;

但这里有个视觉上的问题。当 enabled = false 时,对方的 <video> 元素会显示最后一帧画面,或者黑屏。这取决于浏览器实现。

小技巧: 我建议在关闭摄像头时,把本地 <video> 元素替换成一张占位图(比如灰色头像),这样用户体验更好。对方端也可以做同样的处理。

4.4 切换前后摄像头

移动端经常需要切换前后摄像头。这个操作稍微复杂一点,因为你需要重新获取一个新的视频流。

基本思路是:

  1. 停止当前视频轨道
  2. 用新的 facingMode 重新调用 getUserMedia
  3. 把新轨道替换到原有流中

代码示例:

async function switchCamera() {
  const currentVideoTrack = stream.getVideoTracks()[0];
  const currentFacingMode = currentVideoTrack.getSettings().facingMode;
  
  // 切换 facingMode
  const newFacingMode = currentFacingMode === 'user' ? 'environment' : 'user';
  
  // 停止旧轨道
  currentVideoTrack.stop();
  
  // 获取新流
  const newStream = await navigator.mediaDevices.getUserMedia({
    audio: false,
    video: { facingMode: newFacingMode }
  });
  
  const newVideoTrack = newStream.getVideoTracks()[0];
  
  // 替换轨道
  stream.removeTrack(currentVideoTrack);
  stream.addTrack(newVideoTrack);
  
  // 更新 
核心要点: 切换摄像头不是「切换」,而是「替换」。旧轨道停掉,新轨道加进来。记得用 removeTrackaddTrack 来操作。

我个人习惯在切换时加一个 loading 状态。因为 getUserMedia 需要时间,尤其是从后置切到前置时,有些设备会卡一下。给用户一个反馈,体验会好很多。

4.5 屏幕共享(getDisplayMedia)

屏幕共享是 WebRTC 的另一个重要功能。它跟 getUserMedia 不同,用的是 getDisplayMedia API。

const displayStream = await navigator.mediaDevices.getDisplayMedia({
  video: true,
  audio: true  // 可选,捕获系统音频
});

调用这个 API 后,浏览器会弹出一个选择窗口,让用户选择要共享的内容:整个屏幕、某个窗口,或者某个浏览器标签页。

重要提醒: getDisplayMedia 必须由用户手势触发(比如点击按钮)。你不能在页面加载时自动调用它。这是浏览器的安全策略。

屏幕共享的流跟摄像头流一样,可以添加到 RTCPeerConnection 中:

// 把屏幕共享流添加到连接中
displayStream.getTracks().forEach(track => {
  peerConnection.addTrack(track, displayStream);
});

我曾经遇到过一个场景:用户共享屏幕后,又想切回摄像头。这时候不能直接停掉屏幕共享流,而是要把摄像头流重新加进来。我当时的做法是维护一个 currentStreamType 状态,记录当前是「摄像头」还是「屏幕共享」,切换时做对应的轨道替换。

4.6 综合控制:一个统一的状态管理

在实际项目中,你不会只控制一个轨道。你需要管理多个状态:麦克风开/关、摄像头开/关、前后摄像头、是否在屏幕共享。

我建议用一个对象来统一管理:

const mediaState = {
  audioEnabled: true,
  videoEnabled: true,
  facingMode: 'user',
  screenSharing: false
};

每次用户操作时,更新这个状态,然后调用对应的控制函数。这样代码清晰,也方便调试。

4.7 本章知识体系

下面这张图帮你梳理了本章的核心逻辑:

媒体流控制 麦克风控制 audioTrack.enabled 摄像头控制 videoTrack.enabled 切换摄像头 facingMode 切换 屏幕共享 getDisplayMedia 核心原则:enabled 控制开关,stop 销毁轨道,replace 切换设备 统一状态管理,避免重复获取 getUserMedia

4.8 避坑总结

  • 不要用 stop() 代替 enabled——stop 是销毁,enabled 是静音/黑屏。
  • 切换摄像头要处理 loading——getUserMedia 有延迟,给用户反馈。
  • 屏幕共享必须由用户手势触发——不能自动调用。
  • 屏幕共享流和摄像头流可以共存——但注意轨道替换的逻辑。
  • 统一管理媒体状态——避免多个地方修改同一个流,导致状态混乱。

嗯,这一章的内容就到这里。媒体流控制看起来简单,但实际项目中很容易出问题。记住:enabled 是开关,stop 是销毁,replace 是切换。这三个操作搞清楚了,大部分场景都能应对。


公众号:蓝海资料掘金营,微信deep3321