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
+4 -2
View File
@@ -101,12 +101,14 @@ function Live() {
) {
const group = config.camera_groups[cameraGroup];
return Object.values(config.cameras)
.filter((conf) => conf.enabled && group.cameras.includes(conf.name))
.filter(
(conf) => conf.enabled_in_config && group.cameras.includes(conf.name),
)
.sort((aConf, bConf) => aConf.ui.order - bConf.ui.order);
}
return Object.values(config.cameras)
.filter((conf) => conf.ui.dashboard && conf.enabled)
.filter((conf) => conf.ui.dashboard && conf.enabled_in_config)
.sort((aConf, bConf) => aConf.ui.order - bConf.ui.order);
}, [config, cameraGroup]);
+66 -18
View File
@@ -39,6 +39,7 @@ import SearchSettingsView from "@/views/settings/SearchSettingsView";
import UiSettingsView from "@/views/settings/UiSettingsView";
import { useSearchEffect } from "@/hooks/use-overlay-state";
import { useSearchParams } from "react-router-dom";
import { useInitialCameraState } from "@/api/ws";
const allSettingsViews = [
"UI settings",
@@ -71,12 +72,33 @@ export default function Settings() {
}
return Object.values(config.cameras)
.filter((conf) => conf.ui.dashboard && conf.enabled)
.filter((conf) => conf.ui.dashboard && conf.enabled_in_config)
.sort((aConf, bConf) => aConf.ui.order - bConf.ui.order);
}, [config]);
const [selectedCamera, setSelectedCamera] = useState<string>("");
const { payload: allCameraStates } = useInitialCameraState(
cameras.length > 0 ? cameras[0].name : "",
true,
);
const cameraEnabledStates = useMemo(() => {
const states: Record<string, boolean> = {};
if (allCameraStates) {
Object.entries(allCameraStates).forEach(([camName, state]) => {
states[camName] = state.config?.enabled ?? false;
});
}
// fallback to config if ws data isnt available yet
cameras.forEach((cam) => {
if (!(cam.name in states)) {
states[cam.name] = cam.enabled;
}
});
return states;
}, [allCameraStates, cameras]);
const [filterZoneMask, setFilterZoneMask] = useState<PolygonType[]>();
const handleDialog = useCallback(
@@ -91,10 +113,25 @@ export default function Settings() {
);
useEffect(() => {
if (cameras.length > 0 && selectedCamera === "") {
setSelectedCamera(cameras[0].name);
if (cameras.length > 0) {
if (!selectedCamera) {
// Set to first enabled camera initially if no selection
const firstEnabledCamera =
cameras.find((cam) => cameraEnabledStates[cam.name]) || cameras[0];
setSelectedCamera(firstEnabledCamera.name);
} else if (
!cameraEnabledStates[selectedCamera] &&
page !== "camera settings"
) {
// Switch to first enabled camera if current one is disabled, unless on "camera settings" page
const firstEnabledCamera =
cameras.find((cam) => cameraEnabledStates[cam.name]) || cameras[0];
if (firstEnabledCamera.name !== selectedCamera) {
setSelectedCamera(firstEnabledCamera.name);
}
}
}
}, [cameras, selectedCamera]);
}, [cameras, selectedCamera, cameraEnabledStates, page]);
useEffect(() => {
if (tabsRef.current) {
@@ -177,6 +214,8 @@ export default function Settings() {
allCameras={cameras}
selectedCamera={selectedCamera}
setSelectedCamera={setSelectedCamera}
cameraEnabledStates={cameraEnabledStates}
currentPage={page}
/>
</div>
)}
@@ -244,17 +283,21 @@ type CameraSelectButtonProps = {
allCameras: CameraConfig[];
selectedCamera: string;
setSelectedCamera: React.Dispatch<React.SetStateAction<string>>;
cameraEnabledStates: Record<string, boolean>;
currentPage: SettingsType;
};
function CameraSelectButton({
allCameras,
selectedCamera,
setSelectedCamera,
cameraEnabledStates,
currentPage,
}: CameraSelectButtonProps) {
const [open, setOpen] = useState(false);
if (!allCameras.length) {
return;
return null;
}
const trigger = (
@@ -283,19 +326,24 @@ function CameraSelectButton({
)}
<div className="scrollbar-container mb-5 h-auto max-h-[80dvh] overflow-y-auto overflow-x-hidden p-4 md:mb-1">
<div className="flex flex-col gap-2.5">
{allCameras.map((item) => (
<FilterSwitch
key={item.name}
isChecked={item.name === selectedCamera}
label={item.name.replaceAll("_", " ")}
onCheckedChange={(isChecked) => {
if (isChecked) {
setSelectedCamera(item.name);
setOpen(false);
}
}}
/>
))}
{allCameras.map((item) => {
const isEnabled = cameraEnabledStates[item.name];
const isCameraSettingsPage = currentPage === "camera settings";
return (
<FilterSwitch
key={item.name}
isChecked={item.name === selectedCamera}
label={item.name.replaceAll("_", " ")}
onCheckedChange={(isChecked) => {
if (isChecked && (isEnabled || isCameraSettingsPage)) {
setSelectedCamera(item.name);
setOpen(false);
}
}}
disabled={!isEnabled && !isCameraSettingsPage}
/>
);
})}
</div>
</div>
</>