跳到主要内容

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 用)提示关闭其他视频应用
OverconstrainedErrorconstraints 过严降级再试
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 为空字符串,只能拿到 deviceIdkind

切换设备(推荐方式: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 自动 muteiOS 节能策略监听 mute 事件,恢复时 reload
切换前后置摄像头要重新 getUserMediaiOS 不支持 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. 权威资料