| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381 |
- <!DOCTYPE html>
- <html lang="zh-CN">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>视频监控</title>
- <style>
- * { box-sizing: border-box; margin: 0; padding: 0; }
- body {
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
- background: #1a1a2e; color: #eee; min-height: 100vh; display: flex; flex-direction: column;
- }
- .top-bar {
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- padding: 12px 20px; display: flex; align-items: center; justify-content: space-between;
- flex-shrink: 0;
- }
- .top-bar h1 { font-size: 18px; color: white; }
- .nav-links { display: flex; gap: 8px; }
- .nav-link {
- background: rgba(255,255,255,0.2); color: white; padding: 6px 14px;
- border-radius: 16px; text-decoration: none; font-size: 13px;
- transition: background 0.2s; white-space: nowrap;
- }
- .nav-link:hover { background: rgba(255,255,255,0.35); }
- .nav-link.active { background: rgba(255,255,255,0.45); font-weight: bold; }
- .video-area {
- flex: 1; display: flex; flex-direction: column; align-items: center;
- padding: 16px; overflow: auto;
- }
- .video-wrapper {
- position: relative; background: #000; border-radius: 8px;
- overflow: hidden; box-shadow: 0 4px 20px rgba(0,0,0,0.5);
- max-width: 960px; width: 100%;
- }
- .video-wrapper img { width: 100%; display: block; }
- #roi-canvas {
- position: absolute; top: 0; left: 0; width: 100%; height: 100%;
- cursor: crosshair; display: none; z-index: 10;
- }
- .stream-label {
- position: absolute; top: 8px; left: 12px; background: rgba(0,0,0,0.6);
- color: white; padding: 4px 10px; border-radius: 4px; font-size: 13px;
- pointer-events: none; z-index: 5;
- }
- .stream-status {
- position: absolute; top: 8px; right: 12px; background: rgba(0,0,0,0.6);
- color: #52c41a; padding: 4px 10px; border-radius: 4px; font-size: 12px;
- pointer-events: none; z-index: 5;
- }
- .stream-status.offline { color: #f5222d; }
- .controls {
- margin-top: 14px; display: flex; gap: 10px; flex-wrap: wrap;
- justify-content: center; max-width: 960px; width: 100%;
- }
- .btn {
- padding: 10px 20px; border: 1px solid #444; background: #2a2a4a;
- color: #eee; border-radius: 8px; cursor: pointer; font-size: 14px;
- transition: all 0.2s; display: flex; align-items: center; gap: 6px;
- }
- .btn:hover { background: #3a3a5a; border-color: #667eea; }
- .btn.primary { background: #667eea; border-color: #667eea; }
- .btn.primary:hover { background: #5a6ed6; }
- .btn.danger { background: #ff4d4f; border-color: #ff4d4f; color: white; }
- .btn.danger:hover { background: #e04445; }
- .btn.drawing { background: #ff4d4f; color: white; border-color: #ff4d4f; animation: pulse 1.5s infinite; }
- .btn:disabled { opacity: 0.5; cursor: not-allowed; }
- @keyframes pulse {
- 0%, 100% { opacity: 1; }
- 50% { opacity: 0.7; }
- }
- .roi-info {
- margin-top: 12px; padding: 12px 16px; background: #2a2a4a;
- border-radius: 8px; font-size: 13px; line-height: 1.8;
- max-width: 960px; width: 100%;
- }
- .roi-info .label { color: #999; }
- .roi-info .value { color: #667eea; font-weight: bold; }
- .help-text { margin-top: 8px; font-size: 12px; color: #666; text-align: center; }
- @media (max-width: 600px) {
- .top-bar { flex-direction: column; gap: 8px; }
- .nav-links { flex-wrap: wrap; justify-content: center; }
- }
- </style>
- </head>
- <body>
- <div class="top-bar">
- <h1 id="page-title">视频监控</h1>
- <div class="nav-links">
- <a href="/" class="nav-link">📊 监控台</a>
- <a href="/video?stream=in-low" class="nav-link" id="nav-in-low">低位进站</a>
- <a href="/video?stream=in-high" class="nav-link" id="nav-in-high">高位进站</a>
- <a href="/video?stream=out-low" class="nav-link" id="nav-out-low">低位出站</a>
- <a href="/video?stream=out-high" class="nav-link" id="nav-out-high">高位出站</a>
- </div>
- </div>
- <div class="video-area">
- <div class="video-wrapper" id="video-wrapper">
- <div class="stream-label" id="stream-label">加载中...</div>
- <div class="stream-status" id="stream-status">⏳ 连接中</div>
- <img id="video-stream" src="" alt="视频流">
- <canvas id="roi-canvas"></canvas>
- </div>
- <div class="controls">
- <button class="btn primary" onclick="takeSnapshot()">📷 截图</button>
- <button class="btn" id="btn-draw-roi" onclick="startDrawROI()">✏️ 绘制ROI</button>
- <button class="btn" id="btn-clear-roi" onclick="clearROI()">🗑️ 清除ROI</button>
- <button class="btn" onclick="refreshStream()">🔄 刷新视频</button>
- </div>
- <div class="roi-info" id="roi-info">
- <span class="label">加载配置中...</span>
- </div>
- <div class="help-text" id="help-text"></div>
- </div>
- <script>
- var STREAM_NAMES = {
- 'in-low': '低位进站', 'in-high': '高位进站',
- 'out-low': '低位出站', 'out-high': '高位出站'
- };
- var ROI_STREAMS = ['in-low', 'out-low'];
- var currentStream = 'in-low';
- var hasROI = false;
- var isDrawingROI = false;
- var drawStart = null;
- var currentROI = null;
- var videoResolution = { width: 1920, height: 1080 };
- (function init() {
- var params = new URLSearchParams(window.location.search);
- currentStream = params.get('stream') || 'in-low';
- hasROI = ROI_STREAMS.indexOf(currentStream) >= 0;
- var streamName = STREAM_NAMES[currentStream] || '未知';
- document.title = streamName + ' - 视频监控';
- document.getElementById('page-title').textContent = streamName;
- document.getElementById('stream-label').textContent = streamName;
- var navEl = document.getElementById('nav-' + currentStream);
- if (navEl) navEl.classList.add('active');
- if (!hasROI) {
- document.getElementById('btn-draw-roi').style.display = 'none';
- document.getElementById('btn-clear-roi').style.display = 'none';
- document.getElementById('help-text').textContent = '高位摄像头无ROI功能';
- } else {
- document.getElementById('help-text').innerHTML =
- 'ROI仅影响推理裁剪区域,<b>上传照片仍为原始全帧</b> | ESC退出绘制模式';
- }
- startStream();
- loadRoiConfig();
- document.addEventListener('keydown', function(e) {
- if (e.key === 'Escape' && isDrawingROI) cancelDrawROI();
- });
- var img = document.getElementById('video-stream');
- img.onload = function() {
- document.getElementById('stream-status').textContent = '🟢 在线';
- document.getElementById('stream-status').className = 'stream-status';
- };
- img.onerror = function() {
- document.getElementById('stream-status').textContent = '🔴 离线';
- document.getElementById('stream-status').className = 'stream-status offline';
- setTimeout(function() {
- img.src = '/api/mjpeg?stream=' + currentStream + '&t=' + Date.now();
- }, 3000);
- };
- })();
- function startStream() {
- document.getElementById('video-stream').src = '/api/mjpeg?stream=' + currentStream + '&t=' + Date.now();
- }
- function refreshStream() {
- var img = document.getElementById('video-stream');
- img.src = '';
- setTimeout(function() { img.src = '/api/mjpeg?stream=' + currentStream + '&t=' + Date.now(); }, 100);
- }
- function takeSnapshot() {
- window.open('/api/stream?stream=' + currentStream + '&t=' + Date.now(), '_blank');
- }
- function loadRoiConfig() {
- fetch('/api/roi/config')
- .then(function(r) { return r.json(); })
- .then(function(data) {
- var roi = null;
- if (currentStream === 'in-low') roi = data.roi_in;
- else if (currentStream === 'out-low') roi = data.roi_out;
- if (data.resolution && data.resolution[currentStream]) {
- videoResolution = data.resolution[currentStream];
- }
- var infoEl = document.getElementById('roi-info');
- if (!hasROI) {
- infoEl.innerHTML = '<span class="label">高位摄像头 — 无ROI配置</span>';
- return;
- }
- if (roi && roi.enabled) {
- currentROI = { x: roi.x, y: roi.y, w: roi.w, h: roi.h };
- infoEl.innerHTML =
- '<span class="label">ROI区域:</span> <span class="value">(' +
- roi.x + ', ' + roi.y + ') ' + roi.w + '×' + roi.h +
- '</span> <span class="label">| 分辨率:</span> <span class="value">' +
- videoResolution.width + '×' + videoResolution.height +
- '</span><br><span class="label">状态:</span> <span class="value" style="color:#52c41a;">✅ 已启用</span>' +
- ' <span class="label">| 推理裁剪,上传全帧</span>';
- } else {
- currentROI = null;
- infoEl.innerHTML =
- '<span class="label">ROI状态:</span> <span class="value" style="color:#faad14;">⚪ 未启用(全帧推理)</span>' +
- '<br><span class="label">分辨率:</span> <span class="value">' +
- videoResolution.width + '×' + videoResolution.height + '</span>';
- }
- })
- .catch(function(e) {
- document.getElementById('roi-info').innerHTML =
- '<span class="label" style="color:#f5222d;">⚠️ 配置加载失败</span>';
- });
- }
- function startDrawROI() {
- if (!hasROI) return;
- isDrawingROI = true;
- drawStart = null;
- var img = document.getElementById('video-stream');
- var canvas = document.getElementById('roi-canvas');
- var btn = document.getElementById('btn-draw-roi');
- img.src = '/api/stream?stream=' + currentStream + '&t=' + Date.now();
- resizeCanvas();
- canvas.style.display = 'block';
- btn.classList.add('drawing');
- btn.innerHTML = '🔲 绘制中...';
- canvas.onmousedown = onDrawStart;
- canvas.onmousemove = onDrawMove;
- canvas.onmouseup = onDrawEnd;
- canvas.ontouchstart = onTouchStart;
- canvas.ontouchmove = onTouchMove;
- canvas.ontouchend = onTouchEnd;
- }
- function cancelDrawROI() {
- isDrawingROI = false;
- drawStart = null;
- var canvas = document.getElementById('roi-canvas');
- var btn = document.getElementById('btn-draw-roi');
- canvas.style.display = 'none';
- canvas.getContext('2d').clearRect(0, 0, canvas.width, canvas.height);
- btn.classList.remove('drawing');
- btn.innerHTML = '✏️ 绘制ROI';
- startStream();
- }
- function finishDrawROI(rect) {
- var canvas = document.getElementById('roi-canvas');
- var scaleX = videoResolution.width / canvas.clientWidth;
- var scaleY = videoResolution.height / canvas.clientHeight;
- var roiX = Math.round(rect.x * scaleX);
- var roiY = Math.round(rect.y * scaleY);
- var roiW = Math.round(rect.w * scaleX);
- var roiH = Math.round(rect.h * scaleY);
- roiX = Math.max(0, Math.min(roiX, videoResolution.width - 1));
- roiY = Math.max(0, Math.min(roiY, videoResolution.height - 1));
- roiW = Math.min(roiW, videoResolution.width - roiX);
- roiH = Math.min(roiH, videoResolution.height - roiY);
- if (roiW < 50 || roiH < 50) {
- alert('ROI区域太小(最小50×50像素),请重新绘制');
- cancelDrawROI();
- return;
- }
- var side = (currentStream === 'in-low') ? 'in' : 'out';
- var sideName = (side === 'in') ? '进站' : '出站';
- if (confirm('确认应用' + sideName + 'ROI区域?\n坐标: (' + roiX + ', ' + roiY + ')\n尺寸: ' + roiW + '×' + roiH)) {
- fetch('/api/roi/update', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ side: side, x: roiX, y: roiY, w: roiW, h: roiH, enabled: true })
- })
- .then(function(r) { return r.json(); })
- .then(function(data) {
- if (data.success) {
- alert('✅ ' + sideName + 'ROI已更新并保存到config.ini');
- loadRoiConfig();
- } else {
- alert('❌ 更新失败: ' + (data.message || '未知错误'));
- }
- })
- .catch(function(e) { alert('请求失败: ' + e); });
- }
- cancelDrawROI();
- }
- function clearROI() {
- if (!hasROI) return;
- var side = (currentStream === 'in-low') ? 'in' : 'out';
- var sideName = (side === 'in') ? '进站' : '出站';
- if (!confirm('确定清除' + sideName + 'ROI区域?\n清除后将使用全帧推理')) return;
- fetch('/api/roi/update', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ side: side, x: 0, y: 0, w: 0, h: 0, enabled: false })
- })
- .then(function(r) { return r.json(); })
- .then(function(data) {
- if (data.success) {
- alert('✅ ROI已清除,恢复全帧推理');
- loadRoiConfig();
- } else {
- alert('❌ 清除失败: ' + (data.message || '未知错误'));
- }
- })
- .catch(function(e) { alert('请求失败: ' + e); });
- }
- function resizeCanvas() {
- var wrapper = document.getElementById('video-wrapper');
- var canvas = document.getElementById('roi-canvas');
- canvas.width = wrapper.clientWidth;
- canvas.height = wrapper.clientHeight;
- }
- function getCanvasPos(e) {
- var rect = document.getElementById('roi-canvas').getBoundingClientRect();
- return { x: e.clientX - rect.left, y: e.clientY - rect.top };
- }
- function onDrawStart(e) { drawStart = getCanvasPos(e); }
- function onDrawMove(e) {
- if (!drawStart) return;
- var canvas = document.getElementById('roi-canvas');
- var ctx = canvas.getContext('2d');
- var pos = getCanvasPos(e);
- ctx.clearRect(0, 0, canvas.width, canvas.height);
- var rx = Math.min(drawStart.x, pos.x);
- var ry = Math.min(drawStart.y, pos.y);
- var rw = Math.abs(pos.x - drawStart.x);
- var rh = Math.abs(pos.y - drawStart.y);
- ctx.fillStyle = 'rgba(255, 0, 0, 0.15)';
- ctx.fillRect(rx, ry, rw, rh);
- ctx.strokeStyle = '#ff0000';
- ctx.lineWidth = 2;
- ctx.setLineDash([6, 3]);
- ctx.strokeRect(rx, ry, rw, rh);
- ctx.setLineDash([]);
- var scaleX = videoResolution.width / canvas.clientWidth;
- var scaleY = videoResolution.height / canvas.clientHeight;
- ctx.fillStyle = '#ff0000';
- ctx.font = '14px sans-serif';
- ctx.fillText(Math.round(rw * scaleX) + '×' + Math.round(rh * scaleY), rx + 4, ry + 18);
- }
- function onDrawEnd(e) {
- if (!drawStart) return;
- var pos = getCanvasPos(e);
- var x = Math.min(drawStart.x, pos.x);
- var y = Math.min(drawStart.y, pos.y);
- var w = Math.abs(pos.x - drawStart.x);
- var h = Math.abs(pos.y - drawStart.y);
- if (w < 5 || h < 5) { cancelDrawROI(); return; }
- finishDrawROI({ x: x, y: y, w: w, h: h });
- }
- function onTouchStart(e) {
- e.preventDefault();
- var t = e.touches[0];
- onDrawStart({ clientX: t.clientX, clientY: t.clientY });
- }
- function onTouchMove(e) {
- e.preventDefault();
- var t = e.touches[0];
- onDrawMove({ clientX: t.clientX, clientY: t.clientY });
- }
- function onTouchEnd(e) {
- e.preventDefault();
- var t = e.changedTouches[0];
- onDrawEnd({ clientX: t.clientX, clientY: t.clientY });
- }
- </script>
- </body>
- </html>
|