Updates to discord radio module to try and fix redirection problem

This commit is contained in:
Logan Cusano
2026-01-04 01:32:46 -05:00
parent 00f4ebea2d
commit 4f5dcaf6ce

View File

@@ -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: