3、获取媒体流:getUserMedia API详解、摄像头与麦克风权限、媒体约束配置、屏幕共享基础

好,咱们今天来聊聊 WebRTC 里最基础、也最关键的一步——获取媒体流。说白了,就是怎么把用户的摄像头和麦克风给“请”到浏览器里来。你想想看,没有音视频数据,那还谈什么实时通信?

我个人习惯把 getUserMedia 看作是整个 WebRTC 应用的“水龙头”。你拧开它,数据就流进来了。但怎么拧、拧多大、拧哪个方向,这里头门道不少。我在项目中遇到过不少同学,一上来就调 API,结果要么是权限弹窗没处理好,要么是分辨率不对,画面糊成一团。嗯,咱们今天就把它彻底讲透。

3.1 getUserMedia 的前世今生

getUserMedia 这个 API,最早是 Chrome 和 Firefox 在 2013 年左右开始支持的。那时候还叫 navigator.getUserMedia,写法很别扭,还得加前缀。现在好了,标准化的 navigator.mediaDevices.getUserMedia 已经成了主流。

它的核心作用就一个:请求用户授权,然后返回一个 MediaStream 对象。这个对象里,可能包含视频轨道(摄像头画面)、音频轨道(麦克风声音),也可能两者都有。

核心概念: MediaStream 是一个容器,里面可以装多个 MediaStreamTrack。每个 Track 代表一路独立的媒体数据。

3.2 基础用法:三步走

用起来其实很简单,就三步:

  1. 检查浏览器支持——别一上来就调,万一用户用的是老古董呢?
  2. 调用 getUserMedia——传入约束配置,拿到 Promise。
  3. 处理结果——成功就把流挂到 video 元素上,失败就提示用户。

来看代码:

// 第一步:检查支持
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
  console.error('浏览器不支持 getUserMedia');
  return;
}

// 第二步:调用 API
async function startCamera() {
  try {
    const stream = await navigator.mediaDevices.getUserMedia({
      video: true,
      audio: true
    });
    // 第三步:挂到 video 标签上
    const video = document.getElementById('localVideo');
    video.srcObject = stream;
  } catch (err) {
    console.error('获取媒体流失败:', err);
  }
}

你看,就这么几行。但这里有个坑——video: true 这种写法,其实是让浏览器自己选默认摄像头和默认分辨率。我曾经在一个项目里这么写,结果用户那边画面只有 320x240,糊得不行。所以,咱们得学会配置约束。

3.3 媒体约束配置:精细控制

媒体约束(MediaConstraints)是 getUserMedia 的灵魂。你可以指定分辨率、帧率、摄像头方向、音频采样率等等。说白了,就是告诉浏览器:“我要什么样的流,你给我按这个标准来。”

约束分两种:

  • 强制约束(mandatory):用 minmaxexact 关键字。如果达不到,Promise 直接 reject。
  • 理想约束(ideal):用 ideal 关键字。浏览器会尽量满足,但达不到也不会报错。

来看一个实际配置:

const constraints = {
  video: {
    width: { min: 640, ideal: 1280, max: 1920 },
    height: { min: 480, ideal: 720, max: 1080 },
    frameRate: { ideal: 30, max: 60 },
    facingMode: { ideal: 'user' }  // 'user' 前置,'environment' 后置
  },
  audio: {
    echoCancellation: { exact: true },
    noiseSuppression: true,
    sampleRate: { ideal: 48000 }
  }
};

这里我解释一下:

  • widthheight 我设了范围,浏览器会选一个最接近 1280x720 的。如果设备只支持 640x480,那也没问题。
  • facingModeideal 而不是 exact,这样如果设备没有前置摄像头,它会自动切到后置,不会报错。
  • 音频这边,echoCancellation 我用了 exact,因为回声消除对通话质量太重要了。如果设备不支持,那这个通话体验会很差,不如直接报错让用户换设备。
我的经验: 在移动端,facingModeuserenvironment 是必配的。否则默认可能是后置摄像头,用户自拍时一脸懵。

