最新版代码更新到https://github.com/MotorBottle/HtmlUSBVideoViewer
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<title>USB / UVC 采集卡预览</title>
<style>
body{margin:0;font-family:system-ui,Helvetica,Arial,sans-serif;background:#101010;color:#eaeaea;display:flex;flex-direction:column;height:100vh}
#controls{display:flex;gap:8px;padding:8px;background:#222;align-items:center;flex-wrap:wrap}
select,button{padding:6px 10px;font-size:14px;border-radius:4px;border:none}
button{cursor:pointer;background:#2964ff;color:#fff}
button:hover{background:#1e54e6}
video{flex:1 1 auto;width:100%;height:100%;object-fit:contain;background:#000}
</style>
</head>
<body>
<div id="controls">
<label for="videoSource">视频输入源:</label>
<select id="videoSource"></select>
<button id="fullScreenBtn">进入全屏</button>
</div>
<video id="video" autoplay playsinline></video>
<script>
(async () => {
// DOM
const video = document.getElementById('video');
const select = document.getElementById('videoSource');
const fsBtn = document.getElementById('fullScreenBtn');
const SAVEKEY = 'selectedDeviceId';
// 1. 确保支持
if (!navigator.mediaDevices?.enumerateDevices) {
alert('当前浏览器不支持 WebRTC,无法访问摄像头');
return;
}
// 2. 若无任何权限,先最小化地申请一次,避免空列表
async function ensurePermission() {
try {
await navigator.mediaDevices.getUserMedia({video:true, audio:false});
} catch (e) {
console.error('未能取得摄像头权限:', e);
alert('需要摄像头权限才能列出设备,请允许后刷新页面');
throw e;
}
}
// 3. 列举并填充下拉框
async function populate() {
const devices = await navigator.mediaDevices.enumerateDevices();
const cams = devices.filter(d => d.kind === 'videoinput');
select.innerHTML = '';
cams.forEach((cam, i) => {
const opt = document.createElement('option');
opt.value = cam.deviceId;
opt.textContent = cam.label || `Camera ${i+1}`;
select.appendChild(opt);
});
// 恢复首选
const saved = localStorage.getItem(SAVEKEY);
if (saved && cams.some(c => c.deviceId === saved)) select.value = saved;
// 自动开流
startStream(select.value || cams[0]?.deviceId);
}
// 4. 启动视频流
async function startStream(id) {
if (!id) { console.warn('无可用摄像头'); return; }
const stream = await navigator.mediaDevices.getUserMedia({
video:{deviceId:{exact:id}, width:{ideal:1920}, height:{ideal:1080}, frameRate:{ideal:60}},
audio:false
});
video.srcObject = stream;
localStorage.setItem(SAVEKEY, id);
}
// 5. 事件绑定
select.onchange = () => startStream(select.value);
fsBtn.onclick = () =>
document.fullscreenElement ? document.exitFullscreen()
: document.documentElement.requestFullscreen().catch(console.error);
document.addEventListener('fullscreenchange', () => {
fsBtn.textContent = document.fullscreenElement ? '退出全屏' : '进入全屏';
});
// 6. 设备插拔动态刷新
navigator.mediaDevices.addEventListener('devicechange', populate);
// == 启动流程 ==
await ensurePermission();
await populate();
})();
</script>
</body>
</html>