Переглянути джерело

refactor(web): responsive images on content size, throttle AutoUpdatingCameraImage

Paul Armstrong 4 роки тому
батько
коміт
2ec921593e

+ 1 - 2
web/src/App.jsx

@@ -26,7 +26,7 @@ export default function App() {
     <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">
         <Sidebar />
-        <div className="p-4 min-w-0">
+        <div className="flex-auto p-4 lg:pl-8 lg:pr-8 min-w-0">
           <Router>
             <CameraMap path="/cameras/:camera/editor" />
             <Camera path="/cameras/:camera" />
@@ -39,5 +39,4 @@ export default function App() {
       </div>
     </Config.Provider>
   );
-  return;
 }

+ 1 - 2
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)
 
       <Box className="space-y-4">
         <div className="relative">
-          <CameraImage imageRef={imageRef} camera={camera} />
+          <img ref={imageRef} src={`${apiHost}/api/${camera}/latest.jpg`} />
           <EditableMask
             onChange={handleUpdateEditable}
             points={'subkey' in editing ? editing.set[editing.key][editing.subkey] : editing.set[editing.key]}

+ 57 - 41
web/src/Debug.jsx

@@ -1,4 +1,6 @@
 import { h } from 'preact';
+import Box from './components/Box';
+import Button from './components/Button';
 import Heading from './components/Heading';
 import Link from './components/Link';
 import { ApiHost, Config } from './context';
@@ -39,59 +41,73 @@ export default function Debug() {
   const cameraNames = Object.keys(cameras);
   const cameraDataKeys = Object.keys(cameras[cameraNames[0]]);
 
+  const handleCopyConfig = useCallback(async () => {
+    await window.navigator.clipboard.writeText(JSON.stringify(config, null, 2));
+  }, [config]);
+
   return (
-    <div>
+    <div class="space-y-4">
       <Heading>
         Debug <span className="text-sm">{service.version}</span>
       </Heading>
-      <Table className="w-full">
-        <Thead>
-          <Tr>
-            <Th>detector</Th>
-            {detectorDataKeys.map((name) => (
-              <Th>{name.replace('_', ' ')}</Th>
-            ))}
-          </Tr>
-        </Thead>
-        <Tbody>
-          {detectorNames.map((detector, i) => (
-            <Tr index={i}>
-              <Td>{detector}</Td>
+
+      <Box>
+        <Table className="w-full">
+          <Thead>
+            <Tr>
+              <Th>detector</Th>
               {detectorDataKeys.map((name) => (
-                <Td key={`${name}-${detector}`}>{detectors[detector][name]}</Td>
+                <Th>{name.replace('_', ' ')}</Th>
               ))}
             </Tr>
-          ))}
-        </Tbody>
-      </Table>
-
-      <Table className="w-full">
-        <Thead>
-          <Tr>
-            <Th>camera</Th>
-            {cameraDataKeys.map((name) => (
-              <Th>{name.replace('_', ' ')}</Th>
+          </Thead>
+          <Tbody>
+            {detectorNames.map((detector, i) => (
+              <Tr index={i}>
+                <Td>{detector}</Td>
+                {detectorDataKeys.map((name) => (
+                  <Td key={`${name}-${detector}`}>{detectors[detector][name]}</Td>
+                ))}
+              </Tr>
             ))}
-          </Tr>
-        </Thead>
-        <Tbody>
-          {cameraNames.map((camera, i) => (
-            <Tr index={i}>
-              <Td>
-                <Link href={`/cameras/${camera}`}>{camera}</Link>
-              </Td>
+          </Tbody>
+        </Table>
+      </Box>
+
+      <Box>
+        <Table className="w-full">
+          <Thead>
+            <Tr>
+              <Th>camera</Th>
               {cameraDataKeys.map((name) => (
-                <Td key={`${name}-${camera}`}>{cameras[camera][name]}</Td>
+                <Th>{name.replace('_', ' ')}</Th>
               ))}
             </Tr>
-          ))}
-        </Tbody>
-      </Table>
+          </Thead>
+          <Tbody>
+            {cameraNames.map((camera, i) => (
+              <Tr index={i}>
+                <Td>
+                  <Link href={`/cameras/${camera}`}>{camera}</Link>
+                </Td>
+                {cameraDataKeys.map((name) => (
+                  <Td key={`${name}-${camera}`}>{cameras[camera][name]}</Td>
+                ))}
+              </Tr>
+            ))}
+          </Tbody>
+        </Table>
+      </Box>
 
-      <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">
-        {JSON.stringify(config, null, 2)}
-      </pre>
+      <Box className="relative">
+        <Heading size="sm">Config</Heading>
+        <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)}
+        </pre>
+      </Box>
     </div>
   );
 }

+ 2 - 2
web/src/Events.jsx

@@ -23,7 +23,7 @@ export default function Events({ url } = {}) {
   const searchKeys = Array.from(searchParams.keys());
 
   return (
-    <div className="space-y-4">
+    <div className="space-y-4 w-full">
       <Heading>Events</Heading>
 
       {searchKeys.length ? (
@@ -43,7 +43,7 @@ export default function Events({ url } = {}) {
       ) : null}
 
       <Box className="min-w-0 overflow-auto">
-        <Table>
+        <Table className="w-full">
           <Thead>
             <Tr>
               <Th></Th>

+ 21 - 12
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 <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>
+  );
 }

+ 47 - 25
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 { name, width, height } = config.cameras[camera];
+  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 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;
+
+  const resizeObserver = useMemo(() => {
+    return new ResizeObserver((entries) => {
+      window.requestAnimationFrame(() => {
+        if (Array.isArray(entries) && entries.length) {
+          setAvailableWidth(entries[0].contentRect.width);
+        }
+      });
+    });
+  }, [setAvailableWidth, width]);
+
+  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 (
-    <img
-      className="w-full"
-      srcset={srcset.join(', ')}
-      sizes={sizes.join(', ')}
-      src={`${srcset[srcset.length - 1]}`}
-      alt={name}
-      ref={imageRef}
-    />
+    <div ref={containerRef}>
+      {loadedSrc ? <img width={scaledHeight * aspectRatio} height={scaledHeight} src={loadedSrc} alt={name} /> : null}
+    </div>
   );
 }