// renderer/index.js // Node 18+ recommended // npm install express playwright import express from "express"; import bodyParser from "body-parser"; import { chromium } from "playwright"; const PORT = process.env.PORT || 3000; const TARGET = process.env.TARGET || "https://example.com"; // fallback const SCREENSHOT_INTERVAL_MS = 100; // 10 fps const app = express(); app.use(bodyParser.json({ limit: "1mb" })); let latestFrameBase64 = null; let page = null; let browser = null; async function startBrowser() { browser = await chromium.launch({ headless: true, args: ["--no-sandbox"] }); const context = await browser.newContext({ viewport: { width: 1280, height: 720 }, // tune to desired size }); page = await context.newPage(); await page.goto(TARGET, { waitUntil: "networkidle" }); // optional: hide cursor, inject CSS, etc. // keep capturing setInterval(async () => { try { const buf = await page.screenshot({ type: "png", fullPage: false }); latestFrameBase64 = buf.toString("base64"); } catch (err) { console.error("screenshot error:", err); } }, SCREENSHOT_INTERVAL_MS); } app.get("/health", (req, res) => res.json({ ok: true })); // Return the most recent frame as JSON { image: "" } app.get("/frame", (req, res) => { if (!latestFrameBase64) return res.status(503).json({ error: "no_frame_yet" }); res.json({ image: latestFrameBase64 }); }); // Input event POST body (example): // { type: "mouse", event: "move|down|up", x: 100, y: 200, button: "left" } // { type: "keyboard", event: "down|up", key: "a" } app.post("/input", async (req, res) => { const ev = req.body; if (!page) return res.status(503).json({ error: "page_not_ready" }); try { if (ev.type === "mouse") { const x = Math.round(ev.x), y = Math.round(ev.y); if (ev.event === "move") { await page.mouse.move(x, y); } else if (ev.event === "down") { await page.mouse.down({ button: ev.button || "left" }); } else if (ev.event === "up") { await page.mouse.up({ button: ev.button || "left" }); } else if (ev.event === "click") { await page.mouse.click(x, y, { button: ev.button || "left" }); } } else if (ev.type === "keyboard") { if (ev.event === "down") await page.keyboard.down(ev.key); else if (ev.event === "up") await page.keyboard.up(ev.key); else if (ev.event === "press") await page.keyboard.press(ev.key); } else if (ev.type === "navigate") { // navigate to new URL if (ev.url) { await page.goto(ev.url, { waitUntil: "networkidle" }); } } res.json({ ok: true }); } catch (err) { console.error("input handler err:", err); res.status(500).json({ error: err.toString() }); } }); app.listen(PORT, async () => { console.log(`Renderer listening on ${PORT}, target=${TARGET}`); await startBrowser(); });