From faa0037bf07be2198cc5a830f704de9198fac0fb Mon Sep 17 00:00:00 2001 From: Siddharth Date: Sun, 16 Nov 2025 17:21:10 -0700 Subject: [PATCH] faster exports --- src/components/video-editor/ExportDialog.tsx | 31 +++--------- src/components/video-editor/VideoEditor.tsx | 4 ++ src/lib/exporter/frameRenderer.ts | 6 ++- src/lib/exporter/videoExporter.ts | 53 +++++++++++++------- 4 files changed, 52 insertions(+), 42 deletions(-) diff --git a/src/components/video-editor/ExportDialog.tsx b/src/components/video-editor/ExportDialog.tsx index 1f19767..b75e58b 100644 --- a/src/components/video-editor/ExportDialog.tsx +++ b/src/components/video-editor/ExportDialog.tsx @@ -12,14 +12,6 @@ interface ExportDialogProps { onCancel?: () => void; } -function formatTime(seconds: number): string { - if (!isFinite(seconds) || seconds < 0) return '0:00'; - - const mins = Math.floor(seconds / 60); - const secs = Math.floor(seconds % 60); - return `${mins}:${secs.toString().padStart(2, '0')}`; -} - export function ExportDialog({ isOpen, onClose, @@ -49,7 +41,7 @@ export function ExportDialog({ className="fixed inset-0 bg-black/80 backdrop-blur-sm z-50 animate-in fade-in duration-200" onClick={isExporting ? undefined : onClose} /> -
+
{showSuccess ? ( @@ -107,27 +99,18 @@ export function ExportDialog({
-
-
-
Frame
-
- {progress.currentFrame} / {progress.totalFrames} -
-
-
-
Time Remaining
-
- {formatTime(progress.estimatedTimeRemaining)} -
+
+
Frame
+
+ {progress.currentFrame} / {progress.totalFrames}
{onCancel && ( -
+
diff --git a/src/components/video-editor/VideoEditor.tsx b/src/components/video-editor/VideoEditor.tsx index b0ced25..af817c1 100644 --- a/src/components/video-editor/VideoEditor.tsx +++ b/src/components/video-editor/VideoEditor.tsx @@ -249,6 +249,10 @@ export default function VideoEditor() { if (exporterRef.current) { exporterRef.current.cancel(); toast.info('Export cancelled'); + setShowExportDialog(false); + setIsExporting(false); + setExportProgress(null); + setExportError(null); } }, []); diff --git a/src/lib/exporter/frameRenderer.ts b/src/lib/exporter/frameRenderer.ts index f60cfd1..5440f4e 100644 --- a/src/lib/exporter/frameRenderer.ts +++ b/src/lib/exporter/frameRenderer.ts @@ -55,10 +55,14 @@ export class FrameRenderer { } async initialize(): Promise { - // Create offscreen canvas + // Create offscreen canvas with sRGB color space for fidelity const canvas = document.createElement('canvas'); canvas.width = this.config.width; canvas.height = this.config.height; + if ('colorSpace' in canvas) { + // @ts-ignore + canvas.colorSpace = 'srgb'; + } // Initialize PixiJS app with transparent background (background rendered separately) // Use 2x resolution to match Retina displays and ensure blur quality matches preview diff --git a/src/lib/exporter/videoExporter.ts b/src/lib/exporter/videoExporter.ts index 6b90dee..970ed29 100644 --- a/src/lib/exporter/videoExporter.ts +++ b/src/lib/exporter/videoExporter.ts @@ -29,7 +29,7 @@ export class VideoExporter { private cancelled = false; private encodedChunks: EncodedVideoChunk[] = []; private encodeQueue = 0; - private readonly MAX_ENCODE_QUEUE = 30; + private readonly MAX_ENCODE_QUEUE = 60; // Increased for better throughput private videoDescription: Uint8Array | undefined; constructor(config: VideoExporterConfig) { @@ -74,18 +74,40 @@ export class VideoExporter { throw new Error('Video element not available'); } - // Step 6: Process frames + // Step 6: Process frames with optimized seeking const frameDuration = 1_000_000 / this.config.frameRate; // in microseconds let frameIndex = 0; - const startTime = performance.now(); const timeStep = 1 / this.config.frameRate; + // Optimize: Pre-load first frame + videoElement.currentTime = 0; + await new Promise(resolve => { + const onSeeked = () => { + videoElement.removeEventListener('seeked', onSeeked); + resolve(null); + }; + videoElement.addEventListener('seeked', onSeeked); + }); + while (frameIndex < totalFrames && !this.cancelled) { const timestamp = frameIndex * frameDuration; const videoTime = frameIndex * timeStep; - - // Seek to the frame time - await this.decoder.seekToTime(videoTime); + // Seek to frame (optimized: only seek if not already there) + if (Math.abs(videoElement.currentTime - videoTime) > 0.001) { + videoElement.currentTime = videoTime; + // Wait for seek with timeout to prevent hanging + await Promise.race([ + new Promise(resolve => { + const onSeeked = () => { + videoElement.removeEventListener('seeked', onSeeked); + // Wait for video to render the frame + videoElement.requestVideoFrameCallback(() => resolve(null)); + }; + videoElement.addEventListener('seeked', onSeeked, { once: true }); + }), + new Promise(resolve => setTimeout(resolve, 100)) // 100ms timeout + ]); + } // Create a VideoFrame from the video element (on GPU!) const videoFrame = new VideoFrame(videoElement, { @@ -93,20 +115,20 @@ export class VideoExporter { }); // Render the frame with all effects - await this.renderer.renderFrame(videoFrame, timestamp); + await this.renderer!.renderFrame(videoFrame, timestamp); // Close the video frame as we're done with it videoFrame.close(); - // Wait if encode queue is too large + // Wait if encode queue is too large (backpressure) while (this.encodeQueue >= this.MAX_ENCODE_QUEUE && !this.cancelled) { - await new Promise(resolve => setTimeout(resolve, 10)); + await new Promise(resolve => setTimeout(resolve, 1)); } if (this.cancelled) break; // Create VideoFrame from rendered canvas (on GPU, no pixel read!) - const canvas = this.renderer.getCanvas(); + const canvas = this.renderer!.getCanvas(); const exportFrame = new VideoFrame(canvas, { timestamp, duration: frameDuration, @@ -121,17 +143,13 @@ export class VideoExporter { // Report progress frameIndex++; - const elapsed = (performance.now() - startTime) / 1000; - const framesPerSecond = frameIndex / elapsed; - const remainingFrames = totalFrames - frameIndex; - const estimatedTimeRemaining = remainingFrames / framesPerSecond; if (this.config.onProgress) { this.config.onProgress({ currentFrame: frameIndex, totalFrames, percentage: (frameIndex / totalFrames) * 100, - estimatedTimeRemaining, + estimatedTimeRemaining: 0, }); } } @@ -208,9 +226,10 @@ export class VideoExporter { height: this.config.height, bitrate: this.config.bitrate, framerate: this.config.frameRate, - latencyMode: 'quality', + latencyMode: 'realtime', // Changed from 'quality' for faster encoding bitrateMode: 'variable', - }); + hardwareAcceleration: 'prefer-hardware', // Use GPU encoding + } as VideoEncoderConfig); } cancel(): void {