mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-04-05 22:57:40 +03:00
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
This commit is contained in:
parent
00acb95be4
commit
9307272007
@ -48,6 +48,9 @@ type LivePlayerProps = {
|
|||||||
pip?: boolean;
|
pip?: boolean;
|
||||||
autoLive?: boolean;
|
autoLive?: boolean;
|
||||||
showStats?: boolean;
|
showStats?: boolean;
|
||||||
|
onStatsUpdate?: (stats: PlayerStatsType) => void;
|
||||||
|
onLoadingChange?: (loading: boolean) => void;
|
||||||
|
onActiveMotionChange?: (active: boolean) => void;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
setFullResolution?: React.Dispatch<React.SetStateAction<VideoResolutionType>>;
|
setFullResolution?: React.Dispatch<React.SetStateAction<VideoResolutionType>>;
|
||||||
onError?: (error: LivePlayerError) => void;
|
onError?: (error: LivePlayerError) => void;
|
||||||
@ -73,6 +76,9 @@ export default function LivePlayer({
|
|||||||
pip,
|
pip,
|
||||||
autoLive = true,
|
autoLive = true,
|
||||||
showStats = false,
|
showStats = false,
|
||||||
|
onStatsUpdate,
|
||||||
|
onLoadingChange,
|
||||||
|
onActiveMotionChange,
|
||||||
onClick,
|
onClick,
|
||||||
setFullResolution,
|
setFullResolution,
|
||||||
onError,
|
onError,
|
||||||
@ -105,6 +111,10 @@ export default function LivePlayer({
|
|||||||
droppedFrameRate: 0, // percentage
|
droppedFrameRate: 0, // percentage
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onStatsUpdate?.(stats);
|
||||||
|
}, [stats, onStatsUpdate]);
|
||||||
|
|
||||||
// camera activity
|
// camera activity
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -274,6 +284,42 @@ export default function LivePlayer({
|
|||||||
}
|
}
|
||||||
}, [liveReady, isReEnabling]);
|
}, [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) {
|
if (!cameraConfig) {
|
||||||
return <ActivityIndicator />;
|
return <ActivityIndicator />;
|
||||||
}
|
}
|
||||||
@ -407,7 +453,8 @@ export default function LivePlayer({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{cameraEnabled &&
|
{!onLoadingChange &&
|
||||||
|
cameraEnabled &&
|
||||||
!offline &&
|
!offline &&
|
||||||
(!showStillWithoutActivity || isReEnabling) &&
|
(!showStillWithoutActivity || isReEnabling) &&
|
||||||
!liveReady && <ActivityIndicator />}
|
!liveReady && <ActivityIndicator />}
|
||||||
@ -530,14 +577,15 @@ export default function LivePlayer({
|
|||||||
{cameraName}
|
{cameraName}
|
||||||
</Chip>
|
</Chip>
|
||||||
)}
|
)}
|
||||||
{autoLive &&
|
{!onActiveMotionChange &&
|
||||||
|
autoLive &&
|
||||||
!offline &&
|
!offline &&
|
||||||
activeMotion &&
|
activeMotion &&
|
||||||
((showStillWithoutActivity && !liveReady) || liveReady) && (
|
((showStillWithoutActivity && !liveReady) || liveReady) && (
|
||||||
<MdCircle className="mr-2 size-2 animate-pulse text-danger shadow-danger drop-shadow-md" />
|
<MdCircle className="mr-2 size-2 animate-pulse text-danger shadow-danger drop-shadow-md" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{showStats && (
|
{showStats && !onStatsUpdate && (
|
||||||
<PlayerStats stats={stats} minimal={cameraRef !== undefined} />
|
<PlayerStats stats={stats} minimal={cameraRef !== undefined} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -23,9 +23,13 @@ import {
|
|||||||
AudioState,
|
AudioState,
|
||||||
LivePlayerMode,
|
LivePlayerMode,
|
||||||
LiveStreamMetadata,
|
LiveStreamMetadata,
|
||||||
|
PlayerStatsType,
|
||||||
StatsState,
|
StatsState,
|
||||||
VolumeState,
|
VolumeState,
|
||||||
} from "@/types/live";
|
} 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 { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
|
||||||
import { isEqual } from "lodash";
|
import { isEqual } from "lodash";
|
||||||
@ -431,6 +435,15 @@ export default function DraggableGridLayout({
|
|||||||
const [cameraZoomStates, setCameraZoomStates] = useState<
|
const [cameraZoomStates, setCameraZoomStates] = useState<
|
||||||
Record<string, CameraZoomRuntimeTransform>
|
Record<string, CameraZoomRuntimeTransform>
|
||||||
>({});
|
>({});
|
||||||
|
const [cameraStatsData, setCameraStatsData] = useState<
|
||||||
|
Record<string, PlayerStatsType>
|
||||||
|
>({});
|
||||||
|
const [cameraLoadingStates, setCameraLoadingStates] = useState<
|
||||||
|
Record<string, boolean>
|
||||||
|
>({});
|
||||||
|
const [cameraMotionStates, setCameraMotionStates] = useState<
|
||||||
|
Record<string, boolean>
|
||||||
|
>({});
|
||||||
const cameraZoomViewportRefs = useRef<Record<string, HTMLDivElement | null>>(
|
const cameraZoomViewportRefs = useRef<Record<string, HTMLDivElement | null>>(
|
||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
@ -808,7 +821,7 @@ export default function DraggableGridLayout({
|
|||||||
streamMetadata={streamMetadata}
|
streamMetadata={streamMetadata}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="size-full overflow-hidden"
|
className="relative size-full overflow-hidden"
|
||||||
ref={(node) => {
|
ref={(node) => {
|
||||||
cameraZoomViewportRefs.current[camera.name] = node;
|
cameraZoomViewportRefs.current[camera.name] = node;
|
||||||
|
|
||||||
@ -855,7 +868,25 @@ export default function DraggableGridLayout({
|
|||||||
preferredLiveModes[camera.name] ?? "mse"
|
preferredLiveModes[camera.name] ?? "mse"
|
||||||
}
|
}
|
||||||
playInBackground={false}
|
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={() => {
|
onClick={() => {
|
||||||
!isEditMode && onSelectCamera(camera.name);
|
!isEditMode && onSelectCamera(camera.name);
|
||||||
}}
|
}}
|
||||||
@ -875,6 +906,23 @@ export default function DraggableGridLayout({
|
|||||||
volume={volumeStates[camera.name]}
|
volume={volumeStates[camera.name]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{cameraLoadingStates[camera.name] && (
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
|
<ActivityIndicator />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{statsStates[camera.name] &&
|
||||||
|
cameraStatsData[camera.name] && (
|
||||||
|
<PlayerStats
|
||||||
|
stats={cameraStatsData[camera.name]}
|
||||||
|
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>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{isEditMode && showCircles && <CornerCircles />}
|
{isEditMode && showCircles && <CornerCircles />}
|
||||||
</GridLiveContextMenu>
|
</GridLiveContextMenu>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user