'use client'; import { useState, useEffect, useRef } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { X, ExternalLink, RefreshCw, MapPin, Camera, Maximize2 } from 'lucide-react'; import Hls from 'hls.js'; interface CameraViewerProps { camera: any | null; onClose: () => void; onLocate?: (lat: number, lng: number) => void; } export default function CameraViewer({ camera, onClose, onLocate }: CameraViewerProps) { const [imageUrl, setImageUrl] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(false); const [retryCount, setRetryCount] = useState(0); const [fullscreen, setFullscreen] = useState(false); const [currentTime, setCurrentTime] = useState(''); useEffect(() => { const iv = setInterval(() => { const now = new Date(); setCurrentTime(now.toISOString().split('T')[1].slice(0, 8) + 'Z'); }, 1000); return () => clearInterval(iv); }, []); const camId = camera ? `CAM-${Math.abs(camera.lat * 10000).toFixed(0).padStart(4, '0').slice(-4)}-${Math.abs(camera.lng * 10000).toFixed(0).padStart(4, '0').slice(-4)}` : 'UNKNOWN'; const videoRef = useRef(null); const hlsRef = useRef(null); const streamType = camera?.stream_type || 'jpg'; const externalFeedUrl = camera?.external_url || camera?.feed_url; const externalOnly = Boolean(camera?.external_url && !camera?.feed_url && !camera?.stream_url); useEffect(() => { if (!camera) return; setLoading(true); setError(false); setImageUrl(null); // Cleanup previous HLS instance if (hlsRef.current) { hlsRef.current.destroy(); hlsRef.current = null; } if (externalOnly) { setLoading(false); return; } if (streamType === 'hls' && camera.stream_url) { if (Hls.isSupported() && videoRef.current) { const hls = new Hls({ enableWorker: false }); hlsRef.current = hls; hls.loadSource(camera.stream_url); hls.attachMedia(videoRef.current); hls.on(Hls.Events.MANIFEST_PARSED, () => { setLoading(false); videoRef.current?.play().catch(() => {}); }); hls.on(Hls.Events.ERROR, (event, data) => { if (data.fatal) setError(true); }); } else if (videoRef.current?.canPlayType('application/vnd.apple.mpegurl')) { videoRef.current.src = camera.stream_url; videoRef.current.addEventListener('loadedmetadata', () => { setLoading(false); videoRef.current?.play().catch(() => {}); }); } return; } if (streamType === 'mjpeg' && camera.stream_url) { setLoading(false); return; } if ((streamType === 'iframe' || streamType === 'mp4') && camera.stream_url) { setLoading(false); return; } // JPG fallback const targetUrl = camera.feed_url || camera.stream_url; if (targetUrl) { const url = targetUrl.includes('?') ? `${targetUrl}&_t=${Date.now()}` : `${targetUrl}?_t=${Date.now()}`; setImageUrl(url); } else { setError(true); setLoading(false); } }, [camera, streamType, externalOnly, retryCount]); // Auto-refresh for JPGs useEffect(() => { if (streamType !== 'jpg' || (!camera?.feed_url && !camera?.stream_url)) return; const targetUrl = camera.feed_url || camera.stream_url; if (!targetUrl) return; const iv = setInterval(() => { const url = targetUrl.includes('?') ? `${targetUrl}&_t=${Date.now()}` : `${targetUrl}?_t=${Date.now()}`; setImageUrl(url); }, 5000); // 5s refresh for JPG return () => clearInterval(iv); }, [camera, streamType]); if (!camera) return null; return (
{/* Tactical Grid Overlay on background */}
{/* Tactical Header */}
{/* Top Meta Bar */}
{camId} {camera.lat?.toFixed(4)}, {camera.lng?.toFixed(4)}
{currentTime} SECURE UPLINK
{/* Main Title Bar */}

{camera.name}

{camera.city}, {camera.country} • SOURCE: {camera.source}

{/* Controls */}
{streamType === 'jpg' && ( )} {camera.lat && camera.lng && ( )}
{/* Camera Feed */}
{/* Tactical CRT Overlay */}
{loading && !error && !externalOnly && (
DECRYPTING FEED...
)} {externalOnly ? (

SECURE FEED ENCRYPTED

This feed requires external clearance

ACCESS TERMINAL
) : error ? (
FEED UNAVAILABLE Camera may be offline or restricted
) : streamType === 'hls' ? (