12、媒体流获取与处理:getUserMedia API、屏幕共享(getDisplayMedia)、媒体约束(Constraints)

各位同学,今天我们来聊聊WebRTC里最基础、也最“接地气”的一环——怎么把摄像头、麦克风、屏幕这些硬件设备“请”到浏览器里来。

说白了,没有媒体流,WebRTC就是个空壳子。你没法跟人视频,也没法共享PPT。我当年刚接触WebRTC时,以为这步很简单,不就是调个API嘛。结果第一次上线就翻车了——用户反馈“为什么我点了允许,还是黑屏?”嗯,这里面的坑,咱们今天一个一个填上。

12.1 getUserMedia:打开摄像头和麦克风

getUserMedia 是 WebRTC 媒体获取的入口。它的作用就一句话:向用户请求音视频设备的访问权限,并返回一个 MediaStream 对象。

来看一个最基础的例子:

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

这段代码看起来简单,但我得提醒你几个关键点:

  • 必须通过 HTTPS 或 localhost——浏览器出于安全考虑,非安全上下文下 getUserMedia 会直接拒绝。我在项目中遇到过测试同事在 HTTP 环境下调试,折腾了半天才发现是协议问题。
  • 用户授权是异步的——浏览器会弹出权限请求框,用户点击“允许”或“阻止”后才能继续。如果用户点了阻止,catch 里会收到 NotAllowedError
  • 返回的 stream 是实时的——你可以随时停止某个轨道,比如关掉麦克风:stream.getAudioTracks()[0].stop()
个人习惯:我通常会在调用 getUserMedia 之前先检查 navigator.mediaDevices 是否存在,避免在不支持的浏览器上直接报错。加一句 if (!navigator.mediaDevices) { alert('您的浏览器不支持媒体设备'); } 能省去很多排查时间。

12.2 getDisplayMedia:屏幕共享

屏幕共享是远程会议、在线教育的核心功能。WebRTC 提供了 getDisplayMedia 来实现它。

它的用法和 getUserMedia 很像,但有几个重要区别:

async function startScreenShare() {
  try {
    const stream = await navigator.mediaDevices.getDisplayMedia({
      video: true,
      audio: false  // 屏幕共享通常不包含系统音频
    });
    const videoElement = document.getElementById('screenVideo');
    videoElement.srcObject = stream;
  } catch (err) {
    console.error('屏幕共享失败:', err);
  }
}

这里要注意:

  • 用户必须手动选择共享内容——浏览器会弹出一个选择器,让用户选择共享整个屏幕、某个窗口还是某个浏览器标签页。你不能跳过这一步。
  • 音频默认不包含——如果你想同时共享系统音频(比如播放的视频声音),需要设置 audio: true,但浏览器支持有限。我曾经踩过这个坑:用户共享了一个带声音的视频,结果对方听不到任何音频,排查了半天才发现是 Chrome 当时还不支持系统音频采集。
  • 用户可以通过系统UI停止共享——浏览器会在地址栏附近显示一个“停止共享”按钮,用户点击后 stream.getVideoTracks()[0].onended 事件会触发。你需要监听这个事件,及时清理 UI 状态。
我曾经犯过的错:在屏幕共享时没有监听 onended 事件,结果用户手动停止共享后,远程端还显示着最后一帧画面。用户以为还在共享,其实早就断了。后来我加了一段代码:stream.getVideoTracks()[0].onended = () => { /* 清理UI,通知远端 */ }; 问题解决。

12.3 媒体约束(Constraints):精细控制

你想想看,如果每次打开摄像头都是默认的 640x480 分辨率,那在 4K 显示器上得多模糊?媒体约束就是用来解决这个问题的。

约束分为三类:

类型 说明 示例
基本约束 指定一个确切的值或范围 width: 1280
理想约束 浏览器尽量满足,但不强制 width: { ideal: 1920 }
范围约束 指定最小/最大/精确值 width: { min: 640, max: 1920 }

来看一个实际例子:

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

const stream = await navigator.mediaDevices.getUserMedia(constraints);

这里有几个要点:

  • facingMode 控制前后摄像头:user 是前置,environment 是后置。移动端开发时这个属性很常用。
  • echoCancellationnoiseSuppression 默认就是 true,但如果你在做专业音频处理,可能需要关闭它们,自己实现算法。
  • 约束不是强制的——浏览器会尽量满足你的要求,但如果设备不支持,它会自动降级。比如你要求 4K 分辨率,但摄像头只支持 1080p,浏览器会返回 1080p 的流,不会报错。
避坑指南:我曾经在项目中要求 width: { exact: 1920 },结果部分老旧设备直接抛错,因为 exact 是强制匹配,设备不支持就失败。后来我改成了 ideal,兼容性好了很多。所以,除非你真的必须用某个精确值,否则尽量用 ideal 或范围约束。

12.4 知识体系总览

下面这张图帮你理清本章的核心逻辑:

媒体流获取与处理核心流程 navigator.mediaDevices 选择获取方式 getUserMedia() 摄像头 / 麦克风 getDisplayMedia() 屏幕 / 窗口 / 标签页 MediaStream 对象 Constraints 约束控制

这张图展示了整个媒体获取的脉络:从 navigator.mediaDevices 出发,根据场景选择 getUserMediagetDisplayMedia,同时通过 Constraints 精细控制采集参数,最终得到一个 MediaStream 对象供后续使用。

12.5 实际项目中的经验总结

最后,分享几个我在项目中积累的小经验:

  1. 先检测设备列表——调用 enumerateDevices() 获取可用设备,再决定显示哪些选项给用户。比如没有后置摄像头的设备,就别显示“切换后置”按钮。
  2. 处理权限被拒的情况——用户拒绝授权后,不要只是弹个错误。我习惯在 UI 上显示一个引导提示,告诉用户如何手动开启权限(比如点击地址栏左侧的锁图标)。
  3. 动态调整约束——网络状况不好时,可以降低分辨率或帧率。比如监听 RTCPeerConnectiononiceconnectionstatechange,在连接不稳定时调用 stream.getVideoTracks()[0].applyConstraints({ width: 640, height: 480 })
  4. 别忘了释放资源——页面关闭或通话结束时,记得调用 stream.getTracks().forEach(track => track.stop()),否则摄像头指示灯会一直亮着,用户会以为你在偷拍他。
一个小技巧:调试时可以在浏览器控制台输入 navigator.mediaDevices.getUserMedia({ video: true }) 快速测试摄像头是否正常工作。如果返回 Promise 被拒绝,看看错误信息是 NotAllowedError 还是 NotFoundError,能帮你快速定位问题。

好了,媒体获取这部分就讲到这里。记住一句话:约束是手段,流是结果,用户体验才是目的。下一章我们会聊怎么把这些流通过信令和网络传出去,那才是 WebRTC 真正精彩的地方。


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