28、移动端适配:移动端浏览器兼容性、触摸事件处理、移动端摄像头控制

说实话,做WebRTC音视频采集,最头疼的往往不是PC端,而是移动端。

我刚开始做移动端适配的时候,踩过不少坑。同一个页面,在Chrome上跑得飞起,到了Safari上直接黑屏。或者触摸屏幕想切换摄像头,结果触发了页面滚动。这些场景,我相信很多做移动端开发的朋友都遇到过。

这一章,我就把我在移动端适配上的实战经验,掰开揉碎了讲给你听。

移动端浏览器兼容性:别让浏览器成为拦路虎

移动端浏览器环境比PC复杂得多。你想想看,光是主流浏览器就有Safari、Chrome、UC、QQ浏览器、华为浏览器等等。每个浏览器的内核、版本、对WebRTC的支持程度都不一样。

核心结论:移动端WebRTC兼容性,Safari是最大的变量。

我在项目中遇到过这样的情况:Android端Chrome一切正常,iOS端Safari死活拿不到摄像头权限。后来发现是Safari对getUserMedia的调用时机有严格要求——必须在用户手势事件中触发。

这里我整理了一份移动端主流浏览器的兼容性对照表,供你参考:

浏览器 getUserMedia enumerateDevices H.264编码 屏幕共享
Safari (iOS) ✅ 需用户手势 ❌ 不支持
Chrome (Android)
UC浏览器 ⚠️ 部分版本需降级 ⚠️ 可能不完整 ⚠️ 部分支持
QQ浏览器 ⚠️ 需权限
华为浏览器 ⚠️ 部分支持

我的建议:不要试图兼容所有浏览器。优先保证Safari和Chrome的体验,其他浏览器做降级处理。如果用户使用UC这类浏览器,可以提示「建议使用Chrome或Safari获得最佳体验」。

触摸事件处理:别让手指操作变成灾难

移动端没有鼠标,只有手指。触摸事件的处理,直接决定了用户体验的好坏。

我记得有一次,用户反馈说「点击切换摄像头按钮没反应」。我排查了半天,发现是click事件在移动端有300ms延迟,用户以为没点到,又点了一次,结果触发了两次切换。

解决方案其实很简单——用touchstart代替click,或者使用FastClick库消除延迟。但我个人更推荐原生方案:

// 移动端触摸事件处理示例
const switchCameraBtn = document.getElementById('switchCamera');

// 使用 touchstart 替代 click
switchCameraBtn.addEventListener('touchstart', function(e) {
  e.preventDefault(); // 防止触发滚动
  switchCamera();
});

// 同时保留 click 事件,兼容非触摸设备
switchCameraBtn.addEventListener('click', function(e) {
  switchCamera();
});

function switchCamera() {
  // 切换摄像头的逻辑
  console.log('切换摄像头');
}

这里有个细节要注意:touchstart事件中一定要调用e.preventDefault(),否则手指滑动时会触发页面滚动。嗯,这个坑我踩过不止一次。

触摸事件中的常见陷阱

  • 事件穿透:触摸一个元素,结果触发了它下面元素的事件。解决方案是用pointer-events或阻止事件冒泡。
  • 双击缩放:用户双击屏幕时,浏览器会缩放页面。如果你需要双击切换摄像头,记得用touchstart的时间戳判断是单击还是双击。
  • 长按菜单:移动端长按会弹出系统菜单。如果你需要长按触发某个功能,记得用contextmenu事件并阻止默认行为。

注意:在iOS Safari上,touchstart事件中如果调用了getUserMedia,可能会被浏览器拦截。正确的做法是在touchstart中设置一个标志位,然后在click事件中调用摄像头API。

移动端摄像头控制:前后切换、对焦、闪光灯

移动端摄像头控制,说白了就是让用户能自由切换前后摄像头、调整焦距、控制闪光灯。这些功能在原生App里很常见,但在Web端实现起来,限制还挺多的。

前后摄像头切换

切换摄像头,核心是修改getUserMediafacingMode参数。我习惯这样写:

let currentFacingMode = 'user'; // 'user' 为前置,'environment' 为后置

