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": {
|
"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=="],
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
// 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(
|
||||||
|
capture.stdout,
|
||||||
|
{
|
||||||
width: w || 1920,
|
width: w || 1920,
|
||||||
height: h || 1080,
|
height: h || 1080,
|
||||||
frameRate: this.config.vncFramerate,
|
frameRate: this.config.vncFramerate,
|
||||||
videoCodec: "H264",
|
videoCodec: "H264",
|
||||||
bitrateVideo: this.config.vncBitrateKbps,
|
bitrateVideo: this.config.vncBitrateKbps,
|
||||||
bitrateVideoMax: Math.round(this.config.vncBitrateKbps * 1.5),
|
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;
|
|
||||||
|
|
||||||
const { command, output } = prepareStream(
|
|
||||||
this.config.vncDisplay,
|
|
||||||
options,
|
|
||||||
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?.();
|
||||||
|
|||||||
Reference in New Issue
Block a user