diff --git a/web/src/components/camera/CameraImage.tsx b/web/src/components/camera/CameraImage.tsx index cdf1992fd..9f25f69a3 100644 --- a/web/src/components/camera/CameraImage.tsx +++ b/web/src/components/camera/CameraImage.tsx @@ -4,6 +4,7 @@ import useSWR from "swr"; import ActivityIndicator from "../indicators/activity-indicator"; import { useResizeObserver } from "@/hooks/resize-observer"; import { isDesktop } from "react-device-detect"; +import { cn } from "@/lib/utils"; type CameraImageProps = { className?: string; @@ -20,7 +21,7 @@ export default function CameraImage({ }: CameraImageProps) { const { data: config } = useSWR("config"); const apiHost = useApiHost(); - const [hasLoaded, setHasLoaded] = useState(false); + const [imageLoaded, setImageLoaded] = useState(false); const containerRef = useRef(null); const imgRef = useRef(null); @@ -31,7 +32,7 @@ export default function CameraImage({ useResizeObserver(containerRef); const requestHeight = useMemo(() => { - if (!config || containerHeight == 0 || !hasLoaded) { + if (!config || containerHeight == 0) { return 360; } @@ -39,47 +40,66 @@ export default function CameraImage({ config.cameras[camera].detect.height, Math.round(containerHeight * (isDesktop ? 1.1 : 1.25)), ); - }, [config, camera, containerHeight, hasLoaded]); + }, [config, camera, containerHeight]); - const isPortraitImage = useMemo(() => { - if (imgRef.current && containerWidth && containerHeight && hasLoaded) { - const { naturalHeight, naturalWidth } = imgRef.current; - return naturalWidth / naturalHeight < containerWidth / containerHeight; - } - }, [containerWidth, containerHeight, hasLoaded]); + const [isPortraitImage, setIsPortraitImage] = useState(false); - useEffect(() => setHasLoaded(false), [camera]); + useEffect(() => { + setImageLoaded(false); + setIsPortraitImage(false); + }, [camera]); useEffect(() => { if (!config || !imgRef.current) { return; } - imgRef.current.src = `${apiHost}api/${name}/latest.webp?h=${requestHeight}${ + const newSrc = `${apiHost}api/${name}/latest.webp?h=${requestHeight}${ searchParams ? `&${searchParams}` : "" }`; - }, [apiHost, name, imgRef, searchParams, requestHeight, config]); + + 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(); + } + }; return (
{enabled ? ( { - setHasLoaded(true); - - if (onload) { - onload(); - } - }} + className={cn( + "object-contain", + imageLoaded + ? isPortraitImage + ? "h-full w-auto" + : "h-auto w-full" + : "invisible", + "rounded-lg md:rounded-2xl", + )} + onLoad={handleImageLoad} /> ) : (
Camera is disabled in config, no stream or snapshot available!
)} - {!hasLoaded && enabled ? ( + {!imageLoaded && enabled ? (
diff --git a/web/src/components/settings/PolygonCanvas.tsx b/web/src/components/settings/PolygonCanvas.tsx index 63ce9fa15..6121293e5 100644 --- a/web/src/components/settings/PolygonCanvas.tsx +++ b/web/src/components/settings/PolygonCanvas.tsx @@ -5,6 +5,7 @@ import Konva from "konva"; import type { KonvaEventObject } from "konva/lib/Node"; import { Polygon, PolygonType } from "@/types/canvas"; import { useApiHost } from "@/api"; +import ActivityIndicator from "@/components/indicators/activity-indicator"; type PolygonCanvasProps = { containerRef: RefObject; @@ -29,6 +30,7 @@ export function PolygonCanvas({ hoveredPolygonIndex, selectedZoneMask, }: PolygonCanvasProps) { + const [isLoaded, setIsLoaded] = useState(false); const [image, setImage] = useState(); const imageRef = useRef(null); const stageRef = useRef(null); @@ -36,13 +38,16 @@ export function PolygonCanvas({ const videoElement = useMemo(() => { if (camera && width && height) { + setIsLoaded(false); const element = new window.Image(); element.width = width; element.height = height; element.src = `${apiHost}api/${camera}/latest.webp?cache=${Date.now()}`; return element; } - }, [camera, width, height, apiHost]); + // we know that these deps are correct + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [camera, apiHost]); useEffect(() => { if (!videoElement) { @@ -50,6 +55,7 @@ export function PolygonCanvas({ } const onload = function () { setImage(videoElement); + setIsLoaded(true); }; videoElement.addEventListener("load", onload); return () => { @@ -218,6 +224,10 @@ export function PolygonCanvas({ } }, [activePolygonIndex, polygons, setPolygons]); + if (!isLoaded) { + return ; + } + return (