diff --git a/src/components/video-editor/ExportDialog.tsx b/src/components/video-editor/ExportDialog.tsx
index 7c00271..b36020c 100644
--- a/src/components/video-editor/ExportDialog.tsx
+++ b/src/components/video-editor/ExportDialog.tsx
@@ -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"}
- {isCompiling || isFinalizing ? "Compiling..." : formatLabel}
+ {isFinalizing && exportFormat === "mp4"
+ ? "Finalizing..."
+ : isCompiling || isFinalizing
+ ? "Compiling..."
+ : formatLabel}
diff --git a/src/lib/exporter/audioEncoder.ts b/src/lib/exporter/audioEncoder.ts
index 74be05e..815c8d8 100644
--- a/src/lib/exporter/audioEncoder.ts
+++ b/src/lib/exporter/audioEncoder.ts
@@ -8,7 +8,12 @@ const DECODE_BACKPRESSURE_LIMIT = 20;
export class AudioProcessor {
private cancelled = false;
- async process(demuxer: WebDemuxer, muxer: VideoMuxer, trimRegions?: TrimRegion[]): Promise {
+ async process(
+ demuxer: WebDemuxer,
+ muxer: VideoMuxer,
+ trimRegions?: TrimRegion[],
+ readEndSec?: number,
+ ): Promise {
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).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;
+ 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 */
}
}
diff --git a/src/lib/exporter/videoExporter.ts b/src/lib/exporter/videoExporter.ts
index 583b133..060c9b5 100644
--- a/src/lib/exporter/videoExporter.ts
+++ b/src/lib/exporter/videoExporter.ts
@@ -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,
+ );
}
}