diff --git a/web/src/App.jsx b/web/src/App.jsx index 9cd8f3384..96a08758c 100644 --- a/web/src/App.jsx +++ b/web/src/App.jsx @@ -26,7 +26,7 @@ export default function App() {
-
+
@@ -39,5 +39,4 @@ export default function App() {
); - return; } diff --git a/web/src/CameraMap.jsx b/web/src/CameraMap.jsx index 5c1d7c35c..5f0e7705c 100644 --- a/web/src/CameraMap.jsx +++ b/web/src/CameraMap.jsx @@ -1,7 +1,6 @@ import { h } from 'preact'; import Box from './components/Box'; import Button from './components/Button'; -import CameraImage from './components/CameraImage'; import Heading from './components/Heading'; import Switch from './components/Switch'; import { route } from 'preact-router'; @@ -253,7 +252,7 @@ ${Object.keys(objectMaskPoints)
- + { + await window.navigator.clipboard.writeText(JSON.stringify(config, null, 2)); + }, [config]); + return ( -
+
Debug {service.version} - - - - - {detectorDataKeys.map((name) => ( - - ))} - - - - {detectorNames.map((detector, i) => ( - - + + +
detector{name.replace('_', ' ')}
{detector}
+ + + {detectorDataKeys.map((name) => ( - + ))} - ))} - -
detector{detectors[detector][name]}{name.replace('_', ' ')}
- - - - - - {cameraDataKeys.map((name) => ( - + + + {detectorNames.map((detector, i) => ( + + + {detectorDataKeys.map((name) => ( + + ))} + ))} - - - - {cameraNames.map((camera, i) => ( - - + +
camera{name.replace('_', ' ')}
{detector}{detectors[detector][name]}
- {camera} -
+ + + + + + + {cameraDataKeys.map((name) => ( - + ))} - ))} - -
camera{cameras[camera][name]}{name.replace('_', ' ')}
+ + + {cameraNames.map((camera, i) => ( + + + {camera} + + {cameraDataKeys.map((name) => ( + {cameras[camera][name]} + ))} + + ))} + + +
- Config -
-        {JSON.stringify(config, null, 2)}
-      
+ + Config + +
+          {JSON.stringify(config, null, 2)}
+        
+
); } diff --git a/web/src/Events.jsx b/web/src/Events.jsx index f7565bcc8..2976d7fc0 100644 --- a/web/src/Events.jsx +++ b/web/src/Events.jsx @@ -23,7 +23,7 @@ export default function Events({ url } = {}) { const searchKeys = Array.from(searchParams.keys()); return ( -
+
Events {searchKeys.length ? ( @@ -43,7 +43,7 @@ export default function Events({ url } = {}) { ) : null} - +
diff --git a/web/src/components/AutoUpdatingCameraImage.jsx b/web/src/components/AutoUpdatingCameraImage.jsx index 244aa8a85..bf606a1b8 100644 --- a/web/src/components/AutoUpdatingCameraImage.jsx +++ b/web/src/components/AutoUpdatingCameraImage.jsx @@ -1,20 +1,29 @@ import { h } from 'preact'; import CameraImage from './CameraImage'; import { ApiHost, Config } from '../context'; -import { useCallback, useEffect, useContext, useState } from 'preact/hooks'; +import { useCallback, useState } from 'preact/hooks'; -export default function AutoUpdatingCameraImage({ camera, searchParams }) { - const apiHost = useContext(ApiHost); +const MIN_LOAD_TIMEOUT_MS = 200; +export default function AutoUpdatingCameraImage({ camera, searchParams, showFps = true }) { const [key, setKey] = useState(Date.now()); - useEffect(() => { - const timeoutId = setTimeout(() => { - setKey(Date.now()); - }, 500); - return () => { - clearTimeout(timeoutId); - }; - }, [key, searchParams]); + const [fps, setFps] = useState(0); - return ; + const handleLoad = useCallback(() => { + const loadTime = Date.now() - key; + setFps((1000 / Math.max(loadTime, MIN_LOAD_TIMEOUT_MS)).toFixed(1)); + setTimeout( + () => { + setKey(Date.now()); + }, + loadTime > MIN_LOAD_TIMEOUT_MS ? 1 : MIN_LOAD_TIMEOUT_MS + ); + }, [key, searchParams, setFps]); + + return ( +
+ + {showFps ? Displaying at {fps}fps : null} +
+ ); } diff --git a/web/src/components/CameraImage.jsx b/web/src/components/CameraImage.jsx index 4db1be169..351e3b1cb 100644 --- a/web/src/components/CameraImage.jsx +++ b/web/src/components/CameraImage.jsx @@ -1,38 +1,60 @@ import { h } from 'preact'; import { ApiHost, Config } from '../context'; -import { useCallback, useEffect, useContext, useState } from 'preact/hooks'; +import { useCallback, useEffect, useContext, useMemo, useRef, useState } from 'preact/hooks'; -export default function CameraImage({ camera, searchParams = '', imageRef }) { +export default function CameraImage({ camera, onload, searchParams = '' }) { const config = useContext(Config); const apiHost = useContext(ApiHost); + const [availableWidth, setAvailableWidth] = useState(0); + const [loadedSrc, setLoadedSrc] = useState(null); + const containerRef = useRef(null); + const { name, width, height } = config.cameras[camera]; - const aspectRatio = width / height; - const innerWidth = parseInt(window.innerWidth, 10); - const responsiveWidths = [640, 768, 1024, 1280]; - if (innerWidth > responsiveWidths[responsiveWidths.length - 1]) { - responsiveWidths.push(innerWidth); - } + const resizeObserver = useMemo(() => { + return new ResizeObserver((entries) => { + window.requestAnimationFrame(() => { + if (Array.isArray(entries) && entries.length) { + setAvailableWidth(entries[0].contentRect.width); + } + }); + }); + }, [setAvailableWidth, width]); - const src = `${apiHost}/api/${camera}/latest.jpg`; - const { srcset, sizes } = responsiveWidths.reduce( - (memo, w, i) => { - memo.srcset.push(`${src}?h=${Math.ceil(w / aspectRatio)}&${searchParams} ${w}w`); - memo.sizes.push(`(max-width: ${w}) ${Math.ceil((w / innerWidth) * 100)}vw`); - return memo; + useEffect(() => { + if (!containerRef.current) { + return; + } + resizeObserver.observe(containerRef.current); + }, [resizeObserver, containerRef.current]); + + const scaledHeight = useMemo(() => Math.min(Math.ceil(availableWidth / aspectRatio), height), [ + availableWidth, + aspectRatio, + height, + ]); + + const img = useMemo(() => new Image(), [camera]); + img.onload = useCallback( + (event) => { + const src = event.path[0].currentSrc; + setLoadedSrc(src); + onload && onload(event); }, - { srcset: [], sizes: [] } + [searchParams, onload] ); + useEffect(() => { + if (!scaledHeight) { + return; + } + img.src = `${apiHost}/api/${name}/latest.jpg?h=${scaledHeight}${searchParams ? `&${searchParams}` : ''}`; + }, [apiHost, name, img, searchParams, scaledHeight]); + return ( - {name} +
+ {loadedSrc ? {name} : null} +
); }