5、音视频采集:摄像头调用、麦克风控制、分辨率与帧率设置

好,咱们进入移动端开发最基础也最核心的一环——音视频采集。说白了,就是怎么把摄像头和麦克风的数据拿到手,然后交给WebRTC去处理。这部分我踩过的坑不少,今天一次性给你讲透。

核心要点:在移动端,采集不是简单的打开设备就完事。分辨率、帧率、前后摄像头切换、音频回声消除……每一个细节都可能让你的应用卡死或者音画不同步。

5.1 摄像头调用:从 getUserMedia 说起

WebRTC 采集视频的标准接口是 navigator.mediaDevices.getUserMedia()。这个接口在桌面端很成熟,但在移动端,尤其是 Android 和 iOS 的 WebView 里,有很多隐藏的坑。

先看一个最基础的调用示例:

// 基础视频采集
async function startVideo() {
  try {
    const stream = await navigator.mediaDevices.getUserMedia({
      video: true,
      audio: false
    });
    // 将 stream 绑定到 video 标签
    const video = document.getElementById('localVideo');
    video.srcObject = stream;
  } catch (err) {
    console.error('摄像头打开失败:', err);
  }
}

嗯,这里要注意:video: true 会让浏览器自己选择默认摄像头。但在移动端,默认通常是后置摄像头。如果你需要前置摄像头(比如视频通话),必须显式指定:

// 强制使用前置摄像头
const stream = await navigator.mediaDevices.getUserMedia({
  video: {
    facingMode: 'user'  // 前置
    // facingMode: 'environment'  // 后置
  },
  audio: false
});

我个人习惯在项目里先检测设备支持的摄像头列表,再让用户选择。用 enumerateDevices() 可以拿到所有设备信息:

async function getCameraList() {
  const devices = await navigator.mediaDevices.enumerateDevices();
  const cameras = devices.filter(d => d.kind === 'videoinput');
  console.log('可用摄像头:', cameras);
  // 每个 camera 有 deviceId,可以精确指定
}

避坑指南:我曾经在 Android 的 Chrome 上遇到过一个问题——调用 getUserMedia 后,页面直接白屏。后来发现是 WebView 没有授予摄像头权限。移动端必须处理权限请求的 Promise 拒绝情况,否则用户拒绝授权后,整个应用会卡死。

警告:iOS 上的 Safari 要求页面必须处于用户手势触发的上下文中才能调用 getUserMedia。也就是说,你不能在页面加载时自动打开摄像头,必须绑定一个按钮点击事件。否则接口会静默失败。

5.2 麦克风控制:不只是打开和关闭

音频采集看起来简单,但实际项目中,回声消除、降噪、自动增益控制这些参数才是关键。WebRTC 的 getUserMedia 允许你传递一组音频约束:

const stream = await navigator.mediaDevices.getUserMedia({
  audio: {
    echoCancellation: true,   // 回声消除
    noiseSuppression: true,   // 降噪
    autoGainControl: true,    // 自动增益
    sampleRate: 48000,        // 采样率
    channelCount: 1           // 单声道
  },
  video: false
});

你想想看,如果不开回声消除,对方的声音从扬声器出来,又被你的麦克风收进去,再传回对方耳朵里——那就是无限循环的啸叫。我在做第一个视频通话 Demo 时就吃过这个亏,测试时两个人面对面坐着,声音越来越大,最后直接炸麦。

另外,移动端的麦克风权限比摄像头更敏感。iOS 上如果用户拒绝了麦克风权限,getUserMedia 会抛出一个 NotAllowedError。建议在 UI 层提前检测权限状态:

// 检测麦克风权限状态
navigator.permissions.query({ name: 'microphone' }).then(result => {
  if (result.state === 'denied') {
    // 引导用户去设置中开启
    showPermissionGuide();
  }
});

小技巧:在 Android 上,如果同时采集音频和视频,建议先请求音频权限再请求视频权限。因为有些国产 ROM 会把两个权限合并弹窗,用户一旦拒绝一个,另一个也会失败。

5.3 分辨率与帧率设置:平衡画质与性能

这是移动端最让人头疼的部分。分辨率越高,画面越清晰,但编码压力、网络带宽消耗也越大。帧率同理,30fps 流畅但耗电,15fps 省电但画面卡顿。

WebRTC 允许你通过 video 约束来指定理想的分辨率和帧率:

