← Files Codex Browser RecorderARCHIVED FILE
skills/record-browser/scripts/cursor-recording.mjs
32.7 KB · Sep 30, 2026 · 23:13 UTC
import { spawn } from "node:child_process";
import { rename, rm, stat } from "node:fs/promises";
import { fileURLToPath } from "node:url";
import { normalizeCdpEventBatch } from "./cdp-event-batch.mjs";
import {
RECORDING_FPS,
RECORDING_MAX_OUTPUT_BYTES,
} from "./recording-policy.mjs";
const BINDING_NAME = "__codexBrowserRecorderCursor";
const WORLD_NAME = "codex-browser-recorder-cursor";
const EVENT_METHODS = [
"Page.frameAttached",
"Page.frameDetached",
"Page.frameNavigated",
"Runtime.bindingCalled",
"Target.attachedToTarget",
"Target.detachedFromTarget",
];
const POINTER_TYPES = new Set([
"click",
"double",
"down",
"move",
"up",
"wheel",
]);
const READ_TIMEOUT_MS = 250;
const MAX_STARTUP_TARGET_BATCHES = 8;
const MAX_CURSOR_EVENTS = 6000;
const CURSOR_RENDER_TIMEOUT_MS = 8000;
const CURSOR_RENDER_FORCE_KILL_MS = 500;
const CURSOR_ASSET_PATH = fileURLToPath(
new URL("../assets/codex-style-cursor.xpm", import.meta.url),
);
const RING_ASSET_PATH = fileURLToPath(
new URL("../assets/codex-style-click-ring.xpm", import.meta.url),
);
const CURSOR_HOTSPOT_OFFSET = 1;
const CLICK_RING_OFFSET = 28;
const CLICK_RING_DURATION_MS = 200;
const MOVE_DURATION_MS = 250;
class CursorRecordingError extends Error {
constructor(message) {
super(message);
this.name = "CursorRecordingError";
this.code = "cursor_recording_failed";
}
}
function invalidCursorRecording() {
return new CursorRecordingError("Cursor recording could not be completed");
}
function validateConfiguration({ cdp, mainFrameId, now }) {
if (
typeof cdp?.readEvents !== "function" ||
typeof cdp?.send !== "function" ||
typeof mainFrameId !== "string" ||
mainFrameId.length === 0 ||
typeof now !== "function"
) {
throw invalidCursorRecording();
}
}
function requireCursorEventBatch(batch, currentCursor) {
const normalized = normalizeCdpEventBatch(batch, currentCursor);
if (
normalized === null ||
typeof normalized.hasMore !== "boolean" ||
normalized.truncated === true
) {
throw invalidCursorRecording();
}
return normalized;
}
function parsePointerPayload(payload) {
let parsed;
try {
parsed = JSON.parse(payload);
} catch {
throw invalidCursorRecording();
}
if (
parsed === null ||
typeof parsed !== "object" ||
parsed.version !== 1 ||
!POINTER_TYPES.has(parsed.type) ||
!Number.isFinite(parsed.x) ||
!Number.isFinite(parsed.y) ||
!Number.isFinite(parsed.width) ||
parsed.width <= 0 ||
!Number.isFinite(parsed.height) ||
parsed.height <= 0 ||
!Number.isFinite(parsed.observedAtEpochMs) ||
parsed.observedAtEpochMs < 0 ||
!Number.isInteger(parsed.button) ||
!Number.isInteger(parsed.buttons)
) {
throw invalidCursorRecording();
}
return parsed;
}
function installerExpression(bindingName) {
return `(() => {
const bindingName = ${JSON.stringify(bindingName)};
const cleanupName = "__codexBrowserRecorderCursorCleanup";
globalThis[cleanupName]?.();
const emit = (type, pointer) => globalThis[bindingName](JSON.stringify({
button: Number.isInteger(pointer.button) ? pointer.button : 0,
buttons: Number.isInteger(pointer.buttons) ? pointer.buttons : 0,
height: innerHeight,
observedAtEpochMs: Number.isFinite(pointer.observedAtEpochMs)
? pointer.observedAtEpochMs
: performance.timeOrigin + pointer.timeStamp,
type,
version: 1,
width: innerWidth,
x: pointer.clientX,
y: pointer.clientY,
}));
let animationFrame = null;
let pendingMove = null;
const flushMove = () => {
if (animationFrame !== null) cancelAnimationFrame(animationFrame);
animationFrame = null;
if (pendingMove === null) return;
const move = pendingMove;
pendingMove = null;
emit("move", move);
};
const queueMove = (event) => {
pendingMove = {
button: event.button,
buttons: event.buttons,
clientX: event.clientX,
clientY: event.clientY,
observedAtEpochMs: performance.timeOrigin + event.timeStamp,
};
if (animationFrame === null) {
animationFrame = requestAnimationFrame(flushMove);
}
};
const emitBoundary = (type, event) => {
flushMove();
emit(type, event);
};
const listeners = [
["mousemove", queueMove],
["mousedown", (event) => emitBoundary("down", event)],
["mouseup", (event) => emitBoundary("up", event)],
["click", (event) => emitBoundary("click", event)],
["dblclick", (event) => emitBoundary("double", event)],
["wheel", (event) => emitBoundary("wheel", event)],
];
for (const [type, listener] of listeners) {
addEventListener(type, listener, { capture: true, passive: true });
}
globalThis[cleanupName] = () => {
if (animationFrame !== null) cancelAnimationFrame(animationFrame);
animationFrame = null;
pendingMove = null;
for (const [type, listener] of listeners) {
removeEventListener(type, listener, { capture: true });
}
delete globalThis[cleanupName];
};
return { height: innerHeight, width: innerWidth };
})()`;
}
function cleanupExpression() {
return `(() => {
globalThis.__codexBrowserRecorderCursorCleanup?.();
return true;
})()`;
}
function validateTimeline(timeline) {
if (
timeline === null ||
!Number.isFinite(timeline?.durationMs) ||
timeline.durationMs <= 0 ||
!Number.isFinite(timeline?.viewport?.height) ||
timeline.viewport.height <= 0 ||
!Number.isFinite(timeline?.viewport?.width) ||
timeline.viewport.width <= 0 ||
!Array.isArray(timeline?.events)
) {
throw invalidCursorRecording();
}
let lastAtMs = 0;
for (const event of timeline.events) {
if (
event === null ||
typeof event !== "object" ||
!Number.isFinite(event.atMs) ||
event.atMs < lastAtMs ||
event.atMs > timeline.durationMs ||
!POINTER_TYPES.has(event.type) ||
!Number.isFinite(event.x) ||
!Number.isFinite(event.y) ||
!Number.isInteger(event.button) ||
!Number.isInteger(event.buttons) ||
typeof event.frameId !== "string" ||
event.frameId.length === 0
) {
throw invalidCursorRecording();
}
lastAtMs = event.atMs;
}
}
function collapsePositions(events) {
const positions = [];
for (const event of events) {
const previous = positions.at(-1);
if (previous?.atMs === event.atMs) {
previous.x = event.x;
previous.y = event.y;
} else {
positions.push({ atMs: event.atMs, x: event.x, y: event.y });
}
}
return positions;
}
function positionAt(positions, atMs) {
if (positions.length === 0 || atMs < positions[0].atMs) return null;
let previous = positions[0];
for (let index = 1; index < positions.length; index += 1) {
const next = positions[index];
if (atMs >= next.atMs) {
previous = next;
continue;
}
const movementDuration = Math.min(
MOVE_DURATION_MS,
next.atMs - previous.atMs,
);
const movementStart = next.atMs - movementDuration;
if (atMs <= movementStart) return previous;
const progress = (atMs - movementStart) / movementDuration;
return {
x: previous.x + (next.x - previous.x) * progress,
y: previous.y + (next.y - previous.y) * progress,
};
}
return previous;
}
function activePressAt(presses, atMs) {
let active = null;
for (const press of presses) {
if (press.atMs > atMs) break;
if (atMs < press.atMs + CLICK_RING_DURATION_MS) active = press;
}
return active;
}
function expressionFor(samples, property, offset, viewportSize) {
const visible = samples
.map((sample, index) =>
sample === null
? null
: {
index: index + 1,
value: Number(sample[property].toFixed(3)),
},
)
.filter((sample) => sample !== null);
if (visible.length === 0) return "-1000";
const runs = [];
for (const sample of visible) {
const previous = runs.at(-1);
if (
previous !== undefined &&
previous.end + 1 === sample.index &&
previous.value === sample.value
) {
previous.end = sample.index;
} else {
runs.push({ end: sample.index, start: sample.index, value: sample.value });
}
}
const first = runs[0].start;
const terms = first === 0 ? [] : [`-1000*lt(n,${first})`];
for (const [index, run] of runs.entries()) {
const previous = runs[index - 1];
if (previous !== undefined && previous.end + 1 < run.start) {
const gapStart = previous.end + 1;
const gapEnd = run.start - 1;
const gapSelector =
gapStart === gapEnd
? `eq(n,${gapStart})`
: `between(n,${gapStart},${gapEnd})`;
terms.push(`-1000*${gapSelector}`);
}
const isLast = index === runs.length - 1 && run.end === samples.length - 1;
const selector = isLast
? `gte(n,${run.start})`
: run.start === run.end
? `eq(n,${run.start})`
: `between(n,${run.start},${run.end})`;
const mainSize = property === "x" ? "main_w" : "main_h";
terms.push(
`(${run.value}*${mainSize}/${Number(viewportSize).toFixed(3)}-${offset})*${selector}`,
);
}
const lastVisible = visible.at(-1).index;
if (lastVisible < samples.length) {
terms.push(`-1000*gt(n,${lastVisible})`);
}
return terms.join("+");
}
function cursorExpressions(timeline) {
const frameDurationMs = 1000 / RECORDING_FPS;
const frameCount = Math.ceil(timeline.durationMs / frameDurationMs);
const positions = collapsePositions(timeline.events);
const presses = timeline.events.filter((event) => event.type === "down");
const cursorSamples = [];
const ringSamples = [];
for (let frame = 0; frame < frameCount; frame += 1) {
const atMs = frame * frameDurationMs;
cursorSamples.push(positionAt(positions, atMs));
ringSamples.push(activePressAt(presses, atMs));
}
return {
cursorX: expressionFor(
cursorSamples,
"x",
CURSOR_HOTSPOT_OFFSET,
timeline.viewport.width,
),
cursorY: expressionFor(
cursorSamples,
"y",
CURSOR_HOTSPOT_OFFSET,
timeline.viewport.height,
),
ringX: expressionFor(
ringSamples,
"x",
CLICK_RING_OFFSET,
timeline.viewport.width,
),
ringY: expressionFor(
ringSamples,
"y",
CLICK_RING_OFFSET,
timeline.viewport.height,
),
};
}
function flattenFrameTree(frameTree, parentFrameId = null, frames = []) {
const frame = frameTree?.frame;
if (typeof frame?.id !== "string" || frame.id.length === 0) {
throw invalidCursorRecording();
}
frames.push({
frameId: frame.id,
parentFrameId:
typeof frame.parentId === "string" ? frame.parentId : parentFrameId,
});
for (const child of frameTree.childFrames ?? []) {
flattenFrameTree(child, frame.id, frames);
}
return frames;
}
function validateViewport(value) {
if (
value === null ||
typeof value !== "object" ||
!Number.isFinite(value.width) ||
value.width <= 0 ||
!Number.isFinite(value.height) ||
value.height <= 0
) {
throw invalidCursorRecording();
}
return { height: value.height, width: value.width };
}
function validateOwnerQuad(value) {
if (
!Array.isArray(value) ||
value.length !== 8 ||
value.some((coordinate) => !Number.isFinite(coordinate))
) {
throw invalidCursorRecording();
}
const [x0, y0, x1, y1, x2, y2, x3, y3] = value;
const affineTolerance = 0.01;
const isAffine =
Math.abs(x2 - x1 - x3 + x0) <= affineTolerance &&
Math.abs(y2 - y1 - y3 + y0) <= affineTolerance;
const determinant = (x1 - x0) * (y3 - y0) - (y1 - y0) * (x3 - x0);
if (!isAffine || Math.abs(determinant) <= Number.EPSILON) {
throw invalidCursorRecording();
}
return [...value];
}
function mapPointToTop(frames, frameId, x, y, visited = new Set()) {
const frame = frames.get(frameId);
if (frame === undefined || visited.has(frameId)) {
throw invalidCursorRecording();
}
if (frame.parentFrameId === null) return { x, y };
visited.add(frameId);
const parent = frames.get(frame.parentFrameId);
if (parent === undefined || frame.ownerQuad === null) {
throw invalidCursorRecording();
}
const u = x / frame.viewport.width;
const v = y / frame.viewport.height;
const [x0, y0, x1, y1, x2, y2, x3, y3] = frame.ownerQuad;
const parentX =
(1 - u) * (1 - v) * x0 +
u * (1 - v) * x1 +
u * v * x2 +
(1 - u) * v * x3;
const parentY =
(1 - u) * (1 - v) * y0 +
u * (1 - v) * y1 +
u * v * y2 +
(1 - u) * v * y3;
if (!Number.isFinite(parentX) || !Number.isFinite(parentY)) {
throw invalidCursorRecording();
}
return mapPointToTop(
frames,
parent.frameId,
parentX,
parentY,
visited,
);
}
function runFfmpeg(ffmpegPath, args, { signal, timeoutMs }) {
return new Promise((resolve, reject) => {
let settled = false;
let terminationRequested = false;
let forceKillTimer;
let timeoutTimer;
const child = spawn(ffmpegPath, args, {
stdio: ["ignore", "ignore", "ignore"],
});
const finish = (error) => {
if (settled) return;
settled = true;
clearTimeout(timeoutTimer);
clearTimeout(forceKillTimer);
signal?.removeEventListener("abort", requestTermination);
if (error === null) resolve();
else reject(error);
};
const requestTermination = () => {
if (terminationRequested || settled) return;
terminationRequested = true;
child.kill("SIGTERM");
forceKillTimer = setTimeout(() => {
if (!settled) child.kill("SIGKILL");
}, CURSOR_RENDER_FORCE_KILL_MS);
};
child.once("error", () => {
finish(invalidCursorRecording());
});
child.once("exit", (code, signal) => {
if (!terminationRequested && code === 0 && signal === null) {
finish(null);
} else {
finish(invalidCursorRecording());
}
});
signal?.addEventListener("abort", requestTermination, { once: true });
if (signal?.aborted) requestTermination();
timeoutTimer = setTimeout(requestTermination, timeoutMs);
});
}
export async function renderCursorRecording({
ffmpegPath,
inputPath,
outputPath,
signal,
timeoutMs = CURSOR_RENDER_TIMEOUT_MS,
timeline,
}) {
if (
typeof ffmpegPath !== "string" ||
ffmpegPath.length === 0 ||
typeof inputPath !== "string" ||
inputPath.length === 0 ||
typeof outputPath !== "string" ||
outputPath.length === 0 ||
inputPath === outputPath ||
(signal !== undefined && !(signal instanceof AbortSignal)) ||
!Number.isInteger(timeoutMs) ||
timeoutMs <= 0
) {
throw invalidCursorRecording();
}
validateTimeline(timeline);
const workingOutputPath = `${outputPath}.partial`;
await rm(workingOutputPath, { force: true }).catch(() => {});
try {
const expressions = cursorExpressions(timeline);
const filter = [
`[0:v][1:v]overlay=x='${expressions.cursorX}':y='${expressions.cursorY}':eval=frame:shortest=1:eof_action=repeat[cursor]`,
`[cursor][2:v]overlay=x='${expressions.ringX}':y='${expressions.ringY}':eval=frame:shortest=1:eof_action=repeat[video]`,
].join(";");
await runFfmpeg(ffmpegPath, [
"-hide_banner",
"-loglevel",
"error",
"-filter_complex_threads",
"1",
"-i",
inputPath,
"-loop",
"1",
"-framerate",
String(RECORDING_FPS),
"-i",
CURSOR_ASSET_PATH,
"-loop",
"1",
"-framerate",
String(RECORDING_FPS),
"-i",
RING_ASSET_PATH,
"-filter_complex",
filter,
"-map",
"[video]",
"-an",
"-c:v",
"libx264",
"-preset",
"veryfast",
"-crf",
"18",
"-pix_fmt",
"yuv420p",
"-movflags",
"+faststart",
"-f",
"mp4",
"-y",
workingOutputPath,
], { signal, timeoutMs });
if (signal?.aborted) throw invalidCursorRecording();
const outputBytes = (await stat(workingOutputPath)).size;
if (outputBytes <= 0 || outputBytes > RECORDING_MAX_OUTPUT_BYTES) {
throw invalidCursorRecording();
}
if (signal?.aborted) throw invalidCursorRecording();
await rename(workingOutputPath, outputPath);
return { outputBytes, outputPath };
} catch (error) {
await rm(workingOutputPath, { force: true }).catch(() => {});
throw error?.code === "cursor_recording_failed"
? error
: invalidCursorRecording();
}
}
export async function startCursorCapture({ cdp, mainFrameId, now }) {
validateConfiguration({ cdp, mainFrameId, now });
const startedAt = now();
const contexts = new Map();
const frames = new Map();
const events = [];
const discoveredParents = new Map();
const pendingFrames = new Map();
const bindingTargets = new Map();
const bindingTargetsByName = new Map();
const targetSessions = new Map();
let cursor = 0;
let nextBindingId = 1;
let recordingViewport;
const targetKey = (target) => {
if (typeof target?.sessionId === "string") {
return `session:${target.sessionId}`;
}
if (typeof target?.targetId === "string") {
return `target:${target.targetId}`;
}
return "root";
};
const contextKey = (target, contextId) =>
`${targetKey(target)}:${contextId}`;
const sendTo = (target, method, params) =>
target === null
? cdp.send(method, params)
: cdp.send(method, params, { target });
const targetFromSource = (source) => {
if (typeof source?.sessionId !== "string") return null;
const target = targetSessions.get(source.sessionId);
if (target === undefined) throw invalidCursorRecording();
return target;
};
const rememberPendingFrame = (discovered, target) => {
pendingFrames.set(discovered.frameId, {
discovered: { ...discovered },
target,
});
};
async function addBinding(target) {
const key = targetKey(target);
const existing = bindingTargets.get(key);
if (existing !== undefined) return existing.name;
const name =
target === null
? BINDING_NAME
: `${BINDING_NAME}_${nextBindingId++}`;
await sendTo(target, "Runtime.addBinding", {
executionContextName: WORLD_NAME,
name,
});
bindingTargets.set(key, { name, target });
bindingTargetsByName.set(name, target);
return name;
}
async function removeFrame(frameId, cleanup = false) {
for (const child of [...frames.values()]) {
if (child.parentFrameId === frameId) {
await removeFrame(child.frameId, cleanup);
}
}
const frame = frames.get(frameId);
if (frame === undefined) return;
frames.delete(frameId);
contexts.delete(contextKey(frame.target, frame.contextId));
if (cleanup) {
await sendTo(frame.target, "Runtime.evaluate", {
contextId: frame.contextId,
expression: cleanupExpression(),
returnByValue: true,
});
}
}
async function installFrame(discovered, target = null) {
if (
typeof discovered?.frameId !== "string" ||
discovered.frameId.length === 0 ||
(discovered.parentFrameId !== null &&
(typeof discovered.parentFrameId !== "string" ||
!frames.has(discovered.parentFrameId)))
) {
throw invalidCursorRecording();
}
if (frames.has(discovered.frameId)) {
await removeFrame(discovered.frameId).catch(() => {});
}
let ownerQuad = null;
if (discovered.parentFrameId !== null) {
const parent = frames.get(discovered.parentFrameId);
const owner = await sendTo(parent.target, "DOM.getFrameOwner", {
frameId: discovered.frameId,
});
if (!Number.isInteger(owner?.backendNodeId)) {
throw invalidCursorRecording();
}
const boxModel = await sendTo(parent.target, "DOM.getBoxModel", {
backendNodeId: owner.backendNodeId,
});
ownerQuad = validateOwnerQuad(boxModel?.model?.content);
}
const isolatedWorld = await sendTo(target, "Page.createIsolatedWorld", {
frameId: discovered.frameId,
grantUniveralAccess: false,
worldName: WORLD_NAME,
});
const contextId = isolatedWorld?.executionContextId;
if (!Number.isInteger(contextId) || contextId < 0) {
throw invalidCursorRecording();
}
try {
const installed = await sendTo(target, "Runtime.evaluate", {
contextId,
expression: installerExpression(
bindingTargets.get(targetKey(target))?.name,
),
returnByValue: true,
});
const frame = {
contextId,
frameId: discovered.frameId,
ownerQuad,
parentFrameId: discovered.parentFrameId,
target,
viewport: validateViewport(installed?.result?.value),
};
frames.set(frame.frameId, frame);
contexts.set(contextKey(target, contextId), frame.frameId);
} catch (error) {
await sendTo(target, "Runtime.evaluate", {
contextId,
expression: cleanupExpression(),
returnByValue: true,
}).catch(() => {});
throw error;
}
}
async function readFrameViewport(frame) {
const result = await sendTo(frame.target, "Runtime.evaluate", {
contextId: frame.contextId,
expression: "({ height: innerHeight, width: innerWidth })",
returnByValue: true,
});
return validateViewport(result?.result?.value);
}
async function refreshGeometry(frameId, sourceViewport) {
let frame = frames.get(frameId);
if (frame === undefined) throw invalidCursorRecording();
frame.viewport = validateViewport(sourceViewport);
while (frame.parentFrameId !== null) {
const parent = frames.get(frame.parentFrameId);
if (parent === undefined) throw invalidCursorRecording();
const owner = await sendTo(parent.target, "DOM.getFrameOwner", {
frameId: frame.frameId,
});
if (!Number.isInteger(owner?.backendNodeId)) {
throw invalidCursorRecording();
}
const boxModel = await sendTo(parent.target, "DOM.getBoxModel", {
backendNodeId: owner.backendNodeId,
});
frame.ownerQuad = validateOwnerQuad(boxModel?.model?.content);
parent.viewport = await readFrameViewport(parent);
frame = parent;
}
if (frame.frameId !== mainFrameId) throw invalidCursorRecording();
return frame.viewport;
}
async function attachTarget(params) {
const sessionId = params?.sessionId;
const targetInfo = params?.targetInfo;
if (targetInfo?.type !== "iframe") return;
if (
typeof sessionId !== "string" ||
typeof targetInfo.targetId !== "string"
) {
throw invalidCursorRecording();
}
const target =
bindingTargets.get(`target:${targetInfo.targetId}`)?.target ?? {
sessionId,
};
targetSessions.set(sessionId, target);
await sendTo(target, "Page.enable");
await sendTo(target, "Runtime.enable");
await addBinding(target);
const frameTree = await sendTo(target, "Page.getFrameTree");
const localFrames = flattenFrameTree(frameTree?.frameTree);
for (const [index, localFrame] of localFrames.entries()) {
const fallbackParent =
discoveredParents.get(localFrame.frameId) ??
discoveredParents.get(targetInfo.targetId) ??
null;
const discovered = {
frameId: localFrame.frameId,
parentFrameId:
index === 0 && localFrame.parentFrameId === null
? fallbackParent
: localFrame.parentFrameId,
};
discoveredParents.set(discovered.frameId, discovered.parentFrameId);
if (
discovered.parentFrameId === null ||
!frames.has(discovered.parentFrameId)
) {
rememberPendingFrame(discovered, target);
} else {
await installFrame(discovered, target);
pendingFrames.delete(discovered.frameId);
pendingFrames.delete(targetInfo.targetId);
}
}
}
async function detachTarget(params) {
const sessionId = params?.sessionId;
if (typeof sessionId !== "string") throw invalidCursorRecording();
const target = targetSessions.get(sessionId);
if (target === undefined) return;
const key = targetKey(target);
for (const frame of [...frames.values()]) {
if (targetKey(frame.target) === key) {
await removeFrame(frame.frameId).catch(() => {});
}
}
for (const [frameId, pending] of pendingFrames) {
if (targetKey(pending.target) === key) {
pendingFrames.delete(frameId);
}
}
const binding = bindingTargets.get(key);
if (binding !== undefined) {
bindingTargetsByName.delete(binding.name);
bindingTargets.delete(key);
}
targetSessions.delete(sessionId);
}
const stats = {
cursorChildFrameEventsCaptured: 0,
cursorEventsCaptured: 0,
cursorFramesObserved: 0,
cursorLastEventEpochMs: null,
};
async function handleEvent(event) {
if (event?.method === "Target.attachedToTarget") {
await attachTarget(event.params);
stats.cursorFramesObserved = frames.size;
return;
}
if (event?.method === "Target.detachedFromTarget") {
await detachTarget(event.params);
stats.cursorFramesObserved = frames.size;
return;
}
if (event?.method === "Page.frameAttached") {
const frameId = event.params?.frameId;
const parentFrameId = event.params?.parentFrameId;
if (
typeof frameId !== "string" ||
typeof parentFrameId !== "string"
) {
throw invalidCursorRecording();
}
discoveredParents.set(frameId, parentFrameId);
const pending = pendingFrames.get(frameId);
const target =
pending?.target ??
frames.get(parentFrameId)?.target ??
targetFromSource(event.source);
const discovered = { frameId, parentFrameId };
rememberPendingFrame(discovered, target);
if (frames.has(parentFrameId)) {
try {
await installFrame(discovered, target);
pendingFrames.delete(frameId);
} catch {
// Navigation or target attachment may make the frame observable next.
}
}
stats.cursorFramesObserved = frames.size;
return;
}
if (event?.method === "Page.frameNavigated") {
const frame = event.params?.frame;
if (typeof frame?.id !== "string") throw invalidCursorRecording();
const target =
frames.get(frame.id)?.target ??
pendingFrames.get(frame.id)?.target ??
targetFromSource(event.source);
const parentFrameId =
typeof frame.parentId === "string"
? frame.parentId
: discoveredParents.get(frame.id) ?? null;
discoveredParents.set(frame.id, parentFrameId);
await installFrame({ frameId: frame.id, parentFrameId }, target);
pendingFrames.delete(frame.id);
stats.cursorFramesObserved = frames.size;
return;
}
if (event?.method === "Page.frameDetached") {
const frameId = event.params?.frameId;
if (typeof frameId !== "string") throw invalidCursorRecording();
await removeFrame(frameId).catch(() => {});
pendingFrames.delete(frameId);
discoveredParents.delete(frameId);
stats.cursorFramesObserved = frames.size;
return;
}
if (event?.method !== "Runtime.bindingCalled") {
return;
}
const target = bindingTargetsByName.get(event.params?.name);
if (target === undefined && event.params?.name !== BINDING_NAME) return;
if (target === undefined && !bindingTargetsByName.has(BINDING_NAME)) {
throw invalidCursorRecording();
}
const frameId = contexts.get(
contextKey(target, event.params.executionContextId),
);
if (frameId === undefined || typeof event.params.payload !== "string") {
throw invalidCursorRecording();
}
const pointer = parsePointerPayload(event.params.payload);
const eventAtMs = Math.max(0, now() - startedAt);
const currentTopViewport = await refreshGeometry(frameId, {
height: pointer.height,
width: pointer.width,
});
const point = mapPointToTop(frames, frameId, pointer.x, pointer.y);
if (events.length >= MAX_CURSOR_EVENTS) {
throw invalidCursorRecording();
}
events.push({
atMs: eventAtMs,
button: pointer.button,
buttons: pointer.buttons,
frameId,
type: pointer.type,
x: Number(
(
point.x *
recordingViewport.width /
currentTopViewport.width
).toFixed(3),
),
y: Number(
(
point.y *
recordingViewport.height /
currentTopViewport.height
).toFixed(3),
),
});
const eventFrame = frames.get(frameId);
if (eventFrame !== undefined && eventFrame.parentFrameId !== null) {
stats.cursorChildFrameEventsCaptured += 1;
}
stats.cursorEventsCaptured += 1;
stats.cursorLastEventEpochMs = pointer.observedAtEpochMs;
}
async function cleanup() {
let failed = false;
for (const frame of [...frames.values()].reverse()) {
try {
await sendTo(frame.target, "Runtime.evaluate", {
contextId: frame.contextId,
expression: cleanupExpression(),
returnByValue: true,
});
} catch {
failed = true;
}
}
for (const binding of [...bindingTargets.values()].reverse()) {
try {
await sendTo(binding.target, "Runtime.removeBinding", {
name: binding.name,
});
} catch {
failed = true;
}
}
bindingTargets.clear();
bindingTargetsByName.clear();
return failed;
}
try {
await cdp.send("Page.enable");
await cdp.send("Runtime.enable");
const baselineResult = await cdp.readEvents({
limit: 1,
methods: EVENT_METHODS,
timeoutMs: 0,
});
const baseline = requireCursorEventBatch(baselineResult, cursor);
cursor = baseline.cursor;
const frameTree = await cdp.send("Page.getFrameTree");
const discoveredFrames = flattenFrameTree(frameTree?.frameTree);
if (discoveredFrames[0]?.frameId !== mainFrameId) {
throw invalidCursorRecording();
}
await addBinding(null);
for (const discovered of discoveredFrames) {
discoveredParents.set(discovered.frameId, discovered.parentFrameId);
try {
await installFrame(discovered);
} catch (error) {
if (discovered.parentFrameId === null) throw error;
const target = { targetId: discovered.frameId };
try {
await sendTo(target, "Page.enable");
await sendTo(target, "Runtime.enable");
await addBinding(target);
await installFrame(discovered, target);
} catch {
rememberPendingFrame(discovered, null);
}
}
}
for (
let batchCount = 0;
pendingFrames.size > 0 && batchCount < MAX_STARTUP_TARGET_BATCHES;
batchCount += 1
) {
const batchResult = await cdp.readEvents({
afterSequence: cursor,
limit: 1000,
methods: EVENT_METHODS,
timeoutMs: READ_TIMEOUT_MS,
});
const batch = requireCursorEventBatch(batchResult, cursor);
cursor = batch.cursor;
if (batch.events.length === 0 && !batch.hasMore) break;
for (const event of batch.events) await handleEvent(event);
}
if (pendingFrames.size > 0) throw invalidCursorRecording();
stats.cursorFramesObserved = frames.size;
recordingViewport = { ...frames.get(mainFrameId).viewport };
} catch (error) {
await cleanup().catch(() => {});
throw error?.code === "cursor_recording_failed"
? error
: invalidCursorRecording();
}
let stopped = false;
let stopPromise;
let loopError = null;
const loop = (async () => {
while (true) {
const batchResult = await cdp.readEvents({
afterSequence: cursor,
limit: 1000,
methods: EVENT_METHODS,
timeoutMs: stopped ? 0 : READ_TIMEOUT_MS,
});
const batch = requireCursorEventBatch(batchResult, cursor);
cursor = batch.cursor;
for (const event of batch.events) await handleEvent(event);
if (stopped && !batch.hasMore) break;
}
})().catch((error) => {
loopError =
error?.code === "cursor_recording_failed"
? error
: invalidCursorRecording();
throw loopError;
});
const completion = loop.then(
() => ({ error: null }),
(error) => ({ error }),
);
return {
completion,
stats,
stop() {
if (stopPromise === undefined) {
stopped = true;
stopPromise = (async () => {
await loop.catch(() => {});
const cleanupFailed = await cleanup();
if (loopError !== null) throw loopError;
if (cleanupFailed) throw invalidCursorRecording();
if (pendingFrames.size > 0) throw invalidCursorRecording();
if (recordingViewport === undefined) throw invalidCursorRecording();
return {
durationMs: Math.max(0, now() - startedAt),
events: events.map((event) => ({ ...event })),
viewport: { ...recordingViewport },
};
})();
}
return stopPromise;
},
};
}
SHA-256: 3efd1a2efa85610504fe042f79a742a71acf4dd4a413e64235d11030ff769d8b