fix: guard music playback process lifecycle
This commit is contained in:
@@ -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();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
Reference in New Issue
Block a user