2024-02-10 13:30:53 +01:00
|
|
|
import { useApiHost } from "@/api";
|
|
|
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
|
|
import useSWR from "swr";
|
2024-03-03 17:32:47 +01:00
|
|
|
import ActivityIndicator from "../indicators/activity-indicator";
|
2024-02-10 13:30:53 +01:00
|
|
|
import { useResizeObserver } from "@/hooks/resize-observer";
|
2024-05-07 16:00:25 +02:00
|
|
|
import { cn } from "@/lib/utils";
|
2024-02-10 13:30:53 +01:00
|
|
|
|
|
|
|
type CameraImageProps = {
|
|
|
|
className?: string;
|
|
|
|
camera: string;
|
|
|
|
onload?: (event: Event) => void;
|
2024-02-28 23:23:56 +01:00
|
|
|
searchParams?: string;
|
2024-02-10 13:30:53 +01: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 23:23:56 +01:00
|
|
|
}, [
|
|
|
|
availableWidth,
|
|
|
|
aspectRatio,
|
|
|
|
containerHeight,
|
|
|
|
fitAspect,
|
|
|
|
height,
|
|
|
|
stretch,
|
|
|
|
]);
|
2024-02-10 13:30:53 +01:00
|
|
|
const scaledWidth = useMemo(
|
|
|
|
() => Math.ceil(scaledHeight * aspectRatio - scrollBarWidth),
|
2024-02-28 23:23:56 +01:00
|
|
|
[scaledHeight, aspectRatio, scrollBarWidth],
|
2024-02-10 13:30:53 +01: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 23:23:56 +01:00
|
|
|
[img, scaledHeight, scaledWidth, setHasLoaded, onload, canvasRef],
|
2024-02-10 13:30:53 +01:00
|
|
|
);
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
if (!config || scaledHeight === 0 || !canvasRef.current) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
img.src = `${apiHost}api/${name}/latest.jpg?h=${scaledHeight}${
|
|
|
|
searchParams ? `&${searchParams}` : ""
|
|
|
|
}`;
|
|
|
|
}, [apiHost, canvasRef, name, img, searchParams, scaledHeight, config]);
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div
|
2024-05-07 16:00:25 +02:00
|
|
|
className={cn("relative w-full h-full flex justify-center", className)}
|
2024-02-10 13:30:53 +01:00
|
|
|
ref={containerRef}
|
|
|
|
>
|
|
|
|
{enabled ? (
|
|
|
|
<canvas
|
2024-04-22 17:12:45 +02:00
|
|
|
className="rounded-lg md:rounded-2xl"
|
2024-02-10 13:30:53 +01:00
|
|
|
data-testid="cameraimage-canvas"
|
|
|
|
height={scaledHeight}
|
|
|
|
ref={canvasRef}
|
|
|
|
width={scaledWidth}
|
|
|
|
/>
|
|
|
|
) : (
|
|
|
|
<div className="text-center pt-6">
|
|
|
|
Camera is disabled in config, no stream or snapshot available!
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
{!hasLoaded && enabled ? (
|
|
|
|
<div
|
|
|
|
className="absolute inset-0 flex justify-center"
|
|
|
|
style={{ height: `${scaledHeight}px` }}
|
|
|
|
>
|
|
|
|
<ActivityIndicator />
|
|
|
|
</div>
|
|
|
|
) : null}
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|