7、媒体流控制:停止媒体流(stop()方法)、启用/禁用轨道(enabled属性)、动态切换设备
好,咱们今天聊点实在的。前面几章我们把摄像头和麦克风都打开了,画面也出来了,声音也听到了。但问题来了——你总不能一直开着吧?用户想关掉麦克风、想切换摄像头、想彻底释放资源,这些怎么搞?
这就是本章要解决的三个核心问题:停止媒体流、启用/禁用轨道、动态切换设备。说白了,就是怎么让用户把「水龙头」拧上、拧小、或者换个水龙头。
核心知识点速览
MediaStream.stop()—— 彻底关闭整个流,释放所有资源MediaStreamTrack.enabled—— 临时禁用/启用某个轨道(比如静音麦克风)- 动态切换设备 —— 不重新创建流,直接换摄像头或麦克风
7.1 停止媒体流:stream.stop() 的正确用法
先说说最狠的操作——彻底关闭。当用户挂断电话、退出直播间、或者切换页面时,你得把摄像头和麦克风占用的资源还回去。不然的话,那个摄像头指示灯一直亮着,用户会慌的。
我记得有一次做视频会议项目,测试的时候发现退出房间后,摄像头灯还亮着。用户反馈说「你们是不是在偷看我?」——嗯,这锅我们得背。其实就是忘了调 stop()。
注意: stream.stop() 这个方法在较新的浏览器中已经废弃了。现在推荐的做法是遍历所有轨道,逐个调用 track.stop()。
正确的做法是这样的:
// 停止整个媒体流
function stopMediaStream(stream) {
if (!stream) return;
// 获取所有轨道
const tracks = stream.getTracks();
// 逐个停止
tracks.forEach(track => {
track.stop();
});
// 我个人习惯再加一步:清空引用
stream = null;
}
你想想看,为什么不能直接调 stream.stop()?因为一个 MediaStream 对象可能包含多个轨道(比如音频+视频),而每个轨道是独立管理的。逐个停止,更精细,也更安全。
小技巧: 停止轨道后,最好把 <video> 或 <audio> 元素的 srcObject 也置空,避免残留画面。
videoElement.srcObject = null;
7.2 启用/禁用轨道:enabled 属性的妙用
有时候你并不想彻底关闭,只是想临时「静音」一下。比如开会时你咳嗽一声,想暂时关掉麦克风。这时候用 enabled 属性就对了。
enabled 是 MediaStreamTrack 上的一个布尔属性。设为 false 时,轨道还在,但不再传输数据。说白了,就是「假装关了,其实没关」。
// 获取音频轨道
const audioTrack = stream.getAudioTracks()[0];
// 静音
audioTrack.enabled = false;
// 取消静音
audioTrack.enabled = true;
这里有个坑,我曾经踩过——enabled 和 muted 是两码事。
| 属性 | 含义 | 谁控制的 |
|---|---|---|
enabled |
是否启用轨道(主动控制) | 开发者/用户 |
muted |
是否被系统静音(被动状态) | 系统/硬件 |
举个例子:你把 enabled 设为 false,麦克风图标变红,这是你主动操作的。但如果用户拔掉了耳机,系统会自动把 muted 设为 true,这是被动发生的。两者不冲突,但你要区分清楚。
实战场景: 视频通话中的「关闭摄像头」功能,其实就是把视频轨道的 enabled 设为 false。这时候画面会变黑,但连接还在。对方看到的是一张黑屏,而不是断开连接。
7.3 动态切换设备:不重建流,直接换
这是本章的重头戏。用户想从前置摄像头切换到后置摄像头,或者从耳机麦克风切换到外置麦克风。最笨的办法是:stop() 旧流 → getUserMedia() 新流 → 重新绑定。但这样会闪一下,用户体验很差。
更好的做法是:用 replaceTrack() 方法。它可以在不重建 RTCPeerConnection 的情况下,直接替换轨道。
流程是这样的:
- 调用
navigator.mediaDevices.enumerateDevices()获取所有设备列表 - 用户选择新设备
- 用新设备重新
getUserMedia()获取新轨道 - 用
replaceTrack()替换旧轨道 - 停止旧轨道
// 获取设备列表
async function getDevices() {
const devices = await navigator.mediaDevices.enumerateDevices();
// 只筛选摄像头
const videoDevices = devices.filter(d => d.kind === 'videoinput');
// 生成选项
videoDevices.forEach((device, index) => {
const option = document.createElement('option');
option.value = device.deviceId;
option.text = device.label || `摄像头 ${index + 1}`;
selectElement.appendChild(option);
});
}
// 切换摄像头
async function switchCamera(newDeviceId) {
// 获取新轨道
const newStream = await navigator.mediaDevices.getUserMedia({
video: { deviceId: { exact: newDeviceId } }
});
const newTrack = newStream.getVideoTracks()[0];
// 替换旧轨道
const oldTrack = localStream.getVideoTracks()[0];
if (oldTrack) {
// 如果是 WebRTC 连接,用 peerConnection 的 replaceTrack
// 如果是本地预览,直接替换 video 元素的 srcObject
localStream.removeTrack(oldTrack);
localStream.addTrack(newTrack);
oldTrack.stop();
}
// 更新预览
videoElement.srcObject = localStream;
}
注意: replaceTrack() 是 RTCRtpSender 上的方法,用于替换已发送的轨道。如果是本地预览,直接用 removeTrack() + addTrack() 就行。
嗯,这里要注意一个细节:设备标签(label)在用户授权前是空的。也就是说,如果你没有先调一次 getUserMedia(),enumerateDevices() 返回的设备是没有名字的。我刚开始做的时候就被这个坑过——列表里全是「未知设备」,用户根本不知道选哪个。
解决方案?先调一次 getUserMedia() 获取权限,然后再枚举设备。或者,在用户点击「切换摄像头」按钮时,先请求权限。
我的习惯: 在页面加载时先调一次 getUserMedia({ video: true, audio: true }),然后立即 stop() 掉。这样既拿到了权限,又不会一直占用资源。之后再枚举设备,就能拿到完整的标签了。
7.4 综合实战:一个完整的媒体控制面板
来,我们把上面三个知识点串起来,做一个简单的控制面板。包含:开始/停止、静音/取消静音、切换摄像头。
class MediaController {
constructor() {
this.stream = null;
this.videoElement = document.getElementById('video');
this.audioEnabled = true;
this.videoEnabled = true;
}
// 开始采集
async start() {
this.stream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true
});
this.videoElement.srcObject = this.stream;
}
// 停止采集
stop() {
if (this.stream) {
this.stream.getTracks().forEach(track => track.stop());
this.videoElement.srcObject = null;
this.stream = null;
}
}
// 切换音频
toggleAudio() {
const track = this.stream?.getAudioTracks()[0];
if (track) {
track.enabled = !track.enabled;
this.audioEnabled = track.enabled;
}
}
// 切换视频
toggleVideo() {
const track = this.stream?.getVideoTracks()[0];
if (track) {
track.enabled = !track.enabled;
this.videoEnabled = track.enabled;
}
}
// 切换摄像头
async switchCamera(deviceId) {
const newStream = await navigator.mediaDevices.getUserMedia({
video: { deviceId: { exact: deviceId } }
});
const newTrack = newStream.getVideoTracks()[0];
const oldTrack = this.stream?.getVideoTracks()[0];
if (oldTrack) {
this.stream.removeTrack(oldTrack);
this.stream.addTrack(newTrack);
oldTrack.stop();
}
this.videoElement.srcObject = this.stream;
}
}
这个类封装了所有核心操作。你只需要在 UI 上绑定按钮事件就行。我个人觉得,这种封装方式比到处写散落的 getUserMedia 要清爽得多。
总结一下:
- 彻底关闭用
track.stop(),记得逐个停止所有轨道 - 临时禁用用
track.enabled = false,恢复时设为true - 切换设备用
enumerateDevices()+replaceTrack(),避免重建流 - 别忘了在停止后清空
srcObject,释放 DOM 引用
好了,这一章的内容就到这里。媒体流控制是 WebRTC 开发中非常基础但又容易出错的部分。你只要记住:该关的关,该静的静,该换的换,就不会出大问题。
公众号:蓝海资料掘金营,微信deep3321