From a004890a7ebfba7a26336657364f34cba07fb119 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Fri, 9 Feb 2024 05:34:27 -0700 Subject: [PATCH] Simplify basic image view --- .../camera/AutoUpdatingCameraImage.tsx | 2 - web/src/components/camera/CameraImage.tsx | 88 ++------- .../components/camera/DynamicCameraImage.tsx | 120 ------------ .../components/camera/ResizingCameraImage.tsx | 117 ++++++++++++ web/src/components/player/LivePlayer.tsx | 4 +- web/src/pages/Dashboard.tsx | 176 ------------------ 6 files changed, 139 insertions(+), 368 deletions(-) delete mode 100644 web/src/components/camera/DynamicCameraImage.tsx create mode 100644 web/src/components/camera/ResizingCameraImage.tsx delete mode 100644 web/src/pages/Dashboard.tsx diff --git a/web/src/components/camera/AutoUpdatingCameraImage.tsx b/web/src/components/camera/AutoUpdatingCameraImage.tsx index e8edcf936..8bcced85b 100644 --- a/web/src/components/camera/AutoUpdatingCameraImage.tsx +++ b/web/src/components/camera/AutoUpdatingCameraImage.tsx @@ -18,7 +18,6 @@ export default function AutoUpdatingCameraImage({ showFps = true, className, reloadInterval = MIN_LOAD_TIMEOUT_MS, - fitAspect, }: AutoUpdatingCameraImageProps) { const [key, setKey] = useState(Date.now()); const [fps, setFps] = useState("0"); @@ -43,7 +42,6 @@ export default function AutoUpdatingCameraImage({ {showFps ? Displaying at {fps}fps : null} diff --git a/web/src/components/camera/CameraImage.tsx b/web/src/components/camera/CameraImage.tsx index 18ffabcfb..0c6fb61de 100644 --- a/web/src/components/camera/CameraImage.tsx +++ b/web/src/components/camera/CameraImage.tsx @@ -1,16 +1,13 @@ import { useApiHost } from "@/api"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import useSWR from "swr"; import ActivityIndicator from "../ui/activity-indicator"; -import { useResizeObserver } from "@/hooks/resize-observer"; type CameraImageProps = { className?: string; camera: string; - onload?: (event: Event) => void; + onload?: () => void; searchParams?: {}; - stretch?: boolean; // stretch to fit width - fitAspect?: number; // shrink to fit height }; export default function CameraImage({ @@ -18,73 +15,25 @@ export default function CameraImage({ camera, onload, searchParams = "", - stretch = false, - fitAspect, }: CameraImageProps) { const { data: config } = useSWR("config"); const apiHost = useApiHost(); const [hasLoaded, setHasLoaded] = useState(false); const containerRef = useRef(null); - const canvasRef = useRef(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 imgRef = useRef(null); 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; - }, [availableWidth, aspectRatio, height, stretch]); - const scaledWidth = useMemo( - () => Math.ceil(scaledHeight * aspectRatio - scrollBarWidth), - [scaledHeight, aspectRatio, scrollBarWidth] - ); - - 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); - }, - [img, scaledHeight, scaledWidth, setHasLoaded, onload, canvasRef] - ); useEffect(() => { - if (!config || scaledHeight === 0 || !canvasRef.current) { + if (!config || !imgRef.current) { return; } - img.src = `${apiHost}api/${name}/latest.jpg?h=${scaledHeight}${ - searchParams ? `&${searchParams}` : "" + + imgRef.current.src = `${apiHost}api/${name}/latest.jpg${ + searchParams ? `?${searchParams}` : "" }`; - }, [apiHost, canvasRef, name, img, searchParams, scaledHeight, config]); + }, [apiHost, name, imgRef, searchParams, config]); return (
{enabled ? ( - { + setHasLoaded(true); + + if (onload) { + onload(); + } + }} /> ) : (
@@ -105,10 +58,7 @@ export default function CameraImage({
)} {!hasLoaded && enabled ? ( -
+
) : null} diff --git a/web/src/components/camera/DynamicCameraImage.tsx b/web/src/components/camera/DynamicCameraImage.tsx deleted file mode 100644 index 1f15f595b..000000000 --- a/web/src/components/camera/DynamicCameraImage.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import { useCallback, useEffect, useMemo, useState } from "react"; -import { AspectRatio } from "../ui/aspect-ratio"; -import CameraImage from "./CameraImage"; -import { LuEar } from "react-icons/lu"; -import { CameraConfig } from "@/types/frigateConfig"; -import { TbUserScan } from "react-icons/tb"; -import { MdLeakAdd } from "react-icons/md"; -import { - useAudioActivity, - useFrigateEvents, - useMotionActivity, -} from "@/api/ws"; - -type DynamicCameraImageProps = { - camera: CameraConfig; - aspect: number; -}; - -const INTERVAL_INACTIVE_MS = 60000; // refresh once a minute -const INTERVAL_ACTIVE_MS = 1000; // refresh once a second - -export default function DynamicCameraImage({ - camera, - aspect, -}: DynamicCameraImageProps) { - const [key, setKey] = useState(Date.now()); - const [timeoutId, setTimeoutId] = useState( - undefined - ); - const [activeObjects, setActiveObjects] = useState([]); - const hasActiveObjects = useMemo( - () => activeObjects.length > 0, - [activeObjects] - ); - - const { payload: detectingMotion } = useMotionActivity(camera.name); - const { payload: event } = useFrigateEvents(); - const { payload: audioRms } = useAudioActivity(camera.name); - - useEffect(() => { - if (!event) { - return; - } - - if (event.after.camera != camera.name) { - return; - } - - if (event.type == "end") { - const eventIndex = activeObjects.indexOf(event.after.id); - - if (eventIndex != -1) { - const newActiveObjects = [...activeObjects]; - newActiveObjects.splice(eventIndex, 1); - setActiveObjects(newActiveObjects); - } - } else { - if (!event.after.stationary) { - const eventIndex = activeObjects.indexOf(event.after.id); - - if (eventIndex == -1) { - const newActiveObjects = [...activeObjects, event.after.id]; - setActiveObjects(newActiveObjects); - clearTimeout(timeoutId); - setKey(Date.now()); - } - } - } - }, [event, activeObjects]); - - const handleLoad = useCallback(() => { - const loadTime = Date.now() - key; - const loadInterval = hasActiveObjects - ? INTERVAL_ACTIVE_MS - : INTERVAL_INACTIVE_MS; - - const tId = setTimeout( - () => { - setKey(Date.now()); - }, - loadTime > loadInterval ? 1 : loadInterval - ); - setTimeoutId(tId); - }, [key]); - - return ( - - -
- - 0 ? "text-object" : "text-gray-600" - }`} - /> - {camera.audio.enabled_in_config && ( - = camera.audio.min_volume - ? "text-audio" - : "text-gray-600" - }`} - /> - )} -
-
- ); -} diff --git a/web/src/components/camera/ResizingCameraImage.tsx b/web/src/components/camera/ResizingCameraImage.tsx new file mode 100644 index 000000000..18ffabcfb --- /dev/null +++ b/web/src/components/camera/ResizingCameraImage.tsx @@ -0,0 +1,117 @@ +import { useApiHost } from "@/api"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import useSWR from "swr"; +import ActivityIndicator from "../ui/activity-indicator"; +import { useResizeObserver } from "@/hooks/resize-observer"; + +type CameraImageProps = { + className?: string; + camera: string; + onload?: (event: Event) => void; + searchParams?: {}; + 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(null); + const canvasRef = useRef(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; + }, [availableWidth, aspectRatio, height, stretch]); + const scaledWidth = useMemo( + () => Math.ceil(scaledHeight * aspectRatio - scrollBarWidth), + [scaledHeight, aspectRatio, scrollBarWidth] + ); + + 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); + }, + [img, scaledHeight, scaledWidth, setHasLoaded, onload, canvasRef] + ); + + 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 ( +
+ {enabled ? ( + + ) : ( +
+ Camera is disabled in config, no stream or snapshot available! +
+ )} + {!hasLoaded && enabled ? ( +
+ +
+ ) : null} +
+ ); +} diff --git a/web/src/components/player/LivePlayer.tsx b/web/src/components/player/LivePlayer.tsx index aa63d3f08..fdb5aba09 100644 --- a/web/src/components/player/LivePlayer.tsx +++ b/web/src/components/player/LivePlayer.tsx @@ -216,7 +216,9 @@ export default function LivePlayer({
)} - + {cameraConfig.record.enabled && ( + + )}
{cameraConfig.name.replaceAll("_", " ")}
diff --git a/web/src/pages/Dashboard.tsx b/web/src/pages/Dashboard.tsx deleted file mode 100644 index 6eaf1bff7..000000000 --- a/web/src/pages/Dashboard.tsx +++ /dev/null @@ -1,176 +0,0 @@ -import { useMemo } from "react"; -import ActivityIndicator from "@/components/ui/activity-indicator"; -import { - useAudioState, - useDetectState, - useRecordingsState, - useSnapshotsState, -} from "@/api/ws"; -import useSWR from "swr"; -import { CameraConfig, FrigateConfig } from "@/types/frigateConfig"; -import Heading from "@/components/ui/heading"; -import { Card } from "@/components/ui/card"; -import { Button } from "@/components/ui/button"; -import { AiOutlinePicture } from "react-icons/ai"; -import { FaWalking } from "react-icons/fa"; -import { LuEar } from "react-icons/lu"; -import { TbMovie } from "react-icons/tb"; -import MiniEventCard from "@/components/card/MiniEventCard"; -import { Event as FrigateEvent } from "@/types/event"; -import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; -import DynamicCameraImage from "@/components/camera/DynamicCameraImage"; - -export function Dashboard() { - const { data: config } = useSWR("config"); - const now = new Date(); - now.setMinutes(now.getMinutes() - 30, 0, 0); - const recentTimestamp = now.getTime() / 1000; - const { data: events, mutate: updateEvents } = useSWR([ - "events", - { limit: 10, after: recentTimestamp }, - ]); - - const sortedCameras = useMemo(() => { - if (!config) { - return []; - } - - return Object.values(config.cameras) - .filter((conf) => conf.ui.dashboard) - .sort((aConf, bConf) => aConf.ui.order - bConf.ui.order); - }, [config]); - - return ( - <> - Dashboard - - {!config && } - - {config && ( -
- {events && events.length > 0 && ( - <> - Recent Events - -
- {events.map((event) => { - return ( - updateEvents()} - /> - ); - })} -
- -
- - )} - Cameras -
- {sortedCameras.map((camera) => { - return ; - })} -
-
- )} - - ); -} - -function Camera({ camera }: { camera: CameraConfig }) { - const { payload: detectValue, send: sendDetect } = useDetectState( - camera.name - ); - const { payload: recordValue, send: sendRecord } = useRecordingsState( - camera.name - ); - const { payload: snapshotValue, send: sendSnapshot } = useSnapshotsState( - camera.name - ); - const { payload: audioValue, send: sendAudio } = useAudioState(camera.name); - - return ( - <> - - - -
-
- {camera.name.replaceAll("_", " ")} -
-
- - - - {camera.audio.enabled_in_config && ( - - )} -
-
-
-
- - ); -} - -export default Dashboard;