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

112 lines
3.1 KiB
TypeScript
Raw Normal View History

2023-12-15 16:24:50 -07:00
import { useApiHost } from "@/api";
import { useEffect, useMemo, useRef, useState } from "react";
2023-12-15 16:24:50 -07:00
import useSWR from "swr";
2024-03-03 10:32:47 -06:00
import ActivityIndicator from "../indicators/activity-indicator";
import { useResizeObserver } from "@/hooks/resize-observer";
import { isDesktop } from "react-device-detect";
import { cn } from "@/lib/utils";
2025-03-03 09:30:52 -06:00
import { useEnabledState } from "@/api/ws";
2023-12-15 16:24:50 -07:00
type CameraImageProps = {
2024-02-10 05:30:53 -07:00
className?: string;
2023-12-15 16:24:50 -07:00
camera: string;
2024-02-10 05:30:53 -07:00
onload?: () => void;
2024-02-28 15:23:56 -07:00
searchParams?: string;
2023-12-15 16:24:50 -07:00
};
export default function CameraImage({
2024-02-10 05:30:53 -07:00
className,
2023-12-15 16:24:50 -07:00
camera,
onload,
searchParams = "",
}: CameraImageProps) {
const { data: config } = useSWR("config");
const apiHost = useApiHost();
const [imageLoaded, setImageLoaded] = useState(false);
2023-12-15 16:24:50 -07:00
const containerRef = useRef<HTMLDivElement | null>(null);
2024-02-10 05:30:53 -07:00
const imgRef = useRef<HTMLImageElement | null>(null);
2023-12-15 16:24:50 -07:00
2026-03-04 10:07:34 -06:00
const cameraConfig = config?.cameras?.[camera];
const { name } = cameraConfig ?? { name: camera };
2025-03-03 09:30:52 -06:00
const { payload: enabledState } = useEnabledState(camera);
2025-04-07 12:24:53 -05:00
const enabled = enabledState ? enabledState === "ON" : true;
2023-12-15 16:24:50 -07:00
const [{ width: containerWidth, height: containerHeight }] =
useResizeObserver(containerRef);
const requestHeight = useMemo(() => {
2026-03-04 10:07:34 -06:00
if (!cameraConfig || containerHeight == 0) {
return 360;
}
return Math.min(
2026-03-04 10:07:34 -06:00
cameraConfig.detect.height,
Math.round(containerHeight * (isDesktop ? 1.1 : 1.25)),
);
2026-03-04 10:07:34 -06:00
}, [cameraConfig, containerHeight]);
2024-06-29 10:02:30 -05:00
const [isPortraitImage, setIsPortraitImage] = useState(false);
useEffect(() => {
setImageLoaded(false);
setIsPortraitImage(false);
}, [camera]);
2023-12-15 16:24:50 -07:00
useEffect(() => {
2024-02-10 05:30:53 -07:00
if (!config || !imgRef.current) {
2023-12-15 16:24:50 -07:00
return;
}
2024-02-10 05:30:53 -07:00
2024-09-24 14:05:30 +01:00
const newSrc = `${apiHost}api/${name}/latest.webp?height=${requestHeight}${
searchParams ? `&${searchParams}` : ""
2023-12-15 16:24:50 -07:00
}`;
if (imgRef.current.src !== newSrc) {
imgRef.current.src = newSrc;
}
}, [apiHost, name, searchParams, requestHeight, config, camera]);
const handleImageLoad = () => {
if (imgRef.current && containerWidth && containerHeight) {
const { naturalWidth, naturalHeight } = imgRef.current;
setIsPortraitImage(
naturalWidth / naturalHeight < containerWidth / containerHeight,
);
}
setImageLoaded(true);
if (onload) {
onload();
}
};
2023-12-15 16:24:50 -07:00
return (
<div className={className} ref={containerRef}>
2023-12-15 16:24:50 -07:00
{enabled ? (
2024-02-10 05:30:53 -07:00
<img
ref={imgRef}
className={cn(
"object-contain",
imageLoaded
? isPortraitImage
? "h-full w-auto"
: "h-auto w-full"
: "invisible",
"rounded-lg md:rounded-2xl",
)}
onLoad={handleImageLoad}
2024-09-12 08:46:29 -06:00
loading="lazy"
2023-12-15 16:24:50 -07:00
/>
) : (
2025-03-03 09:30:52 -06:00
<div className="size-full rounded-lg border-2 border-muted bg-background_alt text-center md:rounded-2xl" />
2023-12-15 16:24:50 -07:00
)}
{!imageLoaded && enabled ? (
2024-05-14 10:06:44 -05:00
<div className="absolute bottom-0 left-0 right-0 top-0 flex items-center justify-center">
2023-12-15 16:24:50 -07:00
<ActivityIndicator />
</div>
) : null}
</div>
);
}