现在有些手机更新的很激进,但是却没有很好的实现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[]; // 检查下一个可用idx const 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规范,导致会出一些问题,若是都根据规范来实现接口,我们的工作会轻松挺多!