fix: guard music playback process lifecycle

This commit is contained in:
MythEclipse
2026-05-15 17:23:36 +07:00
parent 9e07a0a1f3
commit 1e0a00d82d
2 changed files with 29 additions and 3 deletions

View File

@@ -21,16 +21,23 @@ export function createMusicPlayer(
return { return {
play(source: ResolvedMediaSource): MusicPlayback { play(source: ResolvedMediaSource): MusicPlayback {
if (!audioPlayer.isConnected()) {
throw new Error("Discord audio player is not connected");
}
const proc = spawn("ffmpeg", buildFfmpegArgs(source.source), { const proc = spawn("ffmpeg", buildFfmpegArgs(source.source), {
stdio: ["ignore", "pipe", "pipe"], stdio: ["ignore", "pipe", "pipe"],
}) as unknown as ChildProcessWithoutNullStreams; }) as unknown as ChildProcessWithoutNullStreams;
proc.stderr.resume();
audioPlayer.playStream(proc.stdout); audioPlayer.playStream(proc.stdout);
let stopped = false;
const done = new Promise<void>((resolve, reject) => { const done = new Promise<void>((resolve, reject) => {
proc.on("error", reject); proc.on("error", reject);
proc.stdout.on("error", reject);
proc.on("close", (code) => { proc.on("close", (code) => {
if (code === 0) { if (code === 0 || stopped) {
resolve(); resolve();
return; return;
} }
@@ -41,6 +48,8 @@ export function createMusicPlayer(
return { return {
done, done,
stop() { stop() {
if (stopped) return;
stopped = true;
proc.kill("SIGTERM"); proc.kill("SIGTERM");
audioPlayer.stop(); audioPlayer.stop();
}, },

View File

@@ -54,7 +54,22 @@ describe("createMusicPlayer", () => {
expect(discordPlayer.playStream).toHaveBeenCalledWith(proc.stdout); expect(discordPlayer.playStream).toHaveBeenCalledWith(proc.stdout);
}); });
it("kills ffmpeg and stops Discord playback", () => { it("rejects playback when Discord is not connected", () => {
const spawn = vi.fn(() => new FakeProcess());
const discordPlayer: DiscordAudioPlayer = {
isConnected: () => false,
playStream: vi.fn(),
stop: vi.fn(),
};
const player = createMusicPlayer({ spawn, discordPlayer });
expect(() =>
player.play({ source: "/tmp/song.ogg", title: "song.ogg", kind: "local" }),
).toThrow("Discord audio player is not connected");
expect(spawn).not.toHaveBeenCalled();
});
it("kills ffmpeg and stops Discord playback once", () => {
const proc = new FakeProcess(); const proc = new FakeProcess();
const discordPlayer: DiscordAudioPlayer = { const discordPlayer: DiscordAudioPlayer = {
isConnected: () => true, isConnected: () => true,
@@ -65,8 +80,10 @@ describe("createMusicPlayer", () => {
const playback = player.play({ source: "/tmp/song.ogg", title: "song.ogg", kind: "local" }); const playback = player.play({ source: "/tmp/song.ogg", title: "song.ogg", kind: "local" });
playback.stop(); playback.stop();
playback.stop();
expect(proc.kill).toHaveBeenCalledTimes(1);
expect(proc.kill).toHaveBeenCalledWith("SIGTERM"); expect(proc.kill).toHaveBeenCalledWith("SIGTERM");
expect(discordPlayer.stop).toHaveBeenCalled(); expect(discordPlayer.stop).toHaveBeenCalledTimes(1);
}); });
}); });