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(); } 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; if (srcAspect > dstAspect) { drawWidth = width; drawHeight = width / srcAspect; } else { drawHeight = height; drawWidth = height * srcAspect; } 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); 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), refetchInterval: () => (document.visibilityState === "visible" ? 250 : false), refetchOnWindowFocus: false, gcTime: 0, staleTime: 0, retry: false, }); useEffect(() => { let cancelled = false; if (!snapshotBlob) return; (async () => { try { const bitmap = await createImageBitmap(snapshotBlob); if (cancelled) { bitmap.close(); return; } if (latestBitmapRef.current) { latestBitmapRef.current.close(); } latestBitmapRef.current = bitmap; draw(); } catch { // noop — fetch handler surfaces the main error path } })(); return () => { cancelled = true; }; }, [snapshotBlob, draw]); useEffect(() => { const onResize = () => draw(); const onDPR = () => draw(); window.addEventListener("resize", onResize); const mql = window.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`); mql.addEventListener?.("change", onDPR); return () => { window.removeEventListener("resize", onResize); mql.removeEventListener?.("change", onDPR); }; }, [draw]); useEffect(() => { const el = canvasRef.current?.parentElement; if (!el) return; const ro = new ResizeObserver(() => draw()); ro.observe(el); return () => ro.disconnect(); }, [draw]); useEffect(() => { return () => { if (latestBitmapRef.current) { latestBitmapRef.current.close(); latestBitmapRef.current = null; } }; }, []); const typedError = error instanceof Error ? error : undefined; return { canvasRef, isError, error: typedError, isPending }; }