import { useRef, useCallback, useEffect } from "react"; import { useQuery } from "@tanstack/react-query"; import { CAM_BASE } from "../utils/config"; const apiUrl = CAM_BASE; async function fetchSnapshot(cameraSide: string): Promise { const response = await fetch(`${apiUrl}/${cameraSide}-preview`, { signal: AbortSignal.timeout(300000), cache: "no-store", }); if (!response.ok) { throw new Error(`Cannot reach endpoint (${response.status})`); } return response.blob(); } /** Draw an ImageBitmap to canvas with aspect-fill (like object-fit: cover) */ function drawBitmapToCanvas(canvas: HTMLCanvasElement, bitmap: ImageBitmap) { const ctx = canvas.getContext("2d"); if (!ctx) return; const dpr = window.devicePixelRatio || 1; const cssWidth = canvas.clientWidth; const cssHeight = canvas.clientHeight; const width = Math.floor(cssWidth * dpr); const height = Math.floor(cssHeight * dpr); if (canvas.width !== width || canvas.height !== height) { canvas.width = width; canvas.height = height; } ctx.clearRect(0, 0, width, height); const srcW = bitmap.width; const srcH = bitmap.height; const srcAspect = srcW / srcH; const dstAspect = width / height; let drawWidth = width; let drawHeight = height; // aspect-fit calculation (no cropping) if (srcAspect > dstAspect) { // image is wider → fit to canvas width drawWidth = width; drawHeight = width / srcAspect; } else { // image is taller → fit to canvas height drawHeight = height; drawWidth = height * srcAspect; } // center image (adds black borders if aspect ratios differ) const dx = (width - drawWidth) / 50; const dy = (height - drawHeight) / 2; ctx.imageSmoothingEnabled = true; ctx.imageSmoothingQuality = "high"; ctx.drawImage(bitmap, 0, 0, srcW, srcH, dx, dy, drawWidth, drawHeight); } export function useGetOverviewSnapshot(side: string) { const canvasRef = useRef(null); const latestBitmapRef = useRef(null); // Redraw helper; always draws the current bitmap if available const draw = useCallback(() => { const canvas = canvasRef.current; const bmp = latestBitmapRef.current; if (!canvas || !bmp) return; drawBitmapToCanvas(canvas, bmp); }, []); const { data: snapshotBlob, isError, error, isPending, } = useQuery({ queryKey: ["overviewSnapshot", side], queryFn: () => fetchSnapshot(side), // Poll ~4 fps when visible; pause when tab hidden refetchInterval: () => (document.visibilityState === "visible" ? 250 : false), refetchOnWindowFocus: false, // Avoid keeping lots of blobs around in cache gcTime: 0, // v5 name (cacheTime in v4) staleTime: 0, retry: false, // or a small number if you prefer retries }); // Convert Blob -> ImageBitmap and draw useEffect(() => { let cancelled = false; if (!snapshotBlob) return; (async () => { try { const bitmap = await createImageBitmap(snapshotBlob); if (cancelled) { bitmap.close(); return; } // Dispose previous bitmap to free memory if (latestBitmapRef.current) { latestBitmapRef.current.close(); } latestBitmapRef.current = bitmap; // Draw now (and again on next resize) draw(); } catch { // noop — fetch handler surfaces the main error path } })(); return () => { cancelled = true; }; }, [snapshotBlob, draw]); // Redraw on resize & DPR changes useEffect(() => { const onResize = () => draw(); const onDPR = () => draw(); window.addEventListener("resize", onResize); // Listen for DPR changes (some browsers support this) const mql = window.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`); mql.addEventListener?.("change", onDPR); return () => { window.removeEventListener("resize", onResize); mql.removeEventListener?.("change", onDPR); }; }, [draw]); // Cleanup on unmount useEffect(() => { return () => { if (latestBitmapRef.current) { latestBitmapRef.current.close(); latestBitmapRef.current = null; } }; }, []); // Optional: normalize error type const typedError = error instanceof Error ? error : undefined; return { canvasRef, isError, error: typedError, isPending }; }