3.4 摄像头与麦克风权限

权限这块,是很多新手翻车的地方。浏览器为了安全,要求 getUserMedia 必须在 HTTPSlocalhost 下才能调用。你想想看,如果随便一个 HTTP 网站都能偷开你的摄像头,那还得了?

权限弹窗的时机:

  • 第一次调用 getUserMedia 时,浏览器会弹出权限请求。
  • 用户可以选择“允许”或“阻止”。
  • 如果用户之前选过“记住”,那下次就不会再弹了。

我曾经遇到一个坑:用户点了“阻止”之后,后续所有 getUserMedia 调用都会直接 reject,而且不会再次弹窗。用户还以为网站坏了。所以,一定要做好错误处理:

try {
  const stream = await navigator.mediaDevices.getUserMedia(constraints);
} catch (err) {
  if (err.name === 'NotAllowedError') {
    // 用户拒绝了权限
    showMessage('请允许摄像头和麦克风权限');
  } else if (err.name === 'NotFoundError') {
    // 没有找到摄像头或麦克风
    showMessage('未检测到摄像头或麦克风');
  } else if (err.name === 'NotReadableError') {
    // 设备被其他应用占用
    showMessage('摄像头或麦克风被其他应用占用');
  }
}
注意: 在 iOS Safari 上,即使用户之前允许过,每次页面刷新后都需要重新触发 getUserMedia 调用。而且,必须由用户手势(点击按钮)触发,不能自动调用。这是苹果的隐私策略。

3.5 屏幕共享基础

屏幕共享是 WebRTC 里一个很实用的功能。它用的不是 getUserMedia,而是 getDisplayMedia。这个 API 会弹出一个选择窗口,让用户选择共享整个屏幕、某个窗口还是某个浏览器标签页。

基本用法:

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

这里有几个要点:

  • getDisplayMedia 返回的也是 MediaStream,所以你可以直接把它传给 RTCPeerConnection。
  • 音频方面,getDisplayMedia 支持采集系统音频(比如播放的视频声音),但需要浏览器支持。Chrome 从 74 版本开始支持。
  • 用户停止共享时,可以通过监听 oninactive 事件来感知。

我在项目中遇到过一个问题:用户共享屏幕后,想切回摄像头。这时候不能直接调用 getUserMedia 覆盖,而是要用 replaceTrack 方法替换轨道。否则会多出一路流,造成资源浪费。

3.6 知识体系总览

为了让你更直观地理解这一章的内容,我画了一张图:

获取媒体流知识体系 getUserMedia 基础用法:三步走 媒体约束配置 摄像头与麦克风权限 屏幕共享:getDisplayMedia 强制约束 (exact/min/max) 理想约束 (ideal) NotAllowedError NotFoundError cursor 配置 系统音频采集 核心:MediaStream → MediaStreamTrack → 轨道替换

3.7 避坑指南

最后,我把自己踩过的坑总结一下:

  • 我曾经在移动端用 exact 约束指定了 4K 分辨率,结果大部分手机都不支持,直接报错。后来改成 ideal 就好了。
  • 我曾经在屏幕共享时忘了处理 oninactive 事件,用户停止共享后,远端还显示着最后一帧画面。嗯,这个 bug 被测试小姐姐追着改了三轮。
  • 我建议在调用 getUserMedia 之前,先用 navigator.mediaDevices.enumerateDevices() 获取设备列表,这样你可以知道用户有哪些摄像头和麦克风,然后动态生成约束。
小技巧: 如果你想让用户能切换摄像头,不要每次都重新调用 getUserMedia。而是用 stream.getVideoTracks()[0].applyConstraints(newConstraints) 来动态调整。这样更高效,也不会重新触发权限弹窗。

好了,这一章的内容就到这里。getUserMedia 虽然基础,但它是整个 WebRTC 应用的起点。把这一块吃透了,后面的信令、P2P 连接、数据传输才能跑得顺畅。


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