fix(selfbot): smooth VNC capture via keepalive + stop ffmpeg leak on stream end
The Go-Live broadcast looked badly choppy: video and scrolling stuttered while the cursor stayed smooth. Root cause is TigerVNC: it only refreshes its framebuffer while a VNC client is attached, but the broadcast reads that framebuffer with x11grab (not as a VNC client). With no viewer attached the captured screen idled at ~1.5 fps (measured 3/30 distinct frames); the cursor looked smooth only because x11grab overlays the live cursor on every frame. - Add a headless RFB keepalive (vnc-keepalive.ts) that stays connected for the life of the stream and requests incremental framebuffer updates at the stream framerate. SelfbotStreamer starts it on broadcast start and tears it down on stop/self-end. Measured 3/30 -> 57/60 distinct frames at 60 fps. Fail-open; authenticates with VNC_PASSWORD or the ~/.config/tigervnc/passwd file. - Fix a resource leak: when the Go-Live ended on its own, only the active flag was cleared, leaving the x11grab->nvenc ffmpeg running forever (pinning a CPU core while no media was transmitted, with only the gateway TCP left and no UDP media). The self-end path now tears down capture, keepalive and voice like stop() does. - Tests for both paths (self-end teardown; keepalive DES auth, port mapping, password resolution). Add @types/bun so bun:test typechecks; document the keepalive and recommended Chrome flags in README and .env.example. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
61
bot/src/stream/selfbot.test.ts
Normal file
61
bot/src/stream/selfbot.test.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
// 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<void>((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;
|
||||
},
|
||||
prepareStream: () => ({ command: { on() {} }, output: {} }),
|
||||
playStream: () => playPromise,
|
||||
}));
|
||||
|
||||
const { SelfbotStreamer } = await import("./selfbot.ts");
|
||||
const s = new SelfbotStreamer({
|
||||
selfbotToken: "token",
|
||||
vncDisplay: ":1",
|
||||
vncResolution: "1920x1080",
|
||||
vncFramerate: 60,
|
||||
vncBitrateKbps: 8000,
|
||||
streamHw: true,
|
||||
} 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);
|
||||
Reference in New Issue
Block a user