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>
This commit is contained in:
89
tests/soak/core/screencaster.ts
Normal file
89
tests/soak/core/screencaster.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* 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)));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user