3、媒体流获取:getUserMedia API详解、摄像头与麦克风控制、屏幕共享、媒体约束与适配。
各位同学,欢迎来到第三讲。
今天我们来聊聊整个WebRTC应用的起点——媒体流获取。说白了,就是怎么让浏览器拿到你的摄像头画面、麦克风声音,甚至是整个屏幕。
这部分我当年刚接触时踩过不少坑。比如有一次,用户反馈说“为什么我点了允许,但画面还是黑的?”——嗯,后来发现是约束条件写得太死,摄像头不支持那个分辨率。所以今天我会把getUserMedia的细节、摄像头麦克风的控制、屏幕共享,以及媒体约束的适配,一次性讲透。
核心知识点速览
- getUserMedia 的调用方式与 Promise 链
- MediaStream 对象的结构
- 摄像头/麦克风的开关与切换
- 屏幕共享的两种实现路径
- 媒体约束的精细控制与设备适配
3.1 getUserMedia 的基本调用
先看最基础的用法。你只需要传入一个MediaStreamConstraints对象,它就会返回一个 Promise,resolve 后给你一个 MediaStream。
// 最简单的调用:同时请求摄像头和麦克风
navigator.mediaDevices.getUserMedia({
video: true,
audio: true
})
.then(function(stream) {
// 把 stream 赋值给 video 元素
const video = document.getElementById('localVideo');
video.srcObject = stream;
})
.catch(function(err) {
console.error('获取媒体流失败:', err.name, err.message);
});
这里有个细节:video: true 表示“随便给我一个摄像头”。但实际项目中,我建议你永远不要只写 true。为什么?因为不同设备的默认摄像头可能不是你想要的——比如笔记本的物理摄像头开关被关闭,或者用户插了外接摄像头。我曾经在一个远程医疗项目里,就因为没指定摄像头,导致医生端一直显示的是红外摄像头,画面一片惨绿……
💡 个人习惯:我每次都会至少指定一个理想分辨率,比如 video: { width: { ideal: 1280 }, height: { ideal: 720 } }。这样浏览器会优先选择能支持该分辨率的摄像头,而不是随便给一个最低配的。
3.2 摄像头与麦克风的精细控制
拿到 MediaStream 之后,你可以通过 getVideoTracks() 和 getAudioTracks() 拿到具体的轨道对象。每个轨道都有 enabled 属性和 stop() 方法。
// 假设 stream 已经获取成功
const videoTrack = stream.getVideoTracks()[0];
const audioTrack = stream.getAudioTracks()[0];
// 关闭摄像头(但保留轨道对象)
videoTrack.enabled = false;
// 彻底停止摄像头(释放资源)
videoTrack.stop();
// 静音麦克风
audioTrack.enabled = false;
嗯,这里要注意:enabled = false 只是“软关闭”,摄像头指示灯可能还亮着。如果你想彻底关掉摄像头,必须调用 stop()。我记得有一次做在线教育,学生端只关了 enabled,结果摄像头灯一直亮着,家长以为被监控了……后来我改成 stop() 才解决。
切换摄像头
多摄像头设备越来越常见。切换的核心思路是:先枚举所有设备,然后重新调用 getUserMedia 并指定 deviceId。
// 枚举所有视频输入设备
navigator.mediaDevices.enumerateDevices()
.then(function(devices) {
const videoDevices = devices.filter(d => d.kind === 'videoinput');
// 假设用户选择了第二个摄像头
const targetDeviceId = videoDevices[1].deviceId;
// 重新获取流
return navigator.mediaDevices.getUserMedia({
video: { deviceId: { exact: targetDeviceId } },
audio: false
});
})
.then(function(newStream) {
// 替换本地视频
localVideo.srcObject = newStream;
// 别忘了停止旧流中的轨道
// oldStream.getTracks().forEach(t => t.stop());
});
⚠️ 避坑指南:我曾经在切换摄像头时忘记停止旧流,结果浏览器同时占用了两个摄像头,导致第二个摄像头无法启动。记住:每次切换前,先 stop 掉所有旧轨道。
3.3 屏幕共享:getDisplayMedia
屏幕共享和摄像头获取是两套 API。屏幕共享使用 navigator.mediaDevices.getDisplayMedia(),它会弹出一个选择窗口,让用户选择共享整个屏幕、某个窗口或某个浏览器标签页。
// 发起屏幕共享
navigator.mediaDevices.getDisplayMedia({
video: {
cursor: 'always' // 始终显示鼠标指针
},
audio: false // 屏幕共享默认不带音频,除非你单独捕获
})
.then(function(screenStream) {
// 把屏幕流显示在远程端
remoteVideo.srcObject = screenStream;
// 注意:屏幕共享的轨道停止后,需要做清理
screenStream.getVideoTracks()[0].addEventListener('ended', () => {
console.log('用户停止了屏幕共享');
// 切换回摄像头画面
});
})
.catch(function(err) {
if (err.name === 'NotAllowedError') {
console.log('用户取消了屏幕共享');
}
});
这里有个关键点:屏幕共享流不能和摄像头流直接混合。如果你想同时显示摄像头和屏幕,需要创建两个独立的 RTCPeerConnection,或者使用 MediaStream 的 addTrack 方法合并。我个人习惯是单独发送屏幕流,这样接收端可以自由切换布局。
屏幕共享 vs 摄像头:核心区别
| 特性 | getUserMedia | getDisplayMedia |
|---|---|---|
| 用户交互 | 浏览器权限弹窗 | 系统级选择器(窗口/标签页/屏幕) |
| 音频支持 | 支持麦克风 | 不支持系统音频(需单独捕获) |
| 分辨率 | 由约束控制 | 由屏幕本身决定 |
| 停止事件 | 手动 stop() | 用户可主动停止,触发 ended 事件 |
3.4 媒体约束与设备适配
媒体约束是 getUserMedia 的灵魂。它分为三种类型:
- 精确值 (exact):必须完全匹配,否则 Promise 会 reject。
- 理想值 (ideal):浏览器会尽量满足,但如果不满足,会降级到最接近的值。
- 范围值 (min / max):设定一个区间,浏览器在区间内选择最优值。
// 一个比较完善的约束示例
const constraints = {
video: {
width: { min: 640, ideal: 1280, max: 1920 },
height: { min: 480, ideal: 720, max: 1080 },
frameRate: { ideal: 30, max: 60 },
facingMode: { ideal: 'user' }, // 优先前置摄像头
deviceId: undefined // 不指定,让浏览器自动选择
},
audio: {
echoCancellation: { exact: true }, // 必须开启回声消除
noiseSuppression: { ideal: true },
sampleRate: { ideal: 48000 }
}
};
你想想看,为什么要有 ideal 和 exact 的区别?因为不同设备的摄像头能力差异太大了。我做过一个跨平台会议应用,在 MacBook Pro 上 width: 1920 没问题,但换到一台老款 ThinkPad 上就直接报错。后来我全部改成 ideal,再也没出过问题。
🎯 适配策略:我建议先调用 enumerateDevices() 获取设备列表,然后根据设备能力动态生成约束。比如检测到只有 720p 摄像头,就不要强行要求 1080p。
常见约束组合速查表
| 场景 | 推荐约束 |
|---|---|
| 高清视频通话 | video: { width: { ideal: 1280 }, height: { ideal: 720 }, frameRate: { ideal: 30 } } |
| 低带宽环境 | video: { width: { ideal: 640 }, height: { ideal: 480 }, frameRate: { ideal: 15 } } |
| 仅音频通话 | audio: { echoCancellation: true, noiseSuppression: true }, video: false |
| 屏幕共享+标注 | video: { cursor: 'always' }, audio: false |
3.5 错误处理与降级
媒体获取失败的原因五花八门:用户拒绝权限、设备被占用、不支持指定的分辨率……我建议你永远不要只写一个 catch,而是针对不同错误做不同处理。
navigator.mediaDevices.getUserMedia(constraints)
.then(handleStream)
.catch(function(err) {
if (err.name === 'NotAllowedError') {
// 用户拒绝了权限,引导用户手动开启
showPermissionGuide();
} else if (err.name === 'NotFoundError') {
// 没有找到摄像头/麦克风
fallbackToAudioOnly();
} else if (err.name === 'NotReadableError') {
// 设备被其他应用占用
alert('请关闭其他正在使用摄像头的应用');
} else {
// 其他未知错误
console.error('getUserMedia error:', err);
}
});
嗯,这里有个小技巧:如果摄像头获取失败,可以尝试降级到仅音频,或者提示用户检查物理开关。我曾经在 Chrome 上遇到过 NotReadableError,原因是用户同时打开了 Zoom 和我们的网页应用。后来我们在错误提示里加了一句“请关闭其他视频会议软件”,反馈好了很多。
总结一下本章核心:
- getUserMedia 是媒体流的入口,永远不要只传
true,要指定约束。 - 摄像头和麦克风通过
enabled和stop()控制,切换设备前务必释放旧资源。 - 屏幕共享用
getDisplayMedia,注意监听ended事件。 - 媒体约束用
ideal代替exact可以提高兼容性。 - 错误处理要区分类型,给用户明确的引导。
好了,这一章的内容就到这里。媒体流是整个 WebRTC 的基石,把它搞扎实了,后面的信令、传输、渲染都会顺畅很多。下一章我们会深入信令服务器的设计与实现——不过那是后话了。