Fix: Voice claim system - radio stops when soundboard plays
Plugins now claim voice per guild via claimVoice(). When soundboard plays a sound, radio's cleanup runs automatically (kills ffmpeg, broadcasts SSE stop event). Fixes stale "now playing" UI on tab switch. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
e0635b30ef
commit
1e4ccfb1f1
3 changed files with 30 additions and 0 deletions
|
|
@ -39,3 +39,25 @@ export function registerPlugin(plugin: Plugin): void {
|
||||||
export function getPlugins(): Plugin[] {
|
export function getPlugins(): Plugin[] {
|
||||||
return [...loadedPlugins];
|
return [...loadedPlugins];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Voice claim system ──
|
||||||
|
// Only one plugin can use voice per guild. When a new plugin claims voice,
|
||||||
|
// the previous claimant's cleanup callback is invoked automatically.
|
||||||
|
type VoiceClaimCleanup = () => void;
|
||||||
|
const voiceClaims = new Map<string, { plugin: string; cleanup: VoiceClaimCleanup }>();
|
||||||
|
|
||||||
|
export function claimVoice(guildId: string, pluginName: string, cleanup: VoiceClaimCleanup): void {
|
||||||
|
const existing = voiceClaims.get(guildId);
|
||||||
|
if (existing && existing.plugin !== pluginName) {
|
||||||
|
console.log(`[Voice] ${pluginName} claims guild ${guildId}, releasing ${existing.plugin}`);
|
||||||
|
existing.cleanup();
|
||||||
|
}
|
||||||
|
voiceClaims.set(guildId, { plugin: pluginName, cleanup });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function releaseVoice(guildId: string, pluginName: string): void {
|
||||||
|
const claim = voiceClaims.get(guildId);
|
||||||
|
if (claim?.plugin === pluginName) {
|
||||||
|
voiceClaims.delete(guildId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import {
|
||||||
import type { VoiceBasedChannel } from 'discord.js';
|
import type { VoiceBasedChannel } from 'discord.js';
|
||||||
import { ChannelType } from 'discord.js';
|
import { ChannelType } from 'discord.js';
|
||||||
import type { Plugin, PluginContext } from '../../core/plugin.js';
|
import type { Plugin, PluginContext } from '../../core/plugin.js';
|
||||||
|
import { claimVoice, releaseVoice } from '../../core/plugin.js';
|
||||||
import { sseBroadcast } from '../../core/sse.js';
|
import { sseBroadcast } from '../../core/sse.js';
|
||||||
import { getState, setState } from '../../core/persistence.js';
|
import { getState, setState } from '../../core/persistence.js';
|
||||||
import {
|
import {
|
||||||
|
|
@ -68,6 +69,7 @@ function stopStream(guildId: string): void {
|
||||||
try { state.player.stop(true); } catch {}
|
try { state.player.stop(true); } catch {}
|
||||||
try { getVoiceConnection(guildId)?.destroy(); } catch {}
|
try { getVoiceConnection(guildId)?.destroy(); } catch {}
|
||||||
guildRadioState.delete(guildId);
|
guildRadioState.delete(guildId);
|
||||||
|
releaseVoice(guildId, 'radio');
|
||||||
broadcastState(guildId);
|
broadcastState(guildId);
|
||||||
console.log(`[Radio] Stopped stream in guild ${guildId}`);
|
console.log(`[Radio] Stopped stream in guild ${guildId}`);
|
||||||
}
|
}
|
||||||
|
|
@ -148,6 +150,9 @@ async function startStream(
|
||||||
stopStream(guildId);
|
stopStream(guildId);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Claim voice for this guild (stops other plugins like soundboard)
|
||||||
|
claimVoice(guildId, 'radio', () => stopStream(guildId));
|
||||||
|
|
||||||
// State tracken
|
// State tracken
|
||||||
const channelName = 'name' in channel ? (channel as any).name : voiceChannelId;
|
const channelName = 'name' in channel ? (channel as any).name : voiceChannelId;
|
||||||
guildRadioState.set(guildId, {
|
guildRadioState.set(guildId, {
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import sodium from 'libsodium-wrappers';
|
||||||
import nacl from 'tweetnacl';
|
import nacl from 'tweetnacl';
|
||||||
import { ChannelType, Events, type VoiceState, type Message } from 'discord.js';
|
import { ChannelType, Events, type VoiceState, type Message } from 'discord.js';
|
||||||
import type { Plugin, PluginContext } from '../../core/plugin.js';
|
import type { Plugin, PluginContext } from '../../core/plugin.js';
|
||||||
|
import { claimVoice } from '../../core/plugin.js';
|
||||||
import { sseBroadcast } from '../../core/sse.js';
|
import { sseBroadcast } from '../../core/sse.js';
|
||||||
|
|
||||||
// ── Config (env) ──
|
// ── Config (env) ──
|
||||||
|
|
@ -332,6 +333,8 @@ let _pluginCtx: PluginContext | null = null;
|
||||||
|
|
||||||
async function playFilePath(guildId: string, channelId: string, filePath: string, volume?: number, relativeKey?: string): Promise<void> {
|
async function playFilePath(guildId: string, channelId: string, filePath: string, volume?: number, relativeKey?: string): Promise<void> {
|
||||||
console.log(`${SB} playFilePath: guild=${guildId} channel=${channelId} file=${path.basename(filePath)} vol=${volume ?? 'default'}`);
|
console.log(`${SB} playFilePath: guild=${guildId} channel=${channelId} file=${path.basename(filePath)} vol=${volume ?? 'default'}`);
|
||||||
|
// Claim voice for this guild (stops radio if playing)
|
||||||
|
claimVoice(guildId, 'soundboard', () => { /* soundboard cleanup handled by lifecycle */ });
|
||||||
const ctx = _pluginCtx!;
|
const ctx = _pluginCtx!;
|
||||||
const guild = ctx.client.guilds.cache.get(guildId);
|
const guild = ctx.client.guilds.cache.get(guildId);
|
||||||
if (!guild) { console.error(`${SB} Guild ${guildId} not found in cache (cached: ${ctx.client.guilds.cache.map(g => g.id).join(', ')})`); throw new Error('Guild nicht gefunden'); }
|
if (!guild) { console.error(`${SB} Guild ${guildId} not found in cache (cached: ${ctx.client.guilds.cache.map(g => g.id).join(', ')})`); throw new Error('Guild nicht gefunden'); }
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue