fix(export): cancel timed out finalization work

This commit is contained in:
wiiiii123
2026-04-20 11:21:52 +07:00
parent 04e013cbbf
commit 19486c7a44
4 changed files with 45 additions and 14 deletions
@@ -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);
});
});
+22 -11
View File
@@ -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<T>({
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<T>({
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);
+1 -1
View File
@@ -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 {
+2 -2
View File
@@ -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 =