diff --git a/src/lib/exporter/finalizationTimeout.test.ts b/src/lib/exporter/finalizationTimeout.test.ts index 91728291..7193470a 100644 --- a/src/lib/exporter/finalizationTimeout.test.ts +++ b/src/lib/exporter/finalizationTimeout.test.ts @@ -189,4 +189,24 @@ describe("finalizationTimeout", () => { expect(advanced.lastRenderProgress).toBe(100); expect(advanced.lastAudioProgress).toBe(0.5); }); + + it("ignores non-finite render progress without poisoning later updates", () => { + const invalid = advanceFinalizationProgress({ + renderProgress: Number.NaN, + audioProgress: 0.25, + state: INITIAL_FINALIZATION_PROGRESS_STATE, + }); + expect(invalid.progressed).toBe(true); + expect(invalid.lastRenderProgress).toBe(-1); + expect(invalid.lastAudioProgress).toBe(0.25); + + const recovered = advanceFinalizationProgress({ + renderProgress: 99, + audioProgress: 0.25, + state: invalid, + }); + expect(recovered.progressed).toBe(true); + expect(recovered.lastRenderProgress).toBe(99); + expect(recovered.lastAudioProgress).toBe(0.25); + }); }); diff --git a/src/lib/exporter/finalizationTimeout.ts b/src/lib/exporter/finalizationTimeout.ts index 86b7e9dd..040409fd 100644 --- a/src/lib/exporter/finalizationTimeout.ts +++ b/src/lib/exporter/finalizationTimeout.ts @@ -75,12 +75,18 @@ export function advanceFinalizationProgress({ audioProgress?: number; state: FinalizationProgressState; }): FinalizationProgressState & { progressed: boolean } { - const normalizedRenderProgress = Math.max(0, Math.min(renderProgress, 100)); + const normalizedRenderProgress = + typeof renderProgress === "number" && Number.isFinite(renderProgress) + ? Math.max(0, Math.min(renderProgress, 100)) + : null; const normalizedAudioProgress = typeof audioProgress === "number" && Number.isFinite(audioProgress) ? Math.max(0, Math.min(audioProgress, 1)) : null; - const nextRenderProgress = Math.max(state.lastRenderProgress, normalizedRenderProgress); + const nextRenderProgress = + normalizedRenderProgress === null + ? state.lastRenderProgress + : Math.max(state.lastRenderProgress, normalizedRenderProgress); const nextAudioProgress = normalizedAudioProgress === null ? state.lastAudioProgress @@ -122,12 +128,17 @@ export async function withFinalizationTimeout({ workload, }) : null; - const watchdog: FinalizationProgressWatchdog | null = - progressAware && idleTimeoutMs !== null && idleTimeoutMs !== undefined - ? { - refreshProgress: () => undefined, - } - : null; + const hasIdleWatchdog = + progressAware && + typeof idleTimeoutMs === "number" && + Number.isFinite(idleTimeoutMs) && + idleTimeoutMs >= 0; + const resolvedIdleTimeoutMs = hasIdleWatchdog ? idleTimeoutMs : null; + const watchdog: FinalizationProgressWatchdog | null = hasIdleWatchdog + ? { + refreshProgress: () => undefined, + } + : null; try { return await Promise.race([ @@ -136,16 +147,16 @@ export async function withFinalizationTimeout({ const rejectWithMessage = (message: string) => { reject(new Error(message)); }; - if (watchdog && idleTimeoutMs !== null) { + if (watchdog && resolvedIdleTimeoutMs !== null) { const refreshProgress = () => { if (idleTimeoutId) { clearTimeout(idleTimeoutId); } idleTimeoutId = setTimeout(() => { rejectWithMessage( - `Export timed out during ${stage} after ${Math.ceil(idleTimeoutMs / 1000)} seconds without observable progress`, + `Export timed out during ${stage} after ${Math.ceil(resolvedIdleTimeoutMs / 1000)} seconds without observable progress`, ); - }, idleTimeoutMs); + }, resolvedIdleTimeoutMs); }; watchdog.refreshProgress = refreshProgress; onWatchdogChanged?.(watchdog); diff --git a/src/lib/exporter/modernVideoExporter.ts b/src/lib/exporter/modernVideoExporter.ts index 35db408d..622a4914 100644 --- a/src/lib/exporter/modernVideoExporter.ts +++ b/src/lib/exporter/modernVideoExporter.ts @@ -985,7 +985,6 @@ export class ModernVideoExporter { } const sessionId = this.nativeExportSessionId; - this.nativeExportSessionId = null; console.log(`[VideoExporter] Finalizing ${NATIVE_EXPORT_ENGINE_NAME} export`, { sessionId, audioMode: audioPlan.audioMode, @@ -1009,6 +1008,7 @@ export class ModernVideoExporter { `${NATIVE_EXPORT_ENGINE_NAME} export finalization`, audioPlan.audioMode === "none" ? "default" : "audio", ); + this.nativeExportSessionId = null; if (!result.success) { return { diff --git a/src/lib/exporter/videoExporter.ts b/src/lib/exporter/videoExporter.ts index ebcad040..c5c53c80 100644 --- a/src/lib/exporter/videoExporter.ts +++ b/src/lib/exporter/videoExporter.ts @@ -710,8 +710,6 @@ export class VideoExporter { } const sessionId = this.nativeExportSessionId; - this.nativeExportSessionId = null; - const result = await this.awaitWithFinalizationTimeout( window.electronAPI.nativeVideoExportFinish(sessionId, { audioMode: audioPlan.audioMode, @@ -727,6 +725,7 @@ export class VideoExporter { "native export finalization", audioPlan.audioMode === "none" ? "default" : "audio", ); + this.nativeExportSessionId = null; if (!result.success || !result.data) { return { @@ -1152,6 +1151,7 @@ export class VideoExporter { } this.muxer = null; + this.audioProcessor?.cancel(); this.audioProcessor = null; this.activeFinalizationProgressWatchdog = null; this.lastFinalizationRenderProgress =