const stream = await navigator.mediaDevices.getUserMedia({
  video: {
    width: { ideal: 1280, min: 640, max: 1920 },
    height: { ideal: 720, min: 480, max: 1080 },
    frameRate: { ideal: 30, min: 15, max: 60 }
  },
  audio: true
});

这里有个关键点:ideal 是理想值,浏览器会尽量满足,但不保证。移动端设备能力参差不齐,比如一些低端 Android 机,你设 1080p 60fps,它可能直接降级到 480p 15fps,甚至采集失败。

我建议的做法是:先检测设备能力,再动态调整。用 MediaStreamTrack.getCapabilities() 可以拿到设备支持的范围:

const track = stream.getVideoTracks()[0];
const capabilities = track.getCapabilities();
console.log('支持的分辨率范围:', capabilities.width, capabilities.height);
console.log('支持的帧率范围:', capabilities.frameRate);

然后根据网络状况动态切换。比如弱网时降到 480p 15fps,强网时升到 720p 30fps。这个我在项目中用 RTCRtpSender.setParameters() 实现过,效果不错。

经验之谈:移动端视频通话,720p 30fps 是一个黄金平衡点。低于这个,画面模糊;高于这个,发热和耗电会明显增加。除非你做的是直播推流,否则不建议上 1080p。

5.4 前后摄像头切换

视频通话中,用户经常需要切换前后摄像头。WebRTC 没有直接的切换 API,但可以通过重新调用 getUserMedia 来实现。不过要注意,重新采集会中断当前的视频流。

更优雅的做法是:先停止当前视频轨道,再重新采集:

async function switchCamera() {
  const stream = localVideo.srcObject;
  const videoTrack = stream.getVideoTracks()[0];
  videoTrack.stop();  // 停止当前轨道

  const facingMode = currentFacing === 'user' ? 'environment' : 'user';
  const newStream = await navigator.mediaDevices.getUserMedia({
    video: { facingMode },
    audio: false
  });

  // 替换视频轨道
  const newVideoTrack = newStream.getVideoTracks()[0];
  stream.removeTrack(videoTrack);
  stream.addTrack(newVideoTrack);
  localVideo.srcObject = stream;
  currentFacing = facingMode;
}

嗯,这里要注意:stop() 之后,原来的摄像头指示灯会熄灭,然后重新亮起。如果切换太快,用户可能会看到黑屏闪烁。我一般会在切换前先显示一个 loading 动画,等新轨道就绪后再隐藏。

警告:iOS 上切换摄像头时,如果前后摄像头的分辨率能力不同(比如后置支持 4K,前置只支持 1080p),新采集的流可能会自动降级。建议在切换前先获取新摄像头的 capabilities,然后重新设置约束。

5.5 知识体系总览

下面这张图总结了本章的核心知识点和它们之间的关系。你可以把它当作一个快速索引,遇到问题时回来对照一下。

音视频采集 摄像头调用 getUserMedia facingMode / deviceId 麦克风控制 回声消除 / 降噪 采样率 / 声道 分辨率与帧率 ideal / min / max getCapabilities() 前后摄像头切换 stop() + addTrack() facingMode 切换 核心流程:设备检测 → 约束设置 → 采集启动 → 动态调整

5.6 常见问题与调试技巧

最后分享几个我在项目中反复遇到的坑,你遇到了可以直接对照排查:

问题现象 可能原因 解决方案
摄像头黑屏 权限未授予 / 设备被占用 检查权限状态,确保没有其他应用占用摄像头
音频有回声 echoCancellation 未开启 在约束中显式设置 echoCancellation: true
切换摄像头后画面卡住 旧轨道未正确停止 调用 videoTrack.stop() 后再采集新流
低端机采集 1080p 失败 设备不支持高分辨率 使用 getCapabilities() 检测后降级
iOS 上 getUserMedia 不执行 不在用户手势上下文中 将调用绑定到按钮点击事件

调试建议:在 Chrome 的开发者工具中,chrome://webrtc-internals 页面可以查看所有采集参数和统计信息。移动端可以通过 USB 调试连接后访问这个页面,非常实用。

好了,音视频采集这部分就讲到这里。记住一个原则:移动端永远不要假设设备能力,先检测,再适配。下一章我们会聊到编码与传输,到时候这些采集到的数据就要真正上路了。


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