2024-05-07 17:28:10 +03:00
|
|
|
import { usePersistence } from "@/hooks/use-persistence";
|
|
|
|
|
import {
|
2025-02-10 19:42:35 +03:00
|
|
|
AllGroupsStreamingSettings,
|
2024-05-07 17:28:10 +03:00
|
|
|
BirdseyeConfig,
|
|
|
|
|
CameraConfig,
|
|
|
|
|
FrigateConfig,
|
|
|
|
|
} from "@/types/frigateConfig";
|
|
|
|
|
import React, {
|
|
|
|
|
useCallback,
|
|
|
|
|
useEffect,
|
2024-05-08 15:53:22 +03:00
|
|
|
useLayoutEffect,
|
2024-05-07 17:28:10 +03:00
|
|
|
useMemo,
|
|
|
|
|
useRef,
|
|
|
|
|
useState,
|
|
|
|
|
} from "react";
|
2024-05-10 19:54:37 +03:00
|
|
|
import {
|
|
|
|
|
ItemCallback,
|
|
|
|
|
Layout,
|
|
|
|
|
Responsive,
|
|
|
|
|
WidthProvider,
|
|
|
|
|
} from "react-grid-layout";
|
2024-05-07 17:28:10 +03:00
|
|
|
import "react-grid-layout/css/styles.css";
|
|
|
|
|
import "react-resizable/css/styles.css";
|
2025-02-10 19:42:35 +03:00
|
|
|
import {
|
|
|
|
|
AudioState,
|
|
|
|
|
LivePlayerMode,
|
|
|
|
|
StatsState,
|
|
|
|
|
VolumeState,
|
|
|
|
|
} from "@/types/live";
|
2024-05-07 17:28:10 +03:00
|
|
|
import { ASPECT_VERTICAL_LAYOUT, ASPECT_WIDE_LAYOUT } from "@/types/record";
|
|
|
|
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
|
|
|
import { useResizeObserver } from "@/hooks/resize-observer";
|
|
|
|
|
import { isEqual } from "lodash";
|
|
|
|
|
import useSWR from "swr";
|
2024-06-29 18:02:30 +03:00
|
|
|
import { isDesktop, isMobile } from "react-device-detect";
|
2024-05-07 17:28:10 +03:00
|
|
|
import BirdseyeLivePlayer from "@/components/player/BirdseyeLivePlayer";
|
|
|
|
|
import LivePlayer from "@/components/player/LivePlayer";
|
2024-05-10 00:08:22 +03:00
|
|
|
import { IoClose } from "react-icons/io5";
|
|
|
|
|
import { LuLayoutDashboard, LuPencil } from "react-icons/lu";
|
|
|
|
|
import { cn } from "@/lib/utils";
|
|
|
|
|
import { EditGroupDialog } from "@/components/filter/CameraGroupSelector";
|
|
|
|
|
import { usePersistedOverlayState } from "@/hooks/use-overlay-state";
|
|
|
|
|
import { FaCompress, FaExpand } from "react-icons/fa";
|
2024-05-07 17:28:10 +03:00
|
|
|
import {
|
|
|
|
|
Tooltip,
|
|
|
|
|
TooltipTrigger,
|
2024-05-10 00:08:22 +03:00
|
|
|
TooltipContent,
|
2024-05-07 17:28:10 +03:00
|
|
|
} from "@/components/ui/tooltip";
|
2024-05-10 19:54:37 +03:00
|
|
|
import { Toaster } from "@/components/ui/sonner";
|
2024-08-17 21:16:48 +03:00
|
|
|
import useCameraLiveMode from "@/hooks/use-camera-live-mode";
|
2025-02-10 19:42:35 +03:00
|
|
|
import LiveContextMenu from "@/components/menu/LiveContextMenu";
|
|
|
|
|
import { useStreamingSettings } from "@/context/streaming-settings-provider";
|
2025-03-16 18:36:20 +03:00
|
|
|
import { useTranslation } from "react-i18next";
|
2024-05-07 17:28:10 +03:00
|
|
|
|
|
|
|
|
type DraggableGridLayoutProps = {
|
|
|
|
|
cameras: CameraConfig[];
|
|
|
|
|
cameraGroup: string;
|
|
|
|
|
cameraRef: (node: HTMLElement | null) => void;
|
2024-05-08 15:53:22 +03:00
|
|
|
containerRef: React.RefObject<HTMLDivElement>;
|
2024-05-07 17:28:10 +03:00
|
|
|
includeBirdseye: boolean;
|
|
|
|
|
onSelectCamera: (camera: string) => void;
|
|
|
|
|
windowVisible: boolean;
|
|
|
|
|
visibleCameras: string[];
|
2024-05-08 15:53:22 +03:00
|
|
|
isEditMode: boolean;
|
|
|
|
|
setIsEditMode: React.Dispatch<React.SetStateAction<boolean>>;
|
2024-05-31 15:58:33 +03:00
|
|
|
fullscreen: boolean;
|
|
|
|
|
toggleFullscreen: () => void;
|
2024-05-07 17:28:10 +03:00
|
|
|
};
|
|
|
|
|
export default function DraggableGridLayout({
|
|
|
|
|
cameras,
|
|
|
|
|
cameraGroup,
|
2024-05-08 15:53:22 +03:00
|
|
|
containerRef,
|
2024-05-07 17:28:10 +03:00
|
|
|
cameraRef,
|
|
|
|
|
includeBirdseye,
|
|
|
|
|
onSelectCamera,
|
|
|
|
|
windowVisible,
|
|
|
|
|
visibleCameras,
|
2024-05-08 15:53:22 +03:00
|
|
|
isEditMode,
|
|
|
|
|
setIsEditMode,
|
2024-05-31 15:58:33 +03:00
|
|
|
fullscreen,
|
|
|
|
|
toggleFullscreen,
|
2024-05-07 17:28:10 +03:00
|
|
|
}: DraggableGridLayoutProps) {
|
2025-03-16 18:36:20 +03:00
|
|
|
const { t } = useTranslation(["views/live"]);
|
2024-05-07 17:28:10 +03:00
|
|
|
const { data: config } = useSWR<FrigateConfig>("config");
|
|
|
|
|
const birdseyeConfig = useMemo(() => config?.birdseye, [config]);
|
|
|
|
|
|
2024-06-29 18:02:30 +03:00
|
|
|
// preferred live modes per camera
|
|
|
|
|
|
2025-02-10 19:42:35 +03:00
|
|
|
const [globalAutoLive] = usePersistence("autoLiveView", true);
|
2025-10-29 17:20:11 +03:00
|
|
|
const [displayCameraNames] = usePersistence("displayCameraNames", false);
|
2025-02-10 19:42:35 +03:00
|
|
|
|
|
|
|
|
const { allGroupsStreamingSettings, setAllGroupsStreamingSettings } =
|
|
|
|
|
useStreamingSettings();
|
|
|
|
|
|
|
|
|
|
const currentGroupStreamingSettings = useMemo(() => {
|
|
|
|
|
if (cameraGroup && cameraGroup != "default" && allGroupsStreamingSettings) {
|
|
|
|
|
return allGroupsStreamingSettings[cameraGroup];
|
|
|
|
|
}
|
|
|
|
|
}, [allGroupsStreamingSettings, cameraGroup]);
|
|
|
|
|
|
2025-11-12 02:00:54 +03:00
|
|
|
const activeStreams = useMemo(() => {
|
|
|
|
|
const streams: { [cameraName: string]: string } = {};
|
|
|
|
|
cameras.forEach((camera) => {
|
|
|
|
|
const availableStreams = camera.live.streams || {};
|
|
|
|
|
const streamNameFromSettings =
|
|
|
|
|
currentGroupStreamingSettings?.[camera.name]?.streamName || "";
|
|
|
|
|
const streamExists =
|
|
|
|
|
streamNameFromSettings &&
|
|
|
|
|
Object.values(availableStreams).includes(streamNameFromSettings);
|
|
|
|
|
|
|
|
|
|
const streamName = streamExists
|
|
|
|
|
? streamNameFromSettings
|
|
|
|
|
: Object.values(availableStreams)[0] || "";
|
|
|
|
|
|
|
|
|
|
streams[camera.name] = streamName;
|
|
|
|
|
});
|
|
|
|
|
return streams;
|
|
|
|
|
}, [cameras, currentGroupStreamingSettings]);
|
|
|
|
|
|
|
|
|
|
const {
|
|
|
|
|
preferredLiveModes,
|
|
|
|
|
setPreferredLiveModes,
|
|
|
|
|
resetPreferredLiveMode,
|
|
|
|
|
isRestreamedStates,
|
|
|
|
|
supportsAudioOutputStates,
|
|
|
|
|
} = useCameraLiveMode(cameras, windowVisible, activeStreams);
|
|
|
|
|
|
2025-02-10 19:42:35 +03:00
|
|
|
// grid layout
|
2024-06-29 18:02:30 +03:00
|
|
|
|
2024-05-07 17:28:10 +03:00
|
|
|
const ResponsiveGridLayout = useMemo(() => WidthProvider(Responsive), []);
|
|
|
|
|
|
|
|
|
|
const [gridLayout, setGridLayout, isGridLayoutLoaded] = usePersistence<
|
|
|
|
|
Layout[]
|
|
|
|
|
>(`${cameraGroup}-draggable-layout`);
|
|
|
|
|
|
2024-05-10 00:08:22 +03:00
|
|
|
const [group] = usePersistedOverlayState("cameraGroup", "default" as string);
|
|
|
|
|
|
|
|
|
|
const groups = useMemo(() => {
|
|
|
|
|
if (!config) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Object.entries(config.camera_groups).sort(
|
|
|
|
|
(a, b) => a[1].order - b[1].order,
|
|
|
|
|
);
|
|
|
|
|
}, [config]);
|
|
|
|
|
|
2024-06-04 18:10:19 +03:00
|
|
|
// editing
|
|
|
|
|
|
2024-05-10 00:08:22 +03:00
|
|
|
const [editGroup, setEditGroup] = useState(false);
|
2024-06-06 15:26:02 +03:00
|
|
|
const [showCircles, setShowCircles] = useState(true);
|
2024-05-10 00:08:22 +03:00
|
|
|
|
2024-06-04 18:10:19 +03:00
|
|
|
useEffect(() => {
|
2024-06-13 22:11:48 +03:00
|
|
|
setIsEditMode(false);
|
2024-06-04 18:10:19 +03:00
|
|
|
setEditGroup(false);
|
2024-06-13 22:11:48 +03:00
|
|
|
}, [cameraGroup, setIsEditMode]);
|
2024-06-04 18:10:19 +03:00
|
|
|
|
|
|
|
|
// camera state
|
|
|
|
|
|
2024-05-07 17:28:10 +03:00
|
|
|
const [currentCameras, setCurrentCameras] = useState<CameraConfig[]>();
|
|
|
|
|
const [currentIncludeBirdseye, setCurrentIncludeBirdseye] =
|
|
|
|
|
useState<boolean>();
|
|
|
|
|
const [currentGridLayout, setCurrentGridLayout] = useState<
|
|
|
|
|
Layout[] | undefined
|
|
|
|
|
>();
|
|
|
|
|
|
|
|
|
|
const handleLayoutChange = useCallback(
|
|
|
|
|
(currentLayout: Layout[]) => {
|
|
|
|
|
if (!isGridLayoutLoaded || !isEqual(gridLayout, currentGridLayout)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// save layout to idb
|
|
|
|
|
setGridLayout(currentLayout);
|
2024-06-06 15:26:02 +03:00
|
|
|
setShowCircles(true);
|
2024-05-07 17:28:10 +03:00
|
|
|
},
|
|
|
|
|
[setGridLayout, isGridLayoutLoaded, gridLayout, currentGridLayout],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const generateLayout = useCallback(() => {
|
|
|
|
|
if (!isGridLayoutLoaded) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const cameraNames =
|
|
|
|
|
includeBirdseye && birdseyeConfig?.enabled
|
|
|
|
|
? ["birdseye", ...cameras.map((camera) => camera?.name || "")]
|
|
|
|
|
: cameras.map((camera) => camera?.name || "");
|
|
|
|
|
|
|
|
|
|
const optionsMap: Layout[] = currentGridLayout
|
|
|
|
|
? currentGridLayout.filter((layout) => cameraNames?.includes(layout.i))
|
|
|
|
|
: [];
|
|
|
|
|
|
|
|
|
|
cameraNames.forEach((cameraName, index) => {
|
|
|
|
|
const existingLayout = optionsMap.find(
|
|
|
|
|
(layout) => layout.i === cameraName,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Skip if the camera already exists in the layout
|
|
|
|
|
if (existingLayout) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let aspectRatio;
|
|
|
|
|
let col;
|
|
|
|
|
|
|
|
|
|
// Handle "birdseye" camera as a special case
|
|
|
|
|
if (cameraName === "birdseye") {
|
|
|
|
|
aspectRatio =
|
|
|
|
|
(birdseyeConfig?.width || 1) / (birdseyeConfig?.height || 1);
|
|
|
|
|
col = 0; // Set birdseye camera in the first column
|
|
|
|
|
} else {
|
|
|
|
|
const camera = cameras.find((cam) => cam.name === cameraName);
|
|
|
|
|
aspectRatio =
|
|
|
|
|
(camera && camera?.detect.width / camera?.detect.height) || 16 / 9;
|
|
|
|
|
col = index % 3; // Regular cameras distributed across columns
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Calculate layout options based on aspect ratio
|
|
|
|
|
const columnsPerPlayer = 4;
|
|
|
|
|
let height;
|
|
|
|
|
let width;
|
|
|
|
|
|
|
|
|
|
if (aspectRatio < 1) {
|
|
|
|
|
// Portrait
|
|
|
|
|
height = 2 * columnsPerPlayer;
|
|
|
|
|
width = columnsPerPlayer;
|
|
|
|
|
} else if (aspectRatio > 2) {
|
|
|
|
|
// Wide
|
|
|
|
|
height = 1 * columnsPerPlayer;
|
|
|
|
|
width = 2 * columnsPerPlayer;
|
|
|
|
|
} else {
|
|
|
|
|
// Landscape
|
|
|
|
|
height = 1 * columnsPerPlayer;
|
|
|
|
|
width = columnsPerPlayer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const options = {
|
|
|
|
|
i: cameraName,
|
|
|
|
|
x: col * width,
|
|
|
|
|
y: 0, // don't set y, grid does automatically
|
|
|
|
|
w: width,
|
|
|
|
|
h: height,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
optionsMap.push(options);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return optionsMap;
|
|
|
|
|
}, [
|
|
|
|
|
cameras,
|
|
|
|
|
isGridLayoutLoaded,
|
|
|
|
|
currentGridLayout,
|
|
|
|
|
includeBirdseye,
|
|
|
|
|
birdseyeConfig,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (isGridLayoutLoaded) {
|
|
|
|
|
if (gridLayout) {
|
|
|
|
|
// set current grid layout from loaded
|
|
|
|
|
setCurrentGridLayout(gridLayout);
|
|
|
|
|
} else {
|
|
|
|
|
// idb is empty, set it with an initial layout
|
|
|
|
|
setGridLayout(generateLayout());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}, [
|
|
|
|
|
isEditMode,
|
|
|
|
|
gridLayout,
|
|
|
|
|
currentGridLayout,
|
|
|
|
|
setGridLayout,
|
|
|
|
|
isGridLayoutLoaded,
|
|
|
|
|
generateLayout,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (
|
|
|
|
|
!isEqual(cameras, currentCameras) ||
|
|
|
|
|
includeBirdseye !== currentIncludeBirdseye
|
|
|
|
|
) {
|
|
|
|
|
setCurrentCameras(cameras);
|
|
|
|
|
setCurrentIncludeBirdseye(includeBirdseye);
|
|
|
|
|
|
|
|
|
|
// set new grid layout in idb
|
|
|
|
|
setGridLayout(generateLayout());
|
|
|
|
|
}
|
|
|
|
|
}, [
|
|
|
|
|
cameras,
|
|
|
|
|
includeBirdseye,
|
|
|
|
|
currentCameras,
|
|
|
|
|
currentIncludeBirdseye,
|
|
|
|
|
setCurrentGridLayout,
|
|
|
|
|
generateLayout,
|
|
|
|
|
setGridLayout,
|
|
|
|
|
isGridLayoutLoaded,
|
|
|
|
|
]);
|
|
|
|
|
|
2024-05-08 15:53:22 +03:00
|
|
|
const [marginValue, setMarginValue] = useState(16);
|
|
|
|
|
|
|
|
|
|
// calculate margin value for browsers that don't have default font size of 16px
|
|
|
|
|
useLayoutEffect(() => {
|
|
|
|
|
const calculateRemValue = () => {
|
|
|
|
|
const htmlElement = document.documentElement;
|
|
|
|
|
const fontSize = window.getComputedStyle(htmlElement).fontSize;
|
|
|
|
|
setMarginValue(parseFloat(fontSize));
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
calculateRemValue();
|
|
|
|
|
}, []);
|
|
|
|
|
|
2024-05-07 17:28:10 +03:00
|
|
|
const gridContainerRef = useRef<HTMLDivElement>(null);
|
|
|
|
|
|
2024-05-08 15:53:22 +03:00
|
|
|
const [{ width: containerWidth, height: containerHeight }] =
|
|
|
|
|
useResizeObserver(gridContainerRef);
|
|
|
|
|
|
2024-05-29 21:05:28 +03:00
|
|
|
const scrollBarWidth = useMemo(() => {
|
|
|
|
|
if (containerWidth && containerHeight && containerRef.current) {
|
|
|
|
|
return (
|
|
|
|
|
containerRef.current.offsetWidth - containerRef.current.clientWidth
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
return 0;
|
|
|
|
|
}, [containerRef, containerHeight, containerWidth]);
|
|
|
|
|
|
|
|
|
|
const availableWidth = useMemo(
|
|
|
|
|
() => (scrollBarWidth ? containerWidth + scrollBarWidth : containerWidth),
|
|
|
|
|
[containerWidth, scrollBarWidth],
|
|
|
|
|
);
|
|
|
|
|
|
2024-05-08 15:53:22 +03:00
|
|
|
const hasScrollbar = useMemo(() => {
|
2024-05-29 21:05:28 +03:00
|
|
|
if (containerHeight && containerRef.current) {
|
|
|
|
|
return (
|
|
|
|
|
containerRef.current.offsetHeight < containerRef.current.scrollHeight
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}, [containerRef, containerHeight]);
|
2024-05-07 17:28:10 +03:00
|
|
|
|
|
|
|
|
const cellHeight = useMemo(() => {
|
|
|
|
|
const aspectRatio = 16 / 9;
|
2024-05-08 15:53:22 +03:00
|
|
|
// subtract container margin, 1 camera takes up at least 4 rows
|
|
|
|
|
// account for additional margin on bottom of each row
|
|
|
|
|
return (
|
2024-05-29 21:05:28 +03:00
|
|
|
((availableWidth ?? window.innerWidth) - 2 * marginValue) /
|
2024-05-08 15:53:22 +03:00
|
|
|
12 /
|
|
|
|
|
aspectRatio -
|
|
|
|
|
marginValue +
|
|
|
|
|
marginValue / 4
|
|
|
|
|
);
|
2024-05-29 21:05:28 +03:00
|
|
|
}, [availableWidth, marginValue]);
|
2024-05-07 17:28:10 +03:00
|
|
|
|
2024-05-10 19:54:37 +03:00
|
|
|
const handleResize: ItemCallback = (
|
|
|
|
|
_: Layout[],
|
|
|
|
|
oldLayoutItem: Layout,
|
|
|
|
|
layoutItem: Layout,
|
|
|
|
|
placeholder: Layout,
|
|
|
|
|
) => {
|
|
|
|
|
const heightDiff = layoutItem.h - oldLayoutItem.h;
|
|
|
|
|
const widthDiff = layoutItem.w - oldLayoutItem.w;
|
|
|
|
|
const changeCoef = oldLayoutItem.w / oldLayoutItem.h;
|
2024-06-06 15:26:02 +03:00
|
|
|
|
|
|
|
|
let newWidth, newHeight;
|
|
|
|
|
|
|
|
|
|
if (Math.abs(heightDiff) < Math.abs(widthDiff)) {
|
|
|
|
|
newHeight = Math.round(layoutItem.w / changeCoef);
|
|
|
|
|
newWidth = Math.round(newHeight * changeCoef);
|
2024-05-10 19:54:37 +03:00
|
|
|
} else {
|
2024-06-06 15:26:02 +03:00
|
|
|
newWidth = Math.round(layoutItem.h * changeCoef);
|
|
|
|
|
newHeight = Math.round(newWidth / changeCoef);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Ensure dimensions maintain aspect ratio and fit within the grid
|
|
|
|
|
if (layoutItem.x + newWidth > 12) {
|
|
|
|
|
newWidth = 12 - layoutItem.x;
|
|
|
|
|
newHeight = Math.round(newWidth / changeCoef);
|
2024-05-10 19:54:37 +03:00
|
|
|
}
|
2024-06-06 15:26:02 +03:00
|
|
|
|
|
|
|
|
if (changeCoef == 0.5) {
|
|
|
|
|
// portrait
|
|
|
|
|
newHeight = Math.ceil(newHeight / 2) * 2;
|
|
|
|
|
} else if (changeCoef == 2) {
|
|
|
|
|
// pano/wide
|
|
|
|
|
newHeight = Math.ceil(newHeight * 2) / 2;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
newWidth = Math.round(newHeight * changeCoef);
|
|
|
|
|
|
|
|
|
|
layoutItem.w = newWidth;
|
|
|
|
|
layoutItem.h = newHeight;
|
|
|
|
|
placeholder.w = layoutItem.w;
|
|
|
|
|
placeholder.h = layoutItem.h;
|
2024-05-10 19:54:37 +03:00
|
|
|
};
|
|
|
|
|
|
2025-02-10 19:42:35 +03:00
|
|
|
// audio and stats states
|
|
|
|
|
|
|
|
|
|
const [audioStates, setAudioStates] = useState<AudioState>({});
|
|
|
|
|
const [volumeStates, setVolumeStates] = useState<VolumeState>({});
|
|
|
|
|
const [statsStates, setStatsStates] = useState<StatsState>(() => {
|
|
|
|
|
const initialStates: StatsState = {};
|
|
|
|
|
cameras.forEach((camera) => {
|
|
|
|
|
initialStates[camera.name] = false;
|
|
|
|
|
});
|
|
|
|
|
return initialStates;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const toggleStats = (cameraName: string): void => {
|
|
|
|
|
setStatsStates((prev) => ({
|
|
|
|
|
...prev,
|
|
|
|
|
[cameraName]: !prev[cameraName],
|
|
|
|
|
}));
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!allGroupsStreamingSettings) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const initialAudioStates: AudioState = {};
|
|
|
|
|
const initialVolumeStates: VolumeState = {};
|
|
|
|
|
|
|
|
|
|
Object.entries(allGroupsStreamingSettings).forEach(([_, groupSettings]) => {
|
2025-03-04 00:05:49 +03:00
|
|
|
if (groupSettings) {
|
|
|
|
|
Object.entries(groupSettings).forEach(([camera, cameraSettings]) => {
|
|
|
|
|
initialAudioStates[camera] = cameraSettings.playAudio ?? false;
|
|
|
|
|
initialVolumeStates[camera] = cameraSettings.volume ?? 1;
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-02-10 19:42:35 +03:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
setAudioStates(initialAudioStates);
|
|
|
|
|
setVolumeStates(initialVolumeStates);
|
|
|
|
|
}, [allGroupsStreamingSettings]);
|
|
|
|
|
|
|
|
|
|
const toggleAudio = (cameraName: string) => {
|
|
|
|
|
setAudioStates((prev) => ({
|
|
|
|
|
...prev,
|
|
|
|
|
[cameraName]: !prev[cameraName],
|
|
|
|
|
}));
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const onSaveMuting = useCallback(
|
|
|
|
|
(playAudio: boolean) => {
|
|
|
|
|
if (!cameraGroup || !allGroupsStreamingSettings) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const existingGroupSettings =
|
|
|
|
|
allGroupsStreamingSettings[cameraGroup] || {};
|
|
|
|
|
|
|
|
|
|
const updatedSettings: AllGroupsStreamingSettings = {
|
|
|
|
|
...Object.fromEntries(
|
|
|
|
|
Object.entries(allGroupsStreamingSettings || {}).filter(
|
|
|
|
|
([key]) => key !== cameraGroup,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
[cameraGroup]: {
|
|
|
|
|
...existingGroupSettings,
|
|
|
|
|
...Object.fromEntries(
|
|
|
|
|
Object.entries(existingGroupSettings).map(
|
|
|
|
|
([cameraName, settings]) => [
|
|
|
|
|
cameraName,
|
|
|
|
|
{
|
|
|
|
|
...settings,
|
|
|
|
|
playAudio: playAudio,
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
setAllGroupsStreamingSettings?.(updatedSettings);
|
|
|
|
|
},
|
|
|
|
|
[cameraGroup, allGroupsStreamingSettings, setAllGroupsStreamingSettings],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const muteAll = () => {
|
|
|
|
|
const updatedStates: AudioState = {};
|
|
|
|
|
cameras.forEach((camera) => {
|
|
|
|
|
updatedStates[camera.name] = false;
|
|
|
|
|
});
|
|
|
|
|
setAudioStates(updatedStates);
|
|
|
|
|
onSaveMuting(false);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const unmuteAll = () => {
|
|
|
|
|
const updatedStates: AudioState = {};
|
|
|
|
|
cameras.forEach((camera) => {
|
|
|
|
|
updatedStates[camera.name] = true;
|
|
|
|
|
});
|
|
|
|
|
setAudioStates(updatedStates);
|
|
|
|
|
onSaveMuting(true);
|
|
|
|
|
};
|
|
|
|
|
|
2024-05-07 17:28:10 +03:00
|
|
|
return (
|
|
|
|
|
<>
|
2024-05-10 19:54:37 +03:00
|
|
|
<Toaster position="top-center" closeButton={true} />
|
2024-05-30 23:17:00 +03:00
|
|
|
{!isGridLayoutLoaded ||
|
|
|
|
|
!currentGridLayout ||
|
|
|
|
|
!isEqual(cameras, currentCameras) ||
|
|
|
|
|
includeBirdseye !== currentIncludeBirdseye ? (
|
2024-05-14 18:06:44 +03:00
|
|
|
<div className="mt-2 grid grid-cols-2 gap-2 px-2 md:gap-4 xl:grid-cols-3 3xl:grid-cols-4">
|
2024-05-07 17:28:10 +03:00
|
|
|
{includeBirdseye && birdseyeConfig?.enabled && (
|
2024-05-08 15:53:22 +03:00
|
|
|
<Skeleton className="size-full rounded-lg md:rounded-2xl" />
|
2024-05-07 17:28:10 +03:00
|
|
|
)}
|
|
|
|
|
{cameras.map((camera) => {
|
|
|
|
|
return (
|
|
|
|
|
<Skeleton
|
|
|
|
|
key={camera.name}
|
2024-05-08 15:53:22 +03:00
|
|
|
className="aspect-video size-full rounded-lg md:rounded-2xl"
|
2024-05-07 17:28:10 +03:00
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<div
|
2025-02-10 19:42:35 +03:00
|
|
|
className="no-scrollbar my-2 select-none overflow-x-hidden px-2 pb-8"
|
2024-05-07 17:28:10 +03:00
|
|
|
ref={gridContainerRef}
|
|
|
|
|
>
|
2024-05-10 00:08:22 +03:00
|
|
|
<EditGroupDialog
|
|
|
|
|
open={editGroup}
|
|
|
|
|
setOpen={setEditGroup}
|
|
|
|
|
currentGroups={groups}
|
|
|
|
|
activeGroup={group}
|
|
|
|
|
/>
|
2024-05-07 17:28:10 +03:00
|
|
|
<ResponsiveGridLayout
|
|
|
|
|
className="grid-layout"
|
|
|
|
|
layouts={{
|
|
|
|
|
lg: currentGridLayout,
|
|
|
|
|
md: currentGridLayout,
|
|
|
|
|
sm: currentGridLayout,
|
|
|
|
|
xs: currentGridLayout,
|
|
|
|
|
xxs: currentGridLayout,
|
|
|
|
|
}}
|
|
|
|
|
rowHeight={cellHeight}
|
|
|
|
|
breakpoints={{ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }}
|
|
|
|
|
cols={{ lg: 12, md: 12, sm: 12, xs: 12, xxs: 12 }}
|
2024-05-08 15:53:22 +03:00
|
|
|
margin={[marginValue, marginValue]}
|
|
|
|
|
containerPadding={[0, isEditMode ? 6 : 3]}
|
|
|
|
|
resizeHandles={isEditMode ? ["sw", "nw", "se", "ne"] : []}
|
2024-05-07 17:28:10 +03:00
|
|
|
onDragStop={handleLayoutChange}
|
2024-05-10 19:54:37 +03:00
|
|
|
onResize={handleResize}
|
2024-06-06 15:26:02 +03:00
|
|
|
onResizeStart={() => setShowCircles(false)}
|
2024-05-07 17:28:10 +03:00
|
|
|
onResizeStop={handleLayoutChange}
|
2024-06-13 22:11:48 +03:00
|
|
|
isDraggable={isEditMode}
|
|
|
|
|
isResizable={isEditMode}
|
2024-05-07 17:28:10 +03:00
|
|
|
>
|
|
|
|
|
{includeBirdseye && birdseyeConfig?.enabled && (
|
|
|
|
|
<BirdseyeLivePlayerGridItem
|
|
|
|
|
key="birdseye"
|
2024-05-08 15:53:22 +03:00
|
|
|
className={cn(
|
|
|
|
|
isEditMode &&
|
2024-06-06 15:26:02 +03:00
|
|
|
showCircles &&
|
2024-05-14 18:06:44 +03:00
|
|
|
"outline outline-2 outline-muted-foreground hover:cursor-grab hover:outline-4 active:cursor-grabbing",
|
2024-05-08 15:53:22 +03:00
|
|
|
)}
|
2024-05-07 17:28:10 +03:00
|
|
|
birdseyeConfig={birdseyeConfig}
|
|
|
|
|
liveMode={birdseyeConfig.restream ? "mse" : "jsmpeg"}
|
|
|
|
|
onClick={() => onSelectCamera("birdseye")}
|
|
|
|
|
>
|
2024-06-06 15:26:02 +03:00
|
|
|
{isEditMode && showCircles && <CornerCircles />}
|
2024-05-07 17:28:10 +03:00
|
|
|
</BirdseyeLivePlayerGridItem>
|
|
|
|
|
)}
|
|
|
|
|
{cameras.map((camera) => {
|
|
|
|
|
let grow;
|
|
|
|
|
const aspectRatio = camera.detect.width / camera.detect.height;
|
|
|
|
|
if (aspectRatio > ASPECT_WIDE_LAYOUT) {
|
2024-05-08 15:53:22 +03:00
|
|
|
grow = `aspect-wide w-full`;
|
2024-05-07 17:28:10 +03:00
|
|
|
} else if (aspectRatio < ASPECT_VERTICAL_LAYOUT) {
|
2024-05-08 15:53:22 +03:00
|
|
|
grow = `aspect-tall h-full`;
|
2024-05-07 17:28:10 +03:00
|
|
|
} else {
|
|
|
|
|
grow = "aspect-video";
|
|
|
|
|
}
|
2025-03-23 21:51:06 +03:00
|
|
|
const availableStreams = camera.live.streams || {};
|
|
|
|
|
const firstStreamEntry = Object.values(availableStreams)[0] || "";
|
|
|
|
|
|
|
|
|
|
const streamNameFromSettings =
|
|
|
|
|
currentGroupStreamingSettings?.[camera.name]?.streamName || "";
|
|
|
|
|
const streamExists =
|
|
|
|
|
streamNameFromSettings &&
|
|
|
|
|
Object.values(availableStreams).includes(
|
|
|
|
|
streamNameFromSettings,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const streamName = streamExists
|
|
|
|
|
? streamNameFromSettings
|
|
|
|
|
: firstStreamEntry;
|
2025-06-03 15:33:32 +03:00
|
|
|
const streamType =
|
|
|
|
|
currentGroupStreamingSettings?.[camera.name]?.streamType;
|
2025-02-10 19:42:35 +03:00
|
|
|
const autoLive =
|
2025-06-03 15:33:32 +03:00
|
|
|
streamType !== undefined
|
|
|
|
|
? streamType !== "no-streaming"
|
|
|
|
|
: undefined;
|
2025-02-10 19:42:35 +03:00
|
|
|
const showStillWithoutActivity =
|
|
|
|
|
currentGroupStreamingSettings?.[camera.name]?.streamType !==
|
|
|
|
|
"continuous";
|
|
|
|
|
const useWebGL =
|
|
|
|
|
currentGroupStreamingSettings?.[camera.name]
|
|
|
|
|
?.compatibilityMode || false;
|
2024-05-07 17:28:10 +03:00
|
|
|
return (
|
2025-02-10 19:42:35 +03:00
|
|
|
<GridLiveContextMenu
|
|
|
|
|
className={grow}
|
2024-05-07 17:28:10 +03:00
|
|
|
key={camera.name}
|
2025-02-10 19:42:35 +03:00
|
|
|
camera={camera.name}
|
|
|
|
|
streamName={streamName}
|
|
|
|
|
cameraGroup={cameraGroup}
|
2024-06-29 18:02:30 +03:00
|
|
|
preferredLiveMode={preferredLiveModes[camera.name] ?? "mse"}
|
2025-02-10 19:42:35 +03:00
|
|
|
isRestreamed={isRestreamedStates[camera.name]}
|
|
|
|
|
supportsAudio={
|
2025-04-02 15:43:28 +03:00
|
|
|
supportsAudioOutputStates[streamName]?.supportsAudio ??
|
|
|
|
|
false
|
2025-02-10 19:42:35 +03:00
|
|
|
}
|
|
|
|
|
audioState={audioStates[camera.name]}
|
|
|
|
|
toggleAudio={() => toggleAudio(camera.name)}
|
|
|
|
|
statsState={statsStates[camera.name]}
|
|
|
|
|
toggleStats={() => toggleStats(camera.name)}
|
|
|
|
|
volumeState={volumeStates[camera.name]}
|
|
|
|
|
setVolumeState={(value) =>
|
|
|
|
|
setVolumeStates({
|
|
|
|
|
[camera.name]: value,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
muteAll={muteAll}
|
|
|
|
|
unmuteAll={unmuteAll}
|
|
|
|
|
resetPreferredLiveMode={() =>
|
|
|
|
|
resetPreferredLiveMode(camera.name)
|
|
|
|
|
}
|
2025-02-17 17:19:03 +03:00
|
|
|
config={config}
|
2024-05-07 17:28:10 +03:00
|
|
|
>
|
2025-02-10 19:42:35 +03:00
|
|
|
<LivePlayer
|
|
|
|
|
key={camera.name}
|
|
|
|
|
streamName={streamName}
|
|
|
|
|
autoLive={autoLive ?? globalAutoLive}
|
|
|
|
|
showStillWithoutActivity={showStillWithoutActivity ?? true}
|
2025-10-29 17:20:11 +03:00
|
|
|
alwaysShowCameraName={displayCameraNames}
|
2025-02-10 19:42:35 +03:00
|
|
|
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}
|
|
|
|
|
showStats={statsStates[camera.name]}
|
|
|
|
|
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)}
|
|
|
|
|
playAudio={audioStates[camera.name]}
|
|
|
|
|
volume={volumeStates[camera.name]}
|
|
|
|
|
/>
|
2024-06-06 15:26:02 +03:00
|
|
|
{isEditMode && showCircles && <CornerCircles />}
|
2025-02-10 19:42:35 +03:00
|
|
|
</GridLiveContextMenu>
|
2024-05-07 17:28:10 +03:00
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</ResponsiveGridLayout>
|
2024-05-10 19:54:37 +03:00
|
|
|
{isDesktop && (
|
2024-05-10 00:08:22 +03:00
|
|
|
<div
|
|
|
|
|
className={cn(
|
|
|
|
|
"fixed",
|
|
|
|
|
isDesktop && "bottom-12 lg:bottom-9",
|
|
|
|
|
isMobile && "bottom-12 lg:bottom-16",
|
|
|
|
|
hasScrollbar && isDesktop ? "right-6" : "right-3",
|
|
|
|
|
"z-50 flex flex-row gap-2",
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
<Tooltip>
|
|
|
|
|
<TooltipTrigger asChild>
|
2024-05-10 19:54:37 +03:00
|
|
|
<div
|
2024-05-14 18:06:44 +03:00
|
|
|
className="cursor-pointer rounded-lg bg-secondary text-secondary-foreground opacity-60 transition-all duration-300 hover:bg-muted hover:opacity-100"
|
2024-05-10 00:08:22 +03:00
|
|
|
onClick={() =>
|
|
|
|
|
setIsEditMode((prevIsEditMode) => !prevIsEditMode)
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
{isEditMode ? (
|
2024-05-10 19:54:37 +03:00
|
|
|
<IoClose className="size-5 md:m-[6px]" />
|
2024-05-10 00:08:22 +03:00
|
|
|
) : (
|
2024-05-10 19:54:37 +03:00
|
|
|
<LuLayoutDashboard className="size-5 md:m-[6px]" />
|
2024-05-10 00:08:22 +03:00
|
|
|
)}
|
2024-05-10 19:54:37 +03:00
|
|
|
</div>
|
2024-05-10 00:08:22 +03:00
|
|
|
</TooltipTrigger>
|
|
|
|
|
<TooltipContent>
|
2025-03-16 18:36:20 +03:00
|
|
|
{isEditMode
|
|
|
|
|
? t("editLayout.exitEdit")
|
|
|
|
|
: t("editLayout.label")}
|
2024-05-10 00:08:22 +03:00
|
|
|
</TooltipContent>
|
|
|
|
|
</Tooltip>
|
|
|
|
|
{!isEditMode && (
|
|
|
|
|
<>
|
2024-05-31 15:58:33 +03:00
|
|
|
{!fullscreen && (
|
|
|
|
|
<Tooltip>
|
|
|
|
|
<TooltipTrigger asChild>
|
|
|
|
|
<div
|
|
|
|
|
className="cursor-pointer rounded-lg bg-secondary text-secondary-foreground opacity-60 transition-all duration-300 hover:bg-muted hover:opacity-100"
|
|
|
|
|
onClick={() =>
|
|
|
|
|
setEditGroup((prevEditGroup) => !prevEditGroup)
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
<LuPencil className="size-5 md:m-[6px]" />
|
|
|
|
|
</div>
|
|
|
|
|
</TooltipTrigger>
|
|
|
|
|
<TooltipContent>
|
2025-03-16 18:36:20 +03:00
|
|
|
{isEditMode
|
|
|
|
|
? t("editLayout.exitEdit")
|
|
|
|
|
: t("editLayout.group.label")}
|
2024-05-31 15:58:33 +03:00
|
|
|
</TooltipContent>
|
|
|
|
|
</Tooltip>
|
|
|
|
|
)}
|
2024-05-10 00:08:22 +03:00
|
|
|
<Tooltip>
|
|
|
|
|
<TooltipTrigger asChild>
|
2024-05-10 19:54:37 +03:00
|
|
|
<div
|
2024-05-14 18:06:44 +03:00
|
|
|
className="cursor-pointer rounded-lg bg-secondary text-secondary-foreground opacity-60 transition-all duration-300 hover:bg-muted hover:opacity-100"
|
2024-05-10 19:54:37 +03:00
|
|
|
onClick={toggleFullscreen}
|
2024-05-10 00:08:22 +03:00
|
|
|
>
|
|
|
|
|
{fullscreen ? (
|
2024-05-10 19:54:37 +03:00
|
|
|
<FaCompress className="size-5 md:m-[6px]" />
|
2024-05-10 00:08:22 +03:00
|
|
|
) : (
|
2024-05-10 19:54:37 +03:00
|
|
|
<FaExpand className="size-5 md:m-[6px]" />
|
2024-05-10 00:08:22 +03:00
|
|
|
)}
|
2024-05-10 19:54:37 +03:00
|
|
|
</div>
|
2024-05-10 00:08:22 +03:00
|
|
|
</TooltipTrigger>
|
|
|
|
|
<TooltipContent>
|
2025-03-16 18:36:20 +03:00
|
|
|
{fullscreen
|
|
|
|
|
? t("button.exitFullscreen", { ns: "common" })
|
|
|
|
|
: t("button.fullscreen", { ns: "common" })}
|
2024-05-10 00:08:22 +03:00
|
|
|
</TooltipContent>
|
|
|
|
|
</Tooltip>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
2024-05-08 15:53:22 +03:00
|
|
|
)}
|
2024-05-07 17:28:10 +03:00
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-08 15:53:22 +03:00
|
|
|
function CornerCircles() {
|
|
|
|
|
return (
|
|
|
|
|
<>
|
2024-05-14 18:06:44 +03:00
|
|
|
<div className="pointer-events-none absolute left-[-4px] top-[-4px] z-50 size-3 rounded-full bg-primary-variant p-2 text-background outline-2 outline-muted" />
|
|
|
|
|
<div className="pointer-events-none absolute right-[-4px] top-[-4px] z-50 size-3 rounded-full bg-primary-variant p-2 text-background outline-2 outline-muted" />
|
|
|
|
|
<div className="pointer-events-none absolute bottom-[-4px] right-[-4px] z-50 size-3 rounded-full bg-primary-variant p-2 text-background outline-2 outline-muted" />
|
|
|
|
|
<div className="pointer-events-none absolute bottom-[-4px] left-[-4px] z-50 size-3 rounded-full bg-primary-variant p-2 text-background outline-2 outline-muted" />
|
2024-05-08 15:53:22 +03:00
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-07 17:28:10 +03:00
|
|
|
type BirdseyeLivePlayerGridItemProps = {
|
|
|
|
|
style?: React.CSSProperties;
|
|
|
|
|
className?: string;
|
|
|
|
|
onMouseDown?: React.MouseEventHandler<HTMLDivElement>;
|
|
|
|
|
onMouseUp?: React.MouseEventHandler<HTMLDivElement>;
|
|
|
|
|
onTouchEnd?: React.TouchEventHandler<HTMLDivElement>;
|
|
|
|
|
children?: React.ReactNode;
|
|
|
|
|
birdseyeConfig: BirdseyeConfig;
|
|
|
|
|
liveMode: LivePlayerMode;
|
|
|
|
|
onClick: () => void;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const BirdseyeLivePlayerGridItem = React.forwardRef<
|
|
|
|
|
HTMLDivElement,
|
|
|
|
|
BirdseyeLivePlayerGridItemProps
|
|
|
|
|
>(
|
|
|
|
|
(
|
|
|
|
|
{
|
|
|
|
|
style,
|
|
|
|
|
className,
|
|
|
|
|
onMouseDown,
|
|
|
|
|
onMouseUp,
|
|
|
|
|
onTouchEnd,
|
|
|
|
|
children,
|
|
|
|
|
birdseyeConfig,
|
|
|
|
|
liveMode,
|
|
|
|
|
onClick,
|
|
|
|
|
...props
|
|
|
|
|
},
|
|
|
|
|
ref,
|
|
|
|
|
) => {
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
style={{ ...style }}
|
|
|
|
|
ref={ref}
|
|
|
|
|
onMouseDown={onMouseDown}
|
|
|
|
|
onMouseUp={onMouseUp}
|
|
|
|
|
onTouchEnd={onTouchEnd}
|
|
|
|
|
{...props}
|
|
|
|
|
>
|
|
|
|
|
<BirdseyeLivePlayer
|
|
|
|
|
className={className}
|
|
|
|
|
birdseyeConfig={birdseyeConfig}
|
|
|
|
|
liveMode={liveMode}
|
|
|
|
|
onClick={onClick}
|
2024-05-29 21:05:28 +03:00
|
|
|
containerRef={ref as React.RefObject<HTMLDivElement>}
|
2024-05-07 17:28:10 +03:00
|
|
|
/>
|
|
|
|
|
{children}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
2025-02-10 19:42:35 +03:00
|
|
|
type GridLiveContextMenuProps = {
|
|
|
|
|
className?: string;
|
2024-05-07 17:28:10 +03:00
|
|
|
style?: React.CSSProperties;
|
|
|
|
|
onMouseDown?: React.MouseEventHandler<HTMLDivElement>;
|
|
|
|
|
onMouseUp?: React.MouseEventHandler<HTMLDivElement>;
|
|
|
|
|
onTouchEnd?: React.TouchEventHandler<HTMLDivElement>;
|
|
|
|
|
children?: React.ReactNode;
|
2025-02-10 19:42:35 +03:00
|
|
|
camera: string;
|
|
|
|
|
streamName: string;
|
|
|
|
|
cameraGroup: string;
|
|
|
|
|
preferredLiveMode: string;
|
|
|
|
|
isRestreamed: boolean;
|
|
|
|
|
supportsAudio: boolean;
|
|
|
|
|
audioState: boolean;
|
|
|
|
|
toggleAudio: () => void;
|
|
|
|
|
statsState: boolean;
|
|
|
|
|
toggleStats: () => void;
|
|
|
|
|
volumeState?: number;
|
|
|
|
|
setVolumeState: (volumeState: number) => void;
|
|
|
|
|
muteAll: () => void;
|
|
|
|
|
unmuteAll: () => void;
|
|
|
|
|
resetPreferredLiveMode: () => void;
|
2025-02-17 17:19:03 +03:00
|
|
|
config?: FrigateConfig;
|
2024-05-07 17:28:10 +03:00
|
|
|
};
|
|
|
|
|
|
2025-02-10 19:42:35 +03:00
|
|
|
const GridLiveContextMenu = React.forwardRef<
|
2024-05-07 17:28:10 +03:00
|
|
|
HTMLDivElement,
|
2025-02-10 19:42:35 +03:00
|
|
|
GridLiveContextMenuProps
|
2024-05-07 17:28:10 +03:00
|
|
|
>(
|
|
|
|
|
(
|
|
|
|
|
{
|
|
|
|
|
className,
|
2025-02-10 19:42:35 +03:00
|
|
|
style,
|
2024-05-07 17:28:10 +03:00
|
|
|
onMouseDown,
|
|
|
|
|
onMouseUp,
|
|
|
|
|
onTouchEnd,
|
|
|
|
|
children,
|
2025-02-10 19:42:35 +03:00
|
|
|
camera,
|
|
|
|
|
streamName,
|
|
|
|
|
cameraGroup,
|
2024-05-07 17:28:10 +03:00
|
|
|
preferredLiveMode,
|
2025-02-10 19:42:35 +03:00
|
|
|
isRestreamed,
|
|
|
|
|
supportsAudio,
|
|
|
|
|
audioState,
|
|
|
|
|
toggleAudio,
|
|
|
|
|
statsState,
|
|
|
|
|
toggleStats,
|
|
|
|
|
volumeState,
|
|
|
|
|
setVolumeState,
|
|
|
|
|
muteAll,
|
|
|
|
|
unmuteAll,
|
|
|
|
|
resetPreferredLiveMode,
|
2025-02-17 17:19:03 +03:00
|
|
|
config,
|
2024-05-07 17:28:10 +03:00
|
|
|
...props
|
|
|
|
|
},
|
|
|
|
|
ref,
|
|
|
|
|
) => {
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
style={{ ...style }}
|
|
|
|
|
ref={ref}
|
|
|
|
|
onMouseDown={onMouseDown}
|
|
|
|
|
onMouseUp={onMouseUp}
|
|
|
|
|
onTouchEnd={onTouchEnd}
|
|
|
|
|
{...props}
|
|
|
|
|
>
|
2025-02-10 19:42:35 +03:00
|
|
|
<LiveContextMenu
|
2024-05-07 17:28:10 +03:00
|
|
|
className={className}
|
2025-02-10 19:42:35 +03:00
|
|
|
camera={camera}
|
|
|
|
|
streamName={streamName}
|
|
|
|
|
cameraGroup={cameraGroup}
|
2024-05-07 17:28:10 +03:00
|
|
|
preferredLiveMode={preferredLiveMode}
|
2025-02-10 19:42:35 +03:00
|
|
|
isRestreamed={isRestreamed}
|
|
|
|
|
supportsAudio={supportsAudio}
|
|
|
|
|
audioState={audioState}
|
|
|
|
|
toggleAudio={toggleAudio}
|
|
|
|
|
statsState={statsState}
|
|
|
|
|
toggleStats={toggleStats}
|
|
|
|
|
volumeState={volumeState}
|
|
|
|
|
setVolumeState={setVolumeState}
|
|
|
|
|
muteAll={muteAll}
|
|
|
|
|
unmuteAll={unmuteAll}
|
|
|
|
|
resetPreferredLiveMode={resetPreferredLiveMode}
|
2025-02-17 17:19:03 +03:00
|
|
|
config={config}
|
2025-02-10 19:42:35 +03:00
|
|
|
>
|
|
|
|
|
{children}
|
|
|
|
|
</LiveContextMenu>
|
2024-05-07 17:28:10 +03:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
);
|