mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-09-27 01:38:58 +03:00
Improve live streaming (#16447)
* config file changes * config migrator * stream selection on single camera live view * camera streaming settings dialog * manage persistent group streaming settings * apply streaming settings in camera groups * add ability to clear all streaming settings from settings * docs * update reference config * fixes * clarify docs * use first stream as default in dialog * ensure still image is visible after switching stream type to none * docs * clarify docs * add ability to continue playing stream in background * fix props * put stream selection inside dropdown on desktop * add capabilities to live mode hook * live context menu component * resize observer: only return new dimensions if they've actually changed * pass volume prop to players * fix slider bug, https://github.com/shadcn-ui/ui/issues/1448 * update react-grid-layout * prevent animated transitions on draggable grid layout * add context menu to dashboards * use provider * streaming dialog from context menu * docs * add jsmpeg warning to context menu * audio and two way talk indicators in single camera view * add link to debug view * don't use hook * create manual events from live camera view * maintain grow classes on grid items * fix initial volume state on default dashboard * fix pointer events causing context menu to end up underneath image on iOS * mobile drawer tweaks * stream stats * show settings menu for non-restreamed cameras * consistent settings icon * tweaks * optional stats to fix birdseye player * add toaster to live camera view * fix crash on initial save in streaming dialog * don't require restreaming for context menu streaming settings * add debug view to context menu * stats fixes * update docs * always show stream info when restreamed * update camera streaming dialog * make note of no h265 support for webrtc * docs clarity * ensure docs show streams as a dict * docs clarity * fix css file * tweaks
This commit is contained in:
@@ -0,0 +1,302 @@
|
||||
import {
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import {
|
||||
ContextMenu,
|
||||
ContextMenuContent,
|
||||
ContextMenuItem,
|
||||
ContextMenuSeparator,
|
||||
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,
|
||||
GroupStreamingSettings,
|
||||
} from "@/types/frigateConfig";
|
||||
import { useStreamingSettings } from "@/context/streaming-settings-provider";
|
||||
import { IoIosWarning } from "react-icons/io";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
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;
|
||||
children?: ReactNode;
|
||||
};
|
||||
export default function LiveContextMenu({
|
||||
className,
|
||||
camera,
|
||||
streamName,
|
||||
cameraGroup,
|
||||
preferredLiveMode,
|
||||
isRestreamed,
|
||||
supportsAudio,
|
||||
audioState,
|
||||
toggleAudio,
|
||||
volumeState,
|
||||
setVolumeState,
|
||||
muteAll,
|
||||
unmuteAll,
|
||||
statsState,
|
||||
toggleStats,
|
||||
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;
|
||||
} 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();
|
||||
|
||||
return (
|
||||
<div className={cn("w-full", className)}>
|
||||
<ContextMenu key={camera} onOpenChange={handleOpenChange}>
|
||||
<ContextMenuTrigger>{children}</ContextMenuTrigger>
|
||||
<ContextMenuContent>
|
||||
<div className="flex flex-col items-start gap-1 py-1 pl-2">
|
||||
<div className="text-md capitalize text-primary-variant">
|
||||
{camera.replaceAll("_", " ")}
|
||||
</div>
|
||||
{preferredLiveMode == "jsmpeg" && isRestreamed && (
|
||||
<div className="flex flex-row items-center gap-1">
|
||||
<IoIosWarning className="mr-1 size-4 text-danger" />
|
||||
<p className="mr-2 text-xs">Low-bandwidth mode</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{preferredLiveMode != "jsmpeg" && isRestreamed && supportsAudio && (
|
||||
<>
|
||||
<ContextMenuSeparator className="mb-1" />
|
||||
<div className="p-2 text-sm">
|
||||
<div className="flex w-full flex-col gap-1">
|
||||
<p>Audio</p>
|
||||
<div className="flex flex-row items-center gap-1">
|
||||
<VolumeIcon
|
||||
className="size-5"
|
||||
onClick={handleVolumeIconClick}
|
||||
/>
|
||||
<VolumeSlider
|
||||
disabled={!audioState}
|
||||
className="my-3 ml-0.5 rounded-lg bg-background/60"
|
||||
value={[volumeState ?? 0]}
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.02}
|
||||
onValueChange={handleVolumeChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<ContextMenuSeparator />
|
||||
<ContextMenuItem>
|
||||
<div
|
||||
className="flex w-full cursor-pointer items-center justify-start gap-2"
|
||||
onClick={muteAll}
|
||||
>
|
||||
<div className="text-primary">Mute All Cameras</div>
|
||||
</div>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem>
|
||||
<div
|
||||
className="flex w-full cursor-pointer items-center justify-start gap-2"
|
||||
onClick={unmuteAll}
|
||||
>
|
||||
<div className="text-primary">Unmute All Cameras</div>
|
||||
</div>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuSeparator />
|
||||
<ContextMenuItem>
|
||||
<div
|
||||
className="flex w-full cursor-pointer items-center justify-start gap-2"
|
||||
onClick={toggleStats}
|
||||
>
|
||||
<div className="text-primary">
|
||||
{statsState ? "Hide" : "Show"} Stream Stats
|
||||
</div>
|
||||
</div>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem>
|
||||
<div
|
||||
className="flex w-full cursor-pointer items-center justify-start gap-2"
|
||||
onClick={() => navigate(`/settings?page=debug&camera=${camera}`)}
|
||||
>
|
||||
<div className="text-primary">Debug View</div>
|
||||
</div>
|
||||
</ContextMenuItem>
|
||||
{cameraGroup && cameraGroup !== "default" && (
|
||||
<>
|
||||
<ContextMenuSeparator />
|
||||
<ContextMenuItem>
|
||||
<div
|
||||
className="flex w-full cursor-pointer items-center justify-start gap-2"
|
||||
onClick={() => setShowSettings(true)}
|
||||
>
|
||||
<div className="text-primary">Streaming Settings</div>
|
||||
</div>
|
||||
</ContextMenuItem>
|
||||
</>
|
||||
)}
|
||||
{preferredLiveMode == "jsmpeg" && isRestreamed && (
|
||||
<>
|
||||
<ContextMenuSeparator />
|
||||
<ContextMenuItem>
|
||||
<div
|
||||
className="flex w-full cursor-pointer items-center justify-start gap-2"
|
||||
onClick={resetPreferredLiveMode}
|
||||
>
|
||||
<div className="text-primary">Reset</div>
|
||||
</div>
|
||||
</ContextMenuItem>
|
||||
</>
|
||||
)}
|
||||
</ContextMenuContent>
|
||||
</ContextMenu>
|
||||
|
||||
<Dialog open={showSettings} onOpenChange={setShowSettings}>
|
||||
<CameraStreamingDialog
|
||||
camera={camera}
|
||||
groupStreamingSettings={groupStreamingSettings}
|
||||
setGroupStreamingSettings={setGroupStreamingSettings}
|
||||
setIsDialogOpen={setShowSettings}
|
||||
onSave={onSave}
|
||||
/>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user