#!/usr/bin/env node /** * DreamStack Screencast — CDP Capture Agent * * Streams any web page to DreamStack panels via Chrome DevTools Protocol. * Zero changes to the target app — just point at any URL. * * Usage: * node capture.js [url] [--headless] [--fps=N] [--quality=N] * * Examples: * node capture.js http://localhost:3000 * node capture.js https://react.dev --headless --fps=30 */ const CDP = require('chrome-remote-interface'); const { WebSocketServer } = require('ws'); const http = require('http'); const { spawn } = require('child_process'); // ─── Config ─── const TARGET_URL = process.argv[2] || 'http://localhost:3000'; const WIDTH = 800; const HEIGHT = 1280; const WS_PORT = 9300; const MONITOR_PORT = 9301; const CDP_PORT = 9222; const QUALITY = parseInt((process.argv.find(a => a.startsWith('--quality=')) || '').split('=')[1] || '75'); const MAX_FPS = parseInt((process.argv.find(a => a.startsWith('--fps=')) || '').split('=')[1] || '30'); const HEADLESS = process.argv.includes('--headless'); const clients = new Set(); let frameCount = 0; let bytesSent = 0; const t0 = Date.now(); // ─── 1. Launch Chrome ─── function launchChrome() { return new Promise((resolve, reject) => { const args = [ `--remote-debugging-port=${CDP_PORT}`, `--window-size=${WIDTH},${HEIGHT}`, '--disable-gpu', '--no-first-run', '--no-default-browser-check', '--disable-extensions', '--disable-translate', '--disable-sync', '--disable-background-networking', '--disable-default-apps', '--mute-audio', '--no-sandbox', ]; if (HEADLESS) args.push('--headless=new'); args.push('about:blank'); const proc = spawn('google-chrome', args, { stdio: ['pipe', 'pipe', 'pipe'] }); proc.stderr.on('data', d => { if (d.toString().includes('DevTools listening')) resolve(proc); }); proc.on('error', reject); proc.on('exit', code => { console.log(`[Chrome] exit ${code}`); process.exit(0); }); // Fallback timeout setTimeout(() => resolve(proc), 4000); }); } // ─── 2. WebSocket server for panels/monitor ─── function startWS() { const wss = new WebSocketServer({ host: '0.0.0.0', port: WS_PORT }); wss.on('connection', (ws, req) => { clients.add(ws); console.log(`[WS] +1 panel (${clients.size}) from ${req.socket.remoteAddress}`); ws.on('close', () => { clients.delete(ws); console.log(`[WS] -1 panel (${clients.size})`); }); ws.on('message', data => { ws._inputHandler?.(data); }); }); console.log(`[WS] Panels: ws://0.0.0.0:${WS_PORT}`); return wss; } // ─── 3. Monitor page ─── function startMonitor() { const html = ` DreamStack Screencast

DreamStack Screencast Monitor

