x-tracker / static /app.js
Marnu25's picture
feat(restore): restore original high-speed Docker x-tracker with T4 CUDA GPU acceleration and zero NSFW tracking
18ef677 verified
Raw History Blame Contribute Delete
37.8 kB
let cameraRunning = false; let isRecording = false; let currentFilter = 'normal'; let activeCameraId = 'laptop_cam'; let galleryFilter = 'all'; let currentZoom = 1.0; let lastUserZoomTime = 0; let aiDetectionActive = true; let hudOverlayActive = true; let captionsActive = false; let speechRecognizer = null; let activeTargetId = null; let activeTrackingMode = 'off'; let browserWebcamActive = false; let browserWebcamStream = null; let browserWebcamWs = null; // DOM Elements const streamFeed = document.getElementById('streamFeed'); const streamBox = document.getElementById('streamBox'); const standbyOverlay = document.getElementById('standbyOverlay'); const flashEffect = document.getElementById('flashEffect'); const overlayStartBtn = document.getElementById('overlayStartBtn'); const camOrb = document.getElementById('camOrb'); const camStateBadge = document.getElementById('camStateBadge'); const camResBadge = document.getElementById('camResBadge'); const camFpsBadge = document.getElementById('camFpsBadge'); const activeFilterBadge = document.getElementById('activeFilterBadge'); const gpuProviderBadge = document.getElementById('gpuProviderBadge'); const recordingTag = document.getElementById('recordingTag'); const recTimerText = document.getElementById('recTimerText'); const clientCount = document.getElementById('clientCount'); const camTabsContainer = document.getElementById('camTabsContainer'); const addCamBtn = document.getElementById('addCamBtn'); const browserWebcamBtn = document.getElementById('browserWebcamBtn'); const filterPills = document.querySelectorAll('.filter-pill'); // Zoom, PTZ Focus & AI Detection Elements const zoomSlider = document.getElementById('zoomSlider'); const zoomValText = document.getElementById('zoomValText'); const zoomInBtn = document.getElementById('zoomInBtn'); const zoomOutBtn = document.getElementById('zoomOutBtn'); const zoomPresetBtns = document.querySelectorAll('.zoom-preset-btn'); const trackPills = document.querySelectorAll('.track-pill'); const resetFocusBtn = document.getElementById('resetFocusBtn'); const aiToggleBtn = document.getElementById('aiToggleBtn'); const aiBtnText = document.getElementById('aiBtnText'); const hudOverlayBtn = document.getElementById('hudOverlayBtn'); const hudOverlayText = document.getElementById('hudOverlayText'); const captionsToggleBtn = document.getElementById('captionsToggleBtn'); const captionsBtnText = document.getElementById('captionsBtnText'); const liveCaptionsBar = document.getElementById('liveCaptionsBar'); const liveCaptionsText = document.getElementById('liveCaptionsText'); const detectionsStrip = document.getElementById('detectionsStrip'); const sceneTitleText = document.getElementById('sceneTitleText'); const sceneDescText = document.getElementById('sceneDescText'); const sceneStatusPill = document.getElementById('sceneStatusPill'); const trackingScoreSummary = document.getElementById('trackingScoreSummary'); const trackingClassBars = document.getElementById('trackingClassBars'); const trackingPartsRow = document.getElementById('trackingPartsRow'); const powerBtn = document.getElementById('powerBtn'); const powerBtnText = document.getElementById('powerBtnText'); const snapBtn = document.getElementById('snapBtn'); const recordBtn = document.getElementById('recordBtn'); const recordBtnText = document.getElementById('recordBtnText'); const fullscreenBtn = document.getElementById('fullscreenBtn'); const lanUrlCode = document.getElementById('lanUrlCode'); const tsUrlCode = document.getElementById('tsUrlCode'); const tsUrlContainer = document.getElementById('tsUrlContainer'); const mediaGrid = document.getElementById('mediaGrid'); const mediaCountBadge = document.getElementById('mediaCountBadge'); const filterBtns = document.querySelectorAll('.filter-btn'); const mediaModal = document.getElementById('mediaModal'); const modalTitle = document.getElementById('modalTitle'); const modalBody = document.getElementById('modalBody'); const modalCloseBtn = document.getElementById('modalCloseBtn'); const addCamModal = document.getElementById('addCamModal'); const addCamForm = document.getElementById('addCamForm'); const addCamCloseBtn = document.getElementById('addCamCloseBtn'); const cancelAddCamBtn = document.getElementById('cancelAddCamBtn'); const toastEl = document.getElementById('toast'); function showToast(msg, duration = 3000) { toastEl.textContent = msg; toastEl.classList.remove('hidden'); setTimeout(() => { toastEl.classList.add('hidden'); }, duration); } function formatTimer(seconds) { const mins = Math.floor(seconds / 60).toString().padStart(2, '0'); const secs = (seconds % 60).toString().padStart(2, '0'); return `${mins}:${secs}`; } function copyUrl(elementId) { const text = document.getElementById(elementId).textContent; navigator.clipboard.writeText(text).then(() => { showToast(`Copied: ${text}`); }).catch(() => { showToast(`Selected: ${text}`); }); } // Camera Tabs function renderCameraTabs(cameras, activeId) { camTabsContainer.innerHTML = cameras.map(cam => { const isActive = (cam.id === activeId); const isDefault = cam.is_default; return ` <div class="cam-tab ${isActive ? 'active' : ''}" onclick="switchCamera('${cam.id}')"> <span>${cam.name}</span> ${!isDefault ? `<span class="cam-tab-del" onclick="deleteCamera(event, '${cam.id}')" title="Delete camera">&times;</span>` : ''} </div> `; }).join(''); } async function switchCamera(camId) { if (camId === activeCameraId) return; try { const res = await fetch(`/api/cameras/active/${camId}`, { method: 'POST' }); const data = await res.json(); if (res.ok) { activeCameraId = camId; showToast(`Switched to: ${data.active_camera.name}`); streamFeed.src = `/api/stream?t=${Date.now()}`; await updateStatus(); } else { showToast(`Error: ${data.detail || 'Failed to switch camera'}`); } } catch (e) { showToast('Camera switch request failed'); } } async function deleteCamera(e, camId) { e.stopPropagation(); if (!confirm('Are you sure you want to remove this IP camera?')) return; try { const res = await fetch(`/api/cameras/${camId}`, { method: 'DELETE' }); const data = await res.json(); if (res.ok) { showToast('Camera removed'); await updateStatus(); } else { showToast(`Error: ${data.detail || 'Delete failed'}`); } } catch (e) { showToast('Delete request failed'); } } // Filter Control (Instant Real-Time) async function setFilter(filterName) { try { const res = await fetch(`/api/filter/${filterName}`, { method: 'POST' }); const data = await res.json(); if (res.ok) { currentFilter = filterName; activeFilterBadge.textContent = filterName.toUpperCase(); filterPills.forEach(pill => { pill.classList.toggle('active', pill.dataset.filter === filterName); }); showToast(`Active Filter: ${filterName.toUpperCase()}`); } else { showToast(`Error: ${data.detail || 'Filter failed'}`); } } catch (e) { showToast('Filter request failed'); } } // Digital Zoom & PTZ Focus let zoomServerDebounce = null; async function applyZoom(val) { const num = Math.max(1.0, Math.min(4.0, Math.round(parseFloat(val) * 10) / 10)); currentZoom = num; lastUserZoomTime = Date.now(); if (zoomSlider) zoomSlider.value = num; if (zoomValText) zoomValText.textContent = `${num.toFixed(1)}x`; zoomPresetBtns.forEach(btn => { const pZoom = parseFloat(btn.dataset.zoom); btn.classList.toggle('active', Math.abs(pZoom - num) < 0.05); }); if (streamFeed) { streamFeed.style.transform = (num > 1.0) ? `scale(${1.0 + (num - 1.0) * 0.15})` : 'scale(1)'; } clearTimeout(zoomServerDebounce); zoomServerDebounce = setTimeout(async () => { try { await fetch(`/api/zoom/${num}`, { method: 'POST' }); } catch (e) { console.error("Zoom server update error", e); } }, 80); } // Targeted Focus on Point (Click on Stream) async function focusOnPoint(cx, cy) { const targetZoom = Math.max(2.0, currentZoom); currentZoom = targetZoom; lastUserZoomTime = Date.now(); if (zoomSlider) zoomSlider.value = targetZoom; if (zoomValText) zoomValText.textContent = `${targetZoom.toFixed(1)}x`; try { const res = await fetch('/api/zoom/focus', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ x: cx, y: cy, zoom: targetZoom }) }); if (res.ok) { showToast(`🎯 Centered Focus on (${(cx*100).toFixed(0)}%, ${(cy*100).toFixed(0)}%)`); } } catch (e) { console.error("Focus error", e); } } // Targeted Focus on Specific Object / Body Part async function focusOnObject(objId, label) { lastUserZoomTime = Date.now(); activeTargetId = objId; try { const res = await fetch(`/api/zoom/target/${objId}`, { method: 'POST' }); if (res.ok) { showToast(`🎯 Zoom Locked on ${label.toUpperCase()} (${objId})`); activeTrackingMode = 'selected'; trackPills.forEach(pill => pill.classList.remove('active')); } } catch (e) { showToast('Failed to lock target object'); } } // Set Auto-Tracking Mode async function setAutoTracking(mode) { try { const res = await fetch(`/api/zoom/track/${mode}`, { method: 'POST' }); if (res.ok) { activeTrackingMode = mode; trackPills.forEach(pill => { pill.classList.toggle('active', pill.dataset.mode === mode); }); showToast(`Auto-Tracking: ${mode.toUpperCase()}`); if (mode !== 'off' && currentZoom <= 1.1) { applyZoom(2.0); } } } catch (e) { showToast('Tracking mode request failed'); } } // Reset Zoom & PTZ Centering async function resetZoomAndFocus() { lastUserZoomTime = Date.now(); currentZoom = 1.0; activeTargetId = null; activeTrackingMode = 'off'; if (zoomSlider) zoomSlider.value = 1.0; if (zoomValText) zoomValText.textContent = '1.0x'; if (streamFeed) streamFeed.style.transform = 'scale(1)'; trackPills.forEach(pill => { pill.classList.toggle('active', pill.dataset.mode === 'off'); }); zoomPresetBtns.forEach(btn => { btn.classList.toggle('active', parseFloat(btn.dataset.zoom) === 1.0); }); try { await fetch('/api/zoom/reset', { method: 'POST' }); showToast('⟲ Zoom & Focus Reset to Normal'); } catch (e) { console.error("Reset error", e); } } // AI Detection Toggle (YOLOv8 + Body-Parts Pose Engine) async function toggleAIDetection() { try { const res = await fetch('/api/detection/toggle', { method: 'POST' }); const data = await res.json(); if (res.ok) { aiDetectionActive = data.ai_detection_enabled; aiToggleBtn.classList.toggle('active', aiDetectionActive); aiBtnText.textContent = `AI Engine: ${aiDetectionActive ? 'ON' : 'OFF'}`; showToast(data.message); } } catch (e) { showToast('Detection toggle failed'); } } // Toggle HUD Boxes & Wireframe Visibility (Tracking stays active without overlay) async function toggleHudOverlay() { const nextState = !hudOverlayActive; try { const res = await fetch(`/api/overlay/${nextState}`, { method: 'POST' }); const data = await res.json(); if (res.ok) { hudOverlayActive = data.show_hud_overlay; if (hudOverlayBtn && hudOverlayText) { hudOverlayBtn.classList.toggle('active', hudOverlayActive); hudOverlayText.textContent = `👁️ HUD Boxes: ${hudOverlayActive ? 'ON' : 'OFF'}`; } showToast(data.message); } } catch (e) { showToast('Failed to toggle HUD overlay'); } } // Toggle Real-Time Spoken Dialogue Captions (Web Speech API) function toggleLiveCaptions() { const SpeechRec = window.SpeechRecognition || window.webkitSpeechRecognition; if (!SpeechRec) { showToast('Browser Speech Recognition API not supported in this browser'); return; } captionsActive = !captionsActive; if (captionsToggleBtn && captionsBtnText) { captionsToggleBtn.classList.toggle('active', captionsActive); captionsBtnText.textContent = `💬 Captions: ${captionsActive ? 'ON' : 'OFF'}`; } if (captionsActive) { if (liveCaptionsBar) liveCaptionsBar.classList.remove('hidden'); if (!speechRecognizer) { speechRecognizer = new SpeechRec(); speechRecognizer.continuous = true; speechRecognizer.interimResults = true; speechRecognizer.lang = navigator.language || 'en-US'; speechRecognizer.onresult = (event) => { let transcript = ''; for (let i = Math.max(0, event.results.length - 2); i < event.results.length; i++) { transcript += event.results[i][0].transcript + ' '; } if (liveCaptionsText && transcript.trim()) { liveCaptionsText.textContent = transcript.trim(); } }; speechRecognizer.onerror = () => {}; speechRecognizer.onend = () => { if (captionsActive) { try { speechRecognizer.start(); } catch (_) {} } }; } try { speechRecognizer.start(); showToast('Live Dialogue Captions enabled'); } catch (_) {} } else { if (liveCaptionsBar) liveCaptionsBar.classList.add('hidden'); if (speechRecognizer) { try { speechRecognizer.stop(); } catch (_) {} } showToast('Live Dialogue Captions hidden'); } } // Render Dedicated Scene Intelligence & Real-Time Pose/Object Telemetry Panel function renderSceneIntelligence(scene) { if (!scene) return; if (sceneTitleText && scene.title) { sceneTitleText.textContent = scene.title; } if (sceneDescText && scene.description) { sceneDescText.textContent = scene.description; } const details = scene.tracking_details || {}; if (sceneStatusPill) { const statusStr = details.status || 'ACTIVE TRACKING'; sceneStatusPill.textContent = statusStr; sceneStatusPill.className = 'badge badge-sfw'; } if (trackingScoreSummary) { const prov = details.provider || 'ONNX Runtime'; const total = details.total_targets ?? 0; trackingScoreSummary.textContent = `${prov} | ${total} Targets`; } if (trackingClassBars && details.category_breakdown) { const entries = Object.entries(details.category_breakdown); trackingClassBars.innerHTML = entries.map(([cls, count]) => { const pct = Math.min(100, Math.max(4, count * 18)); const barColor = cls === 'Body Parts' ? '#10b981' : (cls === 'People' ? '#3b82f6' : '#8b5cf6'); return ` <div class="tracking-bar-item"> <div class="tracking-bar-label"> <span>${cls}</span> <span>${count}</span> </div> <div class="tracking-bar-track"> <div class="tracking-bar-fill" style="width: ${pct}%; background: ${barColor};"></div> </div> </div> `; }).join(''); } if (trackingPartsRow) { const parts = details.visible_body_parts || []; if (parts.length === 0) { trackingPartsRow.innerHTML = `<span class="det-placeholder">No body segments currently in frame</span>`; } else { trackingPartsRow.innerHTML = parts.map(p => { return `<span class="badge badge-secondary">${p.toUpperCase()}</span>`; }).join(''); } } } // Render Detected Objects Chips (Expanded 80 Objects + Body Parts with Click-to-Focus) function renderDetections(detections) { if (!aiDetectionActive || !detections || detections.length === 0) { detectionsStrip.innerHTML = `<span class="det-placeholder">${aiDetectionActive ? 'Scanning with YOLOv8 & Body-Parts Pose...' : 'AI Engine is OFF (Click to Enable)'}</span>`; return; } const icons = { person: '👤', 'head / face': '🧑', 'left hand': '✋', 'right hand': '✋', 'left arm': '💪', 'right arm': '💪', 'torso / chest': '👕', 'chest / torso': '👕', 'pelvic / hips': '🩳', 'left leg': '🦵', 'right leg': '🦵', 'left foot': '🦶', 'right foot': '🦶', 'cell phone': '📱', laptop: '💻', mouse: '🖱️', keyboard: '⌨️', tv: '🖥️', bottle: '🍾', cup: '☕', chair: '🪑', couch: '🛋️', book: '📖', clock: '⏰', car: '🚗', dog: '🐕', cat: '🐈' }; detectionsStrip.innerHTML = detections.map(d => { const baseKey = d.base_label || d.label; const icon = icons[baseKey] || '🎯'; const isLocked = (activeTargetId && d.id === activeTargetId); return ` <div class="det-chip clickable ${isLocked ? 'target-locked' : ''}" onclick="focusOnObject('${d.id}', '${d.label}')" title="Click to zoom and lock focus onto this ${d.label}"> <span>${icon}</span> <span>${d.label.toUpperCase()} ${Math.round(d.confidence)}%</span> </div> `; }).join(''); } // Status Poller async function updateStatus() { try { const res = await fetch('/api/status'); if (!res.ok) return; const data = await res.json(); // Endpoints lanUrlCode.textContent = data.network.lan_url; if (data.network.tailscale_url) { tsUrlCode.textContent = data.network.tailscale_url; tsUrlContainer.style.display = 'flex'; } else { tsUrlContainer.style.display = 'none'; } // Camera List if (data.camera.cameras) { renderCameraTabs(data.camera.cameras, data.camera.active_camera_id); activeCameraId = data.camera.active_camera_id; } // Filter if (data.camera.active_filter) { currentFilter = data.camera.active_filter; activeFilterBadge.textContent = currentFilter.toUpperCase(); filterPills.forEach(pill => { pill.classList.toggle('active', pill.dataset.filter === currentFilter); }); } // Zoom Level Sync (only if user has not interacted recently) if (data.camera.zoom_level !== undefined && (Date.now() - lastUserZoomTime > 3500)) { currentZoom = data.camera.zoom_level; if (zoomSlider && document.activeElement !== zoomSlider) { zoomSlider.value = currentZoom; } if (zoomValText) { zoomValText.textContent = `${currentZoom.toFixed(1)}x`; } zoomPresetBtns.forEach(btn => { const pZoom = parseFloat(btn.dataset.zoom); btn.classList.toggle('active', Math.abs(pZoom - currentZoom) < 0.05); }); } // Tracking Mode & Target ID Sync if (data.camera.auto_tracking_mode) { activeTrackingMode = data.camera.auto_tracking_mode; trackPills.forEach(pill => { pill.classList.toggle('active', pill.dataset.mode === activeTrackingMode); }); } activeTargetId = data.camera.target_object_id; // AI Detection & Detections Strip if (data.camera.ai_detection_enabled !== undefined) { aiDetectionActive = data.camera.ai_detection_enabled; aiToggleBtn.classList.toggle('active', aiDetectionActive); aiBtnText.textContent = `AI Engine: ${aiDetectionActive ? 'ON' : 'OFF'}`; renderDetections(data.camera.detections); } // HUD Overlay Visibility Sync if (data.camera.show_hud_overlay !== undefined && hudOverlayBtn && hudOverlayText) { hudOverlayActive = data.camera.show_hud_overlay; hudOverlayBtn.classList.toggle('active', hudOverlayActive); hudOverlayText.textContent = `👁️ HUD Boxes: ${hudOverlayActive ? 'ON' : 'OFF'}`; } // Scene Intelligence & Tracking Telemetry Panel if (data.camera.scene) { renderSceneIntelligence(data.camera.scene); } // GPU / AI Tracker Provider Badge if (gpuProviderBadge) { const telemetry = data.camera.telemetry || {}; const isGpu = telemetry.gpu_active || (telemetry.provider && telemetry.provider.includes('CUDA')); const count = (data.camera.detections || []).length; gpuProviderBadge.textContent = isGpu ? `⚡ T4 GPU (${count} OBJ)` : `🟢 AI TRACKER (${count} OBJ)`; gpuProviderBadge.className = 'badge badge-sfw'; } // Camera Sensor State cameraRunning = data.camera.running; if (cameraRunning) { camOrb.classList.add('live'); camStateBadge.textContent = 'ONLINE'; camStateBadge.className = 'badge badge-success'; camResBadge.textContent = `${data.camera.width}x${data.camera.height}`; camFpsBadge.textContent = `${data.camera.fps} FPS`; clientCount.textContent = data.camera.client_count; powerBtnText.textContent = 'Standby'; powerBtn.classList.remove('active-standby'); snapBtn.disabled = false; recordBtn.disabled = false; standbyOverlay.classList.add('hidden'); if (!streamFeed.src || streamFeed.src === window.location.href || streamFeed.classList.contains('hidden') || streamFeed.getAttribute('src') === '') { streamFeed.src = `/api/stream?t=${Date.now()}`; streamFeed.classList.remove('hidden'); } } else { camOrb.classList.remove('live'); camStateBadge.textContent = 'STANDBY'; camStateBadge.className = 'badge badge-secondary'; camResBadge.textContent = 'IDLE'; camFpsBadge.textContent = '0.0 FPS'; clientCount.textContent = '0'; powerBtnText.textContent = 'Power On'; powerBtn.classList.add('active-standby'); snapBtn.disabled = true; recordBtn.disabled = true; streamFeed.classList.add('hidden'); standbyOverlay.classList.remove('hidden'); } // Recording State isRecording = data.recorder.is_recording; if (isRecording) { recordingTag.classList.remove('hidden'); recTimerText.textContent = formatTimer(data.recorder.elapsed_seconds); recordBtn.classList.add('recording'); recordBtnText.textContent = 'Stop Recording'; } else { recordingTag.classList.add('hidden'); recordBtn.classList.remove('recording'); recordBtnText.textContent = 'Record Video'; } } catch (e) { camStateBadge.textContent = 'OFFLINE'; camStateBadge.className = 'badge badge-danger'; } } // Power Toggle async function toggleCameraPower() { powerBtn.disabled = true; try { const endpoint = cameraRunning ? '/api/camera/stop' : '/api/camera/start'; const res = await fetch(endpoint, { method: 'POST' }); const data = await res.json(); if (res.ok) { showToast(data.message); await updateStatus(); } else { showToast(`Error: ${data.detail || 'Action failed'}`); } } catch (e) { showToast('Failed to contact camera station'); } finally { powerBtn.disabled = false; } } // Snapshot async function takeSnapshot() { snapBtn.disabled = true; flashEffect.classList.add('trigger'); setTimeout(() => flashEffect.classList.remove('trigger'), 150); try { const res = await fetch('/api/snapshot', { method: 'POST' }); const data = await res.json(); if (res.ok) { showToast(`📸 Snapshot saved: ${data.filename}`); loadGallery(); } else { showToast(`Snapshot error: ${data.detail || 'Failed'}`); } } catch (e) { showToast('Snapshot request failed'); } finally { snapBtn.disabled = false; } } // Recording Toggle async function toggleRecording() { recordBtn.disabled = true; try { if (!isRecording) { const res = await fetch('/api/record/start', { method: 'POST' }); const data = await res.json(); if (res.ok) { showToast(`🎥 Recording started: ${data.filename}`); } else { showToast(`Error: ${data.detail || 'Failed'}`); } } else { const res = await fetch('/api/record/stop', { method: 'POST' }); const data = await res.json(); if (res.ok) { showToast(`💾 Saved: ${data.info.filename} (${data.info.duration}s)`); loadGallery(); } else { showToast('Error stopping recording'); } } await updateStatus(); } catch (e) { showToast('Recording request failed'); } finally { recordBtn.disabled = false; } } // Gallery Loader async function loadGallery() { try { const res = await fetch('/api/media'); if (!res.ok) return; const data = await res.json(); let items = []; if (galleryFilter === 'all' || galleryFilter === 'image') { items = items.concat(data.snapshots); } if (galleryFilter === 'all' || galleryFilter === 'video') { items = items.concat(data.recordings); } items.sort((a, b) => (b.name > a.name ? 1 : -1)); mediaCountBadge.textContent = `${items.length} items`; if (items.length === 0) { mediaGrid.innerHTML = ` <div class="empty-state"> <p>No media captured yet.</p> <small>Snap a photo or start recording to populate the gallery.</small> </div> `; return; } mediaGrid.innerHTML = items.map(item => { if (item.type === 'image') { return ` <div class="media-item"> <div class="media-thumb-wrap" onclick="openMedia('image', '${item.url}', '${item.name}')"> <img src="${item.url}" class="media-thumb" loading="lazy"> </div> <div class="media-info"> <div class="media-meta-left"> <span class="media-name">${item.name}</span> <span class="media-sub">${item.size_formatted} &bull; ${item.modified.split(' ')[1]}</span> </div> <div class="media-actions"> <a href="${item.url}" download class="media-btn" title="Download">⬇</a> </div> </div> </div> `; } else { return ` <div class="media-item"> <div class="media-thumb-wrap" onclick="openMedia('video', '${item.url}', '${item.name}')"> <video src="${item.url}" class="media-thumb" preload="metadata"></video> <div class="play-badge">&#9658;</div> </div> <div class="media-info"> <div class="media-meta-left"> <span class="media-name">${item.name}</span> <span class="media-sub">${item.size_formatted} &bull; ${item.modified.split(' ')[1]}</span> </div> <div class="media-actions"> <a href="${item.url}" download class="media-btn" title="Download">⬇</a> </div> </div> </div> `; } }).join(''); } catch (e) { console.error("Gallery load error:", e); } } // Media Modal function openMedia(type, url, title) { modalTitle.textContent = title; if (type === 'image') { modalBody.innerHTML = `<img src="${url}">`; } else { modalBody.innerHTML = `<video src="${url}" controls autoplay style="width:100%; height:auto;"></video>`; } mediaModal.classList.remove('hidden'); } function closeModal() { modalBody.innerHTML = ''; mediaModal.classList.add('hidden'); } modalCloseBtn.addEventListener('click', closeModal); document.querySelector('#mediaModal .modal-backdrop').addEventListener('click', closeModal); // Add Camera Modal addCamBtn.addEventListener('click', () => { addCamModal.classList.remove('hidden'); }); function closeAddCamModal() { addCamModal.classList.add('hidden'); addCamForm.reset(); } addCamCloseBtn.addEventListener('click', closeAddCamModal); cancelAddCamBtn.addEventListener('click', closeAddCamModal); document.querySelector('#addCamModal .modal-backdrop').addEventListener('click', closeAddCamModal); addCamForm.addEventListener('submit', async (e) => { e.preventDefault(); const name = document.getElementById('camNameInput').value.trim(); const type = document.getElementById('camTypeSelect').value; const source = document.getElementById('camSourceInput').value.trim(); try { const res = await fetch('/api/cameras', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, type, source }) }); const data = await res.json(); if (res.ok) { showToast(`Camera '${name}' linked successfully!`); closeAddCamModal(); await updateStatus(); } else { showToast(`Error: ${data.detail || 'Failed to add camera'}`); } } catch (err) { showToast('Failed to contact server'); } }); // Fullscreen fullscreenBtn.addEventListener('click', () => { if (!document.fullscreenElement) { streamBox.requestFullscreen().catch(err => alert(err.message)); } else { document.exitFullscreen(); } }); // Real-Time Visual Filter Listeners filterPills.forEach(pill => { pill.addEventListener('click', () => { const f = pill.dataset.filter; setFilter(f); }); }); // Digital Zoom Slider & Preset Buttons zoomSlider.addEventListener('input', (e) => { applyZoom(e.target.value); }); zoomInBtn.addEventListener('click', () => { applyZoom(currentZoom + 0.2); }); zoomOutBtn.addEventListener('click', () => { applyZoom(currentZoom - 0.2); }); zoomPresetBtns.forEach(btn => { btn.addEventListener('click', () => { applyZoom(parseFloat(btn.dataset.zoom)); }); }); // Auto-Tracking Target Listeners trackPills.forEach(pill => { pill.addEventListener('click', () => { const mode = pill.dataset.mode; setAutoTracking(mode); }); }); resetFocusBtn.addEventListener('click', resetZoomAndFocus); // Interactive Viewport Click-to-Focus, Wheel Zoom & Double-Click streamBox.addEventListener('click', (e) => { if (e.target.closest('#standbyOverlay')) return; const rect = streamBox.getBoundingClientRect(); const clickX = (e.clientX - rect.left) / rect.width; const clickY = (e.clientY - rect.top) / rect.height; focusOnPoint(clickX, clickY); }); streamBox.addEventListener('wheel', (e) => { e.preventDefault(); const step = e.deltaY < 0 ? 0.2 : -0.2; applyZoom(currentZoom + step); }, { passive: false }); streamBox.addEventListener('dblclick', (e) => { e.preventDefault(); const targetZoom = (currentZoom > 1.2) ? 1.0 : 2.0; applyZoom(targetZoom); }); // AI Detection, HUD Overlay & Captions Toggles aiToggleBtn.addEventListener('click', toggleAIDetection); if (hudOverlayBtn) { hudOverlayBtn.addEventListener('click', toggleHudOverlay); } if (captionsToggleBtn) { captionsToggleBtn.addEventListener('click', toggleLiveCaptions); } // Action Buttons powerBtn.addEventListener('click', toggleCameraPower); overlayStartBtn.addEventListener('click', toggleCameraPower); snapBtn.addEventListener('click', takeSnapshot); recordBtn.addEventListener('click', toggleRecording); // Gallery Filter Tabs filterBtns.forEach(btn => { btn.addEventListener('click', () => { filterBtns.forEach(b => b.classList.remove('active')); btn.classList.add('active'); galleryFilter = btn.dataset.filter; loadGallery(); }); }); // High-FPS Browser Webcam Bridge (WebSocket + HTTP Fallback) const hiddenVideo = document.createElement('video'); hiddenVideo.autoplay = true; hiddenVideo.playsInline = true; hiddenVideo.muted = true; const hiddenCanvas = document.createElement('canvas'); hiddenCanvas.width = 640; hiddenCanvas.height = 360; const hiddenCtx = hiddenCanvas.getContext('2d'); let pushingFrame = false; function startBrowserWebcamBridge(silent = false) { if (browserWebcamActive) return; if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { if (!silent) showToast('Browser Webcam API requires HTTPS or localhost'); return; } navigator.mediaDevices.getUserMedia({ video: { width: { ideal: 640 }, height: { ideal: 360 }, frameRate: { ideal: 30 } }, audio: false }).then(stream => { browserWebcamStream = stream; hiddenVideo.srcObject = stream; browserWebcamActive = true; if (browserWebcamBtn) { browserWebcamBtn.classList.add('active'); browserWebcamBtn.innerHTML = '<span>🟢 Webcam Active</span>'; } if (!silent) showToast('📷 Browser Webcam linked at high FPS'); // Open binary WebSocket for ultra-low-latency frame streaming try { const wsProto = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; browserWebcamWs = new WebSocket(`${wsProto}//${window.location.host}/ws/frame`); } catch (_) { browserWebcamWs = null; } function pumpNextFrame() { if (!browserWebcamActive) return; if (pushingFrame || hiddenVideo.readyState < 2) { setTimeout(pumpNextFrame, 25); return; } pushingFrame = true; hiddenCtx.drawImage(hiddenVideo, 0, 0, 640, 360); hiddenCanvas.toBlob(blob => { if (!blob || !browserWebcamActive) { pushingFrame = false; return; } if (browserWebcamWs && browserWebcamWs.readyState === WebSocket.OPEN) { blob.arrayBuffer().then(buf => { browserWebcamWs.send(buf); pushingFrame = false; setTimeout(pumpNextFrame, 35); }).catch(() => { pushingFrame = false; setTimeout(pumpNextFrame, 45); }); } else { fetch('/api/frame/push', { method: 'POST', body: blob }) .catch(() => {}) .finally(() => { pushingFrame = false; setTimeout(pumpNextFrame, 40); }); } }, 'image/jpeg', 0.80); } pumpNextFrame(); }).catch(() => { if (!silent) showToast('Camera permission denied or no webcam found'); }); } function stopBrowserWebcamBridge() { browserWebcamActive = false; if (browserWebcamWs) { try { browserWebcamWs.close(); } catch (_) {} browserWebcamWs = null; } if (browserWebcamStream) { browserWebcamStream.getTracks().forEach(t => t.stop()); browserWebcamStream = null; } if (browserWebcamBtn) { browserWebcamBtn.classList.remove('active'); browserWebcamBtn.innerHTML = '<span>📷 Browser Webcam</span>'; } showToast('Browser Webcam disconnected'); } if (browserWebcamBtn) { browserWebcamBtn.addEventListener('click', () => { if (browserWebcamActive) { stopBrowserWebcamBridge(); } else { startBrowserWebcamBridge(false); } }); } // Lifecycle Boot setInterval(updateStatus, 1000); updateStatus(); loadGallery(); // Auto-start Browser Webcam Bridge on Hugging Face Spaces if (window.location.hostname.includes('hf.space') || window.location.search.includes('browser_cam=1')) { startBrowserWebcamBridge(true); }
let cameraRunning = false;
let isRecording = false;
let currentFilter = 'normal';
let activeCameraId = 'laptop_cam';
let galleryFilter = 'all';
let currentZoom = 1.0;
let lastUserZoomTime = 0;
let aiDetectionActive = true;
let hudOverlayActive = true;
let captionsActive = false;
let speechRecognizer = null;
let activeTargetId = null;
let activeTrackingMode = 'off';
let browserWebcamActive = false;
let browserWebcamStream = null;
let browserWebcamWs = null;
// DOM Elements
const streamFeed = document.getElementById('streamFeed');
const streamBox = document.getElementById('streamBox');
const standbyOverlay = document.getElementById('standbyOverlay');
const flashEffect = document.getElementById('flashEffect');
const overlayStartBtn = document.getElementById('overlayStartBtn');
const camOrb = document.getElementById('camOrb');
const camStateBadge = document.getElementById('camStateBadge');
const camResBadge = document.getElementById('camResBadge');
const camFpsBadge = document.getElementById('camFpsBadge');
const activeFilterBadge = document.getElementById('activeFilterBadge');
const gpuProviderBadge = document.getElementById('gpuProviderBadge');
const recordingTag = document.getElementById('recordingTag');
const recTimerText = document.getElementById('recTimerText');
const clientCount = document.getElementById('clientCount');
const camTabsContainer = document.getElementById('camTabsContainer');
const addCamBtn = document.getElementById('addCamBtn');
const browserWebcamBtn = document.getElementById('browserWebcamBtn');
const filterPills = document.querySelectorAll('.filter-pill');
// Zoom, PTZ Focus & AI Detection Elements
const zoomSlider = document.getElementById('zoomSlider');
const zoomValText = document.getElementById('zoomValText');
const zoomInBtn = document.getElementById('zoomInBtn');
const zoomOutBtn = document.getElementById('zoomOutBtn');
const zoomPresetBtns = document.querySelectorAll('.zoom-preset-btn');
const trackPills = document.querySelectorAll('.track-pill');
const resetFocusBtn = document.getElementById('resetFocusBtn');
const aiToggleBtn = document.getElementById('aiToggleBtn');
const aiBtnText = document.getElementById('aiBtnText');
const hudOverlayBtn = document.getElementById('hudOverlayBtn');
const hudOverlayText = document.getElementById('hudOverlayText');
const captionsToggleBtn = document.getElementById('captionsToggleBtn');
const captionsBtnText = document.getElementById('captionsBtnText');
const liveCaptionsBar = document.getElementById('liveCaptionsBar');
const liveCaptionsText = document.getElementById('liveCaptionsText');
const detectionsStrip = document.getElementById('detectionsStrip');
const sceneTitleText = document.getElementById('sceneTitleText');
const sceneDescText = document.getElementById('sceneDescText');
const sceneStatusPill = document.getElementById('sceneStatusPill');
const trackingScoreSummary = document.getElementById('trackingScoreSummary');
const trackingClassBars = document.getElementById('trackingClassBars');
const trackingPartsRow = document.getElementById('trackingPartsRow');
const powerBtn = document.getElementById('powerBtn');
const powerBtnText = document.getElementById('powerBtnText');
const snapBtn = document.getElementById('snapBtn');
const recordBtn = document.getElementById('recordBtn');
const recordBtnText = document.getElementById('recordBtnText');
const fullscreenBtn = document.getElementById('fullscreenBtn');
const lanUrlCode = document.getElementById('lanUrlCode');
const tsUrlCode = document.getElementById('tsUrlCode');
const tsUrlContainer = document.getElementById('tsUrlContainer');
const mediaGrid = document.getElementById('mediaGrid');
const mediaCountBadge = document.getElementById('mediaCountBadge');
const filterBtns = document.querySelectorAll('.filter-btn');
const mediaModal = document.getElementById('mediaModal');
const modalTitle = document.getElementById('modalTitle');
const modalBody = document.getElementById('modalBody');
const modalCloseBtn = document.getElementById('modalCloseBtn');
const addCamModal = document.getElementById('addCamModal');
const addCamForm = document.getElementById('addCamForm');
const addCamCloseBtn = document.getElementById('addCamCloseBtn');
const cancelAddCamBtn = document.getElementById('cancelAddCamBtn');
const toastEl = document.getElementById('toast');
function showToast(msg, duration = 3000) {
toastEl.textContent = msg;
toastEl.classList.remove('hidden');
setTimeout(() => {
toastEl.classList.add('hidden');
}, duration);
}