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:
53
bot/src/stream/vnc-keepalive.test.ts
Normal file
53
bot/src/stream/vnc-keepalive.test.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { test, expect } from "bun:test";
|
||||
import crypto from "node:crypto";
|
||||
import {
|
||||
decodeVncPassword,
|
||||
vncChallengeResponse,
|
||||
vncPortForDisplay,
|
||||
resolveVncPassword,
|
||||
} from "./vnc-keepalive.ts";
|
||||
|
||||
// Independent reference for VNC's bit-reversed-key DES, to cross-check the module.
|
||||
const rev = (b: number) => {
|
||||
let r = 0;
|
||||
for (let i = 0; i < 8; i++) r = (r << 1) | ((b >> i) & 1);
|
||||
return r & 0xff;
|
||||
};
|
||||
const vncKey = (buf: Buffer) => Buffer.from([...buf.subarray(0, 8)].map(rev));
|
||||
const desEnc = (key: Buffer, data: Buffer) => {
|
||||
const c = crypto.createCipheriv("des-ecb", key, null);
|
||||
c.setAutoPadding(false);
|
||||
return Buffer.concat([c.update(data), c.final()]);
|
||||
};
|
||||
const FIXED_KEY = Buffer.from([23, 82, 107, 6, 35, 78, 88, 7]);
|
||||
|
||||
test("decodeVncPassword inverts the fixed-key obfuscation", () => {
|
||||
const pw = Buffer.from("s3cr3t\0\0", "binary"); // 8 bytes, trailing nulls
|
||||
const obf = desEnc(vncKey(FIXED_KEY), pw); // how vncpasswd stores it
|
||||
expect(decodeVncPassword(obf).toString()).toBe("s3cr3t");
|
||||
});
|
||||
|
||||
test("vncChallengeResponse encrypts both challenge blocks with the bit-reversed password key", () => {
|
||||
const pw = Buffer.from("hunter12");
|
||||
const challenge = crypto.randomBytes(16);
|
||||
const expected = desEnc(vncKey(pw), challenge);
|
||||
const got = vncChallengeResponse(pw, challenge);
|
||||
expect(got.length).toBe(16);
|
||||
expect(got.equals(expected)).toBe(true);
|
||||
});
|
||||
|
||||
test("vncPortForDisplay maps an X display to its RFB port", () => {
|
||||
expect(vncPortForDisplay(":1")).toBe(5901);
|
||||
expect(vncPortForDisplay(":0")).toBe(5900);
|
||||
expect(vncPortForDisplay(":5")).toBe(5905);
|
||||
});
|
||||
|
||||
test("resolveVncPassword prefers the VNC_PASSWORD env var", () => {
|
||||
const pw = resolveVncPassword({ VNC_PASSWORD: "letmein9" } as NodeJS.ProcessEnv);
|
||||
expect(pw?.toString()).toBe("letmein9");
|
||||
});
|
||||
|
||||
test("resolveVncPassword returns null when nothing is available", () => {
|
||||
const pw = resolveVncPassword({ VNC_PASSWD_FILE: "/nonexistent/path/xyz" } as NodeJS.ProcessEnv);
|
||||
expect(pw).toBeNull();
|
||||
});
|
||||
Reference in New Issue
Block a user