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

ops/2024/12/22 11:00:27/

 现在有些手机更新的很激进,但是却没有很好的实现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();
}

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

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

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[];// 检查下一个可用idxconst ne = vlist.find((e, idx) => !failList.includes(idx) && e.deviceId !== curId);return ne?.deviceId;
};

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

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

4.3 拉流

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

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

 5 小结

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

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

YU.H


http://www.ppmy.cn/ops/123078.html

相关文章

C#使用Lazy<T>提高性能

以下是一些适合使用Lazy<T>的场景&#xff1a; 单例模式 在实现单例模式时&#xff0c;Lazy<T>是非常有用的。如前面提到的示例&#xff0c;它可以确保单例对象在首次被访问时才进行创建&#xff0c;同时在多线程环境下也能保证正确的行为。这种方式比传统的双重检…

Linux的图形系统概述 (TODO)

&#xff08;TODO&#xff09; Linux graphics stack 现代 Linux 图形栈由多个子系统和层次组成&#xff0c;从应用程序到硬件之间的各个层面协同工作来处理图形显示和硬件加速。随着时间的推移&#xff0c;Linux 从传统的 **X Window System** 逐步过渡到 **Wayland**&#x…

Android 安装过程五 MSG_INSTALL消息的处理 安装

现在马上进入正式的安装流程。   从前面文章 Android 安装过程四 MSG_INSTALL消息的处理 安装之前的验证知道&#xff0c;在验证之后没有什么问题的情况下&#xff0c;会回调onVerificationComplete()方法&#xff0c;它位于PackageInstallerSession类中。 private void onVe…

Redis操作(三)RedisTemplate(2)StringRedisTemplate常见API

RedisTemplate 提供了丰富的方法来实现对 Redis 的各种操作&#xff0c;包括但不限于字符串、哈希、列表、集合和有序集合等数据结构的操作。以下是一些常用的 RedisTemplate API&#xff1a; 一、字符串操作 opsForValue().set(key, value): 设置字符串值。opsForValue().ge…

Debezium系列之:Debezium 3.0.0.Final发布

Debezium系列之:Debezium 3.0.0.Final发布 Debezium 核心的变化需要 Java 17基于Kafka 3.8 构建废弃的增量信号字段的删除每个表的详细指标MariaDB连接器的更改版本 11.4.3 支持MongoDB连接器的更改MongoDB sink connectorMySQL连接器的改变MySQL 9MySQL向量数据类型Oracle连接…

浅析Golang的Context

文章目录 1. 简介2. 常见用法2.1 控制goroutine的生命周期&#xff08;cancel&#xff09;2.2 传递超时&#xff08;Timeout&#xff09;信息2.3 传递截止时间&#xff08;Deadline&#xff09;2.4 传递请求范围内的全局数据 &#xff08;value&#xff09; 3 特点3.1 上下文的…

python 实现Greedy Best First Search最佳优先搜索算法

Greedy Best First Search最佳优先搜索算法介绍 Greedy Best First Search&#xff08;GBFS&#xff0c;贪婪最佳优先搜索&#xff09;是一种启发式搜索算法&#xff0c;用于在图或树形结构中寻找从起始节点到目标节点的最优路径。该算法的基本思想是在每一步选择当前看起来最…

Webpack 打包后文件过大,如何优化?

聚沙成塔每天进步一点点 本文回顾 ⭐ 专栏简介Webpack 打包后文件过大&#xff0c;如何优化&#xff1f;1. 代码分割&#xff08;Code Splitting&#xff09;1.1 概念1.2 Webpack 的 SplitChunksPlugin示例配置&#xff1a; 1.3 按需加载&#xff08;Lazy Loading&#xff09;示…