streaming dialog from context menu

This commit is contained in:
Josh Hawkins 2024-12-26 07:51:16 -06:00
parent 826dbc4da3
commit 95047308ba
9 changed files with 543 additions and 350 deletions

View File

@ -9,7 +9,7 @@ import useSWR from "swr";
import { MdHome } from "react-icons/md";
import { usePersistedOverlayState } from "@/hooks/use-overlay-state";
import { Button, buttonVariants } from "../ui/button";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useCallback, useMemo, useState } from "react";
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
import { LuPencil, LuPlus } from "react-icons/lu";
import {
@ -73,17 +73,13 @@ import {
import { Label } from "../ui/label";
import { Switch } from "../ui/switch";
import { CameraStreamingDialog } from "../settings/CameraStreamingDialog";
import { DialogTrigger } from "@radix-ui/react-dialog";
import { useStreamingSettings } from "@/context/streaming-settings-provider";
type CameraGroupSelectorProps = {
className?: string;
setAllGroupsStreamingSettings: React.Dispatch<
React.SetStateAction<AllGroupsStreamingSettings>
>;
};
export function CameraGroupSelector({
className,
setAllGroupsStreamingSettings,
}: CameraGroupSelectorProps) {
export function CameraGroupSelector({ className }: CameraGroupSelectorProps) {
const { data: config } = useSWR<FrigateConfig>("config");
// tooltip
@ -137,7 +133,6 @@ export function CameraGroupSelector({
activeGroup={group}
setGroup={setGroup}
deleteGroup={deleteGroup}
setAllGroupsStreamingSettings={setAllGroupsStreamingSettings}
/>
<Scroller className={`${isMobile ? "whitespace-nowrap" : ""}`}>
<div
@ -227,9 +222,6 @@ type NewGroupDialogProps = {
activeGroup?: string;
setGroup: (value: string | undefined, replace?: boolean | undefined) => void;
deleteGroup: () => void;
setAllGroupsStreamingSettings: React.Dispatch<
React.SetStateAction<AllGroupsStreamingSettings>
>;
};
function NewGroupDialog({
open,
@ -238,7 +230,6 @@ function NewGroupDialog({
activeGroup,
setGroup,
deleteGroup,
setAllGroupsStreamingSettings,
}: NewGroupDialogProps) {
const { mutate: updateConfig } = useSWR<FrigateConfig>("config");
@ -421,7 +412,6 @@ function NewGroupDialog({
setIsLoading={setIsLoading}
onSave={onSave}
onCancel={onCancel}
setAllGroupsStreamingSettings={setAllGroupsStreamingSettings}
/>
</>
)}
@ -436,16 +426,12 @@ type EditGroupDialogProps = {
setOpen: (open: boolean) => void;
currentGroups: [string, CameraGroupConfig][];
activeGroup?: string;
setAllGroupsStreamingSettings: React.Dispatch<
React.SetStateAction<AllGroupsStreamingSettings>
>;
};
export function EditGroupDialog({
open,
setOpen,
currentGroups,
activeGroup,
setAllGroupsStreamingSettings,
}: EditGroupDialogProps) {
const Overlay = isDesktop ? Dialog : MobilePage;
const Content = isDesktop ? DialogContent : MobilePageContent;
@ -497,7 +483,6 @@ export function EditGroupDialog({
setIsLoading={setIsLoading}
onSave={() => setOpen(false)}
onCancel={() => setOpen(false)}
setAllGroupsStreamingSettings={setAllGroupsStreamingSettings}
/>
</div>
</Content>
@ -618,9 +603,6 @@ type CameraGroupEditProps = {
setIsLoading: React.Dispatch<React.SetStateAction<boolean>>;
onSave?: () => void;
onCancel?: () => void;
setAllGroupsStreamingSettings: React.Dispatch<
React.SetStateAction<AllGroupsStreamingSettings>
>;
};
export function CameraGroupEdit({
@ -630,16 +612,19 @@ export function CameraGroupEdit({
setIsLoading,
onSave,
onCancel,
setAllGroupsStreamingSettings,
}: CameraGroupEditProps) {
const { data: config, mutate: updateConfig } =
useSWR<FrigateConfig>("config");
const [groupStreamingSettings, setGroupStreamingSettings] =
useState<GroupStreamingSettings>({});
const { allGroupsStreamingSettings, setAllGroupsStreamingSettings } =
useStreamingSettings();
const [persistedGroupStreamingSettings, setPersistedGroupStreamingSettings] =
usePersistence<AllGroupsStreamingSettings>("streaming-settings");
const [groupStreamingSettings, setGroupStreamingSettings] =
useState<GroupStreamingSettings>(
allGroupsStreamingSettings[editingGroup?.[0] ?? ""],
);
const [openCamera, setOpenCamera] = useState<string | null>();
const birdseyeConfig = useMemo(() => config?.birdseye, [config]);
@ -693,7 +678,7 @@ export function CameraGroupEdit({
// update streaming settings
const updatedSettings: AllGroupsStreamingSettings = {
...Object.fromEntries(
Object.entries(persistedGroupStreamingSettings || {}).filter(
Object.entries(allGroupsStreamingSettings || {}).filter(
([key]) => key !== editingGroup?.[0],
),
),
@ -732,7 +717,6 @@ export function CameraGroupEdit({
if (onSave) {
onSave();
}
await setPersistedGroupStreamingSettings(updatedSettings);
setAllGroupsStreamingSettings(updatedSettings);
} else {
toast.error(`Failed to save config changes: ${res.statusText}`, {
@ -757,8 +741,7 @@ export function CameraGroupEdit({
updateConfig,
editingGroup,
groupStreamingSettings,
setPersistedGroupStreamingSettings,
persistedGroupStreamingSettings,
allGroupsStreamingSettings,
setAllGroupsStreamingSettings,
],
);
@ -773,20 +756,6 @@ export function CameraGroupEdit({
},
});
// streaming settings
useEffect(() => {
if (editingGroup && editingGroup[0] && persistedGroupStreamingSettings) {
setGroupStreamingSettings(
persistedGroupStreamingSettings[editingGroup[0]] || {},
);
}
}, [
editingGroup,
persistedGroupStreamingSettings,
setGroupStreamingSettings,
]);
return (
<Form {...form}>
<form
@ -838,15 +807,43 @@ export function CameraGroupEdit({
<div className="flex items-center gap-x-2">
{camera !== "birdseye" && (
<Dialog
open={openCamera === camera}
onOpenChange={(isOpen) =>
setOpenCamera(isOpen ? camera : null)
}
>
<DialogTrigger asChild>
<Button
className="flex h-auto items-center gap-1"
aria-label="Camera streaming settings"
size="icon"
variant="ghost"
disabled={
!(field.value && field.value.includes(camera))
}
>
<LuIcons.LuSettings
className={cn(
field.value && field.value.includes(camera)
? "text-primary"
: "text-muted-foreground",
"size-5",
)}
/>
</Button>
</DialogTrigger>
<CameraStreamingDialog
camera={camera}
selectedCameras={field.value}
config={config}
groupStreamingSettings={groupStreamingSettings}
setGroupStreamingSettings={
setGroupStreamingSettings
}
setIsDialogOpen={(isOpen) =>
setOpenCamera(isOpen ? camera : null)
}
/>
</Dialog>
)}
<Switch
id={camera.replaceAll("_", " ")}

View File

@ -1,4 +1,11 @@
import { ReactNode, useMemo } from "react";
import {
ReactNode,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import {
ContextMenu,
ContextMenuContent,
@ -12,10 +19,19 @@ import {
MdVolumeOff,
MdVolumeUp,
} from "react-icons/md";
import { Dialog } from "@/components/ui/dialog";
import { VolumeSlider } from "@/components/ui/slider";
import { CameraStreamingDialog } from "../settings/CameraStreamingDialog";
import {
AllGroupsStreamingSettings,
GroupStreamingSettings,
} from "@/types/frigateConfig";
import { useStreamingSettings } from "@/context/streaming-settings-provider";
type LiveContextMenuProps = {
camera: string;
streamName: string;
cameraGroup?: string;
preferredLiveMode: string;
isRestreamed: boolean;
supportsAudio: boolean;
@ -30,6 +46,8 @@ type LiveContextMenuProps = {
};
export default function LiveContextMenu({
camera,
streamName,
cameraGroup,
preferredLiveMode,
isRestreamed,
supportsAudio,
@ -42,6 +60,83 @@ export default function LiveContextMenu({
resetPreferredLiveMode,
children,
}: LiveContextMenuProps) {
const [showSettings, setShowSettings] = useState(false);
// streaming settings
const { allGroupsStreamingSettings, setAllGroupsStreamingSettings } =
useStreamingSettings();
const [groupStreamingSettings, setGroupStreamingSettings] =
useState<GroupStreamingSettings>(
allGroupsStreamingSettings[cameraGroup ?? ""],
);
useEffect(() => {
if (cameraGroup) {
setGroupStreamingSettings(allGroupsStreamingSettings[cameraGroup]);
}
// set individual group when all groups changes
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [allGroupsStreamingSettings]);
const onSave = useCallback(
(settings: GroupStreamingSettings) => {
if (!cameraGroup || !allGroupsStreamingSettings) {
return;
}
const updatedSettings: AllGroupsStreamingSettings = {
...Object.fromEntries(
Object.entries(allGroupsStreamingSettings || {}).filter(
([key]) => key !== cameraGroup,
),
),
[cameraGroup]: {
...Object.fromEntries(
Object.entries(settings).map(([cameraName, cameraSettings]) => [
cameraName,
cameraName === camera
? {
...cameraSettings,
playAudio: audioState ?? cameraSettings.playAudio ?? false,
volume: volumeState ?? cameraSettings.volume ?? 1,
}
: cameraSettings,
]),
),
// Add the current camera if it doesn't exist
...(!settings[camera]
? {
[camera]: {
streamName: streamName,
streamType: "smart",
compatibilityMode: false,
playAudio: audioState,
volume: volumeState ?? 1,
},
}
: {}),
},
};
setAllGroupsStreamingSettings?.(updatedSettings);
},
[
camera,
streamName,
cameraGroup,
allGroupsStreamingSettings,
setAllGroupsStreamingSettings,
audioState,
volumeState,
],
);
// ui
const audioControlsUsed = useRef(false);
const VolumeIcon = useMemo(() => {
if (!volumeState || volumeState == 0.0 || !audioState) {
return MdVolumeOff;
@ -56,8 +151,27 @@ export default function LiveContextMenu({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [volumeState, audioState]);
const handleVolumeIconClick = (e: React.MouseEvent) => {
e.stopPropagation();
audioControlsUsed.current = true;
toggleAudio();
};
const handleVolumeChange = (value: number[]) => {
audioControlsUsed.current = true;
setVolumeState(value[0]);
};
const handleOpenChange = (open: boolean) => {
if (!open && audioControlsUsed.current) {
onSave(groupStreamingSettings);
audioControlsUsed.current = false;
}
};
return (
<ContextMenu key={camera}>
<>
<ContextMenu key={camera} onOpenChange={handleOpenChange}>
<ContextMenuTrigger>{children}</ContextMenuTrigger>
<ContextMenuContent>
<div className="text-md py-1 pl-2 capitalize text-primary-variant">
@ -72,10 +186,7 @@ export default function LiveContextMenu({
<div className="flex flex-row items-center gap-1">
<VolumeIcon
className="size-5"
onClick={(e: React.MouseEvent) => {
e.stopPropagation();
toggleAudio();
}}
onClick={handleVolumeIconClick}
/>
<VolumeSlider
disabled={!audioState}
@ -84,9 +195,7 @@ export default function LiveContextMenu({
min={0}
max={1}
step={0.02}
onValueChange={(value) => {
setVolumeState(value[0]);
}}
onValueChange={handleVolumeChange}
/>
</div>
</div>
@ -110,13 +219,13 @@ export default function LiveContextMenu({
<div className="text-primary">Unmute All Cameras</div>
</div>
</ContextMenuItem>
{isRestreamed && (
{isRestreamed && cameraGroup && (
<>
<ContextMenuSeparator />
<ContextMenuItem>
<div
className="flex w-full cursor-pointer items-center justify-start gap-2"
onClick={() => {}}
onClick={() => setShowSettings(true)}
>
<div className="text-primary">Streaming Settings</div>
</div>
@ -138,5 +247,16 @@ export default function LiveContextMenu({
)}
</ContextMenuContent>
</ContextMenu>
<Dialog open={showSettings} onOpenChange={setShowSettings}>
<CameraStreamingDialog
camera={camera}
groupStreamingSettings={groupStreamingSettings}
setGroupStreamingSettings={setGroupStreamingSettings}
setIsDialogOpen={setShowSettings}
onSave={onSave}
/>
</Dialog>
</>
);
}

View File

@ -6,9 +6,7 @@ import GeneralSettings from "../menu/GeneralSettings";
import AccountSettings from "../menu/AccountSettings";
import useNavigation from "@/hooks/use-navigation";
import { baseUrl } from "@/api/baseUrl";
import { useEffect, useMemo, useState } from "react";
import { usePersistence } from "@/hooks/use-persistence";
import { AllGroupsStreamingSettings } from "@/types/frigateConfig";
import { useMemo } from "react";
function Sidebar() {
const basePath = useMemo(() => new URL(baseUrl).pathname, []);
@ -18,18 +16,6 @@ function Sidebar() {
const navbarLinks = useNavigation();
const [, setAllGroupsStreamingSettings] =
useState<AllGroupsStreamingSettings>({});
const [persistedStreamingSettings, _, isStreamingSettingsLoaded] =
usePersistence<AllGroupsStreamingSettings>("streaming-settings");
useEffect(() => {
if (isStreamingSettingsLoaded) {
setAllGroupsStreamingSettings(persistedStreamingSettings ?? {});
}
}, [isStreamingSettingsLoaded, persistedStreamingSettings]);
return (
<aside className="scrollbar-container scrollbar-hidden absolute inset-y-0 left-0 z-10 flex w-[52px] flex-col justify-between overflow-y-auto border-r border-secondary-highlight bg-background_alt py-4">
<span tabIndex={0} className="sr-only" />
@ -48,12 +34,7 @@ function Sidebar() {
item={item}
Icon={item.icon}
/>
{showCameraGroups && (
<CameraGroupSelector
setAllGroupsStreamingSettings={setAllGroupsStreamingSettings}
className="mb-4"
/>
)}
{showCameraGroups && <CameraGroupSelector className="mb-4" />}
</div>
);
})}

View File

@ -2,11 +2,9 @@ import { useState, useCallback, useEffect } from "react";
import { IoIosWarning } from "react-icons/io";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
DialogDescription,
} from "@/components/ui/dialog";
import {
@ -16,35 +14,48 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Checkbox } from "@/components/ui/checkbox";
import { Label } from "@/components/ui/label";
import { cn } from "@/lib/utils";
import {
FrigateConfig,
GroupStreamingSettings,
StreamType,
} from "@/types/frigateConfig";
import ActivityIndicator from "../indicators/activity-indicator";
import { LuSettings } from "react-icons/lu";
import useSWR from "swr";
import useCameraLiveMode from "@/hooks/use-camera-live-mode";
import { LuCheck, LuExternalLink, LuInfo, LuX } from "react-icons/lu";
import { Link } from "react-router-dom";
type CameraStreamingDialogProps = {
camera: string;
selectedCameras: string[];
config?: FrigateConfig;
groupStreamingSettings: GroupStreamingSettings;
setGroupStreamingSettings: React.Dispatch<
React.SetStateAction<GroupStreamingSettings>
>;
setIsDialogOpen: React.Dispatch<React.SetStateAction<boolean>>;
onSave?: (settings: GroupStreamingSettings) => void;
};
export function CameraStreamingDialog({
camera,
selectedCameras,
config,
groupStreamingSettings,
setGroupStreamingSettings,
setIsDialogOpen,
onSave,
}: CameraStreamingDialogProps) {
const [isDialogOpen, setIsDialogOpen] = useState(false);
const { data: config } = useSWR<FrigateConfig>("config");
const { supportsAudioOutputStates } = useCameraLiveMode(
config?.cameras[camera] ? [config.cameras[camera]] : [],
false,
);
const [isLoading, setIsLoading] = useState(false);
const [streamName, setStreamName] = useState(
@ -74,18 +85,30 @@ export function CameraStreamingDialog({
const handleSave = useCallback(() => {
setIsLoading(true);
setGroupStreamingSettings((prevSettings) => ({
...prevSettings,
[camera]: { streamName, streamType, compatibilityMode },
}));
const updatedSettings = {
...groupStreamingSettings,
[camera]: {
streamName,
streamType,
compatibilityMode,
playAudio: groupStreamingSettings[camera]?.playAudio ?? false,
volume: groupStreamingSettings[camera]?.volume ?? 1,
},
};
setGroupStreamingSettings(updatedSettings);
setIsDialogOpen(false);
setIsLoading(false);
onSave?.(updatedSettings);
}, [
groupStreamingSettings,
setGroupStreamingSettings,
camera,
streamName,
streamType,
compatibilityMode,
setIsDialogOpen,
onSave,
]);
const handleCancel = useCallback(() => {
@ -106,32 +129,13 @@ export function CameraStreamingDialog({
setCompatibilityMode(false);
}
setIsDialogOpen(false);
}, [groupStreamingSettings, camera, config]);
}, [groupStreamingSettings, camera, config, setIsDialogOpen]);
if (!config) {
return null;
}
return (
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogTrigger asChild>
<Button
className="flex h-auto items-center gap-1"
aria-label="Camera streaming settings"
size="icon"
variant="ghost"
disabled={!(selectedCameras && selectedCameras.includes(camera))}
>
<LuSettings
className={cn(
selectedCameras && selectedCameras.includes(camera)
? "text-primary"
: "text-muted-foreground",
"size-5",
)}
/>
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader className="mb-4">
<DialogTitle className="capitalize">
@ -162,6 +166,43 @@ export function CameraStreamingDialog({
),
)}
</SelectContent>
<div className="flex flex-row items-center gap-1 text-sm text-muted-foreground">
{supportsAudioOutputStates[streamName] &&
supportsAudioOutputStates[streamName].supportsAudio ? (
<>
<LuCheck className="size-4 text-success" />
<div>Audio is available for this stream</div>
</>
) : (
<>
<LuX className="size-4 text-danger" />
<div>Audio is unavailable for this stream</div>
<Popover>
<PopoverTrigger asChild>
<div className="cursor-pointer p-0">
<LuInfo className="size-4" />
<span className="sr-only">Info</span>
</div>
</PopoverTrigger>
<PopoverContent className="w-80">
Audio must be output from your camera and configured in
go2rtc for this stream.
<div className="mt-2 flex items-center text-primary">
<Link
to="https://docs.frigate.video/configuration/live"
target="_blank"
rel="noopener noreferrer"
className="inline"
>
Read the documentation{" "}
<LuExternalLink className="ml-2 inline-flex size-3" />
</Link>
</div>
</PopoverContent>
</Popover>
</>
)}
</div>
</Select>
</div>
)}
@ -192,9 +233,9 @@ export function CameraStreamingDialog({
)}
{streamType === "smart" && (
<p className="text-sm text-muted-foreground">
Smart streaming will update your camera image once per minute
when no detectable activity is occurring to conserve bandwidth
and resources. When activity is detected, the image seamlessly
Smart streaming will update your camera image once per minute when
no detectable activity is occurring to conserve bandwidth and
resources. When activity is detected, the image seamlessly
switches to a live stream.
</p>
)}
@ -231,9 +272,9 @@ export function CameraStreamingDialog({
</div>
<div className="flex flex-col gap-2 leading-none">
<p className="text-sm text-muted-foreground">
Enable this option only if your camera's live stream is
displaying color artifacts and has a diagonal line on the right
side of the image.
Enable this option only if your camera's live stream is displaying
color artifacts and has a diagonal line on the right side of the
image.
</p>
</div>
</div>
@ -266,6 +307,5 @@ export function CameraStreamingDialog({
</div>
</div>
</DialogContent>
</Dialog>
);
}

View File

@ -5,6 +5,7 @@ import { ApiProvider } from "@/api";
import { IconContext } from "react-icons";
import { TooltipProvider } from "@/components/ui/tooltip";
import { StatusBarMessagesProvider } from "@/context/statusbar-provider";
import { StreamingSettingsProvider } from "./streaming-settings-provider";
type TProvidersProps = {
children: ReactNode;
@ -17,7 +18,11 @@ function providers({ children }: TProvidersProps) {
<ThemeProvider defaultTheme="system" storageKey="frigate-ui-theme">
<TooltipProvider>
<IconContext.Provider value={{ size: "20" }}>
<StatusBarMessagesProvider>{children}</StatusBarMessagesProvider>
<StatusBarMessagesProvider>
<StreamingSettingsProvider>
{children}
</StreamingSettingsProvider>
</StatusBarMessagesProvider>
</IconContext.Provider>
</TooltipProvider>
</ThemeProvider>

View File

@ -235,6 +235,8 @@ export type CameraStreamingSettings = {
streamName: string;
streamType: StreamType;
compatibilityMode: boolean;
playAudio: boolean;
volume: number;
};
export type GroupStreamingSettings = {

View File

@ -32,3 +32,6 @@ export type LiveStreamMetadata = {
};
export type LivePlayerError = "stalled" | "startup" | "mse-decode";
export type AudioState = Record<string, boolean>;
export type VolumeState = Record<string, number>;

View File

@ -21,7 +21,7 @@ import {
} from "react-grid-layout";
import "react-grid-layout/css/styles.css";
import "react-resizable/css/styles.css";
import { LivePlayerMode } from "@/types/live";
import { AudioState, LivePlayerMode, VolumeState } from "@/types/live";
import { ASPECT_VERTICAL_LAYOUT, ASPECT_WIDE_LAYOUT } from "@/types/record";
import { Skeleton } from "@/components/ui/skeleton";
import { useResizeObserver } from "@/hooks/resize-observer";
@ -44,6 +44,7 @@ import {
import { Toaster } from "@/components/ui/sonner";
import useCameraLiveMode from "@/hooks/use-camera-live-mode";
import LiveContextMenu from "@/components/menu/LiveContextMenu";
import { useStreamingSettings } from "@/context/streaming-settings-provider";
type DraggableGridLayoutProps = {
cameras: CameraConfig[];
@ -58,10 +59,6 @@ type DraggableGridLayoutProps = {
setIsEditMode: React.Dispatch<React.SetStateAction<boolean>>;
fullscreen: boolean;
toggleFullscreen: () => void;
allGroupsStreamingSettings: AllGroupsStreamingSettings;
setAllGroupsStreamingSettings: React.Dispatch<
React.SetStateAction<AllGroupsStreamingSettings>
>;
};
export default function DraggableGridLayout({
cameras,
@ -76,8 +73,6 @@ export default function DraggableGridLayout({
setIsEditMode,
fullscreen,
toggleFullscreen,
allGroupsStreamingSettings,
setAllGroupsStreamingSettings,
}: DraggableGridLayoutProps) {
const { data: config } = useSWR<FrigateConfig>("config");
const birdseyeConfig = useMemo(() => config?.birdseye, [config]);
@ -94,6 +89,9 @@ export default function DraggableGridLayout({
const [globalAutoLive] = usePersistence("autoLiveView", true);
const { allGroupsStreamingSettings, setAllGroupsStreamingSettings } =
useStreamingSettings();
const currentGroupStreamingSettings = useMemo(() => {
if (cameraGroup && cameraGroup != "default" && allGroupsStreamingSettings) {
return allGroupsStreamingSettings[cameraGroup];
@ -367,30 +365,87 @@ export default function DraggableGridLayout({
// audio states
const [audioStates, setAudioStates] = useState<Record<string, boolean>>({});
const [volumeStates, setVolumeStates] = useState<Record<string, number>>({});
const [audioStates, setAudioStates] = useState<AudioState>({});
const [volumeStates, setVolumeStates] = useState<VolumeState>({});
const toggleAudio = (cameraName: string): void => {
useEffect(() => {
if (!allGroupsStreamingSettings) {
return;
}
const initialAudioStates: AudioState = {};
const initialVolumeStates: VolumeState = {};
Object.entries(allGroupsStreamingSettings).forEach(([_, groupSettings]) => {
Object.entries(groupSettings).forEach(([camera, cameraSettings]) => {
initialAudioStates[camera] = cameraSettings.playAudio ?? false;
initialVolumeStates[camera] = cameraSettings.volume ?? 1;
});
});
setAudioStates(initialAudioStates);
setVolumeStates(initialVolumeStates);
}, [allGroupsStreamingSettings]);
const toggleAudio = (cameraName: string) => {
setAudioStates((prev) => ({
...prev,
[cameraName]: !prev[cameraName],
}));
};
const muteAll = (): void => {
const updatedStates: Record<string, boolean> = {};
visibleCameras.forEach((cameraName) => {
updatedStates[cameraName] = false;
});
setAudioStates(updatedStates);
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,
},
],
),
),
},
};
const unmuteAll = (): void => {
const updatedStates: Record<string, boolean> = {};
visibleCameras.forEach((cameraName) => {
updatedStates[cameraName] = true;
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);
};
return (
@ -423,7 +478,6 @@ export default function DraggableGridLayout({
setOpen={setEditGroup}
currentGroups={groups}
activeGroup={group}
setAllGroupsStreamingSettings={setAllGroupsStreamingSettings}
/>
<ResponsiveGridLayout
className="grid-layout"
@ -488,6 +542,8 @@ export default function DraggableGridLayout({
<GridLiveContextMenu
key={camera.name}
camera={camera.name}
streamName={streamName}
cameraGroup={cameraGroup}
preferredLiveMode={preferredLiveModes[camera.name] ?? "mse"}
isRestreamed={isRestreamedStates[camera.name]}
supportsAudio={
@ -542,6 +598,8 @@ export default function DraggableGridLayout({
});
}}
onResetLiveMode={() => resetPreferredLiveMode(camera.name)}
playAudio={audioStates[camera.name]}
volume={volumeStates[camera.name]}
/>
{isEditMode && showCircles && <CornerCircles />}
</GridLiveContextMenu>
@ -694,6 +752,8 @@ type GridLiveContextMenuProps = {
onTouchEnd?: React.TouchEventHandler<HTMLDivElement>;
children?: React.ReactNode;
camera: string;
streamName: string;
cameraGroup: string;
preferredLiveMode: string;
isRestreamed: boolean;
supportsAudio: boolean;
@ -718,6 +778,8 @@ const GridLiveContextMenu = React.forwardRef<
onTouchEnd,
children,
camera,
streamName,
cameraGroup,
preferredLiveMode,
isRestreamed,
supportsAudio,
@ -743,6 +805,8 @@ const GridLiveContextMenu = React.forwardRef<
>
<LiveContextMenu
camera={camera}
streamName={streamName}
cameraGroup={cameraGroup}
preferredLiveMode={preferredLiveMode}
isRestreamed={isRestreamed}
supportsAudio={supportsAudio}

View File

@ -14,11 +14,7 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip";
import { usePersistence } from "@/hooks/use-persistence";
import {
AllGroupsStreamingSettings,
CameraConfig,
FrigateConfig,
} from "@/types/frigateConfig";
import { CameraConfig, FrigateConfig } from "@/types/frigateConfig";
import { ReviewSegment } from "@/types/review";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
@ -197,18 +193,6 @@ export default function LiveDashboardView({
supportsAudioOutputStates,
} = useCameraLiveMode(cameras, windowVisible);
const [allGroupsStreamingSettings, setAllGroupsStreamingSettings] =
useState<AllGroupsStreamingSettings>({});
const [persistedStreamingSettings, _, isStreamingSettingsLoaded] =
usePersistence<AllGroupsStreamingSettings>("streaming-settings");
useEffect(() => {
if (isStreamingSettingsLoaded) {
setAllGroupsStreamingSettings(persistedStreamingSettings ?? {});
}
}, [isStreamingSettingsLoaded, persistedStreamingSettings]);
const cameraRef = useCallback(
(node: HTMLElement | null) => {
if (!visibleCameraObserver.current) {
@ -280,9 +264,7 @@ export default function LiveDashboardView({
<div className="relative flex h-11 items-center justify-between">
<Logo className="absolute inset-x-1/2 h-8 -translate-x-1/2" />
<div className="max-w-[45%]">
<CameraGroupSelector
setAllGroupsStreamingSettings={setAllGroupsStreamingSettings}
/>
<CameraGroupSelector />
</div>
{(!cameraGroup || cameraGroup == "default" || isMobileOnly) && (
<div className="flex items-center gap-1">
@ -401,6 +383,7 @@ export default function LiveDashboardView({
<LiveContextMenu
key={camera.name}
camera={camera.name}
streamName={Object.values(camera.live.streams)?.[0]}
preferredLiveMode={preferredLiveModes[camera.name] ?? "mse"}
isRestreamed={isRestreamedStates[camera.name]}
supportsAudio={
@ -489,8 +472,6 @@ export default function LiveDashboardView({
setIsEditMode={setIsEditMode}
fullscreen={fullscreen}
toggleFullscreen={toggleFullscreen}
allGroupsStreamingSettings={allGroupsStreamingSettings}
setAllGroupsStreamingSettings={setAllGroupsStreamingSettings}
/>
)}
</div>