diff --git a/src/lib/exporter/frameRenderer.ts b/src/lib/exporter/frameRenderer.ts index 20035e4..0f947b4 100644 --- a/src/lib/exporter/frameRenderer.ts +++ b/src/lib/exporter/frameRenderer.ts @@ -53,7 +53,7 @@ export class FrameRenderer { } async initialize(): Promise { - // Create offscreen canvas with sRGB color space for fidelity + // Create canvas for rendering const canvas = document.createElement('canvas'); canvas.width = this.config.width; canvas.height = this.config.height; @@ -249,7 +249,7 @@ export class FrameRenderer { this.currentVideoTime = timestamp / 1000000; - // Create or update video sprite from VideoFrame (optimized to reuse sprite) + // Create or update video sprite from VideoFrame if (!this.videoSprite) { const texture = PIXI.Texture.from(videoFrame as any); this.videoSprite = new PIXI.Sprite(texture); @@ -444,7 +444,7 @@ export class FrameRenderer { console.warn('[FrameRenderer] No background sprite found during compositing!'); } - // Draw video layer with shadows on top of background (using CSS filter for accuracy) + // Draw video layer with shadows on top of background if (this.config.showShadow && this.shadowCanvas && this.shadowCtx) { const shadowCtx = this.shadowCtx; shadowCtx.clearRect(0, 0, w, h); diff --git a/src/lib/exporter/videoExporter.ts b/src/lib/exporter/videoExporter.ts index 769e918..b7c630a 100644 --- a/src/lib/exporter/videoExporter.ts +++ b/src/lib/exporter/videoExporter.ts @@ -69,69 +69,73 @@ export class VideoExporter { throw new Error('Video element not available'); } - // Process frames with optimized seeking (no unnecessary timeouts) + // Process frames with optimized seeking const frameDuration = 1_000_000 / this.config.frameRate; // in microseconds let frameIndex = 0; const timeStep = 1 / this.config.frameRate; + const BATCH_SIZE = 5; // Process frames in batches for better throughput while (frameIndex < totalFrames && !this.cancelled) { - const timestamp = frameIndex * frameDuration; - const videoTime = frameIndex * timeStep; + // Process a batch of frames + const batchEnd = Math.min(frameIndex + BATCH_SIZE, totalFrames); - // Seek if needed or wait for first frame to be ready - const needsSeek = Math.abs(videoElement.currentTime - videoTime) > 0.001; - if (needsSeek || frameIndex === 0) { - if (needsSeek) { - videoElement.currentTime = videoTime; + for (let i = frameIndex; i < batchEnd && !this.cancelled; i++) { + const timestamp = i * frameDuration; + const videoTime = i * timeStep; + + // Seek if needed or wait for first frame to be ready + const needsSeek = Math.abs(videoElement.currentTime - videoTime) > 0.001; + if (needsSeek || i === 0) { + if (needsSeek) { + videoElement.currentTime = videoTime; + } + // Wait for video frame to be ready + await new Promise(resolve => { + videoElement.requestVideoFrameCallback(() => resolve()); + }); } - // Wait for video frame to be ready - await new Promise(resolve => { - videoElement.requestVideoFrameCallback(() => resolve()); + + // Create a VideoFrame from the video element (on GPU!) + const videoFrame = new VideoFrame(videoElement, { + timestamp, }); + + // Render the frame with all effects + await this.renderer!.renderFrame(videoFrame, timestamp); + + videoFrame.close(); + + const canvas = this.renderer!.getCanvas(); + + // Create VideoFrame from canvas on GPU without reading pixels + // @ts-ignore - colorSpace not in TypeScript definitions but works at runtime + const exportFrame = new VideoFrame(canvas, { + timestamp, + duration: frameDuration, + colorSpace: { + primaries: 'bt709', + transfer: 'iec61966-2-1', + matrix: 'rgb', + fullRange: true, + }, + }); + + if (this.encoder && this.encoder.state === 'configured') { + this.encodeQueue++; + this.encoder.encode(exportFrame, { keyFrame: i % 150 === 0 }); + } + exportFrame.close(); } - - // Create a VideoFrame from the video element (on GPU!) - const videoFrame = new VideoFrame(videoElement, { - timestamp, - }); - - // Render the frame with all effects - await this.renderer!.renderFrame(videoFrame, timestamp); - videoFrame.close(); - - // Wait for encoder queue to have space (yield immediately instead of 1ms timeout) + // Wait for encoder queue once per batch while (this.encodeQueue >= this.MAX_ENCODE_QUEUE && !this.cancelled) { await new Promise(resolve => setTimeout(resolve, 0)); } - if (this.cancelled) break; + frameIndex = batchEnd; - const canvas = this.renderer!.getCanvas(); - - // Create VideoFrame from canvas on GPU without reading pixels - // @ts-ignore - colorSpace not in TypeScript definitions but works at runtime - const exportFrame = new VideoFrame(canvas, { - timestamp, - duration: frameDuration, - colorSpace: { - primaries: 'bt709', - transfer: 'iec61966-2-1', - matrix: 'rgb', - fullRange: true, - }, - }); - - if (this.encoder && this.encoder.state === 'configured') { - this.encodeQueue++; - this.encoder.encode(exportFrame, { keyFrame: frameIndex % 150 === 0 }); - } - exportFrame.close(); - - frameIndex++; - - // Batch progress updates to reduce callback overhead (every 5 frames) - if (frameIndex % 5 === 0 && this.config.onProgress) { + // Batch progress updates to reduce callback overhead + if (this.config.onProgress) { this.config.onProgress({ currentFrame: frameIndex, totalFrames, @@ -155,7 +159,7 @@ export class VideoExporter { const chunk = this.encodedChunks[i]; const meta: EncodedVideoChunkMetadata = {}; - // Add decoder config with colorSpace metadata for the first chunk + // Add decoder config for the first chunk if (i === 0 && this.videoDescription) { // Use captured colorSpace from encoder or fallback to default sRGB colorspace const colorSpace = this.videoColorSpace || {