mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-11-14 01:23:01 +03:00
Compare commits
3
Commits
dev
...
improve-live
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
746b733d8b | ||
|
|
3575049cd7 | ||
|
|
c1ecdd80cc |
@@ -173,6 +173,9 @@
|
|||||||
"label": "Play in background",
|
"label": "Play in background",
|
||||||
"tips": "Enable this option to continue streaming when the player is hidden."
|
"tips": "Enable this option to continue streaming when the player is hidden."
|
||||||
},
|
},
|
||||||
|
"metadata": {
|
||||||
|
"loading": "Fetching stream metadata…"
|
||||||
|
},
|
||||||
"mode": "Streaming Technology",
|
"mode": "Streaming Technology",
|
||||||
"technology": {
|
"technology": {
|
||||||
"description": "Choose your preferred streaming technology. Frigate may still fall back to low bandwidth mode on playback or network errors.",
|
"description": "Choose your preferred streaming technology. Frigate may still fall back to low bandwidth mode on playback or network errors.",
|
||||||
|
|||||||
@@ -248,10 +248,21 @@ export default function LivePlayer({
|
|||||||
// remounts them in the same render and the new player is hidden until it
|
// remounts them in the same render and the new player is hidden until it
|
||||||
// plays. jsmpeg plays the camera, not the stream, and keeps playing
|
// plays. jsmpeg plays the camera, not the stream, and keeps playing
|
||||||
const [renderedStream, setRenderedStream] = useState(streamName);
|
const [renderedStream, setRenderedStream] = useState(streamName);
|
||||||
|
const [switchFrame, setSwitchFrame] = useState<HTMLCanvasElement>();
|
||||||
if (renderedStream !== streamName) {
|
if (renderedStream !== streamName) {
|
||||||
setRenderedStream(streamName);
|
setRenderedStream(streamName);
|
||||||
if (preferredLiveMode !== "jsmpeg") {
|
if (preferredLiveMode !== "jsmpeg") {
|
||||||
setLiveReady(false);
|
setLiveReady(false);
|
||||||
|
|
||||||
|
// the old video is still in the DOM until this render commits, so its
|
||||||
|
// last frame can cover the gap until the new stream plays. Tiles that
|
||||||
|
// show the camera still already cover it
|
||||||
|
if (!showStillWithoutActivity) {
|
||||||
|
const frame = captureVideoFrame(overlayRef.current);
|
||||||
|
if (frame) {
|
||||||
|
setSwitchFrame(frame);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,6 +274,7 @@ export default function LivePlayer({
|
|||||||
|
|
||||||
const playerIsPlaying = useCallback(() => {
|
const playerIsPlaying = useCallback(() => {
|
||||||
setLiveReady(true);
|
setLiveReady(true);
|
||||||
|
setSwitchFrame(undefined);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// enabled states
|
// enabled states
|
||||||
@@ -416,6 +428,12 @@ export default function LivePlayer({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{player}
|
{player}
|
||||||
|
{switchFrame && !liveReady && (
|
||||||
|
<div
|
||||||
|
className="absolute inset-0"
|
||||||
|
ref={(node) => node?.replaceChildren(switchFrame)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{cameraEnabled &&
|
{cameraEnabled &&
|
||||||
!offline &&
|
!offline &&
|
||||||
(!showStillWithoutActivity || isReEnabling) &&
|
(!showStillWithoutActivity || isReEnabling) &&
|
||||||
@@ -582,3 +600,19 @@ export default function LivePlayer({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function captureVideoFrame(
|
||||||
|
container: HTMLElement | null,
|
||||||
|
): HTMLCanvasElement | undefined {
|
||||||
|
const video = container?.querySelector("video");
|
||||||
|
if (!video || video.readyState < HTMLMediaElement.HAVE_CURRENT_DATA) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const canvas = document.createElement("canvas");
|
||||||
|
canvas.width = video.videoWidth;
|
||||||
|
canvas.height = video.videoHeight;
|
||||||
|
canvas.className = "size-full object-contain";
|
||||||
|
canvas.getContext("2d")?.drawImage(video, 0, 0);
|
||||||
|
return canvas;
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,11 +12,15 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { LuInfo } from "react-icons/lu";
|
import { LuInfo } from "react-icons/lu";
|
||||||
import { MdOutlineRestartAlt } from "react-icons/md";
|
import { MdOutlineRestartAlt } from "react-icons/md";
|
||||||
import { Button } from "@/components/ui/button";
|
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
|
// Radix Select needs a non-empty value. Auto itself is stored as the
|
||||||
// absence of a pinned stream
|
// absence of a pinned stream
|
||||||
const AUTO_VALUE = "__frigate_auto__";
|
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 = {
|
type LiveStreamSelectProps = {
|
||||||
// friendly name to go2rtc stream name, in ladder order
|
// friendly name to go2rtc stream name, in ladder order
|
||||||
streams: Record<string, string>;
|
streams: Record<string, string>;
|
||||||
@@ -29,6 +33,7 @@ type LiveStreamSelectProps = {
|
|||||||
// sends auto back to the top of the ladder
|
// sends auto back to the top of the ladder
|
||||||
onRetry: () => void;
|
onRetry: () => void;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
|
variant?: "select" | "grid";
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function LiveStreamSelect({
|
export default function LiveStreamSelect({
|
||||||
@@ -40,6 +45,7 @@ export default function LiveStreamSelect({
|
|||||||
onSelect,
|
onSelect,
|
||||||
onRetry,
|
onRetry,
|
||||||
disabled,
|
disabled,
|
||||||
|
variant = "select",
|
||||||
}: LiveStreamSelectProps) {
|
}: LiveStreamSelectProps) {
|
||||||
const { t } = useTranslation(["views/live"]);
|
const { t } = useTranslation(["views/live"]);
|
||||||
|
|
||||||
@@ -52,48 +58,78 @@ export default function LiveStreamSelect({
|
|||||||
const auto = autoAvailable && pinnedStream == undefined;
|
const auto = autoAvailable && pinnedStream == undefined;
|
||||||
const value = auto ? AUTO_VALUE : (pinnedStream ?? playingStream);
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Select
|
{variant === "grid" &&
|
||||||
value={value}
|
!autoAvailable &&
|
||||||
disabled={disabled}
|
Object.keys(streams).length <= 1 ? (
|
||||||
onValueChange={(next) =>
|
<div className="text-sm">{labelFor(playingStream)}</div>
|
||||||
onSelect(next === AUTO_VALUE ? undefined : next)
|
) : variant === "grid" ? (
|
||||||
}
|
<ToggleGroup
|
||||||
>
|
type="single"
|
||||||
<SelectTrigger className="w-full">
|
className="flex flex-wrap items-stretch gap-2"
|
||||||
<SelectValue>
|
value={value}
|
||||||
{auto
|
disabled={disabled}
|
||||||
? t("stream.auto.selected", {
|
onValueChange={(next) => {
|
||||||
stream:
|
// radix sends an empty value when the active item is tapped again
|
||||||
autoReason === "floor"
|
if (next) {
|
||||||
? t("stream.auto.floorLabel")
|
onSelect(next === AUTO_VALUE ? undefined : next);
|
||||||
: labelFor(playingStream),
|
}
|
||||||
})
|
}}
|
||||||
: labelFor(value)}
|
>
|
||||||
</SelectValue>
|
{autoAvailable && (
|
||||||
</SelectTrigger>
|
<ToggleGroupItem value={AUTO_VALUE} className={GRID_ITEM}>
|
||||||
|
{autoLabel}
|
||||||
|
</ToggleGroupItem>
|
||||||
|
)}
|
||||||
|
{Object.entries(streams).map(([label, name]) => (
|
||||||
|
<ToggleGroupItem key={label} value={name} className={GRID_ITEM}>
|
||||||
|
{label}
|
||||||
|
</ToggleGroupItem>
|
||||||
|
))}
|
||||||
|
</ToggleGroup>
|
||||||
|
) : (
|
||||||
|
<Select
|
||||||
|
value={value}
|
||||||
|
disabled={disabled}
|
||||||
|
onValueChange={(next) =>
|
||||||
|
onSelect(next === AUTO_VALUE ? undefined : next)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-full">
|
||||||
|
<SelectValue>{auto ? autoLabel : labelFor(value)}</SelectValue>
|
||||||
|
</SelectTrigger>
|
||||||
|
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectGroup>
|
<SelectGroup>
|
||||||
{autoAvailable && (
|
{autoAvailable && (
|
||||||
<SelectItem className="cursor-pointer" value={AUTO_VALUE}>
|
<SelectItem className="cursor-pointer" value={AUTO_VALUE}>
|
||||||
<span className="flex flex-col gap-0.5 whitespace-normal">
|
<span className="flex flex-col gap-0.5 whitespace-normal">
|
||||||
<span>{t("stream.auto.label")}</span>
|
<span>{t("stream.auto.label")}</span>
|
||||||
<span className="text-xs text-muted-foreground">
|
<span className="text-xs text-muted-foreground">
|
||||||
{t("stream.auto.tips")}
|
{t("stream.auto.tips")}
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</SelectItem>
|
||||||
</SelectItem>
|
)}
|
||||||
)}
|
{Object.entries(streams).map(([label, name]) => (
|
||||||
{Object.entries(streams).map(([label, name]) => (
|
<SelectItem key={label} className="cursor-pointer" value={name}>
|
||||||
<SelectItem key={label} className="cursor-pointer" value={name}>
|
{label}
|
||||||
{label}
|
</SelectItem>
|
||||||
</SelectItem>
|
))}
|
||||||
))}
|
</SelectGroup>
|
||||||
</SelectGroup>
|
</SelectContent>
|
||||||
</SelectContent>
|
</Select>
|
||||||
</Select>
|
)}
|
||||||
|
|
||||||
{auto && autoReason && !disabled && (
|
{auto && autoReason && !disabled && (
|
||||||
<div className="flex flex-col items-center gap-3">
|
<div className="flex flex-col items-center gap-3">
|
||||||
|
|||||||
@@ -230,15 +230,16 @@ export default function LiveCameraView({
|
|||||||
}
|
}
|
||||||
}, [streamNameLoaded, liveStreams, pinnedStream, clearPinnedStream]);
|
}, [streamNameLoaded, liveStreams, pinnedStream, clearPinnedStream]);
|
||||||
|
|
||||||
const { data: cameraMetadata } = useSWR<LiveStreamMetadata>(
|
const { data: cameraMetadata, isLoading: cameraMetadataLoading } =
|
||||||
isRestreamed ? `go2rtc/streams/${streamName}` : null,
|
useSWR<LiveStreamMetadata>(
|
||||||
{
|
isRestreamed ? `go2rtc/streams/${streamName}` : null,
|
||||||
revalidateOnFocus: false,
|
{
|
||||||
revalidateOnReconnect: false,
|
revalidateOnFocus: false,
|
||||||
revalidateIfStale: false,
|
revalidateOnReconnect: false,
|
||||||
dedupingInterval: 60000,
|
revalidateIfStale: false,
|
||||||
},
|
dedupingInterval: 60000,
|
||||||
);
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const webRTCAvailability = useWebRTCAvailableForStream(
|
const webRTCAvailability = useWebRTCAvailableForStream(
|
||||||
cameraMetadata,
|
cameraMetadata,
|
||||||
@@ -847,6 +848,7 @@ export default function LiveCameraView({
|
|||||||
onResetStream={resetStream}
|
onResetStream={resetStream}
|
||||||
supportsAudioOutput={supportsAudioOutput}
|
supportsAudioOutput={supportsAudioOutput}
|
||||||
supports2WayTalk={supports2WayTalk}
|
supports2WayTalk={supports2WayTalk}
|
||||||
|
streamMetadataLoading={cameraMetadataLoading}
|
||||||
cameraEnabled={cameraEnabled}
|
cameraEnabled={cameraEnabled}
|
||||||
debug={debug}
|
debug={debug}
|
||||||
setDebug={setDebug}
|
setDebug={setDebug}
|
||||||
@@ -873,9 +875,12 @@ export default function LiveCameraView({
|
|||||||
growClassName,
|
growClassName,
|
||||||
)}
|
)}
|
||||||
ref={clickOverlayRef}
|
ref={clickOverlayRef}
|
||||||
style={{
|
style={
|
||||||
aspectRatio: constrainedAspectRatio,
|
{
|
||||||
}}
|
aspectRatio: constrainedAspectRatio,
|
||||||
|
"--camera-aspect": cameraAspectRatio,
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{clickOverlay && overlaySize.width > 0 && (
|
{clickOverlay && overlaySize.width > 0 && (
|
||||||
<div
|
<div
|
||||||
@@ -914,7 +919,10 @@ export default function LiveCameraView({
|
|||||||
)}
|
)}
|
||||||
<LivePlayer
|
<LivePlayer
|
||||||
key={camera.name}
|
key={camera.name}
|
||||||
className={fullscreen ? "rounded-none" : ""}
|
className={cn(
|
||||||
|
"aspect-[var(--camera-aspect)]",
|
||||||
|
fullscreen && "rounded-none",
|
||||||
|
)}
|
||||||
windowVisible
|
windowVisible
|
||||||
showStillWithoutActivity={false}
|
showStillWithoutActivity={false}
|
||||||
alwaysShowCameraName={false}
|
alwaysShowCameraName={false}
|
||||||
@@ -1007,6 +1015,7 @@ type FrigateCameraFeaturesProps = {
|
|||||||
onResetStream: () => void;
|
onResetStream: () => void;
|
||||||
supportsAudioOutput: boolean;
|
supportsAudioOutput: boolean;
|
||||||
supports2WayTalk: boolean;
|
supports2WayTalk: boolean;
|
||||||
|
streamMetadataLoading: boolean;
|
||||||
cameraEnabled: boolean;
|
cameraEnabled: boolean;
|
||||||
debug: boolean;
|
debug: boolean;
|
||||||
setDebug: (debug: boolean) => void;
|
setDebug: (debug: boolean) => void;
|
||||||
@@ -1038,6 +1047,7 @@ function FrigateCameraFeatures({
|
|||||||
onResetStream,
|
onResetStream,
|
||||||
supportsAudioOutput,
|
supportsAudioOutput,
|
||||||
supports2WayTalk,
|
supports2WayTalk,
|
||||||
|
streamMetadataLoading,
|
||||||
cameraEnabled,
|
cameraEnabled,
|
||||||
debug,
|
debug,
|
||||||
setDebug,
|
setDebug,
|
||||||
@@ -1434,7 +1444,12 @@ function FrigateCameraFeatures({
|
|||||||
!debug &&
|
!debug &&
|
||||||
isRestreamed && (
|
isRestreamed && (
|
||||||
<div className="flex flex-row items-center gap-1 text-sm text-muted-foreground">
|
<div className="flex flex-row items-center gap-1 text-sm text-muted-foreground">
|
||||||
{supportsAudioOutput ? (
|
{streamMetadataLoading ? (
|
||||||
|
<>
|
||||||
|
<ActivityIndicator className="size-4" />
|
||||||
|
<div>{t("stream.metadata.loading")}</div>
|
||||||
|
</>
|
||||||
|
) : supportsAudioOutput ? (
|
||||||
<>
|
<>
|
||||||
<LuCheck className="size-4 text-success" />
|
<LuCheck className="size-4 text-success" />
|
||||||
<div>{t("stream.audio.available")}</div>
|
<div>{t("stream.audio.available")}</div>
|
||||||
@@ -1693,72 +1708,7 @@ function FrigateCameraFeatures({
|
|||||||
<DrawerContent className="max-h-[75dvh] overflow-hidden rounded-2xl">
|
<DrawerContent className="max-h-[75dvh] overflow-hidden rounded-2xl">
|
||||||
<div className="scrollbar-container mt-2 flex h-auto flex-col gap-2 overflow-y-auto px-2 py-4">
|
<div className="scrollbar-container mt-2 flex h-auto flex-col gap-2 overflow-y-auto px-2 py-4">
|
||||||
<>
|
<>
|
||||||
{isAdmin && (
|
<div className="mb-3 flex flex-col gap-5">
|
||||||
<>
|
|
||||||
<FilterSwitch
|
|
||||||
label={t("cameraSettings.camera")}
|
|
||||||
isChecked={enabledState == "ON"}
|
|
||||||
onCheckedChange={() =>
|
|
||||||
sendEnabled(enabledState == "ON" ? "OFF" : "ON")
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<FilterSwitch
|
|
||||||
label={t("cameraSettings.objectDetection")}
|
|
||||||
isChecked={detectState == "ON"}
|
|
||||||
onCheckedChange={() =>
|
|
||||||
sendDetect(detectState == "ON" ? "OFF" : "ON")
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{recordingEnabled && (
|
|
||||||
<FilterSwitch
|
|
||||||
label={t("cameraSettings.recording")}
|
|
||||||
isChecked={recordState == "ON"}
|
|
||||||
onCheckedChange={() =>
|
|
||||||
sendRecord(recordState == "ON" ? "OFF" : "ON")
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<FilterSwitch
|
|
||||||
label={t("cameraSettings.snapshots")}
|
|
||||||
isChecked={snapshotState == "ON"}
|
|
||||||
onCheckedChange={() =>
|
|
||||||
sendSnapshot(snapshotState == "ON" ? "OFF" : "ON")
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{audioDetectEnabled && (
|
|
||||||
<FilterSwitch
|
|
||||||
label={t("cameraSettings.audioDetection")}
|
|
||||||
isChecked={audioState == "ON"}
|
|
||||||
onCheckedChange={() =>
|
|
||||||
sendAudio(audioState == "ON" ? "OFF" : "ON")
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{audioDetectEnabled && transcriptionEnabled && (
|
|
||||||
<FilterSwitch
|
|
||||||
label={t("cameraSettings.transcription")}
|
|
||||||
disabled={audioState == "OFF"}
|
|
||||||
isChecked={transcriptionState == "ON"}
|
|
||||||
onCheckedChange={() =>
|
|
||||||
sendTranscription(
|
|
||||||
transcriptionState == "ON" ? "OFF" : "ON",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{autotrackingEnabled && (
|
|
||||||
<FilterSwitch
|
|
||||||
label={t("cameraSettings.autotracking")}
|
|
||||||
isChecked={autotrackingState == "ON"}
|
|
||||||
onCheckedChange={() =>
|
|
||||||
sendAutotracking(autotrackingState == "ON" ? "OFF" : "ON")
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="mt-3 flex flex-col gap-5">
|
|
||||||
{!isRestreamed && (
|
{!isRestreamed && (
|
||||||
<div className="flex flex-col gap-2 p-2">
|
<div className="flex flex-col gap-2 p-2">
|
||||||
<Label>{t("stream.title")}</Label>
|
<Label>{t("stream.title")}</Label>
|
||||||
@@ -1811,6 +1761,7 @@ function FrigateCameraFeatures({
|
|||||||
onSelect={onSelectStream}
|
onSelect={onSelectStream}
|
||||||
onRetry={onResetStream}
|
onRetry={onResetStream}
|
||||||
disabled={debug || forceLowBandwidth}
|
disabled={debug || forceLowBandwidth}
|
||||||
|
variant="grid"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{debug && (
|
{debug && (
|
||||||
@@ -1826,7 +1777,12 @@ function FrigateCameraFeatures({
|
|||||||
!debug &&
|
!debug &&
|
||||||
isRestreamed && (
|
isRestreamed && (
|
||||||
<div className="mt-1 flex flex-row items-center gap-1 text-sm text-muted-foreground">
|
<div className="mt-1 flex flex-row items-center gap-1 text-sm text-muted-foreground">
|
||||||
{supportsAudioOutput ? (
|
{streamMetadataLoading ? (
|
||||||
|
<>
|
||||||
|
<ActivityIndicator className="size-4" size={14} />
|
||||||
|
<div>{t("stream.metadata.loading")}</div>
|
||||||
|
</>
|
||||||
|
) : supportsAudioOutput ? (
|
||||||
<>
|
<>
|
||||||
<LuCheck className="size-4 text-success" />
|
<LuCheck className="size-4 text-success" />
|
||||||
<div>{t("stream.audio.available")}</div>
|
<div>{t("stream.audio.available")}</div>
|
||||||
@@ -1955,6 +1911,73 @@ function FrigateCameraFeatures({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
{isAdmin && (
|
||||||
|
<>
|
||||||
|
<FilterSwitch
|
||||||
|
label={t("cameraSettings.camera")}
|
||||||
|
isChecked={enabledState == "ON"}
|
||||||
|
onCheckedChange={() =>
|
||||||
|
sendEnabled(enabledState == "ON" ? "OFF" : "ON")
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<FilterSwitch
|
||||||
|
label={t("cameraSettings.objectDetection")}
|
||||||
|
isChecked={detectState == "ON"}
|
||||||
|
onCheckedChange={() =>
|
||||||
|
sendDetect(detectState == "ON" ? "OFF" : "ON")
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{recordingEnabled && (
|
||||||
|
<FilterSwitch
|
||||||
|
label={t("cameraSettings.recording")}
|
||||||
|
isChecked={recordState == "ON"}
|
||||||
|
onCheckedChange={() =>
|
||||||
|
sendRecord(recordState == "ON" ? "OFF" : "ON")
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<FilterSwitch
|
||||||
|
label={t("cameraSettings.snapshots")}
|
||||||
|
isChecked={snapshotState == "ON"}
|
||||||
|
onCheckedChange={() =>
|
||||||
|
sendSnapshot(snapshotState == "ON" ? "OFF" : "ON")
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{audioDetectEnabled && (
|
||||||
|
<FilterSwitch
|
||||||
|
label={t("cameraSettings.audioDetection")}
|
||||||
|
isChecked={audioState == "ON"}
|
||||||
|
onCheckedChange={() =>
|
||||||
|
sendAudio(audioState == "ON" ? "OFF" : "ON")
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{audioDetectEnabled && transcriptionEnabled && (
|
||||||
|
<FilterSwitch
|
||||||
|
label={t("cameraSettings.transcription")}
|
||||||
|
disabled={audioState == "OFF"}
|
||||||
|
isChecked={transcriptionState == "ON"}
|
||||||
|
onCheckedChange={() =>
|
||||||
|
sendTranscription(
|
||||||
|
transcriptionState == "ON" ? "OFF" : "ON",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{autotrackingEnabled && (
|
||||||
|
<FilterSwitch
|
||||||
|
label={t("cameraSettings.autotracking")}
|
||||||
|
isChecked={autotrackingState == "ON"}
|
||||||
|
onCheckedChange={() =>
|
||||||
|
sendAutotracking(autotrackingState == "ON" ? "OFF" : "ON")
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mt-3 flex flex-col gap-5">
|
||||||
{isRestreamed &&
|
{isRestreamed &&
|
||||||
Object.values(camera.live.streams).length > 0 && (
|
Object.values(camera.live.streams).length > 0 && (
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
|
|||||||
Reference in New Issue
Block a user