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 <noreply@anthropic.com>
This commit is contained in:
Daniel 2026-03-06 11:25:42 +01:00
parent 63afc55836
commit b821ad9615

View file

@ -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,8 +117,14 @@ 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({
// 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);
connection = joinVoiceChannel({
channelId: voiceChannelId,
guildId,
adapterCreator: guild.voiceAdapterCreator,
@ -114,6 +138,7 @@ async function startStream(
connection.destroy();
return { ok: false, error: 'Voice-Verbindung fehlgeschlagen' };
}
}
// ffmpeg spawnen Radio-Stream → raw PCM
const ffmpeg = spawn('ffmpeg', [
@ -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 };
}