fix(recording): clamp cursor samples at pause boundary

This commit is contained in:
wiiiii123 committed 2026-05-09 12:57:05 +07:00
1 parent 7fee8bec2a
commit d7e27d3152
6 files changed
+65 -27

No files matched your search

+2 -2
View File
@@ -254,12 +254,12 @@ interface Window {
message?: string;
error?: string;
}>;
pauseCursorCapture: () => Promise<{
pauseCursorCapture: (pausedAtMs?: number) => Promise<{
success: boolean;
message?: string;
error?: string;
}>;
resumeCursorCapture: () => Promise<{
resumeCursorCapture: (resumedAtMs?: number) => Promise<{
success: boolean;
message?: string;
error?: string;
+18 -1
View File
@@ -29,21 +29,24 @@ vi.mock("../utils", () => ({
})),
}));
import { activeCursorSamples, setActiveCursorSamples, setCursorCaptureStartTimeMs } from "../state";
import {
getCursorCaptureElapsedMs,
normalizeCursorTelemetrySamples,
pauseCursorCapture,
pauseCursorCaptureAtBoundary,
pushCursorSample,
resetCursorCaptureClock,
resumeCursorCapture,
writeCursorTelemetry,
} from "./telemetry";
import { setCursorCaptureStartTimeMs } from "../state";
describe("cursor telemetry pause clock", () => {
beforeEach(() => {
writeFile.mockReset();
rm.mockReset();
setCursorCaptureStartTimeMs(1_000);
setActiveCursorSamples([]);
resetCursorCaptureClock();
});
@@ -66,6 +69,20 @@ describe("cursor telemetry pause clock", () => {
expect(getCursorCaptureElapsedMs(1_900)).toBe(550);
});
it("drops cursor samples captured after the renderer pause boundary", () => {
pushCursorSample(0.1, 0.1, 120, "move");
pushCursorSample(0.2, 0.2, 205, "move");
pushCursorSample(0.3, 0.3, 260, "move");
pauseCursorCaptureAtBoundary(1_200);
expect(getCursorCaptureElapsedMs(1_500)).toBe(200);
expect(activeCursorSamples.map((sample) => sample.timeMs)).toEqual([120]);
resumeCursorCapture(1_700);
expect(getCursorCaptureElapsedMs(1_900)).toBe(400);
});
it("normalizes cursor telemetry samples before persisting them", async () => {
const samples = normalizeCursorTelemetrySamples([
{ timeMs: 30, cx: 2, cy: -1, interactionType: "click", cursorType: "pointer" },
+23 -10
View File
@@ -1,28 +1,29 @@
import fs from "node:fs/promises";
import { getTelemetryPathForVideo, getScreen } from "../utils";
import {
CURSOR_SAMPLE_INTERVAL_MS,
CURSOR_TELEMETRY_VERSION,
MAX_CURSOR_SAMPLES,
CURSOR_SAMPLE_INTERVAL_MS,
} from "../constants";
import type { CursorVisualType, CursorInteractionType, CursorTelemetryPoint } from "../types";
import {
cursorCaptureInterval,
setCursorCaptureInterval,
activeCursorSamples,
currentCursorVisualType,
cursorCaptureAccumulatedPausedMs,
cursorCaptureInterval,
cursorCapturePauseStartedAtMs,
cursorCaptureStartTimeMs,
activeCursorSamples,
pendingCursorSamples,
setPendingCursorSamples,
isCursorCaptureActive,
currentCursorVisualType,
linuxCursorScreenPoint,
pendingCursorSamples,
selectedSource,
selectedWindowBounds,
setActiveCursorSamples,
setCursorCaptureAccumulatedPausedMs,
setCursorCaptureInterval,
setCursorCapturePauseStartedAtMs,
setPendingCursorSamples,
} from "../state";
import type { CursorInteractionType, CursorTelemetryPoint, CursorVisualType } from "../types";
import { getScreen, getTelemetryPathForVideo } from "../utils";
export function clamp(value: number, min: number, max: number) {
return Math.min(max, Math.max(min, value));
@@ -125,6 +126,18 @@ export function pauseCursorCapture(pausedAtMs: number) {
setCursorCapturePauseStartedAtMs(pausedAtMs);
}
export function pauseCursorCaptureAtBoundary(pausedAtMs: number) {
if (cursorCapturePauseStartedAtMs !== null) {
return;
}
const pausedElapsedMs = getCursorCaptureElapsedMs(pausedAtMs);
setActiveCursorSamples(
activeCursorSamples.filter((sample) => sample.timeMs <= pausedElapsedMs),
);
setCursorCapturePauseStartedAtMs(pausedAtMs);
}
export function resumeCursorCapture(resumedAtMs: number) {
if (cursorCapturePauseStartedAtMs === null) {
return;
@@ -310,6 +323,6 @@ export function startCursorSampling() {
setCursorCaptureInterval(setTimeout(tick, CURSOR_SAMPLE_INTERVAL_MS));
}
export { CURSOR_SAMPLE_INTERVAL_MS } from "../constants";
// Re-export for consumers that use it from this module
export { getTelemetryPathForVideo } from "../utils";
export { CURSOR_SAMPLE_INTERVAL_MS } from "../constants";
+14 -6
View File
@@ -19,7 +19,7 @@ import { startInteractionCapture, stopInteractionCapture } from "../cursor/inter
import { startNativeCursorMonitor, stopNativeCursorMonitor } from "../cursor/monitor";
import {
normalizeCursorTelemetrySamples,
pauseCursorCapture,
pauseCursorCaptureAtBoundary,
resetCursorCaptureClock,
resumeCursorCapture,
sampleCursorPoint,
@@ -192,6 +192,15 @@ function pickPrimitiveRecord(value: unknown) {
return entries.length > 0 ? Object.fromEntries(entries) : null;
}
function normalizeRendererTimestampMs(value: unknown) {
const nowMs = Date.now();
if (typeof value !== "number" || !Number.isFinite(value)) {
return nowMs;
}
return Math.min(Math.max(0, Math.round(value)), nowMs + 1000);
}
function pickMicrophoneChunkEvents(value: unknown): MicrophoneChunkTimingEvent[] | null {
if (!Array.isArray(value)) {
return null;
@@ -1715,14 +1724,13 @@ export function registerRecordingHandlers(
}
});
ipcMain.handle("pause-cursor-capture", () => {
sampleCursorPoint();
pauseCursorCapture(Date.now());
ipcMain.handle("pause-cursor-capture", (_, pausedAtMs?: unknown) => {
pauseCursorCaptureAtBoundary(normalizeRendererTimestampMs(pausedAtMs));
return { success: true };
});
ipcMain.handle("resume-cursor-capture", () => {
resumeCursorCapture(Date.now());
ipcMain.handle("resume-cursor-capture", (_, resumedAtMs?: unknown) => {
resumeCursorCapture(normalizeRendererTimestampMs(resumedAtMs));
sampleCursorPoint();
return { success: true };
});
+4 -4
View File
@@ -493,11 +493,11 @@ contextBridge.exposeInMainWorld("electronAPI", {
resumeNativeScreenRecording: () => {
return ipcRenderer.invoke("resume-native-screen-recording");
},
pauseCursorCapture: () => {
return ipcRenderer.invoke("pause-cursor-capture");
pauseCursorCapture: (pausedAtMs?: number) => {
return ipcRenderer.invoke("pause-cursor-capture", pausedAtMs);
},
resumeCursorCapture: () => {
return ipcRenderer.invoke("resume-cursor-capture");
resumeCursorCapture: (resumedAtMs?: number) => {
return ipcRenderer.invoke("resume-cursor-capture", resumedAtMs);
},
startFfmpegRecording: (source: ProcessedDesktopSource) => {
return ipcRenderer.invoke("start-ffmpeg-recording", source);
+4 -4
View File
@@ -1781,7 +1781,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
markRecordingPaused(boundaryMs);
setPaused(true);
try {
await window.electronAPI.pauseCursorCapture();
await window.electronAPI.pauseCursorCapture(boundaryMs);
} catch (error) {
console.warn("Failed to pause cursor capture:", error);
}
@@ -1798,7 +1798,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
markRecordingPaused(boundaryMs);
setPaused(true);
try {
await window.electronAPI.pauseCursorCapture();
await window.electronAPI.pauseCursorCapture(boundaryMs);
} catch (error) {
console.warn("Failed to pause cursor capture:", error);
}
@@ -1827,7 +1827,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
markRecordingResumed(boundaryMs);
setPaused(false);
try {
await window.electronAPI.resumeCursorCapture();
await window.electronAPI.resumeCursorCapture(boundaryMs);
} catch (error) {
console.warn("Failed to resume cursor capture:", error);
}
@@ -1844,7 +1844,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
markRecordingResumed(boundaryMs);
setPaused(false);
try {
await window.electronAPI.resumeCursorCapture();
await window.electronAPI.resumeCursorCapture(boundaryMs);
} catch (error) {
console.warn("Failed to resume cursor capture:", error);
}