3. 音视频采集基础:getUserMedia API详解、摄像头与麦克风的权限管理、音视频约束参数配置、媒体流(MediaStream)对象

好,咱们今天聊点实在的。做WebRTC开发,第一步是什么?不是信令,不是网络穿透,而是——把摄像头和麦克风的数据拿到手。这一步搞不定,后面全是空中楼阁。

我刚开始接触WebRTC时,以为音视频采集就是调个API完事。结果呢?在Chrome上跑得好好的,换到Safari直接黑屏。后来才发现是权限策略和约束参数没配对。嗯,这些坑我今天都给你指出来。

核心一句话:getUserMedia 是浏览器访问音视频设备的唯一入口。它返回一个 MediaStream 对象,里面装着音频轨和视频轨。

3.1 getUserMedia API 详解

先看最基础的调用方式:

navigator.mediaDevices.getUserMedia({
  video: true,
  audio: true
})
.then(function(stream) {
  // 拿到媒体流,可以赋值给 video 标签
  const videoElement = document.getElementById('localVideo');
  videoElement.srcObject = stream;
})
.catch(function(err) {
  console.error('采集失败:', err);
});

这段代码看起来简单,但有几个细节我得提醒你:

  • navigator.mediaDevices 是浏览器提供的媒体设备接口,getUserMedia 是它的核心方法。
  • 参数是一个对象,video 和 audio 分别控制是否采集视频和音频。
  • 返回的是一个 Promise,成功时拿到 MediaStream,失败时抛出错误。

我个人习惯把 getUserMedia 的调用封装成一个函数,方便复用。比如这样:

async function startCapture(constraints) {
  try {
    const stream = await navigator.mediaDevices.getUserMedia(constraints);
    return stream;
  } catch (error) {
    console.error('采集失败:', error.name, error.message);
    throw error;
  }
}

为什么要用 async/await?因为 Promise 链多了之后,错误处理容易乱。async/await 配合 try/catch,代码更清晰。

小技巧:在调用 getUserMedia 之前,可以先检查浏览器是否支持:

if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
  alert('当前浏览器不支持音视频采集');
}

3.2 摄像头与麦克风的权限管理

权限管理是音视频采集中最容易出问题的环节。为什么?因为浏览器对权限的控制越来越严格。

我在项目中遇到过这样的情况:用户第一次拒绝授权后,再次调用 getUserMedia 会直接失败,而且不会弹出权限提示框。用户以为浏览器坏了,其实是权限被永久拒绝了。

权限状态分为三种:

状态 说明 处理方式
granted 已授权 正常调用 getUserMedia
denied 已拒绝 提示用户手动开启权限
prompt 未决定 调用 getUserMedia 会弹出提示框

你可以用 navigator.permissions.query() 来检查权限状态:

async function checkCameraPermission() {
  try {
    const result = await navigator.permissions.query({ name: 'camera' });
    console.log('摄像头权限状态:', result.state);
    
    if (result.state === 'denied') {
      alert('摄像头权限已被拒绝,请在浏览器设置中开启');
    }
    
    // 监听权限状态变化
    result.onchange = function() {
      console.log('权限状态变为:', result.state);
    };
  } catch (error) {
    console.error('权限检查失败:', error);
  }
}

注意:permissions.query 方法在部分浏览器中不支持。而且,即使用户授权了,如果页面不是 HTTPS 环境,getUserMedia 也会失败。这是浏览器的安全策略,没得商量。

我曾经踩过一个坑:在本地开发时用 http://localhost 没问题,但部署到测试服务器后,因为没配 HTTPS,采集直接报错。所以,开发时就用 HTTPS,别给自己找麻烦。

3.3 音视频约束参数配置

约束参数是 getUserMedia 的精髓。说白了,就是告诉浏览器:我要什么样的视频和音频。

先看一个完整的约束示例:

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

约束参数分两种:

  • 强制约束(exact):浏览器必须满足,否则报错。比如 width: { exact: 1920 }
  • 理想约束(ideal):浏览器尽量满足,但不保证。比如 width: { ideal: 1280 }

我个人建议:尽量用 ideal,别用 exact。为什么?因为不同设备的摄像头能力不一样。你用 exact 指定 4K,用户的笔记本摄像头不支持,直接就报错了。用 ideal,浏览器会自动选一个最接近的分辨率。

facingMode 这个参数很有意思:

