拿下奇怪的前端报错:某些多摄手机拉取部分摄像头视频流会导致应用崩溃,该如何改善呢?

现在有些手机更新的很激进,但是却没有很好的实现web规范,不支持facingMode配置来控制前后摄像头,只能根据序号切换,但拉取到某些设备的流会导致应用崩溃,这里就教一招如何尽可能的改善用户体验

至少不至于次次都崩溃,最多崩溃三次后就不崩了(╮(╯▽╰)╭)

1 问题上下文

  • 四摄手机:UA携带ELS-AN00标识
  • 混合应用:基于webview开发
  • 不支持facingMode切换前后摄像头
    • 支持的话就不要用这种方法了,如果做的是面向大众用户,需要多测试不同型号

2. 问题描述 - 非主摄拉流则崩溃

因为不支持facingMode配置来控制前后摄像头,所以只能采用枚举设备id,切换序号的方式来实现相机切换,但一旦拉流时选择的id是非主摄(后摄的子镜头),则会导致应用崩溃退出

3. 解决过程

这个问题比较棘手,触发崩溃需要重启而且导致切换相机功能不可用。崩溃也是比较严重的问题,而且切换必会触发崩溃,又由于是设备不支持对应api,于是只能想出一个临时的解决办法

优选一对index,经测试是 序号 0,4的摄像头--但有测试出某多摄并非这两个序号,只能是最大程度保证体验:不让应用在会导致崩溃的相机上崩溃2次

3.1 方案设计

本地缓存两个数组:可用indexs、失败indexs,每次拉流前标记选择的index,拉流成功删除这个标记并加入到可用列表,开机时检测有无这个标记,有的话加入失败列表。

3.2 初始化UA匹配

需要使用设备调试找出UA的关键字,并测试出不会崩溃的序号作为备选项

TypeScript 复制代码
const fallbackMap = new Map<string, number[]>();
// 加入需要执行回退方案的map
fallbackMap.set("ELS-AN00", [0, 4]);
let preferFallback = false;
let preferIndexs = [0, 4];
{
  const ua = navigator.userAgent;
  for (const key of fallbackMap.keys()) {
    if (ua.includes(key)) {
      preferFallback = true;
      preferIndexs = fallbackMap.get(key)!;
      console.info(`hite ${key}`);
      break;
    }
  }
}
// 如果ua匹配上,查看是否有需要标记失败的
if (shouldCameraFallback()) {
  markDeviceIdTested();
}

3.3 更新测试完的设备标记

  • 场景一,应用启动时,curId为空,通过读取缓存是否存在正在检测的id,如果有则表明这个index的摄像头导致了崩溃(当然还有一种情况,就是用户拉流过程手动退出应用了)
  • 场景二,拉流成功,调用这个方法标记成功的设备id
TypeScript 复制代码
export const markDeviceIdTested = async (curId?: string) => {
  const vlist = await getVideoDevices();
  if (!vlist) {
    return;
  }
  const checkingDevice = localStorage.getItem("capture_checking_device");
  const sucList = getSavedList(
    "capture_success_list",
    preferIndexs
  ) as number[];
  const cidx = vlist.findIndex((v) => v.deviceId === curId);
  if (
    (checkingDevice && checkingDevice === curId) ||
    (!checkingDevice && curId)
  ) {
    if (!sucList.includes(cidx)) {
      sucList.push(cidx);
      localStorage.setItem("capture_success_list", JSON.stringify(sucList));
    }
    localStorage.removeItem("capture_checking_device");
    return;
  }
  if (!checkingDevice) {
    return;
  }
  const failList = getSavedList("capture_fail_list", []) as number[];
  failList.push(cidx);
  localStorage.setItem("capture_fail_list", JSON.stringify(failList));
};

3.4 一些辅助方法

TypeScript 复制代码
// 检查是否需要兼容模式 - 不然可用facingMode控制相机切换
export const shouldCameraFallback = () => {
  return !!localStorage.getItem("camera_fallback_used") || preferFallback;
}
// 根据限制获取设备id
export const getDeviceId = (v?: ConstrainDOMString): string | undefined => {
  return Array.isArray(v)
    ? v[0]
    : typeof v === "object"
    ? getDeviceId(v.exact)
    : v;
}
// 拉流前标记正在检测的设备id
export const markDeviceIdTest = (deviceId: string) => {
  localStorage.setItem("capture_checking_device", deviceId);
}

// 从localstorage读取列表数据
function getSavedList (key: string, def: any) {
  const str = localStorage.getItem(key);
  try {
    const e = JSON.parse(str || "");
    return e || def;
  } catch (error) {
    return def;
  }
}
// 获取摄像头列表
async function getVideoDevices ()  {
  // **************下面的内容需要自己调整,设备枚举前要先获取权限不然获取不到设备列表
  await permission.checkVideoPermission();
  const list = await navigator.mediaDevices.enumerateDevices();
  return list.filter((e) => e.kind === "videoinput").sort();
}

