import { ReactNode, useCallback, useEffect, useMemo, useRef, useState, } from "react"; import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuSeparator, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger, } from "@/components/ui/context-menu"; import { MdVolumeDown, MdVolumeMute, 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, FrigateConfig, GroupStreamingSettings, } from "@/types/frigateConfig"; import { useStreamingSettings } from "@/context/streaming-settings-provider"; import { IoIosNotifications, IoIosNotificationsOff, IoIosWarning, } from "react-icons/io"; import { cn } from "@/lib/utils"; import { useNavigate } from "react-router-dom"; import { formatUnixTimestampToDateTime } from "@/utils/dateUtil"; import { useEnabledState, useNotifications, useNotificationSuspend, } from "@/api/ws"; import { useTranslation } from "react-i18next"; type LiveContextMenuProps = { className?: string; camera: string; streamName: string; cameraGroup?: string; preferredLiveMode: string; isRestreamed: boolean; supportsAudio: boolean; audioState: boolean; toggleAudio: () => void; volumeState?: number; setVolumeState: (volumeState: number) => void; muteAll: () => void; unmuteAll: () => void; statsState: boolean; toggleStats: () => void; resetPreferredLiveMode: () => void; config?: FrigateConfig; children?: ReactNode; }; export default function LiveContextMenu({ className, camera, streamName, cameraGroup, preferredLiveMode, isRestreamed, supportsAudio, audioState, toggleAudio, volumeState, setVolumeState, muteAll, unmuteAll, statsState, toggleStats, resetPreferredLiveMode, config, children, }: LiveContextMenuProps) { const { t } = useTranslation("views/live"); const [showSettings, setShowSettings] = useState(false); // camera enabled const { payload: enabledState, send: sendEnabled } = useEnabledState(camera); const isEnabled = enabledState === "ON"; // streaming settings const { allGroupsStreamingSettings, setAllGroupsStreamingSettings } = useStreamingSettings(); const [groupStreamingSettings, setGroupStreamingSettings] = useState( allGroupsStreamingSettings[cameraGroup ?? ""], ); useEffect(() => { if (cameraGroup && cameraGroup != "default") { setGroupStreamingSettings(allGroupsStreamingSettings[cameraGroup]); } }, [allGroupsStreamingSettings, cameraGroup]); const onSave = useCallback( (settings: GroupStreamingSettings) => { if ( !cameraGroup || !allGroupsStreamingSettings || cameraGroup == "default" || !settings ) { 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; } else if (volumeState <= 0.33) { return MdVolumeMute; } else if (volumeState <= 0.67) { return MdVolumeDown; } else { return MdVolumeUp; } // only update when specific fields change // 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; } }; // navigate for debug view const navigate = useNavigate(); // notifications const notificationsEnabledInConfig = config?.cameras[camera]?.notifications?.enabled_in_config; const { payload: notificationState, send: sendNotification } = useNotifications(camera); const { payload: notificationSuspendUntil, send: sendNotificationSuspend } = useNotificationSuspend(camera); const [isSuspended, setIsSuspended] = useState(false); useEffect(() => { if (notificationSuspendUntil) { setIsSuspended( notificationSuspendUntil !== "0" || notificationState === "OFF", ); } }, [notificationSuspendUntil, notificationState]); const handleSuspend = (duration: string) => { if (duration === "off") { sendNotification("OFF"); } else { sendNotificationSuspend(Number.parseInt(duration)); } }; const formatSuspendedUntil = (timestamp: string) => { // Some languages require a change in word order if (timestamp === "0") return t("time.untilForRestart", { ns: "common" }); const time = formatUnixTimestampToDateTime(Number.parseInt(timestamp), { time_style: "medium", date_style: "medium", timezone: config?.ui.timezone, strftime_fmt: config?.ui.time_format == "24hour" ? t("time.formattedTimestampExcludeSeconds.24hour", { ns: "common" }) : t("time.formattedTimestampExcludeSeconds.12hour", { ns: "common" }), }); return t("time.untilForTime", { ns: "common", time }); }; return (
{children}
{camera.replaceAll("_", " ")}
{preferredLiveMode == "jsmpeg" && isRestreamed && (

{t("lowBandwidthMode")}

)}
{preferredLiveMode != "jsmpeg" && isRestreamed && supportsAudio && ( <>

{t("audio")}

)}
sendEnabled(isEnabled ? "OFF" : "ON")} >
{isEnabled ? t("camera.disable") : t("camera.enable")}
{t("muteCameras.enable")}
{t("muteCameras.disable")}
{statsState ? t("streamStats.disable") : t("streamStats.enable")}
navigate(`/settings?page=debug&camera=${camera}`) : undefined } >
{t("streaming.debugView", { ns: "components/dialog", })}
{cameraGroup && cameraGroup !== "default" && ( <>
setShowSettings(true) : undefined} >
{t("streamingSettings")}
)} {preferredLiveMode == "jsmpeg" && isRestreamed && ( <>
{t("button.reset", { ns: "common" })}
)} {notificationsEnabledInConfig && isEnabled && ( <>
{t("notifications")}
{notificationState === "ON" ? ( <> {isSuspended ? ( <> {t("button.suspended", { ns: "common" })} ) : ( <> {t("button.enabled", { ns: "common" })} )} ) : ( <> {t("button.disabled", { ns: "common" })} )}
{isSuspended && ( {formatSuspendedUntil(notificationSuspendUntil)} )}
{isSuspended ? ( <> { sendNotification("ON"); sendNotificationSuspend(0); } : undefined } >
{notificationState === "ON" ? ( {t("button.unsuspended", { ns: "common" })} ) : ( {t("button.enable", { ns: "common" })} )}
) : ( notificationState === "ON" && ( <>

{t("suspend.forTime")}

handleSuspend("5") : undefined } > {t("time.5minutes", { ns: "common" })} handleSuspend("10") : undefined } > {t("time.10minutes", { ns: "common" })} handleSuspend("30") : undefined } > {t("time.30minutes", { ns: "common" })} handleSuspend("60") : undefined } > {t("time.1hour", { ns: "common" })} handleSuspend("840") : undefined } > {t("time.12hours", { ns: "common" })} handleSuspend("1440") : undefined } > {t("time.24hours", { ns: "common" })} handleSuspend("off") : undefined } > {t("time.untilRestart", { ns: "common" })}
) )}
)}
); }