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:
javis-bot
2026-06-13 15:22:52 +09:00
parent 6f4464b2c4
commit 5961faed38
3 changed files with 164 additions and 13 deletions

View File

@@ -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);