Merge pull request #262 from webadderall/fix/export-pipeline-streaming-chunked

fix: streaming decode + chunked offline audio rendering for export
This commit is contained in:
webadderall
2026-04-18 22:12:10 +10:00
committed by GitHub
11 changed files with 964 additions and 521 deletions
+1 -1
View File
@@ -2,4 +2,4 @@ reviews:
auto_review:
enabled: true
base_branches:
- "*"
- ".*"
+4 -1
View File
@@ -158,7 +158,10 @@ export async function muxNativeMacRecordingWithAudio(
}
if (availableAudioInputs.length === 0) {
console.warn("[mux] No valid audio files to mux");
console.warn(
"[mux] No valid audio files to mux — video will have no audio. " +
`system=${systemAudioPath ?? "none"} mic=${microphonePath ?? "none"}`,
);
return;
}
+4 -5
View File
@@ -4177,8 +4177,8 @@ export default function VideoEditor() {
? Math.min(
typeof exportProgress?.renderProgress === "number"
? exportProgress.renderProgress
: (exportProgress?.percentage ?? 99),
99,
: (exportProgress?.percentage ?? 100),
100,
)
: null;
const isLightningExportInProgress =
@@ -4234,7 +4234,7 @@ export default function VideoEditor() {
})
: isExportFinalizing
? t("editor.exportStatus.finalizingPercent", "Finalizing {{percent}}%", {
percent: Math.round(exportFinalizingProgress ?? 99),
percent: Math.round(exportFinalizingProgress ?? 100),
})
: t("editor.exportStatus.completePercent", "{{percent}}% complete", {
percent: Math.round(exportProgress.percentage),
@@ -4454,8 +4454,7 @@ export default function VideoEditor() {
</p>
{isRenderingAudio ? (
<p className="mt-1 text-[11px] text-muted-foreground/70">
Audio requires real-time playback for speed/overlay
edits
{t("editor.export.processingAudioEdits", "Processing audio with speed/overlay edits")}
</p>
) : exportRenderSpeedLabel ? (
<p className="mt-1 text-[11px] text-muted-foreground/70">
+3
View File
@@ -122,6 +122,9 @@
"complete": "Export complete",
"savedSuccessfully": "Your file was saved successfully."
},
"export": {
"processingAudioEdits": "Processing audio with speed/overlay edits"
},
"toolbar": {
"addLayer": "Add Layer",
"splitClip": "Split Clip (C)"
+3
View File
@@ -122,6 +122,9 @@
"complete": "Exportación completada",
"savedSuccessfully": "Tu archivo se guardó correctamente."
},
"export": {
"processingAudioEdits": "Procesando audio con ediciones de velocidad/superposición"
},
"toolbar": {
"addLayer": "Agregar capa",
"splitClip": "Dividir clip (C)"
+3
View File
@@ -123,6 +123,9 @@
"complete": "내보내기 완료",
"savedSuccessfully": "파일이 성공적으로 저장되었습니다."
},
"export": {
"processingAudioEdits": "속도/오버레이 편집으로 오디오 처리 중"
},
"toolbar": {
"addLayer": "레이어 추가",
"splitClip": "클립 분할 (C)"
+3
View File
@@ -123,6 +123,9 @@
"complete": "Export voltooid",
"savedSuccessfully": "Je bestand is succesvol opgeslagen."
},
"export": {
"processingAudioEdits": "Audio verwerken met snelheids-/overlay-bewerkingen"
},
"toolbar": {
"addLayer": "Laag toevoegen",
"splitClip": "Clip splitsen (C)"
+3
View File
@@ -122,6 +122,9 @@
"complete": "导出完成",
"savedSuccessfully": "文件已成功保存。"
},
"export": {
"processingAudioEdits": "正在处理带有速度/叠加编辑的音频"
},
"toolbar": {
"addLayer": "添加图层",
"splitClip": "拆分片段 (C)"
File diff suppressed because it is too large Load Diff
+12 -2
View File
@@ -434,6 +434,11 @@ export class ModernVideoExporter {
"muxing queued video chunks",
);
// Surface muxing errors before proceeding with finalization
if (this.encoderError) {
throw this.encoderError;
}
if (nativeAudioPlan.audioMode !== "none" && !shouldUseFfmpegAudioFallback && !this.cancelled) {
const demuxer = this.streamingDecoder.getDemuxer();
if (
@@ -1152,10 +1157,10 @@ export class ModernVideoExporter {
const estimatedTimeRemaining =
averageRenderFps > 0 ? remainingFrames / averageRenderFps : 0;
const safeRenderProgress =
phase === "finalizing" ? Math.max(0, Math.min(renderProgress ?? 99, 99)) : undefined;
phase === "finalizing" ? Math.max(0, Math.min(renderProgress ?? 100, 100)) : undefined;
const percentage =
phase === "finalizing"
? (safeRenderProgress ?? 99)
? (safeRenderProgress ?? 100)
: totalFrames > 0
? (currentFrame / totalFrames) * 100
: 100;
@@ -1399,6 +1404,11 @@ export class ModernVideoExporter {
}
} catch (error) {
console.error("Muxing error:", error);
const muxingError = error instanceof Error ? error : new Error(String(error));
if (!this.encoderError) {
this.encoderError = muxingError;
}
this.cancelled = true;
}
});
this.encodeQueue--;
+12 -2
View File
@@ -293,6 +293,11 @@ export class VideoExporter {
"muxing queued video chunks",
);
// Surface muxing errors before proceeding with finalization
if (this.encoderError) {
throw this.encoderError;
}
if (hasAudio && !shouldUseFfmpegAudioFallback && !this.cancelled) {
const demuxer = this.streamingDecoder.getDemuxer();
if (demuxer || hasAudioRegions || hasSourceAudioFallback) {
@@ -885,10 +890,10 @@ export class VideoExporter {
const estimatedTimeRemaining =
averageRenderFps > 0 ? remainingFrames / averageRenderFps : 0;
const safeRenderProgress =
phase === "finalizing" ? Math.max(0, Math.min(renderProgress ?? 99, 99)) : undefined;
phase === "finalizing" ? Math.max(0, Math.min(renderProgress ?? 100, 100)) : undefined;
const percentage =
phase === "finalizing"
? (safeRenderProgress ?? 99)
? (safeRenderProgress ?? 100)
: totalFrames > 0
? (currentFrame / totalFrames) * 100
: 100;
@@ -979,6 +984,11 @@ export class VideoExporter {
}
} catch (error) {
console.error("Muxing error:", error);
const muxingError = error instanceof Error ? error : new Error(String(error));
if (!this.encoderError) {
this.encoderError = muxingError;
}
this.cancelled = true;
}
});
this.encodeQueue--;