Files
golfgame/tests/soak/core/screencaster.ts
adlee-was-taken 34ce7d1d32 feat(soak): Screencaster — CDP Page.startScreencast wrapper
Attach/detach CDP sessions per Playwright Page, start/stop JPEG
screencasts with configurable quality and frame rate, forward each
frame to a callback. Used by the dashboard for click-to-watch
live video (wired in Task 23).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 19:06:17 -04:00

90 lines
2.7 KiB
TypeScript

/**
* 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<string, CDPSession>();
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<void> {
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<void> {
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<void> {
const keys = Array.from(this.sessions.keys());
await Promise.all(keys.map((k) => this.stop(k)));
}
}