fix(export): only refresh watchdog on progress

This commit is contained in:
wiiiii123
2026-04-20 10:39:10 +07:00
parent 6783eeb55c
commit 04e013cbbf
4 changed files with 107 additions and 2 deletions
@@ -1,9 +1,11 @@
import { describe, expect, it, vi } from "vitest";
import {
advanceFinalizationProgress,
type FinalizationProgressWatchdog,
getExportFinalizationIdleTimeoutMs,
getExportFinalizationTimeoutMs,
INITIAL_FINALIZATION_PROGRESS_STATE,
withFinalizationTimeout,
} from "./finalizationTimeout";
@@ -160,4 +162,31 @@ describe("finalizationTimeout", () => {
vi.useRealTimers();
}
});
it("only marks finalization as progressed when normalized progress increases", () => {
const initial = advanceFinalizationProgress({
renderProgress: 99,
audioProgress: 0.5,
state: INITIAL_FINALIZATION_PROGRESS_STATE,
});
expect(initial.progressed).toBe(true);
expect(initial.lastRenderProgress).toBe(99);
expect(initial.lastAudioProgress).toBe(0.5);
const repeated = advanceFinalizationProgress({
renderProgress: 99,
audioProgress: 0.5,
state: initial,
});
expect(repeated.progressed).toBe(false);
const advanced = advanceFinalizationProgress({
renderProgress: 100,
audioProgress: 0.5,
state: repeated,
});
expect(advanced.progressed).toBe(true);
expect(advanced.lastRenderProgress).toBe(100);
expect(advanced.lastAudioProgress).toBe(0.5);
});
});
+38
View File
@@ -2,6 +2,10 @@ export type FinalizationTimeoutWorkload = "default" | "audio";
export type FinalizationProgressWatchdog = {
refreshProgress: () => void;
};
export type FinalizationProgressState = {
lastRenderProgress: number;
lastAudioProgress: number;
};
const BASE_FINALIZATION_TIMEOUT_MS = 10 * 60_000;
const AUDIO_TIMEOUT_HEADROOM_PER_OUTPUT_SECOND_MS = 500;
@@ -10,6 +14,11 @@ const MIN_PROGRESS_IDLE_TIMEOUT_MS = 90_000;
const MAX_PROGRESS_IDLE_TIMEOUT_MS = 5 * 60_000;
const PROGRESS_IDLE_TIMEOUT_FRACTION = 0.25;
export const INITIAL_FINALIZATION_PROGRESS_STATE: FinalizationProgressState = {
lastRenderProgress: -1,
lastAudioProgress: -1,
};
export function getExportFinalizationTimeoutMs({
effectiveDurationSec,
workload = "default",
@@ -57,6 +66,35 @@ export function getExportFinalizationIdleTimeoutMs({
);
}
export function advanceFinalizationProgress({
renderProgress,
audioProgress,
state,
}: {
renderProgress: number;
audioProgress?: number;
state: FinalizationProgressState;
}): FinalizationProgressState & { progressed: boolean } {
const normalizedRenderProgress = Math.max(0, Math.min(renderProgress, 100));
const normalizedAudioProgress =
typeof audioProgress === "number" && Number.isFinite(audioProgress)
? Math.max(0, Math.min(audioProgress, 1))
: null;
const nextRenderProgress = Math.max(state.lastRenderProgress, normalizedRenderProgress);
const nextAudioProgress =
normalizedAudioProgress === null
? state.lastAudioProgress
: Math.max(state.lastAudioProgress, normalizedAudioProgress);
return {
progressed:
nextRenderProgress > state.lastRenderProgress ||
nextAudioProgress > state.lastAudioProgress,
lastRenderProgress: nextRenderProgress,
lastAudioProgress: nextAudioProgress,
};
}
export async function withFinalizationTimeout<T>({
promise,
stage,
+20 -1
View File
@@ -23,8 +23,10 @@ import {
getWebCodecsKeyFrameInterval,
} from "./exportTuning";
import {
advanceFinalizationProgress,
type FinalizationProgressWatchdog,
type FinalizationTimeoutWorkload,
INITIAL_FINALIZATION_PROGRESS_STATE,
withFinalizationTimeout,
} from "./finalizationTimeout";
import { FrameRenderer as ModernFrameRenderer } from "./modernFrameRenderer";
@@ -155,6 +157,8 @@ export class ModernVideoExporter {
private finalizationTimeMs = 0;
private processedFrameCount = 0;
private activeFinalizationProgressWatchdog: FinalizationProgressWatchdog | null = null;
private lastFinalizationRenderProgress = INITIAL_FINALIZATION_PROGRESS_STATE.lastRenderProgress;
private lastFinalizationAudioProgress = INITIAL_FINALIZATION_PROGRESS_STATE.lastAudioProgress;
private lastProgressSampleTimeMs = 0;
private lastProgressSampleFrame = 0;
@@ -1156,7 +1160,19 @@ export class ModernVideoExporter {
renderProgress: number,
audioProgress?: number,
) {
this.activeFinalizationProgressWatchdog?.refreshProgress();
const nextProgress = advanceFinalizationProgress({
renderProgress,
audioProgress,
state: {
lastRenderProgress: this.lastFinalizationRenderProgress,
lastAudioProgress: this.lastFinalizationAudioProgress,
},
});
if (nextProgress.progressed) {
this.activeFinalizationProgressWatchdog?.refreshProgress();
}
this.lastFinalizationRenderProgress = nextProgress.lastRenderProgress;
this.lastFinalizationAudioProgress = nextProgress.lastAudioProgress;
this.reportProgress(totalFrames, totalFrames, "finalizing", renderProgress, audioProgress);
}
@@ -1600,6 +1616,9 @@ export class ModernVideoExporter {
this.finalizationTimeMs = 0;
this.processedFrameCount = 0;
this.activeFinalizationProgressWatchdog = null;
this.lastFinalizationRenderProgress =
INITIAL_FINALIZATION_PROGRESS_STATE.lastRenderProgress;
this.lastFinalizationAudioProgress = INITIAL_FINALIZATION_PROGRESS_STATE.lastAudioProgress;
this.effectiveDurationSec = 0;
this.lastProgressSampleTimeMs = 0;
this.lastProgressSampleFrame = 0;
+20 -1
View File
@@ -14,8 +14,10 @@ import type {
} from "@/components/video-editor/types";
import { AudioProcessor, isAacAudioEncodingSupported } from "./audioEncoder";
import {
advanceFinalizationProgress,
type FinalizationProgressWatchdog,
type FinalizationTimeoutWorkload,
INITIAL_FINALIZATION_PROGRESS_STATE,
withFinalizationTimeout,
} from "./finalizationTimeout";
import { FrameRenderer } from "./frameRenderer";
@@ -113,6 +115,8 @@ export class VideoExporter {
private maxNativeWriteInFlight = 1;
private nativeEncoderError: Error | null = null;
private activeFinalizationProgressWatchdog: FinalizationProgressWatchdog | null = null;
private lastFinalizationRenderProgress = INITIAL_FINALIZATION_PROGRESS_STATE.lastRenderProgress;
private lastFinalizationAudioProgress = INITIAL_FINALIZATION_PROGRESS_STATE.lastAudioProgress;
constructor(config: VideoExporterConfig) {
this.config = config;
@@ -882,7 +886,19 @@ export class VideoExporter {
renderProgress: number,
audioProgress?: number,
) {
this.activeFinalizationProgressWatchdog?.refreshProgress();
const nextProgress = advanceFinalizationProgress({
renderProgress,
audioProgress,
state: {
lastRenderProgress: this.lastFinalizationRenderProgress,
lastAudioProgress: this.lastFinalizationAudioProgress,
},
});
if (nextProgress.progressed) {
this.activeFinalizationProgressWatchdog?.refreshProgress();
}
this.lastFinalizationRenderProgress = nextProgress.lastRenderProgress;
this.lastFinalizationAudioProgress = nextProgress.lastAudioProgress;
this.reportProgress(totalFrames, totalFrames, "finalizing", renderProgress, audioProgress);
}
@@ -1138,6 +1154,9 @@ export class VideoExporter {
this.muxer = null;
this.audioProcessor = null;
this.activeFinalizationProgressWatchdog = null;
this.lastFinalizationRenderProgress =
INITIAL_FINALIZATION_PROGRESS_STATE.lastRenderProgress;
this.lastFinalizationAudioProgress = INITIAL_FINALIZATION_PROGRESS_STATE.lastAudioProgress;
this.encodeQueue = 0;
this.pendingMuxing = Promise.resolve();
this.nativePendingWrite = Promise.resolve();