mirror of
https://github.com/blakeblackshear/frigate.git
synced 2024-11-21 19:07:46 +01:00
refactor(web): responsive images on content size, throttle AutoUpdatingCameraImage
This commit is contained in:
parent
75a01f657e
commit
2ec921593e
@ -26,7 +26,7 @@ export default function App() {
|
|||||||
<Config.Provider value={config}>
|
<Config.Provider value={config}>
|
||||||
<div className="md:flex flex-col md:flex-row md:min-h-screen w-full bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-white">
|
<div className="md:flex flex-col md:flex-row md:min-h-screen w-full bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-white">
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
<div className="p-4 min-w-0">
|
<div className="flex-auto p-4 lg:pl-8 lg:pr-8 min-w-0">
|
||||||
<Router>
|
<Router>
|
||||||
<CameraMap path="/cameras/:camera/editor" />
|
<CameraMap path="/cameras/:camera/editor" />
|
||||||
<Camera path="/cameras/:camera" />
|
<Camera path="/cameras/:camera" />
|
||||||
@ -39,5 +39,4 @@ export default function App() {
|
|||||||
</div>
|
</div>
|
||||||
</Config.Provider>
|
</Config.Provider>
|
||||||
);
|
);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { h } from 'preact';
|
import { h } from 'preact';
|
||||||
import Box from './components/Box';
|
import Box from './components/Box';
|
||||||
import Button from './components/Button';
|
import Button from './components/Button';
|
||||||
import CameraImage from './components/CameraImage';
|
|
||||||
import Heading from './components/Heading';
|
import Heading from './components/Heading';
|
||||||
import Switch from './components/Switch';
|
import Switch from './components/Switch';
|
||||||
import { route } from 'preact-router';
|
import { route } from 'preact-router';
|
||||||
@ -253,7 +252,7 @@ ${Object.keys(objectMaskPoints)
|
|||||||
|
|
||||||
<Box className="space-y-4">
|
<Box className="space-y-4">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<CameraImage imageRef={imageRef} camera={camera} />
|
<img ref={imageRef} src={`${apiHost}/api/${camera}/latest.jpg`} />
|
||||||
<EditableMask
|
<EditableMask
|
||||||
onChange={handleUpdateEditable}
|
onChange={handleUpdateEditable}
|
||||||
points={'subkey' in editing ? editing.set[editing.key][editing.subkey] : editing.set[editing.key]}
|
points={'subkey' in editing ? editing.set[editing.key][editing.subkey] : editing.set[editing.key]}
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import { h } from 'preact';
|
import { h } from 'preact';
|
||||||
|
import Box from './components/Box';
|
||||||
|
import Button from './components/Button';
|
||||||
import Heading from './components/Heading';
|
import Heading from './components/Heading';
|
||||||
import Link from './components/Link';
|
import Link from './components/Link';
|
||||||
import { ApiHost, Config } from './context';
|
import { ApiHost, Config } from './context';
|
||||||
@ -39,11 +41,17 @@ export default function Debug() {
|
|||||||
const cameraNames = Object.keys(cameras);
|
const cameraNames = Object.keys(cameras);
|
||||||
const cameraDataKeys = Object.keys(cameras[cameraNames[0]]);
|
const cameraDataKeys = Object.keys(cameras[cameraNames[0]]);
|
||||||
|
|
||||||
|
const handleCopyConfig = useCallback(async () => {
|
||||||
|
await window.navigator.clipboard.writeText(JSON.stringify(config, null, 2));
|
||||||
|
}, [config]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div class="space-y-4">
|
||||||
<Heading>
|
<Heading>
|
||||||
Debug <span className="text-sm">{service.version}</span>
|
Debug <span className="text-sm">{service.version}</span>
|
||||||
</Heading>
|
</Heading>
|
||||||
|
|
||||||
|
<Box>
|
||||||
<Table className="w-full">
|
<Table className="w-full">
|
||||||
<Thead>
|
<Thead>
|
||||||
<Tr>
|
<Tr>
|
||||||
@ -64,7 +72,9 @@ export default function Debug() {
|
|||||||
))}
|
))}
|
||||||
</Tbody>
|
</Tbody>
|
||||||
</Table>
|
</Table>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
<Table className="w-full">
|
<Table className="w-full">
|
||||||
<Thead>
|
<Thead>
|
||||||
<Tr>
|
<Tr>
|
||||||
@ -87,11 +97,17 @@ export default function Debug() {
|
|||||||
))}
|
))}
|
||||||
</Tbody>
|
</Tbody>
|
||||||
</Table>
|
</Table>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box className="relative">
|
||||||
<Heading size="sm">Config</Heading>
|
<Heading size="sm">Config</Heading>
|
||||||
<pre className="font-mono overflow-y-scroll overflow-x-scroll max-h-96 rounded bg-white dark:bg-gray-900">
|
<Button className="absolute top-4 right-8" onClick={handleCopyConfig}>
|
||||||
|
Copy to Clipboard
|
||||||
|
</Button>
|
||||||
|
<pre className="overflow-auto font-mono text-gray-900 dark:text-gray-100 rounded bg-gray-100 dark:bg-gray-800 p-2 max-h-96">
|
||||||
{JSON.stringify(config, null, 2)}
|
{JSON.stringify(config, null, 2)}
|
||||||
</pre>
|
</pre>
|
||||||
|
</Box>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,7 @@ export default function Events({ url } = {}) {
|
|||||||
const searchKeys = Array.from(searchParams.keys());
|
const searchKeys = Array.from(searchParams.keys());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4 w-full">
|
||||||
<Heading>Events</Heading>
|
<Heading>Events</Heading>
|
||||||
|
|
||||||
{searchKeys.length ? (
|
{searchKeys.length ? (
|
||||||
@ -43,7 +43,7 @@ export default function Events({ url } = {}) {
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<Box className="min-w-0 overflow-auto">
|
<Box className="min-w-0 overflow-auto">
|
||||||
<Table>
|
<Table className="w-full">
|
||||||
<Thead>
|
<Thead>
|
||||||
<Tr>
|
<Tr>
|
||||||
<Th></Th>
|
<Th></Th>
|
||||||
|
@ -1,20 +1,29 @@
|
|||||||
import { h } from 'preact';
|
import { h } from 'preact';
|
||||||
import CameraImage from './CameraImage';
|
import CameraImage from './CameraImage';
|
||||||
import { ApiHost, Config } from '../context';
|
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 MIN_LOAD_TIMEOUT_MS = 200;
|
||||||
const apiHost = useContext(ApiHost);
|
|
||||||
|
|
||||||
|
export default function AutoUpdatingCameraImage({ camera, searchParams, showFps = true }) {
|
||||||
const [key, setKey] = useState(Date.now());
|
const [key, setKey] = useState(Date.now());
|
||||||
useEffect(() => {
|
const [fps, setFps] = useState(0);
|
||||||
const timeoutId = setTimeout(() => {
|
|
||||||
setKey(Date.now());
|
|
||||||
}, 500);
|
|
||||||
return () => {
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
};
|
|
||||||
}, [key, searchParams]);
|
|
||||||
|
|
||||||
return <CameraImage camera={camera} searchParams={`cache=${key}&${searchParams}`} />;
|
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 (
|
||||||
|
<div>
|
||||||
|
<CameraImage camera={camera} onload={handleLoad} searchParams={`cache=${key}&${searchParams}`} />
|
||||||
|
{showFps ? <span className="text-xs">Displaying at {fps}fps</span> : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,38 +1,60 @@
|
|||||||
import { h } from 'preact';
|
import { h } from 'preact';
|
||||||
import { ApiHost, Config } from '../context';
|
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 config = useContext(Config);
|
||||||
const apiHost = useContext(ApiHost);
|
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 { name, width, height } = config.cameras[camera];
|
||||||
|
|
||||||
const aspectRatio = width / height;
|
const aspectRatio = width / height;
|
||||||
const innerWidth = parseInt(window.innerWidth, 10);
|
|
||||||
|
|
||||||
const responsiveWidths = [640, 768, 1024, 1280];
|
const resizeObserver = useMemo(() => {
|
||||||
if (innerWidth > responsiveWidths[responsiveWidths.length - 1]) {
|
return new ResizeObserver((entries) => {
|
||||||
responsiveWidths.push(innerWidth);
|
window.requestAnimationFrame(() => {
|
||||||
|
if (Array.isArray(entries) && entries.length) {
|
||||||
|
setAvailableWidth(entries[0].contentRect.width);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, [setAvailableWidth, width]);
|
||||||
|
|
||||||
const src = `${apiHost}/api/${camera}/latest.jpg`;
|
useEffect(() => {
|
||||||
const { srcset, sizes } = responsiveWidths.reduce(
|
if (!containerRef.current) {
|
||||||
(memo, w, i) => {
|
return;
|
||||||
memo.srcset.push(`${src}?h=${Math.ceil(w / aspectRatio)}&${searchParams} ${w}w`);
|
}
|
||||||
memo.sizes.push(`(max-width: ${w}) ${Math.ceil((w / innerWidth) * 100)}vw`);
|
resizeObserver.observe(containerRef.current);
|
||||||
return memo;
|
}, [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 (
|
return (
|
||||||
<img
|
<div ref={containerRef}>
|
||||||
className="w-full"
|
{loadedSrc ? <img width={scaledHeight * aspectRatio} height={scaledHeight} src={loadedSrc} alt={name} /> : null}
|
||||||
srcset={srcset.join(', ')}
|
</div>
|
||||||
sizes={sizes.join(', ')}
|
|
||||||
src={`${srcset[srcset.length - 1]}`}
|
|
||||||
alt={name}
|
|
||||||
ref={imageRef}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user