From 83b7ec48b81689bf187305e637e5d6b08f81503d Mon Sep 17 00:00:00 2001 From: webadderall <131426131+webadderall@users.noreply.github.com> Date: Sat, 14 Mar 2026 23:29:32 +1100 Subject: [PATCH] fix(windows): resolve cursor telemetry paths and prevent export finalization stalls Credit: Adapted from OpenScreen upstream PR contributions. --- electron/ipc/handlers.ts | 21 ++++++++- .../video-editor/projectPersistence.ts | 10 ++++- src/lib/exporter/videoExporter.ts | 44 +++++++++++++++++-- 3 files changed, 68 insertions(+), 7 deletions(-) diff --git a/electron/ipc/handlers.ts b/electron/ipc/handlers.ts index ab6d8d75..655be536 100644 --- a/electron/ipc/handlers.ts +++ b/electron/ipc/handlers.ts @@ -121,6 +121,23 @@ function getTelemetryPathForVideo(videoPath: string) { return `${videoPath}.cursor.json` } +function normalizeVideoPathInput(videoPath: string) { + if (!videoPath.startsWith('file:')) { + return videoPath + } + + try { + const url = new URL(videoPath) + let filePath = decodeURIComponent(url.pathname) + if (/^\/[A-Za-z]:/.test(filePath)) { + filePath = filePath.slice(1) + } + return filePath + } catch { + return videoPath.replace(/^file:\/\//, '') + } +} + async function hasSiblingProjectFile(videoPath: string) { const baseName = path.basename(videoPath, path.extname(videoPath)) const candidateExtensions = [PROJECT_FILE_EXTENSION, ...LEGACY_PROJECT_FILE_EXTENSIONS] @@ -1770,12 +1787,12 @@ export function registerIpcHandlers( }) ipcMain.handle('get-cursor-telemetry', async (_, videoPath?: string) => { - const targetVideoPath = videoPath ?? currentVideoPath + const targetVideoPath = videoPath ? normalizeVideoPathInput(videoPath) : currentVideoPath if (!targetVideoPath) { return { success: true, samples: [] } } - const telemetryPath = `${targetVideoPath}.cursor.json` + const telemetryPath = getTelemetryPathForVideo(targetVideoPath) try { const content = await fs.readFile(telemetryPath, 'utf-8') const parsed = JSON.parse(content) diff --git a/src/components/video-editor/projectPersistence.ts b/src/components/video-editor/projectPersistence.ts index 493383c8..2eebea86 100644 --- a/src/components/video-editor/projectPersistence.ts +++ b/src/components/video-editor/projectPersistence.ts @@ -25,6 +25,7 @@ export const PROJECT_VERSION = 1; export interface ProjectEditorState { wallpaper: string; + backgroundTransparency: boolean; shadowIntensity: number; backgroundBlur: number; zoomMotionBlur: number; @@ -78,7 +79,11 @@ export function fromFileUrl(fileUrl: string): string { try { const url = new URL(fileUrl); - return decodeURIComponent(url.pathname); + let filePath = decodeURIComponent(url.pathname); + if (/^\/[A-Za-z]:/.test(filePath)) { + filePath = filePath.slice(1); + } + return filePath; } catch { return fileUrl.replace(/^file:\/\//, ""); } @@ -255,6 +260,9 @@ export function normalizeProjectEditor(editor: Partial): Pro return { wallpaper: typeof editor.wallpaper === "string" ? editor.wallpaper : WALLPAPER_PATHS[0], + backgroundTransparency: typeof (editor as Partial).backgroundTransparency === "boolean" + ? (editor as Partial).backgroundTransparency as boolean + : false, shadowIntensity: typeof editor.shadowIntensity === "number" ? editor.shadowIntensity : 0.67, backgroundBlur: normalizedBackgroundBlur, zoomMotionBlur: normalizedZoomMotionBlur, diff --git a/src/lib/exporter/videoExporter.ts b/src/lib/exporter/videoExporter.ts index 8a6dfb14..110e7503 100644 --- a/src/lib/exporter/videoExporter.ts +++ b/src/lib/exporter/videoExporter.ts @@ -8,6 +8,7 @@ import type { ZoomRegion, CropRegion, TrimRegion, AnnotationRegion, SpeedRegion, interface VideoExporterConfig extends ExportConfig { videoUrl: string; wallpaper: string; + backgroundTransparency?: boolean; zoomRegions: ZoomRegion[]; trimRegions?: TrimRegion[]; speedRegions?: SpeedRegion[]; @@ -47,6 +48,7 @@ export class VideoExporter { private videoColorSpace: VideoColorSpaceInit | undefined; private pendingMuxing: Promise = Promise.resolve(); private chunkCount = 0; + private readonly WINDOWS_FINALIZATION_TIMEOUT_MS = 60_000; constructor(config: VideoExporterConfig) { this.config = config; @@ -66,6 +68,7 @@ export class VideoExporter { width: this.config.width, height: this.config.height, wallpaper: this.config.wallpaper, + backgroundTransparency: this.config.backgroundTransparency, zoomRegions: this.config.zoomRegions, showShadow: this.config.showShadow, shadowIntensity: this.config.shadowIntensity, @@ -139,22 +142,25 @@ export class VideoExporter { // Finalize encoding if (this.encoder && this.encoder.state === 'configured') { - await this.encoder.flush(); + await this.awaitWithWindowsTimeout(this.encoder.flush(), 'encoder flush'); } // Wait for queued muxing operations to complete - await this.pendingMuxing; + await this.awaitWithWindowsTimeout(this.pendingMuxing, 'muxing queued video chunks'); if (hasAudio && !this.cancelled) { const demuxer = this.streamingDecoder.getDemuxer(); if (demuxer) { this.audioProcessor = new AudioProcessor(); - await this.audioProcessor.process(demuxer, this.muxer!, this.config.trimRegions); + await this.awaitWithWindowsTimeout( + this.audioProcessor.process(demuxer, this.muxer!, this.config.trimRegions), + 'audio processing', + ); } } // Finalize muxer and get output blob - const blob = await this.muxer!.finalize(); + const blob = await this.awaitWithWindowsTimeout(this.muxer!.finalize(), 'muxer finalization'); return { success: true, blob }; } catch (error) { @@ -168,6 +174,36 @@ export class VideoExporter { } } + private isWindowsPlatform(): boolean { + if (typeof navigator === 'undefined') { + return false; + } + return /Win/i.test(navigator.platform); + } + + private async awaitWithWindowsTimeout(promise: Promise, stage: string): Promise { + if (!this.isWindowsPlatform()) { + return promise; + } + + let timeoutId: ReturnType | null = null; + + try { + return await Promise.race([ + promise, + new Promise((_, reject) => { + timeoutId = setTimeout(() => { + reject(new Error(`Export timed out during ${stage} on Windows`)); + }, this.WINDOWS_FINALIZATION_TIMEOUT_MS); + }), + ]); + } finally { + if (timeoutId) { + clearTimeout(timeoutId); + } + } + } + private async encodeRenderedFrame(timestamp: number, frameDuration: number, frameIndex: number) { const canvas = this.renderer!.getCanvas();