controller: auto-layout v2 — mpp при 2+ active с dynamic main

Logic update в FrigateBridge._update_auto_layout:
  0 active     → quad
  1 active     → single, main_cam = active
  2+ active    → main_plus_preview, mpp_main = highest priority active

Dispatcher.set_mpp_main — ZMQ streamselect@mpp_main map <index>
Config.mpp_main_filter_target = "streamselect@mpp_main"

При каждом auto-layout change controller отправляет 3 ZMQ:
  streamselect@main_cam map N    (single layout main)
  streamselect@mpp_main  map N   (mpp layout main, может быть тот же N)
  streamselect@layout    map L   (final layout selector)

Preview cells в mpp остаются fixed mapping (cell1=cam1/front_yard, cell2=cam2/gate_lpr,
cell3=cam3/back_yard). Если main_cam = cam1/2/3 — preview slot этой cam visible duplicate.
Acceptable v2 trade-off (user warned).

Live verified: 4+ active cameras → mpp, gate_lpr в main slot (priority=10 highest).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
gx
2026-05-21 06:41:11 +01:00
parent a7b1d9b1d9
commit 9080004d48
3 changed files with 28 additions and 3 deletions
@@ -101,6 +101,10 @@ class InstanceCfg(BaseModel):
default="streamselect@main_cam",
description="ZMQ target streamselect filter для dynamic main camera (single layout)",
)
mpp_main_filter_target: str = Field(
default="streamselect@mpp_main",
description="ZMQ target streamselect filter для dynamic main в main_plus_preview layout",
)
layout_map: dict[str, int] = Field(
default_factory=lambda: {"quad": 0, "single": 1, "main_plus_preview": 2},
description="layout name → streamselect map index (соответствует pipeline filter_complex)",
@@ -196,6 +196,20 @@ class CommandDispatcher:
except (TimeoutError, Exception) as e:
log.warning("dispatch.main_cam_fail", instance=instance, error=str(e))
async def set_mpp_main(self, instance: str, cam_index: int) -> None:
"""Switch mpp main camera через streamselect@mpp_main map."""
inst = self._find_instance(instance)
if inst is None:
return
client = self._client(inst)
try:
reply = await client.send_command(
inst.mpp_main_filter_target, "map", str(cam_index)
)
log.info("dispatch.mpp_main_set", instance=instance, cam_index=cam_index, ffmpeg_reply=reply)
except (TimeoutError, Exception) as e:
log.warning("dispatch.mpp_main_fail", instance=instance, error=str(e))
async def _reload_icon(self, instance: str, icon_name: str) -> None:
"""Invalidate cached icon atlas в filter — used by DynamicRenderer."""
inst = self._find_instance(instance)
@@ -213,13 +213,19 @@ class FrigateBridge:
]
active.sort(key=lambda m: -m.priority)
# Auto-layout logic v2:
# 0 active → quad (overview)
# 1 active → single, main_cam = тот один
# 2+ active → main_plus_preview, main_cam = highest priority active
if not active:
# Idle — quad
target_layout = "quad"
target_main_cam = 0
else:
elif len(active) == 1:
target_layout = "single"
target_main_cam = active[0].main_cam_index
else:
target_layout = "main_plus_preview"
target_main_cam = active[0].main_cam_index # highest priority
prev = self._auto_state.get(instance, (None, None))
if prev == (target_layout, target_main_cam):
@@ -227,8 +233,9 @@ class FrigateBridge:
log.info("auto_layout.change", instance=instance,
from_state=prev, to_layout=target_layout, to_main=target_main_cam,
active=[m.frigate_camera for m in active])
# Set main_cam first (so single layout shows correct cam at switch moment)
# Set both main_cam streamselects (single и mpp независимые)
await self.dispatcher.set_main_cam(instance, target_main_cam)
await self.dispatcher.set_mpp_main(instance, target_main_cam)
await self.dispatcher.handle(instance, "layout.set", target_layout)
self._auto_state[instance] = (target_layout, target_main_cam)