feat(radio): add 2-second pre-buffer to reduce audio lag
Buffer 384 KB of PCM data (~2 seconds at 48kHz stereo) via PassThrough stream before starting Discord playback. Falls back to immediate start after 3s timeout for slow streams. Cleanup integrated into stopAudio. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
7786d02f86
commit
06326de465
1 changed files with 41 additions and 3 deletions
|
|
@ -1,5 +1,6 @@
|
||||||
import type express from 'express';
|
import type express from 'express';
|
||||||
import { spawn, type ChildProcess } from 'node:child_process';
|
import { spawn, type ChildProcess } from 'node:child_process';
|
||||||
|
import { PassThrough } from 'node:stream';
|
||||||
import {
|
import {
|
||||||
joinVoiceChannel, createAudioPlayer, createAudioResource,
|
joinVoiceChannel, createAudioPlayer, createAudioResource,
|
||||||
VoiceConnectionStatus, StreamType, getVoiceConnection, entersState,
|
VoiceConnectionStatus, StreamType, getVoiceConnection, entersState,
|
||||||
|
|
@ -37,6 +38,7 @@ interface GuildRadioState {
|
||||||
channelId: string;
|
channelId: string;
|
||||||
channelName: string;
|
channelName: string;
|
||||||
volume: number;
|
volume: number;
|
||||||
|
prebufferTimeout?: ReturnType<typeof setTimeout>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Favorite {
|
interface Favorite {
|
||||||
|
|
@ -75,6 +77,7 @@ function setFavorites(favs: Favorite[]): void {
|
||||||
function stopAudio(guildId: string): void {
|
function stopAudio(guildId: string): void {
|
||||||
const state = guildRadioState.get(guildId);
|
const state = guildRadioState.get(guildId);
|
||||||
if (!state) return;
|
if (!state) return;
|
||||||
|
if (state.prebufferTimeout) clearTimeout(state.prebufferTimeout);
|
||||||
try { state.ffmpeg.kill('SIGKILL'); } catch {}
|
try { state.ffmpeg.kill('SIGKILL'); } catch {}
|
||||||
try { state.player.stop(true); } catch {}
|
try { state.player.stop(true); } catch {}
|
||||||
guildRadioState.delete(guildId);
|
guildRadioState.delete(guildId);
|
||||||
|
|
@ -168,18 +171,53 @@ async function startStream(
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Pre-buffer: 2 Sekunden PCM puffern bevor Wiedergabe startet ──
|
||||||
|
// 48kHz × 2ch × 2byte = 192.000 B/s → 384 KB ≈ 2s
|
||||||
|
const PRE_BUFFER_BYTES = 384 * 1024;
|
||||||
|
const bufferedStream = new PassThrough({ highWaterMark: PRE_BUFFER_BYTES });
|
||||||
|
let buffered = 0;
|
||||||
|
let prebufferDone = false;
|
||||||
|
|
||||||
|
ffmpeg.stdout!.on('data', (chunk: Buffer) => {
|
||||||
|
bufferedStream.write(chunk);
|
||||||
|
if (!prebufferDone) {
|
||||||
|
buffered += chunk.length;
|
||||||
|
if (buffered >= PRE_BUFFER_BYTES) {
|
||||||
|
prebufferDone = true;
|
||||||
|
startPlayback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ffmpeg.stdout!.on('end', () => bufferedStream.end());
|
||||||
|
ffmpeg.stdout!.on('error', (err) => bufferedStream.destroy(err));
|
||||||
|
|
||||||
// AudioResource + Player (inlineVolume für Lautstärkeregelung)
|
// AudioResource + Player (inlineVolume für Lautstärkeregelung)
|
||||||
const vol = getVolume(guildId);
|
const vol = getVolume(guildId);
|
||||||
const resource = createAudioResource(ffmpeg.stdout!, {
|
const resource = createAudioResource(bufferedStream, {
|
||||||
inputType: StreamType.Raw,
|
inputType: StreamType.Raw,
|
||||||
inlineVolume: true,
|
inlineVolume: true,
|
||||||
});
|
});
|
||||||
resource.volume?.setVolume(vol);
|
resource.volume?.setVolume(vol);
|
||||||
|
|
||||||
const player = createAudioPlayer();
|
const player = createAudioPlayer();
|
||||||
player.play(resource);
|
|
||||||
connection.subscribe(player);
|
connection.subscribe(player);
|
||||||
|
|
||||||
|
// Starte Wiedergabe erst wenn Pre-Buffer voll ist (oder nach 3s Timeout)
|
||||||
|
function startPlayback() {
|
||||||
|
if (player.state.status !== 'idle') return; // Already started
|
||||||
|
player.play(resource);
|
||||||
|
console.log(`[Radio] Pre-buffer filled (${Math.round(buffered / 1024)} KB), starting playback`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: starte nach 3s auch bei langsamen Streams
|
||||||
|
const prebufferTimeout = setTimeout(() => {
|
||||||
|
if (!prebufferDone) {
|
||||||
|
prebufferDone = true;
|
||||||
|
startPlayback();
|
||||||
|
console.log(`[Radio] Pre-buffer timeout, starting with ${Math.round(buffered / 1024)} KB`);
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
player.on('error', (err) => {
|
player.on('error', (err) => {
|
||||||
console.error(`[Radio] Player error:`, err.message);
|
console.error(`[Radio] Player error:`, err.message);
|
||||||
stopStream(guildId);
|
stopStream(guildId);
|
||||||
|
|
@ -191,7 +229,7 @@ async function startStream(
|
||||||
stationId, stationName, placeName, country,
|
stationId, stationName, placeName, country,
|
||||||
streamUrl, startedAt: new Date().toISOString(),
|
streamUrl, startedAt: new Date().toISOString(),
|
||||||
ffmpeg, player, resource, channelId: voiceChannelId, channelName,
|
ffmpeg, player, resource, channelId: voiceChannelId, channelName,
|
||||||
volume: vol,
|
volume: vol, prebufferTimeout,
|
||||||
});
|
});
|
||||||
|
|
||||||
broadcastState(guildId);
|
broadcastState(guildId);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue