你的代码实现了以下功能:
枚举视频设备:通过 enumerateDevices()
获取所有可用摄像头。
设备选择策略:优先选择第一个摄像头,若失败则尝试最后一个。
视频流获取:通过 getUserMedia()
按指定分辨率和帧率拉取视频流。
重试机制:使用 setInterval
每隔500ms重试一次,直到成功或失败。
问题 | 说明 |
---|---|
性能浪费 | setInterval 频繁调用 getUserMedia() ,即使设备不可用也会持续消耗资源。 |
错误处理不完善 | 未区分权限拒绝、设备冲突等错误类型,可能导致死循环或用户困惑。 |
设备切换逻辑缺陷 | 仅尝试最后一个设备,未考虑设备列表为空或设备不可用的情况。 |
移动端兼容性问题 | 移动端浏览器可能要求用户手势触发 getUserMedia() ,直接调用可能失败。 |
setInterval
为异步重试使用 async/await
和递归重试代替轮询,减少资源占用:
async function getVideoStreamWithRetry(constrains, retryCount = 3, delay = 500) {
try {
const stream = await navigator.mediaDevices.getUserMedia(constrains);
return stream;
} catch (err) {
if (retryCount <= 0) throw err;
await new Promise(resolve => setTimeout(resolve, delay));
return getVideoStreamWithRetry(constrains, retryCount - 1, delay);
}
}
调用示例:
try {
const stream = await getVideoStreamWithRetry(constrains);
video.srcObject = stream;
} catch (err) {
console.error("获取视频流失败:", err);
}
通过 err.name
区分具体错误类型,提供针对性反馈:
try {
const stream = await navigator.mediaDevices.getUserMedia(constrains);
// 成功处理
} catch (err) {
switch (err.name) {
case 'NotAllowedError':
alert("请手动允许摄像头权限。");
break;
case 'NotFoundError':
alert("未找到可用摄像头,请连接设备。");
break;
case 'NotReadableError':
alert("摄像头正被其他程序占用。");
break;
default:
alert("未知错误:" + err.message);
}
}
优化设备切换逻辑,优先尝试可用设备:
const devices = await navigator.mediaDevices.enumerateDevices();
const videoDevices = devices.filter(device => device.kind === 'videoinput');
if (videoDevices.length === 0) {
alert("未检测到摄像头设备。");
return;
}
// 尝试所有设备,直到成功或全部失败
for (const device of videoDevices) {
const constrains = {
video: {
width: 800,
height: 700,
frameRate: 30,
deviceId: { ideal: device.deviceId } // 使用 ideal 替代强制匹配
},
audio: false
};
try {
const stream = await navigator.mediaDevices.getUserMedia(constrains);
video.srcObject = stream;
return; // 成功则退出
} catch (err) {
console.warn(`设备 ${device.label} 加载失败:`, err);
}
}
alert("所有摄像头设备均无法使用。");
移动端浏览器(如 iOS Safari)要求 getUserMedia()
必须由用户手势(点击、触摸)触发。
解决方案:在按钮点击事件中调用 getUserMedia()
:
document.getElementById('startButton').addEventListener('click', async () => {
const stream = await navigator.mediaDevices.getUserMedia(constrains);
video.srcObject = stream;
});
async function initCamera() {
try {
const devices = await navigator.mediaDevices.enumerateDevices();
const videoDevices = devices.filter(device => device.kind === 'videoinput');
if (videoDevices.length === 0) {
alert("未检测到摄像头设备。");
return;
}
for (const device of videoDevices) {
const constrains = {
video: {
width: { ideal: 800 },
height: { ideal: 700 },
frameRate: { ideal: 30 },
deviceId: { ideal: device.deviceId }
},
audio: false
};
try {
const stream = await navigator.mediaDevices.getUserMedia(constrains);
const video = document.getElementById('videoCammera');
video.srcObject = stream;
video.play();
mediaStream.value = stream;
return; // 成功则退出
} catch (err) {
console.warn(`设备 ${device.label} 加载失败:`, err);
}
}
alert("所有摄像头设备均无法使用。");
} catch (err) {
switch (err.name) {
case 'NotAllowedError':
alert("请手动允许摄像头权限。");
break;
default:
alert("初始化摄像头失败:" + err.message);
}
}
}
优先使用异步递归重试:替代 setInterval
,提升性能与可读性。
完善错误类型处理:区分权限、设备冲突等错误,提供明确用户提示。
动态设备选择策略:尝试所有可用设备,避免单一设备失败导致全流程中断。
移动端手势触发:确保在用户交互后调用 getUserMedia()
,避免移动端限制。
约束条件灵活化:使用 ideal
替代 exact
,适应不同设备的能力差异。
通过以上优化,你的摄像头视频流加载逻辑将更加健壮、高效,适配更多复杂场景。