- fixed cropped live feed and amended APIs for submission
This commit is contained in:
@@ -3,34 +3,75 @@ import { useQuery } from "@tanstack/react-query";
|
||||
import { CAM_BASE } from "../utils/config";
|
||||
|
||||
const apiUrl = CAM_BASE;
|
||||
// const fetch_url = `http://100.82.205.44/Colour-preview`;
|
||||
async function fetchSnapshot(cameraSide: string) {
|
||||
|
||||
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");
|
||||
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;
|
||||
}
|
||||
|
||||
return await response.blob();
|
||||
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 latestUrlRef = useRef<string | null>(null);
|
||||
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||
const imageRef = useRef<HTMLImageElement | null>(null);
|
||||
const latestBitmapRef = useRef<ImageBitmap | null>(null);
|
||||
|
||||
const drawImage = useCallback(() => {
|
||||
// Redraw helper; always draws the current bitmap if available
|
||||
const draw = useCallback(() => {
|
||||
const canvas = canvasRef.current;
|
||||
const ctx = canvas?.getContext("2d");
|
||||
const img = imageRef.current;
|
||||
|
||||
if (!canvas || !ctx || !img) return;
|
||||
|
||||
canvas.width = canvas.clientWidth;
|
||||
canvas.height = canvas.clientHeight;
|
||||
|
||||
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
||||
const bmp = latestBitmapRef.current;
|
||||
if (!canvas || !bmp) return;
|
||||
drawBitmapToCanvas(canvas, bmp);
|
||||
}, []);
|
||||
|
||||
const {
|
||||
@@ -39,43 +80,82 @@ export function useGetOverviewSnapshot(side: string) {
|
||||
error,
|
||||
isPending,
|
||||
} = useQuery({
|
||||
queryKey: ["overviewSnapshot"],
|
||||
queryKey: ["overviewSnapshot", side],
|
||||
queryFn: () => fetchSnapshot(side),
|
||||
// Poll ~4 fps when visible; pause when tab hidden
|
||||
refetchInterval: () => (document.visibilityState === "visible" ? 250 : false),
|
||||
refetchOnWindowFocus: false,
|
||||
refetchInterval: 250,
|
||||
// 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;
|
||||
|
||||
const imgUrl = URL.createObjectURL(snapshotBlob);
|
||||
const img = new Image();
|
||||
imageRef.current = img;
|
||||
(async () => {
|
||||
try {
|
||||
const bitmap = await createImageBitmap(snapshotBlob);
|
||||
if (cancelled) {
|
||||
bitmap.close();
|
||||
return;
|
||||
}
|
||||
|
||||
img.onload = () => {
|
||||
drawImage();
|
||||
};
|
||||
img.src = imgUrl;
|
||||
// Dispose previous bitmap to free memory
|
||||
if (latestBitmapRef.current) {
|
||||
latestBitmapRef.current.close();
|
||||
}
|
||||
latestBitmapRef.current = bitmap;
|
||||
|
||||
if (latestUrlRef.current) {
|
||||
URL.revokeObjectURL(latestUrlRef.current);
|
||||
}
|
||||
latestUrlRef.current = imgUrl;
|
||||
// Draw now (and again on next resize)
|
||||
draw();
|
||||
} catch {
|
||||
// noop — fetch handler surfaces the main error path
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
if (latestUrlRef.current) {
|
||||
URL.revokeObjectURL(latestUrlRef.current);
|
||||
latestUrlRef.current = null;
|
||||
}
|
||||
cancelled = true;
|
||||
};
|
||||
}, [snapshotBlob, drawImage]);
|
||||
}, [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]);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("resize", drawImage);
|
||||
return () => {
|
||||
window.removeEventListener("resize", drawImage);
|
||||
};
|
||||
}, [drawImage]);
|
||||
const el = canvasRef.current?.parentElement; // the box
|
||||
if (!el) return;
|
||||
const ro = new ResizeObserver(() => draw()); // your draw() calls aspect-fit logic
|
||||
ro.observe(el);
|
||||
return () => ro.disconnect();
|
||||
}, [draw]);
|
||||
|
||||
return { canvasRef, isError, error, isPending };
|
||||
// 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 };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user