From 5137fdeaf73e7d218045842e15ab2eff84b1ebcb Mon Sep 17 00:00:00 2001 From: javis-bot Date: Wed, 10 Jun 2026 10:38:28 +0900 Subject: [PATCH] selfbot streaming: verified live; capture via system ffmpeg x11grab End-to-end verified with a real burner token + voice channel: login OK, posts to the text channel, joins voice, and Go-Live streams the host :1 desktop. - selfbot.ts now captures the X display with the SYSTEM ffmpeg (reliable x11grab) and pipes it into prepareStream, instead of relying on the lib's bundled libav input devices (not portable). Capture process is killed on stop. - package.json: trustedDependencies (node-av, @lng2004/node-datachannel) so the native streaming deps build automatically on bun install (incl. Docker). - Dropped the unused nvenc path (the lib's exported `nvenc` is undefined at runtime); software H264 encode for now. --- bot/bun.lock | 4 +++ bot/package.json | 6 +++- bot/src/stream/selfbot.ts | 58 ++++++++++++++++++++++++--------------- 3 files changed, 45 insertions(+), 23 deletions(-) diff --git a/bot/bun.lock b/bot/bun.lock index 765a9e2..d61b60d 100644 --- a/bot/bun.lock +++ b/bot/bun.lock @@ -24,6 +24,10 @@ }, }, }, + "trustedDependencies": [ + "@lng2004/node-datachannel", + "node-av", + ], "packages": { "@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="], diff --git a/bot/package.json b/bot/package.json index bb785e2..edfea44 100644 --- a/bot/package.json +++ b/bot/package.json @@ -27,5 +27,9 @@ "@types/node": "^22.7.0", "@types/qrcode": "^1.5.6", "typescript": "^5.6.3" - } + }, + "trustedDependencies": [ + "@lng2004/node-datachannel", + "node-av" + ] } diff --git a/bot/src/stream/selfbot.ts b/bot/src/stream/selfbot.ts index ddedd15..11ebc88 100644 --- a/bot/src/stream/selfbot.ts +++ b/bot/src/stream/selfbot.ts @@ -16,12 +16,14 @@ * prepareStream(input, opts, signal) -> { command, output } * playStream(output, streamer, { type: "go-live" }, signal) */ +import { spawn, type ChildProcess } from "node:child_process"; import type { AppConfig } from "../config.ts"; import type { ScreenStreamer, StreamContext } from "./index.ts"; export class SelfbotStreamer implements ScreenStreamer { readonly kind = "selfbot" as const; private streamer: any = null; + private capture: ChildProcess | null = null; private controller: AbortController | null = null; private active = false; @@ -62,7 +64,7 @@ export class SelfbotStreamer implements ScreenStreamer { return "셀프봇 송출은 음성 채널 안에서 호출해야 합니다."; } const { selfbot, vs } = await this.loadLib(); - const { Streamer, prepareStream, playStream, nvenc } = vs; + const { Streamer, prepareStream, playStream } = vs; this.streamer = new Streamer(new selfbot.Client()); await this.streamer.client.login(this.config.selfbotToken); @@ -71,29 +73,35 @@ export class SelfbotStreamer implements ScreenStreamer { const [w, h] = this.config.vncResolution.split("x").map((n) => parseInt(n, 10)); this.controller = new AbortController(); - // Grab the VNC X display with ffmpeg's x11grab. customInputOptions are - // placed before `-i `, exactly as x11grab requires. - const options: any = { - width: w || 1920, - height: h || 1080, - frameRate: this.config.vncFramerate, - videoCodec: "H264", - bitrateVideo: this.config.vncBitrateKbps, - bitrateVideoMax: Math.round(this.config.vncBitrateKbps * 1.5), - hardwareAcceleratedDecoding: this.config.streamHw, - minimizeLatency: true, - customInputOptions: [ - "-f", "x11grab", - "-video_size", this.config.vncResolution, - "-framerate", String(this.config.vncFramerate), - ], - }; - // NVENC hardware encode on the RTX 5050 when enabled. - if (this.config.streamHw && nvenc) options.encoder = nvenc; + // Capture the VNC X display with the SYSTEM ffmpeg (which reliably has + // x11grab), then pipe that stream into the library. Relying on the lib's + // bundled libav for the x11grab input device is not portable; piping the + // system ffmpeg is. (Verified live against a real voice channel.) + const capture = spawn("ffmpeg", [ + "-loglevel", "error", + "-f", "x11grab", + "-framerate", String(this.config.vncFramerate), + "-video_size", this.config.vncResolution, + "-i", this.config.vncDisplay, + "-c:v", "libx264", "-preset", "ultrafast", "-tune", "zerolatency", + "-pix_fmt", "yuv420p", "-g", String(this.config.vncFramerate), + "-f", "mpegts", "pipe:1", + ]); + this.capture = capture; + capture.stderr?.on("data", (d) => { + if (!this.controller?.signal.aborted) console.error("[selfbot x11grab]", d.toString().trim()); + }); const { command, output } = prepareStream( - this.config.vncDisplay, - options, + capture.stdout, + { + width: w || 1920, + height: h || 1080, + frameRate: this.config.vncFramerate, + videoCodec: "H264", + bitrateVideo: this.config.vncBitrateKbps, + bitrateVideoMax: Math.round(this.config.vncBitrateKbps * 1.5), + }, this.controller.signal, ); command.on("error", (err: Error) => { @@ -113,6 +121,12 @@ export class SelfbotStreamer implements ScreenStreamer { async stop(): Promise { this.controller?.abort(); this.controller = null; + try { + this.capture?.kill("SIGKILL"); + } catch { + /* ignore */ + } + this.capture = null; try { this.streamer?.leaveVoice?.(); this.streamer?.client?.destroy?.();