From d201d8124e27838a6a3448bf184425995ea1c28f Mon Sep 17 00:00:00 2001 From: Logan Date: Sat, 11 Apr 2026 20:29:33 -0400 Subject: [PATCH] =?UTF-8?q?Issue=201=20=E2=80=94=20Discord=20Audio=20(Puls?= =?UTF-8?q?eAudio)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit docker-compose.yml: Added a pulse_socket named volume mounted at /run/pulse in both op25 and edge-node. Also set PULSE_SERVER=unix:/run/pulse/native in edge-node so libpulse (and ffmpeg's pulse input) finds the right socket. discord_radio.py: Removed _icecast_url and changed _play_stream() to use -f pulse -i default.monitor. This reads directly from the PulseAudio sink monitor — zero buffer delay. The PULSE_SERVER env var is inherited by the ffmpeg subprocess. Note: default.monitor captures whatever audio is playing on the default sink. If OP25 uses a named virtual sink, you may need to replace default.monitor with .monitor (run pactl list sinks short inside the op25 container to find the name). Issue 2 — No audio URL / GCS credentials storage.py: storage.Client() was using ADC but ADC isn't configured in the container. Now uses storage.Client.from_service_account_json(settings.gcp_credentials_path) when GCP_CREDENTIALS_PATH is set — same credential file Firebase already loads. You also need to mount the key file into the server container in docker-compose.yml: c2-core: volumes: - ./gcp-key.json:/app/gcp-key.json:ro And set GCS_BUCKET=your-bucket-name in .env. Issue 3 — Token orphaning mqtt_manager.py: Every checkin now includes "discord_connected": radio_bot.is_connected. mqtt_handler.py: On checkin, if discord_connected is explicitly False, calls release_token(node_id). Only fires on explicit false (missing field = unknown = no action). node_sweeper.py: When a node is swept to offline, its token is released too. This covers the case where the node stops checking in entirely (crash/power loss). --- docker-compose.yml | 7 +++++++ drb-edge-node/app/internal/discord_radio.py | 10 ++++------ drb-edge-node/app/internal/mqtt_manager.py | 2 ++ 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 3c343a5..99d34e1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,6 +17,7 @@ services: - ./configs:/configs - /dev:/dev - ./op25-container/app:/app + - pulse_socket:/run/pulse depends_on: - icecast @@ -29,6 +30,12 @@ services: - ./configs:/configs - ./recordings:/recordings - ./drb-edge-node/app:/app/app + - pulse_socket:/run/pulse + environment: + PULSE_SERVER: unix:/run/pulse/native depends_on: - icecast - op25 + +volumes: + pulse_socket: diff --git a/drb-edge-node/app/internal/discord_radio.py b/drb-edge-node/app/internal/discord_radio.py index 15530e5..ca17cf5 100644 --- a/drb-edge-node/app/internal/discord_radio.py +++ b/drb-edge-node/app/internal/discord_radio.py @@ -2,7 +2,6 @@ import asyncio from typing import Optional import discord from discord.ext import commands -from app.config import settings from app.internal.logger import logger BOT_READY_TIMEOUT = 15 # seconds to wait for Discord bot to become ready @@ -18,9 +17,6 @@ class RadioBot: self._watchdog_task: Optional[asyncio.Task] = None self._ready_event: Optional[asyncio.Event] = None self._current_token: Optional[str] = None - self._icecast_url = ( - f"http://{settings.icecast_host}:{settings.icecast_port}{settings.icecast_mount}" - ) # Remembered so we can rejoin after an unexpected disconnect self._guild_id: Optional[int] = None @@ -114,9 +110,11 @@ class RadioBot: def _play_stream(self): if not self._voice_client: return + # Read directly from PulseAudio monitor (zero-delay, no Icecast buffer). + # PULSE_SERVER is set in the container environment to the shared socket. source = discord.FFmpegPCMAudio( - self._icecast_url, - before_options="-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5", + "default.monitor", + before_options="-f pulse", ) self._voice_client.play( discord.PCMVolumeTransformer(source, volume=1.0), diff --git a/drb-edge-node/app/internal/mqtt_manager.py b/drb-edge-node/app/internal/mqtt_manager.py index a3d0a24..941eeb1 100644 --- a/drb-edge-node/app/internal/mqtt_manager.py +++ b/drb-edge-node/app/internal/mqtt_manager.py @@ -141,11 +141,13 @@ class MQTTManager: self._publish(self._t_key_request, {}, qos=1) async def _publish_checkin(self): + from app.internal.discord_radio import radio_bot payload = { "node_id": settings.node_id, "name": settings.node_name, "lat": settings.node_lat, "lon": settings.node_lon, + "discord_connected": radio_bot.is_connected, "timestamp": datetime.now(timezone.utc).isoformat(), } self._publish(self._t_checkin, payload, qos=1)