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:
@@ -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,19 +108,61 @@ async function main(): Promise<void> {
|
||||
logger,
|
||||
});
|
||||
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 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) => {
|
||||
logger.info('stream_start_requested', { sessionKey: key });
|
||||
// Wired in Task 23
|
||||
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) => {
|
||||
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();
|
||||
@@ -137,25 +180,8 @@ async function main(): Promise<void> {
|
||||
} catch {
|
||||
// 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 = {
|
||||
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();
|
||||
|
||||
Reference in New Issue
Block a user