From 2e62f5822cbf75ede86cf3cb7f5b35f5509713f1 Mon Sep 17 00:00:00 2001 From: ZGaetano Date: Tue, 14 Apr 2026 10:13:16 -0400 Subject: [PATCH] feat: add manual preview start button and HLS polling --- frontend/src/components/VideoPreview.tsx | 125 ++++++++++++++++++----- 1 file changed, 102 insertions(+), 23 deletions(-) diff --git a/frontend/src/components/VideoPreview.tsx b/frontend/src/components/VideoPreview.tsx index 971ce43..a8553f3 100644 --- a/frontend/src/components/VideoPreview.tsx +++ b/frontend/src/components/VideoPreview.tsx @@ -9,36 +9,93 @@ export function VideoPreview({ portIndex }: VideoPreviewProps) { const videoRef = useRef(null); const hlsRef = useRef(null); const [hasSignal, setHasSignal] = useState(false); + const [starting, setStarting] = useState(false); + const [startError, setStartError] = useState(null); + + const src = `/hls/port_${portIndex}.m3u8`; useEffect(() => { - const src = `/hls/port_${portIndex}.m3u8`; setHasSignal(false); + setStartError(null); if (!videoRef.current) return; - if (Hls.isSupported()) { - const hls = new Hls({ enableWorker: true, lowLatencyMode: true }); - hlsRef.current = hls; - hls.loadSource(src); - hls.attachMedia(videoRef.current); - hls.on(Hls.Events.MANIFEST_PARSED, () => { - setHasSignal(true); - videoRef.current?.play().catch(console.error); - }); - hls.on(Hls.Events.ERROR, (_event, data) => { - if (data.fatal) setHasSignal(false); - }); - } else if (videoRef.current.canPlayType('application/vnd.apple.mpegurl')) { - videoRef.current.src = src; - videoRef.current.addEventListener('canplay', () => setHasSignal(true), { once: true }); - videoRef.current.play().catch(console.error); - } + let destroyed = false; + + const tryLoad = () => { + if (destroyed || !videoRef.current) return; + + if (Hls.isSupported()) { + const hls = new Hls({ + enableWorker: true, + lowLatencyMode: true, + // Retry aggressively — the m3u8 may not exist yet + manifestLoadingMaxRetry: 20, + manifestLoadingRetryDelay: 2000, + }); + hlsRef.current = hls; + hls.loadSource(src); + hls.attachMedia(videoRef.current); + hls.on(Hls.Events.MANIFEST_PARSED, () => { + if (!destroyed) { + setHasSignal(true); + videoRef.current?.play().catch(console.error); + } + }); + hls.on(Hls.Events.ERROR, (_event, data) => { + if (data.fatal && !destroyed) { + setHasSignal(false); + } + }); + } else if (videoRef.current.canPlayType('application/vnd.apple.mpegurl')) { + videoRef.current.src = src; + videoRef.current.addEventListener('canplay', () => { + if (!destroyed) setHasSignal(true); + }, { once: true }); + videoRef.current.play().catch(console.error); + } + }; + + tryLoad(); return () => { + destroyed = true; hlsRef.current?.destroy(); hlsRef.current = null; }; - }, [portIndex]); + }, [portIndex, src]); + + const handleStartPreview = async () => { + setStarting(true); + setStartError(null); + try { + const res = await fetch(`/api/ports/${portIndex}/preview/start`, { method: 'POST' }); + if (!res.ok) { + const body = await res.json().catch(() => ({})); + throw new Error(body.detail ?? res.statusText); + } + // Destroy and reload HLS after a short delay for the m3u8 to appear + setTimeout(() => { + hlsRef.current?.destroy(); + hlsRef.current = null; + if (videoRef.current && Hls.isSupported()) { + const hls = new Hls({ enableWorker: true, lowLatencyMode: true }); + hlsRef.current = hls; + hls.loadSource(`/hls/port_${portIndex}.m3u8`); + hls.attachMedia(videoRef.current); + hls.on(Hls.Events.MANIFEST_PARSED, () => { + setHasSignal(true); + videoRef.current?.play().catch(console.error); + }); + hls.on(Hls.Events.ERROR, (_e, d) => { if (d.fatal) setHasSignal(false); }); + } + }, 3000); + } catch (e: any) { + setStartError(e.message ?? 'Failed to start preview'); + } finally { + setStarting(false); + } + }; return (
@@ -55,6 +112,7 @@ export function VideoPreview({ portIndex }: VideoPreviewProps) { position: 'absolute', inset: 0, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', background: 'linear-gradient(135deg, #090d15 0%, #0e1525 100%)', + gap: 14, }}> {/* Grid pattern */}
- + Port {portIndex} — No HLS Feed + + {startError && ( + + {startError} + + )}
)} @@ -78,11 +157,11 @@ export function VideoPreview({ portIndex }: VideoPreviewProps) { fontFamily: 'var(--font-mono)', fontSize: 9, letterSpacing: '0.15em', padding: '3px 8px', background: 'rgba(0,0,0,0.7)', - border: '1px solid rgba(26,58,255,0.4)', - color: '#7aa4ff', + border: `1px solid ${hasSignal ? 'rgba(0,230,118,0.4)' : 'rgba(26,58,255,0.4)'}`, + color: hasSignal ? 'var(--green-safe)' : '#7aa4ff', borderRadius: 2, }}> - HLS ∙ ~5s + {hasSignal ? 'LIVE ∙ HLS' : 'HLS ∙ ~5s'}
);