拿下奇怪的前端报错(六):多摄手机webrtc拉取视频流会导致应用崩溃,从而无法进行人像扫描

本文涉及的产品
视觉智能开放平台,视频资源包5000点
视觉智能开放平台,图像资源包5000点
视觉智能开放平台,分割抠图1万点
简介: 本文介绍了一种解决手机摄像头切换导致应用崩溃的问题的方法。针对不支持facingMode配置的四摄手机,通过缓存和序号切换的方式,确保应用在特定设备上不会频繁崩溃,提升用户体验。

现在有些手机更新的很激进,但是却没有很好的实现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的关键字,并测试出不会崩溃的序号作为备选项

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();
}

image.gif

3.3 更新测试完的设备标记

  • 场景一,应用启动时,curId为空,通过读取缓存是否存在正在检测的id,如果有则表明这个index的摄像头导致了崩溃(当然还有一种情况,就是用户拉流过程手动退出应用了)
  • 场景二,拉流成功,调用这个方法标记成功的设备id
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));
};

image.gif

3.4 一些辅助方法

// 检查是否需要兼容模式 - 不然可用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();
}

image.gif

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

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;
};

image.gif

4 使用示例

4.1 切换相机参数更新

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

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

image.gif

4.2 抽离拉流配置方法

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

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,
  };
}

image.gif

4.3 拉流

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

const config = getMediaConfig();
  console.log("camera", config);
return navigator.mediaDevices
    .getUserMedia(config)

image.gif

5 小结

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


相关文章
|
6天前
|
JavaScript 前端开发 测试技术
前端全栈之路Deno篇(五):如何快速创建 WebSocket 服务端应用 + 客户端应用 - 可能是2025最佳的Websocket全栈实时应用框架
本文介绍了如何使用Deno 2.0快速构建WebSocket全栈应用,包括服务端和客户端的创建。通过一个简单的代码示例,展示了Deno在WebSocket实现中的便捷与强大,无需额外依赖,即可轻松搭建具备基本功能的WebSocket应用。Deno 2.0被认为是最佳的WebSocket全栈应用JS运行时,适合全栈开发者学习和使用。
|
1天前
|
前端开发 API UED
深入理解微前端架构:构建灵活、高效的前端应用
【10月更文挑战第23天】微前端架构是一种将前端应用分解为多个小型、独立、可复用的服务的方法。每个服务独立开发和部署,但共同提供一致的用户体验。本文探讨了微前端架构的核心概念、优势及实施方法,包括定义服务边界、建立通信机制、共享UI组件库和版本控制等。通过实际案例和职业心得,帮助读者更好地理解和应用微前端架构。
|
6天前
|
人工智能 资源调度 数据可视化
【AI应用落地实战】智能文档处理本地部署——可视化文档解析前端TextIn ParseX实践
2024长沙·中国1024程序员节以“智能应用新生态”为主题,吸引了众多技术大咖。合合信息展示了“智能文档处理百宝箱”的三大工具:可视化文档解析前端TextIn ParseX、向量化acge-embedding模型和文档解析测评工具markdown_tester,助力智能文档处理与知识管理。
|
7天前
|
前端开发 API UED
拥抱微前端架构:构建灵活、高效的前端应用
【10月更文挑战第17天】微前端架构是一种将前端应用拆分为多个小型、独立、可复用的服务的方法,每个服务可以独立开发、部署和维护。本文介绍了微前端架构的核心概念、优势及实施步骤,并分享了业界应用案例和职业心得,帮助读者理解和应用这一新兴架构模式。
|
7天前
|
前端开发 JavaScript Docker
拿下奇怪的前端报错(五):SyntaxError: Unexpected token ‘??=‘或‘xxx‘ - 基于容器搭建开发环境或许是更好的选择
在前端开发中,同时维护多个项目时可能会遇到不同Node.js版本的问题。低版本Node.js可能导致依赖无法安装或启动失败,而高版本Node.js则可能引起第三方库的兼容性问题。推荐使用Docker搭建独立的开发环境,以避免版本不一致带来的困扰。
239 1
|
12天前
|
存储 监控 前端开发
掌握微前端架构:构建未来前端应用的基石
【10月更文挑战第12天】随着前端技术的发展,传统的单体应用架构已无法满足现代应用的需求。微前端架构通过将大型应用拆分为独立的小模块,提供了更高的灵活性、可维护性和快速迭代能力。本文介绍了微前端架构的概念、核心优势及实施步骤,并探讨了其在复杂应用中的应用及实战技巧。
|
13天前
|
存储 人工智能 前端开发
前端大模型应用笔记(三):Vue3+Antdv+transformers+本地模型实现浏览器端侧增强搜索
本文介绍了一个纯前端实现的增强列表搜索应用,通过使用Transformer模型,实现了更智能的搜索功能,如使用“番茄”可以搜索到“西红柿”。项目基于Vue3和Ant Design Vue,使用了Xenova的bge-base-zh-v1.5模型。文章详细介绍了从环境搭建、数据准备到具体实现的全过程,并展示了实际效果和待改进点。
|
13天前
|
JavaScript 前端开发 程序员
前端学习笔记——node.js
前端学习笔记——node.js
30 0
|
13天前
|
人工智能 自然语言处理 运维
前端大模型应用笔记(一):两个指令反过来说大模型就理解不了啦?或许该让第三者插足啦 -通过引入中间LLM预处理用户输入以提高多任务处理能力
本文探讨了在多任务处理场景下,自然语言指令解析的困境及解决方案。通过增加一个LLM解析层,将复杂的指令拆解为多个明确的步骤,明确操作类型与对象识别,处理任务依赖关系,并将自然语言转化为具体的工具命令,从而提高指令解析的准确性和执行效率。
|
13天前
|
存储 弹性计算 算法
前端大模型应用笔记(四):如何在资源受限例如1核和1G内存的端侧或ECS上运行一个合适的向量存储库及如何优化
本文探讨了在资源受限的嵌入式设备(如1核处理器和1GB内存)上实现高效向量存储和检索的方法,旨在支持端侧大模型应用。文章分析了Annoy、HNSWLib、NMSLib、FLANN、VP-Trees和Lshbox等向量存储库的特点与适用场景,推荐Annoy作为多数情况下的首选方案,并提出了数据预处理、索引优化、查询优化等策略以提升性能。通过这些方法,即使在资源受限的环境中也能实现高效的向量检索。

热门文章

最新文章