mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-02 04:52:50 +08:00
Merge pull request #15 from ZHANGTIANYAO1/claude/start-music-api-servers-o3HHa
Fix ffmpeg spawn failure causing infinite retry loop
This commit is contained in:
3 files changed
+45
-3
No files matched your search
+40
-3
@@ -37,15 +37,23 @@ function ffmpegWorks(bin: string): boolean {
|
|||||||
|
|
||||||
/** Resolved once at module load — prefer bundled ffmpeg-static, fall back to system. */
|
/** Resolved once at module load — prefer bundled ffmpeg-static, fall back to system. */
|
||||||
const resolvedFfmpeg: string = (() => {
|
const resolvedFfmpeg: string = (() => {
|
||||||
if (ffmpegPath && isExecutable(ffmpegPath) && ffmpegWorks(ffmpegPath)) {
|
// On Windows, ffmpeg-static may return a path with backslashes; on Linux/macOS
|
||||||
|
// it may return a Windows .exe path if node_modules was copied cross-platform.
|
||||||
|
const isWinPath = ffmpegPath ? /\\/.test(ffmpegPath) || ffmpegPath.endsWith(".exe") : false;
|
||||||
|
const onWindows = process.platform === "win32";
|
||||||
|
|
||||||
|
// Only try bundled binary if platform matches
|
||||||
|
if (ffmpegPath && (onWindows === isWinPath)) {
|
||||||
|
if (isExecutable(ffmpegPath) && ffmpegWorks(ffmpegPath)) {
|
||||||
return ffmpegPath;
|
return ffmpegPath;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// Fall back to system ffmpeg
|
// Fall back to system ffmpeg
|
||||||
if (ffmpegWorks("ffmpeg")) {
|
if (ffmpegWorks("ffmpeg")) {
|
||||||
return "ffmpeg";
|
return "ffmpeg";
|
||||||
}
|
}
|
||||||
// Last resort: return whatever we have, will fail at runtime with clear error
|
// Last resort: always use "ffmpeg" so spawn error is clear, never use a cross-platform path
|
||||||
return ffmpegPath ?? "ffmpeg";
|
return "ffmpeg";
|
||||||
})();
|
})();
|
||||||
|
|
||||||
/** Resolve ffmpeg binary: prefer bundled ffmpeg-static, fall back to system PATH. */
|
/** Resolve ffmpeg binary: prefer bundled ffmpeg-static, fall back to system PATH. */
|
||||||
@@ -79,6 +87,9 @@ export class AudioPlayer extends EventEmitter {
|
|||||||
private static readonly BUFFER_HIGH_WATER = 960 * 1024; // ~5s of PCM at 48kHz stereo
|
private static readonly BUFFER_HIGH_WATER = 960 * 1024; // ~5s of PCM at 48kHz stereo
|
||||||
private static readonly BUFFER_LOW_WATER = 480 * 1024; // ~2.5s
|
private static readonly BUFFER_LOW_WATER = 480 * 1024; // ~2.5s
|
||||||
private ffmpegPaused = false;
|
private ffmpegPaused = false;
|
||||||
|
private spawnFailed = false; // true if ffmpeg spawn errored (prevent trackEnd cascade)
|
||||||
|
private consecutiveFailures = 0;
|
||||||
|
private static readonly MAX_CONSECUTIVE_FAILURES = 3;
|
||||||
|
|
||||||
constructor(logger: Logger) {
|
constructor(logger: Logger) {
|
||||||
super();
|
super();
|
||||||
@@ -94,6 +105,18 @@ export class AudioPlayer extends EventEmitter {
|
|||||||
this.seekOffset = seekSeconds;
|
this.seekOffset = seekSeconds;
|
||||||
this.framesPlayed = 0;
|
this.framesPlayed = 0;
|
||||||
this.ffmpegPaused = false;
|
this.ffmpegPaused = false;
|
||||||
|
this.spawnFailed = false;
|
||||||
|
|
||||||
|
// Prevent rapid-fire spawn attempts when ffmpeg is broken
|
||||||
|
if (this.consecutiveFailures >= AudioPlayer.MAX_CONSECUTIVE_FAILURES) {
|
||||||
|
this.logger.error(
|
||||||
|
{ failures: this.consecutiveFailures, ffmpeg: getFfmpegCommand() },
|
||||||
|
"Too many consecutive ffmpeg failures — ffmpeg binary may be missing or broken. Stopping playback."
|
||||||
|
);
|
||||||
|
this.state = "idle";
|
||||||
|
this.emit("error", new Error("ffmpeg unavailable after repeated failures"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.logger.info({ url: url.slice(0, 80), seek: seekSeconds }, "Starting playback");
|
this.logger.info({ url: url.slice(0, 80), seek: seekSeconds }, "Starting playback");
|
||||||
|
|
||||||
@@ -160,6 +183,8 @@ export class AudioPlayer extends EventEmitter {
|
|||||||
this.ffmpeg.on("error", (err) => {
|
this.ffmpeg.on("error", (err) => {
|
||||||
this.logger.error({ err }, "FFmpeg error");
|
this.logger.error({ err }, "FFmpeg error");
|
||||||
if (this.sessionId === playSessionId) {
|
if (this.sessionId === playSessionId) {
|
||||||
|
this.spawnFailed = true;
|
||||||
|
this.consecutiveFailures++;
|
||||||
this.emit("error", err);
|
this.emit("error", err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -210,8 +235,14 @@ export class AudioPlayer extends EventEmitter {
|
|||||||
this.frameLoopRunning = false;
|
this.frameLoopRunning = false;
|
||||||
if (this.state !== "idle") {
|
if (this.state !== "idle") {
|
||||||
this.state = "idle";
|
this.state = "idle";
|
||||||
|
// Don't emit trackEnd if ffmpeg spawn failed — prevents infinite retry cascade
|
||||||
|
if (this.spawnFailed) {
|
||||||
|
this.logger.warn("Suppressing trackEnd due to ffmpeg spawn failure");
|
||||||
|
} else {
|
||||||
|
this.consecutiveFailures = 0; // Reset on successful track completion
|
||||||
this.emit("trackEnd");
|
this.emit("trackEnd");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -303,12 +334,18 @@ export class AudioPlayer extends EventEmitter {
|
|||||||
}
|
}
|
||||||
this.pcmBuffer = Buffer.alloc(0);
|
this.pcmBuffer = Buffer.alloc(0);
|
||||||
this.ffmpegPaused = false;
|
this.ffmpegPaused = false;
|
||||||
|
this.spawnFailed = false;
|
||||||
this.state = "idle";
|
this.state = "idle";
|
||||||
this.currentUrl = "";
|
this.currentUrl = "";
|
||||||
this.seekOffset = 0;
|
this.seekOffset = 0;
|
||||||
this.framesPlayed = 0;
|
this.framesPlayed = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Reset the consecutive failure counter (e.g. after user action) */
|
||||||
|
resetFailures(): void {
|
||||||
|
this.consecutiveFailures = 0;
|
||||||
|
}
|
||||||
|
|
||||||
setVolume(vol: number): void {
|
setVolume(vol: number): void {
|
||||||
this.volume = Math.max(0, Math.min(100, vol));
|
this.volume = Math.max(0, Math.min(100, vol));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -263,6 +263,8 @@ export class BotInstance extends EventEmitter {
|
|||||||
this.queue.add({ ...song, platform: provider.platform });
|
this.queue.add({ ...song, platform: provider.platform });
|
||||||
this.queue.play();
|
this.queue.play();
|
||||||
|
|
||||||
|
// Reset failure counter on user-initiated play
|
||||||
|
this.player.resetFailures();
|
||||||
const ok = await this.resolveAndPlay(this.queue.current()!);
|
const ok = await this.resolveAndPlay(this.queue.current()!);
|
||||||
if (!ok) return `Cannot play: ${song.name}`;
|
if (!ok) return `Cannot play: ${song.name}`;
|
||||||
return `Now playing: ${song.name} - ${song.artist}`;
|
return `Now playing: ${song.name} - ${song.artist}`;
|
||||||
|
|||||||
@@ -154,6 +154,7 @@ export function createPlayerRouter(
|
|||||||
}
|
}
|
||||||
const queue = bot.getQueueManager();
|
const queue = bot.getQueueManager();
|
||||||
bot.getPlayer().stop(); // Stop current playback first
|
bot.getPlayer().stop(); // Stop current playback first
|
||||||
|
bot.getPlayer().resetFailures(); // Reset on user-initiated play
|
||||||
const song = queue.playAt(index);
|
const song = queue.playAt(index);
|
||||||
if (!song) {
|
if (!song) {
|
||||||
res.status(400).json({ error: "Invalid queue index" });
|
res.status(400).json({ error: "Invalid queue index" });
|
||||||
@@ -200,6 +201,7 @@ export function createPlayerRouter(
|
|||||||
|
|
||||||
// Stop current playback
|
// Stop current playback
|
||||||
bot.getPlayer().stop();
|
bot.getPlayer().stop();
|
||||||
|
bot.getPlayer().resetFailures();
|
||||||
|
|
||||||
const songs = await provider.getPlaylistSongs(playlistId);
|
const songs = await provider.getPlaylistSongs(playlistId);
|
||||||
if (songs.length === 0) {
|
if (songs.length === 0) {
|
||||||
@@ -256,6 +258,7 @@ export function createPlayerRouter(
|
|||||||
queue.add({ ...song, platform: provider.platform });
|
queue.add({ ...song, platform: provider.platform });
|
||||||
queue.play();
|
queue.play();
|
||||||
|
|
||||||
|
bot.getPlayer().resetFailures();
|
||||||
const ok = await bot.resolveAndPlay(queue.current()!);
|
const ok = await bot.resolveAndPlay(queue.current()!);
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
res.json({ message: `Cannot play: ${song.name}` });
|
res.json({ message: `Cannot play: ${song.name}` });
|
||||||
|
|||||||
Reference in new issue
Block a user