// 视频实时监控页面脚本 let streams = []; let detectionStatus = {}; let processedFrames = {}; // 存储处理后的帧 // 加载视频流列表 async function loadStreams() { try { const res = await fetch('/streams'); const data = await res.json(); if (data.code === 0) { streams = data.data; document.getElementById('cameraCount').textContent = streams.length; renderVideos(); } } catch (e) { addLog('加载视频流失败: ' + e.message, 'error'); } } // 加载检测状态 async function loadStatus() { try { const res = await fetch('/status'); const data = await res.json(); if (data.code === 0) { document.getElementById('checkInterval').textContent = data.data.check_interval; } } catch (e) { console.error('加载状态失败:', e); } } // 渲染视频卡片 function renderVideos() { const container = document.getElementById('videoContainer'); if (streams.length === 0) { container.innerHTML = '
暂无视频流
'; return; } container.innerHTML = streams.map(stream => `
${stream.app} / ${stream.stream}
无人 ${stream.resolution} @ ${stream.fps}fps
原始视频
检测状态 正常
最后检测 -
码率 ${formatBytes(stream.bytesSpeed)}/s
协议 JPEG帧
`).join(''); // 初始化所有视频流 streams.forEach(stream => initVideo(stream)); } // 切换原始/处理后画面 function toggleProcess(streamKey, type) { const rawFrame = document.getElementById(`frame-raw-${streamKey}`); const processedFrame = document.getElementById(`frame-processed-${streamKey}`); const rawBtn = document.getElementById(`btn-raw-${streamKey}`); const processedBtn = document.getElementById(`btn-processed-${streamKey}`); const stream = streams.find(s => s.key === streamKey); if (!stream) return; if (type === 'raw') { rawFrame.classList.remove('hidden'); processedFrame.classList.add('hidden'); rawBtn.classList.add('active'); processedBtn.classList.remove('active'); // 立即刷新原始帧 rawFrame.src = `/proxy/zlm/snap?app=${stream.app}&stream=${stream.stream}&t=${Date.now()}`; } else { rawFrame.classList.add('hidden'); processedFrame.classList.remove('hidden'); rawBtn.classList.remove('active'); processedBtn.classList.add('active'); // 立即刷新处理后帧 processedFrame.src = `/proxy/zlm/snap?app=${stream.app}&stream=${stream.stream}&t=${Date.now()}&processed=1`; } } // 初始化单个视频 function initVideo(stream) { const rawFrame = document.getElementById(`frame-raw-${stream.key}`); const processedFrame = document.getElementById(`frame-processed-${stream.key}`); const canvas = document.getElementById(`roi-${stream.key}`); // 加载并绘制ROI区域 loadAndDrawROI(stream.key, canvas); // 开始拉取视频帧 startFramePolling(stream.key, rawFrame, processedFrame); } // 轮询获取视频帧 function startFramePolling(streamKey, rawFrame, processedFrame) { // 使用 ZLM 的截图API获取帧 const stream = streams.find(s => s.key === streamKey); if (!stream) { console.error('未找到流:', streamKey); return; } // 构建截图URL (使用 ZLM 的 getSnap 接口) const snapUrl = `/proxy/zlm/snap?app=${stream.app}&stream=${stream.stream}`; console.log('开始轮询截图:', streamKey, 'URL:', snapUrl); // 添加图片加载错误处理 rawFrame.onerror = function() { console.error('原始帧加载失败:', streamKey); }; processedFrame.onerror = function() { console.error('处理后帧加载失败:', streamKey); }; rawFrame.onload = function() { console.log('原始帧加载成功:', streamKey); }; // 定期刷新帧 let lastTimestamp = 0; const refreshFrame = () => { try { // 添加时间戳防止缓存 const timestamp = Date.now(); if (timestamp - lastTimestamp < 100) { return; // 防止过于频繁的刷新 } lastTimestamp = timestamp; // 根据当前显示状态刷新对应的帧 if (!rawFrame.classList.contains('hidden')) { rawFrame.src = `${snapUrl}&t=${timestamp}`; } if (!processedFrame.classList.contains('hidden')) { processedFrame.src = `${snapUrl}&t=${timestamp}&processed=1`; } } catch (e) { console.error('获取帧失败:', e); } }; // 先加载一次 refreshFrame(); // 每500ms刷新一次 (2fps) - 降低频率避免过载 const interval = setInterval(refreshFrame, 500); // 保存interval以便清理 rawFrame.dataset.interval = interval; } // 加载并绘制ROI async function loadAndDrawROI(streamKey, canvas) { try { const res = await fetch('/getRoi?stream_key=' + streamKey); const data = await res.json(); if (data.code === 0 && data.data) { drawROI(canvas, data.data); } } catch (e) { console.error('加载ROI失败:', e); } } // 绘制ROI区域 function drawROI(canvas, points) { const ctx = canvas.getContext('2d'); const rect = canvas.getBoundingClientRect(); canvas.width = rect.width; canvas.height = rect.height; // 坐标转换(假设原始坐标是640x480) const scaleX = canvas.width / 640; const scaleY = canvas.height / 480; ctx.strokeStyle = '#e94560'; ctx.lineWidth = 2; ctx.fillStyle = 'rgba(233, 69, 96, 0.2)'; ctx.beginPath(); points.forEach((p, i) => { const x = p.x * scaleX; const y = p.y * scaleY; if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); }); ctx.closePath(); ctx.fill(); ctx.stroke(); } // 格式化字节 function formatBytes(bytes) { if (!bytes || bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } // 添加日志 function addLog(message, type = 'info') { const logContent = document.getElementById('logContent'); const time = new Date().toLocaleTimeString(); const entry = document.createElement('div'); entry.className = 'log-entry'; entry.innerHTML = ` ${time} ${message} `; logContent.insertBefore(entry, logContent.firstChild); if (logContent.children.length > 50) { logContent.removeChild(logContent.lastChild); } } // 模拟检测状态更新(实际应通过WebSocket) function simulateDetection() { streams.forEach(stream => { const isDetected = Math.random() > 0.9; const badge = document.getElementById('badge-' + stream.key); const detectStatus = document.getElementById('detect-' + stream.key); const lastCheck = document.getElementById('last-' + stream.key); if (badge && detectStatus && lastCheck) { if (isDetected) { badge.className = 'detection-badge detected'; badge.textContent = '有人'; detectStatus.className = 'info-value detected'; detectStatus.textContent = '检测到人员'; } else { badge.className = 'detection-badge idle'; badge.textContent = '无人'; detectStatus.className = 'info-value'; detectStatus.textContent = '正常'; } lastCheck.textContent = new Date().toLocaleTimeString(); } }); } // 初始化 loadStreams(); loadStatus(); setInterval(loadStreams, 30000); // 30秒刷新流列表 setInterval(simulateDetection, 1000); // 1秒更新检测状态 // 窗口大小改变时重绘ROI window.addEventListener('resize', () => { streams.forEach(stream => { const canvas = document.getElementById('roi-' + stream.key); if (canvas) loadAndDrawROI(stream.key, canvas); }); });