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, }); } }