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
10 changes: 10 additions & 0 deletions electron/electron-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,16 @@ interface Window {
message?: string;
error?: string;
}>;
pauseCursorCapture: (boundaryMs?: number) => Promise<{
success: boolean;
message?: string;
error?: string;
}>;
resumeCursorCapture: (boundaryMs?: number) => Promise<{
success: boolean;
message?: string;
error?: string;
}>;
startFfmpegRecording: (
source: ProcessedDesktopSource,
) => Promise<{ success: boolean; path?: string; message?: string; error?: string }>;
Expand Down
17 changes: 11 additions & 6 deletions electron/ipc/cursor/interaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { createRequire } from "node:module";
import type { HookMouseEvent, UiohookLike, UiohookModuleNamespace, CursorInteractionType } from "../types";
import {
isCursorCaptureActive,
cursorCaptureStartTimeMs,
interactionCaptureCleanup,
setInteractionCaptureCleanup,
hasLoggedInteractionHookFailure,
Expand All @@ -13,7 +12,9 @@ import {
} from "../state";
import {
getNormalizedCursorPoint,
getCursorCaptureElapsedMs,
getHookCursorScreenPoint,
isCursorCapturePaused,
pushCursorSample,
} from "./telemetry";

Expand Down Expand Up @@ -119,7 +120,7 @@ export async function startInteractionCapture() {
}

const onMouseDown = (event: HookMouseEvent) => {
if (!isCursorCaptureActive) {
if (!isCursorCaptureActive || isCursorCapturePaused()) {
return;
}

Expand All @@ -128,7 +129,7 @@ export async function startInteractionCapture() {
return;
}

const timeMs = Date.now() - cursorCaptureStartTimeMs;
const timeMs = getCursorCaptureElapsedMs();
const button = getHookMouseButton(event);
let interactionType: CursorInteractionType = "click";

Expand Down Expand Up @@ -157,7 +158,7 @@ export async function startInteractionCapture() {
};

const onMouseUp = () => {
if (!isCursorCaptureActive) {
if (!isCursorCaptureActive || isCursorCapturePaused()) {
return;
}

Expand All @@ -166,12 +167,16 @@ export async function startInteractionCapture() {
return;
}

const timeMs = Date.now() - cursorCaptureStartTimeMs;
const timeMs = getCursorCaptureElapsedMs();
pushCursorSample(point.cx, point.cy, timeMs, "mouseup");
};

const onMouseMove = (event: HookMouseEvent) => {
if (process.platform !== "linux" || !isCursorCaptureActive) {
if (
process.platform !== "linux" ||
!isCursorCaptureActive ||
isCursorCapturePaused()
) {
return;
}

Expand Down
51 changes: 51 additions & 0 deletions electron/ipc/cursor/telemetry.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { beforeEach, describe, expect, it, vi } from "vitest";

vi.mock("electron", () => ({
app: {
getPath: vi.fn(() => "/tmp"),
},
}));

vi.mock("../utils", () => ({
getTelemetryPathForVideo: vi.fn(() => "/tmp/recording.cursor.json"),
getScreen: vi.fn(() => ({
getCursorScreenPoint: () => ({ x: 0, y: 0 }),
getPrimaryDisplay: () => ({ scaleFactor: 1 }),
getDisplayNearestPoint: () => ({ bounds: { x: 0, y: 0, width: 1, height: 1 } }),
getAllDisplays: () => [],
})),
}));

import {
getCursorCaptureElapsedMs,
pauseCursorCapture,
resetCursorCaptureClock,
resumeCursorCapture,
} from "./telemetry";
import { setCursorCaptureStartTimeMs } from "../state";

describe("cursor telemetry pause clock", () => {
beforeEach(() => {
setCursorCaptureStartTimeMs(1_000);
resetCursorCaptureClock();
});

it("subtracts paused time from elapsed cursor timestamps", () => {
expect(getCursorCaptureElapsedMs(1_120)).toBe(120);

pauseCursorCapture(1_200);
expect(getCursorCaptureElapsedMs(1_450)).toBe(200);

resumeCursorCapture(1_700);
expect(getCursorCaptureElapsedMs(1_900)).toBe(400);
});

it("ignores duplicate pause or resume transitions", () => {
pauseCursorCapture(1_150);
pauseCursorCapture(1_250);
resumeCursorCapture(1_500);
resumeCursorCapture(1_650);

expect(getCursorCaptureElapsedMs(1_900)).toBe(550);
});
});
59 changes: 56 additions & 3 deletions electron/ipc/cursor/telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import type { CursorVisualType, CursorInteractionType, CursorTelemetryPoint } fr
import {
cursorCaptureInterval,
setCursorCaptureInterval,
cursorCaptureAccumulatedPausedMs,
cursorCapturePauseStartedAtMs,
cursorCaptureStartTimeMs,
activeCursorSamples,
pendingCursorSamples,
Expand All @@ -18,6 +20,8 @@ import {
linuxCursorScreenPoint,
selectedSource,
selectedWindowBounds,
setCursorCaptureAccumulatedPausedMs,
setCursorCapturePauseStartedAtMs,
} from "../state";

export function clamp(value: number, min: number, max: number) {
Expand All @@ -31,6 +35,55 @@ export function stopCursorCapture() {
}
}

export function resetCursorCaptureClock() {
setCursorCaptureAccumulatedPausedMs(0);
setCursorCapturePauseStartedAtMs(null);
}

export function isCursorCapturePaused() {
return cursorCapturePauseStartedAtMs !== null;
}

export function pauseCursorCapture(pausedAtMs: number) {
if (cursorCapturePauseStartedAtMs !== null) {
return;
}

setCursorCapturePauseStartedAtMs(pausedAtMs);
}

export function resumeCursorCapture(resumedAtMs: number) {
if (cursorCapturePauseStartedAtMs === null) {
return;
}

const pauseDurationMs = Math.max(0, resumedAtMs - cursorCapturePauseStartedAtMs);
setCursorCaptureAccumulatedPausedMs(
cursorCaptureAccumulatedPausedMs + pauseDurationMs,
);
setCursorCapturePauseStartedAtMs(null);
}

export function getCursorCaptureElapsedMs(nowMs = Date.now()) {
if (!Number.isFinite(cursorCaptureStartTimeMs) || cursorCaptureStartTimeMs <= 0) {
return 0;
}

const safeNowMs = Math.max(cursorCaptureStartTimeMs, nowMs);
const activePauseDurationMs =
cursorCapturePauseStartedAtMs === null
? 0
: Math.max(0, safeNowMs - cursorCapturePauseStartedAtMs);

return Math.max(
0,
safeNowMs -
cursorCaptureStartTimeMs -
Math.max(0, cursorCaptureAccumulatedPausedMs) -
activePauseDurationMs,
);
}

export function getNormalizedCursorPoint() {
const fallbackCursor = getScreen().getCursorScreenPoint();
const linuxCursorCache = process.platform === "linux" ? linuxCursorScreenPoint : null;
Expand Down Expand Up @@ -115,9 +168,9 @@ export function pushCursorSample(
}
}

export function sampleCursorPoint() {
export function sampleCursorPoint(sampledAtMs = Date.now()) {
const point = getNormalizedCursorPoint();
pushCursorSample(point.cx, point.cy, Date.now() - cursorCaptureStartTimeMs, "move");
pushCursorSample(point.cx, point.cy, getCursorCaptureElapsedMs(sampledAtMs), "move");
}

export async function persistPendingCursorTelemetry(videoPath: string) {
Expand Down Expand Up @@ -163,7 +216,7 @@ export function startCursorSampling() {
let nextExpectedMs = Date.now() + CURSOR_SAMPLE_INTERVAL_MS;

const tick = () => {
if (isCursorCaptureActive) {
if (isCursorCaptureActive && !isCursorCapturePaused()) {
sampleCursorPoint();
}

Expand Down
25 changes: 25 additions & 0 deletions electron/ipc/register/recording.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ import { startInteractionCapture, stopInteractionCapture } from "../cursor/inter
import { startNativeCursorMonitor, stopNativeCursorMonitor } from "../cursor/monitor";
import {
clamp,
pauseCursorCapture,
resumeCursorCapture,
resetCursorCaptureClock,
sampleCursorPoint,
snapshotCursorTelemetryForPersistence,
startCursorSampling,
Expand Down Expand Up @@ -1275,6 +1278,7 @@ export function registerRecordingHandlers(
setActiveCursorSamples([]);
setPendingCursorSamples([]);
setCursorCaptureStartTimeMs(Date.now());
resetCursorCaptureClock();
setLinuxCursorScreenPoint(null);
setLastLeftClick(null);
sampleCursorPoint();
Expand All @@ -1288,6 +1292,7 @@ export function registerRecordingHandlers(
stopNativeCursorMonitor();
showCursor();
setLinuxCursorScreenPoint(null);
resetCursorCaptureClock();
snapshotCursorTelemetryForPersistence();
setActiveCursorSamples([]);
}
Expand All @@ -1307,6 +1312,26 @@ export function registerRecordingHandlers(
}
});

ipcMain.handle("pause-cursor-capture", (_event, boundaryMs?: number) => {
const timestamp =
typeof boundaryMs === "number" && Number.isFinite(boundaryMs)
? boundaryMs
: Date.now();
sampleCursorPoint(timestamp);
pauseCursorCapture(timestamp);
return { success: true };
});

ipcMain.handle("resume-cursor-capture", (_event, boundaryMs?: number) => {
const timestamp =
typeof boundaryMs === "number" && Number.isFinite(boundaryMs)
? boundaryMs
: Date.now();
resumeCursorCapture(timestamp);
sampleCursorPoint(timestamp);
return { success: true };
});

ipcMain.handle("get-cursor-telemetry", async (_, videoPath?: string) => {
const targetVideoPath = normalizeVideoSourcePath(videoPath ?? currentVideoPath);
if (!targetVideoPath) {
Expand Down
8 changes: 8 additions & 0 deletions electron/ipc/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ export let currentCursorVisualType: CursorVisualType | undefined = undefined;
// ── Cursor telemetry ──────────────────────────────────────────────────────────
export let cursorCaptureInterval: NodeJS.Timeout | null = null;
export let cursorCaptureStartTimeMs = 0;
export let cursorCaptureAccumulatedPausedMs = 0;
export let cursorCapturePauseStartedAtMs: number | null = null;
export let activeCursorSamples: CursorTelemetryPoint[] = [];
export let pendingCursorSamples: CursorTelemetryPoint[] = [];
export let isCursorCaptureActive = false;
Expand Down Expand Up @@ -237,6 +239,12 @@ export function setCursorCaptureInterval(v: NodeJS.Timeout | null) {
export function setCursorCaptureStartTimeMs(v: number) {
cursorCaptureStartTimeMs = v;
}
export function setCursorCaptureAccumulatedPausedMs(v: number) {
cursorCaptureAccumulatedPausedMs = v;
}
export function setCursorCapturePauseStartedAtMs(v: number | null) {
cursorCapturePauseStartedAtMs = v;
}
export function setActiveCursorSamples(v: CursorTelemetryPoint[]) {
activeCursorSamples = v;
}
Expand Down
6 changes: 6 additions & 0 deletions electron/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,12 @@ contextBridge.exposeInMainWorld("electronAPI", {
resumeNativeScreenRecording: () => {
return ipcRenderer.invoke("resume-native-screen-recording");
},
pauseCursorCapture: (boundaryMs?: number) => {
return ipcRenderer.invoke("pause-cursor-capture", boundaryMs);
},
resumeCursorCapture: (boundaryMs?: number) => {
return ipcRenderer.invoke("resume-cursor-capture", boundaryMs);
},
startFfmpegRecording: (source: ProcessedDesktopSource) => {
return ipcRenderer.invoke("start-ffmpeg-recording", source);
},
Expand Down
Loading