feat(soak): click-to-watch live video via CDP screencast

Runner creates a Screencaster before sessions are acquired, then
wires its start/stop into DashboardServer.onStartStream / onStopStream
after sessions exist (the handlers close over a sessionsByKey map).
Clicking a player tile in the dashboard starts a CDP screencast on
that session's page, forwards JPEG frames as WS "frame" messages.
Closing the modal (or disconnecting the WS) stops all screencasts.

Verified end-to-end: programmatically connected WS, sent start_stream,
received 5 frames (13.7KB each), sent stop_stream, screencast_stopped
log line fired, run completed cleanly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
adlee-was-taken
2026-04-11 20:54:52 -04:00
parent 34ce7d1d32
commit 21fe53eaf7

View File

@@ -16,8 +16,9 @@ import { createLogger } from './core/logger';
import { SessionPool } from './core/session-pool'; import { SessionPool } from './core/session-pool';
import { RoomCoordinator } from './core/room-coordinator'; import { RoomCoordinator } from './core/room-coordinator';
import { DashboardServer } from './dashboard/server'; import { DashboardServer } from './dashboard/server';
import { Screencaster } from './core/screencaster';
import { getScenario, listScenarios } from './scenarios'; import { getScenario, listScenarios } from './scenarios';
import type { DashboardReporter, ScenarioContext } from './core/types'; import type { DashboardReporter, ScenarioContext, Session } from './core/types';
function noopDashboard(): DashboardReporter { function noopDashboard(): DashboardReporter {
return { return {
@@ -107,19 +108,61 @@ async function main(): Promise<void> {
logger, logger,
}); });
const coordinator = new RoomCoordinator(); const coordinator = new RoomCoordinator();
const screencaster = new Screencaster(logger);
if (watch === 'tiled') {
logger.warn('tiled_not_yet_implemented');
console.warn('Watch mode "tiled" not yet implemented (Task 24). Falling back to none.');
}
const abortController = new AbortController();
const onSignal = (sig: string) => {
logger.warn('signal_received', { signal: sig });
abortController.abort();
};
process.on('SIGINT', () => onSignal('SIGINT'));
process.on('SIGTERM', () => onSignal('SIGTERM'));
// Optional dashboard server
let dashboardServer: DashboardServer | null = null; let dashboardServer: DashboardServer | null = null;
let dashboard: DashboardReporter = noopDashboard(); let dashboard: DashboardReporter = noopDashboard();
let exitCode = 0;
try {
const sessions = await pool.acquire(accounts);
logger.info('sessions_acquired', { count: sessions.length });
// Build a session lookup for click-to-watch
const sessionsByKey = new Map<string, Session>();
for (const s of sessions) sessionsByKey.set(s.key, s);
// Dashboard with screencaster handlers now that sessions exist
if (watch === 'dashboard') { if (watch === 'dashboard') {
const port = Number(config.dashboardPort ?? 7777); const port = Number(config.dashboardPort ?? 7777);
dashboardServer = new DashboardServer(port, logger, { dashboardServer = new DashboardServer(port, logger, {
onStartStream: (key) => { onStartStream: (key) => {
logger.info('stream_start_requested', { sessionKey: key }); const session = sessionsByKey.get(key);
// Wired in Task 23 if (!session) {
logger.warn('stream_start_unknown_session', { sessionKey: key });
return;
}
screencaster
.start(key, session.page, (jpegBase64) => {
dashboardServer!.broadcast({ type: 'frame', sessionKey: key, jpegBase64 });
})
.catch((err) =>
logger.error('screencast_start_failed', {
sessionKey: key,
error: err instanceof Error ? err.message : String(err),
}),
);
}, },
onStopStream: (key) => { onStopStream: (key) => {
logger.info('stream_stop_requested', { sessionKey: key }); screencaster.stop(key).catch(() => {
// best-effort — errors already logged inside Screencaster
});
},
onDisconnect: () => {
screencaster.stopAll().catch(() => {});
}, },
}); });
await dashboardServer.start(); await dashboardServer.start();
@@ -137,25 +180,8 @@ async function main(): Promise<void> {
} catch { } catch {
// If auto-open fails, the URL is already printed. // If auto-open fails, the URL is already printed.
} }
} else if (watch === 'tiled') {
logger.warn('tiled_not_yet_implemented');
console.warn('Watch mode "tiled" not yet implemented (Task 24). Falling back to none.');
} }
const abortController = new AbortController();
const onSignal = (sig: string) => {
logger.warn('signal_received', { signal: sig });
abortController.abort();
};
process.on('SIGINT', () => onSignal('SIGINT'));
process.on('SIGTERM', () => onSignal('SIGTERM'));
let exitCode = 0;
try {
const sessions = await pool.acquire(accounts);
logger.info('sessions_acquired', { count: sessions.length });
const ctx: ScenarioContext = { const ctx: ScenarioContext = {
config, config,
sessions, sessions,
@@ -191,6 +217,7 @@ async function main(): Promise<void> {
}); });
exitCode = 1; exitCode = 1;
} finally { } finally {
await screencaster.stopAll();
await pool.release(); await pool.release();
if (dashboardServer) { if (dashboardServer) {
await dashboardServer.stop(); await dashboardServer.stop();