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 切换前后摄像头
移动端经常需要切换前后摄像头。这个操作稍微复杂一点,因为你需要重新获取一个新的视频流。
基本思路是:
- 停止当前视频轨道
- 用新的
facingMode重新调用getUserMedia - 把新轨道替换到原有流中
代码示例:
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);
// 更新
removeTrack 和 addTrack 来操作。
我个人习惯在切换时加一个 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 本章知识体系
下面这张图帮你梳理了本章的核心逻辑:
4.8 避坑总结
- 不要用 stop() 代替 enabled——stop 是销毁,enabled 是静音/黑屏。
- 切换摄像头要处理 loading——getUserMedia 有延迟,给用户反馈。
- 屏幕共享必须由用户手势触发——不能自动调用。
- 屏幕共享流和摄像头流可以共存——但注意轨道替换的逻辑。
- 统一管理媒体状态——避免多个地方修改同一个流,导致状态混乱。
嗯,这一章的内容就到这里。媒体流控制看起来简单,但实际项目中很容易出问题。记住:enabled 是开关,stop 是销毁,replace 是切换。这三个操作搞清楚了,大部分场景都能应对。