mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-04-05 22:57:40 +03:00
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:
commit
9836871718
@ -50,7 +50,7 @@ type LivePlayerProps = {
|
|||||||
showStats?: boolean;
|
showStats?: boolean;
|
||||||
onStatsUpdate?: (stats: PlayerStatsType) => void;
|
onStatsUpdate?: (stats: PlayerStatsType) => void;
|
||||||
onLoadingChange?: (loading: boolean) => void;
|
onLoadingChange?: (loading: boolean) => void;
|
||||||
onActiveMotionChange?: (active: boolean) => void;
|
showMotionDot?: boolean;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
setFullResolution?: React.Dispatch<React.SetStateAction<VideoResolutionType>>;
|
setFullResolution?: React.Dispatch<React.SetStateAction<VideoResolutionType>>;
|
||||||
onError?: (error: LivePlayerError) => void;
|
onError?: (error: LivePlayerError) => void;
|
||||||
@ -78,7 +78,7 @@ export default function LivePlayer({
|
|||||||
showStats = false,
|
showStats = false,
|
||||||
onStatsUpdate,
|
onStatsUpdate,
|
||||||
onLoadingChange,
|
onLoadingChange,
|
||||||
onActiveMotionChange,
|
showMotionDot = true,
|
||||||
onClick,
|
onClick,
|
||||||
setFullResolution,
|
setFullResolution,
|
||||||
onError,
|
onError,
|
||||||
@ -113,10 +113,8 @@ export default function LivePlayer({
|
|||||||
|
|
||||||
const onStatsUpdateRef = useRef(onStatsUpdate);
|
const onStatsUpdateRef = useRef(onStatsUpdate);
|
||||||
const onLoadingChangeRef = useRef(onLoadingChange);
|
const onLoadingChangeRef = useRef(onLoadingChange);
|
||||||
const onActiveMotionChangeRef = useRef(onActiveMotionChange);
|
|
||||||
onStatsUpdateRef.current = onStatsUpdate;
|
onStatsUpdateRef.current = onStatsUpdate;
|
||||||
onLoadingChangeRef.current = onLoadingChange;
|
onLoadingChangeRef.current = onLoadingChange;
|
||||||
onActiveMotionChangeRef.current = onActiveMotionChange;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onStatsUpdateRef.current?.(stats);
|
onStatsUpdateRef.current?.(stats);
|
||||||
@ -301,14 +299,6 @@ export default function LivePlayer({
|
|||||||
onLoadingChangeRef.current?.(loading);
|
onLoadingChangeRef.current?.(loading);
|
||||||
}, [cameraEnabled, offline, showStillWithoutActivity, isReEnabling, liveReady]);
|
}, [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) {
|
if (!cameraConfig) {
|
||||||
return <ActivityIndicator />;
|
return <ActivityIndicator />;
|
||||||
}
|
}
|
||||||
@ -566,7 +556,7 @@ export default function LivePlayer({
|
|||||||
{cameraName}
|
{cameraName}
|
||||||
</Chip>
|
</Chip>
|
||||||
)}
|
)}
|
||||||
{!onActiveMotionChangeRef.current &&
|
{showMotionDot &&
|
||||||
autoLive &&
|
autoLive &&
|
||||||
!offline &&
|
!offline &&
|
||||||
activeMotion &&
|
activeMotion &&
|
||||||
|
|||||||
@ -30,6 +30,7 @@ import {
|
|||||||
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
||||||
import { PlayerStats } from "@/components/player/PlayerStats";
|
import { PlayerStats } from "@/components/player/PlayerStats";
|
||||||
import { MdCircle } from "react-icons/md";
|
import { MdCircle } from "react-icons/md";
|
||||||
|
import { useCameraActivity } from "@/hooks/use-camera-activity";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
|
||||||
import { isEqual } from "lodash";
|
import { isEqual } from "lodash";
|
||||||
@ -441,9 +442,6 @@ export default function DraggableGridLayout({
|
|||||||
const [cameraLoadingStates, setCameraLoadingStates] = useState<
|
const [cameraLoadingStates, setCameraLoadingStates] = useState<
|
||||||
Record<string, boolean>
|
Record<string, boolean>
|
||||||
>({});
|
>({});
|
||||||
const [cameraMotionStates, setCameraMotionStates] = useState<
|
|
||||||
Record<string, boolean>
|
|
||||||
>({});
|
|
||||||
const cameraZoomViewportRefs = useRef<Record<string, HTMLDivElement | null>>(
|
const cameraZoomViewportRefs = useRef<Record<string, HTMLDivElement | null>>(
|
||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
@ -881,12 +879,7 @@ export default function DraggableGridLayout({
|
|||||||
[camera.name]: loading,
|
[camera.name]: loading,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
onActiveMotionChange={(active) =>
|
showMotionDot={false}
|
||||||
setCameraMotionStates((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[camera.name]: active,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
!isEditMode && onSelectCamera(camera.name);
|
!isEditMode && onSelectCamera(camera.name);
|
||||||
}}
|
}}
|
||||||
@ -918,11 +911,10 @@ export default function DraggableGridLayout({
|
|||||||
minimal={true}
|
minimal={true}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{cameraMotionStates[camera.name] && (
|
<CameraMotionDot
|
||||||
<div className="absolute right-2 top-2 z-40">
|
camera={camera}
|
||||||
<MdCircle className="mr-2 size-2 animate-pulse text-danger shadow-danger drop-shadow-md" />
|
autoLive={autoLive ?? globalAutoLive}
|
||||||
</div>
|
/>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
{isEditMode && showCircles && <CornerCircles />}
|
{isEditMode && showCircles && <CornerCircles />}
|
||||||
</GridLiveContextMenu>
|
</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 = {
|
type GridLiveContextMenuProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user