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 { spawn, type ChildProcess } from 'node:child_process';
|
||||
import { PassThrough } from 'node:stream';
|
||||
import {
|
||||
joinVoiceChannel, createAudioPlayer, createAudioResource,
|
||||
VoiceConnectionStatus, StreamType, getVoiceConnection, entersState,
|
||||
|
|
@ -37,6 +38,7 @@ interface GuildRadioState {
|
|||
channelId: string;
|
||||
channelName: string;
|
||||
volume: number;
|
||||
prebufferTimeout?: ReturnType<typeof setTimeout>;
|
||||
}
|
||||
|
||||
interface Favorite {
|
||||
|
|
@ -75,6 +77,7 @@ function setFavorites(favs: Favorite[]): void {
|
|||
function stopAudio(guildId: string): void {
|
||||
const state = guildRadioState.get(guildId);
|
||||
if (!state) return;
|
||||
if (state.prebufferTimeout) clearTimeout(state.prebufferTimeout);
|
||||
try { state.ffmpeg.kill('SIGKILL'); } catch {}
|
||||
try { state.player.stop(true); } catch {}
|
||||
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)
|
||||
const vol = getVolume(guildId);
|
||||
const resource = createAudioResource(ffmpeg.stdout!, {
|
||||
const resource = createAudioResource(bufferedStream, {
|
||||
inputType: StreamType.Raw,
|
||||
inlineVolume: true,
|
||||
});
|
||||
resource.volume?.setVolume(vol);
|
||||
|
||||
const player = createAudioPlayer();
|
||||
player.play(resource);
|
||||
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) => {
|
||||
console.error(`[Radio] Player error:`, err.message);
|
||||
stopStream(guildId);
|
||||
|
|
@ -191,7 +229,7 @@ async function startStream(
|
|||
stationId, stationName, placeName, country,
|
||||
streamUrl, startedAt: new Date().toISOString(),
|
||||
ffmpeg, player, resource, channelId: voiceChannelId, channelName,
|
||||
volume: vol,
|
||||
volume: vol, prebufferTimeout,
|
||||
});
|
||||
|
||||
broadcastState(guildId);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue