Dynamically enable/disable cameras (#16894)

* config options

* metrics

* stop and restart ffmpeg processes

* dispatcher

* frontend websocket

* buttons for testing

* don't recreate log pipe

* add/remove cam from birdseye when enabling/disabling

* end all objects and send empty camera activity

* enable/disable switch in ui

* disable buttons when camera is disabled

* use enabled_in_config for some frontend checks

* tweaks

* handle settings pane with disabled cameras

* frontend tweaks

* change to debug log

* mqtt docs

* tweak

* ensure all ffmpeg processes are initially started

* clean up

* use zmq

* remove camera metrics

* remove camera metrics

* tweaks

* frontend tweaks
This commit is contained in:
Josh Hawkins
2025-03-03 08:30:52 -07:00
committed by GitHub
parent 71e6e04d77
commit 531042467a
24 changed files with 713 additions and 202 deletions
+31 -1
View File
@@ -2,6 +2,7 @@ import {
useAudioState,
useAutotrackingState,
useDetectState,
useEnabledState,
usePtzCommand,
useRecordingsState,
useSnapshotsState,
@@ -82,6 +83,8 @@ import {
LuHistory,
LuInfo,
LuPictureInPicture,
LuPower,
LuPowerOff,
LuVideo,
LuVideoOff,
LuX,
@@ -185,6 +188,10 @@ export default function LiveCameraView({
);
}, [cameraMetadata]);
// camera enabled state
const { payload: enabledState } = useEnabledState(camera.name);
const cameraEnabled = enabledState === "ON";
// click overlay for ptzs
const [clickOverlay, setClickOverlay] = useState(false);
@@ -470,6 +477,7 @@ export default function LiveCameraView({
setPip(false);
}
}}
disabled={!cameraEnabled}
/>
)}
{supports2WayTalk && (
@@ -481,11 +489,11 @@ export default function LiveCameraView({
title={`${mic ? "Disable" : "Enable"} Two Way Talk`}
onClick={() => {
setMic(!mic);
// Turn on audio when enabling the mic if audio is currently off
if (!mic && !audio) {
setAudio(true);
}
}}
disabled={!cameraEnabled}
/>
)}
{supportsAudioOutput && preferredLiveMode != "jsmpeg" && (
@@ -496,6 +504,7 @@ export default function LiveCameraView({
isActive={audio ?? false}
title={`${audio ? "Disable" : "Enable"} Camera Audio`}
onClick={() => setAudio(!audio)}
disabled={!cameraEnabled}
/>
)}
<FrigateCameraFeatures
@@ -517,6 +526,7 @@ export default function LiveCameraView({
setLowBandwidth={setLowBandwidth}
supportsAudioOutput={supportsAudioOutput}
supports2WayTalk={supports2WayTalk}
cameraEnabled={cameraEnabled}
/>
</div>
</TooltipProvider>
@@ -913,6 +923,7 @@ type FrigateCameraFeaturesProps = {
setLowBandwidth: React.Dispatch<React.SetStateAction<boolean>>;
supportsAudioOutput: boolean;
supports2WayTalk: boolean;
cameraEnabled: boolean;
};
function FrigateCameraFeatures({
camera,
@@ -931,10 +942,14 @@ function FrigateCameraFeatures({
setLowBandwidth,
supportsAudioOutput,
supports2WayTalk,
cameraEnabled,
}: FrigateCameraFeaturesProps) {
const { payload: detectState, send: sendDetect } = useDetectState(
camera.name,
);
const { payload: enabledState, send: sendEnabled } = useEnabledState(
camera.name,
);
const { payload: recordState, send: sendRecord } = useRecordingsState(
camera.name,
);
@@ -1043,6 +1058,15 @@ function FrigateCameraFeatures({
if (isDesktop || isTablet) {
return (
<>
<CameraFeatureToggle
className="p-2 md:p-0"
variant={fullscreen ? "overlay" : "primary"}
Icon={enabledState == "ON" ? LuPower : LuPowerOff}
isActive={enabledState == "ON"}
title={`${enabledState == "ON" ? "Disable" : "Enable"} Camera`}
onClick={() => sendEnabled(enabledState == "ON" ? "OFF" : "ON")}
disabled={false}
/>
<CameraFeatureToggle
className="p-2 md:p-0"
variant={fullscreen ? "overlay" : "primary"}
@@ -1050,6 +1074,7 @@ function FrigateCameraFeatures({
isActive={detectState == "ON"}
title={`${detectState == "ON" ? "Disable" : "Enable"} Detect`}
onClick={() => sendDetect(detectState == "ON" ? "OFF" : "ON")}
disabled={!cameraEnabled}
/>
<CameraFeatureToggle
className="p-2 md:p-0"
@@ -1058,6 +1083,7 @@ function FrigateCameraFeatures({
isActive={recordState == "ON"}
title={`${recordState == "ON" ? "Disable" : "Enable"} Recording`}
onClick={() => sendRecord(recordState == "ON" ? "OFF" : "ON")}
disabled={!cameraEnabled}
/>
<CameraFeatureToggle
className="p-2 md:p-0"
@@ -1066,6 +1092,7 @@ function FrigateCameraFeatures({
isActive={snapshotState == "ON"}
title={`${snapshotState == "ON" ? "Disable" : "Enable"} Snapshots`}
onClick={() => sendSnapshot(snapshotState == "ON" ? "OFF" : "ON")}
disabled={!cameraEnabled}
/>
{audioDetectEnabled && (
<CameraFeatureToggle
@@ -1075,6 +1102,7 @@ function FrigateCameraFeatures({
isActive={audioState == "ON"}
title={`${audioState == "ON" ? "Disable" : "Enable"} Audio Detect`}
onClick={() => sendAudio(audioState == "ON" ? "OFF" : "ON")}
disabled={!cameraEnabled}
/>
)}
{autotrackingEnabled && (
@@ -1087,6 +1115,7 @@ function FrigateCameraFeatures({
onClick={() =>
sendAutotracking(autotrackingState == "ON" ? "OFF" : "ON")
}
disabled={!cameraEnabled}
/>
)}
<CameraFeatureToggle
@@ -1099,6 +1128,7 @@ function FrigateCameraFeatures({
isActive={isRecording}
title={`${isRecording ? "Stop" : "Start"} on-demand recording`}
onClick={handleEventButtonClick}
disabled={!cameraEnabled}
/>
<DropdownMenu modal={false}>
+28 -1
View File
@@ -29,7 +29,7 @@ import { MdCircle } from "react-icons/md";
import { cn } from "@/lib/utils";
import { Switch } from "@/components/ui/switch";
import { Label } from "@/components/ui/label";
import { useAlertsState, useDetectionsState } from "@/api/ws";
import { useAlertsState, useDetectionsState, useEnabledState } from "@/api/ws";
type CameraSettingsViewProps = {
selectedCamera: string;
@@ -108,6 +108,8 @@ export default function CameraSettingsView({
const watchedAlertsZones = form.watch("alerts_zones");
const watchedDetectionsZones = form.watch("detections_zones");
const { payload: enabledState, send: sendEnabled } =
useEnabledState(selectedCamera);
const { payload: alertsState, send: sendAlerts } =
useAlertsState(selectedCamera);
const { payload: detectionsState, send: sendDetections } =
@@ -252,6 +254,31 @@ export default function CameraSettingsView({
<Separator className="my-2 flex bg-secondary" />
<Heading as="h4" className="my-2">
Streams
</Heading>
<div className="flex flex-row items-center">
<Switch
id="camera-enabled"
className="mr-3"
checked={enabledState === "ON"}
onCheckedChange={(isChecked) => {
sendEnabled(isChecked ? "ON" : "OFF");
}}
/>
<div className="space-y-0.5">
<Label htmlFor="camera-enabled">Enable</Label>
</div>
</div>
<div className="mt-3 text-sm text-muted-foreground">
Disabling a camera completely stops Frigate's processing of this
camera's streams. Detection, recording, and debugging will be
unavailable.
<br /> <em>Note: This does not disable go2rtc restreams.</em>
</div>
<Separator className="mb-2 mt-4 flex bg-secondary" />
<Heading as="h4" className="my-2">
Review
</Heading>
@@ -80,7 +80,7 @@ export default function NotificationView({
return Object.values(config.cameras)
.filter(
(conf) =>
conf.enabled &&
conf.enabled_in_config &&
conf.notifications &&
conf.notifications.enabled_in_config,
)