Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion electron/electron-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ interface Window {
message?: string;
error?: string;
}>;
setRecordingState: (recording: boolean) => Promise<void>;
setRecordingState: (recording: boolean, recordingId?: number) => Promise<void>;
discardCursorTelemetry: (recordingId: number) => Promise<void>;
getCursorTelemetry: (videoPath?: string) => Promise<{
success: boolean;
samples: CursorTelemetryPoint[];
Expand Down
61 changes: 36 additions & 25 deletions electron/ipc/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ import {
shell,
systemPreferences,
} from "electron";
import {
type CursorTelemetryPoint,
createCursorTelemetryBuffer,
} from "../../src/lib/cursorTelemetryBuffer";
import {
normalizeProjectMedia,
normalizeRecordingSession,
Expand Down Expand Up @@ -275,14 +279,23 @@ async function storeRecordedSessionFiles(payload: StoreRecordedSessionInput) {
currentProjectPath = null;

const telemetryPath = `${screenVideoPath}.cursor.json`;
if (pendingCursorSamples.length > 0) {
await fs.writeFile(
telemetryPath,
JSON.stringify({ version: CURSOR_TELEMETRY_VERSION, samples: pendingCursorSamples }, null, 2),
"utf-8",
);
const pendingBatch = cursorTelemetryBuffer.takeNextBatch();
if (pendingBatch && pendingBatch.samples.length > 0) {
try {
await fs.writeFile(
telemetryPath,
JSON.stringify(
{ version: CURSOR_TELEMETRY_VERSION, samples: pendingBatch.samples },
null,
2,
),
"utf-8",
);
} catch (err) {
cursorTelemetryBuffer.prependBatch(pendingBatch);
throw err;
}
}
pendingCursorSamples = [];

const sessionManifestPath = path.join(
RECORDINGS_DIR,
Expand All @@ -302,16 +315,11 @@ const CURSOR_TELEMETRY_VERSION = 1;
const CURSOR_SAMPLE_INTERVAL_MS = 100;
const MAX_CURSOR_SAMPLES = 60 * 60 * 10; // 1 hour @ 10Hz

interface CursorTelemetryPoint {
timeMs: number;
cx: number;
cy: number;
}

let cursorCaptureInterval: NodeJS.Timeout | null = null;
let cursorCaptureStartTimeMs = 0;
let activeCursorSamples: CursorTelemetryPoint[] = [];
let pendingCursorSamples: CursorTelemetryPoint[] = [];
const cursorTelemetryBuffer = createCursorTelemetryBuffer({
maxActiveSamples: MAX_CURSOR_SAMPLES,
});

function clamp(value: number, min: number, max: number) {
return Math.min(max, Math.max(min, value));
Expand All @@ -338,15 +346,11 @@ function sampleCursorPoint() {
const cx = clamp((cursor.x - bounds.x) / width, 0, 1);
const cy = clamp((cursor.y - bounds.y) / height, 0, 1);

activeCursorSamples.push({
cursorTelemetryBuffer.push({
timeMs: Math.max(0, Date.now() - cursorCaptureStartTimeMs),
cx,
cy,
});

if (activeCursorSamples.length > MAX_CURSOR_SAMPLES) {
activeCursorSamples.shift();
}
}

export function registerIpcHandlers(
Expand Down Expand Up @@ -531,18 +535,21 @@ export function registerIpcHandlers(
}
});

ipcMain.handle("set-recording-state", (_, recording: boolean) => {
ipcMain.handle("set-recording-state", (_, recording: boolean, recordingId?: number) => {
if (recording) {
stopCursorCapture();
activeCursorSamples = [];
pendingCursorSamples = [];
// The renderer is the source of truth for the recording id (it
// uses the same id as the saved fileName). Fall back to a
// timestamp only if the renderer didn't supply one, so the
// buffer always has a stable key per session.
const id = typeof recordingId === "number" ? recordingId : Date.now();
cursorTelemetryBuffer.startSession(id);
Comment on lines +545 to +546
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Harden recordingId validation before starting a session.

Line 545 accepts any JS number, including NaN/Infinity. That can produce non-matchable or unstable batch keys. Use a finite positive integer guard before startSession, else fallback to Date.now().

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@electron/ipc/handlers.ts` around lines 545 - 546, The code currently treats
any JS number as a valid recordingId before calling
cursorTelemetryBuffer.startSession; change the validation so that recordingId is
only accepted when it's a finite positive integer (use
Number.isFinite(recordingId) && Number.isInteger(recordingId) && recordingId >
0); otherwise compute id = Date.now() and then call
cursorTelemetryBuffer.startSession(id). Update the id assignment and use the
existing cursorTelemetryBuffer.startSession call (and any downstream logic that
relies on id) to ensure unstable values like NaN/Infinity are never passed.

cursorCaptureStartTimeMs = Date.now();
sampleCursorPoint();
cursorCaptureInterval = setInterval(sampleCursorPoint, CURSOR_SAMPLE_INTERVAL_MS);
} else {
stopCursorCapture();
pendingCursorSamples = [...activeCursorSamples];
activeCursorSamples = [];
cursorTelemetryBuffer.endSession();
}

const source = selectedSource || { name: "Screen" };
Expand All @@ -551,6 +558,10 @@ export function registerIpcHandlers(
}
});

ipcMain.handle("discard-cursor-telemetry", (_, recordingId: number) => {
cursorTelemetryBuffer.discardBatch(recordingId);
});

ipcMain.handle("get-cursor-telemetry", async (_, videoPath?: string) => {
const targetVideoPath = normalizeVideoSourcePath(
videoPath ?? currentRecordingSession?.screenVideoPath,
Expand Down
7 changes: 5 additions & 2 deletions electron/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,15 @@ contextBridge.exposeInMainWorld("electronAPI", {
getRecordedVideoPath: () => {
return ipcRenderer.invoke("get-recorded-video-path");
},
setRecordingState: (recording: boolean) => {
return ipcRenderer.invoke("set-recording-state", recording);
setRecordingState: (recording: boolean, recordingId?: number) => {
return ipcRenderer.invoke("set-recording-state", recording, recordingId);
},
getCursorTelemetry: (videoPath?: string) => {
return ipcRenderer.invoke("get-cursor-telemetry", videoPath);
},
discardCursorTelemetry: (recordingId: number) => {
return ipcRenderer.invoke("discard-cursor-telemetry", recordingId);
},
onStopRecordingFromTray: (callback: () => void) => {
const listener = () => callback();
ipcRenderer.on("stop-recording-from-tray", listener);
Expand Down
3 changes: 2 additions & 1 deletion src/hooks/useScreenRecorder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
try {
const screenBlob = await activeScreenRecorder.recordedBlobPromise;
if (discardRecordingId.current === activeRecordingId) {
window.electronAPI?.discardCursorTelemetry(activeRecordingId);
return;
}
if (screenBlob.size === 0) {
Expand Down Expand Up @@ -553,7 +554,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
setRecording(true);
setPaused(false);
setElapsedSeconds(0);
window.electronAPI?.setRecordingState(true);
window.electronAPI?.setRecordingState(true, recordingId.current);

const activeScreenRecorder = screenRecorder.current;
const activeWebcamRecorder = webcamRecorder.current;
Expand Down
Loading
Loading