diff --git a/VERSION b/VERSION index 8624408..f8ee452 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.0.124 +0.0.126 diff --git a/backend/app/api/cameras.py b/backend/app/api/cameras.py index bb17a73..25a4bc7 100644 --- a/backend/app/api/cameras.py +++ b/backend/app/api/cameras.py @@ -33,6 +33,7 @@ def _camera_from_body(body: dict, cam_id: str) -> Camera: id=cam_id, name=(body.get("name") or cam_id).strip(), enabled=bool(body.get("enabled", True)), + record=bool(body.get("record", True)), ip=ip, user=user, password=password, @@ -50,6 +51,7 @@ def _camera_payload(cam_id: str) -> dict: "id": cam.id, "name": cam.name, "enabled": cam.enabled, + "record": cam.record, "ip": cam.ip, "user": cam.user, "password": cam.password, @@ -255,7 +257,8 @@ async def create_camera(request: Request) -> dict: runtime.config.cameras.append(cam) save_config(runtime.config) runtime.supervisor.add_camera(cam) - await go2rtc.add_camera_streams(runtime.config, cam) + if cam.enabled: + await go2rtc.add_camera_streams(runtime.config, cam) return _camera_payload(cam_id) @@ -270,7 +273,10 @@ async def update_camera(cam_id: str, request: Request) -> dict: runtime.config.cameras[idx] = cam save_config(runtime.config) await runtime.supervisor.replace_camera(cam) - await go2rtc.add_camera_streams(runtime.config, cam) + if cam.enabled: + await go2rtc.add_camera_streams(runtime.config, cam) + else: + await go2rtc.remove_camera_streams(runtime.config, cam_id) return _camera_payload(cam_id) @@ -301,21 +307,34 @@ async def clear_cameras(request: Request) -> dict: return {"ok": True, "removed": len(ids)} -@router.post("/{cam_id}/record") -async def set_record(cam_id: str, request: Request) -> dict: - """Включает/выключает запись камеры на диск (тумблер REC). +@router.post("/{cam_id}/toggle") +async def toggle_camera(cam_id: str, request: Request) -> dict: + """Тумблеры камеры: enabled (вкл/выкл камеру) и/или record (REC — запись на диск). - enabled=False — запись не идёт, но go2rtc/live не трогаем → просмотр работает. - Меняет только рекордер (без пересоздания go2rtc-потоков, чтобы live не мигал).""" + enabled=False — камера выключена полностью: убираем go2rtc-потоки → нет live и записи + (камера остаётся зарегистрированной). record управляет только записью. go2rtc трогаем + лишь при смене enabled, поэтому переключение REC не мигает live.""" require_role(request, "admin") cam = runtime.config.camera(cam_id) if not cam: raise HTTPException(404, "camera not found") body = await request.json() - on = bool(body.get("enabled", True)) + kw = {} + if "enabled" in body: + kw["enabled"] = bool(body["enabled"]) + if "record" in body: + kw["record"] = bool(body["record"]) + if not kw: + raise HTTPException(400, "ожидается enabled и/или record") + enabled_changed = "enabled" in kw and kw["enabled"] != cam.enabled + new = replace(cam, **kw) idx = next(i for i, c in enumerate(runtime.config.cameras) if c.id == cam_id) - new = replace(cam, enabled=on) runtime.config.cameras[idx] = new save_config(runtime.config) - await runtime.supervisor.replace_camera(new) # старт/стоп записи; go2rtc не трогаем - return {"ok": True, "enabled": on} + if enabled_changed: # потоки go2rtc только при смене вкл/выкл камеры + if new.enabled: + await go2rtc.add_camera_streams(runtime.config, new) + else: + await go2rtc.remove_camera_streams(runtime.config, cam_id) + await runtime.supervisor.replace_camera(new) # рекордер запускается при enabled AND record + return {"ok": True, "enabled": new.enabled, "record": new.record} diff --git a/backend/app/config.py b/backend/app/config.py index 3a4c74d..c9a90ec 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -18,6 +18,7 @@ class Camera: password: str main_path: str sub_path: str | None = None + record: bool = True # REC: писать на диск. enabled — камера вообще включена (live+запись) def rtsp_url(self, path: str) -> str: return f"rtsp://{self.user}:{self.password}@{self.ip}:554{path}" @@ -70,27 +71,37 @@ def load_config(path: str | None = None) -> Config: storage = Storage(**(raw.get("storage") or {})) live = Live(**(raw.get("live") or {})) - cameras = [ - Camera( - id=c["id"], - name=c.get("name", c["id"]), - enabled=c.get("enabled", True), - ip=c["ip"], - user=c["user"], - password=c["password"], - main_path=c["main_path"], - sub_path=c.get("sub_path"), - ) - for c in (raw.get("cameras") or []) - ] + cameras = [_camera_from_raw(c) for c in (raw.get("cameras") or [])] return Config(storage=storage, live=live, cameras=cameras) +def _camera_from_raw(c: dict) -> Camera: + # Миграция со старой схемы: раньше `enabled` означал «писать на диск» (live был всегда), + # отдельного «камера выключена» не существовало. Старый файл (без ключа record) трактуем + # так: запись = старый enabled, а сама камера включена. Новый файл (с record) — как есть. + if "record" in c: + enabled, record = bool(c.get("enabled", True)), bool(c["record"]) + else: + enabled, record = True, bool(c.get("enabled", True)) + return Camera( + id=c["id"], + name=c.get("name", c["id"]), + enabled=enabled, + record=record, + ip=c["ip"], + user=c["user"], + password=c["password"], + main_path=c["main_path"], + sub_path=c.get("sub_path"), + ) + + def camera_to_yaml(c: Camera) -> dict: return { "id": c.id, "name": c.name, "enabled": c.enabled, + "record": c.record, "ip": c.ip, "user": c.user, "password": c.password, diff --git a/backend/app/recorder/supervisor.py b/backend/app/recorder/supervisor.py index 6383dee..38e52a0 100644 --- a/backend/app/recorder/supervisor.py +++ b/backend/app/recorder/supervisor.py @@ -26,11 +26,11 @@ class Supervisor: task = RecorderTask(cam, self.config.storage, on_status=self.on_status, input_url=self._input_url(cam)) self.tasks[cam.id] = task - if cam.enabled: + if cam.enabled and cam.record: task.start() log.info("recorder started: %s", cam.id) else: - log.info("recorder skipped (disabled): %s", cam.id) + log.info("recorder skipped (enabled=%s record=%s): %s", cam.enabled, cam.record, cam.id) async def stop_all(self) -> None: for task in self.tasks.values(): @@ -60,7 +60,7 @@ class Supervisor: task = RecorderTask(cam, self.config.storage, on_status=self.on_status, input_url=self._input_url(cam)) self.tasks[cam.id] = task - if cam.enabled: + if cam.enabled and cam.record: task.start() log.info("recorder added: %s", cam.id) diff --git a/backend/app/services/go2rtc.py b/backend/app/services/go2rtc.py index 5721564..ae8fcd7 100644 --- a/backend/app/services/go2rtc.py +++ b/backend/app/services/go2rtc.py @@ -60,6 +60,8 @@ def _streams_for(cam: Camera) -> dict[str, str]: def build_streams(config: Config) -> dict: streams: dict = {} for c in config.cameras: + if not c.enabled: # выключенная камера — без потоков (ни live, ни записи) + continue for name, src in _streams_for(c).items(): streams[name] = [src] return streams @@ -120,6 +122,8 @@ async def sync_all(config: Config) -> None: write_go2rtc_yaml(config) base = config.live.go2rtc_url.rstrip("/") for cam in config.cameras: + if not cam.enabled: + continue for name, src in _streams_for(cam).items(): await _call("PUT", _put(base, name, src)) @@ -151,6 +155,8 @@ async def reconcile(config: Config) -> int: return 0 added = 0 for cam in config.cameras: + if not cam.enabled: # выключенные камеры в go2rtc не держим + continue for name, src in _streams_for(cam).items(): if name not in existing: await _call("PUT", _put(base, name, src)) diff --git a/frontend/settings.html b/frontend/settings.html index ce9cc47..2ce93d2 100644 --- a/frontend/settings.html +++ b/frontend/settings.html @@ -55,7 +55,7 @@
| ID | Камера | IP | Потоки | Запись | Статус | ||
|---|---|---|---|---|---|---|---|
| ID | Камера | IP | Потоки | Вкл | Запись | Статус |