3. 音视频采集基础:getUserMedia API详解、摄像头与麦克风的权限管理、音视频约束参数配置、媒体流(MediaStream)对象
好,咱们今天聊点实在的。做WebRTC开发,第一步是什么?不是信令,不是网络穿透,而是——把摄像头和麦克风的数据拿到手。这一步搞不定,后面全是空中楼阁。
我刚开始接触WebRTC时,以为音视频采集就是调个API完事。结果呢?在Chrome上跑得好好的,换到Safari直接黑屏。后来才发现是权限策略和约束参数没配对。嗯,这些坑我今天都给你指出来。
核心一句话:getUserMedia 是浏览器访问音视频设备的唯一入口。它返回一个 MediaStream 对象,里面装着音频轨和视频轨。
3.1 getUserMedia API 详解
先看最基础的调用方式:
navigator.mediaDevices.getUserMedia({
video: true,
audio: true
})
.then(function(stream) {
// 拿到媒体流,可以赋值给 video 标签
const videoElement = document.getElementById('localVideo');
videoElement.srcObject = stream;
})
.catch(function(err) {
console.error('采集失败:', err);
});
这段代码看起来简单,但有几个细节我得提醒你:
- navigator.mediaDevices 是浏览器提供的媒体设备接口,getUserMedia 是它的核心方法。
- 参数是一个对象,video 和 audio 分别控制是否采集视频和音频。
- 返回的是一个 Promise,成功时拿到 MediaStream,失败时抛出错误。
我个人习惯把 getUserMedia 的调用封装成一个函数,方便复用。比如这样:
async function startCapture(constraints) {
try {
const stream = await navigator.mediaDevices.getUserMedia(constraints);
return stream;
} catch (error) {
console.error('采集失败:', error.name, error.message);
throw error;
}
}
为什么要用 async/await?因为 Promise 链多了之后,错误处理容易乱。async/await 配合 try/catch,代码更清晰。
小技巧:在调用 getUserMedia 之前,可以先检查浏览器是否支持:
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
alert('当前浏览器不支持音视频采集');
}
3.2 摄像头与麦克风的权限管理
权限管理是音视频采集中最容易出问题的环节。为什么?因为浏览器对权限的控制越来越严格。
我在项目中遇到过这样的情况:用户第一次拒绝授权后,再次调用 getUserMedia 会直接失败,而且不会弹出权限提示框。用户以为浏览器坏了,其实是权限被永久拒绝了。
权限状态分为三种:
| 状态 | 说明 | 处理方式 |
|---|---|---|
| granted | 已授权 | 正常调用 getUserMedia |
| denied | 已拒绝 | 提示用户手动开启权限 |
| prompt | 未决定 | 调用 getUserMedia 会弹出提示框 |
你可以用 navigator.permissions.query() 来检查权限状态:
async function checkCameraPermission() {
try {
const result = await navigator.permissions.query({ name: 'camera' });
console.log('摄像头权限状态:', result.state);
if (result.state === 'denied') {
alert('摄像头权限已被拒绝,请在浏览器设置中开启');
}
// 监听权限状态变化
result.onchange = function() {
console.log('权限状态变为:', result.state);
};
} catch (error) {
console.error('权限检查失败:', error);
}
}
注意:permissions.query 方法在部分浏览器中不支持。而且,即使用户授权了,如果页面不是 HTTPS 环境,getUserMedia 也会失败。这是浏览器的安全策略,没得商量。
我曾经踩过一个坑:在本地开发时用 http://localhost 没问题,但部署到测试服务器后,因为没配 HTTPS,采集直接报错。所以,开发时就用 HTTPS,别给自己找麻烦。
3.3 音视频约束参数配置
约束参数是 getUserMedia 的精髓。说白了,就是告诉浏览器:我要什么样的视频和音频。
先看一个完整的约束示例:
const constraints = {
video: {
width: { ideal: 1280, min: 640, max: 1920 },
height: { ideal: 720, min: 480, max: 1080 },
frameRate: { ideal: 30, min: 15, max: 60 },
facingMode: 'user', // 'user' 前置,'environment' 后置
aspectRatio: 16 / 9
},
audio: {
echoCancellation: true,
noiseSuppression: true,
sampleRate: 48000,
channelCount: 2
}
};
约束参数分两种:
- 强制约束(exact):浏览器必须满足,否则报错。比如
width: { exact: 1920 }。 - 理想约束(ideal):浏览器尽量满足,但不保证。比如
width: { ideal: 1280 }。
我个人建议:尽量用 ideal,别用 exact。为什么?因为不同设备的摄像头能力不一样。你用 exact 指定 4K,用户的笔记本摄像头不支持,直接就报错了。用 ideal,浏览器会自动选一个最接近的分辨率。
facingMode 这个参数很有意思:
| 值 | 含义 | 适用场景 |
|---|---|---|
| user | 前置摄像头 | 视频通话、自拍 |
| environment | 后置摄像头 | 扫码、拍摄实物 |
| left | 前置偏左 | 部分设备支持 |
| right | 前置偏右 | 部分设备支持 |
音频约束里,echoCancellation 和 noiseSuppression 是必开的。你想想看,如果不开回声消除,对方说话的声音从你的扬声器出来,又被麦克风收进去,再传回去,那不就是无限循环吗?
调试技巧:用 MediaDevices.getSupportedConstraints() 查看当前浏览器支持哪些约束:
const supported = navigator.mediaDevices.getSupportedConstraints();
console.table(supported);
3.4 媒体流(MediaStream)对象
getUserMedia 返回的 MediaStream 对象,是音视频数据的容器。它里面可以包含多个轨道(MediaStreamTrack),每个轨道对应一路音频或视频。
来看 MediaStream 的常用操作:
// 获取所有轨道
const tracks = stream.getTracks();
console.log('轨道数量:', tracks.length);
// 获取视频轨道
const videoTracks = stream.getVideoTracks();
console.log('视频轨道:', videoTracks[0].label);
// 获取音频轨道
const audioTracks = stream.getAudioTracks();
console.log('音频轨道:', audioTracks[0].label);
// 停止所有轨道(释放摄像头和麦克风)
stream.getTracks().forEach(track => track.stop());
// 或者只停止视频轨道
videoTracks[0].stop();
这里有个关键点:记得释放资源。当用户挂断通话或离开页面时,如果不停止轨道,摄像头指示灯会一直亮着。用户会怎么想?"这个网页在偷拍我!"
MediaStreamTrack 对象还有一些有用的属性:
| 属性 | 说明 | 示例值 |
|---|---|---|
| kind | 轨道类型 | 'video' 或 'audio' |
| label | 设备名称 | 'FaceTime HD Camera' |
| enabled | 是否启用 | true / false |
| muted | 是否静音 | true / false |
| readyState | 轨道状态 | 'live' 或 'ended' |
你可以通过修改 enabled 来控制轨道的开关,而不需要重新采集:
// 静音麦克风(不采集音频数据,但保留轨道)
audioTrack.enabled = false;
// 关闭摄像头(停止采集视频数据)
videoTrack.enabled = false;
这和 stop() 的区别在于:enabled 可以随时再打开,stop() 之后轨道就结束了,需要重新调用 getUserMedia。
3.5 知识体系总览
下面这张图把本章的核心逻辑串起来了:
这张图展示了从用户操作到拿到 MediaStream 的完整链路。每一步都可能出问题,所以一定要做好错误处理。
3.6 完整示例:采集并预览本地视频
最后,给你一个可以直接运行的示例。把摄像头画面显示在页面上:
<!DOCTYPE html>
<html>
<head>
<title>音视频采集示例</title>
</head>
<body>
<video id="localVideo" autoplay playsinline muted></video>
<button id="startBtn">开始采集</button>
<button id="stopBtn">停止采集</button>
<script>
let localStream = null;
document.getElementById('startBtn').onclick = async function() {
try {
const constraints = {
video: {
width: { ideal: 1280 },
height: { ideal: 720 },
facingMode: 'user'
},
audio: {
echoCancellation: true,
noiseSuppression: true
}
};
localStream = await navigator.mediaDevices.getUserMedia(constraints);
const video = document.getElementById('localVideo');
video.srcObject = localStream;
console.log('采集成功,视频轨道:', localStream.getVideoTracks()[0].label);
} catch (error) {
console.error('采集失败:', error.name, error.message);
alert('采集失败: ' + error.message);
}
};
document.getElementById('stopBtn').onclick = function() {
if (localStream) {
localStream.getTracks().forEach(track => track.stop());
document.getElementById('localVideo').srcObject = null;
localStream = null;
console.log('已停止采集');
}
};
</script>
</body>
</html>
重要提醒:video 标签一定要加 autoplay playsinline muted。autoplay 让视频自动播放,playsinline 在 iOS 上避免全屏播放,muted 是自动播放的前提条件(浏览器策略)。
好了,音视频采集的基础就这些。记住:权限管理要提前检查,约束参数用 ideal 别用 exact,用完资源记得释放。把这些搞定了,后面信令和网络传输才能玩得转。
公众号:蓝海资料掘金营,微信deep3321