3、浏览器端音视频采集:MediaStream API详解、摄像头/麦克风控制、屏幕共享实现、分辨率与帧率调优
好,咱们今天来聊聊浏览器端最基础、也最核心的一环——音视频采集。你想想看,不管你是做视频会议、直播连麦,还是搞AI人脸识别,第一步都是要把摄像头和麦克风的数据拿过来。这一步要是没做好,后面再牛的AI算法也是白搭。
我个人习惯把浏览器端的采集能力分成三块:设备控制、屏幕共享、参数调优。这三块搞明白了,你就能像搭积木一样,灵活组合出各种采集方案。
核心知识点速览
- MediaStream API 的核心对象与生命周期
- 摄像头/麦克风的枚举、切换与开关控制
- 屏幕共享的两种实现方式与权限处理
- 分辨率、帧率、码率的平衡艺术
3.1 MediaStream API 核心概念
MediaStream 说白了就是一个容器。它里面装着若干个 MediaStreamTrack,每个 track 代表一路独立的音视频数据。比如你打开摄像头,会得到一个视频 track;打开麦克风,得到一个音频 track。这两个 track 组合在一起,就是一个完整的 MediaStream。
我记得刚接触 WebRTC 那会儿,总搞不清 stream 和 track 的关系。后来在做一个多摄像头切换的项目时,才彻底明白——stream 是逻辑上的"通道",track 才是真正的数据源。你可以把多个 track 塞进一个 stream,也可以把一个 track 从 stream 里拆出来单独处理。
小技巧: 用 stream.getTracks() 可以拿到所有 track,用 stream.getVideoTracks() 只拿视频。调试时我习惯先打印一下 track 的 label 属性,看看当前用的是哪个设备。
3.2 摄像头与麦克风控制
控制摄像头和麦克风,核心就两个 API:navigator.mediaDevices.enumerateDevices() 和 navigator.mediaDevices.getUserMedia()。前者用来枚举设备,后者用来启动采集。
先看枚举设备。你可能会问:"为什么非要枚举?直接 getUserMedia 不就行了?" 嗯,这里要注意——用户可能插了多个摄像头。比如笔记本自带一个,外接一个高清摄像头。如果你不枚举,系统可能默认选了个低分辨率的,画质惨不忍睹。
// 枚举所有音视频设备
async function listDevices() {
const devices = await navigator.mediaDevices.enumerateDevices();
devices.forEach(device => {
console.log(`${device.kind}: ${device.label} [${device.deviceId}]`);
});
}
// 指定设备启动采集
async function startCamera(deviceId) {
const constraints = {
video: { deviceId: { exact: deviceId } },
audio: true
};
const stream = await navigator.mediaDevices.getUserMedia(constraints);
return stream;
}
我在项目中遇到过一个问题:用户第一次访问页面时,enumerateDevices 返回的 label 全是空字符串。为什么?因为浏览器为了保护隐私,在用户未授权前不暴露设备名称。解决办法是先调一次 getUserMedia 获取权限,再枚举就能拿到完整信息了。
避坑指南: 我曾经在切换摄像头时直接调了两次 getUserMedia,结果旧摄像头没关,新摄像头又开了,浏览器直接卡死。正确的做法是:先调用 track.stop() 停止旧 track,再启动新的。或者用 stream.getTracks().forEach(t => t.stop()) 一次性清理。
3.3 屏幕共享实现
屏幕共享用的是 getDisplayMedia(),不是 getUserMedia。这个 API 会弹出一个系统级的选择窗口,让用户选择共享整个屏幕、某个窗口还是某个浏览器标签页。
说白了,getDisplayMedia 返回的也是一个 MediaStream,但它的视频 track 有一些特殊属性。比如你可以通过 track.getSettings() 拿到 displaySurface 字段,判断用户共享的是屏幕还是窗口。
async function startScreenShare() {
try {
const stream = await navigator.mediaDevices.getDisplayMedia({
video: {
cursor: "always" // 始终显示鼠标指针
},
audio: {
echoCancellation: true,
noiseSuppression: true
}
});
return stream;
} catch (err) {
console.error("用户取消了屏幕共享:", err);
}
}
这里有个坑:屏幕共享的音频。Chrome 在 2022 年之后支持了屏幕共享时采集系统音频(比如播放的视频声音),但需要用户在弹窗里手动勾选"分享音频"。你代码里写了 audio: true 只是申请,最终决定权在用户手里。
重要提醒: 屏幕共享的 track 有一个 ended 事件。当用户点击浏览器工具栏的"停止共享"按钮时,这个事件会触发。一定要监听它,及时清理 UI 状态。
screenTrack.onended = () => {
console.log("用户停止了屏幕共享");
// 清理视频元素、释放资源
videoElement.srcObject = null;
};
3.4 分辨率与帧率调优
分辨率、帧率、码率,这三者是音视频领域的"不可能三角"。你想想看,想要高分辨率高帧率,码率就得往上飙;想要低码率保流畅,就得牺牲分辨率或帧率。
我个人习惯把调优分成两步:先定上限,再动态调整。
定上限就是在 getUserMedia 时设置理想值:
const constraints = {
video: {
width: { ideal: 1280, max: 1920 },
height: { ideal: 720, max: 1080 },
frameRate: { ideal: 30, max: 60 }
}
};
注意这里用的是 ideal 和 max,不是 exact。为什么?因为浏览器会根据当前硬件能力自动选择最接近 ideal 的值。你如果写 exact: 1920,万一摄像头不支持,直接报错。
动态调整用的是 track.applyConstraints()。比如在弱网环境下,你可以动态降低分辨率:
async function downgradeResolution(track) {
await track.applyConstraints({
width: { ideal: 640 },
height: { ideal: 480 },
frameRate: { ideal: 15 }
});
}
我在做远程医疗项目时,遇到过一个问题:摄像头支持 4K 30fps,但用户电脑性能不够,画面卡成 PPT。后来我加了一个自动检测机制——如果连续 5 秒帧率低于 10fps,就自动降一档分辨率。效果立竿见影。
调优建议:
- 视频会议: 720p 30fps 足够,码率控制在 1-2Mbps
- 屏幕共享(代码/文档): 1080p 15fps 即可,码率可以低一些
- AI 人脸识别: 480p 15fps 就够,分辨率太高反而增加处理延迟
- 直播推流: 根据上行带宽动态调整,建议从 720p 30fps 起步
3.5 综合实战:一个可切换的采集器
最后,我把今天讲的东西串起来,写一个简单的采集器。它支持切换摄像头、开关麦克风、动态调整分辨率。
class MediaCapture {
constructor() {
this.stream = null;
this.videoTrack = null;
this.audioTrack = null;
}
async start(videoDeviceId, audioDeviceId) {
// 先停止旧的
this.stop();
const constraints = {
video: {
deviceId: videoDeviceId ? { exact: videoDeviceId } : undefined,
width: { ideal: 1280 },
height: { ideal: 720 },
frameRate: { ideal: 30 }
},
audio: {
deviceId: audioDeviceId ? { exact: audioDeviceId } : undefined,
echoCancellation: true
}
};
this.stream = await navigator.mediaDevices.getUserMedia(constraints);
this.videoTrack = this.stream.getVideoTracks()[0];
this.audioTrack = this.stream.getAudioTracks()[0];
return this.stream;
}
stop() {
if (this.stream) {
this.stream.getTracks().forEach(t => t.stop());
this.stream = null;
this.videoTrack = null;
this.audioTrack = null;
}
}
async switchCamera(deviceId) {
if (!this.videoTrack) return;
await this.videoTrack.applyConstraints({
deviceId: { exact: deviceId }
});
}
toggleMic() {
if (this.audioTrack) {
this.audioTrack.enabled = !this.audioTrack.enabled;
}
}
async setResolution(width, height) {
if (!this.videoTrack) return;
await this.videoTrack.applyConstraints({
width: { exact: width },
height: { exact: height }
});
}
}
这个类封装了常见的采集操作。你可以在项目里直接复用,也可以根据需求扩展。比如加上码率控制、自动重连、设备热插拔监听等。
好了,关于浏览器端音视频采集,核心就是这些。记住一句话:MediaStream 是入口,track 是灵魂,constraints 是武器。把这三点吃透,后面学 WebRTC 的传输、处理、AI 融合,都会轻松很多。
公众号:蓝海资料掘金营,微信deep3321