Skip to content

Commit c7810b6

Browse files
committed
Fix export finalization stalls on Windows
1 parent fa339ae commit c7810b6

File tree

3 files changed

+58
-12
lines changed

3 files changed

+58
-12
lines changed

src/components/video-editor/ExportDialog.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ export function ExportDialog({
6666
const getStatusMessage = () => {
6767
if (error) return "Please try again";
6868
if (isCompiling || isFinalizing) {
69+
if (exportFormat === "mp4") {
70+
return "Finalizing video export...";
71+
}
6972
if (renderProgress !== undefined && renderProgress > 0) {
7073
return `Compiling GIF... ${renderProgress}%`;
7174
}
@@ -77,6 +80,7 @@ export function ExportDialog({
7780
// Get title based on phase
7881
const getTitle = () => {
7982
if (error) return "Export Failed";
83+
if (isFinalizing && exportFormat === "mp4") return "Finalizing Video";
8084
if (isCompiling || isFinalizing) return "Compiling GIF";
8185
return `Exporting ${formatLabel}`;
8286
};
@@ -233,7 +237,11 @@ export function ExportDialog({
233237
{isCompiling || isFinalizing ? "Status" : "Format"}
234238
</div>
235239
<div className="text-slate-200 font-medium text-sm">
236-
{isCompiling || isFinalizing ? "Compiling..." : formatLabel}
240+
{isFinalizing && exportFormat === "mp4"
241+
? "Finalizing..."
242+
: isCompiling || isFinalizing
243+
? "Compiling..."
244+
: formatLabel}
237245
</div>
238246
</div>
239247
<div className="bg-white/5 rounded-xl p-3 border border-white/5">

src/lib/exporter/audioEncoder.ts

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@ const DECODE_BACKPRESSURE_LIMIT = 20;
88
export class AudioProcessor {
99
private cancelled = false;
1010

11-
async process(demuxer: WebDemuxer, muxer: VideoMuxer, trimRegions?: TrimRegion[]): Promise<void> {
11+
async process(
12+
demuxer: WebDemuxer,
13+
muxer: VideoMuxer,
14+
trimRegions?: TrimRegion[],
15+
readEndSec?: number,
16+
): Promise<void> {
1217
let audioConfig: AudioDecoderConfig;
1318
try {
1419
audioConfig = (await demuxer.getDecoderConfig("audio")) as AudioDecoderConfig;
@@ -34,19 +39,36 @@ export class AudioProcessor {
3439
});
3540
decoder.configure(audioConfig);
3641

37-
const reader = (demuxer.read("audio") as ReadableStream<EncodedAudioChunk>).getReader();
42+
const safeReadEndSec =
43+
typeof readEndSec === "number" && Number.isFinite(readEndSec)
44+
? Math.max(0, readEndSec)
45+
: undefined;
46+
const audioStream = (
47+
safeReadEndSec !== undefined
48+
? demuxer.read("audio", 0, safeReadEndSec)
49+
: demuxer.read("audio")
50+
) as ReadableStream<EncodedAudioChunk>;
51+
const reader = audioStream.getReader();
3852

39-
while (!this.cancelled) {
40-
const { done, value: chunk } = await reader.read();
41-
if (done || !chunk) break;
53+
try {
54+
while (!this.cancelled) {
55+
const { done, value: chunk } = await reader.read();
56+
if (done || !chunk) break;
4257

43-
const timestampMs = chunk.timestamp / 1000;
44-
if (this.isInTrimRegion(timestampMs, sortedTrims)) continue;
58+
const timestampMs = chunk.timestamp / 1000;
59+
if (this.isInTrimRegion(timestampMs, sortedTrims)) continue;
4560

46-
decoder.decode(chunk);
61+
decoder.decode(chunk);
4762

48-
while (decoder.decodeQueueSize > DECODE_BACKPRESSURE_LIMIT && !this.cancelled) {
49-
await new Promise((resolve) => setTimeout(resolve, 1));
63+
while (decoder.decodeQueueSize > DECODE_BACKPRESSURE_LIMIT && !this.cancelled) {
64+
await new Promise((resolve) => setTimeout(resolve, 1));
65+
}
66+
}
67+
} finally {
68+
try {
69+
await reader.cancel();
70+
} catch {
71+
/* reader already closed */
5072
}
5173
}
5274

src/lib/exporter/videoExporter.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ export class VideoExporter {
9797
this.config.speedRegions,
9898
);
9999
const totalFrames = Math.ceil(effectiveDuration * this.config.frameRate);
100+
const readEndSec = Math.max(videoInfo.duration, videoInfo.streamDuration ?? 0) + 0.5;
100101

101102
console.log("[VideoExporter] Original duration:", videoInfo.duration, "s");
102103
console.log("[VideoExporter] Effective duration:", effectiveDuration, "s");
@@ -183,13 +184,28 @@ export class VideoExporter {
183184
// Wait for all video muxing operations to complete
184185
await Promise.all(this.muxingPromises);
185186

187+
if (this.config.onProgress) {
188+
this.config.onProgress({
189+
currentFrame: totalFrames,
190+
totalFrames,
191+
percentage: 100,
192+
estimatedTimeRemaining: 0,
193+
phase: "finalizing",
194+
});
195+
}
196+
186197
// Process audio track if present
187198
if (hasAudio && !this.cancelled) {
188199
const demuxer = this.streamingDecoder!.getDemuxer();
189200
if (demuxer) {
190201
console.log("[VideoExporter] Processing audio track...");
191202
this.audioProcessor = new AudioProcessor();
192-
await this.audioProcessor.process(demuxer, this.muxer!, this.config.trimRegions);
203+
await this.audioProcessor.process(
204+
demuxer,
205+
this.muxer!,
206+
this.config.trimRegions,
207+
readEndSec,
208+
);
193209
}
194210
}
195211

0 commit comments

Comments
 (0)