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