mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-09-28 07:48:57 +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:
@@ -1,5 +1,6 @@
|
||||
import { usePersistence } from "@/hooks/use-persistence";
|
||||
import {
|
||||
AllGroupsStreamingSettings,
|
||||
BirdseyeConfig,
|
||||
CameraConfig,
|
||||
FrigateConfig,
|
||||
@@ -20,7 +21,12 @@ import {
|
||||
} from "react-grid-layout";
|
||||
import "react-grid-layout/css/styles.css";
|
||||
import "react-resizable/css/styles.css";
|
||||
import { LivePlayerError, LivePlayerMode } from "@/types/live";
|
||||
import {
|
||||
AudioState,
|
||||
LivePlayerMode,
|
||||
StatsState,
|
||||
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";
|
||||
@@ -42,6 +48,8 @@ import {
|
||||
} from "@/components/ui/tooltip";
|
||||
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[];
|
||||
@@ -76,8 +84,26 @@ export default function DraggableGridLayout({
|
||||
|
||||
// preferred live modes per camera
|
||||
|
||||
const { preferredLiveModes, setPreferredLiveModes, resetPreferredLiveMode } =
|
||||
useCameraLiveMode(cameras, windowVisible);
|
||||
const {
|
||||
preferredLiveModes,
|
||||
setPreferredLiveModes,
|
||||
resetPreferredLiveMode,
|
||||
isRestreamedStates,
|
||||
supportsAudioOutputStates,
|
||||
} = useCameraLiveMode(cameras, windowVisible);
|
||||
|
||||
const [globalAutoLive] = usePersistence("autoLiveView", true);
|
||||
|
||||
const { allGroupsStreamingSettings, setAllGroupsStreamingSettings } =
|
||||
useStreamingSettings();
|
||||
|
||||
const currentGroupStreamingSettings = useMemo(() => {
|
||||
if (cameraGroup && cameraGroup != "default" && allGroupsStreamingSettings) {
|
||||
return allGroupsStreamingSettings[cameraGroup];
|
||||
}
|
||||
}, [allGroupsStreamingSettings, cameraGroup]);
|
||||
|
||||
// grid layout
|
||||
|
||||
const ResponsiveGridLayout = useMemo(() => WidthProvider(Responsive), []);
|
||||
|
||||
@@ -342,6 +368,105 @@ export default function DraggableGridLayout({
|
||||
placeholder.h = layoutItem.h;
|
||||
};
|
||||
|
||||
// audio and stats states
|
||||
|
||||
const [audioStates, setAudioStates] = useState<AudioState>({});
|
||||
const [volumeStates, setVolumeStates] = useState<VolumeState>({});
|
||||
const [statsStates, setStatsStates] = useState<StatsState>(() => {
|
||||
const initialStates: StatsState = {};
|
||||
cameras.forEach((camera) => {
|
||||
initialStates[camera.name] = false;
|
||||
});
|
||||
return initialStates;
|
||||
});
|
||||
|
||||
const toggleStats = (cameraName: string): void => {
|
||||
setStatsStates((prev) => ({
|
||||
...prev,
|
||||
[cameraName]: !prev[cameraName],
|
||||
}));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!allGroupsStreamingSettings) {
|
||||
return;
|
||||
}
|
||||
|
||||
const initialAudioStates: AudioState = {};
|
||||
const initialVolumeStates: VolumeState = {};
|
||||
|
||||
Object.entries(allGroupsStreamingSettings).forEach(([_, groupSettings]) => {
|
||||
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 onSaveMuting = useCallback(
|
||||
(playAudio: boolean) => {
|
||||
if (!cameraGroup || !allGroupsStreamingSettings) {
|
||||
return;
|
||||
}
|
||||
|
||||
const existingGroupSettings =
|
||||
allGroupsStreamingSettings[cameraGroup] || {};
|
||||
|
||||
const updatedSettings: AllGroupsStreamingSettings = {
|
||||
...Object.fromEntries(
|
||||
Object.entries(allGroupsStreamingSettings || {}).filter(
|
||||
([key]) => key !== cameraGroup,
|
||||
),
|
||||
),
|
||||
[cameraGroup]: {
|
||||
...existingGroupSettings,
|
||||
...Object.fromEntries(
|
||||
Object.entries(existingGroupSettings).map(
|
||||
([cameraName, settings]) => [
|
||||
cameraName,
|
||||
{
|
||||
...settings,
|
||||
playAudio: playAudio,
|
||||
},
|
||||
],
|
||||
),
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
setAllGroupsStreamingSettings?.(updatedSettings);
|
||||
},
|
||||
[cameraGroup, allGroupsStreamingSettings, setAllGroupsStreamingSettings],
|
||||
);
|
||||
|
||||
const muteAll = () => {
|
||||
const updatedStates: AudioState = {};
|
||||
cameras.forEach((camera) => {
|
||||
updatedStates[camera.name] = false;
|
||||
});
|
||||
setAudioStates(updatedStates);
|
||||
onSaveMuting(false);
|
||||
};
|
||||
|
||||
const unmuteAll = () => {
|
||||
const updatedStates: AudioState = {};
|
||||
cameras.forEach((camera) => {
|
||||
updatedStates[camera.name] = true;
|
||||
});
|
||||
setAudioStates(updatedStates);
|
||||
onSaveMuting(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Toaster position="top-center" closeButton={true} />
|
||||
@@ -364,7 +489,7 @@ export default function DraggableGridLayout({
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="no-scrollbar my-2 overflow-x-hidden px-2 pb-8"
|
||||
className="no-scrollbar my-2 select-none overflow-x-hidden px-2 pb-8"
|
||||
ref={gridContainerRef}
|
||||
>
|
||||
<EditGroupDialog
|
||||
@@ -420,40 +545,87 @@ export default function DraggableGridLayout({
|
||||
} else {
|
||||
grow = "aspect-video";
|
||||
}
|
||||
const streamName =
|
||||
currentGroupStreamingSettings?.[camera.name]?.streamName ||
|
||||
Object.values(camera.live.streams)[0];
|
||||
const autoLive =
|
||||
currentGroupStreamingSettings?.[camera.name]?.streamType !==
|
||||
"no-streaming";
|
||||
const showStillWithoutActivity =
|
||||
currentGroupStreamingSettings?.[camera.name]?.streamType !==
|
||||
"continuous";
|
||||
const useWebGL =
|
||||
currentGroupStreamingSettings?.[camera.name]
|
||||
?.compatibilityMode || false;
|
||||
return (
|
||||
<LivePlayerGridItem
|
||||
<GridLiveContextMenu
|
||||
className={grow}
|
||||
key={camera.name}
|
||||
cameraRef={cameraRef}
|
||||
className={cn(
|
||||
"rounded-lg bg-black md:rounded-2xl",
|
||||
grow,
|
||||
isEditMode &&
|
||||
showCircles &&
|
||||
"outline-2 outline-muted-foreground hover:cursor-grab hover:outline-4 active:cursor-grabbing",
|
||||
)}
|
||||
windowVisible={
|
||||
windowVisible && visibleCameras.includes(camera.name)
|
||||
}
|
||||
cameraConfig={camera}
|
||||
camera={camera.name}
|
||||
streamName={streamName}
|
||||
cameraGroup={cameraGroup}
|
||||
preferredLiveMode={preferredLiveModes[camera.name] ?? "mse"}
|
||||
onClick={() => {
|
||||
!isEditMode && onSelectCamera(camera.name);
|
||||
}}
|
||||
onError={(e) => {
|
||||
setPreferredLiveModes((prevModes) => {
|
||||
const newModes = { ...prevModes };
|
||||
if (e === "mse-decode") {
|
||||
newModes[camera.name] = "webrtc";
|
||||
} else {
|
||||
newModes[camera.name] = "jsmpeg";
|
||||
}
|
||||
return newModes;
|
||||
});
|
||||
}}
|
||||
onResetLiveMode={() => resetPreferredLiveMode(camera.name)}
|
||||
isRestreamed={isRestreamedStates[camera.name]}
|
||||
supportsAudio={
|
||||
supportsAudioOutputStates[streamName].supportsAudio
|
||||
}
|
||||
audioState={audioStates[camera.name]}
|
||||
toggleAudio={() => toggleAudio(camera.name)}
|
||||
statsState={statsStates[camera.name]}
|
||||
toggleStats={() => toggleStats(camera.name)}
|
||||
volumeState={volumeStates[camera.name]}
|
||||
setVolumeState={(value) =>
|
||||
setVolumeStates({
|
||||
[camera.name]: value,
|
||||
})
|
||||
}
|
||||
muteAll={muteAll}
|
||||
unmuteAll={unmuteAll}
|
||||
resetPreferredLiveMode={() =>
|
||||
resetPreferredLiveMode(camera.name)
|
||||
}
|
||||
>
|
||||
<LivePlayer
|
||||
key={camera.name}
|
||||
streamName={streamName}
|
||||
autoLive={autoLive ?? globalAutoLive}
|
||||
showStillWithoutActivity={showStillWithoutActivity ?? true}
|
||||
useWebGL={useWebGL}
|
||||
cameraRef={cameraRef}
|
||||
className={cn(
|
||||
"rounded-lg bg-black md:rounded-2xl",
|
||||
grow,
|
||||
isEditMode &&
|
||||
showCircles &&
|
||||
"outline-2 outline-muted-foreground hover:cursor-grab hover:outline-4 active:cursor-grabbing",
|
||||
)}
|
||||
windowVisible={
|
||||
windowVisible && visibleCameras.includes(camera.name)
|
||||
}
|
||||
cameraConfig={camera}
|
||||
preferredLiveMode={preferredLiveModes[camera.name] ?? "mse"}
|
||||
playInBackground={false}
|
||||
showStats={statsStates[camera.name]}
|
||||
onClick={() => {
|
||||
!isEditMode && onSelectCamera(camera.name);
|
||||
}}
|
||||
onError={(e) => {
|
||||
setPreferredLiveModes((prevModes) => {
|
||||
const newModes = { ...prevModes };
|
||||
if (e === "mse-decode") {
|
||||
newModes[camera.name] = "webrtc";
|
||||
} else {
|
||||
newModes[camera.name] = "jsmpeg";
|
||||
}
|
||||
return newModes;
|
||||
});
|
||||
}}
|
||||
onResetLiveMode={() => resetPreferredLiveMode(camera.name)}
|
||||
playAudio={audioStates[camera.name]}
|
||||
volume={volumeStates[camera.name]}
|
||||
/>
|
||||
{isEditMode && showCircles && <CornerCircles />}
|
||||
</LivePlayerGridItem>
|
||||
</GridLiveContextMenu>
|
||||
);
|
||||
})}
|
||||
</ResponsiveGridLayout>
|
||||
@@ -596,41 +768,57 @@ const BirdseyeLivePlayerGridItem = React.forwardRef<
|
||||
},
|
||||
);
|
||||
|
||||
type LivePlayerGridItemProps = {
|
||||
type GridLiveContextMenuProps = {
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
className: string;
|
||||
onMouseDown?: React.MouseEventHandler<HTMLDivElement>;
|
||||
onMouseUp?: React.MouseEventHandler<HTMLDivElement>;
|
||||
onTouchEnd?: React.TouchEventHandler<HTMLDivElement>;
|
||||
children?: React.ReactNode;
|
||||
cameraRef: (node: HTMLElement | null) => void;
|
||||
windowVisible: boolean;
|
||||
cameraConfig: CameraConfig;
|
||||
preferredLiveMode: LivePlayerMode;
|
||||
onClick: () => void;
|
||||
onError: (e: LivePlayerError) => void;
|
||||
onResetLiveMode: () => void;
|
||||
camera: string;
|
||||
streamName: string;
|
||||
cameraGroup: string;
|
||||
preferredLiveMode: string;
|
||||
isRestreamed: boolean;
|
||||
supportsAudio: boolean;
|
||||
audioState: boolean;
|
||||
toggleAudio: () => void;
|
||||
statsState: boolean;
|
||||
toggleStats: () => void;
|
||||
volumeState?: number;
|
||||
setVolumeState: (volumeState: number) => void;
|
||||
muteAll: () => void;
|
||||
unmuteAll: () => void;
|
||||
resetPreferredLiveMode: () => void;
|
||||
};
|
||||
|
||||
const LivePlayerGridItem = React.forwardRef<
|
||||
const GridLiveContextMenu = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
LivePlayerGridItemProps
|
||||
GridLiveContextMenuProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
style,
|
||||
className,
|
||||
style,
|
||||
onMouseDown,
|
||||
onMouseUp,
|
||||
onTouchEnd,
|
||||
children,
|
||||
cameraRef,
|
||||
windowVisible,
|
||||
cameraConfig,
|
||||
camera,
|
||||
streamName,
|
||||
cameraGroup,
|
||||
preferredLiveMode,
|
||||
onClick,
|
||||
onError,
|
||||
onResetLiveMode,
|
||||
isRestreamed,
|
||||
supportsAudio,
|
||||
audioState,
|
||||
toggleAudio,
|
||||
statsState,
|
||||
toggleStats,
|
||||
volumeState,
|
||||
setVolumeState,
|
||||
muteAll,
|
||||
unmuteAll,
|
||||
resetPreferredLiveMode,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
@@ -644,18 +832,26 @@ const LivePlayerGridItem = React.forwardRef<
|
||||
onTouchEnd={onTouchEnd}
|
||||
{...props}
|
||||
>
|
||||
<LivePlayer
|
||||
cameraRef={cameraRef}
|
||||
<LiveContextMenu
|
||||
className={className}
|
||||
windowVisible={windowVisible}
|
||||
cameraConfig={cameraConfig}
|
||||
camera={camera}
|
||||
streamName={streamName}
|
||||
cameraGroup={cameraGroup}
|
||||
preferredLiveMode={preferredLiveMode}
|
||||
onClick={onClick}
|
||||
onError={onError}
|
||||
onResetLiveMode={onResetLiveMode}
|
||||
containerRef={ref as React.RefObject<HTMLDivElement>}
|
||||
/>
|
||||
{children}
|
||||
isRestreamed={isRestreamed}
|
||||
supportsAudio={supportsAudio}
|
||||
audioState={audioState}
|
||||
toggleAudio={toggleAudio}
|
||||
statsState={statsState}
|
||||
toggleStats={toggleStats}
|
||||
volumeState={volumeState}
|
||||
setVolumeState={setVolumeState}
|
||||
muteAll={muteAll}
|
||||
unmuteAll={unmuteAll}
|
||||
resetPreferredLiveMode={resetPreferredLiveMode}
|
||||
>
|
||||
{children}
|
||||
</LiveContextMenu>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user