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:
@@ -57,7 +57,12 @@ class InstanceCfg(BaseModel):
|
||||
|
||||
name: str = Field(description="уникальное имя — становится частью HA entity ID")
|
||||
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"
|
||||
filter_target: str = Field(
|
||||
|
||||
@@ -109,6 +109,17 @@ class CommandDispatcher:
|
||||
self._zmq_clients[inst.name] = 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:
|
||||
return next((i for i in self.cfg.instances if i.name == name), None)
|
||||
|
||||
@@ -141,7 +152,8 @@ class CommandDispatcher:
|
||||
# ─── Audio ─────────────────────────────────────────────────────
|
||||
|
||||
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:
|
||||
log.warning("dispatch.no_audio_sources", instance=inst.name)
|
||||
return
|
||||
@@ -152,7 +164,7 @@ class CommandDispatcher:
|
||||
available=[s.name for s in inst.audio_sources])
|
||||
return
|
||||
|
||||
client = self._client(inst)
|
||||
client = self._audio_client(inst)
|
||||
try:
|
||||
reply = await client.send_command(
|
||||
inst.audio_filter_target, "map", str(src.index)
|
||||
@@ -183,8 +195,8 @@ class CommandDispatcher:
|
||||
|
||||
async def _intercom_set(self, inst: InstanceCfg, active: bool) -> None:
|
||||
"""Ducking pattern: при intercom ON → music volume ↓ + intercom ↑.
|
||||
После end — restore. Volume commands отправляются параллельно."""
|
||||
client = self._client(inst)
|
||||
После end — restore. Phase 5d: команды идут в audio sidecar."""
|
||||
client = self._audio_client(inst)
|
||||
music_vol = inst.music_ducked_volume if active else 1.0
|
||||
intercom_vol = 1.0 if active else 0.0
|
||||
|
||||
|
||||
Reference in New Issue
Block a user