diff --git a/controller/cuda_grid_controller/frigate_bridge.py b/controller/cuda_grid_controller/frigate_bridge.py index 93f71ab..e3c60ad 100644 --- a/controller/cuda_grid_controller/frigate_bridge.py +++ b/controller/cuda_grid_controller/frigate_bridge.py @@ -17,6 +17,7 @@ from __future__ import annotations import asyncio import json +import time from typing import TYPE_CHECKING import structlog @@ -82,6 +83,15 @@ class FrigateBridgeCfg(BaseModel): auto_hysteresis_sec: float = Field(default=3.0, ge=0.0, le=60.0, description="Debounce — apply layout change только если state стабилен N sec. " "0 = немедленно. Защищает от short motion blips дёргающих layout") + bbox_min_interval_sec: float = Field(default=0.25, ge=0.0, le=5.0, + description="Min interval между bbox update ZMQ commands per event_id. " + "Frigate publishes 5-10 events/sec при motion → throttle reduces " + "render-thread pause (см. cuda-grid-filter-stutter memory). " + "0 = no throttle.") + bbox_delta_threshold: float = Field(default=0.02, ge=0.0, le=1.0, + description="Min relative bbox shift (fraction of camera dim) для new update. " + "Skip update если все coords changed < threshold от last. " + "0.02 = 2% → 38px на 1920 cam. 0 = no delta filter.") class FrigateBridge: @@ -98,6 +108,8 @@ class FrigateBridge: self._by_camera = {m.frigate_camera: m for m in cfg.mappings if m.enabled} self._motion_overlays: dict[str, str] = {} # legacy — unused if border theme active self._event_overlays: dict[str, tuple[str, str]] = {} + # Throttling state per event_id: (last_apply_ts, last_nx, last_ny, last_nw, last_nh) + self._event_bbox_last: dict[str, tuple[float, float, float, float, float]] = {} self._borders_initialized: dict[str, bool] = {} # target_instance → bool self._cell_states: dict[str, set[str]] = {} # ":" → set of active cameras с motion self._focus_dims: dict[str, set[int]] = {} # instance → set of cells which сейчас затемнены @@ -359,6 +371,22 @@ class FrigateBridge: nw = max(0.01, min(1.0 - nx, nw)) nh = max(0.01, min(1.0 - ny, nh)) + # Throttle: skip update if too frequent OR bbox barely moved. + # First emission всегда applies (regardless of throttle). + now = time.monotonic() + prev = self._event_bbox_last.get(event_id) + if prev is not None: + prev_ts, px, py, pw, ph = prev + if self.cfg.bbox_min_interval_sec > 0 and \ + (now - prev_ts) < self.cfg.bbox_min_interval_sec: + return # rate-limit + if self.cfg.bbox_delta_threshold > 0: + dt = self.cfg.bbox_delta_threshold + if abs(nx - px) < dt and abs(ny - py) < dt and \ + abs(nw - pw) < dt and abs(nh - ph) < dt: + return # delta filter — barely changed + self._event_bbox_last[event_id] = (now, nx, ny, nw, nh) + # Short id'ы — кутаем event_id чтобы влезть в 32 chars eid_short = event_id.replace("-", "")[:8] rect_id = f"e{eid_short}r" @@ -390,6 +418,7 @@ class FrigateBridge: await self.dispatcher.handle(mapping.target_instance, "overlay.add", text.model_dump_json()) async def _remove_event_overlay(self, mapping, event_id: str) -> None: + self._event_bbox_last.pop(event_id, None) # cleanup throttle state stored = self._event_overlays.pop(event_id, None) if not stored: return