Files
Mav-Mobile-UI/src/hooks/useGetOverviewSnapshot.ts
Toba Ojo ea93053dd3 - sesstions start by default
- added restart session button
2025-11-19 11:52:37 +00:00

150 lines
3.7 KiB
TypeScript

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<Blob> {
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<HTMLCanvasElement | null>(null);
const latestBitmapRef = useRef<ImageBitmap | null>(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 };
}