From 08aff31351e4e2771a1dd04bf30c1401886f021e Mon Sep 17 00:00:00 2001 From: Azeru Date: Sat, 11 Apr 2026 17:27:52 +0100 Subject: [PATCH 1/4] fix(windows): normalize export save path and relax early decode end --- electron/ipc/handlers.ts | 13 +++++++++---- src/lib/exporter/streamingDecoder.ts | 16 +++++++++++++--- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/electron/ipc/handlers.ts b/electron/ipc/handlers.ts index 4cb4875..9a5a6c0 100644 --- a/electron/ipc/handlers.ts +++ b/electron/ipc/handlers.ts @@ -638,7 +638,6 @@ export function registerIpcHandlers( return null; } }); - ipcMain.handle("save-exported-video", async (_, videoData: ArrayBuffer, fileName: string) => { try { // Determine file type from extension @@ -664,11 +663,18 @@ export function registerIpcHandlers( }; } - await fs.writeFile(result.filePath, Buffer.from(videoData)); + // --- FIX: Normalize the path for Windows compatibility --- + const normalizedPath = path.normalize(result.filePath); + + // Ensure the parent directory exists (Windows may fail if the folder is missing) + await fs.mkdir(path.dirname(normalizedPath), { recursive: true }); + // --- END FIX --- + + await fs.writeFile(normalizedPath, Buffer.from(videoData)); return { success: true, - path: result.filePath, + path: normalizedPath, message: "Video exported successfully", }; } catch (error) { @@ -680,7 +686,6 @@ export function registerIpcHandlers( }; } }); - ipcMain.handle("open-video-file-picker", async () => { try { const result = await dialog.showOpenDialog({ diff --git a/src/lib/exporter/streamingDecoder.ts b/src/lib/exporter/streamingDecoder.ts index ee67576..bed103b 100644 --- a/src/lib/exporter/streamingDecoder.ts +++ b/src/lib/exporter/streamingDecoder.ts @@ -492,6 +492,8 @@ export class StreamingVideoDecoder { this.decoder = null; const requiredEndSec = segments.length > 0 ? segments[segments.length - 1].endSec : 0; + const isWindows = typeof navigator !== "undefined" && /Windows/.test(navigator.userAgent); + if ( shouldFailDecodeEndedEarly({ cancelled: this.cancelled, @@ -502,9 +504,17 @@ export class StreamingVideoDecoder { ) { const decodedAtLabel = lastDecodedFrameSec === null ? "no decoded frame" : `${lastDecodedFrameSec.toFixed(3)}s`; - throw new Error( - `Video decode ended early at ${decodedAtLabel} (needed ${requiredEndSec.toFixed(3)}s).`, - ); + + if (isWindows) { + console.warn( + `[StreamingVideoDecoder] Decode ended early on Windows at ${decodedAtLabel} (needed ${requiredEndSec.toFixed(3)}s) – proceeding anyway.`, + ); + // Do not throw on Windows; allow export to complete with the frames we have. + } else { + throw new Error( + `Video decode ended early at ${decodedAtLabel} (needed ${requiredEndSec.toFixed(3)}s).`, + ); + } } } From 05da56fdc825267b685bf306072574644694212f Mon Sep 17 00:00:00 2001 From: Azeru Date: Sat, 11 Apr 2026 17:45:23 +0100 Subject: [PATCH 2/4] fix(export): relax early decode termination on Windows On Windows, tolerate small decode gaps (<=3 seconds) to work around driver quirks, allowing export to complete with available frames. --- electron/ipc/handlers.ts | 12 ++++++ src/lib/exporter/streamingDecoder.ts | 56 +++++++++++++++------------- 2 files changed, 42 insertions(+), 26 deletions(-) diff --git a/electron/ipc/handlers.ts b/electron/ipc/handlers.ts index 9a5a6c0..6eb2d48 100644 --- a/electron/ipc/handlers.ts +++ b/electron/ipc/handlers.ts @@ -638,6 +638,18 @@ export function registerIpcHandlers( return null; } }); + +/** + * Handles saving an exported video file. + * Shows a save dialog, normalizes the file path for the current OS, + * ensures the directory exists, and writes the video data. + * @param _ - Unused event parameter. + * @param videoData - The exported video as an ArrayBuffer. + * @param fileName - Suggested filename for the save dialog. + * @returns Object with success status, optional file path, and error details. + */ + + ipcMain.handle("save-exported-video", async (_, videoData: ArrayBuffer, fileName: string) => { try { // Determine file type from extension diff --git a/src/lib/exporter/streamingDecoder.ts b/src/lib/exporter/streamingDecoder.ts index bed103b..cb5bf7b 100644 --- a/src/lib/exporter/streamingDecoder.ts +++ b/src/lib/exporter/streamingDecoder.ts @@ -217,7 +217,15 @@ export class StreamingVideoDecoder { return this.metadata; } - +/** + * Decodes all video frames from the loaded source and invokes a callback for each. + * Handles trimming and speed adjustments, and resamples to the target frame rate. + * On Windows, early decode termination is tolerated to work around driver quirks. + * @param targetFrameRate - Desired output frame rate. + * @param trimRegions - Array of time regions to keep (others discarded). + * @param speedRegions - Array of speed adjustments for specific time ranges. + * @param onFrame - Async callback receiving each decoded VideoFrame. + */ async decodeAll( targetFrameRate: number, trimRegions: TrimRegion[] | undefined, @@ -491,33 +499,29 @@ export class StreamingVideoDecoder { } this.decoder = null; - const requiredEndSec = segments.length > 0 ? segments[segments.length - 1].endSec : 0; - const isWindows = typeof navigator !== "undefined" && /Windows/.test(navigator.userAgent); + const isWindows = typeof navigator !== "undefined" && /Windows/.test(navigator.userAgent); - if ( - shouldFailDecodeEndedEarly({ - cancelled: this.cancelled, - lastDecodedFrameSec, - requiredEndSec, - streamDurationSec: this.metadata.streamDuration, - }) - ) { - const decodedAtLabel = - lastDecodedFrameSec === null ? "no decoded frame" : `${lastDecodedFrameSec.toFixed(3)}s`; - - if (isWindows) { - console.warn( - `[StreamingVideoDecoder] Decode ended early on Windows at ${decodedAtLabel} (needed ${requiredEndSec.toFixed(3)}s) – proceeding anyway.`, - ); - // Do not throw on Windows; allow export to complete with the frames we have. - } else { - throw new Error( - `Video decode ended early at ${decodedAtLabel} (needed ${requiredEndSec.toFixed(3)}s).`, - ); - } - } - } +if (shouldFailDecodeEndedEarly({ + cancelled: this.cancelled, + lastDecodedFrameSec, + requiredEndSec, + streamDurationSec: this.metadata.streamDuration, +})) { + const decodedAtLabel = lastDecodedFrameSec === null ? "no decoded frame" : `${lastDecodedFrameSec.toFixed(3)}s`; + const decodeGapSec = lastDecodedFrameSec === null ? Infinity : requiredEndSec - lastDecodedFrameSec; + // On Windows, tolerate a small decode gap (<= 3 seconds) to work around driver quirks. + // For severe failures (no frames at all, or a large gap), still throw. + if (isWindows && lastDecodedFrameSec !== null && decodeGapSec <= 3.0) { + console.warn( + `[StreamingVideoDecoder] Decode ended early on Windows with a small gap (${decodeGapSec.toFixed(2)}s) – proceeding anyway.`, + ); + } else { + throw new Error( + `Video decode ended early at ${decodedAtLabel} (needed ${requiredEndSec.toFixed(3)}s).`, + ); + } +} private computeSegments( totalDuration: number, trimRegions?: TrimRegion[], From d40f40d69d100102a7adbea313b417a365c8a4d8 Mon Sep 17 00:00:00 2001 From: Azeru Date: Sat, 11 Apr 2026 17:55:05 +0100 Subject: [PATCH 3/4] fix(export): compute requiredEndSec for decode termination handling Add requiredEndSec calculation to properly handle early decode termination by using the last segment's end time. This addresses issues with export processing on Windows platforms. --- src/lib/exporter/streamingDecoder.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/lib/exporter/streamingDecoder.ts b/src/lib/exporter/streamingDecoder.ts index cb5bf7b..f6db016 100644 --- a/src/lib/exporter/streamingDecoder.ts +++ b/src/lib/exporter/streamingDecoder.ts @@ -253,6 +253,8 @@ export class StreamingVideoDecoder { this.computeSegments(this.metadata.duration, trimRegions), speedRegions, ); + const requiredEndSec = segments[segments.length - 1]?.endSec ?? 0; + const segmentOutputFrameCounts = segments.map((segment) => Math.ceil(((segment.endSec - segment.startSec) / segment.speed) * targetFrameRate), ); From e4d4ce284bf47fb3d9ebd2186a7a571b53c3c23a Mon Sep 17 00:00:00 2001 From: Azeru Date: Sat, 11 Apr 2026 17:55:05 +0100 Subject: [PATCH 4/4] fix(export): compute requiredEndSec for decode termination handling Add requiredEndSec calculation to properly handle early decode termination by using the last segment's end time. This addresses issues with export processing on Windows platforms. --- src/lib/exporter/streamingDecoder.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/lib/exporter/streamingDecoder.ts b/src/lib/exporter/streamingDecoder.ts index cb5bf7b..6001cac 100644 --- a/src/lib/exporter/streamingDecoder.ts +++ b/src/lib/exporter/streamingDecoder.ts @@ -253,6 +253,8 @@ export class StreamingVideoDecoder { this.computeSegments(this.metadata.duration, trimRegions), speedRegions, ); + const requiredEndSec = segments[segments.length - 1]?.endSec ?? 0; + const segmentOutputFrameCounts = segments.map((segment) => Math.ceil(((segment.endSec - segment.startSec) / segment.speed) * targetFrameRate), ); @@ -510,11 +512,13 @@ if (shouldFailDecodeEndedEarly({ const decodedAtLabel = lastDecodedFrameSec === null ? "no decoded frame" : `${lastDecodedFrameSec.toFixed(3)}s`; const decodeGapSec = lastDecodedFrameSec === null ? Infinity : requiredEndSec - lastDecodedFrameSec; - // On Windows, tolerate a small decode gap (<= 3 seconds) to work around driver quirks. - // For severe failures (no frames at all, or a large gap), still throw. - if (isWindows && lastDecodedFrameSec !== null && decodeGapSec <= 3.0) { + // On Windows, tolerate a small decode gap: up to 10% of required duration, capped at 3 seconds. + const maxToleratedGap = Math.min(3.0, requiredEndSec * 0.1); + + if (isWindows && lastDecodedFrameSec !== null && decodeGapSec <= maxToleratedGap) { console.warn( - `[StreamingVideoDecoder] Decode ended early on Windows with a small gap (${decodeGapSec.toFixed(2)}s) – proceeding anyway.`, + `[StreamingVideoDecoder] Decode ended early on Windows with a gap of ${decodeGapSec.toFixed(2)}s ` + + `(max tolerated: ${maxToleratedGap.toFixed(2)}s) – proceeding anyway.`, ); } else { throw new Error( @@ -522,6 +526,8 @@ if (shouldFailDecodeEndedEarly({ ); } } + } + private computeSegments( totalDuration: number, trimRegions?: TrimRegion[],