Files
vf-cuda-grid/controller/cuda_grid_controller/state.py
T
gx 48eb62bddc controller: UI-toggle audio output + astreamselect decouple (Phase 7 Вариант 2)
Web UI checkbox в Audio source карточке: «вывод аудио в стрим». Off →
TV получает silence + downstream audio chain isolated от live-audio RTSP
(нестабильный sidecar поток больше не блокирует video pipeline).

Изменения:
  state.InstanceState: + audio_output_enabled (default True)
  config.InstanceCfg:
    + audio_output_volume_target (default volume@output_audio)
    + audio_input_select_target (default astreamselect@audio_input)
  dispatch.set_audio_output_enabled(enabled):
    enabled=False → astreamselect map=1 (anullsrc) + volume=0
    enabled=True  → astreamselect map=0 (live-audio) + volume=1
    Двойная команда: select decouples upstream, volume гарантирует тишину
    на случай если в anullsrc что-то не так.
  http_api: + GET /audio-output/{instance}, POST /audio-output/{instance}
  static/index.html: + checkbox в Audio source header + loadAudioOut/toggleAudioOut

ZMQ smoke test OK. HTTP roundtrip OK.

Сопутствующий pipeline change: docker-compose.phase7.yml — amix заменён
на astreamselect@audio_input (см. localhost-infra commit).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 20:44:50 +01:00

91 lines
3.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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)
audio_output_enabled: bool = True
# 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 []
# ─── Audio output mute ──────────────────────────────────────────
async def set_audio_output_enabled(self, instance: str, enabled: bool) -> None:
async with self._lock:
st = self.instances.get(instance)
if st is not None:
st.audio_output_enabled = enabled
async def get_audio_output_enabled(self, instance: str) -> bool:
async with self._lock:
st = self.instances.get(instance)
return st.audio_output_enabled if st else True
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