mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-25 15:25:44 +00:00
feat: show real-time audio rendering progress during export finalization
Instead of freezing at 'Finalizing 99%' while audio renders in real-time, the export UI now shows 'Rendering audio XX%' with a live progress bar and an explanatory note about why real-time playback is required. Progress is reported every 250ms from the RAF tick loop in AudioProcessor and piped through VideoExporter to the UI via the existing onProgress callback.
This commit is contained in:
@@ -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() {
|
||||
<div
|
||||
className="h-full bg-[#2563EB] transition-all duration-300 ease-out"
|
||||
style={{
|
||||
width: `${Math.min(exportFinalizingProgress ?? (exportProgress?.percentage ?? 8), 100)}%`,
|
||||
width: `${Math.min(isRenderingAudio ? (exportProgress.audioProgress ?? 0) * 100 : (exportFinalizingProgress ?? (exportProgress?.percentage ?? 8)), 100)}%`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<p className="mt-2 text-xs text-slate-400">{exportPercentLabel}</p>
|
||||
{exportRenderSpeedLabel ? (
|
||||
{isRenderingAudio ? (
|
||||
<p className="mt-1 text-[11px] text-slate-500">Audio requires real-time playback for speed/overlay edits</p>
|
||||
) : exportRenderSpeedLabel ? (
|
||||
<p className="mt-1 text-[11px] text-slate-500">{exportRenderSpeedLabel}</p>
|
||||
) : null}
|
||||
{exportRuntimeLabel ? (
|
||||
|
||||
@@ -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<void>((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)
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<ExportResult> {
|
||||
private async finishNativeVideoExport(audioPlan: NativeAudioPlan, totalFrames: number): Promise<ExportResult> {
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user