5、媒体流获取:getUserMedia详解、摄像头与麦克风选择、分辨率与帧率设置、媒体约束(MediaConstraints)

好,咱们进入正题。这一章要聊的,是WebRTC里最基础、也最核心的一步——拿到用户的摄像头和麦克风。说白了,没有媒体流,后面的一切都是空谈。

我记得刚接触WebRTC那会儿,觉得getUserMedia就是个简单的API,调一下就能出画面。结果真到项目里,踩的坑一个接一个。用户拒绝权限怎么办?设备选错了怎么办?分辨率不对怎么办?嗯,今天咱们就把这些事彻底聊透。

5.1 getUserMedia:一切的开端

getUserMedia是浏览器提供的一个API,用来获取用户的音视频流。它的调用方式很简单,但背后的逻辑其实挺多的。

// 最基本的调用
navigator.mediaDevices.getUserMedia({
  video: true,
  audio: true
})
.then(function(stream) {
  // 拿到流了,可以塞给video标签
  const video = document.getElementById('localVideo');
  video.srcObject = stream;
})
.catch(function(err) {
  console.error('获取媒体流失败:', err);
});

你看,就这么几行代码,就能把摄像头画面显示在页面上。但实际项目中,我从来不会这么写。为什么?因为video: true这种写法太粗糙了,浏览器会按默认参数来,画质、帧率、设备选择全交给浏览器决定——这往往不是我们想要的。

核心要点:getUserMedia返回的是一个Promise,成功时拿到MediaStream对象,失败时抛出错误。错误类型主要有三种:NotAllowedError(用户拒绝)、NotFoundError(没有设备)、NotReadableError(设备被占用)。

5.2 摄像头与麦克风的选择

你有没有遇到过这种情况:用户有前后两个摄像头,但你的应用只能调用默认的那个?我遇到过,而且不止一次。

要解决这个问题,得用到MediaDeviceInfo。浏览器会给你一个设备列表,每个设备都有唯一的deviceId。你可以在getUserMedia里指定用哪个设备。

// 先枚举所有设备
navigator.mediaDevices.enumerateDevices()
.then(function(devices) {
  // 过滤出摄像头
  const videoDevices = devices.filter(d => d.kind === 'videoinput');
  console.log('可用摄像头:', videoDevices);

  // 假设我们想用第二个摄像头
  if (videoDevices.length > 1) {
    return navigator.mediaDevices.getUserMedia({
      video: {
        deviceId: { exact: videoDevices[1].deviceId }
      },
      audio: false
    });
  }
})
.then(function(stream) {
  // 拿到指定摄像头的流
});

这里有个细节:deviceId的值每次页面刷新都可能变,所以不能硬编码。我一般会在用户首次进入时枚举一次,然后存到localStorage里,下次直接读取。

小技巧:麦克风的选择逻辑和摄像头完全一样,只是把kind改成audioinput。另外,enumerateDevices需要在用户授权后才能拿到完整的设备列表,否则只能看到默认设备。

5.3 分辨率与帧率设置

分辨率这事,说起来简单,做起来坑多。你设个1920x1080,浏览器不一定给你这个分辨率。它会找一个最接近的。为什么?因为摄像头硬件支持的分辨率是有限的,不是你想要什么就有什么。

我个人习惯用widthheight的约束来指定分辨率,但要注意写法:

// 推荐写法:指定理想值
navigator.mediaDevices.getUserMedia({
  video: {
    width: { ideal: 1280 },
    height: { ideal: 720 },
    frameRate: { ideal: 30 }
  }
});

// 强制写法:必须精确匹配
navigator.mediaDevices.getUserMedia({
  video: {
    width: { exact: 1920 },
    height: { exact: 1080 }
  }
});

ideal是理想值,浏览器会尽量接近,但不保证完全一致。exact是强制值,如果设备不支持,直接报错。我在项目中一般用ideal,除非有特殊需求——比如做扫码应用,必须用特定分辨率。

帧率也是同理。30fps是标准,但有些低端设备只能跑到15fps。你设个ideal: 30,它跑不到也不会报错,只是画面会卡一点。

约束类型 说明 示例
ideal 理想值,浏览器尽量满足 width: { ideal: 1280 }
exact 精确值,不满足则报错 width: { exact: 1920 }
min 最小值,低于此值报错 width: { min: 640 }
max 最大值,高于此值报错 width: { max: 1920 }

5.4 媒体约束(MediaConstraints)详解

媒体约束是getUserMedia的灵魂。它不只是控制分辨率和帧率,还能控制很多细节。比如:

  • facingMode:控制前置还是后置摄像头。移动端特别有用。
  • echoCancellation:回声消除,语音通话必备。
  • noiseSuppression:降噪,环境嘈杂时很有用。
  • sampleRate:音频采样率,影响音质。
// 一个完整的约束示例
const constraints = {
  audio: {
    echoCancellation: true,
    noiseSuppression: true,
    sampleRate: 48000
  },
  video: {
    width: { ideal: 1280, max: 1920 },
    height: { ideal: 720, max: 1080 },
    frameRate: { ideal: 30, max: 60 },
    facingMode: { ideal: 'user' }  // 前置摄像头
  }
};

navigator.mediaDevices.getUserMedia(constraints)
.then(function(stream) {
  // 成功
})
.catch(function(err) {
  // 处理错误
});
注意:约束不是越多越好。设得太多,浏览器可能找不到完全匹配的组合,导致失败。我建议只设关键参数,其他的让浏览器自己选。另外,facingModeuser是前置,environment是后置,别搞反了。

5.5 避坑指南

我曾经在一个视频会议项目里,遇到用户反馈说摄像头打不开。排查了半天,发现是用户同时开了另一个视频软件,摄像头被占用了。这种情况,getUserMedia会抛NotReadableError。解决方案是提示用户关闭其他占用摄像头的应用。

还有一次,用户说画面是黑的。我一看代码,发现他用了exact: 3840x2160,但他的摄像头最高只支持1080p。浏览器直接报错,连降级的机会都没有。从那以后,我基本只用ideal,除非有100%的把握。

另外,移动端有个常见问题:用户拒绝权限后,再次调用getUserMedia会直接失败,不会弹出权限请求。你需要引导用户去系统设置里手动开启权限。这个体验很糟糕,但没办法,浏览器的安全策略就是这样。

5.6 知识体系总览

下面这张图,把本章的核心知识点串起来了。你可以看到,从getUserMedia出发,到设备选择、分辨率帧率、约束设置,再到错误处理,每一步都有讲究。

getUserMedia 设备选择 分辨率与帧率 媒体约束 enumerateDevices 枚举 deviceId 精确指定 facingMode 前后置 ideal / exact / min / max width / height 组合 frameRate 帧率控制 echoCancellation noiseSuppression sampleRate 采样率 错误处理:NotAllowedError / NotFoundError / NotReadableError

嗯,这一章的内容就这些。getUserMedia看似简单,但用好它,需要你对设备、约束、错误处理都有清晰的认识。下次写代码的时候,记得多想想:用户用的是什么设备?会不会拒绝权限?分辨率设得合不合理?把这些想清楚了,你的应用才能在各种环境下稳定运行。

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