3、媒体流获取:getUserMedia API详解、音视频约束配置、屏幕共享与录制

好,咱们今天聊点实在的。WebRTC 的第一步是什么?不是信令,不是 ICE,而是——拿到媒体流。说白了,你得先有摄像头和麦克风的数据,才能谈后面的传输。这个入口就是 getUserMedia

我在项目中见过太多人一上来就调 API,结果要么黑屏,要么没声音,要么浏览器直接报错。嗯,这章咱们就把这个坎儿彻底踩平。

3.1 getUserMedia 基础调用

先看最基础的用法。你只需要传一个约束对象,它返回一个 Promise。

navigator.mediaDevices.getUserMedia({ video: true, audio: true })
  .then(stream => {
    // 拿到 stream,可以赋值给 video 标签
    videoElement.srcObject = stream;
  })
  .catch(err => {
    console.error('获取媒体流失败:', err);
  });

就这么简单?对,但坑也在这里。你想想看,如果用户拒绝授权,或者设备不存在,Promise 就会 reject。我曾经在一个项目中忘了处理 catch,结果用户点了「拒绝」之后整个页面卡死——嗯,那场面挺尴尬的。

核心要点:
  • getUserMedia 必须在 HTTPS 或 localhost 下调用
  • 用户必须主动触发(比如点击按钮),不能页面加载就自动弹窗
  • 返回的 MediaStream 包含多个 MediaStreamTrack

3.2 音视频约束配置详解

约束配置才是真正体现功力的地方。很多人只传 { video: true },但实际项目中你需要精确控制分辨率、帧率、码率,甚至指定使用前置还是后置摄像头。

3.2.1 视频约束

const constraints = {
  video: {
    width: { ideal: 1280 },
    height: { ideal: 720 },
    frameRate: { ideal: 30, min: 15, max: 60 },
    facingMode: 'user',  // 'user' 前置, 'environment' 后置
    aspectRatio: 16 / 9
  },
  audio: {
    sampleRate: { ideal: 48000 },
    channelCount: { ideal: 2 },
    echoCancellation: true,
    noiseSuppression: true
  }
};

这里有个细节:ideal 是理想值,浏览器会尽量满足但不保证;minmax 是硬约束,不满足就直接 reject。我建议你只用 ideal,除非你有非常特殊的需求。

我的习惯: 先调用 enumerateDevices() 获取设备列表,再根据用户选择的设备动态生成约束。这样用户体验会好很多。

3.2.2 音频约束

属性 说明 推荐值
sampleRate 采样率,单位 Hz ideal: 48000
channelCount 声道数 ideal: 2(立体声)
echoCancellation 回声消除 true
noiseSuppression 降噪 true
autoGainControl 自动增益 true

为什么回声消除默认要开?因为不开的话,对方听到的全是回声。我在做视频会议项目时踩过这个坑,测试的时候自己跟自己说话没感觉,一联调才发现问题。

3.3 屏幕共享

屏幕共享用的是 getDisplayMedia,不是 getUserMedia。这个 API 会弹出一个系统级的窗口,让用户选择共享整个屏幕、某个窗口还是某个浏览器标签页。

async function startScreenShare() {
  try {
    const stream = await navigator.mediaDevices.getDisplayMedia({
      video: {
        cursor: 'always'  // 显示鼠标指针
      },
      audio: {
        echoCancellation: true,
        noiseSuppression: true
      }
    });
    // 处理 stream
  } catch (err) {
    if (err.name === 'NotAllowedError') {
      console.log('用户取消了屏幕共享');
    }
  }
}
注意: 屏幕共享的音频支持有限。Chrome 只支持共享标签页时的音频,不支持共享整个屏幕或窗口时的音频。我曾经被这个坑过,测试了半天发现没声音,最后查文档才明白。

3.4 媒体录制

拿到流之后,录制就顺理成章了。用 MediaRecorder 可以把流录制成视频文件。

const mediaRecorder = new MediaRecorder(stream, {
  mimeType: 'video/webm;codecs=vp9'
});

let chunks = [];
mediaRecorder.ondataavailable = event => {
  if (event.data.size > 0) {
    chunks.push(event.data);
  }
};

mediaRecorder.onstop = () => {
  const blob = new Blob(chunks, { type: 'video/webm' });
  const url = URL.createObjectURL(blob);
  // 可以下载或预览
};

mediaRecorder.start(1000); // 每秒触发一次 ondataavailable

这里有个小技巧:start() 的参数控制数据回调的频率。设得越小,内存占用越大,但录制更实时。我一般设 1000ms,平衡一下。

支持的编码格式:
  • video/webm;codecs=vp8
  • video/webm;codecs=vp9
  • video/webm;codecs=h264(部分浏览器支持)
  • audio/webm;codecs=opus

3.5 知识体系总览

下面这张图把本章的核心逻辑串起来了。你可以看到,从设备获取到最终录制,每一步都有对应的 API 和约束。

媒体流获取核心流程 摄像头 / 麦克风 / 屏幕 getUserMedia / getDisplayMedia 约束配置 width / height / frameRate / facingMode / echoCancellation / noiseSuppression MediaStream → 预览 / 录制 / 传输 核心参数

3.6 避坑指南

最后,分享几个我踩过的坑,希望能帮你省点时间。

我曾经...
  • 在 iOS Safari 上调 facingMode: 'user' 发现没效果——后来才知道 iOS 需要先调用 enumerateDevices 获取设备 ID,再用 deviceId 精确指定。
  • 录制时没检查 mimeType 是否支持,结果录出来的文件打不开。建议先调 MediaRecorder.isTypeSupported() 做兼容。
  • 屏幕共享时忘了处理用户取消的情况,导致 Promise 一直 pending。记得加 catch,判断 NotAllowedError

嗯,这一章的内容就到这里。媒体流获取是整个 WebRTC 的基石,把这块吃透了,后面的信令、传输、渲染都会顺畅很多。

公众号:蓝海资料掘金营,微信deep3321