3.5 获取下一个设备id - 切换相机的实现

TypeScript 复制代码
export const getNextDeviceId = async (curId: string) => {
  // ***** 需要自己调整下面的相机权限获取方法
  await permission.checkVideoPermission();
  const list = await navigator.mediaDevices.enumerateDevices();
  const vlist = list.filter((e) => e.kind === "videoinput");
  let sucList = getSavedList("capture_success_list", preferIndexs) as number[];
  let cidx = vlist.findIndex((v) => v.deviceId === curId);
  if (cidx === -1) {
    cidx = 0;
    localStorage.setItem("capture_success_list", JSON.stringify(preferIndexs));
    sucList = preferIndexs;
  }
  if (sucList.includes(cidx)) {
    // 如果成功列表有两个摄像头了,就用这俩,就不会发生崩溃啦!!
    if (sucList.length > 1) {
      const lidx = sucList.indexOf(cidx);
      const nSucIdx = lidx === sucList.length - 1 ? 0 : lidx + 1;
      const nIdx = sucList[nSucIdx];
      const dev = vlist[nIdx];
      console.log(`use sucList ${nSucIdx} - realIndex ${nIdx} `);
      return dev.deviceId;
    }
  }
  const failList = getSavedList("capture_fail_list", []) as number[];
  // 检查下一个可用idx
  const ne = vlist.find(
    (e, idx) => !failList.includes(idx) && e.deviceId !== curId
  );
  return ne?.deviceId;
};

4 使用示例

4.1 切换相机参数更新

核心就是兼容模式则使用deviceId,否则使用facingMode

TypeScript 复制代码
const updateSwitchParams = async () => {
  if (!shouldCameraFallback()) {
    facingMode = facingMode === "user" ? "environment" : "user";
    return;
  }
  await markDeviceIdTested(deviceId);
  deviceId = (await getNextDeviceId(deviceId)) || "";
  if (!deviceId) {
    d.canSwitch = false;
  }
};

4.2 抽离拉流配置方法

兼容模式时用deviceId控制拉流的摄像头,其它则使用facingMode

TypeScript 复制代码
const getMediaConfig = () => {
  const video = {
    width: { min: 640, ideal: 1280 },
    height: { min: 480, ideal: 720 },
  } as any;
  const audio = mode === 2;
  if (shouldCameraFallback()) {
    if (deviceId) {
      video.deviceId = deviceId;
    }
    markDeviceIdTest(deviceId);
    return {
      video,
      audio,
    };
  }
  if (facingMode) {
    video.facingMode = facingMode;
  }
  if (deviceId && deviceIdFaceMode === facingMode) {
    video.deviceId = deviceId;
  }
  return {
    video,
    audio,
  };
}

4.3 拉流

就是简单的使用getUserMedia,不过参数是动态获取的,当使用特定设备时会使用回退设置。切换摄像头,只需要调用updateSwitchParams 即可

TypeScript 复制代码
const config = getMediaConfig();
  console.log("camera", config);
return navigator.mediaDevices
    .getUserMedia(config)

5 小结

本文介绍的是一个很特殊的场景的一个解决办法,因为现在有些手机挺激进的,三摄像头四摄像头,但是却没有很好的实现web规范,导致会出一些问题,若是都根据规范来实现接口,我们的工作会轻松挺多!

因为心里想着下班放假,就不补充太多细节了,有疑问的可以留言~

YU.H

相关推荐
web150850966413 小时前
【React&前端】大屏适配解决方案&从框架结构到实现(超详细)(附代码)
前端·react.js·前端框架
理想不理想v3 小时前
前端项目性能优化(详细)
前端·性能优化
CodeToGym3 小时前
使用 Vite 和 Redux Toolkit 创建 React 项目
前端·javascript·react.js·redux
Cachel wood4 小时前
Vue.js前端框架教程8:Vue消息提示ElMessage和ElMessageBox
linux·前端·javascript·vue.js·前端框架·ecmascript
PP东5 小时前
ES6学习Generator 函数(生成器)(八)
javascript·学习·es6
桃园码工6 小时前
4_使用 HTML5 Canvas API (3) --[HTML5 API 学习之旅]
前端·html5·canvas
桃园码工6 小时前
9_HTML5 SVG (5) --[HTML5 API 学习之旅]
前端·html5·svg
人才程序员6 小时前
QML z轴(z-order)前后层级
c语言·前端·c++·qt·软件工程·用户界面·界面
m0_548514776 小时前
前端三大主流框架:React、Vue、Angular
前端·vue.js·react.js
m0_748232397 小时前
单页面应用 (SPA):现代 Web 开发的全新视角
前端