fix(selfbot): gate broadcast 'live' on real Go-Live WebRTC connect
isActive() was a local flag set at start() time, and start() fired playStream without awaiting the actual stream connection, so 'broadcasting' was reported even when nothing reached Discord (auto-start log + ffmpeg/NVENC running are not proof of transmission). start() now waits for the streaming library's real readiness signal (the stream connection's WebRTC reaching "connected") before declaring live. On timeout it logs a compact connection-state diagnostic, tears the local ffmpeg pipeline down immediately, and returns an explicit failure. isActive() reports the real live state. Timeout is config-driven (STREAM_READY_TIMEOUT_MS, default 25s). Adds a test for the timeout/teardown path and updates the existing leak-teardown test. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -33,6 +33,11 @@ test("a self-ended stream tears down the capture pipeline (no ffmpeg leak)", asy
|
||||
}
|
||||
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,
|
||||
@@ -46,6 +51,7 @@ test("a self-ended stream tears down the capture pipeline (no ffmpeg leak)", asy
|
||||
vncFramerate: 60,
|
||||
vncBitrateKbps: 8000,
|
||||
streamHw: true,
|
||||
streamReadyTimeoutMs: 2000,
|
||||
} as any);
|
||||
|
||||
await s.start({ guildId: "g", voiceChannelId: "v" } as any);
|
||||
@@ -59,3 +65,57 @@ test("a self-ended stream tears down the capture pipeline (no ffmpeg leak)", asy
|
||||
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<void>(() => {});
|
||||
|
||||
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",
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user