/** * Screencaster — CDP Page.startScreencast wrapper for live video. * * Attach a CDP session to a Playwright Page, start emitting JPEG * frames at the configured frame rate, forward each frame to a * callback, detach on stop. Used by the dashboard's click-to-watch * feature: when a user clicks a player tile, the runner calls * `start(key, page, frame => ws.send(...))`; when they close the * modal it calls `stop(key)`. */ import type { Page, CDPSession } from 'playwright-core'; import type { Logger } from './types'; export interface ScreencastOptions { format?: 'jpeg' | 'png'; quality?: number; maxWidth?: number; maxHeight?: number; everyNthFrame?: number; } export type FrameCallback = (jpegBase64: string) => void; export class Screencaster { private sessions = new Map(); constructor(private logger: Logger) {} /** * Attach a CDP session to the given page and start forwarding frames. * If a screencast is already running for this sessionKey, no-op. */ async start( sessionKey: string, page: Page, onFrame: FrameCallback, opts: ScreencastOptions = {}, ): Promise { if (this.sessions.has(sessionKey)) { this.logger.warn('screencast_already_running', { sessionKey }); return; } const client = await page.context().newCDPSession(page); this.sessions.set(sessionKey, client); client.on('Page.screencastFrame', async (evt: { data: string; sessionId: number }) => { try { onFrame(evt.data); await client.send('Page.screencastFrameAck', { sessionId: evt.sessionId }); } catch (err) { this.logger.warn('screencast_frame_error', { sessionKey, error: err instanceof Error ? err.message : String(err), }); } }); await client.send('Page.startScreencast', { format: opts.format ?? 'jpeg', quality: opts.quality ?? 60, maxWidth: opts.maxWidth ?? 640, maxHeight: opts.maxHeight ?? 360, everyNthFrame: opts.everyNthFrame ?? 2, }); this.logger.info('screencast_started', { sessionKey }); } async stop(sessionKey: string): Promise { const client = this.sessions.get(sessionKey); if (!client) return; try { await client.send('Page.stopScreencast'); await client.detach(); } catch (err) { this.logger.warn('screencast_stop_error', { sessionKey, error: err instanceof Error ? err.message : String(err), }); } this.sessions.delete(sessionKey); this.logger.info('screencast_stopped', { sessionKey }); } async stopAll(): Promise { const keys = Array.from(this.sessions.keys()); await Promise.all(keys.map((k) => this.stop(k))); } }