Files
frigate/web/src/components/camera/ResizingCameraImage.tsx
T

124 lines
3.6 KiB
TypeScript
Raw Normal View History

2024-02-10 05:30:53 -07:00
import { useApiHost } from "@/api";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import useSWR from "swr";
2024-03-03 10:32:47 -06:00
import ActivityIndicator from "../indicators/activity-indicator";
2024-02-10 05:30:53 -07:00
import { useResizeObserver } from "@/hooks/resize-observer";
import { cn } from "@/lib/utils";
2024-02-10 05:30:53 -07:00
type CameraImageProps = {
className?: string;
camera: string;
onload?: (event: Event) => void;
2024-02-28 15:23:56 -07:00
searchParams?: string;
2024-02-10 05:30:53 -07:00
stretch?: boolean; // stretch to fit width
fitAspect?: number; // shrink to fit height
};
export default function CameraImage({
className,
camera,
onload,
searchParams = "",
stretch = false,
fitAspect,
}: CameraImageProps) {
const { data: config } = useSWR("config");
const apiHost = useApiHost();
const [hasLoaded, setHasLoaded] = useState(false);
const containerRef = useRef<HTMLDivElement | null>(null);
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const [{ width: containerWidth, height: containerHeight }] =
useResizeObserver(containerRef);
// Add scrollbar width (when visible) to the available observer width to eliminate screen juddering.
// https://github.com/blakeblackshear/frigate/issues/1657
let scrollBarWidth = 0;
if (window.innerWidth && document.body.offsetWidth) {
scrollBarWidth = window.innerWidth - document.body.offsetWidth;
}
const availableWidth = scrollBarWidth
? containerWidth + scrollBarWidth
: containerWidth;
const { name } = config ? config.cameras[camera] : "";
const enabled = config ? config.cameras[camera].enabled : "True";
const { width, height } = config
? config.cameras[camera].detect
: { width: 1, height: 1 };
const aspectRatio = width / height;
const scaledHeight = useMemo(() => {
const scaledHeight =
aspectRatio < (fitAspect ?? 0)
? Math.floor(containerHeight)
: Math.floor(availableWidth / aspectRatio);
const finalHeight = stretch ? scaledHeight : Math.min(scaledHeight, height);
if (finalHeight > 0) {
return finalHeight;
}
return 100;
2024-02-28 15:23:56 -07:00
}, [
availableWidth,
aspectRatio,
containerHeight,
fitAspect,
height,
stretch,
]);
2024-02-10 05:30:53 -07:00
const scaledWidth = useMemo(
() => Math.ceil(scaledHeight * aspectRatio - scrollBarWidth),
2024-02-28 15:23:56 -07:00
[scaledHeight, aspectRatio, scrollBarWidth],
2024-02-10 05:30:53 -07:00
);
const img = useMemo(() => new Image(), []);
img.onload = useCallback(
(event: Event) => {
setHasLoaded(true);
if (canvasRef.current) {
const ctx = canvasRef.current.getContext("2d");
ctx?.drawImage(img, 0, 0, scaledWidth, scaledHeight);
}
onload && onload(event);
},
2024-02-28 15:23:56 -07:00
[img, scaledHeight, scaledWidth, setHasLoaded, onload, canvasRef],
2024-02-10 05:30:53 -07:00
);
useEffect(() => {
if (!config || scaledHeight === 0 || !canvasRef.current) {
return;
}
2024-09-24 14:05:30 +01:00
img.src = `${apiHost}api/${name}/latest.webp?height=${scaledHeight}${
2024-02-10 05:30:53 -07:00
searchParams ? `&${searchParams}` : ""
}`;
}, [apiHost, canvasRef, name, img, searchParams, scaledHeight, config]);
return (
<div
2024-05-14 10:06:44 -05:00
className={cn("relative flex h-full w-full justify-center", className)}
2024-02-10 05:30:53 -07:00
ref={containerRef}
>
{enabled ? (
<canvas
className="rounded-lg md:rounded-2xl"
2024-02-10 05:30:53 -07:00
data-testid="cameraimage-canvas"
height={scaledHeight}
ref={canvasRef}
width={scaledWidth}
/>
) : (
2025-03-03 09:30:52 -06:00
<div className="pt-6 text-center">Camera is disabled.</div>
2024-02-10 05:30:53 -07:00
)}
{!hasLoaded && enabled ? (
<div
className="absolute inset-0 flex justify-center"
style={{ height: `${scaledHeight}px` }}
>
<ActivityIndicator />
</div>
) : null}
</div>
);
}