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 { 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,37 +108,9 @@ async function main(): Promise<void> {
|
|||||||
logger,
|
logger,
|
||||||
});
|
});
|
||||||
const coordinator = new RoomCoordinator();
|
const coordinator = new RoomCoordinator();
|
||||||
|
const screencaster = new Screencaster(logger);
|
||||||
|
|
||||||
// Optional dashboard server
|
if (watch === 'tiled') {
|
||||||
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') {
|
|
||||||
logger.warn('tiled_not_yet_implemented');
|
logger.warn('tiled_not_yet_implemented');
|
||||||
console.warn('Watch mode "tiled" not yet implemented (Task 24). Falling back to none.');
|
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('SIGINT', () => onSignal('SIGINT'));
|
||||||
process.on('SIGTERM', () => onSignal('SIGTERM'));
|
process.on('SIGTERM', () => onSignal('SIGTERM'));
|
||||||
|
|
||||||
|
let dashboardServer: DashboardServer | null = null;
|
||||||
|
let dashboard: DashboardReporter = noopDashboard();
|
||||||
let exitCode = 0;
|
let exitCode = 0;
|
||||||
try {
|
try {
|
||||||
const sessions = await pool.acquire(accounts);
|
const sessions = await pool.acquire(accounts);
|
||||||
logger.info('sessions_acquired', { count: sessions.length });
|
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 = {
|
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();
|
||||||
|
|||||||
Reference in New Issue
Block a user