Connecting…
`; http.createServer((_, res) => { res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(html); }) .listen(MONITOR_PORT, '0.0.0.0', () => console.log(`[Monitor] http://0.0.0.0:${MONITOR_PORT}`)); } // ─── 4. CDP screencast loop ─── async function startScreencast() { let client; // Retry CDP connect (Chrome may still be starting) for (let i = 0; i < 10; i++) { try { client = await CDP({ port: CDP_PORT }); break; } catch { await new Promise(r => setTimeout(r, 1000)); } } if (!client) throw new Error('Cannot connect to Chrome CDP'); const { Page, Input, Emulation } = client; await Page.enable(); await Emulation.setDeviceMetricsOverride({ width: WIDTH, height: HEIGHT, deviceScaleFactor: 1, mobile: true, }); await Emulation.setTouchEmulationEnabled({ enabled: true }); await Page.navigate({ url: TARGET_URL }); await new Promise(r => setTimeout(r, 2000)); // let page load // Wire up input forwarding from panels for (const ws of clients) { ws._inputHandler = (data) => handleInput(Buffer.from(data), Input, Page); } // Also for future connections const origAdd = clients.add.bind(clients); clients.add = function (ws) { origAdd(ws); ws._inputHandler = (data) => handleInput(Buffer.from(data), Input, Page); }; // Start screencast await Page.startScreencast({ format: 'jpeg', quality: QUALITY, maxWidth: WIDTH, maxHeight: HEIGHT, everyNthFrame: Math.max(1, Math.round(60 / MAX_FPS)), }); // Listen for frames via the event API client.on('event', (message) => { if (message.method !== 'Page.screencastFrame') return; const { sessionId, data, metadata } = message.params; // ACK immediately (fire-and-forget) Page.screencastFrameAck({ sessionId }).catch(() => { }); frameCount++; const jpegBuf = Buffer.from(data, 'base64'); bytesSent += jpegBuf.length; // Build frame: [0x50][ts:u32LE][w:u16LE][h:u16LE][jpeg...] const hdr = Buffer.alloc(9); hdr[0] = 0x50; hdr.writeUInt32LE((Date.now() - t0) >>> 0, 1); hdr.writeUInt16LE(metadata.deviceWidth || WIDTH, 5); hdr.writeUInt16LE(metadata.deviceHeight || HEIGHT, 7); const frame = Buffer.concat([hdr, jpegBuf]); // Broadcast for (const ws of clients) { if (ws.readyState === 1) ws.send(frame); } if (frameCount % 60 === 0) { const elapsed = (Date.now() - t0) / 1000; console.log(`[Cast] #${frameCount} | ${(jpegBuf.length / 1024).toFixed(1)}KB | avg ${(bytesSent / 1024 / elapsed).toFixed(0)} KB/s | ${clients.size} panels`); } }); console.log(`[CDP] Casting ${TARGET_URL} → ${WIDTH}×${HEIGHT} @ q${QUALITY}`); } // ─── Input handler ─── function handleInput(buf, Input, Page) { if (buf.length < 1) return; const t = buf[0]; if (t === 0x60 && buf.length >= 7) { const phase = buf[1]; const x = buf.readUInt16LE(2), y = buf.readUInt16LE(4); const type = phase === 0 ? 'touchStart' : phase === 1 ? 'touchMove' : 'touchEnd'; Input.dispatchTouchEvent({ type, touchPoints: [{ x, y, id: buf[6] || 0, radiusX: 10, radiusY: 10, force: phase === 2 ? 0 : 1 }] }).catch(() => { }); } if (t === 0x61 && buf.length >= 6) { const x = buf.readUInt16LE(1), y = buf.readUInt16LE(3); Input.dispatchMouseEvent({ type: 'mousePressed', x, y, button: 'left', clickCount: 1 }).catch(() => { }); setTimeout(() => Input.dispatchMouseEvent({ type: 'mouseReleased', x, y, button: 'left', clickCount: 1 }).catch(() => { }), 50); } if (t === 0x63 && buf.length >= 3) { const len = buf.readUInt16LE(1); const url = buf.slice(3, 3 + len).toString(); Page.navigate({ url }).catch(() => { }); console.log(`[Nav] → ${url}`); } } // ─── Main ─── async function main() { console.log(`\n DreamStack Screencast`); console.log(` ─────────────────────`); console.log(` URL: ${TARGET_URL}`); console.log(` Viewport: ${WIDTH}×${HEIGHT}`); console.log(` Quality: ${QUALITY}% FPS: ${MAX_FPS}`); console.log(` Headless: ${HEADLESS}\n`); const chrome = await launchChrome(); startWS(); startMonitor(); await startScreencast(); console.log(`\n ✓ Streaming! Panels → ws://0.0.0.0:${WS_PORT}`); console.log(` ✓ Monitor → http://localhost:${MONITOR_PORT}\n`); process.on('SIGINT', () => { console.log('\n[Stop]'); chrome.kill(); process.exit(0); }); } main().catch(err => { console.error('Fatal:', err); process.exit(1); });