From 05da56fdc825267b685bf306072574644694212f Mon Sep 17 00:00:00 2001 From: Azeru Date: Sat, 11 Apr 2026 17:45:23 +0100 Subject: [PATCH] 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[],