From 4f5dcaf6ceb4c2d329f25b951049dc4a7728972b Mon Sep 17 00:00:00 2001 From: Logan Cusano Date: Sun, 4 Jan 2026 01:32:46 -0500 Subject: [PATCH] Updates to discord radio module to try and fix redirection problem --- app/internal/discord_radio.py | 55 ++++++++++++++++++----------------- 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/app/internal/discord_radio.py b/app/internal/discord_radio.py index d8599af..edf6ebc 100644 --- a/app/internal/discord_radio.py +++ b/app/internal/discord_radio.py @@ -3,28 +3,23 @@ import asyncio import socket import logging import threading -import struct import subprocess import shlex +import time from collections import deque -from typing import Optional, List, Tuple +from typing import Optional, List LOGGER = logging.getLogger("discord_radio") class UDPAudioSource(discord.AudioSource): """ A custom Discord AudioSource that reads raw PCM audio from a thread-safe buffer. - This buffer is filled by the UDPListener running in the background. - It pipes the raw 8000Hz 16-bit audio into FFmpeg to convert it to Discord's required 48000Hz stereo Opus format. """ def __init__(self, buffer: deque): self.buffer = buffer # We start an FFmpeg process that reads from stdin (pipe) and outputs raw PCM (s16le) at 48k - # discord.py then encodes this to Opus. - # Note: We use a pipe here because we are feeding it chunks from our UDP buffer. - self.ffmpeg_process = subprocess.Popen( shlex.split( "ffmpeg -f s16le -ar 8000 -ac 1 -i pipe:0 -f s16le -ar 48000 -ac 2 pipe:1 -loglevel panic" @@ -80,19 +75,25 @@ class DiscordRadioBot(discord.Client): - Packet Forwarding (to keep Liquidsoap/Icecast working) - Manual Channel Move detection - Dynamic Token/Channel joining + - Debug Stats logging """ def __init__(self, listen_port=23456, forward_ports: List[int] = None): intents = discord.Intents.default() super().__init__(intents=intents) self.listen_port = listen_port - self.forward_ports = forward_ports or [] # List of ports to forward UDP packets to (e.g. [23457]) + self.forward_ports = forward_ports or [] self.audio_buffer = deque(maxlen=160000) # Buffer ~10 seconds of audio self.udp_sock = None self.running = False self.voice_client: Optional[discord.VoiceClient] = None + # Debug Stats + self.packets_received = 0 + self.packets_forwarded = 0 + self.last_log_time = time.time() + # Start the UDP Listener in a separate daemon thread immediately self.udp_thread = threading.Thread(target=self._udp_listener_loop, daemon=True) self.udp_thread.start() @@ -100,16 +101,14 @@ class DiscordRadioBot(discord.Client): def _udp_listener_loop(self): """ Runs in a background thread. Listens for UDP packets from OP25. - 1. Adds data to internal audio buffer for Discord. - 2. Forwards data to other local ports (for Liquidsoap). """ self.udp_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - self.udp_sock.bind(('127.0.0.1', self.listen_port)) + # Bind to 0.0.0.0 to ensure we catch traffic from any interface (Docker/Localhost) + self.udp_sock.bind(('0.0.0.0', self.listen_port)) self.udp_sock.settimeout(1.0) - LOGGER.info(f"UDP Audio Bridge listening on 127.0.0.1:{self.listen_port}") - if self.forward_ports: - LOGGER.info(f"Forwarding audio to: {self.forward_ports}") + LOGGER.info(f"UDP Audio Bridge listening on 0.0.0.0:{self.listen_port}") + LOGGER.info(f"Forwarding audio to: {self.forward_ports}") forward_socks = [] for port in self.forward_ports: @@ -120,56 +119,60 @@ class DiscordRadioBot(discord.Client): while self.running: try: data, addr = self.udp_sock.recvfrom(4096) + self.packets_received += 1 # 1. Forward to Liquidsoap/Other tools for sock, port in forward_socks: + # Send to localhost (where Liquidsoap should be listening) sock.sendto(data, ('127.0.0.1', port)) + self.packets_forwarded += 1 # 2. Add to Discord Buffer - # We extend the deque with the raw bytes self.audio_buffer.extend(data) + # Periodic Debug Logging (Every 5 seconds, only if active) + if time.time() - self.last_log_time > 5: + if self.packets_received > 0: + LOGGER.info(f"UDP Stats [5s]: Rx {self.packets_received} pkts | Tx {self.packets_forwarded} pkts | Buffer: {len(self.audio_buffer)} bytes") + + # Reset counters + self.packets_received = 0 + self.packets_forwarded = 0 + self.last_log_time = time.time() + except socket.timeout: continue except Exception as e: LOGGER.error(f"UDP Listener Error: {e}") - # Prevent tight loop on socket fail - asyncio.sleep(0.1) + time.sleep(0.1) async def on_ready(self): LOGGER.info(f'Discord Radio Bot connected as {self.user}') async def on_voice_state_update(self, member, before, after): """ - Handles manual moves. If the bot is moved to a new channel by a user, - update our internal voice_client reference to ensure we keep streaming. + Handles manual moves. """ if member.id == self.user.id: if after.channel is not None and before.channel != after.channel: LOGGER.info(f"Bot was moved to channel: {after.channel.name}") - # discord.py handles the connection handoff automatically, - # but we log it to confirm persistence. self.voice_client = member.guild.voice_client async def start_session(self, token: str, channel_id: int): """ Connects the bot to Discord and joins the specified channel. - This should be called when the 'start' command is received from C2. """ - # Start the client in the background asyncio.create_task(self.start(token)) - # Wait for login to complete (simple poll) + # Wait for login to complete for _ in range(20): if self.is_ready(): break await asyncio.sleep(0.5) - # Join Channel try: channel = self.get_channel(int(channel_id)) if not channel: - # If cache not ready, try fetching channel = await self.fetch_channel(int(channel_id)) if channel: