From 826790fe522cfb7fb678518dba860eac9f6200d2 Mon Sep 17 00:00:00 2001 From: EtienneLescot Date: Tue, 5 May 2026 22:07:08 +0200 Subject: [PATCH] fix: address native cursor review findings --- .../windowsNativeRecordingSession.ts | 20 ++++-- src/components/video-editor/VideoEditor.tsx | 9 +++ src/components/video-editor/VideoPlayback.tsx | 19 ++++++ src/lib/cursor/nativeCursor.ts | 66 +++++++++++++------ src/lib/exporter/frameRenderer.ts | 1 + 5 files changed, 92 insertions(+), 23 deletions(-) diff --git a/electron/native-bridge/cursor/recording/windowsNativeRecordingSession.ts b/electron/native-bridge/cursor/recording/windowsNativeRecordingSession.ts index 59acf80..dd4aab0 100644 --- a/electron/native-bridge/cursor/recording/windowsNativeRecordingSession.ts +++ b/electron/native-bridge/cursor/recording/windowsNativeRecordingSession.ts @@ -118,7 +118,13 @@ export class WindowsNativeRecordingSession implements CursorRecordingSession { this.rejectReady(error); }); - await this.waitUntilReady(); + try { + await this.waitUntilReady(); + } catch (error) { + this.terminateHelperProcess(); + this.cleanupHelperScript(helperScriptPath); + throw error; + } } async stop(): Promise { @@ -126,9 +132,7 @@ export class WindowsNativeRecordingSession implements CursorRecordingSession { this.process = null; this.clearReadyState(); - if (child && !child.killed) { - child.kill(); - } + this.killHelperProcess(child); this.logDiagnostic("stop", { sampleCount: this.sampleCount, @@ -287,8 +291,16 @@ export class WindowsNativeRecordingSession implements CursorRecordingSession { private failHelper(error: Error) { this.rejectReady(error); + this.terminateHelperProcess(); + } + + private terminateHelperProcess() { const child = this.process; this.process = null; + this.killHelperProcess(child); + } + + private killHelperProcess(child: ChildProcessByStdio | null) { if (child && !child.killed) { child.kill(); } diff --git a/src/components/video-editor/VideoEditor.tsx b/src/components/video-editor/VideoEditor.tsx index 1122200..9af6c25 100644 --- a/src/components/video-editor/VideoEditor.tsx +++ b/src/components/video-editor/VideoEditor.tsx @@ -89,6 +89,15 @@ import { import { UnsavedChangesDialog } from "./UnsavedChangesDialog"; import VideoPlayback, { VideoPlaybackRef } from "./VideoPlayback"; +function isClickInteractionType(interactionType: string | null | undefined) { + return ( + interactionType === "click" || + interactionType === "double-click" || + interactionType === "right-click" || + interactionType === "middle-click" + ); +} + export default function VideoEditor() { const { state: editorState, diff --git a/src/components/video-editor/VideoPlayback.tsx b/src/components/video-editor/VideoPlayback.tsx index bf93e8b..368d465 100644 --- a/src/components/video-editor/VideoPlayback.tsx +++ b/src/components/video-editor/VideoPlayback.tsx @@ -188,6 +188,25 @@ function getResolvedVideoDuration(video: HTMLVideoElement): number | null { return null; } +function getEndedVideoDuration(video: HTMLVideoElement): number | null { + const currentTime = video.currentTime; + if (!Number.isFinite(currentTime) || currentTime <= 0) { + return null; + } + + if (video.ended) { + return currentTime; + } + + const resolvedDuration = getResolvedVideoDuration(video); + const durationEpsilonSeconds = 0.05; + if (resolvedDuration && currentTime >= resolvedDuration - durationEpsilonSeconds) { + return resolvedDuration; + } + + return null; +} + const VideoPlayback = forwardRef( ( { diff --git a/src/lib/cursor/nativeCursor.ts b/src/lib/cursor/nativeCursor.ts index 0291b15..02f4d06 100644 --- a/src/lib/cursor/nativeCursor.ts +++ b/src/lib/cursor/nativeCursor.ts @@ -59,6 +59,43 @@ function clamp(value: number, min: number, max: number) { const NATIVE_CURSOR_CLICK_ANIMATION_MS = 140; const NATIVE_CURSOR_MOTION_BLUR_MAX_PX = 6; +const nativeCursorAssetMapCache = new WeakMap< + CursorRecordingData, + Map +>(); + +function findNativeCursorSampleIndexAtOrBefore(samples: CursorRecordingSample[], timeMs: number) { + let low = 0; + let high = samples.length - 1; + let result = -1; + + while (low <= high) { + const middle = low + Math.floor((high - low) / 2); + if (samples[middle].timeMs <= timeMs) { + result = middle; + low = middle + 1; + } else { + high = middle - 1; + } + } + + return result; +} + +function getNativeCursorAssetMap(recordingData: CursorRecordingData) { + const cached = nativeCursorAssetMapCache.get(recordingData); + if (cached) { + return cached; + } + + const assetMap = new Map(recordingData.assets.map((asset) => [asset.id, asset])); + nativeCursorAssetMapCache.set(recordingData, assetMap); + return assetMap; +} + +function getNativeCursorAsset(recordingData: CursorRecordingData, assetId: string) { + return getNativeCursorAssetMap(recordingData).get(assetId) ?? null; +} interface PrettyNativeCursorAsset { imageDataUrl: string; @@ -296,12 +333,12 @@ export function getNativeCursorClickBounceProgress( return 0; } - for (let index = recordingData.samples.length - 1; index >= 0; index -= 1) { + for ( + let index = findNativeCursorSampleIndexAtOrBefore(recordingData.samples, timeMs); + index >= 0; + index -= 1 + ) { const sample = recordingData.samples[index]; - if (sample.timeMs > timeMs) { - continue; - } - const ageMs = timeMs - sample.timeMs; if (ageMs > NATIVE_CURSOR_CLICK_ANIMATION_MS) { return 0; @@ -397,17 +434,15 @@ export function resolveActiveNativeCursorFrame( return null; } - for (let index = recordingData.samples.length - 1; index >= 0; index -= 1) { + const index = findNativeCursorSampleIndexAtOrBefore(recordingData.samples, timeMs); + if (index >= 0) { const sample = recordingData.samples[index]; - if (sample.timeMs > timeMs) { - continue; - } if (sample.visible === false || !sample.assetId) { return null; } - const asset = recordingData.assets.find((candidate) => candidate.id === sample.assetId); + const asset = getNativeCursorAsset(recordingData, sample.assetId); if (!asset) { return null; } @@ -427,14 +462,7 @@ export function resolveInterpolatedNativeCursorFrame( } const samples = recordingData.samples; - let activeIndex = -1; - - for (let index = samples.length - 1; index >= 0; index -= 1) { - if (samples[index].timeMs <= timeMs) { - activeIndex = index; - break; - } - } + const activeIndex = findNativeCursorSampleIndexAtOrBefore(samples, timeMs); if (activeIndex < 0) { return null; @@ -445,7 +473,7 @@ export function resolveInterpolatedNativeCursorFrame( return null; } - const asset = recordingData.assets.find((candidate) => candidate.id === activeSample.assetId); + const asset = getNativeCursorAsset(recordingData, activeSample.assetId); if (!asset) { return null; } diff --git a/src/lib/exporter/frameRenderer.ts b/src/lib/exporter/frameRenderer.ts index 7b17632..ed87eb0 100644 --- a/src/lib/exporter/frameRenderer.ts +++ b/src/lib/exporter/frameRenderer.ts @@ -601,6 +601,7 @@ export class FrameRenderer { sample: displaySample, }); if (!projectedPoint) { + resetNativeCursorSmoothingState(this.nativeCursorSmoothingState); resetNativeCursorMotionBlurState(this.nativeCursorMotionBlurState); return; }