Issue 1 — Discord Audio (PulseAudio)
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 <sink_name>.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).
This commit is contained in:
@@ -17,6 +17,7 @@ services:
|
|||||||
- ./configs:/configs
|
- ./configs:/configs
|
||||||
- /dev:/dev
|
- /dev:/dev
|
||||||
- ./op25-container/app:/app
|
- ./op25-container/app:/app
|
||||||
|
- pulse_socket:/run/pulse
|
||||||
depends_on:
|
depends_on:
|
||||||
- icecast
|
- icecast
|
||||||
|
|
||||||
@@ -29,6 +30,12 @@ services:
|
|||||||
- ./configs:/configs
|
- ./configs:/configs
|
||||||
- ./recordings:/recordings
|
- ./recordings:/recordings
|
||||||
- ./drb-edge-node/app:/app/app
|
- ./drb-edge-node/app:/app/app
|
||||||
|
- pulse_socket:/run/pulse
|
||||||
|
environment:
|
||||||
|
PULSE_SERVER: unix:/run/pulse/native
|
||||||
depends_on:
|
depends_on:
|
||||||
- icecast
|
- icecast
|
||||||
- op25
|
- op25
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
pulse_socket:
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import asyncio
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
import discord
|
import discord
|
||||||
from discord.ext import commands
|
from discord.ext import commands
|
||||||
from app.config import settings
|
|
||||||
from app.internal.logger import logger
|
from app.internal.logger import logger
|
||||||
|
|
||||||
BOT_READY_TIMEOUT = 15 # seconds to wait for Discord bot to become ready
|
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._watchdog_task: Optional[asyncio.Task] = None
|
||||||
self._ready_event: Optional[asyncio.Event] = None
|
self._ready_event: Optional[asyncio.Event] = None
|
||||||
self._current_token: Optional[str] = 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
|
# Remembered so we can rejoin after an unexpected disconnect
|
||||||
self._guild_id: Optional[int] = None
|
self._guild_id: Optional[int] = None
|
||||||
@@ -114,9 +110,11 @@ class RadioBot:
|
|||||||
def _play_stream(self):
|
def _play_stream(self):
|
||||||
if not self._voice_client:
|
if not self._voice_client:
|
||||||
return
|
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(
|
source = discord.FFmpegPCMAudio(
|
||||||
self._icecast_url,
|
"default.monitor",
|
||||||
before_options="-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5",
|
before_options="-f pulse",
|
||||||
)
|
)
|
||||||
self._voice_client.play(
|
self._voice_client.play(
|
||||||
discord.PCMVolumeTransformer(source, volume=1.0),
|
discord.PCMVolumeTransformer(source, volume=1.0),
|
||||||
|
|||||||
@@ -141,11 +141,13 @@ class MQTTManager:
|
|||||||
self._publish(self._t_key_request, {}, qos=1)
|
self._publish(self._t_key_request, {}, qos=1)
|
||||||
|
|
||||||
async def _publish_checkin(self):
|
async def _publish_checkin(self):
|
||||||
|
from app.internal.discord_radio import radio_bot
|
||||||
payload = {
|
payload = {
|
||||||
"node_id": settings.node_id,
|
"node_id": settings.node_id,
|
||||||
"name": settings.node_name,
|
"name": settings.node_name,
|
||||||
"lat": settings.node_lat,
|
"lat": settings.node_lat,
|
||||||
"lon": settings.node_lon,
|
"lon": settings.node_lon,
|
||||||
|
"discord_connected": radio_bot.is_connected,
|
||||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||||
}
|
}
|
||||||
self._publish(self._t_checkin, payload, qos=1)
|
self._publish(self._t_checkin, payload, qos=1)
|
||||||
|
|||||||
Reference in New Issue
Block a user