controller: Phase 5d — audio_zmq_endpoint для split-process architecture

InstanceCfg.audio_zmq_endpoint (optional) — ZMQ адрес отдельного audio
sidecar ffmpeg. dispatcher._audio_set + _intercom_set теперь используют
_audio_client(inst) — separate ZMQ socket к sidecar.

Fallback: если audio_zmq_endpoint=null → реверт к video pipeline zmq
(Phase 5a single-source behaviour).

Зачем: multi-source audio chain в одном pipeline с video блокирует
video pacing (см. feedback_audio-chain-blocks-video). Split разделяет
A/V на 2 ffmpeg процесса; audio sidecar publish'нет к mediamtx, video
pipeline consume'нет audio как RTSP source + remux к combined output.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
gx
2026-05-20 23:15:13 +01:00
parent d807cd2c23
commit d8674e599d
2 changed files with 22 additions and 5 deletions
+6 -1
View File
@@ -57,7 +57,12 @@ class InstanceCfg(BaseModel):
name: str = Field(description="уникальное имя — становится частью HA entity ID") name: str = Field(description="уникальное имя — становится частью HA entity ID")
zmq_endpoint: str = Field( zmq_endpoint: str = Field(
description="ZMQ endpoint FFmpeg's zmq filter (tcp://host:port)" description="ZMQ endpoint видео-pipeline (cuda_grid + overlay filters)"
)
audio_zmq_endpoint: str | None = Field(
default=None,
description="ZMQ endpoint отдельного audio sidecar (Phase 5d split-process). "
"None = audio_set/intercom вызывают video pipeline (Phase 5a single source)",
) )
default_layout: str = "quad" default_layout: str = "quad"
filter_target: str = Field( filter_target: str = Field(
+16 -4
View File
@@ -109,6 +109,17 @@ class CommandDispatcher:
self._zmq_clients[inst.name] = c self._zmq_clients[inst.name] = c
return c return c
def _audio_client(self, inst: InstanceCfg) -> FFmpegZmqClient:
"""ZMQ client к audio sidecar (Phase 5d). Fallback к video pipeline
если split-process не configured."""
endpoint = inst.audio_zmq_endpoint or inst.zmq_endpoint
key = f"{inst.name}:audio"
c = self._zmq_clients.get(key)
if c is None:
c = FFmpegZmqClient(endpoint)
self._zmq_clients[key] = c
return c
def _find_instance(self, name: str) -> InstanceCfg | None: def _find_instance(self, name: str) -> InstanceCfg | None:
return next((i for i in self.cfg.instances if i.name == name), None) return next((i for i in self.cfg.instances if i.name == name), None)
@@ -141,7 +152,8 @@ class CommandDispatcher:
# ─── Audio ───────────────────────────────────────────────────── # ─── Audio ─────────────────────────────────────────────────────
async def _audio_set(self, inst: InstanceCfg, source_name: str) -> None: async def _audio_set(self, inst: InstanceCfg, source_name: str) -> None:
"""Switch audio к source_name через ZMQ команду astreamselect map <index>.""" """Switch audio к source_name через ZMQ команду astreamselect map <index>.
Phase 5d: команда идёт в audio sidecar (отдельный ffmpeg)."""
if not inst.audio_sources: if not inst.audio_sources:
log.warning("dispatch.no_audio_sources", instance=inst.name) log.warning("dispatch.no_audio_sources", instance=inst.name)
return return
@@ -152,7 +164,7 @@ class CommandDispatcher:
available=[s.name for s in inst.audio_sources]) available=[s.name for s in inst.audio_sources])
return return
client = self._client(inst) client = self._audio_client(inst)
try: try:
reply = await client.send_command( reply = await client.send_command(
inst.audio_filter_target, "map", str(src.index) inst.audio_filter_target, "map", str(src.index)
@@ -183,8 +195,8 @@ class CommandDispatcher:
async def _intercom_set(self, inst: InstanceCfg, active: bool) -> None: async def _intercom_set(self, inst: InstanceCfg, active: bool) -> None:
"""Ducking pattern: при intercom ON → music volume ↓ + intercom ↑. """Ducking pattern: при intercom ON → music volume ↓ + intercom ↑.
После end — restore. Volume commands отправляются параллельно.""" После end — restore. Phase 5d: команды идут в audio sidecar."""
client = self._client(inst) client = self._audio_client(inst)
music_vol = inst.music_ducked_volume if active else 1.0 music_vol = inst.music_ducked_volume if active else 1.0
intercom_vol = 1.0 if active else 0.0 intercom_vol = 1.0 if active else 0.0