4. 获取用户媒体流:深入 getUserMedia API
这一章,我们来啃一块硬骨头——getUserMedia。
说白了,它就是 WebRTC 的“水源”。没有它,摄像头、麦克风的数据根本进不了浏览器。我刚开始接触 WebRTC 时,觉得这 API 不就是调个方法嘛,有啥好研究的?结果真到项目里,权限拒绝、设备没插、格式不支持……各种坑踩了个遍。
今天咱们就把 getUserMedia 从头到尾捋一遍。你跟着我走一遍,以后遇到媒体流相关的问题,心里就有底了。
4.1 核心概念:MediaStream 与 MediaStreamTrack
先搞清楚两个基础概念。
- MediaStream:一个媒体流容器。你可以把它想象成一个“管道”,里面可以同时走视频和音频数据。
- MediaStreamTrack:管道里的“单根线”。一根线只传一种数据——要么是视频轨道(VideoTrack),要么是音频轨道(AudioTrack)。
举个例子:你打开一个视频通话,一个 MediaStream 里通常包含两个 Track——一个摄像头视频轨道,一个麦克风音频轨道。
关键点:一个 MediaStream 可以包含多个 Track,也可以只包含一个。你可以单独控制每个 Track 的启用/停用,甚至动态添加或移除。
4.2 基础调用:navigator.mediaDevices.getUserMedia
这是整个章节的核心方法。调用方式很简单:
navigator.mediaDevices.getUserMedia(constraints)
.then(function(stream) {
// 拿到 stream,可以用了
})
.catch(function(err) {
// 出错了,处理错误
});
嗯,就这么几行。但真正的坑,全在 constraints 参数里。
4.3 constraints 参数详解
constraints 是一个对象,告诉浏览器你想要什么。最基本的写法:
// 同时要摄像头和麦克风
const constraints = {
video: true,
audio: true
};
但实际项目中,你很少会这么写。为什么?因为 true 表示“随便给我一个”,分辨率、帧率、设备类型全交给浏览器决定。我遇到过好几次,用户摄像头明明是 1080p 的,结果浏览器给了一个 640x480 的流——用户体验直接崩了。
所以,我建议你永远指定具体参数:
const constraints = {
video: {
width: { ideal: 1280 },
height: { ideal: 720 },
frameRate: { ideal: 30 }
},
audio: {
sampleRate: { ideal: 48000 },
channelCount: { ideal: 2 }
}
};
这里用 ideal 而不是 exact,意思是“尽量满足,不行就降级”。如果你非要 4K 不可,可以用 exact,但要做好用户设备不支持时直接报错的心理准备。
| 约束模式 | 含义 | 适用场景 |
|---|---|---|
exact |
精确匹配,不满足就报错 | 必须特定分辨率/帧率 |
ideal |
理想值,浏览器尽量满足 | 大多数场景,推荐使用 |
min / max |
范围约束,在范围内取最优 | 保证最低质量要求 |
我的习惯:先调用 enumerateDevices() 获取用户设备列表,再根据设备能力动态生成 constraints。这样用户体验最好,不会出现“明明有 4K 摄像头却只给 720p”的尴尬。
4.4 实战:获取摄像头与麦克风权限
光说不练假把式。咱们直接写一个完整的例子。
async function startMedia() {
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: {
width: { ideal: 1280 },
height: { ideal: 720 },
facingMode: 'user' // 前置摄像头
},
audio: {
echoCancellation: true,
noiseSuppression: true
}
});
// 把流绑定到 video 元素
const videoElement = document.getElementById('localVideo');
videoElement.srcObject = stream;
// 获取轨道信息
const videoTrack = stream.getVideoTracks()[0];
const audioTrack = stream.getAudioTracks()[0];
console.log('视频轨道:', videoTrack.label);
console.log('音频轨道:', audioTrack.label);
return stream;
} catch (err) {
console.error('获取媒体流失败:', err);
handleError(err);
}
}
这里有几个细节要注意:
facingMode: 'user'表示前置摄像头,'environment'是后置。移动端特别有用。echoCancellation和noiseSuppression是音频处理的关键。不开启的话,对方会听到你的回声和背景噪音。stream.getVideoTracks()[0]拿到的是第一个视频轨道。如果用户有多个摄像头,你可以让用户选择。
注意:getUserMedia 必须在 HTTPS 或 localhost 下才能使用。HTTP 环境下会直接报错。我曾经在开发阶段用 HTTP 调试了半天,一直拿不到流,最后才发现是协议问题——嗯,这个坑我替你们踩过了。
4.5 错误处理:用户拒绝权限怎么办?
用户点了“拒绝”按钮,或者浏览器设置里禁用了摄像头/麦克风,getUserMedia 会抛出一个 NotAllowedError。
我的做法是:
function handleError(err) {
if (err.name === 'NotAllowedError') {
alert('请允许使用摄像头和麦克风权限');
// 可以引导用户去浏览器设置里手动开启
} else if (err.name === 'NotFoundError') {
alert('未检测到摄像头或麦克风设备');
} else if (err.name === 'NotReadableError') {
alert('设备被其他应用占用,请关闭其他程序后重试');
} else {
alert('未知错误:' + err.message);
}
}
你想想看,用户拒绝权限后,如果不给任何提示,页面就卡在那里了。用户体验极差。所以一定要做友好的错误提示。
4.6 流处理:停止、切换与克隆
拿到流之后,你还需要知道怎么管理它。
停止轨道
// 停止所有轨道
stream.getTracks().forEach(track => track.stop());
// 或者只停止视频
stream.getVideoTracks().forEach(track => track.stop());
为什么要手动停止?因为浏览器不会自动释放摄像头和麦克风。如果你不停止,摄像头灯会一直亮着——用户会以为你在偷拍他。
切换摄像头
async function switchCamera() {
const stream = await navigator.mediaDevices.getUserMedia({
video: {
facingMode: { exact: 'environment' } // 切换到后置
}
});
// 替换 video 元素的流
videoElement.srcObject = stream;
}
注意:切换时最好先停止旧流,否则摄像头可能被占用。
克隆轨道
const clonedTrack = videoTrack.clone();
// 克隆的轨道可以单独控制,互不影响
克隆轨道在录制、截图等场景中很有用。比如你想同时录制视频和拍照,克隆一份轨道给录制器,原轨道继续显示预览。
4.7 知识体系总览
下面这张图,把 getUserMedia 的核心流程串起来了。你对照着看,思路会更清晰。
4.8 避坑指南
最后,分享几个我踩过的坑:
- 权限弹窗被拦截:有些浏览器会阻止自动弹出权限请求。我建议在用户点击按钮后再调用 getUserMedia,而不是页面加载时自动调用。
- 移动端同时使用前后摄像头:大部分手机不支持同时开启前后摄像头。如果你需要双摄,得用两个不同的 MediaStream。
- 音频自动播放限制:拿到音频流后,如果想直接播放,可能会被浏览器拦截。需要用户先与页面交互一次。
- 设备热插拔:用户可能在通话中拔掉摄像头。监听
devicechange事件,动态处理设备变化。
小技巧:调试时可以在 Chrome 开发者工具的 Application → Media 面板里,手动模拟设备权限和流。省得每次都要插拔摄像头。