- {formatTime(progress.estimatedTimeRemaining)}
-
+
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 {