Fix export finalization stalls on Windows

This commit is contained in:
Etienne Lescot
2026-03-14 11:57:59 +01:00
parent 5e8bb99e96
commit b5cc7777d7
3 changed files with 58 additions and 12 deletions
+9 -1
View File
@@ -66,6 +66,9 @@ export function ExportDialog({
const getStatusMessage = () => {
if (error) return "Please try again";
if (isCompiling || isFinalizing) {
if (exportFormat === "mp4") {
return "Finalizing video export...";
}
if (renderProgress !== undefined && renderProgress > 0) {
return `Compiling GIF... ${renderProgress}%`;
}
@@ -77,6 +80,7 @@ export function ExportDialog({
// Get title based on phase
const getTitle = () => {
if (error) return "Export Failed";
if (isFinalizing && exportFormat === "mp4") return "Finalizing Video";
if (isCompiling || isFinalizing) return "Compiling GIF";
return `Exporting ${formatLabel}`;
};
@@ -233,7 +237,11 @@ export function ExportDialog({
{isCompiling || isFinalizing ? "Status" : "Format"}
</div>
<div className="text-slate-200 font-medium text-sm">
{isCompiling || isFinalizing ? "Compiling..." : formatLabel}
{isFinalizing && exportFormat === "mp4"
? "Finalizing..."
: isCompiling || isFinalizing
? "Compiling..."
: formatLabel}
</div>
</div>
<div className="bg-white/5 rounded-xl p-3 border border-white/5">
+32 -10
View File
@@ -8,7 +8,12 @@ const DECODE_BACKPRESSURE_LIMIT = 20;
export class AudioProcessor {
private cancelled = false;
async process(demuxer: WebDemuxer, muxer: VideoMuxer, trimRegions?: TrimRegion[]): Promise<void> {
async process(
demuxer: WebDemuxer,
muxer: VideoMuxer,
trimRegions?: TrimRegion[],
readEndSec?: number,
): Promise<void> {
let audioConfig: AudioDecoderConfig;
try {
audioConfig = (await demuxer.getDecoderConfig("audio")) as AudioDecoderConfig;
@@ -34,19 +39,36 @@ export class AudioProcessor {
});
decoder.configure(audioConfig);
const reader = (demuxer.read("audio") as ReadableStream<EncodedAudioChunk>).getReader();
const safeReadEndSec =
typeof readEndSec === "number" && Number.isFinite(readEndSec)
? Math.max(0, readEndSec)
: undefined;
const audioStream = (
safeReadEndSec !== undefined
? demuxer.read("audio", 0, safeReadEndSec)
: demuxer.read("audio")
) as ReadableStream<EncodedAudioChunk>;
const reader = audioStream.getReader();
while (!this.cancelled) {
const { done, value: chunk } = await reader.read();
if (done || !chunk) break;
try {
while (!this.cancelled) {
const { done, value: chunk } = await reader.read();
if (done || !chunk) break;
const timestampMs = chunk.timestamp / 1000;
if (this.isInTrimRegion(timestampMs, sortedTrims)) continue;
const timestampMs = chunk.timestamp / 1000;
if (this.isInTrimRegion(timestampMs, sortedTrims)) continue;
decoder.decode(chunk);
decoder.decode(chunk);
while (decoder.decodeQueueSize > DECODE_BACKPRESSURE_LIMIT && !this.cancelled) {
await new Promise((resolve) => setTimeout(resolve, 1));
while (decoder.decodeQueueSize > DECODE_BACKPRESSURE_LIMIT && !this.cancelled) {
await new Promise((resolve) => setTimeout(resolve, 1));
}
}
} finally {
try {
await reader.cancel();
} catch {
/* reader already closed */
}
}
+17 -1
View File
@@ -97,6 +97,7 @@ export class VideoExporter {
this.config.speedRegions,
);
const totalFrames = Math.ceil(effectiveDuration * this.config.frameRate);
const readEndSec = Math.max(videoInfo.duration, videoInfo.streamDuration ?? 0) + 0.5;
console.log("[VideoExporter] Original duration:", videoInfo.duration, "s");
console.log("[VideoExporter] Effective duration:", effectiveDuration, "s");
@@ -183,13 +184,28 @@ export class VideoExporter {
// Wait for all video muxing operations to complete
await Promise.all(this.muxingPromises);
if (this.config.onProgress) {
this.config.onProgress({
currentFrame: totalFrames,
totalFrames,
percentage: 100,
estimatedTimeRemaining: 0,
phase: "finalizing",
});
}
// Process audio track if present
if (hasAudio && !this.cancelled) {
const demuxer = this.streamingDecoder!.getDemuxer();
if (demuxer) {
console.log("[VideoExporter] Processing audio track...");
this.audioProcessor = new AudioProcessor();
await this.audioProcessor.process(demuxer, this.muxer!, this.config.trimRegions);
await this.audioProcessor.process(
demuxer,
this.muxer!,
this.config.trimRegions,
readEndSec,
);
}
}