mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-19 01:17:06 +03:00
streaming dialog from context menu
This commit is contained in:
parent
826dbc4da3
commit
95047308ba
@ -9,7 +9,7 @@ import useSWR from "swr";
|
|||||||
import { MdHome } from "react-icons/md";
|
import { MdHome } from "react-icons/md";
|
||||||
import { usePersistedOverlayState } from "@/hooks/use-overlay-state";
|
import { usePersistedOverlayState } from "@/hooks/use-overlay-state";
|
||||||
import { Button, buttonVariants } from "../ui/button";
|
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 { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
|
||||||
import { LuPencil, LuPlus } from "react-icons/lu";
|
import { LuPencil, LuPlus } from "react-icons/lu";
|
||||||
import {
|
import {
|
||||||
@ -73,17 +73,13 @@ import {
|
|||||||
import { Label } from "../ui/label";
|
import { Label } from "../ui/label";
|
||||||
import { Switch } from "../ui/switch";
|
import { Switch } from "../ui/switch";
|
||||||
import { CameraStreamingDialog } from "../settings/CameraStreamingDialog";
|
import { CameraStreamingDialog } from "../settings/CameraStreamingDialog";
|
||||||
|
import { DialogTrigger } from "@radix-ui/react-dialog";
|
||||||
|
import { useStreamingSettings } from "@/context/streaming-settings-provider";
|
||||||
|
|
||||||
type CameraGroupSelectorProps = {
|
type CameraGroupSelectorProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
setAllGroupsStreamingSettings: React.Dispatch<
|
|
||||||
React.SetStateAction<AllGroupsStreamingSettings>
|
|
||||||
>;
|
|
||||||
};
|
};
|
||||||
export function CameraGroupSelector({
|
export function CameraGroupSelector({ className }: CameraGroupSelectorProps) {
|
||||||
className,
|
|
||||||
setAllGroupsStreamingSettings,
|
|
||||||
}: CameraGroupSelectorProps) {
|
|
||||||
const { data: config } = useSWR<FrigateConfig>("config");
|
const { data: config } = useSWR<FrigateConfig>("config");
|
||||||
|
|
||||||
// tooltip
|
// tooltip
|
||||||
@ -137,7 +133,6 @@ export function CameraGroupSelector({
|
|||||||
activeGroup={group}
|
activeGroup={group}
|
||||||
setGroup={setGroup}
|
setGroup={setGroup}
|
||||||
deleteGroup={deleteGroup}
|
deleteGroup={deleteGroup}
|
||||||
setAllGroupsStreamingSettings={setAllGroupsStreamingSettings}
|
|
||||||
/>
|
/>
|
||||||
<Scroller className={`${isMobile ? "whitespace-nowrap" : ""}`}>
|
<Scroller className={`${isMobile ? "whitespace-nowrap" : ""}`}>
|
||||||
<div
|
<div
|
||||||
@ -227,9 +222,6 @@ type NewGroupDialogProps = {
|
|||||||
activeGroup?: string;
|
activeGroup?: string;
|
||||||
setGroup: (value: string | undefined, replace?: boolean | undefined) => void;
|
setGroup: (value: string | undefined, replace?: boolean | undefined) => void;
|
||||||
deleteGroup: () => void;
|
deleteGroup: () => void;
|
||||||
setAllGroupsStreamingSettings: React.Dispatch<
|
|
||||||
React.SetStateAction<AllGroupsStreamingSettings>
|
|
||||||
>;
|
|
||||||
};
|
};
|
||||||
function NewGroupDialog({
|
function NewGroupDialog({
|
||||||
open,
|
open,
|
||||||
@ -238,7 +230,6 @@ function NewGroupDialog({
|
|||||||
activeGroup,
|
activeGroup,
|
||||||
setGroup,
|
setGroup,
|
||||||
deleteGroup,
|
deleteGroup,
|
||||||
setAllGroupsStreamingSettings,
|
|
||||||
}: NewGroupDialogProps) {
|
}: NewGroupDialogProps) {
|
||||||
const { mutate: updateConfig } = useSWR<FrigateConfig>("config");
|
const { mutate: updateConfig } = useSWR<FrigateConfig>("config");
|
||||||
|
|
||||||
@ -421,7 +412,6 @@ function NewGroupDialog({
|
|||||||
setIsLoading={setIsLoading}
|
setIsLoading={setIsLoading}
|
||||||
onSave={onSave}
|
onSave={onSave}
|
||||||
onCancel={onCancel}
|
onCancel={onCancel}
|
||||||
setAllGroupsStreamingSettings={setAllGroupsStreamingSettings}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@ -436,16 +426,12 @@ type EditGroupDialogProps = {
|
|||||||
setOpen: (open: boolean) => void;
|
setOpen: (open: boolean) => void;
|
||||||
currentGroups: [string, CameraGroupConfig][];
|
currentGroups: [string, CameraGroupConfig][];
|
||||||
activeGroup?: string;
|
activeGroup?: string;
|
||||||
setAllGroupsStreamingSettings: React.Dispatch<
|
|
||||||
React.SetStateAction<AllGroupsStreamingSettings>
|
|
||||||
>;
|
|
||||||
};
|
};
|
||||||
export function EditGroupDialog({
|
export function EditGroupDialog({
|
||||||
open,
|
open,
|
||||||
setOpen,
|
setOpen,
|
||||||
currentGroups,
|
currentGroups,
|
||||||
activeGroup,
|
activeGroup,
|
||||||
setAllGroupsStreamingSettings,
|
|
||||||
}: EditGroupDialogProps) {
|
}: EditGroupDialogProps) {
|
||||||
const Overlay = isDesktop ? Dialog : MobilePage;
|
const Overlay = isDesktop ? Dialog : MobilePage;
|
||||||
const Content = isDesktop ? DialogContent : MobilePageContent;
|
const Content = isDesktop ? DialogContent : MobilePageContent;
|
||||||
@ -497,7 +483,6 @@ export function EditGroupDialog({
|
|||||||
setIsLoading={setIsLoading}
|
setIsLoading={setIsLoading}
|
||||||
onSave={() => setOpen(false)}
|
onSave={() => setOpen(false)}
|
||||||
onCancel={() => setOpen(false)}
|
onCancel={() => setOpen(false)}
|
||||||
setAllGroupsStreamingSettings={setAllGroupsStreamingSettings}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Content>
|
</Content>
|
||||||
@ -618,9 +603,6 @@ type CameraGroupEditProps = {
|
|||||||
setIsLoading: React.Dispatch<React.SetStateAction<boolean>>;
|
setIsLoading: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
onSave?: () => void;
|
onSave?: () => void;
|
||||||
onCancel?: () => void;
|
onCancel?: () => void;
|
||||||
setAllGroupsStreamingSettings: React.Dispatch<
|
|
||||||
React.SetStateAction<AllGroupsStreamingSettings>
|
|
||||||
>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function CameraGroupEdit({
|
export function CameraGroupEdit({
|
||||||
@ -630,16 +612,19 @@ export function CameraGroupEdit({
|
|||||||
setIsLoading,
|
setIsLoading,
|
||||||
onSave,
|
onSave,
|
||||||
onCancel,
|
onCancel,
|
||||||
setAllGroupsStreamingSettings,
|
|
||||||
}: CameraGroupEditProps) {
|
}: CameraGroupEditProps) {
|
||||||
const { data: config, mutate: updateConfig } =
|
const { data: config, mutate: updateConfig } =
|
||||||
useSWR<FrigateConfig>("config");
|
useSWR<FrigateConfig>("config");
|
||||||
|
|
||||||
const [groupStreamingSettings, setGroupStreamingSettings] =
|
const { allGroupsStreamingSettings, setAllGroupsStreamingSettings } =
|
||||||
useState<GroupStreamingSettings>({});
|
useStreamingSettings();
|
||||||
|
|
||||||
const [persistedGroupStreamingSettings, setPersistedGroupStreamingSettings] =
|
const [groupStreamingSettings, setGroupStreamingSettings] =
|
||||||
usePersistence<AllGroupsStreamingSettings>("streaming-settings");
|
useState<GroupStreamingSettings>(
|
||||||
|
allGroupsStreamingSettings[editingGroup?.[0] ?? ""],
|
||||||
|
);
|
||||||
|
|
||||||
|
const [openCamera, setOpenCamera] = useState<string | null>();
|
||||||
|
|
||||||
const birdseyeConfig = useMemo(() => config?.birdseye, [config]);
|
const birdseyeConfig = useMemo(() => config?.birdseye, [config]);
|
||||||
|
|
||||||
@ -693,7 +678,7 @@ export function CameraGroupEdit({
|
|||||||
// update streaming settings
|
// update streaming settings
|
||||||
const updatedSettings: AllGroupsStreamingSettings = {
|
const updatedSettings: AllGroupsStreamingSettings = {
|
||||||
...Object.fromEntries(
|
...Object.fromEntries(
|
||||||
Object.entries(persistedGroupStreamingSettings || {}).filter(
|
Object.entries(allGroupsStreamingSettings || {}).filter(
|
||||||
([key]) => key !== editingGroup?.[0],
|
([key]) => key !== editingGroup?.[0],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -732,7 +717,6 @@ export function CameraGroupEdit({
|
|||||||
if (onSave) {
|
if (onSave) {
|
||||||
onSave();
|
onSave();
|
||||||
}
|
}
|
||||||
await setPersistedGroupStreamingSettings(updatedSettings);
|
|
||||||
setAllGroupsStreamingSettings(updatedSettings);
|
setAllGroupsStreamingSettings(updatedSettings);
|
||||||
} else {
|
} else {
|
||||||
toast.error(`Failed to save config changes: ${res.statusText}`, {
|
toast.error(`Failed to save config changes: ${res.statusText}`, {
|
||||||
@ -757,8 +741,7 @@ export function CameraGroupEdit({
|
|||||||
updateConfig,
|
updateConfig,
|
||||||
editingGroup,
|
editingGroup,
|
||||||
groupStreamingSettings,
|
groupStreamingSettings,
|
||||||
setPersistedGroupStreamingSettings,
|
allGroupsStreamingSettings,
|
||||||
persistedGroupStreamingSettings,
|
|
||||||
setAllGroupsStreamingSettings,
|
setAllGroupsStreamingSettings,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@ -773,20 +756,6 @@ export function CameraGroupEdit({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// streaming settings
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (editingGroup && editingGroup[0] && persistedGroupStreamingSettings) {
|
|
||||||
setGroupStreamingSettings(
|
|
||||||
persistedGroupStreamingSettings[editingGroup[0]] || {},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
editingGroup,
|
|
||||||
persistedGroupStreamingSettings,
|
|
||||||
setGroupStreamingSettings,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
@ -838,15 +807,43 @@ export function CameraGroupEdit({
|
|||||||
|
|
||||||
<div className="flex items-center gap-x-2">
|
<div className="flex items-center gap-x-2">
|
||||||
{camera !== "birdseye" && (
|
{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
|
<CameraStreamingDialog
|
||||||
camera={camera}
|
camera={camera}
|
||||||
selectedCameras={field.value}
|
|
||||||
config={config}
|
|
||||||
groupStreamingSettings={groupStreamingSettings}
|
groupStreamingSettings={groupStreamingSettings}
|
||||||
setGroupStreamingSettings={
|
setGroupStreamingSettings={
|
||||||
setGroupStreamingSettings
|
setGroupStreamingSettings
|
||||||
}
|
}
|
||||||
|
setIsDialogOpen={(isOpen) =>
|
||||||
|
setOpenCamera(isOpen ? camera : null)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
</Dialog>
|
||||||
)}
|
)}
|
||||||
<Switch
|
<Switch
|
||||||
id={camera.replaceAll("_", " ")}
|
id={camera.replaceAll("_", " ")}
|
||||||
|
|||||||
@ -1,4 +1,11 @@
|
|||||||
import { ReactNode, useMemo } from "react";
|
import {
|
||||||
|
ReactNode,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
import {
|
import {
|
||||||
ContextMenu,
|
ContextMenu,
|
||||||
ContextMenuContent,
|
ContextMenuContent,
|
||||||
@ -12,10 +19,19 @@ import {
|
|||||||
MdVolumeOff,
|
MdVolumeOff,
|
||||||
MdVolumeUp,
|
MdVolumeUp,
|
||||||
} from "react-icons/md";
|
} from "react-icons/md";
|
||||||
|
import { Dialog } from "@/components/ui/dialog";
|
||||||
import { VolumeSlider } from "@/components/ui/slider";
|
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 = {
|
type LiveContextMenuProps = {
|
||||||
camera: string;
|
camera: string;
|
||||||
|
streamName: string;
|
||||||
|
cameraGroup?: string;
|
||||||
preferredLiveMode: string;
|
preferredLiveMode: string;
|
||||||
isRestreamed: boolean;
|
isRestreamed: boolean;
|
||||||
supportsAudio: boolean;
|
supportsAudio: boolean;
|
||||||
@ -30,6 +46,8 @@ type LiveContextMenuProps = {
|
|||||||
};
|
};
|
||||||
export default function LiveContextMenu({
|
export default function LiveContextMenu({
|
||||||
camera,
|
camera,
|
||||||
|
streamName,
|
||||||
|
cameraGroup,
|
||||||
preferredLiveMode,
|
preferredLiveMode,
|
||||||
isRestreamed,
|
isRestreamed,
|
||||||
supportsAudio,
|
supportsAudio,
|
||||||
@ -42,6 +60,83 @@ export default function LiveContextMenu({
|
|||||||
resetPreferredLiveMode,
|
resetPreferredLiveMode,
|
||||||
children,
|
children,
|
||||||
}: LiveContextMenuProps) {
|
}: 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(() => {
|
const VolumeIcon = useMemo(() => {
|
||||||
if (!volumeState || volumeState == 0.0 || !audioState) {
|
if (!volumeState || volumeState == 0.0 || !audioState) {
|
||||||
return MdVolumeOff;
|
return MdVolumeOff;
|
||||||
@ -56,8 +151,27 @@ export default function LiveContextMenu({
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [volumeState, audioState]);
|
}, [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 (
|
return (
|
||||||
<ContextMenu key={camera}>
|
<>
|
||||||
|
<ContextMenu key={camera} onOpenChange={handleOpenChange}>
|
||||||
<ContextMenuTrigger>{children}</ContextMenuTrigger>
|
<ContextMenuTrigger>{children}</ContextMenuTrigger>
|
||||||
<ContextMenuContent>
|
<ContextMenuContent>
|
||||||
<div className="text-md py-1 pl-2 capitalize text-primary-variant">
|
<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">
|
<div className="flex flex-row items-center gap-1">
|
||||||
<VolumeIcon
|
<VolumeIcon
|
||||||
className="size-5"
|
className="size-5"
|
||||||
onClick={(e: React.MouseEvent) => {
|
onClick={handleVolumeIconClick}
|
||||||
e.stopPropagation();
|
|
||||||
toggleAudio();
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<VolumeSlider
|
<VolumeSlider
|
||||||
disabled={!audioState}
|
disabled={!audioState}
|
||||||
@ -84,9 +195,7 @@ export default function LiveContextMenu({
|
|||||||
min={0}
|
min={0}
|
||||||
max={1}
|
max={1}
|
||||||
step={0.02}
|
step={0.02}
|
||||||
onValueChange={(value) => {
|
onValueChange={handleVolumeChange}
|
||||||
setVolumeState(value[0]);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -110,13 +219,13 @@ export default function LiveContextMenu({
|
|||||||
<div className="text-primary">Unmute All Cameras</div>
|
<div className="text-primary">Unmute All Cameras</div>
|
||||||
</div>
|
</div>
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
{isRestreamed && (
|
{isRestreamed && cameraGroup && (
|
||||||
<>
|
<>
|
||||||
<ContextMenuSeparator />
|
<ContextMenuSeparator />
|
||||||
<ContextMenuItem>
|
<ContextMenuItem>
|
||||||
<div
|
<div
|
||||||
className="flex w-full cursor-pointer items-center justify-start gap-2"
|
className="flex w-full cursor-pointer items-center justify-start gap-2"
|
||||||
onClick={() => {}}
|
onClick={() => setShowSettings(true)}
|
||||||
>
|
>
|
||||||
<div className="text-primary">Streaming Settings</div>
|
<div className="text-primary">Streaming Settings</div>
|
||||||
</div>
|
</div>
|
||||||
@ -138,5 +247,16 @@ export default function LiveContextMenu({
|
|||||||
)}
|
)}
|
||||||
</ContextMenuContent>
|
</ContextMenuContent>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
|
|
||||||
|
<Dialog open={showSettings} onOpenChange={setShowSettings}>
|
||||||
|
<CameraStreamingDialog
|
||||||
|
camera={camera}
|
||||||
|
groupStreamingSettings={groupStreamingSettings}
|
||||||
|
setGroupStreamingSettings={setGroupStreamingSettings}
|
||||||
|
setIsDialogOpen={setShowSettings}
|
||||||
|
onSave={onSave}
|
||||||
|
/>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,9 +6,7 @@ import GeneralSettings from "../menu/GeneralSettings";
|
|||||||
import AccountSettings from "../menu/AccountSettings";
|
import AccountSettings from "../menu/AccountSettings";
|
||||||
import useNavigation from "@/hooks/use-navigation";
|
import useNavigation from "@/hooks/use-navigation";
|
||||||
import { baseUrl } from "@/api/baseUrl";
|
import { baseUrl } from "@/api/baseUrl";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useMemo } from "react";
|
||||||
import { usePersistence } from "@/hooks/use-persistence";
|
|
||||||
import { AllGroupsStreamingSettings } from "@/types/frigateConfig";
|
|
||||||
|
|
||||||
function Sidebar() {
|
function Sidebar() {
|
||||||
const basePath = useMemo(() => new URL(baseUrl).pathname, []);
|
const basePath = useMemo(() => new URL(baseUrl).pathname, []);
|
||||||
@ -18,18 +16,6 @@ function Sidebar() {
|
|||||||
|
|
||||||
const navbarLinks = useNavigation();
|
const navbarLinks = useNavigation();
|
||||||
|
|
||||||
const [, setAllGroupsStreamingSettings] =
|
|
||||||
useState<AllGroupsStreamingSettings>({});
|
|
||||||
|
|
||||||
const [persistedStreamingSettings, _, isStreamingSettingsLoaded] =
|
|
||||||
usePersistence<AllGroupsStreamingSettings>("streaming-settings");
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isStreamingSettingsLoaded) {
|
|
||||||
setAllGroupsStreamingSettings(persistedStreamingSettings ?? {});
|
|
||||||
}
|
|
||||||
}, [isStreamingSettingsLoaded, persistedStreamingSettings]);
|
|
||||||
|
|
||||||
return (
|
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">
|
<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" />
|
<span tabIndex={0} className="sr-only" />
|
||||||
@ -48,12 +34,7 @@ function Sidebar() {
|
|||||||
item={item}
|
item={item}
|
||||||
Icon={item.icon}
|
Icon={item.icon}
|
||||||
/>
|
/>
|
||||||
{showCameraGroups && (
|
{showCameraGroups && <CameraGroupSelector className="mb-4" />}
|
||||||
<CameraGroupSelector
|
|
||||||
setAllGroupsStreamingSettings={setAllGroupsStreamingSettings}
|
|
||||||
className="mb-4"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@ -2,11 +2,9 @@ import { useState, useCallback, useEffect } from "react";
|
|||||||
import { IoIosWarning } from "react-icons/io";
|
import { IoIosWarning } from "react-icons/io";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import {
|
import {
|
||||||
@ -16,35 +14,48 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "@/components/ui/popover";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import {
|
import {
|
||||||
FrigateConfig,
|
FrigateConfig,
|
||||||
GroupStreamingSettings,
|
GroupStreamingSettings,
|
||||||
StreamType,
|
StreamType,
|
||||||
} from "@/types/frigateConfig";
|
} from "@/types/frigateConfig";
|
||||||
import ActivityIndicator from "../indicators/activity-indicator";
|
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 = {
|
type CameraStreamingDialogProps = {
|
||||||
camera: string;
|
camera: string;
|
||||||
selectedCameras: string[];
|
|
||||||
config?: FrigateConfig;
|
|
||||||
groupStreamingSettings: GroupStreamingSettings;
|
groupStreamingSettings: GroupStreamingSettings;
|
||||||
setGroupStreamingSettings: React.Dispatch<
|
setGroupStreamingSettings: React.Dispatch<
|
||||||
React.SetStateAction<GroupStreamingSettings>
|
React.SetStateAction<GroupStreamingSettings>
|
||||||
>;
|
>;
|
||||||
|
setIsDialogOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
onSave?: (settings: GroupStreamingSettings) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function CameraStreamingDialog({
|
export function CameraStreamingDialog({
|
||||||
camera,
|
camera,
|
||||||
selectedCameras,
|
|
||||||
config,
|
|
||||||
groupStreamingSettings,
|
groupStreamingSettings,
|
||||||
setGroupStreamingSettings,
|
setGroupStreamingSettings,
|
||||||
|
setIsDialogOpen,
|
||||||
|
onSave,
|
||||||
}: CameraStreamingDialogProps) {
|
}: 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 [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
const [streamName, setStreamName] = useState(
|
const [streamName, setStreamName] = useState(
|
||||||
@ -74,18 +85,30 @@ export function CameraStreamingDialog({
|
|||||||
|
|
||||||
const handleSave = useCallback(() => {
|
const handleSave = useCallback(() => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setGroupStreamingSettings((prevSettings) => ({
|
const updatedSettings = {
|
||||||
...prevSettings,
|
...groupStreamingSettings,
|
||||||
[camera]: { streamName, streamType, compatibilityMode },
|
[camera]: {
|
||||||
}));
|
streamName,
|
||||||
|
streamType,
|
||||||
|
compatibilityMode,
|
||||||
|
playAudio: groupStreamingSettings[camera]?.playAudio ?? false,
|
||||||
|
volume: groupStreamingSettings[camera]?.volume ?? 1,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
setGroupStreamingSettings(updatedSettings);
|
||||||
setIsDialogOpen(false);
|
setIsDialogOpen(false);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
|
onSave?.(updatedSettings);
|
||||||
}, [
|
}, [
|
||||||
|
groupStreamingSettings,
|
||||||
setGroupStreamingSettings,
|
setGroupStreamingSettings,
|
||||||
camera,
|
camera,
|
||||||
streamName,
|
streamName,
|
||||||
streamType,
|
streamType,
|
||||||
compatibilityMode,
|
compatibilityMode,
|
||||||
|
setIsDialogOpen,
|
||||||
|
onSave,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const handleCancel = useCallback(() => {
|
const handleCancel = useCallback(() => {
|
||||||
@ -106,32 +129,13 @@ export function CameraStreamingDialog({
|
|||||||
setCompatibilityMode(false);
|
setCompatibilityMode(false);
|
||||||
}
|
}
|
||||||
setIsDialogOpen(false);
|
setIsDialogOpen(false);
|
||||||
}, [groupStreamingSettings, camera, config]);
|
}, [groupStreamingSettings, camera, config, setIsDialogOpen]);
|
||||||
|
|
||||||
if (!config) {
|
if (!config) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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]">
|
<DialogContent className="sm:max-w-[425px]">
|
||||||
<DialogHeader className="mb-4">
|
<DialogHeader className="mb-4">
|
||||||
<DialogTitle className="capitalize">
|
<DialogTitle className="capitalize">
|
||||||
@ -162,6 +166,43 @@ export function CameraStreamingDialog({
|
|||||||
),
|
),
|
||||||
)}
|
)}
|
||||||
</SelectContent>
|
</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>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -192,9 +233,9 @@ export function CameraStreamingDialog({
|
|||||||
)}
|
)}
|
||||||
{streamType === "smart" && (
|
{streamType === "smart" && (
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
Smart streaming will update your camera image once per minute
|
Smart streaming will update your camera image once per minute when
|
||||||
when no detectable activity is occurring to conserve bandwidth
|
no detectable activity is occurring to conserve bandwidth and
|
||||||
and resources. When activity is detected, the image seamlessly
|
resources. When activity is detected, the image seamlessly
|
||||||
switches to a live stream.
|
switches to a live stream.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@ -231,9 +272,9 @@ export function CameraStreamingDialog({
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2 leading-none">
|
<div className="flex flex-col gap-2 leading-none">
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
Enable this option only if your camera's live stream is
|
Enable this option only if your camera's live stream is displaying
|
||||||
displaying color artifacts and has a diagonal line on the right
|
color artifacts and has a diagonal line on the right side of the
|
||||||
side of the image.
|
image.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -266,6 +307,5 @@ export function CameraStreamingDialog({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { ApiProvider } from "@/api";
|
|||||||
import { IconContext } from "react-icons";
|
import { IconContext } from "react-icons";
|
||||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||||
import { StatusBarMessagesProvider } from "@/context/statusbar-provider";
|
import { StatusBarMessagesProvider } from "@/context/statusbar-provider";
|
||||||
|
import { StreamingSettingsProvider } from "./streaming-settings-provider";
|
||||||
|
|
||||||
type TProvidersProps = {
|
type TProvidersProps = {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
@ -17,7 +18,11 @@ function providers({ children }: TProvidersProps) {
|
|||||||
<ThemeProvider defaultTheme="system" storageKey="frigate-ui-theme">
|
<ThemeProvider defaultTheme="system" storageKey="frigate-ui-theme">
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<IconContext.Provider value={{ size: "20" }}>
|
<IconContext.Provider value={{ size: "20" }}>
|
||||||
<StatusBarMessagesProvider>{children}</StatusBarMessagesProvider>
|
<StatusBarMessagesProvider>
|
||||||
|
<StreamingSettingsProvider>
|
||||||
|
{children}
|
||||||
|
</StreamingSettingsProvider>
|
||||||
|
</StatusBarMessagesProvider>
|
||||||
</IconContext.Provider>
|
</IconContext.Provider>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
|
|||||||
@ -235,6 +235,8 @@ export type CameraStreamingSettings = {
|
|||||||
streamName: string;
|
streamName: string;
|
||||||
streamType: StreamType;
|
streamType: StreamType;
|
||||||
compatibilityMode: boolean;
|
compatibilityMode: boolean;
|
||||||
|
playAudio: boolean;
|
||||||
|
volume: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GroupStreamingSettings = {
|
export type GroupStreamingSettings = {
|
||||||
|
|||||||
@ -32,3 +32,6 @@ export type LiveStreamMetadata = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type LivePlayerError = "stalled" | "startup" | "mse-decode";
|
export type LivePlayerError = "stalled" | "startup" | "mse-decode";
|
||||||
|
|
||||||
|
export type AudioState = Record<string, boolean>;
|
||||||
|
export type VolumeState = Record<string, number>;
|
||||||
|
|||||||
@ -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 { LivePlayerMode } from "@/types/live";
|
import { AudioState, LivePlayerMode, VolumeState } 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";
|
||||||
@ -44,6 +44,7 @@ import {
|
|||||||
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";
|
import LiveContextMenu from "@/components/menu/LiveContextMenu";
|
||||||
|
import { useStreamingSettings } from "@/context/streaming-settings-provider";
|
||||||
|
|
||||||
type DraggableGridLayoutProps = {
|
type DraggableGridLayoutProps = {
|
||||||
cameras: CameraConfig[];
|
cameras: CameraConfig[];
|
||||||
@ -58,10 +59,6 @@ type DraggableGridLayoutProps = {
|
|||||||
setIsEditMode: React.Dispatch<React.SetStateAction<boolean>>;
|
setIsEditMode: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
fullscreen: boolean;
|
fullscreen: boolean;
|
||||||
toggleFullscreen: () => void;
|
toggleFullscreen: () => void;
|
||||||
allGroupsStreamingSettings: AllGroupsStreamingSettings;
|
|
||||||
setAllGroupsStreamingSettings: React.Dispatch<
|
|
||||||
React.SetStateAction<AllGroupsStreamingSettings>
|
|
||||||
>;
|
|
||||||
};
|
};
|
||||||
export default function DraggableGridLayout({
|
export default function DraggableGridLayout({
|
||||||
cameras,
|
cameras,
|
||||||
@ -76,8 +73,6 @@ export default function DraggableGridLayout({
|
|||||||
setIsEditMode,
|
setIsEditMode,
|
||||||
fullscreen,
|
fullscreen,
|
||||||
toggleFullscreen,
|
toggleFullscreen,
|
||||||
allGroupsStreamingSettings,
|
|
||||||
setAllGroupsStreamingSettings,
|
|
||||||
}: DraggableGridLayoutProps) {
|
}: DraggableGridLayoutProps) {
|
||||||
const { data: config } = useSWR<FrigateConfig>("config");
|
const { data: config } = useSWR<FrigateConfig>("config");
|
||||||
const birdseyeConfig = useMemo(() => config?.birdseye, [config]);
|
const birdseyeConfig = useMemo(() => config?.birdseye, [config]);
|
||||||
@ -94,6 +89,9 @@ export default function DraggableGridLayout({
|
|||||||
|
|
||||||
const [globalAutoLive] = usePersistence("autoLiveView", true);
|
const [globalAutoLive] = usePersistence("autoLiveView", true);
|
||||||
|
|
||||||
|
const { allGroupsStreamingSettings, setAllGroupsStreamingSettings } =
|
||||||
|
useStreamingSettings();
|
||||||
|
|
||||||
const currentGroupStreamingSettings = useMemo(() => {
|
const currentGroupStreamingSettings = useMemo(() => {
|
||||||
if (cameraGroup && cameraGroup != "default" && allGroupsStreamingSettings) {
|
if (cameraGroup && cameraGroup != "default" && allGroupsStreamingSettings) {
|
||||||
return allGroupsStreamingSettings[cameraGroup];
|
return allGroupsStreamingSettings[cameraGroup];
|
||||||
@ -367,30 +365,87 @@ export default function DraggableGridLayout({
|
|||||||
|
|
||||||
// audio states
|
// audio states
|
||||||
|
|
||||||
const [audioStates, setAudioStates] = useState<Record<string, boolean>>({});
|
const [audioStates, setAudioStates] = useState<AudioState>({});
|
||||||
const [volumeStates, setVolumeStates] = useState<Record<string, number>>({});
|
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) => ({
|
setAudioStates((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[cameraName]: !prev[cameraName],
|
[cameraName]: !prev[cameraName],
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const muteAll = (): void => {
|
const onSaveMuting = useCallback(
|
||||||
const updatedStates: Record<string, boolean> = {};
|
(playAudio: boolean) => {
|
||||||
visibleCameras.forEach((cameraName) => {
|
if (!cameraGroup || !allGroupsStreamingSettings) {
|
||||||
updatedStates[cameraName] = false;
|
return;
|
||||||
});
|
}
|
||||||
setAudioStates(updatedStates);
|
|
||||||
|
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 => {
|
setAllGroupsStreamingSettings?.(updatedSettings);
|
||||||
const updatedStates: Record<string, boolean> = {};
|
},
|
||||||
visibleCameras.forEach((cameraName) => {
|
[cameraGroup, allGroupsStreamingSettings, setAllGroupsStreamingSettings],
|
||||||
updatedStates[cameraName] = true;
|
);
|
||||||
|
|
||||||
|
const muteAll = () => {
|
||||||
|
const updatedStates: AudioState = {};
|
||||||
|
cameras.forEach((camera) => {
|
||||||
|
updatedStates[camera.name] = false;
|
||||||
});
|
});
|
||||||
setAudioStates(updatedStates);
|
setAudioStates(updatedStates);
|
||||||
|
onSaveMuting(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const unmuteAll = () => {
|
||||||
|
const updatedStates: AudioState = {};
|
||||||
|
cameras.forEach((camera) => {
|
||||||
|
updatedStates[camera.name] = true;
|
||||||
|
});
|
||||||
|
setAudioStates(updatedStates);
|
||||||
|
onSaveMuting(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -423,7 +478,6 @@ export default function DraggableGridLayout({
|
|||||||
setOpen={setEditGroup}
|
setOpen={setEditGroup}
|
||||||
currentGroups={groups}
|
currentGroups={groups}
|
||||||
activeGroup={group}
|
activeGroup={group}
|
||||||
setAllGroupsStreamingSettings={setAllGroupsStreamingSettings}
|
|
||||||
/>
|
/>
|
||||||
<ResponsiveGridLayout
|
<ResponsiveGridLayout
|
||||||
className="grid-layout"
|
className="grid-layout"
|
||||||
@ -488,6 +542,8 @@ export default function DraggableGridLayout({
|
|||||||
<GridLiveContextMenu
|
<GridLiveContextMenu
|
||||||
key={camera.name}
|
key={camera.name}
|
||||||
camera={camera.name}
|
camera={camera.name}
|
||||||
|
streamName={streamName}
|
||||||
|
cameraGroup={cameraGroup}
|
||||||
preferredLiveMode={preferredLiveModes[camera.name] ?? "mse"}
|
preferredLiveMode={preferredLiveModes[camera.name] ?? "mse"}
|
||||||
isRestreamed={isRestreamedStates[camera.name]}
|
isRestreamed={isRestreamedStates[camera.name]}
|
||||||
supportsAudio={
|
supportsAudio={
|
||||||
@ -542,6 +598,8 @@ export default function DraggableGridLayout({
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
onResetLiveMode={() => resetPreferredLiveMode(camera.name)}
|
onResetLiveMode={() => resetPreferredLiveMode(camera.name)}
|
||||||
|
playAudio={audioStates[camera.name]}
|
||||||
|
volume={volumeStates[camera.name]}
|
||||||
/>
|
/>
|
||||||
{isEditMode && showCircles && <CornerCircles />}
|
{isEditMode && showCircles && <CornerCircles />}
|
||||||
</GridLiveContextMenu>
|
</GridLiveContextMenu>
|
||||||
@ -694,6 +752,8 @@ type GridLiveContextMenuProps = {
|
|||||||
onTouchEnd?: React.TouchEventHandler<HTMLDivElement>;
|
onTouchEnd?: React.TouchEventHandler<HTMLDivElement>;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
camera: string;
|
camera: string;
|
||||||
|
streamName: string;
|
||||||
|
cameraGroup: string;
|
||||||
preferredLiveMode: string;
|
preferredLiveMode: string;
|
||||||
isRestreamed: boolean;
|
isRestreamed: boolean;
|
||||||
supportsAudio: boolean;
|
supportsAudio: boolean;
|
||||||
@ -718,6 +778,8 @@ const GridLiveContextMenu = React.forwardRef<
|
|||||||
onTouchEnd,
|
onTouchEnd,
|
||||||
children,
|
children,
|
||||||
camera,
|
camera,
|
||||||
|
streamName,
|
||||||
|
cameraGroup,
|
||||||
preferredLiveMode,
|
preferredLiveMode,
|
||||||
isRestreamed,
|
isRestreamed,
|
||||||
supportsAudio,
|
supportsAudio,
|
||||||
@ -743,6 +805,8 @@ const GridLiveContextMenu = React.forwardRef<
|
|||||||
>
|
>
|
||||||
<LiveContextMenu
|
<LiveContextMenu
|
||||||
camera={camera}
|
camera={camera}
|
||||||
|
streamName={streamName}
|
||||||
|
cameraGroup={cameraGroup}
|
||||||
preferredLiveMode={preferredLiveMode}
|
preferredLiveMode={preferredLiveMode}
|
||||||
isRestreamed={isRestreamed}
|
isRestreamed={isRestreamed}
|
||||||
supportsAudio={supportsAudio}
|
supportsAudio={supportsAudio}
|
||||||
|
|||||||
@ -14,11 +14,7 @@ import {
|
|||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { usePersistence } from "@/hooks/use-persistence";
|
import { usePersistence } from "@/hooks/use-persistence";
|
||||||
import {
|
import { CameraConfig, FrigateConfig } from "@/types/frigateConfig";
|
||||||
AllGroupsStreamingSettings,
|
|
||||||
CameraConfig,
|
|
||||||
FrigateConfig,
|
|
||||||
} from "@/types/frigateConfig";
|
|
||||||
import { ReviewSegment } from "@/types/review";
|
import { ReviewSegment } from "@/types/review";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
@ -197,18 +193,6 @@ export default function LiveDashboardView({
|
|||||||
supportsAudioOutputStates,
|
supportsAudioOutputStates,
|
||||||
} = useCameraLiveMode(cameras, windowVisible);
|
} = 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(
|
const cameraRef = useCallback(
|
||||||
(node: HTMLElement | null) => {
|
(node: HTMLElement | null) => {
|
||||||
if (!visibleCameraObserver.current) {
|
if (!visibleCameraObserver.current) {
|
||||||
@ -280,9 +264,7 @@ export default function LiveDashboardView({
|
|||||||
<div className="relative flex h-11 items-center justify-between">
|
<div className="relative flex h-11 items-center justify-between">
|
||||||
<Logo className="absolute inset-x-1/2 h-8 -translate-x-1/2" />
|
<Logo className="absolute inset-x-1/2 h-8 -translate-x-1/2" />
|
||||||
<div className="max-w-[45%]">
|
<div className="max-w-[45%]">
|
||||||
<CameraGroupSelector
|
<CameraGroupSelector />
|
||||||
setAllGroupsStreamingSettings={setAllGroupsStreamingSettings}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
{(!cameraGroup || cameraGroup == "default" || isMobileOnly) && (
|
{(!cameraGroup || cameraGroup == "default" || isMobileOnly) && (
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
@ -401,6 +383,7 @@ export default function LiveDashboardView({
|
|||||||
<LiveContextMenu
|
<LiveContextMenu
|
||||||
key={camera.name}
|
key={camera.name}
|
||||||
camera={camera.name}
|
camera={camera.name}
|
||||||
|
streamName={Object.values(camera.live.streams)?.[0]}
|
||||||
preferredLiveMode={preferredLiveModes[camera.name] ?? "mse"}
|
preferredLiveMode={preferredLiveModes[camera.name] ?? "mse"}
|
||||||
isRestreamed={isRestreamedStates[camera.name]}
|
isRestreamed={isRestreamedStates[camera.name]}
|
||||||
supportsAudio={
|
supportsAudio={
|
||||||
@ -489,8 +472,6 @@ export default function LiveDashboardView({
|
|||||||
setIsEditMode={setIsEditMode}
|
setIsEditMode={setIsEditMode}
|
||||||
fullscreen={fullscreen}
|
fullscreen={fullscreen}
|
||||||
toggleFullscreen={toggleFullscreen}
|
toggleFullscreen={toggleFullscreen}
|
||||||
allGroupsStreamingSettings={allGroupsStreamingSettings}
|
|
||||||
setAllGroupsStreamingSettings={setAllGroupsStreamingSettings}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user