diff --git a/web/src/components/player/PreviewPlayer.tsx b/web/src/components/player/PreviewPlayer.tsx index d103ecb36..4e807d771 100644 --- a/web/src/components/player/PreviewPlayer.tsx +++ b/web/src/components/player/PreviewPlayer.tsx @@ -23,6 +23,7 @@ import { import { useTranslation } from "react-i18next"; type PreviewPlayerProps = { + previewRef?: (ref: HTMLDivElement | null) => void; className?: string; camera: string; timeRange: TimeRange; @@ -30,16 +31,19 @@ type PreviewPlayerProps = { startTime?: number; isScrubbing: boolean; forceAspect?: number; + isVisible?: boolean; onControllerReady: (controller: PreviewController) => void; onClick?: () => void; }; export default function PreviewPlayer({ + previewRef, className, camera, timeRange, cameraPreviews, startTime, isScrubbing, + isVisible = true, onControllerReady, onClick, }: PreviewPlayerProps) { @@ -54,6 +58,7 @@ export default function PreviewPlayer({ if (currentPreview) { return ( void; className?: string; camera: string; timeRange: TimeRange; @@ -117,12 +124,14 @@ type PreviewVideoPlayerProps = { initialPreview?: Preview; startTime?: number; isScrubbing: boolean; + isVisible: boolean; currentHourFrame?: string; onControllerReady: (controller: PreviewVideoController) => void; onClick?: () => void; setCurrentHourFrame: (src: string | undefined) => void; }; function PreviewVideoPlayer({ + visibilityRef, className, camera, timeRange, @@ -130,6 +139,7 @@ function PreviewVideoPlayer({ initialPreview, startTime, isScrubbing, + isVisible, currentHourFrame, onControllerReady, onClick, @@ -267,11 +277,13 @@ function PreviewVideoPlayer({ return (
- + )} {cameraPreviews && !currentPreview && (
{t("noPreviewFoundFor", { camera: camera.replaceAll("_", " ") })} diff --git a/web/src/views/recording/RecordingView.tsx b/web/src/views/recording/RecordingView.tsx index ecb11ee20..344fd8caa 100644 --- a/web/src/views/recording/RecordingView.tsx +++ b/web/src/views/recording/RecordingView.tsx @@ -385,6 +385,55 @@ export function RecordingView({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [previewRowRef.current?.scrollWidth, previewRowRef.current?.scrollHeight]); + // visibility listener for lazy loading + + const [visiblePreviews, setVisiblePreviews] = useState([]); + const visiblePreviewObserver = useRef(null); + useEffect(() => { + const visibleCameras = new Set(); + visiblePreviewObserver.current = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + const camera = (entry.target as HTMLElement).dataset.camera; + + if (!camera) { + return; + } + + if (entry.isIntersecting) { + visibleCameras.add(camera); + } else { + visibleCameras.delete(camera); + } + + setVisiblePreviews([...visibleCameras]); + }); + }, + { threshold: 0.1 }, + ); + + return () => { + visiblePreviewObserver.current?.disconnect(); + }; + }, []); + + const previewRef = useCallback( + (node: HTMLElement | null) => { + if (!visiblePreviewObserver.current) { + return; + } + + try { + if (node) visiblePreviewObserver.current.observe(node); + } catch (e) { + // no op + } + }, + // we need to listen on the value of the ref + // eslint-disable-next-line react-hooks/exhaustive-deps + [visiblePreviewObserver.current], + ); + return (
@@ -631,12 +680,14 @@ export function RecordingView({ }} > { previewRefs.current[cam] = controller; controller.scrubToTimestamp(startTime);