4、浏览器端快速入门:getUserMedia API详解、MediaStream与MediaStreamTrack、浏览器兼容性处理、第一个视频通话Demo
好,咱们正式开始动手了。
前面几章聊了WebRTC的架构、信令、网络穿透这些底层逻辑。说实话,光看理论容易犯困。这一章咱们直接打开浏览器,把摄像头和麦克风调出来,看看真正的媒体流长什么样。
你想想看,WebRTC最核心的能力是什么?就是让浏览器能直接采集、处理和传输音视频。而这一切的起点,就是 getUserMedia 这个API。我当年第一次调通它的时候,看到自己在屏幕上动起来,那种感觉——嗯,挺奇妙的。
本章核心脉络:从采集设备权限 → 拿到媒体流对象 → 操作音视频轨道 → 处理浏览器差异 → 最终跑通一个点对点Demo。每一步我都会把实际项目中踩过的坑指给你看。
4.1 getUserMedia:打开媒体设备的第一把钥匙
navigator.mediaDevices.getUserMedia() 这个API,说白了就是向用户请求摄像头和麦克风权限的入口。它返回一个Promise,resolve之后给你一个 MediaStream 对象。
最基本的调用方式是这样的:
// 最简单的调用
async function startCamera() {
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true
});
// 把流绑定到 video 元素上
const video = document.getElementById('localVideo');
video.srcObject = stream;
} catch (err) {
console.error('获取媒体设备失败:', err);
}
}
嗯,这里要注意几个点。第一,getUserMedia 必须在用户手势(比如点击按钮)之后调用,否则浏览器会直接拒绝。我见过不少新手在页面加载时直接调用,结果Promise一直pending——说白了就是被浏览器拦截了。
第二,video: true 这种写法虽然简单,但实际项目中我很少这么用。为什么?因为浏览器会给你一个默认的分辨率,可能是320x240,也可能是1280x720,完全看浏览器心情。你想想看,如果做视频会议,320x240的画质能看吗?
我的习惯:永远显式指定分辨率约束。哪怕只是做个Demo,也把理想值写清楚,这样行为可预测。
4.2 约束(Constraints)的精细控制
getUserMedia 的约束参数其实很强大。它支持三种粒度:exact(精确值)、ideal(理想值)、min/max(范围)。
我给大家看一个我在项目中常用的配置:
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: { ideal: 'user' } // 前置摄像头
},
audio: {
echoCancellation: { exact: true },
noiseSuppression: { ideal: true },
sampleRate: { ideal: 48000 }
}
};
const stream = await navigator.mediaDevices.getUserMedia(constraints);
这里我解释一下为什么这么写:
- width/height:给一个理想值,浏览器会尽量满足。如果设备不支持,它会降级到范围内的最低值。我曾经在老旧笔记本上遇到过摄像头只支持640x480的情况,加了
min约束后至少保证画面不糊。 - facingMode:移动端特别重要。
user是前置摄像头,environment是后置。我踩过坑——在iOS Safari上如果不指定,默认可能给你后置摄像头,用户一脸懵。 - echoCancellation:回声消除。这个我建议用
exact: true,因为如果设备不支持,你至少知道问题出在哪。否则开了回声消除但没效果,排查起来很头疼。
注意:exact 约束是强制的。如果设备不满足,Promise会reject。所以除非你确定设备支持,否则优先用 ideal。
4.3 MediaStream 与 MediaStreamTrack
拿到 MediaStream 之后,你会发现它其实是一个容器。里面装的是什么呢?是一个或多个 MediaStreamTrack 对象。
我习惯把 MediaStream 比作一个「音视频通道」,而 MediaStreamTrack 就是通道里的「独立车道」——一个车道跑视频,一个车道跑音频。你可以单独控制每个车道。
常用的操作:
// 获取所有轨道
const tracks = stream.getTracks();
// 获取视频轨道
const videoTracks = stream.getVideoTracks();
// 获取音频轨道
const audioTracks = stream.getAudioTracks();
// 停止某个轨道(比如关掉摄像头)
videoTracks[0].stop();
// 启用/禁用轨道(静音麦克风但不释放资源)
audioTracks[0].enabled = false;
// 克隆轨道(用于多路输出)
const clonedTrack = videoTracks[0].clone();
这里有个容易混淆的点:stop() 和 enabled = false 的区别。我刚开始做WebRTC时也搞混过——stop() 会彻底释放设备资源,之后这个轨道就不能再用了;而 enabled = false 只是暂时静音或黑屏,设备还在占用着。所以如果你只是临时关一下麦克风,用 enabled 就好;如果确定不再需要摄像头了,记得调 stop() 释放资源。
4.4 浏览器兼容性处理
说到兼容性,嗯,这里我得好好聊聊。WebRTC的API虽然已经标准化了,但不同浏览器的实现细节还是有差异的。我整理了一个表格,方便大家对照:
| 浏览器 | getUserMedia 前缀 | 注意事项 |
|---|---|---|
| Chrome 53+ | 标准 navigator.mediaDevices.getUserMedia |
需 HTTPS 或 localhost;支持大部分约束 |
| Firefox 36+ | 标准 API | 对 facingMode 支持较好;部分旧版本需 moz 前缀 |
| Safari 11+ | 标准 API | iOS 上必须用户手势触发;不支持 exact 约束的某些值 |
| Edge (Chromium) | 标准 API | 与 Chrome 行为基本一致 |
| 旧版浏览器 | navigator.getUserMedia(带前缀) |
建议使用 adapter.js 库统一处理 |
我给大家一个兼容性处理的代码模板,这是我项目里一直在用的:
// 兼容性处理函数
function getMediaStream(constraints) {
// 优先使用标准 API
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
return navigator.mediaDevices.getUserMedia(constraints);
}
// 降级到旧版 API(带浏览器前缀)
const getUserMedia = navigator.getUserMedia
|| navigator.webkitGetUserMedia
|| navigator.mozGetUserMedia
|| navigator.msGetUserMedia;
if (!getUserMedia) {
return Promise.reject(new Error('当前浏览器不支持 getUserMedia'));
}
// 将旧版回调转为 Promise
return new Promise((resolve, reject) => {
getUserMedia.call(navigator, constraints, resolve, reject);
});
}
我的建议:虽然现在主流浏览器都支持标准API了,但如果你要兼容老旧设备(比如学校机房里的旧电脑),这个降级处理还是很有必要的。另外,adapter.js 这个库可以帮你省掉很多兼容性代码,我一般都会引入它。
4.5 第一个视频通话Demo
好了,理论说完了,咱们来写一个真正的Demo。这个Demo会打开本地摄像头,然后把画面显示在页面上。虽然只是单机版,但你已经能看到媒体流的全貌了。
HTML部分:
<!DOCTYPE html>
<html>
<head>
<title>我的第一个视频通话Demo</title>
<style>
body { font-family: Arial, sans-serif; padding: 20px; background: #f0f8ff; }
video { width: 640px; height: 480px; border: 2px solid #3498db; border-radius: 8px; }
button { padding: 10px 20px; margin: 10px; background: #3498db; color: white; border: none; border-radius: 4px; cursor: pointer; }
button:hover { background: #2980b9; }
.status { margin: 10px 0; padding: 10px; background: #fff3cd; border-radius: 4px; }
</style>
</head>
<body>
<h2>本地摄像头预览</h2>
<video id="localVideo" autoplay playsinline></video>
<div>
<button id="startBtn">打开摄像头</button>
<button id="stopBtn">关闭摄像头</button>
</div>
<div id="status" class="status">点击按钮开始</div>
<script src="script.js"></script>
</body>
</html>
JavaScript部分(script.js):
const video = document.getElementById('localVideo');
const startBtn = document.getElementById('startBtn');
const stopBtn = document.getElementById('stopBtn');
const statusDiv = document.getElementById('status');
let localStream = null;
// 打开摄像头
async function startCamera() {
try {
const constraints = {
video: {
width: { ideal: 1280 },
height: { ideal: 720 },
frameRate: { ideal: 30 }
},
audio: {
echoCancellation: true,
noiseSuppression: true
}
};
localStream = await navigator.mediaDevices.getUserMedia(constraints);
video.srcObject = localStream;
statusDiv.textContent = '✅ 摄像头已打开';
statusDiv.style.background = '#d4edda';
} catch (err) {
statusDiv.textContent = '❌ 打开失败: ' + err.message;
statusDiv.style.background = '#f8d7da';
console.error('getUserMedia error:', err);
}
}
// 关闭摄像头
function stopCamera() {
if (localStream) {
// 停止所有轨道
localStream.getTracks().forEach(track => track.stop());
video.srcObject = null;
localStream = null;
statusDiv.textContent = '🛑 摄像头已关闭';
statusDiv.style.background = '#fff3cd';
}
}
// 事件绑定
startBtn.addEventListener('click', startCamera);
stopBtn.addEventListener('click', stopCamera);
这个Demo虽然简单,但包含了几个关键点:
- autoplay playsinline:这两个属性必须加。iOS Safari上不加
playsinline的话,视频会全屏播放,用户体验很差。 - 轨道释放:关闭摄像头时一定要调
track.stop(),否则摄像头指示灯一直亮着,用户会以为被偷拍了。 - 错误处理:用户拒绝权限、设备不存在、约束不满足——这些情况都要处理。我见过线上产品因为没处理
NotAllowedError导致页面白屏的。
我曾经踩过的坑:有一次在Chrome上测试,发现关闭摄像头后再打开,画面卡住了。排查了半天,发现是 srcObject 没有置为null,导致旧流和新流冲突。所以关闭时一定要 video.srcObject = null。
4.6 小结:从采集到预览,你已迈出第一步
这一章我们走通了浏览器端媒体流处理的完整链路:从 getUserMedia 的权限请求,到约束的精细控制,再到 MediaStream 和 MediaStreamTrack 的操作,最后用兼容性代码跑通了一个Demo。
说实话,能走到这一步,你已经掌握了WebRTC最基础也最核心的能力——采集媒体流。后面的信令、网络传输、编解码,都是建立在这个基础之上的。
嗯,我建议你把Demo跑起来,然后试着改改约束参数,看看不同分辨率、帧率下的效果。动手试过的东西,印象才深。