add context menu to dashboards

This commit is contained in:
Josh Hawkins 2024-12-22 12:29:07 -06:00
parent 9d2b678965
commit 1c5b6adf2b
2 changed files with 211 additions and 97 deletions

View File

@ -21,7 +21,7 @@ import {
} from "react-grid-layout"; } from "react-grid-layout";
import "react-grid-layout/css/styles.css"; import "react-grid-layout/css/styles.css";
import "react-resizable/css/styles.css"; import "react-resizable/css/styles.css";
import { LivePlayerError, LivePlayerMode } from "@/types/live"; import { LivePlayerMode } from "@/types/live";
import { ASPECT_VERTICAL_LAYOUT, ASPECT_WIDE_LAYOUT } from "@/types/record"; import { ASPECT_VERTICAL_LAYOUT, ASPECT_WIDE_LAYOUT } from "@/types/record";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { useResizeObserver } from "@/hooks/resize-observer"; import { useResizeObserver } from "@/hooks/resize-observer";
@ -43,6 +43,7 @@ import {
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { Toaster } from "@/components/ui/sonner"; import { Toaster } from "@/components/ui/sonner";
import useCameraLiveMode from "@/hooks/use-camera-live-mode"; import useCameraLiveMode from "@/hooks/use-camera-live-mode";
import LiveContextMenu from "@/components/menu/LiveContextMenu";
type DraggableGridLayoutProps = { type DraggableGridLayoutProps = {
cameras: CameraConfig[]; cameras: CameraConfig[];
@ -83,8 +84,13 @@ export default function DraggableGridLayout({
// preferred live modes per camera // preferred live modes per camera
const { preferredLiveModes, setPreferredLiveModes, resetPreferredLiveMode } = const {
useCameraLiveMode(cameras, windowVisible); preferredLiveModes,
setPreferredLiveModes,
resetPreferredLiveMode,
isRestreamedStates,
supportsAudioOutputStates,
} = useCameraLiveMode(cameras, windowVisible);
const [globalAutoLive] = usePersistence("autoLiveView", true); const [globalAutoLive] = usePersistence("autoLiveView", true);
@ -359,6 +365,34 @@ export default function DraggableGridLayout({
placeholder.h = layoutItem.h; placeholder.h = layoutItem.h;
}; };
// audio states
const [audioStates, setAudioStates] = useState<Record<string, boolean>>({});
const [volumeStates, setVolumeStates] = useState<Record<string, number>>({});
const toggleAudio = (cameraName: string): void => {
setAudioStates((prev) => ({
...prev,
[cameraName]: !prev[cameraName],
}));
};
const muteAll = (): void => {
const updatedStates: Record<string, boolean> = {};
visibleCameras.forEach((cameraName) => {
updatedStates[cameraName] = false;
});
setAudioStates(updatedStates);
};
const unmuteAll = (): void => {
const updatedStates: Record<string, boolean> = {};
visibleCameras.forEach((cameraName) => {
updatedStates[cameraName] = true;
});
setAudioStates(updatedStates);
};
return ( return (
<> <>
<Toaster position="top-center" closeButton={true} /> <Toaster position="top-center" closeButton={true} />
@ -381,7 +415,7 @@ export default function DraggableGridLayout({
</div> </div>
) : ( ) : (
<div <div
className="no-scrollbar my-2 overflow-x-hidden px-2 pb-8" className="no-scrollbar my-2 select-none overflow-x-hidden px-2 pb-8"
ref={gridContainerRef} ref={gridContainerRef}
> >
<EditGroupDialog <EditGroupDialog
@ -451,43 +485,66 @@ export default function DraggableGridLayout({
currentGroupStreamingSettings?.[camera.name] currentGroupStreamingSettings?.[camera.name]
?.compatibilityMode || false; ?.compatibilityMode || false;
return ( return (
<LivePlayerGridItem <GridLiveContextMenu
key={camera.name} key={camera.name}
streamName={streamName} camera={camera.name}
autoLive={autoLive ?? globalAutoLive}
showStillWithoutActivity={showStillWithoutActivity ?? true}
useWebGL={useWebGL}
cameraRef={cameraRef}
className={cn(
"rounded-lg bg-black md:rounded-2xl",
grow,
isEditMode &&
showCircles &&
"outline-2 outline-muted-foreground hover:cursor-grab hover:outline-4 active:cursor-grabbing",
)}
windowVisible={
windowVisible && visibleCameras.includes(camera.name)
}
cameraConfig={camera}
preferredLiveMode={preferredLiveModes[camera.name] ?? "mse"} preferredLiveMode={preferredLiveModes[camera.name] ?? "mse"}
onClick={() => { isRestreamed={isRestreamedStates[camera.name]}
!isEditMode && onSelectCamera(camera.name); supportsAudio={
}} supportsAudioOutputStates[streamName].supportsAudio
onError={(e) => { }
setPreferredLiveModes((prevModes) => { audioState={audioStates[camera.name]}
const newModes = { ...prevModes }; toggleAudio={() => toggleAudio(camera.name)}
if (e === "mse-decode") { volumeState={volumeStates[camera.name]}
newModes[camera.name] = "webrtc"; setVolumeState={(value) =>
} else { setVolumeStates({
newModes[camera.name] = "jsmpeg"; [camera.name]: value,
} })
return newModes; }
}); muteAll={muteAll}
}} unmuteAll={unmuteAll}
onResetLiveMode={() => resetPreferredLiveMode(camera.name)} resetPreferredLiveMode={() =>
resetPreferredLiveMode(camera.name)
}
> >
<LivePlayer
key={camera.name}
streamName={streamName}
autoLive={autoLive ?? globalAutoLive}
showStillWithoutActivity={showStillWithoutActivity ?? true}
useWebGL={useWebGL}
cameraRef={cameraRef}
className={cn(
"rounded-lg bg-black md:rounded-2xl",
grow,
isEditMode &&
showCircles &&
"outline-2 outline-muted-foreground hover:cursor-grab hover:outline-4 active:cursor-grabbing",
)}
windowVisible={
windowVisible && visibleCameras.includes(camera.name)
}
cameraConfig={camera}
preferredLiveMode={preferredLiveModes[camera.name] ?? "mse"}
playInBackground={false}
onClick={() => {
!isEditMode && onSelectCamera(camera.name);
}}
onError={(e) => {
setPreferredLiveModes((prevModes) => {
const newModes = { ...prevModes };
if (e === "mse-decode") {
newModes[camera.name] = "webrtc";
} else {
newModes[camera.name] = "jsmpeg";
}
return newModes;
});
}}
onResetLiveMode={() => resetPreferredLiveMode(camera.name)}
/>
{isEditMode && showCircles && <CornerCircles />} {isEditMode && showCircles && <CornerCircles />}
</LivePlayerGridItem> </GridLiveContextMenu>
); );
})} })}
</ResponsiveGridLayout> </ResponsiveGridLayout>
@ -630,49 +687,47 @@ const BirdseyeLivePlayerGridItem = React.forwardRef<
}, },
); );
type LivePlayerGridItemProps = { type GridLiveContextMenuProps = {
style?: React.CSSProperties; style?: React.CSSProperties;
className: string;
onMouseDown?: React.MouseEventHandler<HTMLDivElement>; onMouseDown?: React.MouseEventHandler<HTMLDivElement>;
onMouseUp?: React.MouseEventHandler<HTMLDivElement>; onMouseUp?: React.MouseEventHandler<HTMLDivElement>;
onTouchEnd?: React.TouchEventHandler<HTMLDivElement>; onTouchEnd?: React.TouchEventHandler<HTMLDivElement>;
children?: React.ReactNode; children?: React.ReactNode;
cameraRef: (node: HTMLElement | null) => void; camera: string;
windowVisible: boolean; preferredLiveMode: string;
cameraConfig: CameraConfig; isRestreamed: boolean;
preferredLiveMode: LivePlayerMode; supportsAudio: boolean;
onClick: () => void; audioState: boolean;
onError: (e: LivePlayerError) => void; toggleAudio: () => void;
onResetLiveMode: () => void; volumeState?: number;
streamName: string; setVolumeState: (volumeState: number) => void;
autoLive: boolean; muteAll: () => void;
showStillWithoutActivity: boolean; unmuteAll: () => void;
useWebGL: boolean; resetPreferredLiveMode: () => void;
}; };
const LivePlayerGridItem = React.forwardRef< const GridLiveContextMenu = React.forwardRef<
HTMLDivElement, HTMLDivElement,
LivePlayerGridItemProps GridLiveContextMenuProps
>( >(
( (
{ {
style, style,
className,
onMouseDown, onMouseDown,
onMouseUp, onMouseUp,
onTouchEnd, onTouchEnd,
children, children,
cameraRef, camera,
windowVisible,
cameraConfig,
preferredLiveMode, preferredLiveMode,
onClick, isRestreamed,
onError, supportsAudio,
onResetLiveMode, audioState,
autoLive, toggleAudio,
showStillWithoutActivity, volumeState,
streamName, setVolumeState,
useWebGL, muteAll,
unmuteAll,
resetPreferredLiveMode,
...props ...props
}, },
ref, ref,
@ -686,23 +741,21 @@ const LivePlayerGridItem = React.forwardRef<
onTouchEnd={onTouchEnd} onTouchEnd={onTouchEnd}
{...props} {...props}
> >
<LivePlayer <LiveContextMenu
cameraRef={cameraRef} camera={camera}
className={className}
windowVisible={windowVisible}
cameraConfig={cameraConfig}
preferredLiveMode={preferredLiveMode} preferredLiveMode={preferredLiveMode}
streamName={streamName} isRestreamed={isRestreamed}
onClick={onClick} supportsAudio={supportsAudio}
onError={onError} audioState={audioState}
onResetLiveMode={onResetLiveMode} toggleAudio={toggleAudio}
containerRef={ref as React.RefObject<HTMLDivElement>} volumeState={volumeState}
autoLive={autoLive} setVolumeState={setVolumeState}
playInBackground={false} muteAll={muteAll}
showStillWithoutActivity={showStillWithoutActivity} unmuteAll={unmuteAll}
useWebGL={useWebGL} resetPreferredLiveMode={resetPreferredLiveMode}
/> >
{children} {children}
</LiveContextMenu>
</div> </div>
); );
}, },

View File

@ -36,6 +36,7 @@ import { LivePlayerError } from "@/types/live";
import { FaCompress, FaExpand } from "react-icons/fa"; import { FaCompress, FaExpand } from "react-icons/fa";
import useCameraLiveMode from "@/hooks/use-camera-live-mode"; import useCameraLiveMode from "@/hooks/use-camera-live-mode";
import { useResizeObserver } from "@/hooks/resize-observer"; import { useResizeObserver } from "@/hooks/resize-observer";
import LiveContextMenu from "@/components/menu/LiveContextMenu";
type LiveDashboardViewProps = { type LiveDashboardViewProps = {
cameras: CameraConfig[]; cameras: CameraConfig[];
@ -188,8 +189,13 @@ export default function LiveDashboardView({
}; };
}, []); }, []);
const { preferredLiveModes, setPreferredLiveModes, resetPreferredLiveMode } = const {
useCameraLiveMode(cameras, windowVisible); preferredLiveModes,
setPreferredLiveModes,
resetPreferredLiveMode,
isRestreamedStates,
supportsAudioOutputStates,
} = useCameraLiveMode(cameras, windowVisible);
const [allGroupsStreamingSettings, setAllGroupsStreamingSettings] = const [allGroupsStreamingSettings, setAllGroupsStreamingSettings] =
useState<AllGroupsStreamingSettings>({}); useState<AllGroupsStreamingSettings>({});
@ -237,9 +243,37 @@ export default function LiveDashboardView({
[setPreferredLiveModes], [setPreferredLiveModes],
); );
// audio states
const [audioStates, setAudioStates] = useState<Record<string, boolean>>({});
const [volumeStates, setVolumeStates] = useState<Record<string, number>>({});
const toggleAudio = (cameraName: string): void => {
setAudioStates((prev) => ({
...prev,
[cameraName]: !prev[cameraName],
}));
};
const muteAll = (): void => {
const updatedStates: Record<string, boolean> = {};
visibleCameras.forEach((cameraName) => {
updatedStates[cameraName] = false;
});
setAudioStates(updatedStates);
};
const unmuteAll = (): void => {
const updatedStates: Record<string, boolean> = {};
visibleCameras.forEach((cameraName) => {
updatedStates[cameraName] = true;
});
setAudioStates(updatedStates);
};
return ( return (
<div <div
className="scrollbar-container size-full overflow-y-auto px-1 pt-2 md:p-2" className="scrollbar-container size-full select-none overflow-y-auto px-1 pt-2 md:p-2"
ref={containerRef} ref={containerRef}
> >
{isMobile && ( {isMobile && (
@ -364,23 +398,50 @@ export default function LiveDashboardView({
grow = "aspect-video"; grow = "aspect-video";
} }
return ( return (
<LivePlayer <LiveContextMenu
cameraRef={cameraRef}
key={camera.name} key={camera.name}
className={`${grow} rounded-lg bg-black md:rounded-2xl`} camera={camera.name}
windowVisible={
windowVisible && visibleCameras.includes(camera.name)
}
cameraConfig={camera}
preferredLiveMode={preferredLiveModes[camera.name] ?? "mse"} preferredLiveMode={preferredLiveModes[camera.name] ?? "mse"}
autoLive={autoLiveView} isRestreamed={isRestreamedStates[camera.name]}
useWebGL={false} supportsAudio={
playInBackground={false} supportsAudioOutputStates[
streamName={Object.values(camera.live.streams)[0]} Object.values(camera.live.streams)?.[0]
onClick={() => onSelectCamera(camera.name)} ]?.supportsAudio ?? false
onError={(e) => handleError(camera.name, e)} }
onResetLiveMode={() => resetPreferredLiveMode(camera.name)} audioState={audioStates[camera.name]}
/> toggleAudio={() => toggleAudio(camera.name)}
volumeState={volumeStates[camera.name]}
setVolumeState={(value) =>
setVolumeStates({
[camera.name]: value,
})
}
muteAll={muteAll}
unmuteAll={unmuteAll}
resetPreferredLiveMode={() =>
resetPreferredLiveMode(camera.name)
}
>
<LivePlayer
cameraRef={cameraRef}
key={camera.name}
className={`${grow} rounded-lg bg-black md:rounded-2xl`}
windowVisible={
windowVisible && visibleCameras.includes(camera.name)
}
cameraConfig={camera}
preferredLiveMode={preferredLiveModes[camera.name] ?? "mse"}
autoLive={autoLiveView}
useWebGL={false}
playInBackground={false}
streamName={Object.values(camera.live.streams)[0]}
onClick={() => onSelectCamera(camera.name)}
onError={(e) => handleError(camera.name, e)}
onResetLiveMode={() => resetPreferredLiveMode(camera.name)}
playAudio={audioStates[camera.name] ?? false}
volume={volumeStates[camera.name]}
/>
</LiveContextMenu>
); );
})} })}
</div> </div>