含义 适用场景
user 前置摄像头 视频通话、自拍
environment 后置摄像头 扫码、拍摄实物
left 前置偏左 部分设备支持
right 前置偏右 部分设备支持

音频约束里,echoCancellation 和 noiseSuppression 是必开的。你想想看,如果不开回声消除,对方说话的声音从你的扬声器出来,又被麦克风收进去,再传回去,那不就是无限循环吗?

调试技巧:MediaDevices.getSupportedConstraints() 查看当前浏览器支持哪些约束:

const supported = navigator.mediaDevices.getSupportedConstraints();
console.table(supported);

3.4 媒体流(MediaStream)对象

getUserMedia 返回的 MediaStream 对象,是音视频数据的容器。它里面可以包含多个轨道(MediaStreamTrack),每个轨道对应一路音频或视频。

来看 MediaStream 的常用操作:

// 获取所有轨道
const tracks = stream.getTracks();
console.log('轨道数量:', tracks.length);

// 获取视频轨道
const videoTracks = stream.getVideoTracks();
console.log('视频轨道:', videoTracks[0].label);

// 获取音频轨道
const audioTracks = stream.getAudioTracks();
console.log('音频轨道:', audioTracks[0].label);

// 停止所有轨道(释放摄像头和麦克风)
stream.getTracks().forEach(track => track.stop());

// 或者只停止视频轨道
videoTracks[0].stop();

这里有个关键点:记得释放资源。当用户挂断通话或离开页面时,如果不停止轨道,摄像头指示灯会一直亮着。用户会怎么想?"这个网页在偷拍我!"

MediaStreamTrack 对象还有一些有用的属性:

属性 说明 示例值
kind 轨道类型 'video' 或 'audio'
label 设备名称 'FaceTime HD Camera'
enabled 是否启用 true / false
muted 是否静音 true / false
readyState 轨道状态 'live' 或 'ended'

你可以通过修改 enabled 来控制轨道的开关,而不需要重新采集:

// 静音麦克风(不采集音频数据,但保留轨道)
audioTrack.enabled = false;

// 关闭摄像头(停止采集视频数据)
videoTrack.enabled = false;

这和 stop() 的区别在于:enabled 可以随时再打开,stop() 之后轨道就结束了,需要重新调用 getUserMedia。

3.5 知识体系总览

下面这张图把本章的核心逻辑串起来了:

音视频采集核心流程 用户点击"开始采集" 权限检查(permissions.query) 已授权 → 继续 未授权 → 弹出提示框 已拒绝 → 引导手动开启 getUserMedia(constraints) MediaStream 对象

这张图展示了从用户操作到拿到 MediaStream 的完整链路。每一步都可能出问题,所以一定要做好错误处理。

3.6 完整示例:采集并预览本地视频

最后,给你一个可以直接运行的示例。把摄像头画面显示在页面上:

<!DOCTYPE html>
<html>
<head>
  <title>音视频采集示例</title>
</head>
<body>
  <video id="localVideo" autoplay playsinline muted></video>
  <button id="startBtn">开始采集</button>
  <button id="stopBtn">停止采集</button>

  <script>
    let localStream = null;

    document.getElementById('startBtn').onclick = async function() {
      try {
        const constraints = {
          video: {
            width: { ideal: 1280 },
            height: { ideal: 720 },
            facingMode: 'user'
          },
          audio: {
            echoCancellation: true,
            noiseSuppression: true
          }
        };

        localStream = await navigator.mediaDevices.getUserMedia(constraints);
        const video = document.getElementById('localVideo');
        video.srcObject = localStream;
        
        console.log('采集成功,视频轨道:', localStream.getVideoTracks()[0].label);
      } catch (error) {
        console.error('采集失败:', error.name, error.message);
        alert('采集失败: ' + error.message);
      }
    };

    document.getElementById('stopBtn').onclick = function() {
      if (localStream) {
        localStream.getTracks().forEach(track => track.stop());
        document.getElementById('localVideo').srcObject = null;
        localStream = null;
        console.log('已停止采集');
      }
    };
  </script>
</body>
</html>

重要提醒:video 标签一定要加 autoplay playsinline muted。autoplay 让视频自动播放,playsinline 在 iOS 上避免全屏播放,muted 是自动播放的前提条件(浏览器策略)。

好了,音视频采集的基础就这些。记住:权限管理要提前检查,约束参数用 ideal 别用 exact,用完资源记得释放。把这些搞定了,后面信令和网络传输才能玩得转。


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