Merge pull request #64 from ibs0d/claude/fix-zoom-statistics-WFvOm

fix: replace callback motion dot with direct WS subscription component
This commit is contained in:
ibs0d 2026-03-17 18:16:44 +11:00 committed by GitHub
commit 9836871718
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 28 additions and 27 deletions

View File

@ -50,7 +50,7 @@ type LivePlayerProps = {
showStats?: boolean;
onStatsUpdate?: (stats: PlayerStatsType) => void;
onLoadingChange?: (loading: boolean) => void;
onActiveMotionChange?: (active: boolean) => void;
showMotionDot?: boolean;
onClick?: () => void;
setFullResolution?: React.Dispatch<React.SetStateAction<VideoResolutionType>>;
onError?: (error: LivePlayerError) => void;
@ -78,7 +78,7 @@ export default function LivePlayer({
showStats = false,
onStatsUpdate,
onLoadingChange,
onActiveMotionChange,
showMotionDot = true,
onClick,
setFullResolution,
onError,
@ -113,10 +113,8 @@ export default function LivePlayer({
const onStatsUpdateRef = useRef(onStatsUpdate);
const onLoadingChangeRef = useRef(onLoadingChange);
const onActiveMotionChangeRef = useRef(onActiveMotionChange);
onStatsUpdateRef.current = onStatsUpdate;
onLoadingChangeRef.current = onLoadingChange;
onActiveMotionChangeRef.current = onActiveMotionChange;
useEffect(() => {
onStatsUpdateRef.current?.(stats);
@ -301,14 +299,6 @@ export default function LivePlayer({
onLoadingChangeRef.current?.(loading);
}, [cameraEnabled, offline, showStillWithoutActivity, isReEnabling, liveReady]);
// When the parent manages the dot via callback (grid view), show motion
// without gating on liveReady — the dot should reflect actual motion state
// regardless of stream load status to avoid the dot not appearing for
// cameras in continuous mode while the stream is reconnecting.
useEffect(() => {
onActiveMotionChangeRef.current?.(!!(autoLive && !offline && activeMotion));
}, [autoLive, offline, activeMotion]);
if (!cameraConfig) {
return <ActivityIndicator />;
}
@ -566,7 +556,7 @@ export default function LivePlayer({
{cameraName}
</Chip>
)}
{!onActiveMotionChangeRef.current &&
{showMotionDot &&
autoLive &&
!offline &&
activeMotion &&

View File

@ -30,6 +30,7 @@ import {
import ActivityIndicator from "@/components/indicators/activity-indicator";
import { PlayerStats } from "@/components/player/PlayerStats";
import { MdCircle } from "react-icons/md";
import { useCameraActivity } from "@/hooks/use-camera-activity";
import { Skeleton } from "@/components/ui/skeleton";
import { isEqual } from "lodash";
@ -441,9 +442,6 @@ export default function DraggableGridLayout({
const [cameraLoadingStates, setCameraLoadingStates] = useState<
Record<string, boolean>
>({});
const [cameraMotionStates, setCameraMotionStates] = useState<
Record<string, boolean>
>({});
const cameraZoomViewportRefs = useRef<Record<string, HTMLDivElement | null>>(
{},
);
@ -881,12 +879,7 @@ export default function DraggableGridLayout({
[camera.name]: loading,
}))
}
onActiveMotionChange={(active) =>
setCameraMotionStates((prev) => ({
...prev,
[camera.name]: active,
}))
}
showMotionDot={false}
onClick={() => {
!isEditMode && onSelectCamera(camera.name);
}}
@ -918,11 +911,10 @@ export default function DraggableGridLayout({
minimal={true}
/>
)}
{cameraMotionStates[camera.name] && (
<div className="absolute right-2 top-2 z-40">
<MdCircle className="mr-2 size-2 animate-pulse text-danger shadow-danger drop-shadow-md" />
</div>
)}
<CameraMotionDot
camera={camera}
autoLive={autoLive ?? globalAutoLive}
/>
</div>
{isEditMode && showCircles && <CornerCircles />}
</GridLiveContextMenu>
@ -1089,6 +1081,25 @@ const BirdseyeLivePlayerGridItem = React.forwardRef<
},
);
// Separate component so it can call useCameraActivity as a hook (no hooks in loops).
// Direct WS subscription guarantees the dot reacts to motion changes in real-time
// without relying on an intermediate callback → parent-state chain.
function CameraMotionDot({
camera,
autoLive,
}: {
camera: CameraConfig;
autoLive: boolean;
}) {
const { activeMotion, offline } = useCameraActivity(camera);
if (!autoLive || offline || !activeMotion) return null;
return (
<div className="absolute right-2 top-2 z-40">
<MdCircle className="mr-2 size-2 animate-pulse text-danger shadow-danger drop-shadow-md" />
</div>
);
}
type GridLiveContextMenuProps = {
className?: string;
style?: React.CSSProperties;