a1090a5f4c
Phase 4a deliverable (no filter rendering yet — это Phase 4b).
End-to-end pipeline: HA/HTTP/MQTT → controller → ZMQ → FFmpeg (logged).
Modules:
- overlays.py — 7 discriminated union types через pydantic:
rect, text, icon, image, dim, graph, chat. Normalized coords (0.0-1.0),
optional cell binding, z_order, opacity, visible.
- state.py — overlay storage per instance (CRUD: add/remove/update/get/clear)
- dispatch.py — overlay.add/remove/clear actions:
- parses JSON payload в Overlay через TypeAdapter
- serializes to ZMQ string: "<id> <type> <full-json>"
- sends via FFmpeg process_command (filter will парсить в Phase 4b)
- updates state + publishes events (overlay_added, overlay_removed, overlays_cleared)
- http_api.py — REST endpoints:
- POST /overlay/{inst}/add (body = Overlay JSON, returns id)
- GET /overlay/{inst} — list all
- DELETE /overlay/{inst}/{id} — single
- DELETE /overlay/{inst} — clear all
- PATCH /overlay/{inst}/{id} — update
- mqtt_loop.py — already subscribes cuda_grid/cmd/<inst>/+/+; teper handles
overlay/add (JSON payload), overlay/remove (id), overlay/clear
- frigate_bridge.py — FrigateBridge skeleton:
- subscribe frigate/+/motion + frigate/events
- mapping camera_name → target_instance + cell index
- Phase 4a: log received events (rendering в Phase 4b)
- config.py — frigate: optional section
- examples/controller.yaml — frigate mappings для 4 наших камер
State management:
- ControllerState.add/remove/update/get/clear_overlay (asyncio.Lock guarded)
- InstanceState.overlays: dict[str, Overlay]
- IDs generated via uuid4()[:8]
Phase 4a limitations:
- Filter side ничего не рендерит (just logs ZMQ commands)
- Frigate bridge принимает events но не auto-generates overlays
- HA Discovery не имеет overlay-specific entities (overlays через REST API)
Phase 4b: filter-side AVFrame side data + CUDA kernels (rect first, NPP-based,
потом text via freetype atlas, потом icon sprite blit).
77 lines
2.6 KiB
Python
77 lines
2.6 KiB
Python
"""In-memory state controller'а."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import asyncio
|
||
from dataclasses import dataclass, field
|
||
|
||
from .overlays import Overlay
|
||
|
||
|
||
@dataclass
|
||
class InstanceState:
|
||
name: str
|
||
active_layout: str
|
||
overlays: dict[str, Overlay] = field(default_factory=dict)
|
||
# Future: fps_out, dropped_frames, motion_cameras, last_event_ts
|
||
|
||
|
||
@dataclass
|
||
class ControllerState:
|
||
instances: dict[str, InstanceState] = field(default_factory=dict)
|
||
_lock: asyncio.Lock = field(default_factory=asyncio.Lock)
|
||
|
||
async def set_layout(self, instance: str, layout: str) -> None:
|
||
async with self._lock:
|
||
st = self.instances.get(instance)
|
||
if st is None:
|
||
st = InstanceState(name=instance, active_layout=layout)
|
||
self.instances[instance] = st
|
||
else:
|
||
st.active_layout = layout
|
||
|
||
async def get_layout(self, instance: str) -> str | None:
|
||
async with self._lock:
|
||
st = self.instances.get(instance)
|
||
return st.active_layout if st else None
|
||
|
||
# ─── Overlay state ──────────────────────────────────────────────
|
||
|
||
async def add_overlay(self, instance: str, overlay: Overlay) -> str:
|
||
async with self._lock:
|
||
st = self.instances.get(instance)
|
||
if st is None:
|
||
raise KeyError(f"unknown instance '{instance}'")
|
||
st.overlays[overlay.id] = overlay
|
||
return overlay.id
|
||
|
||
async def remove_overlay(self, instance: str, overlay_id: str) -> bool:
|
||
async with self._lock:
|
||
st = self.instances.get(instance)
|
||
if st is None or overlay_id not in st.overlays:
|
||
return False
|
||
del st.overlays[overlay_id]
|
||
return True
|
||
|
||
async def update_overlay(self, instance: str, overlay: Overlay) -> bool:
|
||
async with self._lock:
|
||
st = self.instances.get(instance)
|
||
if st is None or overlay.id not in st.overlays:
|
||
return False
|
||
st.overlays[overlay.id] = overlay
|
||
return True
|
||
|
||
async def get_overlays(self, instance: str) -> list[Overlay]:
|
||
async with self._lock:
|
||
st = self.instances.get(instance)
|
||
return list(st.overlays.values()) if st else []
|
||
|
||
async def clear_overlays(self, instance: str) -> int:
|
||
async with self._lock:
|
||
st = self.instances.get(instance)
|
||
if st is None:
|
||
return 0
|
||
n = len(st.overlays)
|
||
st.overlays.clear()
|
||
return n
|