mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-12-08 06:15:43 +03:00
Compare commits
2 Commits
481469bde2
...
63c9be1f82
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
63c9be1f82 | ||
|
|
1f5b7fb946 |
@ -48,6 +48,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { useDateLocale } from "@/hooks/use-date-locale";
|
import { useDateLocale } from "@/hooks/use-date-locale";
|
||||||
import { useIsAdmin } from "@/hooks/use-is-admin";
|
import { useIsAdmin } from "@/hooks/use-is-admin";
|
||||||
import { CameraNameLabel } from "../camera/FriendlyNameLabel";
|
import { CameraNameLabel } from "../camera/FriendlyNameLabel";
|
||||||
|
import { LiveStreamMetadata } from "@/types/live";
|
||||||
|
|
||||||
type LiveContextMenuProps = {
|
type LiveContextMenuProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
@ -68,6 +69,7 @@ type LiveContextMenuProps = {
|
|||||||
resetPreferredLiveMode: () => void;
|
resetPreferredLiveMode: () => void;
|
||||||
config?: FrigateConfig;
|
config?: FrigateConfig;
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
|
streamMetadata?: { [key: string]: LiveStreamMetadata };
|
||||||
};
|
};
|
||||||
export default function LiveContextMenu({
|
export default function LiveContextMenu({
|
||||||
className,
|
className,
|
||||||
@ -88,6 +90,7 @@ export default function LiveContextMenu({
|
|||||||
resetPreferredLiveMode,
|
resetPreferredLiveMode,
|
||||||
config,
|
config,
|
||||||
children,
|
children,
|
||||||
|
streamMetadata,
|
||||||
}: LiveContextMenuProps) {
|
}: LiveContextMenuProps) {
|
||||||
const { t } = useTranslation("views/live");
|
const { t } = useTranslation("views/live");
|
||||||
const [showSettings, setShowSettings] = useState(false);
|
const [showSettings, setShowSettings] = useState(false);
|
||||||
@ -558,6 +561,7 @@ export default function LiveContextMenu({
|
|||||||
setGroupStreamingSettings={setGroupStreamingSettings}
|
setGroupStreamingSettings={setGroupStreamingSettings}
|
||||||
setIsDialogOpen={setShowSettings}
|
setIsDialogOpen={setShowSettings}
|
||||||
onSave={onSave}
|
onSave={onSave}
|
||||||
|
streamMetadata={streamMetadata}
|
||||||
/>
|
/>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -38,6 +38,7 @@ import { useCameraFriendlyName } from "@/hooks/use-camera-friendly-name";
|
|||||||
type CameraStreamingDialogProps = {
|
type CameraStreamingDialogProps = {
|
||||||
camera: string;
|
camera: string;
|
||||||
groupStreamingSettings: GroupStreamingSettings;
|
groupStreamingSettings: GroupStreamingSettings;
|
||||||
|
streamMetadata?: { [key: string]: LiveStreamMetadata };
|
||||||
setGroupStreamingSettings: React.Dispatch<
|
setGroupStreamingSettings: React.Dispatch<
|
||||||
React.SetStateAction<GroupStreamingSettings>
|
React.SetStateAction<GroupStreamingSettings>
|
||||||
>;
|
>;
|
||||||
@ -48,6 +49,7 @@ type CameraStreamingDialogProps = {
|
|||||||
export function CameraStreamingDialog({
|
export function CameraStreamingDialog({
|
||||||
camera,
|
camera,
|
||||||
groupStreamingSettings,
|
groupStreamingSettings,
|
||||||
|
streamMetadata,
|
||||||
setGroupStreamingSettings,
|
setGroupStreamingSettings,
|
||||||
setIsDialogOpen,
|
setIsDialogOpen,
|
||||||
onSave,
|
onSave,
|
||||||
@ -76,12 +78,7 @@ export function CameraStreamingDialog({
|
|||||||
[config, streamName],
|
[config, streamName],
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data: cameraMetadata } = useSWR<LiveStreamMetadata>(
|
const cameraMetadata = streamName ? streamMetadata?.[streamName] : undefined;
|
||||||
isRestreamed ? `go2rtc/streams/${streamName}` : null,
|
|
||||||
{
|
|
||||||
revalidateOnFocus: false,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const supportsAudioOutput = useMemo(() => {
|
const supportsAudioOutput = useMemo(() => {
|
||||||
if (!cameraMetadata) {
|
if (!cameraMetadata) {
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import { baseUrl } from "@/api/baseUrl";
|
|
||||||
import { CameraConfig, FrigateConfig } from "@/types/frigateConfig";
|
import { CameraConfig, FrigateConfig } from "@/types/frigateConfig";
|
||||||
import { useCallback, useEffect, useState, useMemo } from "react";
|
import { useCallback, useEffect, useState, useMemo } from "react";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { LivePlayerMode, LiveStreamMetadata } from "@/types/live";
|
import { LivePlayerMode } from "@/types/live";
|
||||||
|
import useDeferredStreamMetadata from "./use-deferred-stream-metadata";
|
||||||
|
|
||||||
export default function useCameraLiveMode(
|
export default function useCameraLiveMode(
|
||||||
cameras: CameraConfig[],
|
cameras: CameraConfig[],
|
||||||
@ -11,9 +11,9 @@ export default function useCameraLiveMode(
|
|||||||
) {
|
) {
|
||||||
const { data: config } = useSWR<FrigateConfig>("config");
|
const { data: config } = useSWR<FrigateConfig>("config");
|
||||||
|
|
||||||
// Get comma-separated list of restreamed stream names for SWR key
|
// Compute which streams need metadata (restreamed streams only)
|
||||||
const restreamedStreamsKey = useMemo(() => {
|
const restreamedStreamNames = useMemo(() => {
|
||||||
if (!cameras || !config) return null;
|
if (!cameras || !config) return [];
|
||||||
|
|
||||||
const streamNames = new Set<string>();
|
const streamNames = new Set<string>();
|
||||||
cameras.forEach((camera) => {
|
cameras.forEach((camera) => {
|
||||||
@ -32,56 +32,13 @@ export default function useCameraLiveMode(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return streamNames.size > 0
|
return Array.from(streamNames);
|
||||||
? Array.from(streamNames).sort().join(",")
|
|
||||||
: null;
|
|
||||||
}, [cameras, config, activeStreams]);
|
}, [cameras, config, activeStreams]);
|
||||||
|
|
||||||
const streamsFetcher = useCallback(async (key: string) => {
|
// Fetch stream metadata with deferred loading (doesn't block initial render)
|
||||||
const streamNames = key.split(",");
|
const streamMetadata = useDeferredStreamMetadata(restreamedStreamNames);
|
||||||
|
|
||||||
const metadataPromises = streamNames.map(async (streamName) => {
|
|
||||||
try {
|
|
||||||
const response = await fetch(
|
|
||||||
`${baseUrl}api/go2rtc/streams/${streamName}`,
|
|
||||||
{
|
|
||||||
priority: "low",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
const data = await response.json();
|
|
||||||
return { streamName, data };
|
|
||||||
}
|
|
||||||
return { streamName, data: null };
|
|
||||||
} catch (error) {
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.error(`Failed to fetch metadata for ${streamName}:`, error);
|
|
||||||
return { streamName, data: null };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const results = await Promise.allSettled(metadataPromises);
|
|
||||||
|
|
||||||
const metadata: { [key: string]: LiveStreamMetadata } = {};
|
|
||||||
results.forEach((result) => {
|
|
||||||
if (result.status === "fulfilled" && result.value.data) {
|
|
||||||
metadata[result.value.streamName] = result.value.data;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return metadata;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const { data: allStreamMetadata = {} } = useSWR<{
|
|
||||||
[key: string]: LiveStreamMetadata;
|
|
||||||
}>(restreamedStreamsKey, streamsFetcher, {
|
|
||||||
revalidateOnFocus: false,
|
|
||||||
revalidateOnReconnect: false,
|
|
||||||
revalidateIfStale: false,
|
|
||||||
dedupingInterval: 60000,
|
|
||||||
});
|
|
||||||
|
|
||||||
|
// Compute live mode states
|
||||||
const [preferredLiveModes, setPreferredLiveModes] = useState<{
|
const [preferredLiveModes, setPreferredLiveModes] = useState<{
|
||||||
[key: string]: LivePlayerMode;
|
[key: string]: LivePlayerMode;
|
||||||
}>({});
|
}>({});
|
||||||
@ -122,10 +79,10 @@ export default function useCameraLiveMode(
|
|||||||
newPreferredLiveModes[camera.name] = isRestreamed ? "mse" : "jsmpeg";
|
newPreferredLiveModes[camera.name] = isRestreamed ? "mse" : "jsmpeg";
|
||||||
}
|
}
|
||||||
|
|
||||||
// check each stream for audio support
|
// Check each stream for audio support
|
||||||
if (isRestreamed) {
|
if (isRestreamed) {
|
||||||
Object.values(camera.live.streams).forEach((streamName) => {
|
Object.values(camera.live.streams).forEach((streamName) => {
|
||||||
const metadata = allStreamMetadata?.[streamName];
|
const metadata = streamMetadata[streamName];
|
||||||
newSupportsAudioOutputStates[streamName] = {
|
newSupportsAudioOutputStates[streamName] = {
|
||||||
supportsAudio: metadata
|
supportsAudio: metadata
|
||||||
? metadata.producers.find(
|
? metadata.producers.find(
|
||||||
@ -150,7 +107,7 @@ export default function useCameraLiveMode(
|
|||||||
setPreferredLiveModes(newPreferredLiveModes);
|
setPreferredLiveModes(newPreferredLiveModes);
|
||||||
setIsRestreamedStates(newIsRestreamedStates);
|
setIsRestreamedStates(newIsRestreamedStates);
|
||||||
setSupportsAudioOutputStates(newSupportsAudioOutputStates);
|
setSupportsAudioOutputStates(newSupportsAudioOutputStates);
|
||||||
}, [cameras, config, windowVisible, allStreamMetadata]);
|
}, [cameras, config, windowVisible, streamMetadata]);
|
||||||
|
|
||||||
const resetPreferredLiveMode = useCallback(
|
const resetPreferredLiveMode = useCallback(
|
||||||
(cameraName: string) => {
|
(cameraName: string) => {
|
||||||
@ -180,5 +137,6 @@ export default function useCameraLiveMode(
|
|||||||
resetPreferredLiveMode,
|
resetPreferredLiveMode,
|
||||||
isRestreamedStates,
|
isRestreamedStates,
|
||||||
supportsAudioOutputStates,
|
supportsAudioOutputStates,
|
||||||
|
streamMetadata,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
90
web/src/hooks/use-deferred-stream-metadata.ts
Normal file
90
web/src/hooks/use-deferred-stream-metadata.ts
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import { baseUrl } from "@/api/baseUrl";
|
||||||
|
import { useCallback, useEffect, useState, useMemo } from "react";
|
||||||
|
import useSWR from "swr";
|
||||||
|
import { LiveStreamMetadata } from "@/types/live";
|
||||||
|
|
||||||
|
const FETCH_TIMEOUT_MS = 10000;
|
||||||
|
const DEFER_DELAY_MS = 2000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook that fetches go2rtc stream metadata with deferred loading.
|
||||||
|
*
|
||||||
|
* Metadata fetching is delayed to prevent blocking initial page load
|
||||||
|
* and camera image requests.
|
||||||
|
*
|
||||||
|
* @param streamNames - Array of stream names to fetch metadata for
|
||||||
|
* @returns Object containing stream metadata keyed by stream name
|
||||||
|
*/
|
||||||
|
export default function useDeferredStreamMetadata(streamNames: string[]) {
|
||||||
|
const [fetchEnabled, setFetchEnabled] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
setFetchEnabled(true);
|
||||||
|
}, DEFER_DELAY_MS);
|
||||||
|
|
||||||
|
return () => clearTimeout(timeoutId);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const swrKey = useMemo(() => {
|
||||||
|
if (!fetchEnabled || streamNames.length === 0) return null;
|
||||||
|
// Use spread to avoid mutating the original array
|
||||||
|
return `deferred-streams:${[...streamNames].sort().join(",")}`;
|
||||||
|
}, [fetchEnabled, streamNames]);
|
||||||
|
|
||||||
|
const fetcher = useCallback(async (key: string) => {
|
||||||
|
// Extract stream names from key (remove prefix)
|
||||||
|
const names = key.replace("deferred-streams:", "").split(",");
|
||||||
|
|
||||||
|
const promises = names.map(async (streamName) => {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`${baseUrl}api/go2rtc/streams/${streamName}`,
|
||||||
|
{
|
||||||
|
priority: "low",
|
||||||
|
signal: controller.signal,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
return { streamName, data };
|
||||||
|
}
|
||||||
|
return { streamName, data: null };
|
||||||
|
} catch (error) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
if ((error as Error).name !== "AbortError") {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error(`Failed to fetch metadata for ${streamName}:`, error);
|
||||||
|
}
|
||||||
|
return { streamName, data: null };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const results = await Promise.allSettled(promises);
|
||||||
|
|
||||||
|
const metadata: { [key: string]: LiveStreamMetadata } = {};
|
||||||
|
results.forEach((result) => {
|
||||||
|
if (result.status === "fulfilled" && result.value.data) {
|
||||||
|
metadata[result.value.streamName] = result.value.data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return metadata;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const { data: metadata = {} } = useSWR<{
|
||||||
|
[key: string]: LiveStreamMetadata;
|
||||||
|
}>(swrKey, fetcher, {
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
revalidateOnReconnect: false,
|
||||||
|
revalidateIfStale: false,
|
||||||
|
dedupingInterval: 60000,
|
||||||
|
});
|
||||||
|
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
@ -24,6 +24,7 @@ import "react-resizable/css/styles.css";
|
|||||||
import {
|
import {
|
||||||
AudioState,
|
AudioState,
|
||||||
LivePlayerMode,
|
LivePlayerMode,
|
||||||
|
LiveStreamMetadata,
|
||||||
StatsState,
|
StatsState,
|
||||||
VolumeState,
|
VolumeState,
|
||||||
} from "@/types/live";
|
} from "@/types/live";
|
||||||
@ -47,7 +48,6 @@ import {
|
|||||||
TooltipContent,
|
TooltipContent,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { Toaster } from "@/components/ui/sonner";
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
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";
|
import { useStreamingSettings } from "@/context/streaming-settings-provider";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@ -65,6 +65,16 @@ type DraggableGridLayoutProps = {
|
|||||||
setIsEditMode: React.Dispatch<React.SetStateAction<boolean>>;
|
setIsEditMode: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
fullscreen: boolean;
|
fullscreen: boolean;
|
||||||
toggleFullscreen: () => void;
|
toggleFullscreen: () => void;
|
||||||
|
preferredLiveModes: { [key: string]: LivePlayerMode };
|
||||||
|
setPreferredLiveModes: React.Dispatch<
|
||||||
|
React.SetStateAction<{ [key: string]: LivePlayerMode }>
|
||||||
|
>;
|
||||||
|
resetPreferredLiveMode: (cameraName: string) => void;
|
||||||
|
isRestreamedStates: { [key: string]: boolean };
|
||||||
|
supportsAudioOutputStates: {
|
||||||
|
[key: string]: { supportsAudio: boolean; cameraName: string };
|
||||||
|
};
|
||||||
|
streamMetadata: { [key: string]: LiveStreamMetadata };
|
||||||
};
|
};
|
||||||
export default function DraggableGridLayout({
|
export default function DraggableGridLayout({
|
||||||
cameras,
|
cameras,
|
||||||
@ -79,6 +89,12 @@ export default function DraggableGridLayout({
|
|||||||
setIsEditMode,
|
setIsEditMode,
|
||||||
fullscreen,
|
fullscreen,
|
||||||
toggleFullscreen,
|
toggleFullscreen,
|
||||||
|
preferredLiveModes,
|
||||||
|
setPreferredLiveModes,
|
||||||
|
resetPreferredLiveMode,
|
||||||
|
isRestreamedStates,
|
||||||
|
supportsAudioOutputStates,
|
||||||
|
streamMetadata,
|
||||||
}: DraggableGridLayoutProps) {
|
}: DraggableGridLayoutProps) {
|
||||||
const { t } = useTranslation(["views/live"]);
|
const { t } = useTranslation(["views/live"]);
|
||||||
const { data: config } = useSWR<FrigateConfig>("config");
|
const { data: config } = useSWR<FrigateConfig>("config");
|
||||||
@ -98,33 +114,6 @@ export default function DraggableGridLayout({
|
|||||||
}
|
}
|
||||||
}, [allGroupsStreamingSettings, cameraGroup]);
|
}, [allGroupsStreamingSettings, cameraGroup]);
|
||||||
|
|
||||||
const activeStreams = useMemo(() => {
|
|
||||||
const streams: { [cameraName: string]: string } = {};
|
|
||||||
cameras.forEach((camera) => {
|
|
||||||
const availableStreams = camera.live.streams || {};
|
|
||||||
const streamNameFromSettings =
|
|
||||||
currentGroupStreamingSettings?.[camera.name]?.streamName || "";
|
|
||||||
const streamExists =
|
|
||||||
streamNameFromSettings &&
|
|
||||||
Object.values(availableStreams).includes(streamNameFromSettings);
|
|
||||||
|
|
||||||
const streamName = streamExists
|
|
||||||
? streamNameFromSettings
|
|
||||||
: Object.values(availableStreams)[0] || "";
|
|
||||||
|
|
||||||
streams[camera.name] = streamName;
|
|
||||||
});
|
|
||||||
return streams;
|
|
||||||
}, [cameras, currentGroupStreamingSettings]);
|
|
||||||
|
|
||||||
const {
|
|
||||||
preferredLiveModes,
|
|
||||||
setPreferredLiveModes,
|
|
||||||
resetPreferredLiveMode,
|
|
||||||
isRestreamedStates,
|
|
||||||
supportsAudioOutputStates,
|
|
||||||
} = useCameraLiveMode(cameras, windowVisible, activeStreams);
|
|
||||||
|
|
||||||
// grid layout
|
// grid layout
|
||||||
|
|
||||||
const ResponsiveGridLayout = useMemo(() => WidthProvider(Responsive), []);
|
const ResponsiveGridLayout = useMemo(() => WidthProvider(Responsive), []);
|
||||||
@ -624,6 +613,7 @@ export default function DraggableGridLayout({
|
|||||||
resetPreferredLiveMode(camera.name)
|
resetPreferredLiveMode(camera.name)
|
||||||
}
|
}
|
||||||
config={config}
|
config={config}
|
||||||
|
streamMetadata={streamMetadata}
|
||||||
>
|
>
|
||||||
<LivePlayer
|
<LivePlayer
|
||||||
key={camera.name}
|
key={camera.name}
|
||||||
@ -838,6 +828,7 @@ type GridLiveContextMenuProps = {
|
|||||||
unmuteAll: () => void;
|
unmuteAll: () => void;
|
||||||
resetPreferredLiveMode: () => void;
|
resetPreferredLiveMode: () => void;
|
||||||
config?: FrigateConfig;
|
config?: FrigateConfig;
|
||||||
|
streamMetadata?: { [key: string]: LiveStreamMetadata };
|
||||||
};
|
};
|
||||||
|
|
||||||
const GridLiveContextMenu = React.forwardRef<
|
const GridLiveContextMenu = React.forwardRef<
|
||||||
@ -868,6 +859,7 @@ const GridLiveContextMenu = React.forwardRef<
|
|||||||
unmuteAll,
|
unmuteAll,
|
||||||
resetPreferredLiveMode,
|
resetPreferredLiveMode,
|
||||||
config,
|
config,
|
||||||
|
streamMetadata,
|
||||||
...props
|
...props
|
||||||
},
|
},
|
||||||
ref,
|
ref,
|
||||||
@ -899,6 +891,7 @@ const GridLiveContextMenu = React.forwardRef<
|
|||||||
unmuteAll={unmuteAll}
|
unmuteAll={unmuteAll}
|
||||||
resetPreferredLiveMode={resetPreferredLiveMode}
|
resetPreferredLiveMode={resetPreferredLiveMode}
|
||||||
config={config}
|
config={config}
|
||||||
|
streamMetadata={streamMetadata}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</LiveContextMenu>
|
</LiveContextMenu>
|
||||||
|
|||||||
@ -265,6 +265,7 @@ export default function LiveDashboardView({
|
|||||||
resetPreferredLiveMode,
|
resetPreferredLiveMode,
|
||||||
isRestreamedStates,
|
isRestreamedStates,
|
||||||
supportsAudioOutputStates,
|
supportsAudioOutputStates,
|
||||||
|
streamMetadata,
|
||||||
} = useCameraLiveMode(cameras, windowVisible, activeStreams);
|
} = useCameraLiveMode(cameras, windowVisible, activeStreams);
|
||||||
|
|
||||||
const birdseyeConfig = useMemo(() => config?.birdseye, [config]);
|
const birdseyeConfig = useMemo(() => config?.birdseye, [config]);
|
||||||
@ -650,6 +651,12 @@ export default function LiveDashboardView({
|
|||||||
setIsEditMode={setIsEditMode}
|
setIsEditMode={setIsEditMode}
|
||||||
fullscreen={fullscreen}
|
fullscreen={fullscreen}
|
||||||
toggleFullscreen={toggleFullscreen}
|
toggleFullscreen={toggleFullscreen}
|
||||||
|
preferredLiveModes={preferredLiveModes}
|
||||||
|
setPreferredLiveModes={setPreferredLiveModes}
|
||||||
|
resetPreferredLiveMode={resetPreferredLiveMode}
|
||||||
|
isRestreamedStates={isRestreamedStates}
|
||||||
|
supportsAudioOutputStates={supportsAudioOutputStates}
|
||||||
|
streamMetadata={streamMetadata}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user