diff --git a/src/components/video-editor/VideoEditor.tsx b/src/components/video-editor/VideoEditor.tsx
index d352b6d0..0c4e5f2d 100644
--- a/src/components/video-editor/VideoEditor.tsx
+++ b/src/components/video-editor/VideoEditor.tsx
@@ -3803,6 +3803,7 @@ export default function VideoEditor() {
const isExportSaving = exportProgress?.phase === "saving";
const isExportFinalizing = exportProgress?.phase === "finalizing";
+ const isRenderingAudio = isExportFinalizing && typeof exportProgress?.audioProgress === "number";
const exportFinalizingProgress = isExportFinalizing
? Math.min(
typeof exportProgress?.renderProgress === "number"
@@ -3858,13 +3859,17 @@ export default function VideoEditor() {
const exportPercentLabel = exportProgress
? isExportSaving
? t("editor.exportStatus.saving", "Opening save dialog...")
- : isExportFinalizing
- ? t("editor.exportStatus.finalizingPercent", "Finalizing {{percent}}%", {
- percent: Math.round(exportFinalizingProgress ?? 99),
- })
- : t("editor.exportStatus.completePercent", "{{percent}}% complete", {
- percent: Math.round(exportProgress.percentage),
+ : isRenderingAudio
+ ? t("editor.exportStatus.renderingAudio", "Rendering audio {{percent}}%", {
+ percent: Math.round((exportProgress.audioProgress ?? 0) * 100),
})
+ : isExportFinalizing
+ ? t("editor.exportStatus.finalizingPercent", "Finalizing {{percent}}%", {
+ percent: Math.round(exportFinalizingProgress ?? 99),
+ })
+ : t("editor.exportStatus.completePercent", "{{percent}}% complete", {
+ percent: Math.round(exportProgress.percentage),
+ })
: t("editor.exportStatus.preparing", "Preparing export...");
const projectBrowser = (
@@ -4062,13 +4067,15 @@ export default function VideoEditor() {
)}
{exportPercentLabel}
- {exportRenderSpeedLabel ? (
+ {isRenderingAudio ? (
+ Audio requires real-time playback for speed/overlay edits
+ ) : exportRenderSpeedLabel ? (
{exportRenderSpeedLabel}
) : null}
{exportRuntimeLabel ? (
diff --git a/src/lib/exporter/audioEncoder.ts b/src/lib/exporter/audioEncoder.ts
index fa158b46..104df89a 100644
--- a/src/lib/exporter/audioEncoder.ts
+++ b/src/lib/exporter/audioEncoder.ts
@@ -18,6 +18,7 @@ type TrimLikeRegion = TrimRegion | ClipRegion
export class AudioProcessor {
private cancelled = false
+ private onProgress?: (progress: number) => void
private isPassthroughAudioCodec(codec: string | undefined): boolean {
if (!codec) {
@@ -88,6 +89,10 @@ export class AudioProcessor {
* 1) no speed regions -> fast WebCodecs trim-only pipeline
* 2) speed regions present -> pitch-preserving rendered timeline pipeline
*/
+ setOnProgress(callback: (progress: number) => void) {
+ this.onProgress = callback
+ }
+
async process(
demuxer: WebDemuxer | null,
muxer: VideoMuxer,
@@ -567,6 +572,9 @@ export class AudioProcessor {
await this.seekTo(timelineMedia, 0)
await timelineMedia.play()
+ const totalDurationMs = (timelineMedia.duration || 0) * 1000
+ let lastProgressReport = 0
+
await new Promise((resolve, reject) => {
const cleanup = () => {
if (rafId !== null) {
@@ -594,6 +602,16 @@ export class AudioProcessor {
return
}
+ // Report audio rendering progress
+ if (this.onProgress && totalDurationMs > 0) {
+ const now = performance.now()
+ if (now - lastProgressReport > 250) {
+ lastProgressReport = now
+ const progress = Math.min((timelineMedia.currentTime * 1000) / totalDurationMs, 1)
+ this.onProgress(progress)
+ }
+ }
+
let currentTimeMs = timelineMedia.currentTime * 1000
const activeTrimRegion = this.findActiveTrimRegion(currentTimeMs, trimRegions)
diff --git a/src/lib/exporter/types.ts b/src/lib/exporter/types.ts
index 9063c881..73a32395 100644
--- a/src/lib/exporter/types.ts
+++ b/src/lib/exporter/types.ts
@@ -29,6 +29,7 @@ export interface ExportProgress {
encoderName?: string;
phase?: 'extracting' | 'finalizing' | 'saving'; // Phase of export
renderProgress?: number; // 0-100, progress of GIF rendering phase
+ audioProgress?: number; // 0-1, progress of real-time audio rendering (speed/audio regions)
}
export interface ExportMetrics {
diff --git a/src/lib/exporter/videoExporter.ts b/src/lib/exporter/videoExporter.ts
index 9c19306a..1943c67c 100644
--- a/src/lib/exporter/videoExporter.ts
+++ b/src/lib/exporter/videoExporter.ts
@@ -246,8 +246,8 @@ export class VideoExporter {
this.reportFinalizingProgress(totalFrames, 96);
if (useNativeEncoder && nativeAudioPlan) {
- this.reportFinalizingProgress(totalFrames, 99);
- return await this.finishNativeVideoExport(nativeAudioPlan);
+ this.reportFinalizingProgress(totalFrames, 99, 0);
+ return await this.finishNativeVideoExport(nativeAudioPlan, totalFrames);
}
// Finalize encoding
@@ -264,7 +264,10 @@ export class VideoExporter {
const demuxer = this.streamingDecoder.getDemuxer();
if (demuxer || hasAudioRegions || hasSourceAudioFallback) {
this.audioProcessor = new AudioProcessor();
- this.reportFinalizingProgress(totalFrames, 99);
+ this.audioProcessor.setOnProgress((progress) => {
+ this.reportFinalizingProgress(totalFrames, 99, progress);
+ });
+ this.reportFinalizingProgress(totalFrames, 99, 0);
await this.awaitWithFinalizationTimeout(
this.audioProcessor.process(
demuxer,
@@ -502,7 +505,7 @@ export class VideoExporter {
}
}
- private async finishNativeVideoExport(audioPlan: NativeAudioPlan): Promise {
+ private async finishNativeVideoExport(audioPlan: NativeAudioPlan, totalFrames: number): Promise {
if (!this.nativeExportSessionId) {
return { success: false, error: "Native export session is not active" };
}
@@ -512,6 +515,9 @@ export class VideoExporter {
if (audioPlan.audioMode === "edited-track") {
this.audioProcessor = new AudioProcessor();
+ this.audioProcessor.setOnProgress((progress) => {
+ this.reportFinalizingProgress(totalFrames, 99, progress);
+ });
const audioBlob = await this.awaitWithFinalizationTimeout(
this.audioProcessor.renderEditedAudioTrack(
this.config.videoUrl,
@@ -593,8 +599,8 @@ export class VideoExporter {
exportFrame.close();
}
- private reportFinalizingProgress(totalFrames: number, renderProgress: number) {
- this.reportProgress(totalFrames, totalFrames, "finalizing", renderProgress);
+ private reportFinalizingProgress(totalFrames: number, renderProgress: number, audioProgress?: number) {
+ this.reportProgress(totalFrames, totalFrames, "finalizing", renderProgress, audioProgress);
}
private reportProgress(
@@ -602,6 +608,7 @@ export class VideoExporter {
totalFrames: number,
phase: ExportProgress["phase"] = "extracting",
renderProgress?: number,
+ audioProgress?: number,
) {
const nowMs = this.getNowMs();
const elapsedSeconds = Math.max((nowMs - this.exportStartTimeMs) / 1000, 0.001);
@@ -637,6 +644,7 @@ export class VideoExporter {
renderFps,
phase,
renderProgress: safeRenderProgress,
+ audioProgress: typeof audioProgress === "number" ? Math.max(0, Math.min(audioProgress, 1)) : undefined,
});
}
}