3、浏览器端音视频采集:MediaStream API详解、摄像头/麦克风控制、屏幕共享实现、分辨率与帧率调优

好,咱们今天来聊聊浏览器端最基础、也最核心的一环——音视频采集。你想想看,不管你是做视频会议、直播连麦,还是搞AI人脸识别,第一步都是要把摄像头和麦克风的数据拿过来。这一步要是没做好,后面再牛的AI算法也是白搭。

我个人习惯把浏览器端的采集能力分成三块:设备控制屏幕共享参数调优。这三块搞明白了,你就能像搭积木一样,灵活组合出各种采集方案。

核心知识点速览

  • MediaStream API 的核心对象与生命周期
  • 摄像头/麦克风的枚举、切换与开关控制
  • 屏幕共享的两种实现方式与权限处理
  • 分辨率、帧率、码率的平衡艺术
浏览器音视频采集 设备控制 enumerateDevices() getUserMedia() track.stop() 屏幕共享 getDisplayMedia() surfaceSwitcher 权限弹窗处理 参数调优 分辨率/帧率 applyConstraints() 码率控制 MediaStream 是这一切的基石

3.1 MediaStream API 核心概念

MediaStream 说白了就是一个容器。它里面装着若干个 MediaStreamTrack,每个 track 代表一路独立的音视频数据。比如你打开摄像头,会得到一个视频 track;打开麦克风,得到一个音频 track。这两个 track 组合在一起,就是一个完整的 MediaStream。

我记得刚接触 WebRTC 那会儿,总搞不清 stream 和 track 的关系。后来在做一个多摄像头切换的项目时,才彻底明白——stream 是逻辑上的"通道",track 才是真正的数据源。你可以把多个 track 塞进一个 stream,也可以把一个 track 从 stream 里拆出来单独处理。

小技巧:stream.getTracks() 可以拿到所有 track,用 stream.getVideoTracks() 只拿视频。调试时我习惯先打印一下 track 的 label 属性,看看当前用的是哪个设备。

3.2 摄像头与麦克风控制

控制摄像头和麦克风,核心就两个 API:navigator.mediaDevices.enumerateDevices()navigator.mediaDevices.getUserMedia()。前者用来枚举设备,后者用来启动采集。

先看枚举设备。你可能会问:"为什么非要枚举?直接 getUserMedia 不就行了?" 嗯,这里要注意——用户可能插了多个摄像头。比如笔记本自带一个,外接一个高清摄像头。如果你不枚举,系统可能默认选了个低分辨率的,画质惨不忍睹。

// 枚举所有音视频设备
async function listDevices() {
  const devices = await navigator.mediaDevices.enumerateDevices();
  devices.forEach(device => {
    console.log(`${device.kind}: ${device.label} [${device.deviceId}]`);
  });
}

// 指定设备启动采集
async function startCamera(deviceId) {
  const constraints = {
    video: { deviceId: { exact: deviceId } },
    audio: true
  };
  const stream = await navigator.mediaDevices.getUserMedia(constraints);
  return stream;
}

我在项目中遇到过一个问题:用户第一次访问页面时,enumerateDevices 返回的 label 全是空字符串。为什么?因为浏览器为了保护隐私,在用户未授权前不暴露设备名称。解决办法是先调一次 getUserMedia 获取权限,再枚举就能拿到完整信息了。

避坑指南: 我曾经在切换摄像头时直接调了两次 getUserMedia,结果旧摄像头没关,新摄像头又开了,浏览器直接卡死。正确的做法是:先调用 track.stop() 停止旧 track,再启动新的。或者用 stream.getTracks().forEach(t => t.stop()) 一次性清理。

3.3 屏幕共享实现

屏幕共享用的是 getDisplayMedia(),不是 getUserMedia。这个 API 会弹出一个系统级的选择窗口,让用户选择共享整个屏幕、某个窗口还是某个浏览器标签页。

说白了,getDisplayMedia 返回的也是一个 MediaStream,但它的视频 track 有一些特殊属性。比如你可以通过 track.getSettings() 拿到 displaySurface 字段,判断用户共享的是屏幕还是窗口。

async function startScreenShare() {
  try {
    const stream = await navigator.mediaDevices.getDisplayMedia({
      video: {
        cursor: "always"  // 始终显示鼠标指针
      },
      audio: {
        echoCancellation: true,
        noiseSuppression: true
      }
    });
    return stream;
  } catch (err) {
    console.error("用户取消了屏幕共享:", err);
  }
}

