selfbot streaming: verified live; capture via system ffmpeg x11grab
Some checks failed
Release / build-windows (push) Blocked by required conditions
Release / build-macos (arm64, macos-latest) (push) Blocked by required conditions
Release / build-macos (x64, macos-15-intel) (push) Blocked by required conditions
Release / release-main (push) Blocked by required conditions
Release / release-develop (push) Blocked by required conditions
Release / semantic-release (push) Successful in 24s
tests / Unit tests (Linux, Python 3.11) (push) Successful in 10m1s
Release / build-linux (push) Failing after 7m35s

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.
This commit is contained in:
javis-bot
2026-06-10 10:38:28 +09:00
parent 7aac92fc2c
commit 5137fdeaf7
3 changed files with 45 additions and 23 deletions

View File

@@ -24,6 +24,10 @@
}, },
}, },
}, },
"trustedDependencies": [
"@lng2004/node-datachannel",
"node-av",
],
"packages": { "packages": {
"@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="], "@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="],

View File

@@ -27,5 +27,9 @@
"@types/node": "^22.7.0", "@types/node": "^22.7.0",
"@types/qrcode": "^1.5.6", "@types/qrcode": "^1.5.6",
"typescript": "^5.6.3" "typescript": "^5.6.3"
} },
"trustedDependencies": [
"@lng2004/node-datachannel",
"node-av"
]
} }

View File

@@ -16,12 +16,14 @@
* prepareStream(input, opts, signal) -> { command, output } * prepareStream(input, opts, signal) -> { command, output }
* playStream(output, streamer, { type: "go-live" }, signal) * playStream(output, streamer, { type: "go-live" }, signal)
*/ */
import { spawn, type ChildProcess } from "node:child_process";
import type { AppConfig } from "../config.ts"; import type { AppConfig } from "../config.ts";
import type { ScreenStreamer, StreamContext } from "./index.ts"; import type { ScreenStreamer, StreamContext } from "./index.ts";
export class SelfbotStreamer implements ScreenStreamer { export class SelfbotStreamer implements ScreenStreamer {
readonly kind = "selfbot" as const; readonly kind = "selfbot" as const;
private streamer: any = null; private streamer: any = null;
private capture: ChildProcess | null = null;
private controller: AbortController | null = null; private controller: AbortController | null = null;
private active = false; private active = false;
@@ -62,7 +64,7 @@ export class SelfbotStreamer implements ScreenStreamer {
return "셀프봇 송출은 음성 채널 안에서 호출해야 합니다."; return "셀프봇 송출은 음성 채널 안에서 호출해야 합니다.";
} }
const { selfbot, vs } = await this.loadLib(); const { selfbot, vs } = await this.loadLib();
const { Streamer, prepareStream, playStream, nvenc } = vs; const { Streamer, prepareStream, playStream } = vs;
this.streamer = new Streamer(new selfbot.Client()); this.streamer = new Streamer(new selfbot.Client());
await this.streamer.client.login(this.config.selfbotToken); 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)); const [w, h] = this.config.vncResolution.split("x").map((n) => parseInt(n, 10));
this.controller = new AbortController(); this.controller = new AbortController();
// Grab the VNC X display with ffmpeg's x11grab. customInputOptions are // Capture the VNC X display with the SYSTEM ffmpeg (which reliably has
// placed before `-i <display>`, exactly as x11grab requires. // x11grab), then pipe that stream into the library. Relying on the lib's
const options: any = { // bundled libav for the x11grab input device is not portable; piping the
width: w || 1920, // system ffmpeg is. (Verified live against a real voice channel.)
height: h || 1080, const capture = spawn("ffmpeg", [
frameRate: this.config.vncFramerate, "-loglevel", "error",
videoCodec: "H264", "-f", "x11grab",
bitrateVideo: this.config.vncBitrateKbps, "-framerate", String(this.config.vncFramerate),
bitrateVideoMax: Math.round(this.config.vncBitrateKbps * 1.5), "-video_size", this.config.vncResolution,
hardwareAcceleratedDecoding: this.config.streamHw, "-i", this.config.vncDisplay,
minimizeLatency: true, "-c:v", "libx264", "-preset", "ultrafast", "-tune", "zerolatency",
customInputOptions: [ "-pix_fmt", "yuv420p", "-g", String(this.config.vncFramerate),
"-f", "x11grab", "-f", "mpegts", "pipe:1",
"-video_size", this.config.vncResolution, ]);
"-framerate", String(this.config.vncFramerate), this.capture = capture;
], capture.stderr?.on("data", (d) => {
}; if (!this.controller?.signal.aborted) console.error("[selfbot x11grab]", d.toString().trim());
// NVENC hardware encode on the RTX 5050 when enabled. });
if (this.config.streamHw && nvenc) options.encoder = nvenc;
const { command, output } = prepareStream( const { command, output } = prepareStream(
this.config.vncDisplay, capture.stdout,
options, {
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, this.controller.signal,
); );
command.on("error", (err: Error) => { command.on("error", (err: Error) => {
@@ -113,6 +121,12 @@ export class SelfbotStreamer implements ScreenStreamer {
async stop(): Promise<void> { async stop(): Promise<void> {
this.controller?.abort(); this.controller?.abort();
this.controller = null; this.controller = null;
try {
this.capture?.kill("SIGKILL");
} catch {
/* ignore */
}
this.capture = null;
try { try {
this.streamer?.leaveVoice?.(); this.streamer?.leaveVoice?.();
this.streamer?.client?.destroy?.(); this.streamer?.client?.destroy?.();