Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>PTZ Vision Control</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://unpkg.com/feather-icons"></script> | |
| <style> | |
| .camera-container { | |
| position: relative; | |
| padding-bottom: 56.25%; /* 16:9 aspect ratio */ | |
| height: 0; | |
| overflow: hidden; | |
| } | |
| .camera-feed { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background-color: #111; | |
| } | |
| .control-panel { | |
| transition: all 0.3s ease; | |
| } | |
| .ptz-value { | |
| font-family: monospace; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-900 text-white"> | |
| <div class="container mx-auto px-4 py-8"> | |
| <header class="mb-8"> | |
| <h1 class="text-3xl font-bold text-center mb-2">PTZ Vision Control</h1> | |
| <p class="text-center text-gray-400">Thermal & Daylight Camera Control System</p> | |
| </header> | |
| <div class="bg-gray-800 rounded-lg overflow-hidden shadow-xl mb-8"> | |
| <div class="bg-gray-700 px-4 py-3 flex justify-between items-center"> | |
| <div class="flex space-x-4"> | |
| <button id="thermal-btn" class="bg-red-600 hover:bg-red-700 text-white py-1 px-3 rounded flex items-center"> | |
| <i data-feather="thermometer" class="mr-2"></i> Thermal | |
| </button> | |
| <button id="daylight-btn" class="bg-blue-600 hover:bg-blue-700 text-white py-1 px-3 rounded flex items-center"> | |
| <i data-feather="sun" class="mr-2"></i> Daylight | |
| </button> | |
| </div> | |
| <span id="camera-status" class="bg-red-500 text-xs px-2 py-1 rounded-full">LIVE (Thermal)</span> | |
| </div> | |
| <div class="camera-container"> | |
| <img id="thermal-feed" class="camera-feed block" src="http://static.photos/black/640x360" alt="Thermal Camera Feed"> | |
| <img id="daylight-feed" class="camera-feed hidden" src="http://static.photos/nature/640x360" alt="Daylight Camera Feed"> | |
| </div> | |
| </div> | |
| <!-- Control Panel --> | |
| <div class="bg-gray-800 rounded-lg shadow-xl overflow-hidden mb-8"> | |
| <div class="bg-gray-700 px-4 py-3 flex justify-between items-center cursor-pointer" id="control-header"> | |
| <h2 class="font-semibold">PTZ Controls</h2> | |
| <i data-feather="chevron-down" class="transform transition-transform"></i> | |
| </div> | |
| <div class="control-panel p-4" id="control-content"> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-6"> | |
| <!-- PTZ Status --> | |
| <div class="bg-gray-700 p-4 rounded-lg"> | |
| <h3 class="font-medium mb-4 flex items-center"> | |
| <i data-feather="navigation" class="mr-2"></i> Current Position | |
| </h3> | |
| <div class="grid grid-cols-3 gap-2"> | |
| <div class="bg-gray-600 p-2 rounded"> | |
| <p class="text-xs text-gray-400">PAN</p> | |
| <p class="ptz-value">0.0°</p> | |
| </div> | |
| <div class="bg-gray-600 p-2 rounded"> | |
| <p class="text-xs text-gray-400">TILT</p> | |
| <p class="ptz-value">0.0°</p> | |
| </div> | |
| <div class="bg-gray-600 p-2 rounded"> | |
| <p class="text-xs text-gray-400">ZOOM</p> | |
| <p class="ptz-value">1.0x</p> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- PTZ Controls --> | |
| <div class="bg-gray-700 p-4 rounded-lg col-span-1 md:col-span-2"> | |
| <h3 class="font-medium mb-4 flex items-center"> | |
| <i data-feather="move" class="mr-2"></i> Manual Control | |
| </h3> | |
| <div class="grid grid-cols-3 gap-4"> | |
| <!-- Pan/Tilt Controls --> | |
| <div class="col-span-2"> | |
| <div class="relative bg-gray-900 rounded-lg p-4" style="aspect-ratio: 1/1;"> | |
| <div class="absolute inset-0 flex items-center justify-center"> | |
| <div class="grid grid-cols-3 grid-rows-3 w-full h-full"> | |
| <button class="p-2 flex items-center justify-center opacity-0"></button> | |
| <button id="tilt-up" class="p-2 flex items-center justify-center hover:bg-gray-700 rounded-t-lg"> | |
| <i data-feather="chevron-up"></i> | |
| </button> | |
| <button class="p-2 flex items-center justify-center opacity-0"></button> | |
| <button id="pan-left" class="p-2 flex items-center justify-center hover:bg-gray-700 rounded-l-lg"> | |
| <i data-feather="chevron-left"></i> | |
| </button> | |
| <button class="p-2 flex items-center justify-center hover:bg-gray-700 rounded-full"> | |
| <i data-feather="target"></i> | |
| </button> | |
| <button id="pan-right" class="p-2 flex items-center justify-center hover:bg-gray-700 rounded-r-lg"> | |
| <i data-feather="chevron-right"></i> | |
| </button> | |
| <button class="p-2 flex items-center justify-center opacity-0"></button> | |
| <button id="tilt-down" class="p-2 flex items-center justify-center hover:bg-gray-700 rounded-b-lg"> | |
| <i data-feather="chevron-down"></i> | |
| </button> | |
| <button class="p-2 flex items-center justify-center opacity-0"></button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Zoom Controls --> | |
| <div class="flex flex-col justify-between"> | |
| <div class="bg-gray-900 rounded-lg p-4 flex flex-col items-center"> | |
| <button id="zoom-in" class="p-2 hover:bg-gray-700 rounded-full mb-4"> | |
| <i data-feather="zoom-in"></i> | |
| </button> | |
| <div class="w-full bg-gray-700 h-32 rounded-full flex items-center px-2 mb-4"> | |
| <div class="bg-blue-500 w-1/2 h-3 rounded-full"></div> | |
| </div> | |
| <button id="zoom-out" class="p-2 hover:bg-gray-700 rounded-full"> | |
| <i data-feather="zoom-out"></i> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Presets --> | |
| <div class="bg-gray-700 p-4 rounded-lg col-span-1 md:col-span-2"> | |
| <h3 class="font-medium mb-4 flex items-center"> | |
| <i data-feather="bookmark" class="mr-2"></i> Camera Presets | |
| </h3> | |
| <div class="grid grid-cols-3 md:grid-cols-6 gap-2"> | |
| <button class="bg-gray-600 hover:bg-gray-500 py-2 rounded">1</button> | |
| <button class="bg-gray-600 hover:bg-gray-500 py-2 rounded">2</button> | |
| <button class="bg-gray-600 hover:bg-gray-500 py-2 rounded">3</button> | |
| <button class="bg-gray-600 hover:bg-gray-500 py-2 rounded">4</button> | |
| <button class="bg-gray-600 hover:bg-gray-500 py-2 rounded">5</button> | |
| <button class="bg-gray-600 hover:bg-gray-500 py-2 rounded">6</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Configuration Panel --> | |
| <div class="bg-gray-800 rounded-lg shadow-xl overflow-hidden"> | |
| <div class="bg-gray-700 px-4 py-3 flex justify-between items-center cursor-pointer" id="config-header"> | |
| <h2 class="font-semibold">Configuration</h2> | |
| <i data-feather="chevron-down" class="transform transition-transform"></i> | |
| </div> | |
| <div class="control-panel p-4 hidden" id="config-content"> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-6"> | |
| <!-- RTSP Settings --> | |
| <div class="bg-gray-700 p-4 rounded-lg"> | |
| <h3 class="font-medium mb-4 flex items-center"> | |
| <i data-feather="video" class="mr-2"></i> RTSP Streams | |
| </h3> | |
| <div class="space-y-4"> | |
| <div> | |
| <label class="block text-sm text-gray-400 mb-1">Thermal Camera URL</label> | |
| <input type="text" class="w-full bg-gray-800 border border-gray-600 rounded px-3 py-2" value="rtsp://thermal-camera/stream"> | |
| </div> | |
| <div> | |
| <label class="block text-sm text-gray-400 mb-1">Daylight Camera URL</label> | |
| <input type="text" class="w-full bg-gray-800 border border-gray-600 rounded px-3 py-2" value="rtsp://daylight-camera/stream"> | |
| </div> | |
| <button class="bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded"> | |
| Update Streams | |
| </button> | |
| </div> | |
| </div> | |
| <!-- UDP Settings --> | |
| <div class="bg-gray-700 p-4 rounded-lg"> | |
| <h3 class="font-medium mb-4 flex items-center"> | |
| <i data-feather="send" class="mr-2"></i> UDP Communication | |
| </h3> | |
| <div class="space-y-4"> | |
| <div> | |
| <label class="block text-sm text-gray-400 mb-1">Server IP</label> | |
| <input type="text" class="w-full bg-gray-800 border border-gray-600 rounded px-3 py-2" value="192.168.1.100"> | |
| </div> | |
| <div> | |
| <label class="block text-sm text-gray-400 mb-1">Port</label> | |
| <input type="number" class="w-full bg-gray-800 border border-gray-600 rounded px-3 py-2" value="40000"> | |
| </div> | |
| <div class="flex items-center"> | |
| <div class="w-3 h-3 rounded-full bg-green-500 mr-2"></div> | |
| <span class="text-sm">Connected</span> | |
| </div> | |
| <button class="bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded"> | |
| Update Connection | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Advanced Settings --> | |
| <div class="bg-gray-700 p-4 rounded-lg col-span-1 md:col-span-2"> | |
| <h3 class="font-medium mb-4 flex items-center"> | |
| <i data-feather="settings" class="mr-2"></i> Advanced Settings | |
| </h3> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| <div> | |
| <label class="flex items-center"> | |
| <input type="checkbox" class="form-checkbox" checked> | |
| <span class="ml-2 text-sm">Auto-reconnect</span> | |
| </label> | |
| </div> | |
| <div> | |
| <label class="flex items-center"> | |
| <input type="checkbox" class="form-checkbox"> | |
| <span class="ml-2 text-sm">Show PTZ overlay</span> | |
| </label> | |
| </div> | |
| <div> | |
| <label class="flex items-center"> | |
| <input type="checkbox" class="form-checkbox" checked> | |
| <span class="ml-2 text-sm">Enable UDP logging</span> | |
| </label> | |
| </div> | |
| <div> | |
| <label class="flex items-center"> | |
| <input type="checkbox" class="form-checkbox"> | |
| <span class="ml-2 text-sm">Dark mode</span> | |
| </label> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // Initialize feather icons | |
| feather.replace(); | |
| // Toggle control panels | |
| document.getElementById('control-header').addEventListener('click', function() { | |
| const content = document.getElementById('control-content'); | |
| const icon = this.querySelector('i'); | |
| content.classList.toggle('hidden'); | |
| icon.classList.toggle('rotate-180'); | |
| }); | |
| document.getElementById('config-header').addEventListener('click', function() { | |
| const content = document.getElementById('config-content'); | |
| const icon = this.querySelector('i'); | |
| content.classList.toggle('hidden'); | |
| icon.classList.toggle('rotate-180'); | |
| }); | |
| // Camera switching logic | |
| let activeCamera = 'thermal'; | |
| document.getElementById('thermal-btn').addEventListener('click', function() { | |
| activeCamera = 'thermal'; | |
| document.getElementById('thermal-feed').classList.remove('hidden'); | |
| document.getElementById('daylight-feed').classList.add('hidden'); | |
| document.getElementById('camera-status').textContent = 'LIVE (Thermal)'; | |
| document.getElementById('camera-status').classList.remove('bg-green-500', 'bg-blue-500'); | |
| document.getElementById('camera-status').classList.add('bg-red-500'); | |
| }); | |
| document.getElementById('daylight-btn').addEventListener('click', function() { | |
| activeCamera = 'daylight'; | |
| document.getElementById('daylight-feed').classList.remove('hidden'); | |
| document.getElementById('thermal-feed').classList.add('hidden'); | |
| document.getElementById('camera-status').textContent = 'LIVE (Daylight)'; | |
| document.getElementById('camera-status').classList.remove('bg-red-500', 'bg-blue-500'); | |
| document.getElementById('camera-status').classList.add('bg-green-500'); | |
| }); | |
| // PTZ control buttons | |
| const ptzButtons = ['pan-left', 'pan-right', 'tilt-up', 'tilt-down', 'zoom-in', 'zoom-out']; | |
| ptzButtons.forEach(btnId => { | |
| document.getElementById(btnId).addEventListener('click', function() { | |
| // In a real implementation, this would send UDP commands | |
| console.log(`PTZ command: ${btnId} for ${activeCamera} camera`); | |
| // Update UI (mock) | |
| if(btnId === 'pan-left') updatePan(-5); | |
| if(btnId === 'pan-right') updatePan(5); | |
| if(btnId === 'tilt-up') updateTilt(5); | |
| if(btnId === 'tilt-down') updateTilt(-5); | |
| if(btnId === 'zoom-in') updateZoom(0.5); | |
| if(btnId === 'zoom-out') updateZoom(-0.5); | |
| }); | |
| }); | |
| // Mock PTZ value updates | |
| let panValue = 0; | |
| let tiltValue = 0; | |
| let zoomValue = 1; | |
| function updatePan(change) { | |
| panValue += change; | |
| document.querySelectorAll('.ptz-value')[0].textContent = `${panValue.toFixed(1)}°`; | |
| } | |
| function updateTilt(change) { | |
| tiltValue += change; | |
| document.querySelectorAll('.ptz-value')[1].textContent = `${tiltValue.toFixed(1)}°`; | |
| } | |
| function updateZoom(change) { | |
| zoomValue = Math.max(1, zoomValue + change); | |
| document.querySelectorAll('.ptz-value')[2].textContent = `${zoomValue.toFixed(1)}x`; | |
| // Update zoom slider UI | |
| const zoomPercentage = ((zoomValue - 1) / 20) * 100; | |
| document.querySelector('.bg-blue-500').style.width = `${Math.min(100, zoomPercentage)}%`; | |
| } | |
| // In a real implementation, you would have WebSocket or UDP communication here | |
| // to send commands to the PTZ cameras and receive position updates | |
| </script> | |
| </body> | |
| </html> | |