const connection = document.querySelector("#connection"); const detectionsEl = document.querySelector("#detections"); const frameIdEl = document.querySelector("#frame-id"); const fpsEl = document.querySelector("#fps"); const errorEl = document.querySelector("#error"); const sourceEl = document.querySelector("#source"); const deviceSelect = document.querySelector("#device-select"); const timingTokenEl = document.querySelector("#timing-token"); const timingSignEl = document.querySelector("#timing-sign"); const timingUrlEl = document.querySelector("#timing-url"); const timingOpenEl = document.querySelector("#timing-open"); const timingFrameEl = document.querySelector("#timing-frame"); let selectedDevice = ""; let pendingDevice = ""; let queuedDevice = ""; let switching = false; let devicesSignature = ""; let lastWsSignature = ""; function setConnection(online, text) { connection.textContent = text; connection.classList.toggle("online", online); connection.classList.toggle("offline", !online); } function formatMs(value) { if (value === undefined || value === null || value === 0) { return "-"; } return `${Number(value).toFixed(2)} ms`; } function renderDevices(devices, currentDeviceNum) { if (!devices.length) { deviceSelect.innerHTML = ''; deviceSelect.disabled = true; return; } const displayDevice = pendingDevice || currentDeviceNum; const nextSignature = `${displayDevice}|${devices.map((device) => device.device_num).join(",")}`; if (nextSignature === devicesSignature) { return; } selectedDevice = displayDevice; devicesSignature = nextSignature; deviceSelect.innerHTML = devices .map((device) => { const selected = device.device_num === displayDevice ? "selected" : ""; return ``; }) .join(""); } function renderTimings(timings) { timingTokenEl.textContent = timings?.token_cache ? "缓存" : formatMs(timings?.token_ms); timingSignEl.textContent = formatMs(timings?.sign_ms); timingUrlEl.textContent = formatMs(timings?.stream_url_ms); timingOpenEl.textContent = formatMs(timings?.open_ms); timingFrameEl.textContent = formatMs(timings?.first_frame_ms); } function renderDetections(detections) { if (!detections.length) { detectionsEl.className = "detections empty"; detectionsEl.textContent = "暂无目标"; return; } detectionsEl.className = "detections"; detectionsEl.innerHTML = detections .map((det) => { const score = `${(det.score * 100).toFixed(1)}%`; const box = det.box.join(", "); return `
${det.label} ${score}
box: [${box}]
`; }) .join(""); } async function performSwitch(deviceNum) { switching = true; pendingDevice = deviceNum; devicesSignature = ""; setConnection(false, "切换中"); console.log("[device-switch] start", { deviceNum }); try { const response = await fetch(`/devices/${encodeURIComponent(deviceNum)}`, { method: "POST" }); if (!response.ok) { throw new Error("切换摄像头失败"); } const result = await response.json(); const video = document.querySelector("#video"); if (video) { video.src = `/video?t=${Date.now()}`; } document.querySelectorAll(".grid-video").forEach((item) => { item.src = `${item.dataset.src}?t=${Date.now()}`; }); console.log("[device-switch] requested", { deviceNum, version: result.version }); } catch (error) { pendingDevice = ""; devicesSignature = ""; setConnection(false, "切换失败"); console.error("[device-switch] failed", { deviceNum, error }); } finally { switching = false; if (queuedDevice && queuedDevice !== deviceNum) { const nextDevice = queuedDevice; queuedDevice = ""; return performSwitch(nextDevice); } queuedDevice = ""; } } function switchDevice(deviceNum) { if (switching) { queuedDevice = deviceNum; pendingDevice = deviceNum; devicesSignature = ""; setConnection(false, "等待切换"); console.log("[device-switch] queued", { deviceNum }); return Promise.resolve(); } return performSwitch(deviceNum); } function connectWebSocket() { const protocol = window.location.protocol === "https:" ? "wss" : "ws"; const ws = new WebSocket(`${protocol}://${window.location.host}/ws/detections`); ws.addEventListener("open", () => setConnection(true, "已连接")); ws.addEventListener("message", (event) => { const data = JSON.parse(event.data); frameIdEl.textContent = data.frame_id ?? "-"; fpsEl.textContent = data.fps ?? "-"; errorEl.textContent = data.error || (data.connected ? "正常" : "未连接"); sourceEl.textContent = data.source || "-"; setConnection(Boolean(data.connected), data.connected ? "已连接" : "重连中"); const wsSignature = `${data.current_device_num}|${data.connected}|${data.frame_id}|${data.error || ""}`; if (wsSignature !== lastWsSignature) { lastWsSignature = wsSignature; console.log("[device-switch] ws", { currentDeviceNum: data.current_device_num, pendingDevice, queuedDevice, connected: data.connected, frameId: data.frame_id, error: data.error, }); } if (pendingDevice && data.current_device_num === pendingDevice) { pendingDevice = ""; deviceSelect.disabled = false; } renderDevices(data.devices || [], data.current_device_num || ""); renderTimings(data.timings || {}); renderDetections(data.detections || []); }); ws.addEventListener("close", () => { setConnection(false, "已断开"); setTimeout(connectWebSocket, 1500); }); ws.addEventListener("error", () => { setConnection(false, "连接错误"); ws.close(); }); } deviceSelect.addEventListener("change", (event) => { selectedDevice = event.target.value; switchDevice(event.target.value).catch(() => { pendingDevice = ""; devicesSignature = ""; setConnection(false, "切换失败"); deviceSelect.disabled = false; }); }); connectWebSocket();