mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-25 07:16:02 +00:00
Fix cursor sync after recording pause
This commit is contained in:
Vendored
+10
@@ -151,6 +151,16 @@ interface Window {
|
||||
message?: string;
|
||||
error?: string;
|
||||
}>;
|
||||
pauseCursorCapture: () => Promise<{
|
||||
success: boolean;
|
||||
message?: string;
|
||||
error?: string;
|
||||
}>;
|
||||
resumeCursorCapture: () => Promise<{
|
||||
success: boolean;
|
||||
message?: string;
|
||||
error?: string;
|
||||
}>;
|
||||
startFfmpegRecording: (
|
||||
source: ProcessedDesktopSource,
|
||||
) => Promise<{ success: boolean; path?: string; message?: string; error?: string }>;
|
||||
|
||||
@@ -2,7 +2,6 @@ import { createRequire } from "node:module";
|
||||
import type { HookMouseEvent, UiohookLike, UiohookModuleNamespace, CursorInteractionType } from "../types";
|
||||
import {
|
||||
isCursorCaptureActive,
|
||||
cursorCaptureStartTimeMs,
|
||||
interactionCaptureCleanup,
|
||||
setInteractionCaptureCleanup,
|
||||
hasLoggedInteractionHookFailure,
|
||||
@@ -13,7 +12,9 @@ import {
|
||||
} from "../state";
|
||||
import {
|
||||
getNormalizedCursorPoint,
|
||||
getCursorCaptureElapsedMs,
|
||||
getHookCursorScreenPoint,
|
||||
isCursorCapturePaused,
|
||||
pushCursorSample,
|
||||
} from "./telemetry";
|
||||
|
||||
@@ -119,7 +120,7 @@ export async function startInteractionCapture() {
|
||||
}
|
||||
|
||||
const onMouseDown = (event: HookMouseEvent) => {
|
||||
if (!isCursorCaptureActive) {
|
||||
if (!isCursorCaptureActive || isCursorCapturePaused()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -157,7 +158,7 @@ export async function startInteractionCapture() {
|
||||
};
|
||||
|
||||
const onMouseUp = () => {
|
||||
if (!isCursorCaptureActive) {
|
||||
if (!isCursorCaptureActive || isCursorCapturePaused()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -9,6 +9,8 @@ import type { CursorVisualType, CursorInteractionType, CursorTelemetryPoint } fr
|
||||
import {
|
||||
cursorCaptureInterval,
|
||||
setCursorCaptureInterval,
|
||||
cursorCaptureAccumulatedPausedMs,
|
||||
cursorCapturePauseStartedAtMs,
|
||||
cursorCaptureStartTimeMs,
|
||||
activeCursorSamples,
|
||||
pendingCursorSamples,
|
||||
@@ -18,6 +20,8 @@ import {
|
||||
linuxCursorScreenPoint,
|
||||
selectedSource,
|
||||
selectedWindowBounds,
|
||||
setCursorCaptureAccumulatedPausedMs,
|
||||
setCursorCapturePauseStartedAtMs,
|
||||
} from "../state";
|
||||
|
||||
export function clamp(value: number, min: number, max: number) {
|
||||
@@ -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;
|
||||
@@ -117,7 +170,7 @@ export function pushCursorSample(
|
||||
|
||||
export function sampleCursorPoint() {
|
||||
const point = getNormalizedCursorPoint();
|
||||
pushCursorSample(point.cx, point.cy, Date.now() - cursorCaptureStartTimeMs, "move");
|
||||
pushCursorSample(point.cx, point.cy, getCursorCaptureElapsedMs(), "move");
|
||||
}
|
||||
|
||||
export async function persistPendingCursorTelemetry(videoPath: string) {
|
||||
@@ -163,7 +216,7 @@ export function startCursorSampling() {
|
||||
let nextExpectedMs = Date.now() + CURSOR_SAMPLE_INTERVAL_MS;
|
||||
|
||||
const tick = () => {
|
||||
if (isCursorCaptureActive) {
|
||||
if (isCursorCaptureActive && !isCursorCapturePaused()) {
|
||||
sampleCursorPoint();
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,9 @@ import { startInteractionCapture, stopInteractionCapture } from "../cursor/inter
|
||||
import { startNativeCursorMonitor, stopNativeCursorMonitor } from "../cursor/monitor";
|
||||
import {
|
||||
clamp,
|
||||
pauseCursorCapture,
|
||||
resumeCursorCapture,
|
||||
resetCursorCaptureClock,
|
||||
sampleCursorPoint,
|
||||
snapshotCursorTelemetryForPersistence,
|
||||
startCursorSampling,
|
||||
@@ -1275,6 +1278,7 @@ export function registerRecordingHandlers(
|
||||
setActiveCursorSamples([]);
|
||||
setPendingCursorSamples([]);
|
||||
setCursorCaptureStartTimeMs(Date.now());
|
||||
resetCursorCaptureClock();
|
||||
setLinuxCursorScreenPoint(null);
|
||||
setLastLeftClick(null);
|
||||
sampleCursorPoint();
|
||||
@@ -1288,6 +1292,7 @@ export function registerRecordingHandlers(
|
||||
stopNativeCursorMonitor();
|
||||
showCursor();
|
||||
setLinuxCursorScreenPoint(null);
|
||||
resetCursorCaptureClock();
|
||||
snapshotCursorTelemetryForPersistence();
|
||||
setActiveCursorSamples([]);
|
||||
}
|
||||
@@ -1307,6 +1312,18 @@ export function registerRecordingHandlers(
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle("pause-cursor-capture", () => {
|
||||
sampleCursorPoint();
|
||||
pauseCursorCapture(Date.now());
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
ipcMain.handle("resume-cursor-capture", () => {
|
||||
resumeCursorCapture(Date.now());
|
||||
sampleCursorPoint();
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
ipcMain.handle("get-cursor-telemetry", async (_, videoPath?: string) => {
|
||||
const targetVideoPath = normalizeVideoSourcePath(videoPath ?? currentVideoPath);
|
||||
if (!targetVideoPath) {
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -293,6 +293,12 @@ contextBridge.exposeInMainWorld("electronAPI", {
|
||||
resumeNativeScreenRecording: () => {
|
||||
return ipcRenderer.invoke("resume-native-screen-recording");
|
||||
},
|
||||
pauseCursorCapture: () => {
|
||||
return ipcRenderer.invoke("pause-cursor-capture");
|
||||
},
|
||||
resumeCursorCapture: () => {
|
||||
return ipcRenderer.invoke("resume-cursor-capture");
|
||||
},
|
||||
startFfmpegRecording: (source: ProcessedDesktopSource) => {
|
||||
return ipcRenderer.invoke("start-ffmpeg-recording", source);
|
||||
},
|
||||
|
||||
@@ -1094,7 +1094,6 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
|
||||
}
|
||||
}
|
||||
|
||||
const wantsAudioCapture = microphoneEnabled || systemAudioEnabled;
|
||||
const browserCaptureSource = await resolveBrowserCaptureSource(selectedSource);
|
||||
|
||||
if (
|
||||
@@ -1441,6 +1440,11 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
|
||||
if (webcamRecorder.current?.state === "recording") {
|
||||
webcamRecorder.current.pause();
|
||||
}
|
||||
try {
|
||||
await window.electronAPI.pauseCursorCapture();
|
||||
} catch (error) {
|
||||
console.warn("Failed to pause cursor capture:", error);
|
||||
}
|
||||
markRecordingPaused(Date.now());
|
||||
setPaused(true);
|
||||
})();
|
||||
@@ -1451,8 +1455,15 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
|
||||
if (webcamRecorder.current?.state === "recording") {
|
||||
webcamRecorder.current.pause();
|
||||
}
|
||||
markRecordingPaused(Date.now());
|
||||
setPaused(true);
|
||||
void (async () => {
|
||||
try {
|
||||
await window.electronAPI.pauseCursorCapture();
|
||||
} catch (error) {
|
||||
console.warn("Failed to pause cursor capture:", error);
|
||||
}
|
||||
markRecordingPaused(Date.now());
|
||||
setPaused(true);
|
||||
})();
|
||||
}
|
||||
}, [markRecordingPaused, paused, recording]);
|
||||
|
||||
@@ -1472,6 +1483,11 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
|
||||
if (webcamRecorder.current?.state === "paused") {
|
||||
webcamRecorder.current.resume();
|
||||
}
|
||||
try {
|
||||
await window.electronAPI.resumeCursorCapture();
|
||||
} catch (error) {
|
||||
console.warn("Failed to resume cursor capture:", error);
|
||||
}
|
||||
markRecordingResumed(Date.now());
|
||||
setPaused(false);
|
||||
})();
|
||||
@@ -1482,8 +1498,15 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
|
||||
if (webcamRecorder.current?.state === "paused") {
|
||||
webcamRecorder.current.resume();
|
||||
}
|
||||
markRecordingResumed(Date.now());
|
||||
setPaused(false);
|
||||
void (async () => {
|
||||
try {
|
||||
await window.electronAPI.resumeCursorCapture();
|
||||
} catch (error) {
|
||||
console.warn("Failed to resume cursor capture:", error);
|
||||
}
|
||||
markRecordingResumed(Date.now());
|
||||
setPaused(false);
|
||||
})();
|
||||
}
|
||||
}, [markRecordingResumed, paused, recording]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user