4. 信令机制设计

这一章,我们来啃一块硬骨头——getUserMedia。

说白了,它就是 WebRTC 的“水源”。没有它,摄像头、麦克风的数据根本进不了浏览器。我刚开始接触 WebRTC 时,觉得这 API 不就是调个方法嘛,有啥好研究的?结果真到项目里,权限拒绝、设备没插、格式不支持……各种坑踩了个遍。

今天咱们就把 getUserMedia 从头到尾捋一遍。你跟着我走一遍,以后遇到媒体流相关的问题,心里就有底了。

4.1 核心概念:MediaStream 与 MediaStreamTrack

先搞清楚两个基础概念。

  • MediaStream:一个媒体流容器。你可以把它想象成一个“管道”,里面可以同时走视频和音频数据。
  • MediaStreamTrack:管道里的“单根线”。一根线只传一种数据——要么是视频轨道(VideoTrack),要么是音频轨道(AudioTrack)。

举个例子:你打开一个视频通话,一个 MediaStream 里通常包含两个 Track——一个摄像头视频轨道,一个麦克风音频轨道。

关键点:一个 MediaStream 可以包含多个 Track,也可以只包含一个。你可以单独控制每个 Track 的启用/停用,甚至动态添加或移除。

4.2 基础调用:navigator.mediaDevices.getUserMedia

这是整个章节的核心方法。调用方式很简单:

navigator.mediaDevices.getUserMedia(constraints)
  .then(function(stream) {
    // 拿到 stream,可以用了
  })
  .catch(function(err) {
    // 出错了,处理错误
  });

嗯,就这么几行。但真正的坑,全在 constraints 参数里。

4.3 constraints 参数详解

constraints 是一个对象,告诉浏览器你想要什么。最基本的写法:

// 同时要摄像头和麦克风
const constraints = {
  video: true,
  audio: true
};

但实际项目中,你很少会这么写。为什么?因为 true 表示“随便给我一个”,分辨率、帧率、设备类型全交给浏览器决定。我遇到过好几次,用户摄像头明明是 1080p 的,结果浏览器给了一个 640x480 的流——用户体验直接崩了。

所以,我建议你永远指定具体参数:

const constraints = {
  video: {
    width: { ideal: 1280 },
    height: { ideal: 720 },
    frameRate: { ideal: 30 }
  },
  audio: {
    sampleRate: { ideal: 48000 },
    channelCount: { ideal: 2 }
  }
};

这里用 ideal 而不是 exact,意思是“尽量满足,不行就降级”。如果你非要 4K 不可,可以用 exact,但要做好用户设备不支持时直接报错的心理准备。

约束模式 含义 适用场景
exact 精确匹配,不满足就报错 必须特定分辨率/帧率
ideal 理想值,浏览器尽量满足 大多数场景,推荐使用
min / max 范围约束,在范围内取最优 保证最低质量要求

我的习惯:先调用 enumerateDevices() 获取用户设备列表,再根据设备能力动态生成 constraints。这样用户体验最好,不会出现“明明有 4K 摄像头却只给 720p”的尴尬。

4.4 实战:获取摄像头与麦克风权限

光说不练假把式。咱们直接写一个完整的例子。

async function startMedia() {
  try {
    const stream = await navigator.mediaDevices.getUserMedia({
      video: {
        width: { ideal: 1280 },
        height: { ideal: 720 },
        facingMode: 'user'  // 前置摄像头
      },
      audio: {
        echoCancellation: true,
        noiseSuppression: true
      }
    });

    // 把流绑定到 video 元素
    const videoElement = document.getElementById('localVideo');
    videoElement.srcObject = stream;

    // 获取轨道信息
    const videoTrack = stream.getVideoTracks()[0];
    const audioTrack = stream.getAudioTracks()[0];

    console.log('视频轨道:', videoTrack.label);
    console.log('音频轨道:', audioTrack.label);

    return stream;
  } catch (err) {
    console.error('获取媒体流失败:', err);
    handleError(err);
  }
}

