| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Live Camera - NETRA</title> |
| <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}"> |
| <link rel="stylesheet" href="{{ url_for('static', filename='css/dark-theme.css') }}"> |
| <style> |
| #video-container { |
| min-height: 400px; |
| position: relative; |
| border-radius: var(--radius-md); |
| overflow: hidden; |
| border: 2px solid var(--line); |
| } |
| |
| .model-checkbox-label { |
| display: flex; |
| align-items: center; |
| gap: 0.8rem; |
| padding: 0.8rem; |
| border-radius: 10px; |
| cursor: pointer; |
| transition: var(--transition); |
| background: var(--white); |
| border: 1px solid var(--line); |
| } |
| |
| .model-checkbox-label:hover { |
| background: var(--blue-100); |
| border-color: var(--blue-600); |
| } |
| |
| .model-checkbox-label input { |
| cursor: pointer; |
| width: 18px; |
| height: 18px; |
| } |
| |
| @media (max-width: 960px) { |
| #video-container { min-height: 300px; } |
| .live-camera-layout { grid-template-columns: 1fr; } |
| .storage-drawer { width: 100%; right: -100%; } |
| } |
| |
| |
| #storage-backdrop { |
| position: fixed; inset: 0; |
| background: rgba(0,0,0,0.45); |
| backdrop-filter: blur(3px); |
| z-index: 1100; |
| opacity: 0; pointer-events: none; |
| transition: opacity 0.3s ease; |
| } |
| #storage-backdrop.open { opacity: 1; pointer-events: all; } |
| |
| .storage-drawer { |
| position: fixed; |
| top: 0; right: -500px; |
| width: 480px; height: 100vh; |
| background: var(--white); |
| box-shadow: -6px 0 32px rgba(0,0,0,0.18); |
| z-index: 1101; |
| display: flex; flex-direction: column; |
| transition: right 0.32s cubic-bezier(.4,0,.2,1); |
| } |
| .storage-drawer.open { right: 0; } |
| |
| .sd-header { |
| padding: 1rem 1.2rem 0; |
| border-bottom: 1px solid var(--line); |
| flex-shrink: 0; |
| } |
| .sd-title-row { |
| display: flex; align-items: center; justify-content: space-between; |
| margin-bottom: 0.8rem; |
| } |
| .sd-title { font-size: 1.15rem; font-weight: 700; color: var(--slate-900); } |
| .sd-close { |
| width: 32px; height: 32px; border-radius: 50%; |
| border: none; background: var(--slate-100); |
| cursor: pointer; font-size: 1.1rem; color: var(--slate-600); |
| display: flex; align-items: center; justify-content: center; |
| transition: background 0.15s; |
| } |
| .sd-close:hover { background: var(--slate-200); color: var(--slate-900); } |
| |
| .sd-stats-row { |
| display: flex; gap: 1rem; margin-bottom: 0.8rem; |
| } |
| .sd-stat { |
| background: var(--slate-100); border-radius: 8px; |
| padding: 0.35rem 0.8rem; font-size: 0.8rem; color: var(--slate-700); |
| } |
| .sd-stat strong { color: var(--slate-900); } |
| |
| .sd-tabs { |
| display: flex; gap: 0; |
| } |
| .sd-tab { |
| flex: 1; padding: 0.6rem 0.5rem; |
| border: none; background: none; cursor: pointer; |
| font-size: 0.85rem; font-weight: 600; color: var(--slate-500); |
| border-bottom: 3px solid transparent; |
| transition: color 0.15s, border-color 0.15s; |
| } |
| .sd-tab:hover { color: var(--slate-800); } |
| .sd-tab.active { color: var(--blue-700); border-color: var(--blue-600); } |
| |
| .sd-body { |
| flex: 1; overflow-y: auto; padding: 0.8rem 1rem; |
| } |
| |
| |
| .sd-sessions-grid { |
| display: flex; flex-direction: column; gap: 0.8rem; |
| } |
| .sd-session-card { |
| border: 1px solid var(--line); border-radius: 12px; |
| overflow: hidden; background: var(--white); |
| transition: box-shadow 0.2s; |
| } |
| .sd-session-card:hover { box-shadow: 0 4px 14px rgba(0,0,0,0.09); } |
| .sd-session-thumb { |
| width: 100%; height: 120px; object-fit: cover; display: block; |
| background: var(--slate-100); |
| } |
| .sd-session-thumb-placeholder { |
| width: 100%; height: 120px; |
| background: linear-gradient(135deg, var(--slate-200), var(--slate-300)); |
| display: flex; align-items: center; justify-content: center; |
| font-size: 2.5rem; color: var(--slate-400); |
| } |
| .sd-session-body { padding: 0.75rem; } |
| .sd-session-name { |
| font-weight: 600; font-size: 0.9rem; color: var(--slate-900); |
| white-space: nowrap; overflow: hidden; text-overflow: ellipsis; |
| margin-bottom: 0.3rem; |
| } |
| .sd-session-meta { font-size: 0.78rem; color: var(--slate-500); margin-bottom: 0.55rem; } |
| .sd-session-badges { display: flex; gap: 0.4rem; flex-wrap: wrap; margin-bottom: 0.65rem; } |
| .sd-badge { |
| font-size: 0.72rem; padding: 0.2rem 0.5rem; border-radius: 20px; |
| font-weight: 600; background: var(--slate-100); color: var(--slate-700); |
| } |
| .sd-badge.danger { background: #fee2e2; color: var(--danger); } |
| .sd-badge.blue { background: var(--blue-100); color: var(--blue-700); } |
| .sd-badge.critical { background: var(--danger); color: white; } |
| .sd-session-actions { display: flex; gap: 0.4rem; } |
| .sd-btn { |
| flex: 1; padding: 0.4rem 0; font-size: 0.8rem; font-weight: 600; |
| border: 1px solid var(--line); border-radius: 7px; cursor: pointer; |
| background: var(--white); color: var(--slate-700); |
| transition: background 0.15s, border-color 0.15s; |
| } |
| .sd-btn:hover { background: var(--slate-100); } |
| .sd-btn.primary { background: var(--blue-600); color: white; border-color: var(--blue-600); } |
| .sd-btn.primary:hover { background: var(--blue-700); } |
| .sd-btn.danger { border-color: #fca5a5; color: var(--danger); } |
| .sd-btn.danger:hover { background: #fee2e2; } |
| |
| |
| .sd-recording-item { |
| display: flex; align-items: center; gap: 0.8rem; |
| padding: 0.75rem 0; border-bottom: 1px solid var(--line); |
| } |
| .sd-recording-item:last-child { border-bottom: none; } |
| .sd-rec-icon { |
| width: 40px; height: 40px; background: var(--blue-100); |
| border-radius: 8px; display: flex; align-items: center; |
| justify-content: center; font-size: 1.2rem; flex-shrink: 0; |
| } |
| .sd-rec-name { |
| font-weight: 600; font-size: 0.85rem; color: var(--slate-900); |
| word-break: break-all; |
| } |
| .sd-rec-meta { font-size: 0.75rem; color: var(--slate-500); margin-top: 0.1rem; } |
| .sd-rec-actions { display: flex; gap: 0.3rem; flex-shrink: 0; } |
| .sd-icon-btn { |
| width: 30px; height: 30px; border: 1px solid var(--line); |
| border-radius: 6px; background: var(--white); cursor: pointer; |
| display: flex; align-items: center; justify-content: center; |
| font-size: 0.85rem; transition: background 0.15s; |
| } |
| .sd-icon-btn:hover { background: var(--slate-100); } |
| .sd-icon-btn.danger:hover { background: #fee2e2; border-color: #fca5a5; } |
| |
| |
| .sd-img-grid { |
| display: grid; |
| grid-template-columns: repeat(3, 1fr); |
| gap: 0.5rem; |
| } |
| .sd-img-tile { |
| position: relative; border-radius: 8px; overflow: hidden; |
| aspect-ratio: 1; cursor: pointer; background: var(--slate-100); |
| } |
| .sd-img-tile img { width: 100%; height: 100%; object-fit: cover; display: block; } |
| .sd-img-overlay { |
| position: absolute; inset: 0; |
| background: rgba(0,0,0,0.55); |
| display: flex; align-items: center; justify-content: center; gap: 0.4rem; |
| opacity: 0; transition: opacity 0.2s; |
| } |
| .sd-img-tile:hover .sd-img-overlay { opacity: 1; } |
| .sd-img-overlay-btn { |
| width: 30px; height: 30px; border-radius: 50%; |
| background: rgba(255,255,255,0.9); border: none; cursor: pointer; |
| font-size: 0.85rem; display: flex; align-items: center; justify-content: center; |
| } |
| .sd-img-tile .sd-img-level { |
| position: absolute; top: 4px; right: 4px; |
| font-size: 0.65rem; font-weight: 700; color: white; |
| background: var(--danger); padding: 1px 5px; border-radius: 4px; |
| } |
| |
| |
| .sd-empty { |
| text-align: center; padding: 3rem 1rem; color: var(--slate-500); |
| } |
| .sd-empty-icon { font-size: 2.5rem; margin-bottom: 0.8rem; } |
| .sd-empty p { font-size: 0.9rem; } |
| |
| |
| #sd-inline-player { |
| margin-bottom: 0.8rem; border-radius: 10px; overflow: hidden; |
| background: #000; display: none; |
| } |
| #sd-inline-player video { width: 100%; display: block; max-height: 220px; } |
| #sd-inline-player .sd-player-close { |
| display: flex; justify-content: flex-end; padding: 0.3rem 0.5rem; |
| background: #111; |
| } |
| #sd-inline-player .sd-player-close button { |
| background: none; border: none; color: #aaa; cursor: pointer; |
| font-size: 0.85rem; |
| } |
| </style> |
| </head> |
| <body> |
| |
| <div id="toast-container"></div> |
|
|
| |
| <nav class="navbar"> |
| <div class="nav-container"> |
| <div class="nav-brand"> |
| <span class="brand-icon">👁️</span> |
| <span>NETRA</span> |
| </div> |
| <button class="hamburger-menu" id="hamburger-toggle"> |
| <span></span> |
| <span></span> |
| <span></span> |
| </button> |
| <div class="nav-menu" id="nav-menu"> |
| <a href="{{ url_for('dashboard') }}" class="nav-link">Dashboard</a> |
| <div class="dropdown" id="analysis-dropdown"> |
| <button class="nav-link dropdown-btn" onclick="toggleDropdown(event)">📊 Analysis ▼</button> |
| <div class="dropdown-content"> |
| <a href="{{ url_for('live_camera') }}" style="opacity: 0.6;">📹 Live Camera</a> |
| <a href="{{ url_for('video_analysis') }}">📋 Video Analysis</a> |
| </div> |
| </div> |
| <a href="{{ url_for('logout') }}" class="btn btn-outline">Logout</a> |
| </div> |
| </div> |
| </nav> |
| |
| <div class="content-container"> |
| |
| <div class="page-header reveal" style="animation: slideInDown 0.6s ease both;"> |
| <h1>🎥 Live Camera Monitoring</h1> |
| <p>Real-time AI-powered surveillance with instant threat detection</p> |
| </div> |
| |
| |
| <div class="upload-section reveal" style="animation: slideInUp 0.6s ease 0.1s both;"> |
| <div style="background: var(--blue-50); border: 1px solid var(--blue-200); border-radius: var(--radius-md); padding: 1.2rem;"> |
| <div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 0.8rem;"> |
| <h3 style="color: var(--blue-900); margin: 0;">🎯 Recent Detections</h3> |
| <span id="detection-count-badge" style="background: var(--danger); color: white; padding: 0.3rem 0.6rem; border-radius: 12px; font-weight: 600; font-size: 0.85rem;">0</span> |
| </div> |
| <div id="detection-gallery" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); gap: 0.8rem; min-height: 120px;"> |
| <div style="grid-column: 1 / -1; text-align: center; color: var(--slate-500); padding: 2rem;"> |
| <p style="margin: 0;">📸 No detections yet</p> |
| <small>Detected threats will appear here</small> |
| </div> |
| </div> |
| </div> |
| </div> |
| |
| |
| <div class="upload-section reveal" style="animation: slideInUp 0.6s ease 0.2s both;"> |
| |
| <div class="model-selection-panel" style="margin-bottom: 1.5rem;"> |
| <div class="panel-header" style="padding: 1rem; border-bottom: 1px solid var(--line);"> |
| <label style="display: flex; align-items: center; gap: 0.8rem; margin: 0; cursor: pointer;"> |
| <span style="font-size: 1.2rem;">📷</span> |
| <span style="font-weight: 600; color: var(--slate-900); flex: 1;">Select Camera Device</span> |
| </label> |
| <select id="camera-select" style="padding: 0.6rem 1rem; border: 1px solid var(--line); border-radius: var(--radius-md); background: white; color: var(--slate-900); font-size: 0.95rem; cursor: pointer; width: 100%; margin-top: 0.8rem;"> |
| <option value="">Loading cameras...</option> |
| </select> |
| <small style="color: var(--slate-600); display: block; margin-top: 0.5rem;"> |
| 💡 If USB camera not detected: ensure it's connected, restart the browser, or try a different USB port |
| </small> |
| </div> |
| </div> |
|
|
| |
| <div class="model-selection-panel"> |
| <div class="panel-header" onclick="toggleModelPanel()" style="padding: 1rem; border-bottom: 1px solid var(--line);"> |
| <div style="display: flex; align-items: center; justify-content: space-between;"> |
| <span class="panel-title">🤖 Select Detection Models</span> |
| <span class="toggle-icon" id="model-toggle-icon">▼</span> |
| </div> |
| </div> |
| <div id="model-panel-content" class="panel-content"> |
| <div class="models-grid" id="models-grid"> |
| <p class="loading-text" style="text-align: center; color: var(--slate-700);"> |
| <span class="spinner" style="width: 14px; height: 14px; margin-right: 0.5rem;"></span> |
| Loading available models... |
| </p> |
| </div> |
| <div class="panel-actions" style="margin-top: 1rem;"> |
| <button class="btn btn-secondary" onclick="selectAllModels()">✓ Select All</button> |
| <button class="btn btn-secondary" onclick="deselectAllModels()">✗ Deselect All</button> |
| <button class="btn btn-primary" onclick="applyModelSelection()">⚙️ Apply Selection</button> |
| </div> |
| </div> |
| </div> |
| </div> |
| |
| |
| <div id="camera-status-info" class="status-info" style="display: none; animation: slideInDown 0.4s ease; background: var(--blue-100); border: 1px solid var(--blue-200); border-radius: var(--radius-md); padding: 1rem; margin-bottom: 1rem;"> |
| <p style="color: var(--blue-900); font-weight: 600;">📊 Session Info</p> |
| <p style="color: var(--slate-700); margin-top: 0.5rem;">⏱️ Duration: <span id="session-duration" style="font-weight: 700;">00:00</span></p> |
| <p style="color: var(--slate-700); margin-top: 0.3rem;">🤖 Models: <span id="session-models" style="font-weight: 700;">-</span></p> |
| </div> |
| |
| |
| <div class="live-camera-layout reveal" style="animation: slideInUp 0.6s ease 0.3s both;"> |
| |
| <div class="video-section"> |
| <div class="video-container" id="video-container"> |
| <img id="camera-feed" src="" alt="Camera Feed" style="display: none; width: 100%; height: 100%; object-fit: cover;"> |
| <div class="video-overlay" id="video-overlay" style="display: none;"> |
| <div class="status-badge" id="status-badge"> |
| <span class="status-dot active"></span> |
| <span>● LIVE</span> |
| </div> |
| </div> |
| <div id="camera-placeholder" style="text-align: center; color: var(--slate-500); padding: 2rem;"> |
| <p style="font-size: 3rem; margin-bottom: 1rem; animation: bounce 2s ease-in-out infinite;">📷</p> |
| <p style="font-weight: 600;">Select detection models above and click "Start Camera"</p> |
| <p style="font-size: 0.9rem; margin-top: 0.5rem;">Real-time AI analysis will begin immediately</p> |
| </div> |
| </div> |
| |
| |
| <div class="video-controls"> |
| <button id="start-btn" class="btn btn-success" onclick="startCamera()" style="display: block; flex: 1;" title="Start camera feed"> |
| ▶️ Start Camera |
| </button> |
| <button id="stop-btn" class="btn btn-danger" onclick="stopCamera()" style="display: none; flex: 1;" title="Stop camera feed"> |
| ⏹️ Stop Camera |
| </button> |
| <button id="record-btn" class="btn btn-secondary" onclick="toggleRecording()" style="display: none;" title="Record video feed"> |
| 🔴 Record |
| </button> |
| <button id="fullscreen-btn" class="btn btn-secondary" onclick="toggleFullscreen()" title="Toggle fullscreen mode"> |
| ⛶ Fullscreen |
| </button> |
| <button class="btn btn-secondary" onclick="openStorageManager()" title="View saved videos and images"> |
| 💾 Storage |
| </button> |
| </div> |
| </div> |
| |
| |
| <div class="alerts-panel"> |
| |
| <div style="margin-bottom: 1.2rem;"> |
| <h2 style="color: var(--blue-900); margin-bottom: 0.8rem;">🚨 Detection Alerts</h2> |
| <div class="alerts-container" id="alerts-container"> |
| <div class="alert-placeholder"> |
| <p style="color: var(--slate-700); margin-bottom: 0.3rem;">✓ No alerts detected</p> |
| <small style="color: var(--slate-500);">Alerts will appear here when unusual activity is detected</small> |
| </div> |
| </div> |
| </div> |
| |
| |
| <div class="stats-container" style="background: var(--blue-100); border: 1px solid var(--blue-200); border-radius: var(--radius-md); padding: 1rem;"> |
| <h3 style="color: var(--blue-900); margin-bottom: 0.8rem;">📊 Statistics</h3> |
| <div class="stat-item"> |
| <span style="color: var(--slate-700);">Detections:</span> |
| <span class="stat-value" id="total-detections" style="color: var(--blue-700);">0</span> |
| </div> |
| <div class="stat-item"> |
| <span style="color: var(--slate-700);">Alerts:</span> |
| <span class="stat-value" id="total-alerts" style="color: var(--danger);">0</span> |
| </div> |
| <div class="stat-item"> |
| <span style="color: var(--slate-700);">👤 Person Detected:</span> |
| <span class="stat-value" id="person-detected" style="color: var(--blue-700);">0</span> |
| </div> |
| <div class="stat-item"> |
| <span style="color: var(--slate-700);">👁️ Person Present:</span> |
| <span class="stat-value" id="person-present" style="color: var(--ok);">❌ No</span> |
| </div> |
| <div class="stat-item"> |
| <span style="color: var(--slate-700);">Status:</span> |
| <span class="stat-value status-active" style="color: var(--ok);">🟢 Ready</span> |
| </div> |
| </div> |
| |
| |
| <div style="background: var(--orange-100); border: 1px solid var(--orange-500); border-radius: var(--radius-md); padding: 1rem; margin-top: 1rem;"> |
| <h3 style="color: var(--orange-600); margin-bottom: 0.8rem;">🧍 Pose Analysis</h3> |
| <div class="stat-item"> |
| <span style="color: var(--slate-700);">Risk Level:</span> |
| <span class="stat-value" id="pose-risk-level" style="color: var(--ok);">SAFE</span> |
| </div> |
| <div class="stat-item"> |
| <span style="color: var(--slate-700);">Action:</span> |
| <span class="stat-value" id="pose-action" style="color: var(--slate-700);">—</span> |
| </div> |
| <div class="stat-item"> |
| <span style="color: var(--slate-700);">Score:</span> |
| <span class="stat-value" id="pose-score" style="color: var(--blue-700);">0.00</span> |
| </div> |
| </div> |
| </div> |
| </div> |
| |
| |
| <div class="info-box reveal" style="animation: slideInUp 0.6s ease 0.4s both; margin-top: 1.5rem;"> |
| <h3 style="color: var(--blue-900); margin-bottom: 0.8rem;">ℹ️ How Live Monitoring Works</h3> |
| <ul style="color: var(--slate-700); line-height: 1.8;"> |
| <li>✓ The camera feed is processed in real-time using multiple AI models</li> |
| <li>✓ Objects, activities, and unusual behavior are automatically detected</li> |
| <li>✓ High-priority alerts are highlighted and logged instantly</li> |
| <li>✓ All detections are recorded for your review and analysis</li> |
| <li>✓ Multiple cameras can be monitored simultaneously</li> |
| </ul> |
| </div> |
| </div> |
| |
| |
| <div id="storage-backdrop" onclick="closeStorageDrawer()"></div> |
| <div id="storage-drawer" class="storage-drawer"> |
| <div class="sd-header"> |
| <div class="sd-title-row"> |
| <span class="sd-title">💾 Storage</span> |
| <button class="sd-close" onclick="closeStorageDrawer()" title="Close">✕</button> |
| </div> |
| <div class="sd-stats-row" id="sd-stats-row"> |
| <div class="sd-stat">Loading…</div> |
| </div> |
| <div class="sd-tabs"> |
| <button class="sd-tab active" id="sd-tab-sessions" onclick="switchStorageTab('sessions')">🎥 Sessions</button> |
| <button class="sd-tab" id="sd-tab-recordings" onclick="switchStorageTab('recordings')">🎬 Recordings</button> |
| <button class="sd-tab" id="sd-tab-images" onclick="switchStorageTab('images')">📷 Images</button> |
| </div> |
| </div> |
| <div class="sd-body" id="sd-body"> |
| |
| <div id="sd-inline-player"> |
| <div class="sd-player-close"> |
| <button onclick="closeInlinePlayer()">✕ Close player</button> |
| </div> |
| <video id="sd-video-el" controls></video> |
| </div> |
| |
| <div id="sd-content"></div> |
| </div> |
| </div> |
|
|
| |
| <script src="{{ url_for('static', filename='js/ui-utils.js') }}"></script> |
| <script src="{{ url_for('static', filename='js/live_camera.js') }}"></script> |
| |
| <script> |
| |
| const hamburger = document.getElementById('hamburger-toggle'); |
| const navMenu = document.getElementById('nav-menu'); |
| |
| if (hamburger) { |
| hamburger.addEventListener('click', function(e) { |
| e.stopPropagation(); |
| hamburger.classList.toggle('active'); |
| navMenu.classList.toggle('active'); |
| }); |
| } |
| |
| |
| document.querySelectorAll('.nav-link, .nav-menu a').forEach(link => { |
| link.addEventListener('click', function() { |
| hamburger?.classList.remove('active'); |
| navMenu?.classList.remove('active'); |
| }); |
| }); |
| |
| |
| document.addEventListener('click', function(e) { |
| if (!e.target.closest('.nav-container')) { |
| hamburger?.classList.remove('active'); |
| navMenu?.classList.remove('active'); |
| } |
| }); |
| |
| |
| function toggleDropdown(e) { |
| e.preventDefault(); |
| const dropdown = e.target.closest('.dropdown'); |
| dropdown.classList.toggle('active'); |
| } |
| </script> |
| </body> |
| </html> |
|
|