// Regression: when the Go-Live stream ends on its own (Discord closes it, the // voice UDP drops, or ffmpeg exits) instead of via stop(), the capture ffmpeg // MUST be killed and voice left. Otherwise the x11grab->nvenc encoder keeps // running forever, feeding a pipe nobody reads, pinning a CPU core while no // media is actually transmitted (observed live: 0 UDP sockets, 100% CPU). import { test, expect, mock } from "bun:test"; test("a self-ended stream tears down the capture pipeline (no ffmpeg leak)", async () => { const kill = mock(() => {}); const leaveVoice = mock(() => {}); const destroy = mock(() => {}); // Controllable Go-Live: resolve endStream() to simulate Discord closing it. let endStream!: () => void; const playPromise = new Promise((r) => { endStream = r; }); mock.module("node:child_process", () => ({ spawn: () => ({ stdout: {}, stderr: { on() {} }, kill }), })); mock.module("discord.js-selfbot-v13", () => ({ Client: class { destroy = destroy; async login() {} }, })); mock.module("@dank074/discord-video-stream", () => ({ Streamer: class { client: any; constructor(c: any) { this.client = c; } async joinVoice() {} leaveVoice = leaveVoice; // The Go-Live readiness signal start() now waits on: the stream // connection's WebRTC reaching "connected". get voiceConnection() { return { streamConnection: { webRtcConn: { ready: true } } }; } }, prepareStream: () => ({ command: { on() {} }, output: {} }), playStream: () => playPromise, })); const { SelfbotStreamer } = await import("./selfbot.ts"); const s = new SelfbotStreamer({ selfbotToken: "token", streamToken: "stream-token", // dedicated broadcast account -> broadcast allowed botToken: "", vncDisplay: ":1", vncResolution: "1920x1080", vncFramerate: 60, vncBitrateKbps: 8000, streamHw: true, streamReadyTimeoutMs: 2000, } as any); await s.start({ guildId: "g", voiceChannelId: "v" } as any); expect(s.isActive()).toBe(true); // Discord closes the Go-Live on its own (not a stop() call). endStream(); await new Promise((r) => setTimeout(r, 0)); expect(kill).toHaveBeenCalled(); // capture ffmpeg killed -> no CPU-burning orphan expect(leaveVoice).toHaveBeenCalled(); // voice connection released expect(s.isActive()).toBe(false); }, 30000); test("a Go-Live that never connects is reported as failed, not live, and is torn down", async () => { const kill = mock(() => {}); const leaveVoice = mock(() => {}); const destroy = mock(() => {}); // playStream never resolves (the stream is "running" locally) but the WebRTC // never reaches "connected" -> start() must time out rather than claim live. const neverEnds = new Promise(() => {}); mock.module("node:child_process", () => ({ spawn: () => ({ stdout: {}, stderr: { on() {} }, kill }), })); mock.module("discord.js-selfbot-v13", () => ({ Client: class { destroy = destroy; async login() {} }, })); mock.module("@dank074/discord-video-stream", () => ({ Streamer: class { client: any; constructor(c: any) { this.client = c; } async joinVoice() {} leaveVoice = leaveVoice; // Stream connection exists but its WebRTC never connects. get voiceConnection() { return { streamConnection: { webRtcConn: { ready: false } } }; } }, prepareStream: () => ({ command: { on() {} }, output: {} }), playStream: () => neverEnds, })); const { SelfbotStreamer } = await import("./selfbot.ts"); const s = new SelfbotStreamer({ selfbotToken: "token", streamToken: "stream-token", // dedicated broadcast account -> broadcast allowed botToken: "", vncDisplay: ":1", vncResolution: "1920x1080", vncFramerate: 60, vncBitrateKbps: 8000, streamHw: true, streamReadyTimeoutMs: 800, } as any); const msg = await s.start({ guildId: "g", voiceChannelId: "v" } as any); expect(s.isActive()).toBe(false); // never claimed live expect(msg).toContain("방송을 시작하지 못했습니다"); // explicit failure status expect(kill).toHaveBeenCalled(); // local ffmpeg torn down on failure expect(leaveVoice).toHaveBeenCalled(); }, 30000);