/** * Screenshot backend (safe, no ban risk, not real-time). * * Periodically grabs a frame from the VNC X display with ffmpeg's x11grab and * posts it to the invoking text channel. Low FPS, but works with a normal bot * account and never touches Discord's selfbot surface. */ import { spawn } from "node:child_process"; import type { AppConfig } from "../config.ts"; import type { ScreenStreamer, StreamContext } from "./index.ts"; function grabFrame(display: string, size: string): Promise { return new Promise((resolve, reject) => { const ff = spawn("ffmpeg", [ "-loglevel", "error", "-f", "x11grab", "-video_size", size, "-i", display, "-frames:v", "1", "-f", "image2pipe", "-vcodec", "png", "pipe:1", ]); const chunks: Buffer[] = []; ff.stdout.on("data", (c) => chunks.push(c)); ff.on("error", reject); ff.on("close", (code) => code === 0 ? resolve(Buffer.concat(chunks)) : reject(new Error(`ffmpeg exited ${code}`)), ); }); } export class ScreenshotStreamer implements ScreenStreamer { readonly kind = "screenshot" as const; private timer: ReturnType | null = null; constructor(private config: AppConfig) {} isActive() { return this.timer !== null; } async start(ctx: StreamContext): Promise { if (!ctx.postImage) return "스크린샷을 올릴 텍스트 채널 컨텍스트가 없습니다."; if (this.timer) return "이미 스크린샷 송출 중입니다."; const tick = async () => { try { const png = await grabFrame(this.config.vncDisplay, this.config.vncResolution); await ctx.postImage!(png, "vnc.png"); } catch (e) { console.error("[screenshot] grab failed:", e); } }; this.timer = setInterval(tick, this.config.screenshotIntervalSec * 1000); void tick(); return `📸 ${this.config.screenshotIntervalSec}초마다 VNC 스크린샷을 이 채널에 올립니다.`; } async stop(): Promise { if (this.timer) clearInterval(this.timer); this.timer = null; } }