MediaStream与采集
MediaStream 不是 WebRTC 独有,而是整个浏览器媒体世界的"流容器"。WebRTC、<video>、MediaRecorder、Canvas、Web Audio 都用它做交换格式。
1. 三个层次
MediaStream
└── MediaStreamTrack[] (音频或视频轨道)
└── 来自硬件 / 屏幕 / Canvas / Web Audio / 远端
- MediaStream:一组 Track 的容器,仅是引用关系,可被多个 stream 共享。
- MediaStreamTrack:单条音频或视频轨道,是 WebRTC 真正传输的单位。
- 轨道源:摄像头、麦克风、屏幕、
<canvas>.captureStream()、AudioContext.createMediaStreamDestination()、远端 PeerConnection。
2. 采集摄像头/麦克风
const stream = await navigator.mediaDevices.getUserMedia({
audio: {
echoCancellation: true, // 回声消除
noiseSuppression: true, // 降噪
autoGainControl: true, // 自动增益
channelCount: { ideal: 2 },
sampleRate: { ideal: 48000 },
},
video: {
width: { ideal: 1280, max: 1920 },
height: { ideal: 720, max: 1080 },
frameRate: { ideal: 30, max: 60 },
facingMode: { ideal: 'user' }, // 'user' 前置 / 'environment' 后置
deviceId: { exact: cameraId }, // 指定具体设备
},
});
2.1 Constraints 的三种语义
| 语法 | 含义 | 不满足时 |
|---|---|---|
{ ideal: 30 } | 期望值 | 浏览器降级,不报错 |
{ exact: 30 } | 必须等于 | OverconstrainedError |
{ min: 24, max: 60 } | 范围 | 不满足直接拒绝 |
生产建议:写 ideal,不要写 exact。然后用 track.getSettings() 复核真实值。
const track = stream.getVideoTracks()[0];
console.log(track.getSettings());
// { width: 960, height: 720, frameRate: 30, deviceId: '...', ... }
2.2 错误类型
| 错误 | 含义 | 处理 |
|---|---|---|
NotAllowedError | 用户拒绝 | 显示"请到设置开启权限"引导 |
NotFoundError | 没有匹配设备 | 让用户切设备 |
NotReadableError | 设备被占用(被其他 App 用) | 提示关闭其他视频应用 |
OverconstrainedError | constraints 过严 | 降级再试 |
SecurityError | 非 Secure Context | 检查 HTTPS |
3. 采集屏幕
const screen = await navigator.mediaDevices.getDisplayMedia({
video: {
displaySurface: 'monitor', // 'monitor' / 'window' / 'browser'
frameRate: 30,
cursor: 'always', // 'always' / 'motion' / 'never'
},
audio: { // 系统声音(仅 Chrome 主流支持)
echoCancellation: false,
noiseSuppression: false,
},
selfBrowserSurface: 'exclude', // 防止用户选自己(递归画面)
surfaceSwitching: 'include', // 允许切换共享内容
systemAudio: 'include',
});
坑:
- iOS Safari 不支持
getDisplayMedia。 - 要保留点击位置,必须设
cursor: 'always'。 - 系统声音采集 Chrome 必须勾选"分享音频"复选框,不能强制。
4. 设备枚举与切换
const devices = await navigator.mediaDevices.enumerateDevices();
const cameras = devices.filter(d => d.kind === 'videoinput');
const mics = devices.filter(d => d.kind === 'audioinput');
const speakers= devices.filter(d => d.kind === 'audiooutput');
未授权前返回的设备 label 为空字符串,只能拿到 deviceId 和 kind。
切换设备(推荐方式:replaceTrack)
// 不重新协商,无停顿
const newStream = await navigator.mediaDevices.getUserMedia({
video: { deviceId: { exact: newCameraId } },
});
const newTrack = newStream.getVideoTracks()[0];
const sender = pc.getSenders().find(s => s.track?.kind === 'video');
await sender.replaceTrack(newTrack);
// 旧 Track 要主动停掉
oldTrack.stop();
replaceTrack 是 1:1 替换,不会触发重新协商,比 removeTrack + addTrack 平滑得多。
监听设备变化
navigator.mediaDevices.addEventListener('devicechange', async () => {
const devices = await navigator.mediaDevices.enumerateDevices();
// 用户插拔了摄像头/耳机
});
5. Track 控制
| 操作 | 含义 | 触发协商? |
|---|---|---|
track.enabled = false | 静音/黑画面(不停采集) | 否 |
track.stop() | 停止采集,释放硬件 | 否(但 mute 事件触发) |
sender.replaceTrack(newTrack) | 切换源 | 否 |
pc.removeTrack(sender) | 移除整路媒体 | 是 |
enabled = false 仍在传——传的是黑帧或静音帧,带宽不会显著下降。要省带宽必须 replaceTrack(null) 或 transceiver.direction = 'inactive'。
6. 把多个源合成一路(Canvas + Web Audio)
虚拟背景、画中画、混音等场景常用:
// 视频:Canvas 合成
const canvas = document.createElement('canvas');
canvas.width = 1280; canvas.height = 720;
const ctx = canvas.getContext('2d');
function render() {
ctx.drawImage(cameraVideo, 0, 0);
ctx.drawImage(overlayImage, 100, 100);
requestAnimationFrame(render);
}
render();
const videoTrack = canvas.captureStream(30).getVideoTracks()[0];
// 音频:Web Audio 混音
const audioCtx = new AudioContext();
const mic = audioCtx.createMediaStreamSource(micStream);
const music = audioCtx.createMediaStreamSource(musicStream);
const dest = audioCtx.createMediaStreamDestination();
mic.connect(dest);
music.connect(dest);
const audioTrack = dest.stream.getAudioTracks()[0];
const composedStream = new MediaStream([videoTrack, audioTrack]);
坑:Canvas captureStream 在某些浏览器不会自动停止,标签页隐藏后帧率可能掉到 0。
7. 录制:MediaRecorder
WebRTC 之外的能力,但和 MediaStream 配合紧密:
const recorder = new MediaRecorder(stream, {
mimeType: 'video/webm;codecs=vp9,opus',
videoBitsPerSecond: 2_500_000,
});
const chunks = [];
recorder.ondataavailable = (e) => chunks.push(e.data);
recorder.onstop = () => {
const blob = new Blob(chunks, { type: 'video/webm' });
// 上传或下载
};
recorder.start(1000); // 每秒切片
Safari 直到 14.1 才支持 MediaRecorder,且 codec 列表受限。生产录制推荐放服务端做(SFU 旁路出 RTP → ffmpeg 转 MP4)。
8. iOS 与移动端坑
| 问题 | 原因 | 解决 |
|---|---|---|
| 后台被挂起,Track 自动 mute | iOS 节能策略 | 监听 mute 事件,恢复时 reload |
切换前后置摄像头要重新 getUserMedia | iOS 不支持 applyConstraints 切 facingMode | 直接 getUserMedia 新流 |
<video> 不自动播放 | 自动播放策略 | 必须 playsinline + muted |
| 唤醒后画面卡死 | 标签页节流 | 监听 visibilitychange 重启 |
9. 常见检查代码片段
// 1. 检查浏览器是否支持
if (!navigator.mediaDevices?.getUserMedia) {
throw new Error('当前环境不支持 WebRTC 采集');
}
// 2. 探测可用 codec(推流前确认)
const caps = RTCRtpSender.getCapabilities('video');
console.log(caps.codecs);
// 3. 复核真实分辨率
track.addEventListener('settingschange', () =>
console.log(track.getSettings()));
// 4. 监听硬件级 mute(耳机拔出、被其他应用抢占)
track.addEventListener('mute', () => console.log('硬件静音'));
track.addEventListener('unmute', () => console.log('恢复'));
track.addEventListener('ended', () => console.log('硬件停止,需重采')) ;
10. 权威资料
- W3C Media Capture and Streams: https://www.w3.org/TR/mediacapture-streams/
- W3C Screen Capture: https://www.w3.org/TR/screen-capture/
- MDN MediaDevices API: https://developer.mozilla.org/docs/Web/API/MediaDevices
- 核对日期:2026-06-22