feat: Radio volume control - server-wide slider synced via SSE
- Server: inlineVolume on AudioResource, POST /api/radio/volume endpoint - Volume persisted per guild, broadcast via SSE to all clients - Frontend: volume slider in bottom bar with debounced API calls - Volume icon changes based on level (muted/low/normal) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
971fd2c3fc
commit
d1ae2db00b
3 changed files with 128 additions and 3 deletions
|
|
@ -24,8 +24,10 @@ interface GuildRadioState {
|
|||
startedAt: string;
|
||||
ffmpeg: ChildProcess;
|
||||
player: ReturnType<typeof createAudioPlayer>;
|
||||
resource: ReturnType<typeof createAudioResource>;
|
||||
channelId: string;
|
||||
channelName: string;
|
||||
volume: number;
|
||||
}
|
||||
|
||||
interface Favorite {
|
||||
|
|
@ -39,6 +41,17 @@ interface Favorite {
|
|||
// ── State ──
|
||||
const guildRadioState = new Map<string, GuildRadioState>();
|
||||
|
||||
function getVolume(guildId: string): number {
|
||||
const vols = getState<Record<string, number>>('radio_volumes', {});
|
||||
return vols[guildId] ?? 0.5;
|
||||
}
|
||||
|
||||
function setVolume(guildId: string, vol: number): void {
|
||||
const vols = getState<Record<string, number>>('radio_volumes', {});
|
||||
vols[guildId] = vol;
|
||||
setState('radio_volumes', vols);
|
||||
}
|
||||
|
||||
function getFavorites(): Favorite[] {
|
||||
return getState<Favorite[]>('radio_favorites', []);
|
||||
}
|
||||
|
|
@ -118,10 +131,13 @@ async function startStream(
|
|||
}
|
||||
});
|
||||
|
||||
// AudioResource + Player
|
||||
// AudioResource + Player (inlineVolume für Lautstärkeregelung)
|
||||
const vol = getVolume(guildId);
|
||||
const resource = createAudioResource(ffmpeg.stdout!, {
|
||||
inputType: StreamType.Raw,
|
||||
inlineVolume: true,
|
||||
});
|
||||
resource.volume?.setVolume(vol);
|
||||
|
||||
const player = createAudioPlayer();
|
||||
player.play(resource);
|
||||
|
|
@ -137,7 +153,8 @@ async function startStream(
|
|||
guildRadioState.set(guildId, {
|
||||
stationId, stationName, placeName, country,
|
||||
streamUrl, startedAt: new Date().toISOString(),
|
||||
ffmpeg, player, channelId: voiceChannelId, channelName,
|
||||
ffmpeg, player, resource, channelId: voiceChannelId, channelName,
|
||||
volume: vol,
|
||||
});
|
||||
|
||||
broadcastState(guildId);
|
||||
|
|
@ -151,6 +168,7 @@ function broadcastState(guildId: string): void {
|
|||
type: 'radio',
|
||||
plugin: 'radio',
|
||||
guildId,
|
||||
volume: state?.volume ?? getVolume(guildId),
|
||||
playing: state ? {
|
||||
stationId: state.stationId,
|
||||
stationName: state.stationName,
|
||||
|
|
@ -252,6 +270,21 @@ const radioPlugin: Plugin = {
|
|||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
// ── Volume ──
|
||||
app.post('/api/radio/volume', (req, res) => {
|
||||
const { guildId, volume } = req.body ?? {};
|
||||
if (!guildId || volume == null) return res.status(400).json({ error: 'guildId, volume required' });
|
||||
const vol = Math.max(0, Math.min(1, Number(volume)));
|
||||
setVolume(guildId, vol);
|
||||
const state = guildRadioState.get(guildId);
|
||||
if (state) {
|
||||
state.volume = vol;
|
||||
state.resource.volume?.setVolume(vol);
|
||||
}
|
||||
sseBroadcast({ type: 'radio_volume', plugin: 'radio', guildId, volume: vol });
|
||||
res.json({ ok: true, volume: vol });
|
||||
});
|
||||
|
||||
// ── Favoriten lesen ──
|
||||
app.get('/api/radio/favorites', (_req, res) => {
|
||||
res.json(getFavorites());
|
||||
|
|
@ -299,6 +332,7 @@ const radioPlugin: Plugin = {
|
|||
|
||||
getSnapshot() {
|
||||
const playing: Record<string, any> = {};
|
||||
const volumes: Record<string, number> = {};
|
||||
for (const [gId, st] of guildRadioState) {
|
||||
playing[gId] = {
|
||||
stationId: st.stationId,
|
||||
|
|
@ -308,10 +342,12 @@ const radioPlugin: Plugin = {
|
|||
startedAt: st.startedAt,
|
||||
channelName: st.channelName,
|
||||
};
|
||||
volumes[gId] = st.volume;
|
||||
}
|
||||
return {
|
||||
radio: {
|
||||
playing,
|
||||
volumes,
|
||||
favorites: getFavorites(),
|
||||
placesCount: getPlacesCount(),
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue