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 { RoomCoordinator } from './core/room-coordinator';
import { DashboardServer } from './dashboard/server';
import { Screencaster } from './core/screencaster';
import { getScenario, listScenarios } from './scenarios';
import type { DashboardReporter, ScenarioContext } from './core/types';
import type { DashboardReporter, ScenarioContext, Session } from './core/types';
function noopDashboard(): DashboardReporter {
return {
@@ -107,37 +108,9 @@ async function main(): Promise<void> {
logger,
});
const coordinator = new RoomCoordinator();
const screencaster = new Screencaster(logger);
// Optional dashboard server
let dashboardServer: DashboardServer | null = null;
let dashboard: DashboardReporter = noopDashboard();
if (watch === 'dashboard') {
const port = Number(config.dashboardPort ?? 7777);
dashboardServer = new DashboardServer(port, logger, {
onStartStream: (key) => {
logger.info('stream_start_requested', { sessionKey: key });
// Wired in Task 23
},
onStopStream: (key) => {
logger.info('stream_stop_requested', { sessionKey: key });
},
});
await dashboardServer.start();
dashboard = dashboardServer.reporter();
const url = `http://localhost:${port}`;
console.log(`Dashboard: ${url}`);
try {
const opener =
process.platform === 'darwin'
? 'open'
: process.platform === 'win32'
? 'start'
: 'xdg-open';
spawn(opener, [url], { stdio: 'ignore', detached: true }).unref();
} catch {
// If auto-open fails, the URL is already printed.
}
} else if (watch === 'tiled') {
if (watch === 'tiled') {
logger.warn('tiled_not_yet_implemented');
console.warn('Watch mode "tiled" not yet implemented (Task 24). Falling back to none.');
}
@@ -151,11 +124,64 @@ async function main(): Promise<void> {
process.on('SIGINT', () => onSignal('SIGINT'));
process.on('SIGTERM', () => onSignal('SIGTERM'));
let dashboardServer: DashboardServer | null = null;
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') {
const port = Number(config.dashboardPort ?? 7777);
dashboardServer = new DashboardServer(port, logger, {
onStartStream: (key) => {
const session = sessionsByKey.get(key);
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) => {
screencaster.stop(key).catch(() => {
// best-effort — errors already logged inside Screencaster
});
},
onDisconnect: () => {
screencaster.stopAll().catch(() => {});
},
});
await dashboardServer.start();
dashboard = dashboardServer.reporter();
const url = `http://localhost:${port}`;
console.log(`Dashboard: ${url}`);
try {
const opener =
process.platform === 'darwin'
? 'open'
: process.platform === 'win32'
? 'start'
: 'xdg-open';
spawn(opener, [url], { stdio: 'ignore', detached: true }).unref();
} catch {
// If auto-open fails, the URL is already printed.
}
}
const ctx: ScenarioContext = {
config,
sessions,
@@ -191,6 +217,7 @@ async function main(): Promise<void> {
});
exitCode = 1;
} finally {
await screencaster.stopAll();
await pool.release();
if (dashboardServer) {
await dashboardServer.stop();