diff --git a/web/public/locales/en/views/live.json b/web/public/locales/en/views/live.json index c4f3f91b0b..3ea4689fe5 100644 --- a/web/public/locales/en/views/live.json +++ b/web/public/locales/en/views/live.json @@ -173,6 +173,9 @@ "label": "Play in background", "tips": "Enable this option to continue streaming when the player is hidden." }, + "metadata": { + "loading": "Fetching stream metadata…" + }, "mode": "Streaming Technology", "technology": { "description": "Choose your preferred streaming technology. Frigate may still fall back to low bandwidth mode on playback or network errors.", diff --git a/web/src/components/player/LiveStreamSelect.tsx b/web/src/components/player/LiveStreamSelect.tsx index fee9351c00..97d5cc56cf 100644 --- a/web/src/components/player/LiveStreamSelect.tsx +++ b/web/src/components/player/LiveStreamSelect.tsx @@ -12,11 +12,15 @@ import { useTranslation } from "react-i18next"; import { LuInfo } from "react-icons/lu"; import { MdOutlineRestartAlt } from "react-icons/md"; import { Button } from "@/components/ui/button"; +import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; // Radix Select needs a non-empty value. Auto itself is stored as the // absence of a pinned stream const AUTO_VALUE = "__frigate_auto__"; +const GRID_ITEM = + "h-auto min-h-10 min-w-[calc((100%-1rem)/3)] grow whitespace-normal bg-secondary [overflow-wrap:anywhere] px-2 py-2 text-secondary-foreground hover:bg-secondary hover:text-secondary-foreground data-[state=on]:bg-selected data-[state=on]:text-selected-foreground data-[state=on]:hover:bg-selected data-[state=on]:hover:text-selected-foreground"; + type LiveStreamSelectProps = { // friendly name to go2rtc stream name, in ladder order streams: Record; @@ -29,6 +33,7 @@ type LiveStreamSelectProps = { // sends auto back to the top of the ladder onRetry: () => void; disabled: boolean; + variant?: "select" | "grid"; }; export default function LiveStreamSelect({ @@ -40,6 +45,7 @@ export default function LiveStreamSelect({ onSelect, onRetry, disabled, + variant = "select", }: LiveStreamSelectProps) { const { t } = useTranslation(["views/live"]); @@ -52,48 +58,78 @@ export default function LiveStreamSelect({ const auto = autoAvailable && pinnedStream == undefined; const value = auto ? AUTO_VALUE : (pinnedStream ?? playingStream); + const autoLabel = auto + ? t("stream.auto.selected", { + stream: + autoReason === "floor" + ? t("stream.auto.floorLabel") + : labelFor(playingStream), + }) + : t("stream.auto.label"); + return ( <> - + onSelect(next === AUTO_VALUE ? undefined : next) + } + > + + {auto ? autoLabel : labelFor(value)} + - - - {autoAvailable && ( - - - {t("stream.auto.label")} - - {t("stream.auto.tips")} + + + {autoAvailable && ( + + + {t("stream.auto.label")} + + {t("stream.auto.tips")} + - - - )} - {Object.entries(streams).map(([label, name]) => ( - - {label} - - ))} - - - + + )} + {Object.entries(streams).map(([label, name]) => ( + + {label} + + ))} + + + + )} {auto && autoReason && !disabled && (
diff --git a/web/src/views/live/LiveCameraView.tsx b/web/src/views/live/LiveCameraView.tsx index 0e107f8153..71c8e7a7ee 100644 --- a/web/src/views/live/LiveCameraView.tsx +++ b/web/src/views/live/LiveCameraView.tsx @@ -230,15 +230,16 @@ export default function LiveCameraView({ } }, [streamNameLoaded, liveStreams, pinnedStream, clearPinnedStream]); - const { data: cameraMetadata } = useSWR( - isRestreamed ? `go2rtc/streams/${streamName}` : null, - { - revalidateOnFocus: false, - revalidateOnReconnect: false, - revalidateIfStale: false, - dedupingInterval: 60000, - }, - ); + const { data: cameraMetadata, isLoading: cameraMetadataLoading } = + useSWR( + isRestreamed ? `go2rtc/streams/${streamName}` : null, + { + revalidateOnFocus: false, + revalidateOnReconnect: false, + revalidateIfStale: false, + dedupingInterval: 60000, + }, + ); const webRTCAvailability = useWebRTCAvailableForStream( cameraMetadata, @@ -847,6 +848,7 @@ export default function LiveCameraView({ onResetStream={resetStream} supportsAudioOutput={supportsAudioOutput} supports2WayTalk={supports2WayTalk} + streamMetadataLoading={cameraMetadataLoading} cameraEnabled={cameraEnabled} debug={debug} setDebug={setDebug} @@ -1013,6 +1015,7 @@ type FrigateCameraFeaturesProps = { onResetStream: () => void; supportsAudioOutput: boolean; supports2WayTalk: boolean; + streamMetadataLoading: boolean; cameraEnabled: boolean; debug: boolean; setDebug: (debug: boolean) => void; @@ -1044,6 +1047,7 @@ function FrigateCameraFeatures({ onResetStream, supportsAudioOutput, supports2WayTalk, + streamMetadataLoading, cameraEnabled, debug, setDebug, @@ -1440,7 +1444,12 @@ function FrigateCameraFeatures({ !debug && isRestreamed && (
- {supportsAudioOutput ? ( + {streamMetadataLoading ? ( + <> + +
{t("stream.metadata.loading")}
+ + ) : supportsAudioOutput ? ( <>
{t("stream.audio.available")}
@@ -1699,72 +1708,7 @@ function FrigateCameraFeatures({
<> - {isAdmin && ( - <> - - sendEnabled(enabledState == "ON" ? "OFF" : "ON") - } - /> - - sendDetect(detectState == "ON" ? "OFF" : "ON") - } - /> - {recordingEnabled && ( - - sendRecord(recordState == "ON" ? "OFF" : "ON") - } - /> - )} - - sendSnapshot(snapshotState == "ON" ? "OFF" : "ON") - } - /> - {audioDetectEnabled && ( - - sendAudio(audioState == "ON" ? "OFF" : "ON") - } - /> - )} - {audioDetectEnabled && transcriptionEnabled && ( - - sendTranscription( - transcriptionState == "ON" ? "OFF" : "ON", - ) - } - /> - )} - {autotrackingEnabled && ( - - sendAutotracking(autotrackingState == "ON" ? "OFF" : "ON") - } - /> - )} - - )} - -
+
{!isRestreamed && (
@@ -1817,6 +1761,7 @@ function FrigateCameraFeatures({ onSelect={onSelectStream} onRetry={onResetStream} disabled={debug || forceLowBandwidth} + variant="grid" /> {debug && ( @@ -1832,7 +1777,12 @@ function FrigateCameraFeatures({ !debug && isRestreamed && (
- {supportsAudioOutput ? ( + {streamMetadataLoading ? ( + <> + +
{t("stream.metadata.loading")}
+ + ) : supportsAudioOutput ? ( <>
{t("stream.audio.available")}
@@ -1961,6 +1911,73 @@ function FrigateCameraFeatures({

)} +
+ {isAdmin && ( + <> + + sendEnabled(enabledState == "ON" ? "OFF" : "ON") + } + /> + + sendDetect(detectState == "ON" ? "OFF" : "ON") + } + /> + {recordingEnabled && ( + + sendRecord(recordState == "ON" ? "OFF" : "ON") + } + /> + )} + + sendSnapshot(snapshotState == "ON" ? "OFF" : "ON") + } + /> + {audioDetectEnabled && ( + + sendAudio(audioState == "ON" ? "OFF" : "ON") + } + /> + )} + {audioDetectEnabled && transcriptionEnabled && ( + + sendTranscription( + transcriptionState == "ON" ? "OFF" : "ON", + ) + } + /> + )} + {autotrackingEnabled && ( + + sendAutotracking(autotrackingState == "ON" ? "OFF" : "ON") + } + /> + )} + + )} + +
{isRestreamed && Object.values(camera.live.streams).length > 0 && (