From b821ad96157f0675aef18c1fd3012711aac7ad1e Mon Sep 17 00:00:00 2001 From: Daniel Date: Fri, 6 Mar 2026 11:25:42 +0100 Subject: [PATCH] Fix: Reuse voice connection when switching radio stations Instead of destroying and recreating the voice connection on every station change, now checks if the bot is already in the target channel. If same channel: only stops ffmpeg/player, spawns new stream, reuses the existing connection (no reconnect flicker). If different channel: full disconnect + reconnect as before. Co-Authored-By: Claude Opus 4.6 --- server/src/plugins/radio/index.ts | 64 ++++++++++++++++++++++--------- 1 file changed, 45 insertions(+), 19 deletions(-) diff --git a/server/src/plugins/radio/index.ts b/server/src/plugins/radio/index.ts index 5c94d78..5e2fcf8 100644 --- a/server/src/plugins/radio/index.ts +++ b/server/src/plugins/radio/index.ts @@ -70,13 +70,20 @@ function setFavorites(favs: Favorite[]): void { } // ── Streaming ── -function stopStream(guildId: string): void { + +/** Stop only audio (ffmpeg + player), keep voice connection alive */ +function stopAudio(guildId: string): void { const state = guildRadioState.get(guildId); if (!state) return; try { state.ffmpeg.kill('SIGKILL'); } catch {} try { state.player.stop(true); } catch {} - try { getVoiceConnection(guildId, 'radio')?.destroy(); } catch {} guildRadioState.delete(guildId); +} + +/** Full stop: audio + voice connection + broadcast */ +function stopStream(guildId: string): void { + stopAudio(guildId); + try { getVoiceConnection(guildId, 'radio')?.destroy(); } catch {} connectedSince.delete(guildId); broadcastState(guildId); console.log(`[Radio] Stopped stream in guild ${guildId}`); @@ -86,12 +93,23 @@ async function startStream( ctx: PluginContext, guildId: string, voiceChannelId: string, stationId: string, stationName: string, placeName: string, country: string, ): Promise<{ ok: boolean; error?: string }> { - // Stoppe laufenden Stream in diesem Guild - stopStream(guildId); + // Check if we can reuse the existing voice connection + const prev = guildRadioState.get(guildId); + const existingConn = getVoiceConnection(guildId, 'radio'); + const sameChannel = prev && existingConn && prev.channelId === voiceChannelId + && existingConn.state.status === VoiceConnectionStatus.Ready; + + // Stop only audio (keep connection if same channel) + stopAudio(guildId); // Stream-URL auflösen const streamUrl = await resolveStreamUrl(stationId); - if (!streamUrl) return { ok: false, error: 'Stream-URL konnte nicht aufgelöst werden' }; + if (!streamUrl) { + // No stream → full disconnect if nothing playing + if (!sameChannel) try { existingConn?.destroy(); } catch {} + broadcastState(guildId); + return { ok: false, error: 'Stream-URL konnte nicht aufgelöst werden' }; + } // Guild + Channel finden const guild = ctx.client.guilds.cache.get(guildId); @@ -99,20 +117,27 @@ async function startStream( const channel = guild.channels.cache.get(voiceChannelId) as VoiceBasedChannel | undefined; if (!channel) return { ok: false, error: 'Voice Channel nicht gefunden' }; - // Voice-Channel joinen - const connection = joinVoiceChannel({ - channelId: voiceChannelId, - guildId, - adapterCreator: guild.voiceAdapterCreator, - selfDeaf: true, - group: 'radio', - }); + // Reuse or create voice connection + let connection = sameChannel ? existingConn! : null; + if (!connection) { + // Different channel or no connection → full join + try { existingConn?.destroy(); } catch {} + connectedSince.delete(guildId); - try { - await entersState(connection, VoiceConnectionStatus.Ready, 10_000); - } catch { - connection.destroy(); - return { ok: false, error: 'Voice-Verbindung fehlgeschlagen' }; + connection = joinVoiceChannel({ + channelId: voiceChannelId, + guildId, + adapterCreator: guild.voiceAdapterCreator, + selfDeaf: true, + group: 'radio', + }); + + try { + await entersState(connection, VoiceConnectionStatus.Ready, 10_000); + } catch { + connection.destroy(); + return { ok: false, error: 'Voice-Verbindung fehlgeschlagen' }; + } } // ffmpeg spawnen – Radio-Stream → raw PCM @@ -138,6 +163,7 @@ async function startStream( console.log(`[Radio] ffmpeg exited (code ${code}), cleaning up`); guildRadioState.delete(guildId); try { getVoiceConnection(guildId, 'radio')?.destroy(); } catch {} + connectedSince.delete(guildId); broadcastState(guildId); } }); @@ -169,7 +195,7 @@ async function startStream( }); broadcastState(guildId); - console.log(`[Radio] ▶ "${stationName}" (${placeName}, ${country}) → ${guild.name}/#${channelName}`); + console.log(`[Radio] ${sameChannel ? '\u{1F504}' : '\u25B6'} "${stationName}" (${placeName}, ${country}) → ${guild.name}/#${channelName}`); return { ok: true }; }