Fix export finalization stalls on Windows
This commit is contained in:
@@ -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">
|
||||
|
||||
@@ -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 */
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user