fix(windows): resolve cursor telemetry paths and prevent export finalization stalls

Credit: Adapted from OpenScreen upstream PR contributions.
This commit is contained in:
webadderall
2026-03-14 23:29:32 +11:00
parent 72552215ee
commit 83b7ec48b8
3 changed files with 68 additions and 7 deletions
+19 -2
View File
@@ -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)
@@ -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<ProjectEditorState>): Pro
return {
wallpaper: typeof editor.wallpaper === "string" ? editor.wallpaper : WALLPAPER_PATHS[0],
backgroundTransparency: typeof (editor as Partial<ProjectEditorState>).backgroundTransparency === "boolean"
? (editor as Partial<ProjectEditorState>).backgroundTransparency as boolean
: false,
shadowIntensity: typeof editor.shadowIntensity === "number" ? editor.shadowIntensity : 0.67,
backgroundBlur: normalizedBackgroundBlur,
zoomMotionBlur: normalizedZoomMotionBlur,
+40 -4
View File
@@ -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<void> = 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<T>(promise: Promise<T>, stage: string): Promise<T> {
if (!this.isWindowsPlatform()) {
return promise;
}
let timeoutId: ReturnType<typeof setTimeout> | null = null;
try {
return await Promise.race([
promise,
new Promise<T>((_, 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();