diff --git a/streamer/src/chrome.ts b/streamer/src/chrome.ts new file mode 100644 index 0000000..f5acf86 --- /dev/null +++ b/streamer/src/chrome.ts @@ -0,0 +1,60 @@ +import { EventEmitter } from "node:events"; +import puppeteer, { Browser, CDPSession, Page } from "puppeteer-core"; + +export interface ChromeOpts { + pageUrl: string; + width: number; + height: number; + framerate: number; + jpegQuality?: number; // 0..100, default 85 + executablePath?: string; // default /usr/bin/chromium +} + +export class ChromeRenderer extends EventEmitter { + private browser: Browser | null = null; + private page: Page | null = null; + private cdp: CDPSession | null = null; + + constructor(private opts: ChromeOpts) { super(); } + + async start(): Promise { + this.browser = await puppeteer.launch({ + executablePath: this.opts.executablePath ?? "/usr/bin/chromium", + headless: true, + args: [ + "--no-sandbox", + "--disable-dev-shm-usage", + "--disable-gpu", + "--hide-scrollbars", + `--window-size=${this.opts.width},${this.opts.height}`, + "--autoplay-policy=no-user-gesture-required", + ], + defaultViewport: { width: this.opts.width, height: this.opts.height }, + }); + this.page = await this.browser.newPage(); + await this.page.goto(this.opts.pageUrl, { waitUntil: "networkidle2" }); + + this.cdp = await this.page.target().createCDPSession(); + await this.cdp.send("Page.startScreencast", { + format: "jpeg", + quality: this.opts.jpegQuality ?? 85, + everyNthFrame: 1, + }); + + this.cdp.on("Page.screencastFrame", async (frame) => { + const buf = Buffer.from(frame.data, "base64"); + this.emit("frame", buf); + await this.cdp!.send("Page.screencastFrameAck", { sessionId: frame.sessionId }); + }); + + this.browser.on("disconnected", () => this.emit("disconnected")); + } + + async stop(): Promise { + try { await this.cdp?.send("Page.stopScreencast"); } catch { /* ignore */ } + await this.browser?.close().catch(() => {}); + this.browser = null; + this.page = null; + this.cdp = null; + } +}