From 930727200727d79df11d30c9b4cb2d501c72abf2 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 16 Mar 2026 09:08:43 +0000 Subject: [PATCH] fix: exclude stats, spinner and motion dot from camera zoom transform Move PlayerStats, ActivityIndicator and motion dot rendering outside the zoom transform div in DraggableGridLayout so they are not scaled when the user zooms with Shift+Wheel. - Add onStatsUpdate, onLoadingChange, onActiveMotionChange callback props to LivePlayer; when provided, suppress the internal overlay elements and bubble state up to the parent instead - In DraggableGridLayout, maintain per-camera overlay states and render the three overlays as siblings to the zoom div (inside the clipping viewport) so they remain at natural size regardless of zoom level https://claude.ai/code/session_019B4dJXtcxvHn97ZaqHUB62 --- web/src/components/player/LivePlayer.tsx | 54 ++++++++++++++++++++-- web/src/views/live/DraggableGridLayout.tsx | 52 ++++++++++++++++++++- 2 files changed, 101 insertions(+), 5 deletions(-) diff --git a/web/src/components/player/LivePlayer.tsx b/web/src/components/player/LivePlayer.tsx index 1009f03bc..af0511ac1 100644 --- a/web/src/components/player/LivePlayer.tsx +++ b/web/src/components/player/LivePlayer.tsx @@ -48,6 +48,9 @@ type LivePlayerProps = { pip?: boolean; autoLive?: boolean; showStats?: boolean; + onStatsUpdate?: (stats: PlayerStatsType) => void; + onLoadingChange?: (loading: boolean) => void; + onActiveMotionChange?: (active: boolean) => void; onClick?: () => void; setFullResolution?: React.Dispatch>; onError?: (error: LivePlayerError) => void; @@ -73,6 +76,9 @@ export default function LivePlayer({ pip, autoLive = true, showStats = false, + onStatsUpdate, + onLoadingChange, + onActiveMotionChange, onClick, setFullResolution, onError, @@ -105,6 +111,10 @@ export default function LivePlayer({ droppedFrameRate: 0, // percentage }); + useEffect(() => { + onStatsUpdate?.(stats); + }, [stats, onStatsUpdate]); + // camera activity const { @@ -274,6 +284,42 @@ export default function LivePlayer({ } }, [liveReady, isReEnabling]); + useEffect(() => { + if (!onLoadingChange) return; + const loading = !!( + cameraEnabled && + !offline && + (!showStillWithoutActivity || isReEnabling) && + !liveReady + ); + onLoadingChange(loading); + }, [ + onLoadingChange, + cameraEnabled, + offline, + showStillWithoutActivity, + isReEnabling, + liveReady, + ]); + + useEffect(() => { + if (!onActiveMotionChange) return; + const motionVisible = !!( + autoLive && + !offline && + activeMotion && + ((showStillWithoutActivity && !liveReady) || liveReady) + ); + onActiveMotionChange(motionVisible); + }, [ + onActiveMotionChange, + autoLive, + offline, + activeMotion, + showStillWithoutActivity, + liveReady, + ]); + if (!cameraConfig) { return ; } @@ -407,7 +453,8 @@ export default function LivePlayer({ /> - {cameraEnabled && + {!onLoadingChange && + cameraEnabled && !offline && (!showStillWithoutActivity || isReEnabling) && !liveReady && } @@ -530,14 +577,15 @@ export default function LivePlayer({ {cameraName} )} - {autoLive && + {!onActiveMotionChange && + autoLive && !offline && activeMotion && ((showStillWithoutActivity && !liveReady) || liveReady) && ( )} - {showStats && ( + {showStats && !onStatsUpdate && ( )} diff --git a/web/src/views/live/DraggableGridLayout.tsx b/web/src/views/live/DraggableGridLayout.tsx index 522787bcf..5cf596cfc 100644 --- a/web/src/views/live/DraggableGridLayout.tsx +++ b/web/src/views/live/DraggableGridLayout.tsx @@ -23,9 +23,13 @@ import { AudioState, LivePlayerMode, LiveStreamMetadata, + PlayerStatsType, StatsState, VolumeState, } from "@/types/live"; +import ActivityIndicator from "@/components/indicators/activity-indicator"; +import { PlayerStats } from "@/components/player/PlayerStats"; +import { MdCircle } from "react-icons/md"; import { Skeleton } from "@/components/ui/skeleton"; import { isEqual } from "lodash"; @@ -431,6 +435,15 @@ export default function DraggableGridLayout({ const [cameraZoomStates, setCameraZoomStates] = useState< Record >({}); + const [cameraStatsData, setCameraStatsData] = useState< + Record + >({}); + const [cameraLoadingStates, setCameraLoadingStates] = useState< + Record + >({}); + const [cameraMotionStates, setCameraMotionStates] = useState< + Record + >({}); const cameraZoomViewportRefs = useRef>( {}, ); @@ -808,7 +821,7 @@ export default function DraggableGridLayout({ streamMetadata={streamMetadata} >
{ cameraZoomViewportRefs.current[camera.name] = node; @@ -855,7 +868,25 @@ export default function DraggableGridLayout({ preferredLiveModes[camera.name] ?? "mse" } playInBackground={false} - showStats={statsStates[camera.name] ?? true} + showStats={false} + onStatsUpdate={(stats) => + setCameraStatsData((prev) => ({ + ...prev, + [camera.name]: stats, + })) + } + onLoadingChange={(loading) => + setCameraLoadingStates((prev) => ({ + ...prev, + [camera.name]: loading, + })) + } + onActiveMotionChange={(active) => + setCameraMotionStates((prev) => ({ + ...prev, + [camera.name]: active, + })) + } onClick={() => { !isEditMode && onSelectCamera(camera.name); }} @@ -875,6 +906,23 @@ export default function DraggableGridLayout({ volume={volumeStates[camera.name]} />
+ {cameraLoadingStates[camera.name] && ( +
+ +
+ )} + {statsStates[camera.name] && + cameraStatsData[camera.name] && ( + + )} + {cameraMotionStates[camera.name] && ( +
+ +
+ )} {isEditMode && showCircles && }