4. 媒体流获取:getUserMedia API详解、摄像头与麦克风控制、屏幕共享实现

好,咱们今天聊点实在的。WebRTC 最核心的能力是什么?说白了,就是能把你的摄像头、麦克风,甚至整个屏幕的画面,实时传给对方。而这一切的起点,就是 getUserMedia 这个 API。

我记得刚接触 WebRTC 那会儿,觉得这玩意儿挺神奇的——浏览器里调个 API,摄像头就亮了。后来踩的坑多了,才发现里面门道不少。今天我就把这几年的经验掰开揉碎,跟你聊聊。

4.1 getUserMedia 基础用法

getUserMedianavigator.mediaDevices 下的一个方法。它的任务很简单:向用户请求媒体权限,然后返回一个 MediaStream 对象。

来看个最基础的例子:

async function startCamera() {
  try {
    const stream = await navigator.mediaDevices.getUserMedia({
      video: true,
      audio: true
    });
    const video = document.getElementById('localVideo');
    video.srcObject = stream;
  } catch (err) {
    console.error('获取媒体流失败:', err);
  }
}

这段代码跑起来,浏览器就会弹出权限请求。用户点了「允许」,摄像头和麦克风就开始工作了。

小提示: 我个人习惯把 getUserMedia 放在一个 async 函数里,用 try/catch 包着。因为用户拒绝权限、设备不存在、或者浏览器不支持,都会抛出异常。不处理的话,页面直接白屏,用户体验很差。

4.2 约束条件:精细控制摄像头与麦克风

光开摄像头还不够,很多时候我们需要精细控制。比如指定用前置还是后置摄像头,设置分辨率,或者调整帧率。这些通过 constraints 参数来实现。

4.2.1 视频约束

const constraints = {
  video: {
    width: { ideal: 1280 },
    height: { ideal: 720 },
    frameRate: { ideal: 30 },
    facingMode: 'user'  // 'user' 前置,'environment' 后置
  },
  audio: true
};

这里有个细节:ideal 表示理想值,浏览器会尽量满足,但不保证。如果你非要某个分辨率不可,可以用 exact

video: {
  width: { exact: 1920 },
  height: { exact: 1080 }
}
注意:exact 要小心。如果设备不支持你指定的分辨率,getUserMedia 会直接抛 OverconstrainedError。我曾经在一个项目里踩过这个坑——用户的老笔记本摄像头只支持 640x480,我硬要 1080p,结果直接崩溃。后来改成 ideal 就没事了。

4.2.2 音频约束

音频的约束相对简单,但有几个参数值得关注:

参数 说明 示例
sampleRate 采样率,单位 Hz { ideal: 48000 }
channelCount 声道数,1 单声道,2 立体声 { ideal: 2 }
echoCancellation 回声消除,默认 true { ideal: true }
noiseSuppression 降噪,默认 true { ideal: true }

嗯,这里要注意:回声消除和降噪虽然默认开启,但不同浏览器的实现质量差别很大。我在做远程医疗项目时,Chrome 的降噪效果就比 Safari 好不少。如果你对音质要求高,建议在用户端做个测试。

4.3 设备枚举与切换

很多时候,用户有多个摄像头或麦克风。比如笔记本自带一个,外接一个。这时候就需要枚举设备,让用户自己选。

async function getDevices() {
  const devices = await navigator.mediaDevices.enumerateDevices();
  const videoInputs = devices.filter(d => d.kind === 'videoinput');
  const audioInputs = devices.filter(d => d.kind === 'audioinput');
  
  videoInputs.forEach((device, index) => {
    console.log(`摄像头 ${index + 1}: ${device.label}`);
  });
}

拿到设备列表后,切换设备其实很简单——重新调一次 getUserMedia,把 deviceId 传进去就行:

const constraints = {
  video: {
    deviceId: { exact: selectedDeviceId }
  },
  audio: true
};
核心要点: 切换设备时,记得先停止旧的媒体流,释放资源。不然摄像头指示灯一直亮着,用户会困惑。

4.4 屏幕共享实现

屏幕共享是 WebRTC 的另一个杀手级功能。和摄像头不同,它用的是 getDisplayMedia 方法。

async function startScreenShare() {
  try {
    const stream = await navigator.mediaDevices.getDisplayMedia({
      video: {
        cursor: 'always'  // 显示鼠标指针
      },
      audio: false  // 屏幕共享一般不传音频
    });
    const video = document.getElementById('screenVideo');
    video.srcObject = stream;
  } catch (err) {
    console.error('屏幕共享失败:', err);
  }
}

调用 getDisplayMedia 后,浏览器会弹出一个选择窗口,让用户选择共享整个屏幕、某个窗口,还是某个浏览器标签页。这个选择权完全在用户手里,开发者无法控制。

重要提醒: 屏幕共享流里有一个 ended 事件。当用户点击「停止共享」按钮时,这个事件会触发。你一定要监听它,及时清理资源。
const track = stream.getVideoTracks()[0];
track.addEventListener('ended', () => {
  // 用户停止了屏幕共享
  stopScreenShare();
});

4.5 知识体系总览

说了这么多,我画了张图帮你理清思路。这张图把媒体流获取的核心流程串起来了:

媒体流获取核心流程 navigator.mediaDevices getUserMedia() getDisplayMedia() 约束条件 constraints 选择窗口/屏幕/标签页 MediaStream 对象 MediaStream 对象 设备枚举 → 约束配置 → 获取流 → 绑定到 video 元素

4.6 常见问题与避坑指南

最后,我把自己这些年踩过的坑整理一下,你遇到了可以直接翻出来看。

  • 权限被拒: 用户点了「禁止」后,getUserMedia 会抛 NotAllowedError。这时候别傻等,给用户一个友好的提示,引导他去浏览器设置里开启权限。
  • 设备不存在: 比如你指定了 facingMode: 'environment',但设备只有前置摄像头。这时候会抛 NotFoundError。我的做法是先枚举设备,再动态生成约束。
  • iOS Safari 的特殊性: 在 iOS 上,getUserMedia 必须在用户手势(比如点击按钮)的回调里调用。而且 enumerateDevices 在未授权时返回空数组。我曾经被这个坑了一整个下午。
  • 屏幕共享的音频: getDisplayMediaaudio 参数虽然可以传 true,但实际支持很差。Chrome 只支持共享标签页时采集音频,窗口和整个屏幕都不行。所以我的建议是:屏幕共享默认不传音频,如果需要,单独用 getUserMedia 采集麦克风。
一句话总结: 媒体流获取是 WebRTC 的基石。掌握 getUserMediagetDisplayMedia,理解约束条件,学会设备枚举与切换,你就能应对绝大多数音视频采集场景。

好了,这一章的内容就到这里。代码不多,但都是实战中反复验证过的。你写的时候如果遇到问题,随时翻回来看看。

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