import { useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { VideoOff, WifiOff } from 'lucide-react'; import { getAuthToken, withStreamToken } from '../api/client'; export type CameraTileMode = 'live' | 'snapshot' | 'paused'; interface CameraTileProps { printerId: number; printerName: string; cameraRotation?: number; mode: CameraTileMode; snapshotIntervalMs: number; connected: boolean; onClick?: () => void; } // Tiles render lighter than EmbeddedCameraViewer's full window: lower fps, // no drag/resize/zoom shell, and snapshot fallback when off-cap. The server // still does the MJPEG fan-out, so per-tile cost is one TLS pull on the wire. const LIVE_FPS = 8; export function CameraTile({ printerId, printerName, cameraRotation = 0, mode, snapshotIntervalMs, connected, onClick, }: CameraTileProps) { const { t } = useTranslation(); const [bust, setBust] = useState(0); const [errored, setErrored] = useState(false); const lastModeRef = useRef(mode); // Tell the backend to release its MJPEG transcoder when this tile stops // being live — either by unmounting or by transitioning to snapshot/paused. // EmbeddedCameraViewer uses the same /camera/stop with keepalive on unmount. useEffect(() => { const wasLive = lastModeRef.current === 'live'; const isLive = mode === 'live'; lastModeRef.current = mode; if (wasLive && !isLive) { const headers: Record = {}; const token = getAuthToken(); if (token) headers['Authorization'] = `Bearer ${token}`; fetch(`/api/v1/printers/${printerId}/camera/stop`, { method: 'POST', keepalive: true, headers, }).catch(() => {}); } setErrored(false); setBust((b) => b + 1); }, [mode, printerId]); useEffect(() => { return () => { if (lastModeRef.current === 'live') { const headers: Record = {}; const token = getAuthToken(); if (token) headers['Authorization'] = `Bearer ${token}`; fetch(`/api/v1/printers/${printerId}/camera/stop`, { method: 'POST', keepalive: true, headers, }).catch(() => {}); } }; }, [printerId]); useEffect(() => { if (mode !== 'snapshot') return; const interval = setInterval(() => setBust((b) => b + 1), snapshotIntervalMs); return () => clearInterval(interval); }, [mode, snapshotIntervalMs]); const liveUrl = withStreamToken( `/api/v1/printers/${printerId}/camera/stream?fps=${LIVE_FPS}&t=${bust}`, ); const snapshotUrl = withStreamToken( `/api/v1/printers/${printerId}/camera/snapshot?t=${bust}`, ); const handleClick = () => { if (onClick) onClick(); }; const transform = cameraRotation ? `rotate(${cameraRotation}deg)` : undefined; return ( ); }