77 lines
2.6 KiB
Python
77 lines
2.6 KiB
Python
"""
|
|
PulseAudio readiness helpers.
|
|
|
|
The PulseAudio daemon lives in the `op25` container and exposes its native
|
|
socket on the shared `pulse_socket` docker volume (mounted at /run/pulse in
|
|
both containers, with PULSE_SERVER=unix:/run/pulse/native).
|
|
|
|
`op25-container/docker-entrypoint.sh` waits up to ~10 s for that socket before
|
|
starting its own app, but the edge-node historically had *no* equivalent wait:
|
|
FFmpeg would be launched with `-f pulse` before the socket existed, fail
|
|
instantly, and the audio path would stay dead for the lifetime of the process.
|
|
This module is the missing wait.
|
|
|
|
NOTE on the source name: the op25 entrypoint starts pulseaudio with `-n`, which
|
|
skips /etc/pulse/system.pa entirely and loads modules from the command line
|
|
instead. That means the `set-default-source drb_sink.monitor` line in system.pa
|
|
is NOT applied at runtime, so `-i default` is unreliable. Always address the
|
|
monitor explicitly via settings.pulse_source (default "drb_sink.monitor").
|
|
"""
|
|
import asyncio
|
|
import os
|
|
import stat
|
|
from typing import Optional
|
|
|
|
from app.config import settings
|
|
from app.internal.logger import logger
|
|
|
|
DEFAULT_SOCKET_PATH = "/run/pulse/native"
|
|
POLL_INTERVAL = 0.5
|
|
|
|
|
|
def socket_path() -> str:
|
|
"""Resolve the PulseAudio socket path from PULSE_SERVER (`unix:/path` form)."""
|
|
server = os.environ.get("PULSE_SERVER", "")
|
|
if server.startswith("unix:"):
|
|
candidate = server[len("unix:"):].strip()
|
|
if candidate:
|
|
return candidate
|
|
return DEFAULT_SOCKET_PATH
|
|
|
|
|
|
def is_ready() -> bool:
|
|
"""True when the PulseAudio native socket exists and really is a socket."""
|
|
try:
|
|
return stat.S_ISSOCK(os.stat(socket_path()).st_mode)
|
|
except OSError:
|
|
return False
|
|
|
|
|
|
async def wait_until_ready(timeout: Optional[float] = None) -> bool:
|
|
"""
|
|
Block until the PulseAudio socket appears, or `timeout` seconds elapse.
|
|
|
|
Bounded on purpose — never hang the caller forever. Returns True if the
|
|
socket is present, False on timeout (caller decides whether to retry).
|
|
"""
|
|
limit = settings.pulse_wait_timeout if timeout is None else timeout
|
|
path = socket_path()
|
|
|
|
if is_ready():
|
|
return True
|
|
|
|
logger.info(f"Waiting up to {limit:.0f}s for PulseAudio socket at {path}…")
|
|
waited = 0.0
|
|
while waited < limit:
|
|
await asyncio.sleep(POLL_INTERVAL)
|
|
waited += POLL_INTERVAL
|
|
if is_ready():
|
|
logger.info(f"PulseAudio socket ready after {waited:.1f}s.")
|
|
return True
|
|
|
|
logger.error(
|
|
f"PulseAudio socket {path} not present after {limit:.0f}s — "
|
|
"is the op25 container running? Audio capture will retry."
|
|
)
|
|
return False
|