Move recording and Discord voice to PulseAudio
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user