这里有几个细节要注意:

  • facingMode: 'user' 表示前置摄像头,'environment' 是后置。移动端特别有用。
  • echoCancellation 和 noiseSuppression 是音频处理的关键。不开启的话,对方会听到你的回声和背景噪音。
  • stream.getVideoTracks()[0] 拿到的是第一个视频轨道。如果用户有多个摄像头,你可以让用户选择。

注意:getUserMedia 必须在 HTTPS 或 localhost 下才能使用。HTTP 环境下会直接报错。我曾经在开发阶段用 HTTP 调试了半天,一直拿不到流,最后才发现是协议问题——嗯,这个坑我替你们踩过了。

4.5 错误处理:用户拒绝权限怎么办?

用户点了“拒绝”按钮,或者浏览器设置里禁用了摄像头/麦克风,getUserMedia 会抛出一个 NotAllowedError。

我的做法是:

function handleError(err) {
  if (err.name === 'NotAllowedError') {
    alert('请允许使用摄像头和麦克风权限');
    // 可以引导用户去浏览器设置里手动开启
  } else if (err.name === 'NotFoundError') {
    alert('未检测到摄像头或麦克风设备');
  } else if (err.name === 'NotReadableError') {
    alert('设备被其他应用占用,请关闭其他程序后重试');
  } else {
    alert('未知错误:' + err.message);
  }
}

你想想看,用户拒绝权限后,如果不给任何提示,页面就卡在那里了。用户体验极差。所以一定要做友好的错误提示。

4.6 流处理:停止、切换与克隆

拿到流之后,你还需要知道怎么管理它。

停止轨道

// 停止所有轨道
stream.getTracks().forEach(track => track.stop());

// 或者只停止视频
stream.getVideoTracks().forEach(track => track.stop());

为什么要手动停止?因为浏览器不会自动释放摄像头和麦克风。如果你不停止,摄像头灯会一直亮着——用户会以为你在偷拍他。

切换摄像头

async function switchCamera() {
  const stream = await navigator.mediaDevices.getUserMedia({
    video: {
      facingMode: { exact: 'environment' }  // 切换到后置
    }
  });
  // 替换 video 元素的流
  videoElement.srcObject = stream;
}

注意:切换时最好先停止旧流,否则摄像头可能被占用。

克隆轨道

const clonedTrack = videoTrack.clone();
// 克隆的轨道可以单独控制,互不影响

克隆轨道在录制、截图等场景中很有用。比如你想同时录制视频和拍照,克隆一份轨道给录制器,原轨道继续显示预览。

4.7 知识体系总览

下面这张图,把 getUserMedia 的核心流程串起来了。你对照着看,思路会更清晰。

getUserMedia 核心流程 用户触发调用 传入 constraints 参数 浏览器检查权限 拒绝 → NotAllowedError 同意 → 获取设备 检查设备可用性 返回 MediaStream 常见错误 NotAllowedError / NotFoundError NotReadableError / OverconstrainedError

4.8 避坑指南

最后,分享几个我踩过的坑:

  • 权限弹窗被拦截:有些浏览器会阻止自动弹出权限请求。我建议在用户点击按钮后再调用 getUserMedia,而不是页面加载时自动调用。
  • 移动端同时使用前后摄像头:大部分手机不支持同时开启前后摄像头。如果你需要双摄,得用两个不同的 MediaStream。
  • 音频自动播放限制:拿到音频流后,如果想直接播放,可能会被浏览器拦截。需要用户先与页面交互一次。
  • 设备热插拔:用户可能在通话中拔掉摄像头。监听 devicechange 事件,动态处理设备变化。

小技巧:调试时可以在 Chrome 开发者工具的 Application → Media 面板里,手动模拟设备权限和流。省得每次都要插拔摄像头。