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; 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>

View File

@ -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>