mirror of
				https://github.com/blakeblackshear/frigate.git
				synced 2025-10-27 10:52:11 +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,59 +41,73 @@ 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> | ||||||
|       <Table className="w-full"> | 
 | ||||||
|         <Thead> |       <Box> | ||||||
|           <Tr> |         <Table className="w-full"> | ||||||
|             <Th>detector</Th> |           <Thead> | ||||||
|             {detectorDataKeys.map((name) => ( |             <Tr> | ||||||
|               <Th>{name.replace('_', ' ')}</Th> |               <Th>detector</Th> | ||||||
|             ))} |  | ||||||
|           </Tr> |  | ||||||
|         </Thead> |  | ||||||
|         <Tbody> |  | ||||||
|           {detectorNames.map((detector, i) => ( |  | ||||||
|             <Tr index={i}> |  | ||||||
|               <Td>{detector}</Td> |  | ||||||
|               {detectorDataKeys.map((name) => ( |               {detectorDataKeys.map((name) => ( | ||||||
|                 <Td key={`${name}-${detector}`}>{detectors[detector][name]}</Td> |                 <Th>{name.replace('_', ' ')}</Th> | ||||||
|               ))} |               ))} | ||||||
|             </Tr> |             </Tr> | ||||||
|           ))} |           </Thead> | ||||||
|         </Tbody> |           <Tbody> | ||||||
|       </Table> |             {detectorNames.map((detector, i) => ( | ||||||
| 
 |               <Tr index={i}> | ||||||
|       <Table className="w-full"> |                 <Td>{detector}</Td> | ||||||
|         <Thead> |                 {detectorDataKeys.map((name) => ( | ||||||
|           <Tr> |                   <Td key={`${name}-${detector}`}>{detectors[detector][name]}</Td> | ||||||
|             <Th>camera</Th> |                 ))} | ||||||
|             {cameraDataKeys.map((name) => ( |               </Tr> | ||||||
|               <Th>{name.replace('_', ' ')}</Th> |  | ||||||
|             ))} |             ))} | ||||||
|           </Tr> |           </Tbody> | ||||||
|         </Thead> |         </Table> | ||||||
|         <Tbody> |       </Box> | ||||||
|           {cameraNames.map((camera, i) => ( | 
 | ||||||
|             <Tr index={i}> |       <Box> | ||||||
|               <Td> |         <Table className="w-full"> | ||||||
|                 <Link href={`/cameras/${camera}`}>{camera}</Link> |           <Thead> | ||||||
|               </Td> |             <Tr> | ||||||
|  |               <Th>camera</Th> | ||||||
|               {cameraDataKeys.map((name) => ( |               {cameraDataKeys.map((name) => ( | ||||||
|                 <Td key={`${name}-${camera}`}>{cameras[camera][name]}</Td> |                 <Th>{name.replace('_', ' ')}</Th> | ||||||
|               ))} |               ))} | ||||||
|             </Tr> |             </Tr> | ||||||
|           ))} |           </Thead> | ||||||
|         </Tbody> |           <Tbody> | ||||||
|       </Table> |             {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> |       <Box className="relative"> | ||||||
|       <pre className="font-mono overflow-y-scroll overflow-x-scroll max-h-96 rounded bg-white dark:bg-gray-900"> |         <Heading size="sm">Config</Heading> | ||||||
|         {JSON.stringify(config, null, 2)} |         <Button className="absolute top-4 right-8" onClick={handleCopyConfig}> | ||||||
|       </pre> |           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> |     </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