async function switchCamera() {
  // 切换 facingMode
  currentFacingMode = currentFacingMode === 'user' ? 'environment' : 'user';
  
  // 停止当前流
  if (localStream) {
    localStream.getTracks().forEach(track => track.stop());
  }
  
  try {
    const constraints = {
      video: {
        facingMode: currentFacingMode,
        width: { ideal: 1280 },
        height: { ideal: 720 }
      },
      audio: true
    };
    
    localStream = await navigator.mediaDevices.getUserMedia(constraints);
    videoElement.srcObject = localStream;
  } catch (err) {
    console.error('切换摄像头失败:', err);
  }
}

这里有个坑:有些Android设备不支持facingMode: 'environment',会直接报错。我曾经在小米手机上遇到过这个问题。解决方案是先用enumerateDevices获取所有摄像头,然后根据设备ID来切换:

async function getCameraDevices() {
  const devices = await navigator.mediaDevices.enumerateDevices();
  const videoDevices = devices.filter(d => d.kind === 'videoinput');
  
  // 通常第一个是前置,第二个是后置
  return videoDevices;
}

async function switchCameraByDeviceId() {
  const cameras = await getCameraDevices();
  if (cameras.length < 2) {
    console.warn('只有一个摄像头,无法切换');
    return;
  }
  
  // 切换到另一个摄像头
  const targetCamera = cameras.find(c => c.deviceId !== currentDeviceId);
  if (!targetCamera) return;
  
  // 停止当前流
  localStream.getTracks().forEach(track => track.stop());
  
  const constraints = {
    video: {
      deviceId: { exact: targetCamera.deviceId },
      width: { ideal: 1280 },
      height: { ideal: 720 }
    },
    audio: true
  };
  
  localStream = await navigator.mediaDevices.getUserMedia(constraints);
  videoElement.srcObject = localStream;
  currentDeviceId = targetCamera.deviceId;
}

摄像头对焦与缩放

移动端摄像头支持zoomfocusMode,但并不是所有设备都支持。我建议先检测一下:

// 检测是否支持缩放
async function checkZoomSupport() {
  const stream = await navigator.mediaDevices.getUserMedia({ video: true });
  const track = stream.getVideoTracks()[0];
  const capabilities = track.getCapabilities();
  
  if (capabilities.zoom) {
    console.log('支持缩放,范围:', capabilities.zoom.min, '-', capabilities.zoom.max);
    return true;
  }
  return false;
}

// 设置缩放
function setZoom(level) {
  const track = localStream.getVideoTracks()[0];
  if (track.getCapabilities().zoom) {
    track.applyConstraints({
      advanced: [{ zoom: level }]
    });
  }
}

对焦的话,大部分移动设备默认是自动对焦。如果你想手动对焦,可以用focusModefocusDistance。不过说实话,我在Web端很少看到有人做手动对焦,因为用户体验不太好做。

闪光灯控制

闪光灯控制,其实就是控制torch模式。注意,只有后置摄像头才支持闪光灯:

async function toggleTorch() {
  const track = localStream.getVideoTracks()[0];
  const capabilities = track.getCapabilities();
  
  if (!capabilities.torch) {
    console.warn('当前设备不支持闪光灯');
    return;
  }
  
  try {
    await track.applyConstraints({
      advanced: [{ torch: !torchEnabled }]
    });
    torchEnabled = !torchEnabled;
  } catch (err) {
    console.error('控制闪光灯失败:', err);
  }
}

小技巧:在调用applyConstraints之前,先检查getCapabilities()。这样可以避免在不支持的设备上抛出异常。我习惯在页面初始化时就做一次能力检测,把结果缓存起来。

移动端适配的核心逻辑

说了这么多,我画了一张图,帮你梳理移动端适配的核心逻辑:

移动端WebRTC适配核心逻辑 浏览器兼容性检测 Safari / Chrome / UC / QQ / 华为浏览器 触摸事件处理 touchstart / click / 事件穿透 / 双击缩放 / 长按菜单 摄像头控制 前后切换 / 对焦缩放 / 闪光灯 / 能力检测 每一层都依赖上一层的正确实现,缺一不可

从这张图你可以看到,移动端适配是一个层层递进的过程。先搞定浏览器兼容性,再处理好触摸事件,最后才是摄像头控制。每一层都依赖上一层的正确实现,缺一不可。

好了,这一章的内容就到这里。移动端适配确实有不少细节,但只要你掌握了这些核心要点,大部分场景都能应对。如果你在实际开发中遇到什么奇怪的问题,欢迎随时交流。


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