4. 媒体流获取:getUserMedia API详解、摄像头与麦克风控制、屏幕共享实现
好,咱们今天聊点实在的。WebRTC 最核心的能力是什么?说白了,就是能把你的摄像头、麦克风,甚至整个屏幕的画面,实时传给对方。而这一切的起点,就是 getUserMedia 这个 API。
我记得刚接触 WebRTC 那会儿,觉得这玩意儿挺神奇的——浏览器里调个 API,摄像头就亮了。后来踩的坑多了,才发现里面门道不少。今天我就把这几年的经验掰开揉碎,跟你聊聊。
4.1 getUserMedia 基础用法
getUserMedia 是 navigator.mediaDevices 下的一个方法。它的任务很简单:向用户请求媒体权限,然后返回一个 MediaStream 对象。
来看个最基础的例子:
async function startCamera() {
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true
});
const video = document.getElementById('localVideo');
video.srcObject = stream;
} catch (err) {
console.error('获取媒体流失败:', err);
}
}
这段代码跑起来,浏览器就会弹出权限请求。用户点了「允许」,摄像头和麦克风就开始工作了。
getUserMedia 放在一个 async 函数里,用 try/catch 包着。因为用户拒绝权限、设备不存在、或者浏览器不支持,都会抛出异常。不处理的话,页面直接白屏,用户体验很差。
4.2 约束条件:精细控制摄像头与麦克风
光开摄像头还不够,很多时候我们需要精细控制。比如指定用前置还是后置摄像头,设置分辨率,或者调整帧率。这些通过 constraints 参数来实现。
4.2.1 视频约束
const constraints = {
video: {
width: { ideal: 1280 },
height: { ideal: 720 },
frameRate: { ideal: 30 },
facingMode: 'user' // 'user' 前置,'environment' 后置
},
audio: true
};
这里有个细节:ideal 表示理想值,浏览器会尽量满足,但不保证。如果你非要某个分辨率不可,可以用 exact:
video: {
width: { exact: 1920 },
height: { exact: 1080 }
}
exact 要小心。如果设备不支持你指定的分辨率,getUserMedia 会直接抛 OverconstrainedError。我曾经在一个项目里踩过这个坑——用户的老笔记本摄像头只支持 640x480,我硬要 1080p,结果直接崩溃。后来改成 ideal 就没事了。
4.2.2 音频约束
音频的约束相对简单,但有几个参数值得关注:
| 参数 | 说明 | 示例 |
|---|---|---|
sampleRate |
采样率,单位 Hz | { ideal: 48000 } |
channelCount |
声道数,1 单声道,2 立体声 | { ideal: 2 } |
echoCancellation |
回声消除,默认 true | { ideal: true } |
noiseSuppression |
降噪,默认 true | { ideal: true } |
嗯,这里要注意:回声消除和降噪虽然默认开启,但不同浏览器的实现质量差别很大。我在做远程医疗项目时,Chrome 的降噪效果就比 Safari 好不少。如果你对音质要求高,建议在用户端做个测试。
4.3 设备枚举与切换
很多时候,用户有多个摄像头或麦克风。比如笔记本自带一个,外接一个。这时候就需要枚举设备,让用户自己选。
async function getDevices() {
const devices = await navigator.mediaDevices.enumerateDevices();
const videoInputs = devices.filter(d => d.kind === 'videoinput');
const audioInputs = devices.filter(d => d.kind === 'audioinput');
videoInputs.forEach((device, index) => {
console.log(`摄像头 ${index + 1}: ${device.label}`);
});
}
拿到设备列表后,切换设备其实很简单——重新调一次 getUserMedia,把 deviceId 传进去就行:
const constraints = {
video: {
deviceId: { exact: selectedDeviceId }
},
audio: true
};
4.4 屏幕共享实现
屏幕共享是 WebRTC 的另一个杀手级功能。和摄像头不同,它用的是 getDisplayMedia 方法。
async function startScreenShare() {
try {
const stream = await navigator.mediaDevices.getDisplayMedia({
video: {
cursor: 'always' // 显示鼠标指针
},
audio: false // 屏幕共享一般不传音频
});
const video = document.getElementById('screenVideo');
video.srcObject = stream;
} catch (err) {
console.error('屏幕共享失败:', err);
}
}
调用 getDisplayMedia 后,浏览器会弹出一个选择窗口,让用户选择共享整个屏幕、某个窗口,还是某个浏览器标签页。这个选择权完全在用户手里,开发者无法控制。
ended 事件。当用户点击「停止共享」按钮时,这个事件会触发。你一定要监听它,及时清理资源。
const track = stream.getVideoTracks()[0];
track.addEventListener('ended', () => {
// 用户停止了屏幕共享
stopScreenShare();
});
4.5 知识体系总览
说了这么多,我画了张图帮你理清思路。这张图把媒体流获取的核心流程串起来了:
4.6 常见问题与避坑指南
最后,我把自己这些年踩过的坑整理一下,你遇到了可以直接翻出来看。
- 权限被拒: 用户点了「禁止」后,
getUserMedia会抛NotAllowedError。这时候别傻等,给用户一个友好的提示,引导他去浏览器设置里开启权限。 - 设备不存在: 比如你指定了
facingMode: 'environment',但设备只有前置摄像头。这时候会抛NotFoundError。我的做法是先枚举设备,再动态生成约束。 - iOS Safari 的特殊性: 在 iOS 上,
getUserMedia必须在用户手势(比如点击按钮)的回调里调用。而且enumerateDevices在未授权时返回空数组。我曾经被这个坑了一整个下午。 - 屏幕共享的音频:
getDisplayMedia的audio参数虽然可以传true,但实际支持很差。Chrome 只支持共享标签页时采集音频,窗口和整个屏幕都不行。所以我的建议是:屏幕共享默认不传音频,如果需要,单独用getUserMedia采集麦克风。
getUserMedia 和 getDisplayMedia,理解约束条件,学会设备枚举与切换,你就能应对绝大多数音视频采集场景。
好了,这一章的内容就到这里。代码不多,但都是实战中反复验证过的。你写的时候如果遇到问题,随时翻回来看看。