← Files Codex Browser RecorderARCHIVED FILE
skills/record-browser/scripts/browser-recording.mjs
20.4 KB · Sep 30, 2026 · 23:13 UTC
import { rm, stat } from "node:fs/promises";
import {
renderCursorRecording,
startCursorCapture,
} from "./cursor-recording.mjs";
import {
createFfmpegSink,
startFramePump,
} from "./media-recorder.mjs";
import {
originOf,
RECORDING_FPS,
RECORDING_HARD_LIMIT_MS,
RECORDING_JPEG_QUALITY,
RECORDING_MAX_DECODED_BYTES,
RECORDING_MAX_HEIGHT,
RECORDING_MAX_OUTPUT_BYTES,
RECORDING_MAX_WIDTH,
} from "./recording-policy.mjs";
const SCREENCAST_EVENT_METHODS = [
"Page.frameNavigated",
"Page.screencastFrame",
"Page.screencastVisibilityChanged",
];
const FIRST_FRAME_TIMEOUT_MS = 5000;
// media-recorder.mjs:157-167 requires its derived read stall timeout to exceed this timeout.
const READ_TIMEOUT_MS = 1000;
const RESOURCE_CHECK_INTERVAL_MS = 1000;
class BrowserRecordingError extends Error {
constructor(code, message) {
super(message);
this.name = "BrowserRecordingError";
this.code = code;
}
}
function cancellationFailure() {
return new BrowserRecordingError(
"recording_cancelled",
"Recording was cancelled",
);
}
function awaitAbortable(operation, signal, onLateSuccess) {
const operationPromise = Promise.resolve(operation);
if (signal === undefined) return operationPromise;
return new Promise((resolve, reject) => {
let settled = false;
const finishLateSuccess = (value) => {
if (typeof onLateSuccess !== "function") return;
void Promise.resolve(onLateSuccess(value)).catch(() => {});
};
const abort = () => {
if (settled) return;
settled = true;
signal.removeEventListener("abort", abort);
operationPromise.then(finishLateSuccess, () => {});
reject(cancellationFailure());
};
signal.addEventListener("abort", abort, { once: true });
if (signal.aborted) abort();
operationPromise.then(
(value) => {
if (settled) return;
settled = true;
signal.removeEventListener("abort", abort);
resolve(value);
},
(error) => {
if (settled) return;
settled = true;
signal.removeEventListener("abort", abort);
reject(error);
},
);
});
}
export async function primeBrowserPixelCapture(tab) {
if (typeof tab?.screenshot !== "function") return;
try {
const screenshot = await tab.screenshot({ fullPage: false });
if (
screenshot === null ||
typeof screenshot !== "object" ||
!Number.isInteger(screenshot.byteLength) ||
screenshot.byteLength <= 0
) {
throw new Error("Browser screenshot is empty");
}
} catch {
throw new BrowserRecordingError(
"frame_stream_unavailable",
"The Browser pixel capture pipeline could not start",
);
}
}
function createFrameDeadline(timeoutMs) {
let timer;
const promise = new Promise((_, reject) => {
timer = setTimeout(
() =>
reject(
new BrowserRecordingError(
"frame_stream_unavailable",
"The page frame was not available before the timeout",
),
),
timeoutMs,
);
});
return {
clear() {
clearTimeout(timer);
},
promise,
};
}
async function readOutputSize(outputPath) {
try {
return (await stat(outputPath)).size;
} catch (error) {
if (error?.code === "ENOENT") {
return 0;
}
throw error;
}
}
function validateProduction({
approvedOrigin,
cdp,
outputPath,
requirePointerEvents,
signal,
}) {
if (
originOf(approvedOrigin) !== approvedOrigin ||
typeof cdp?.readEvents !== "function" ||
typeof cdp?.send !== "function" ||
typeof outputPath !== "string" ||
outputPath.length === 0 ||
typeof requirePointerEvents !== "boolean" ||
(signal !== undefined &&
(typeof signal?.addEventListener !== "function" ||
typeof signal?.removeEventListener !== "function"))
) {
throw new BrowserRecordingError(
"invalid_configuration",
"Browser recording configuration is invalid",
);
}
}
function validateLimits({
firstFrameTimeoutMs,
fps,
maxDecodedBytes,
maxDurationMs,
maxOutputBytes,
readTimeoutMs,
resourceCheckIntervalMs,
}) {
if (
!Number.isInteger(firstFrameTimeoutMs) ||
firstFrameTimeoutMs <= 0 ||
!Number.isFinite(fps) ||
fps <= 0 ||
!Number.isInteger(maxDecodedBytes) ||
maxDecodedBytes <= 0 ||
!Number.isInteger(maxDurationMs) ||
maxDurationMs <= 0 ||
!Number.isInteger(maxOutputBytes) ||
maxOutputBytes <= 0 ||
!Number.isInteger(readTimeoutMs) ||
readTimeoutMs < 0 ||
!Number.isInteger(resourceCheckIntervalMs) ||
resourceCheckIntervalMs <= 0
) {
throw new BrowserRecordingError(
"invalid_configuration",
"Browser recording configuration is invalid",
);
}
}
function validateAdapters({
cursorCaptureFactory,
cursorRenderer,
getOutputSize,
now,
sinkFactory,
}) {
if (
typeof cursorCaptureFactory !== "function" ||
typeof cursorRenderer !== "function" ||
typeof getOutputSize !== "function" ||
typeof now !== "function" ||
typeof sinkFactory !== "function"
) {
throw new BrowserRecordingError(
"invalid_configuration",
"Browser recording configuration is invalid",
);
}
}
export async function startBrowserRecordingForTab({
approvedOrigin,
ffmpegPath,
outputPath,
requirePointerEvents,
signal,
tab,
}) {
if (typeof tab?.capabilities?.get !== "function") {
throw new BrowserRecordingError(
"cdp_unavailable",
"The selected Browser tab does not expose capabilities",
);
}
const cdp = await awaitAbortable(tab.capabilities.get("cdp"), signal);
if (
typeof cdp?.readEvents !== "function" ||
typeof cdp?.send !== "function"
) {
throw new BrowserRecordingError(
"cdp_unavailable",
"Full CDP access is unavailable for the selected Browser tab",
);
}
await awaitAbortable(primeBrowserPixelCapture(tab), signal);
return startBrowserRecordingInternal({
approvedOrigin,
cdp,
ffmpegPath,
outputPath,
requirePointerEvents,
signal,
});
}
export async function startBrowserRecordingInternal({
approvedOrigin,
cdp,
ffmpegPath,
outputPath,
requirePointerEvents,
signal,
limits,
adapters,
}) {
const {
fps = RECORDING_FPS,
maxDecodedBytes = RECORDING_MAX_DECODED_BYTES,
maxOutputBytes = RECORDING_MAX_OUTPUT_BYTES,
maxDurationMs = RECORDING_HARD_LIMIT_MS,
firstFrameTimeoutMs = FIRST_FRAME_TIMEOUT_MS,
readTimeoutMs = READ_TIMEOUT_MS,
resourceCheckIntervalMs = RESOURCE_CHECK_INTERVAL_MS,
} = limits ?? {};
const {
sink: sinkFactory = createFfmpegSink,
cursorCapture: cursorCaptureFactory = startCursorCapture,
cursorRenderer = renderCursorRecording,
outputSize: getOutputSize = readOutputSize,
now = () => performance.now(),
} = adapters ?? {};
validateProduction({
approvedOrigin,
cdp,
outputPath,
requirePointerEvents,
signal,
});
validateLimits({
firstFrameTimeoutMs,
fps,
maxDecodedBytes,
maxDurationMs,
maxOutputBytes,
readTimeoutMs,
resourceCheckIntervalMs,
});
validateAdapters({
cursorCaptureFactory,
cursorRenderer,
getOutputSize,
now,
sinkFactory,
});
let startupCancellation = null;
const startupAbortListener = () => {
startupCancellation ??= new BrowserRecordingError(
"recording_cancelled",
"Recording was cancelled",
);
};
signal?.addEventListener("abort", startupAbortListener, { once: true });
if (signal?.aborted) startupAbortListener();
function throwIfStartupCancelled() {
if (startupCancellation !== null) throw startupCancellation;
}
async function awaitStartup(operation, onLateSuccess) {
try {
const result = await awaitAbortable(operation, signal, onLateSuccess);
throwIfStartupCancelled();
return result;
} catch (error) {
throw startupCancellation ?? error;
}
}
let baseline;
let cursorCapture;
let cursorTimeOrigin = null;
let mainFrameId;
let screencastAttempted = false;
let screencastPending = false;
let sink;
let baseOutputMayExist = false;
const baseOutputPath = `${outputPath}.cursor-base.mp4`;
const cursorNow = () =>
cursorTimeOrigin === null ? 0 : Math.max(0, now() - cursorTimeOrigin);
try {
throwIfStartupCancelled();
await awaitStartup(cdp.send("Page.enable"));
baseline = await awaitStartup(
cdp.readEvents({
limit: 1,
methods: SCREENCAST_EVENT_METHODS,
timeoutMs: 0,
}),
);
if (
baseline === null ||
typeof baseline !== "object" ||
!Number.isInteger(baseline.cursor) ||
baseline.cursor < 0
) {
throw new BrowserRecordingError(
"event_stream_invalid",
"CDP event stream returned an invalid baseline",
);
}
({ frameId: mainFrameId } = await awaitStartup(
inspectTopLevelFrame({ approvedOrigin, cdp }),
));
cursorCapture = await awaitStartup(
cursorCaptureFactory({ cdp, mainFrameId, now: cursorNow }),
(lateCapture) => lateCapture?.stop?.(),
);
screencastAttempted = true;
screencastPending = true;
const startScreencast = Promise.resolve(
cdp.send("Page.startScreencast", {
everyNthFrame: 1,
format: "jpeg",
maxHeight: RECORDING_MAX_HEIGHT,
maxWidth: RECORDING_MAX_WIDTH,
quality: RECORDING_JPEG_QUALITY,
}),
).finally(() => {
screencastPending = false;
});
await awaitStartup(
startScreencast,
() => cdp.send("Page.stopScreencast"),
);
baseOutputMayExist = true;
sink = sinkFactory({
ffmpegPath,
fps,
maxOutputBytes,
outputPath: baseOutputPath,
});
throwIfStartupCancelled();
} catch (error) {
if (screencastAttempted && !screencastPending) {
try {
await cdp.send("Page.stopScreencast");
} catch {
// Preserve the bounded startup failure as primary.
}
}
if (sink !== undefined) {
try {
await sink.stop({ discard: true });
} catch {
// Preserve the bounded startup failure as primary.
}
}
if (cursorCapture !== undefined) {
try {
await cursorCapture.stop();
} catch {
// Preserve the bounded startup failure as primary.
}
}
if (baseOutputMayExist) {
await rm(baseOutputPath, { force: true }).catch(() => {});
}
signal?.removeEventListener("abort", startupAbortListener);
throw startupCancellation ?? error;
}
let startedAt = null;
let stopPromise = null;
const acceptFrame = (jpeg) => {
if (stopPromise !== null) return false;
const frameAt = now();
cursorTimeOrigin ??= frameAt;
const accepted = sink.accept(jpeg);
if (accepted !== false) startedAt ??= frameAt;
return accepted;
};
const firstFrameDeadline = createFrameDeadline(firstFrameTimeoutMs);
const pump = startFramePump({
cdp,
initialCursor: baseline.cursor,
maxDecodedBytes,
onFrame: async (frame) => {
if (acceptFrame(frame.jpeg) === false) {
if (stopPromise !== null) return false;
throw new BrowserRecordingError(
"frame_stream_unavailable",
"The page frame could not be recorded",
);
}
return true;
},
onTopFrameNavigation(url) {
if (originOf(url) !== approvedOrigin) {
throw new BrowserRecordingError(
"origin_changed_during_recording",
"The recording page left the approved origin",
);
}
},
readTimeoutMs,
});
let resolveCompletion;
let terminationError = null;
const completion = new Promise((resolve) => {
resolveCompletion = resolve;
});
let resourceCheckRunning = false;
const resourceStats = {
elapsedMs: null,
maxObservedOutputBytes: 0,
terminationReason: null,
};
const recordingReady = Promise.race([
pump.ready,
firstFrameDeadline.promise,
]).finally(() => firstFrameDeadline.clear());
async function assertApprovedOrigin() {
try {
return await awaitAbortable(
inspectTopLevelFrame({ approvedOrigin, cdp }),
signal,
);
} catch (error) {
if (error?.code === "origin_not_allowed") {
throw new BrowserRecordingError(
"origin_changed_during_recording",
"The recording page left the approved origin",
);
}
throw error;
}
}
let durationTimer = null;
void recordingReady.then(
() => {
if (stopPromise !== null) return;
durationTimer = setTimeout(() => {
terminate(
new BrowserRecordingError(
"recording_duration_limit",
"Recording reached the configured duration limit",
),
);
}, maxDurationMs);
durationTimer.unref?.();
},
() => {
// Pump completion propagates the same failure into termination.
},
);
const resourceTimer = setInterval(async () => {
if (stopPromise !== null || resourceCheckRunning) {
return;
}
resourceCheckRunning = true;
try {
const outputBytes = await getOutputSize(
sink.workingOutputPath ?? baseOutputPath,
);
if (!Number.isFinite(outputBytes) || outputBytes < 0) {
throw new Error("Output size monitor returned an invalid value");
}
resourceStats.maxObservedOutputBytes = Math.max(
resourceStats.maxObservedOutputBytes,
outputBytes,
);
if (outputBytes > maxOutputBytes) {
terminate(
new BrowserRecordingError(
"recording_output_limit",
"Recording exceeded the configured output size limit",
),
);
return;
}
} catch (error) {
terminate(
error instanceof BrowserRecordingError
? error
: new BrowserRecordingError(
"output_monitor_failed",
"Recording output could not be monitored",
),
);
} finally {
resourceCheckRunning = false;
}
}, resourceCheckIntervalMs);
resourceTimer.unref?.();
const abortListener = () => {
terminate(
startupCancellation ??
new BrowserRecordingError("recording_cancelled", "Recording was cancelled"),
);
};
signal?.addEventListener("abort", abortListener, { once: true });
signal?.removeEventListener("abort", startupAbortListener);
if (startupCancellation !== null || signal?.aborted) abortListener();
if (typeof sink.completion?.then === "function") {
void sink.completion.then(() => {
if (stopPromise === null) {
terminate(
new BrowserRecordingError(
"encoder_failed",
"FFmpeg exited before recording was stopped",
),
);
}
});
}
if (typeof cursorCapture.completion?.then === "function") {
void cursorCapture.completion.then((outcome) => {
const error = outcome?.error;
if (error !== null && error !== undefined && stopPromise === null) {
terminate(error);
}
});
}
void pump.completion.then(({ error }) => {
if (error !== null && stopPromise === null) terminate(error);
});
async function finalize() {
clearTimeout(durationTimer);
clearInterval(resourceTimer);
signal?.removeEventListener("abort", abortListener);
let cleanupError = null;
let cursorError = null;
let cursorTimeline = null;
let pumpError = null;
let renderedCursor = null;
if (terminationError === null && !signal?.aborted) {
try {
await inspectTopLevelFrame({ approvedOrigin, cdp });
} catch (error) {
pumpError =
error?.code === "origin_not_allowed"
? new BrowserRecordingError(
"origin_changed_during_recording",
"The recording page left the approved origin",
)
: error;
}
}
const pumpStop = pump.stop();
void pumpStop.catch(() => {});
try {
await cdp.send("Page.stopScreencast");
} catch (error) {
cleanupError = error;
}
try {
await pumpStop;
} catch (error) {
pumpError ??= error;
}
try {
cursorTimeline = await cursorCapture.stop();
if (
requirePointerEvents &&
(!Array.isArray(cursorTimeline?.events) ||
cursorTimeline.events.length === 0)
) {
cursorError = new BrowserRecordingError(
"cursor_recording_failed",
"The approved pointer flow produced no observable pointer event",
);
}
} catch (error) {
cursorError = error;
}
try {
await sink.stop({
discard:
cleanupError !== null ||
cursorError !== null ||
pumpError !== null ||
terminationError !== null,
});
} catch (error) {
cleanupError ??= error;
} finally {
resourceStats.maxObservedOutputBytes = Math.max(
resourceStats.maxObservedOutputBytes,
sink.stats?.outputBytes ?? 0,
);
}
if (signal?.aborted) cursorError ??= cancellationFailure();
if (
cleanupError === null &&
cursorError === null &&
pumpError === null &&
terminationError === null
) {
try {
renderedCursor = await cursorRenderer({
ffmpegPath,
inputPath: baseOutputPath,
outputPath,
signal,
timeline: cursorTimeline,
});
if (signal?.aborted) cursorError ??= cancellationFailure();
} catch (error) {
cursorError =
error?.code === "cursor_recording_failed"
? error
: new BrowserRecordingError(
"cursor_recording_failed",
"Cursor recording could not be completed",
);
}
}
try {
await rm(baseOutputPath, { force: true });
} catch {
cursorError ??= new BrowserRecordingError(
"cursor_recording_failed",
"Cursor recording could not be completed",
);
}
if (terminationError !== null) {
throw terminationError;
}
if (pumpError !== null) {
throw pumpError;
}
if (cursorError !== null) {
throw cursorError;
}
if (cleanupError !== null) {
throw cleanupError;
}
return {
...cursorCapture.stats,
...pump.stats,
...sink.stats,
...(Number.isFinite(renderedCursor?.outputBytes)
? { outputBytes: renderedCursor.outputBytes }
: {}),
...resourceStats,
outputPath,
};
}
return {
assertApprovedOrigin,
completion,
ready: recordingReady.catch(
async (error) => {
terminationError ??= error;
resourceStats.terminationReason =
error?.code ?? "frame_stream_unavailable";
try {
await stop();
} catch {
// Preserve the readiness failure as the primary error.
}
throw error;
},
),
stats: {
cursor: cursorCapture.stats,
framePump: pump.stats,
resources: resourceStats,
sink: sink.stats,
},
stop,
};
function stop() {
if (stopPromise === null) {
const stoppedAt = now();
resourceStats.elapsedMs =
startedAt === null ? null : Math.max(0, stoppedAt - startedAt);
stopPromise = finalize();
void stopPromise.then(
(result) => resolveCompletion({ error: null, result }),
(error) => resolveCompletion({ error, result: null }),
);
}
return stopPromise;
}
function terminate(error) {
if (terminationError === null && stopPromise === null) {
terminationError = error;
resourceStats.terminationReason = error.code;
void stop().catch(() => {
// The caller observes the same failure through session.stop().
});
}
}
}
export async function inspectTopLevelFrame({ approvedOrigin, cdp }) {
if (
typeof cdp?.send !== "function" ||
originOf(approvedOrigin) !== approvedOrigin
) {
throw new BrowserRecordingError(
"invalid_configuration",
"Top-level origin verification configuration is invalid",
);
}
let frameTree;
try {
frameTree = await cdp.send("Page.getFrameTree");
} catch {
throw new BrowserRecordingError(
"origin_verification_failed",
"The recording page origin could not be verified",
);
}
const frame = frameTree?.frameTree?.frame;
if (
typeof frame?.id !== "string" ||
frame.id.length === 0 ||
originOf(frame.url) !== approvedOrigin
) {
throw new BrowserRecordingError(
originOf(frame?.url) === null
? "origin_verification_failed"
: "origin_not_allowed",
"The recording page is outside the approved origin",
);
}
return { frameId: frame.id };
}
SHA-256: cad06a4415feef45b047d0263a147c2585eee1668eb187367a71c0f72a44fb09