这里有个坑:屏幕共享的音频。Chrome 在 2022 年之后支持了屏幕共享时采集系统音频(比如播放的视频声音),但需要用户在弹窗里手动勾选"分享音频"。你代码里写了 audio: true 只是申请,最终决定权在用户手里。

重要提醒: 屏幕共享的 track 有一个 ended 事件。当用户点击浏览器工具栏的"停止共享"按钮时,这个事件会触发。一定要监听它,及时清理 UI 状态。

screenTrack.onended = () => {
  console.log("用户停止了屏幕共享");
  // 清理视频元素、释放资源
  videoElement.srcObject = null;
};

3.4 分辨率与帧率调优

分辨率、帧率、码率,这三者是音视频领域的"不可能三角"。你想想看,想要高分辨率高帧率,码率就得往上飙;想要低码率保流畅,就得牺牲分辨率或帧率。

我个人习惯把调优分成两步:先定上限,再动态调整

定上限就是在 getUserMedia 时设置理想值:

const constraints = {
  video: {
    width: { ideal: 1280, max: 1920 },
    height: { ideal: 720, max: 1080 },
    frameRate: { ideal: 30, max: 60 }
  }
};

注意这里用的是 idealmax,不是 exact。为什么?因为浏览器会根据当前硬件能力自动选择最接近 ideal 的值。你如果写 exact: 1920,万一摄像头不支持,直接报错。

动态调整用的是 track.applyConstraints()。比如在弱网环境下,你可以动态降低分辨率:

async function downgradeResolution(track) {
  await track.applyConstraints({
    width: { ideal: 640 },
    height: { ideal: 480 },
    frameRate: { ideal: 15 }
  });
}

我在做远程医疗项目时,遇到过一个问题:摄像头支持 4K 30fps,但用户电脑性能不够,画面卡成 PPT。后来我加了一个自动检测机制——如果连续 5 秒帧率低于 10fps,就自动降一档分辨率。效果立竿见影。

调优建议:

  • 视频会议: 720p 30fps 足够,码率控制在 1-2Mbps
  • 屏幕共享(代码/文档): 1080p 15fps 即可,码率可以低一些
  • AI 人脸识别: 480p 15fps 就够,分辨率太高反而增加处理延迟
  • 直播推流: 根据上行带宽动态调整,建议从 720p 30fps 起步

3.5 综合实战:一个可切换的采集器

最后,我把今天讲的东西串起来,写一个简单的采集器。它支持切换摄像头、开关麦克风、动态调整分辨率。

class MediaCapture {
  constructor() {
    this.stream = null;
    this.videoTrack = null;
    this.audioTrack = null;
  }

  async start(videoDeviceId, audioDeviceId) {
    // 先停止旧的
    this.stop();

    const constraints = {
      video: {
        deviceId: videoDeviceId ? { exact: videoDeviceId } : undefined,
        width: { ideal: 1280 },
        height: { ideal: 720 },
        frameRate: { ideal: 30 }
      },
      audio: {
        deviceId: audioDeviceId ? { exact: audioDeviceId } : undefined,
        echoCancellation: true
      }
    };

    this.stream = await navigator.mediaDevices.getUserMedia(constraints);
    this.videoTrack = this.stream.getVideoTracks()[0];
    this.audioTrack = this.stream.getAudioTracks()[0];
    return this.stream;
  }

  stop() {
    if (this.stream) {
      this.stream.getTracks().forEach(t => t.stop());
      this.stream = null;
      this.videoTrack = null;
      this.audioTrack = null;
    }
  }

  async switchCamera(deviceId) {
    if (!this.videoTrack) return;
    await this.videoTrack.applyConstraints({
      deviceId: { exact: deviceId }
    });
  }

  toggleMic() {
    if (this.audioTrack) {
      this.audioTrack.enabled = !this.audioTrack.enabled;
    }
  }

  async setResolution(width, height) {
    if (!this.videoTrack) return;
    await this.videoTrack.applyConstraints({
      width: { exact: width },
      height: { exact: height }
    });
  }
}

这个类封装了常见的采集操作。你可以在项目里直接复用,也可以根据需求扩展。比如加上码率控制、自动重连、设备热插拔监听等。

好了,关于浏览器端音视频采集,核心就是这些。记住一句话:MediaStream 是入口,track 是灵魂,constraints 是武器。把这三点吃透,后面学 WebRTC 的传输、处理、AI 融合,都会轻松很多。


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