mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-11-09 07:23:14 +00:00
fix(recording): clamp cursor samples at pause boundary
This commit is contained in:
1 parent
7fee8bec2a
commit
d7e27d3152
6 files changed
+65
-27
No files matched your search
Vendored
+2
-2
@@ -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;
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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";
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user