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
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:
@@ -24,6 +24,10 @@
|
||||
},
|
||||
},
|
||||
},
|
||||
"trustedDependencies": [
|
||||
"@lng2004/node-datachannel",
|
||||
"node-av",
|
||||
],
|
||||
"packages": {
|
||||
"@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="],
|
||||
|
||||
|
||||
@@ -27,5 +27,9 @@
|
||||
"@types/node": "^22.7.0",
|
||||
"@types/qrcode": "^1.5.6",
|
||||
"typescript": "^5.6.3"
|
||||
}
|
||||
},
|
||||
"trustedDependencies": [
|
||||
"@lng2004/node-datachannel",
|
||||
"node-av"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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 <display>`, 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<void> {
|
||||
this.controller?.abort();
|
||||
this.controller = null;
|
||||
try {
|
||||
this.capture?.kill("SIGKILL");
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
this.capture = null;
|
||||
try {
|
||||
this.streamer?.leaveVoice?.();
|
||||
this.streamer?.client?.destroy?.();
|
||||
|
||||
Reference in New Issue
Block a user