12、媒体流获取与处理:getUserMedia API、屏幕共享(getDisplayMedia)、媒体约束(Constraints)
各位同学,今天我们来聊聊WebRTC里最基础、也最“接地气”的一环——怎么把摄像头、麦克风、屏幕这些硬件设备“请”到浏览器里来。
说白了,没有媒体流,WebRTC就是个空壳子。你没法跟人视频,也没法共享PPT。我当年刚接触WebRTC时,以为这步很简单,不就是调个API嘛。结果第一次上线就翻车了——用户反馈“为什么我点了允许,还是黑屏?”嗯,这里面的坑,咱们今天一个一个填上。
12.1 getUserMedia:打开摄像头和麦克风
getUserMedia 是 WebRTC 媒体获取的入口。它的作用就一句话:向用户请求音视频设备的访问权限,并返回一个 MediaStream 对象。
来看一个最基础的例子:
async function startCamera() {
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true
});
const videoElement = document.getElementById('localVideo');
videoElement.srcObject = stream;
} catch (err) {
console.error('获取媒体失败:', err);
}
}
这段代码看起来简单,但我得提醒你几个关键点:
- 必须通过 HTTPS 或 localhost——浏览器出于安全考虑,非安全上下文下
getUserMedia会直接拒绝。我在项目中遇到过测试同事在 HTTP 环境下调试,折腾了半天才发现是协议问题。 - 用户授权是异步的——浏览器会弹出权限请求框,用户点击“允许”或“阻止”后才能继续。如果用户点了阻止,
catch里会收到NotAllowedError。 - 返回的 stream 是实时的——你可以随时停止某个轨道,比如关掉麦克风:
stream.getAudioTracks()[0].stop()。
个人习惯:我通常会在调用
getUserMedia 之前先检查 navigator.mediaDevices 是否存在,避免在不支持的浏览器上直接报错。加一句 if (!navigator.mediaDevices) { alert('您的浏览器不支持媒体设备'); } 能省去很多排查时间。
12.2 getDisplayMedia:屏幕共享
屏幕共享是远程会议、在线教育的核心功能。WebRTC 提供了 getDisplayMedia 来实现它。
它的用法和 getUserMedia 很像,但有几个重要区别:
async function startScreenShare() {
try {
const stream = await navigator.mediaDevices.getDisplayMedia({
video: true,
audio: false // 屏幕共享通常不包含系统音频
});
const videoElement = document.getElementById('screenVideo');
videoElement.srcObject = stream;
} catch (err) {
console.error('屏幕共享失败:', err);
}
}
这里要注意:
- 用户必须手动选择共享内容——浏览器会弹出一个选择器,让用户选择共享整个屏幕、某个窗口还是某个浏览器标签页。你不能跳过这一步。
- 音频默认不包含——如果你想同时共享系统音频(比如播放的视频声音),需要设置
audio: true,但浏览器支持有限。我曾经踩过这个坑:用户共享了一个带声音的视频,结果对方听不到任何音频,排查了半天才发现是 Chrome 当时还不支持系统音频采集。 - 用户可以通过系统UI停止共享——浏览器会在地址栏附近显示一个“停止共享”按钮,用户点击后
stream.getVideoTracks()[0].onended事件会触发。你需要监听这个事件,及时清理 UI 状态。
我曾经犯过的错:在屏幕共享时没有监听
onended 事件,结果用户手动停止共享后,远程端还显示着最后一帧画面。用户以为还在共享,其实早就断了。后来我加了一段代码:stream.getVideoTracks()[0].onended = () => { /* 清理UI,通知远端 */ }; 问题解决。
12.3 媒体约束(Constraints):精细控制
你想想看,如果每次打开摄像头都是默认的 640x480 分辨率,那在 4K 显示器上得多模糊?媒体约束就是用来解决这个问题的。
约束分为三类:
| 类型 | 说明 | 示例 |
|---|---|---|
| 基本约束 | 指定一个确切的值或范围 | width: 1280 |
| 理想约束 | 浏览器尽量满足,但不强制 | width: { ideal: 1920 } |
| 范围约束 | 指定最小/最大/精确值 | width: { min: 640, max: 1920 } |
来看一个实际例子:
const constraints = {
video: {
width: { min: 640, ideal: 1280, max: 1920 },
height: { min: 480, ideal: 720, max: 1080 },
frameRate: { ideal: 30, max: 60 },
facingMode: 'user' // 前置摄像头
},
audio: {
echoCancellation: true,
noiseSuppression: true,
sampleRate: 48000
}
};
const stream = await navigator.mediaDevices.getUserMedia(constraints);
这里有几个要点:
facingMode控制前后摄像头:user是前置,environment是后置。移动端开发时这个属性很常用。echoCancellation和noiseSuppression默认就是true,但如果你在做专业音频处理,可能需要关闭它们,自己实现算法。- 约束不是强制的——浏览器会尽量满足你的要求,但如果设备不支持,它会自动降级。比如你要求 4K 分辨率,但摄像头只支持 1080p,浏览器会返回 1080p 的流,不会报错。
避坑指南:我曾经在项目中要求
width: { exact: 1920 },结果部分老旧设备直接抛错,因为 exact 是强制匹配,设备不支持就失败。后来我改成了 ideal,兼容性好了很多。所以,除非你真的必须用某个精确值,否则尽量用 ideal 或范围约束。
12.4 知识体系总览
下面这张图帮你理清本章的核心逻辑:
这张图展示了整个媒体获取的脉络:从 navigator.mediaDevices 出发,根据场景选择 getUserMedia 或 getDisplayMedia,同时通过 Constraints 精细控制采集参数,最终得到一个 MediaStream 对象供后续使用。
12.5 实际项目中的经验总结
最后,分享几个我在项目中积累的小经验:
- 先检测设备列表——调用
enumerateDevices()获取可用设备,再决定显示哪些选项给用户。比如没有后置摄像头的设备,就别显示“切换后置”按钮。 - 处理权限被拒的情况——用户拒绝授权后,不要只是弹个错误。我习惯在 UI 上显示一个引导提示,告诉用户如何手动开启权限(比如点击地址栏左侧的锁图标)。
- 动态调整约束——网络状况不好时,可以降低分辨率或帧率。比如监听
RTCPeerConnection的oniceconnectionstatechange,在连接不稳定时调用stream.getVideoTracks()[0].applyConstraints({ width: 640, height: 480 })。 - 别忘了释放资源——页面关闭或通话结束时,记得调用
stream.getTracks().forEach(track => track.stop()),否则摄像头指示灯会一直亮着,用户会以为你在偷拍他。
一个小技巧:调试时可以在浏览器控制台输入
navigator.mediaDevices.getUserMedia({ video: true }) 快速测试摄像头是否正常工作。如果返回 Promise 被拒绝,看看错误信息是 NotAllowedError 还是 NotFoundError,能帮你快速定位问题。
好了,媒体获取这部分就讲到这里。记住一句话:约束是手段,流是结果,用户体验才是目的。下一章我们会聊怎么把这些流通过信令和网络传出去,那才是 WebRTC 真正精彩的地方。