// ROI配置页面脚本 let streams = []; let currentStream = null; let roiPoints = []; let isDrawing = false; let canvas = null; let ctx = null; let img = null; let draggedPoint = null; let scaleX = 1, scaleY = 1; // 加载视频流列表 async function loadStreams() { try { const res = await fetch('/streams'); const data = await res.json(); if (data.code === 0) { streams = data.data; renderStreamList(); } } catch (e) { showToast('加载视频流失败', 'error'); } } // 渲染流列表 function renderStreamList() { const list = document.getElementById('streamList'); if (streams.length === 0) { list.innerHTML = '
暂无视频流
'; return; } list.innerHTML = streams.map(stream => `
${stream.app} / ${stream.stream}
${stream.resolution} @ ${stream.fps}fps
`).join(''); } // 选择视频流 async function selectStream(streamKey) { currentStream = streams.find(s => s.key === streamKey); if (!currentStream) return; // 更新UI document.querySelectorAll('.stream-item').forEach(item => { item.classList.toggle('active', item.dataset.key === streamKey); }); document.getElementById('currentStream').textContent = currentStream.app + ' / ' + currentStream.stream; // 加载视频和ROI await loadVideo(currentStream); await loadROI(streamKey); } // 加载视频 async function loadVideo(stream) { const area = document.getElementById('videoArea'); area.innerHTML = `
视频预览
坐标: 0, 0
`; img = document.getElementById('previewImg'); canvas = document.getElementById('roiCanvas'); ctx = canvas.getContext('2d'); // 使用 ZLM 截图API获取帧 const snapUrl = `/proxy/zlm/snap?app=${stream.app}&stream=${stream.stream}`; console.log('ROI页面截图URL:', snapUrl); // 添加错误处理 img.onerror = function() { console.error('ROI页面图片加载失败'); }; // 定期刷新帧 let lastTimestamp = 0; const refreshFrame = () => { const timestamp = Date.now(); if (timestamp - lastTimestamp < 100) return; lastTimestamp = timestamp; img.src = snapUrl + '&t=' + timestamp; }; img.onload = () => { console.log('ROI页面图片加载成功'); resizeCanvas(); }; refreshFrame(); const interval = setInterval(refreshFrame, 500); // 每500ms刷新 img.dataset.interval = interval; // 绑定画布事件 bindCanvasEvents(); } // 调整画布大小 function resizeCanvas() { if (!img || !canvas) return; const rect = img.getBoundingClientRect(); canvas.width = rect.width; canvas.height = rect.height; scaleX = 640 / rect.width; scaleY = 480 / rect.height; drawROI(); } // 加载ROI配置 async function loadROI(streamKey) { try { const res = await fetch('/getRoi?stream_key=' + streamKey); const data = await res.json(); if (data.code === 0 && data.data) { roiPoints = data.data.map(p => ({ x: p.x / scaleX, y: p.y / scaleY })); drawROI(); } } catch (e) { console.error('加载ROI失败:', e); } } // 绑定画布事件 function bindCanvasEvents() { canvas.addEventListener('mousedown', onMouseDown); canvas.addEventListener('mousemove', onMouseMove); canvas.addEventListener('mouseup', onMouseUp); canvas.addEventListener('dblclick', onDoubleClick); canvas.addEventListener('contextmenu', onRightClick); // 坐标显示 canvas.addEventListener('mousemove', (e) => { const rect = canvas.getBoundingClientRect(); const x = Math.round((e.clientX - rect.left) * scaleX); const y = Math.round((e.clientY - rect.top) * scaleY); document.getElementById('coordDisplay').textContent = `坐标: ${x}, ${y}`; }); } // 鼠标按下 function onMouseDown(e) { const rect = canvas.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; // 检查是否点击了现有点 const pointIndex = findPointAt(x, y); if (pointIndex !== -1) { draggedPoint = pointIndex; return; } // 添加新点(最多4个点) if (isDrawing && roiPoints.length < 4) { roiPoints.push({ x, y }); drawROI(); } } // 鼠标移动 function onMouseMove(e) { if (draggedPoint !== null) { const rect = canvas.getBoundingClientRect(); roiPoints[draggedPoint].x = e.clientX - rect.left; roiPoints[draggedPoint].y = e.clientY - rect.top; drawROI(); } } // 鼠标释放 function onMouseUp() { draggedPoint = null; } // 双击删除点 function onDoubleClick(e) { const rect = canvas.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; const pointIndex = findPointAt(x, y); if (pointIndex !== -1) { roiPoints.splice(pointIndex, 1); drawROI(); } } // 右键完成绘制 function onRightClick(e) { e.preventDefault(); if (isDrawing && roiPoints.length >= 3) { isDrawing = false; canvas.classList.remove('drawing'); document.getElementById('btnDraw').disabled = false; showToast('绘制完成,可以拖拽调整顶点'); } } // 查找点击的点 function findPointAt(x, y) { const threshold = 10; for (let i = 0; i < roiPoints.length; i++) { const p = roiPoints[i]; const dist = Math.sqrt((p.x - x) ** 2 + (p.y - y) ** 2); if (dist < threshold) return i; } return -1; } // 绘制ROI function drawROI() { ctx.clearRect(0, 0, canvas.width, canvas.height); if (roiPoints.length === 0) return; // 绘制填充区域 ctx.fillStyle = 'rgba(233, 69, 96, 0.2)'; ctx.strokeStyle = '#e94560'; ctx.lineWidth = 2; ctx.beginPath(); roiPoints.forEach((p, i) => { if (i === 0) ctx.moveTo(p.x, p.y); else ctx.lineTo(p.x, p.y); }); if (roiPoints.length >= 3 && !isDrawing) { ctx.closePath(); } ctx.fill(); ctx.stroke(); // 绘制顶点 roiPoints.forEach((p, i) => { ctx.beginPath(); ctx.arc(p.x, p.y, 6, 0, Math.PI * 2); ctx.fillStyle = '#e94560'; ctx.fill(); ctx.strokeStyle = '#fff'; ctx.lineWidth = 2; ctx.stroke(); // 绘制序号 ctx.fillStyle = '#fff'; ctx.font = '12px sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText((i + 1).toString(), p.x, p.y); }); // 绘制连线提示 if (isDrawing && roiPoints.length > 0) { ctx.setLineDash([5, 5]); ctx.strokeStyle = '#888'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(roiPoints[roiPoints.length - 1].x, roiPoints[roiPoints.length - 1].y); ctx.lineTo(roiPoints[0].x, roiPoints[0].y); ctx.stroke(); ctx.setLineDash([]); } } // 开始绘制 function startDrawing() { if (!currentStream) { showToast('请先选择视频流', 'error'); return; } isDrawing = true; roiPoints = []; canvas.classList.add('drawing'); document.getElementById('btnDraw').disabled = true; drawROI(); showToast('点击画布添加顶点,最多4个点,右键完成'); } // 清空ROI function clearROI() { roiPoints = []; isDrawing = false; canvas.classList.remove('drawing'); document.getElementById('btnDraw').disabled = false; drawROI(); } // 重置为默认ROI function resetROI() { if (!img) return; const rect = img.getBoundingClientRect(); roiPoints = [ { x: 0, y: 0 }, { x: rect.width, y: 0 }, { x: rect.width, y: rect.height }, { x: 0, y: rect.height } ]; isDrawing = false; canvas.classList.remove('drawing'); document.getElementById('btnDraw').disabled = false; drawROI(); } // 保存ROI async function saveROI() { if (!currentStream) { showToast('请先选择视频流', 'error'); return; } if (roiPoints.length < 3) { showToast('至少需要3个点形成检测区域', 'error'); return; } // 转换为原始坐标 const normalizedPoints = roiPoints.map(p => ({ x: Math.round(p.x * scaleX), y: Math.round(p.y * scaleY) })); try { const res = await fetch('/saveRoi', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: 'stream_key=' + encodeURIComponent(currentStream.key) + '&roi=' + encodeURIComponent(JSON.stringify(normalizedPoints)) }); const data = await res.json(); if (data.code === 0) { showToast('保存成功'); } else { showToast(data.msg || '保存失败', 'error'); } } catch (e) { showToast('保存失败: ' + e.message, 'error'); } } // 显示提示 function showToast(message, type = 'success') { const toast = document.getElementById('toast'); toast.textContent = message; toast.className = 'toast show' + (type === 'error' ? ' error' : ''); setTimeout(() => { toast.classList.remove('show'); }, 3000); } // 初始化 loadStreams(); // 窗口大小改变时重绘 window.addEventListener('resize', () => { if (img) { setTimeout(resizeCanvas, 100); } });