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

fix: exclude stats, spinner and motion dot from camera zoom transform
This commit is contained in:
ibs0d 2026-03-16 20:32:14 +11:00 committed by GitHub
commit c3465dd611
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 101 additions and 5 deletions

View File

@ -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<React.SetStateAction<VideoResolutionType>>;
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 <ActivityIndicator />;
}
@ -407,7 +453,8 @@ export default function LivePlayer({
/>
</div>
{cameraEnabled &&
{!onLoadingChange &&
cameraEnabled &&
!offline &&
(!showStillWithoutActivity || isReEnabling) &&
!liveReady && <ActivityIndicator />}
@ -530,14 +577,15 @@ export default function LivePlayer({
{cameraName}
</Chip>
)}
{autoLive &&
{!onActiveMotionChange &&
autoLive &&
!offline &&
activeMotion &&
((showStillWithoutActivity && !liveReady) || liveReady) && (
<MdCircle className="mr-2 size-2 animate-pulse text-danger shadow-danger drop-shadow-md" />
)}
</div>
{showStats && (
{showStats && !onStatsUpdate && (
<PlayerStats stats={stats} minimal={cameraRef !== undefined} />
)}
</div>

View File

@ -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<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>>(
{},
);
@ -808,7 +821,7 @@ export default function DraggableGridLayout({
streamMetadata={streamMetadata}
>
<div
className="size-full overflow-hidden"
className="relative size-full overflow-hidden"
ref={(node) => {
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]}
/>
</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>
{isEditMode && showCircles && <CornerCircles />}
</GridLiveContextMenu>