@editframe/elements 0.45.2 → 0.45.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/DelayedLoadingState.js.map +1 -1
- package/dist/EF_FRAMEGEN.js.map +1 -1
- package/dist/EF_RENDERING.js.map +1 -1
- package/dist/canvas/EFCanvas.js +3 -3
- package/dist/canvas/EFCanvas.js.map +1 -1
- package/dist/canvas/EFCanvasItem.js.map +1 -1
- package/dist/canvas/api/CanvasAPI.js.map +1 -1
- package/dist/canvas/getElementBounds.js.map +1 -1
- package/dist/canvas/overlays/SelectionOverlay.js.map +1 -1
- package/dist/canvas/overlays/overlayState.js.map +1 -1
- package/dist/canvas/selection/SelectionController.js +25 -23
- package/dist/canvas/selection/SelectionController.js.map +1 -1
- package/dist/canvas/selection/SelectionModel.js.map +1 -1
- package/dist/canvas/selection/selectionContext.js.map +1 -1
- package/dist/elements/ContainerInfo.js.map +1 -1
- package/dist/elements/CrossUpdateController.js.map +1 -1
- package/dist/elements/EFAudio.js.map +1 -1
- package/dist/elements/EFCaptions.js.map +1 -1
- package/dist/elements/EFImage.js +1 -1
- package/dist/elements/EFImage.js.map +1 -1
- package/dist/elements/EFMedia/BufferedSeekingInput.js.map +1 -1
- package/dist/elements/EFMedia/CachedFetcher.js.map +1 -1
- package/dist/elements/EFMedia/MediaEngine.js.map +1 -1
- package/dist/elements/EFMedia/SegmentIndex.js.map +1 -1
- package/dist/elements/EFMedia/SegmentTransport.js.map +1 -1
- package/dist/elements/EFMedia/TimingModel.js.map +1 -1
- package/dist/elements/EFMedia/shared/AudioSpanUtils.js.map +1 -1
- package/dist/elements/EFMedia/shared/GlobalInputCache.js.map +1 -1
- package/dist/elements/EFMedia/shared/PrecisionUtils.js.map +1 -1
- package/dist/elements/EFMedia/shared/ThumbnailExtractor.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/MainVideoInputCache.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/ScrubInputCache.js.map +1 -1
- package/dist/elements/EFMedia.js.map +1 -1
- package/dist/elements/EFPanZoom.js +9 -8
- package/dist/elements/EFPanZoom.js.map +1 -1
- package/dist/elements/EFSourceMixin.js +1 -1
- package/dist/elements/EFSourceMixin.js.map +1 -1
- package/dist/elements/EFSurface.js.map +1 -1
- package/dist/elements/EFTemporal.js.map +1 -1
- package/dist/elements/EFText.d.ts +4 -4
- package/dist/elements/EFText.js.map +1 -1
- package/dist/elements/EFTextSegment.d.ts +4 -4
- package/dist/elements/EFTimegroup.js +7 -8
- package/dist/elements/EFTimegroup.js.map +1 -1
- package/dist/elements/EFVideo.d.ts +4 -4
- package/dist/elements/EFVideo.js.map +1 -1
- package/dist/elements/EFWaveform.d.ts +4 -4
- package/dist/elements/EFWaveform.js.map +1 -1
- package/dist/elements/ElementPositionInfo.js.map +1 -1
- package/dist/elements/FetchMixin.js.map +1 -1
- package/dist/elements/SampleBuffer.js.map +1 -1
- package/dist/elements/TargetController.js.map +1 -1
- package/dist/elements/TimegroupController.js.map +1 -1
- package/dist/elements/cloneFactoryRegistry.js.map +1 -1
- package/dist/elements/durationConverter.js.map +1 -1
- package/dist/elements/easingUtils.js.map +1 -1
- package/dist/elements/renderTemporalAudio.js.map +1 -1
- package/dist/elements/setupTemporalHierarchy.js.map +1 -1
- package/dist/elements/updateAnimations.js +1 -1
- package/dist/elements/updateAnimations.js.map +1 -1
- package/dist/getRenderInfo.js.map +1 -1
- package/dist/gui/ContextMixin.js.map +1 -1
- package/dist/gui/Controllable.js.map +1 -1
- package/dist/gui/EFActiveRootTemporal.d.ts +4 -4
- package/dist/gui/EFActiveRootTemporal.js.map +1 -1
- package/dist/gui/EFConfiguration.d.ts +4 -4
- package/dist/gui/EFControls.js.map +1 -1
- package/dist/gui/EFDial.d.ts +4 -4
- package/dist/gui/EFFilmstrip.d.ts +4 -4
- package/dist/gui/EFFilmstrip.js.map +1 -1
- package/dist/gui/EFFitScale.js.map +1 -1
- package/dist/gui/EFOverlayItem.d.ts +4 -4
- package/dist/gui/EFOverlayItem.js.map +1 -1
- package/dist/gui/EFOverlayLayer.d.ts +4 -4
- package/dist/gui/EFOverlayLayer.js.map +1 -1
- package/dist/gui/EFPause.d.ts +4 -4
- package/dist/gui/EFPlay.d.ts +4 -4
- package/dist/gui/EFPreview.d.ts +4 -4
- package/dist/gui/EFPreview.js.map +1 -1
- package/dist/gui/EFResizableBox.js.map +1 -1
- package/dist/gui/EFScrubber.d.ts +4 -4
- package/dist/gui/EFScrubber.js.map +1 -1
- package/dist/gui/EFTimeDisplay.d.ts +4 -4
- package/dist/gui/EFTimeDisplay.js.map +1 -1
- package/dist/gui/EFTimelineRuler.d.ts +4 -4
- package/dist/gui/EFTimelineRuler.js.map +1 -1
- package/dist/gui/EFToggleLoop.d.ts +4 -4
- package/dist/gui/EFTogglePlay.d.ts +4 -4
- package/dist/gui/EFTogglePlay.js.map +1 -1
- package/dist/gui/EFTransformHandles.js.map +1 -1
- package/dist/gui/EFWorkbench.d.ts +4 -4
- package/dist/gui/EFWorkbench.js.map +1 -1
- package/dist/gui/FitScaleHelpers.js.map +1 -1
- package/dist/gui/PlaybackController.js.map +1 -1
- package/dist/gui/TWMixin2.js.map +1 -1
- package/dist/gui/TargetOrContextMixin.js.map +1 -1
- package/dist/gui/currentTimeContext.js.map +1 -1
- package/dist/gui/efContext.js.map +1 -1
- package/dist/gui/fetchContext.js.map +1 -1
- package/dist/gui/hierarchy/EFHierarchy.d.ts +4 -4
- package/dist/gui/hierarchy/EFHierarchy.js.map +1 -1
- package/dist/gui/hierarchy/EFHierarchyItem.d.ts +2 -2
- package/dist/gui/hierarchy/EFHierarchyItem.js.map +1 -1
- package/dist/gui/hierarchy/hierarchyContext.js.map +1 -1
- package/dist/gui/panZoomTransformContext.js.map +1 -1
- package/dist/gui/previewSettingsContext.js.map +1 -1
- package/dist/gui/theme.js.map +1 -1
- package/dist/gui/timeline/EFTimeline.d.ts +2 -2
- package/dist/gui/timeline/EFTimeline.js +0 -1
- package/dist/gui/timeline/EFTimeline.js.map +1 -1
- package/dist/gui/timeline/EFTimelineRow.js.map +1 -1
- package/dist/gui/timeline/TrimHandles.d.ts +4 -4
- package/dist/gui/timeline/TrimHandles.js.map +1 -1
- package/dist/gui/timeline/flattenHierarchy.js.map +1 -1
- package/dist/gui/timeline/timelineStateContext.js.map +1 -1
- package/dist/gui/timeline/tracks/AudioTrack.js.map +1 -1
- package/dist/gui/timeline/tracks/CaptionsTrack.js.map +1 -1
- package/dist/gui/timeline/tracks/EFThumbnailStrip.js.map +1 -1
- package/dist/gui/timeline/tracks/ImageTrack.js.map +1 -1
- package/dist/gui/timeline/tracks/TextTrack.js.map +1 -1
- package/dist/gui/timeline/tracks/TimegroupTrack.js.map +1 -1
- package/dist/gui/timeline/tracks/TrackItem.js.map +1 -1
- package/dist/gui/timeline/tracks/VideoTrack.js.map +1 -1
- package/dist/gui/timeline/tracks/renderTrackChildren.js.map +1 -1
- package/dist/gui/timeline/tracks/waveformUtils.js.map +1 -1
- package/dist/gui/transformCalculations.js.map +1 -1
- package/dist/gui/transformUtils.js.map +1 -1
- package/dist/gui/tree/EFTree.d.ts +4 -4
- package/dist/gui/tree/EFTree.js.map +1 -1
- package/dist/gui/tree/EFTreeItem.d.ts +4 -4
- package/dist/gui/tree/EFTreeItem.js.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/otel/BridgeSpanExporter.js.map +1 -1
- package/dist/otel/setupBrowserTracing.js.map +1 -1
- package/dist/otel/tracingHelpers.js.map +1 -1
- package/dist/preview/AdaptiveResolutionTracker.js.map +1 -1
- package/dist/preview/FrameController.js.map +1 -1
- package/dist/preview/QualityUpgradeScheduler.js.map +1 -1
- package/dist/preview/RenderContext.js.map +1 -1
- package/dist/preview/RenderProfiler.js.map +1 -1
- package/dist/preview/RenderStats.js.map +1 -1
- package/dist/preview/encoding/canvasEncoder.js.map +1 -1
- package/dist/preview/encoding/mainThreadEncoder.js +1 -1
- package/dist/preview/encoding/mainThreadEncoder.js.map +1 -1
- package/dist/preview/previewSettings.js.map +1 -1
- package/dist/preview/previewTypes.js.map +1 -1
- package/dist/preview/renderElementToCanvas.js.map +1 -1
- package/dist/preview/renderTimegroupToCanvas.js +2 -44
- package/dist/preview/renderTimegroupToCanvas.js.map +1 -1
- package/dist/preview/renderTimegroupToVideo.js +2 -2
- package/dist/preview/renderTimegroupToVideo.js.map +1 -1
- package/dist/preview/renderVideoToVideo.js +2 -2
- package/dist/preview/renderVideoToVideo.js.map +1 -1
- package/dist/preview/renderers.js.map +1 -1
- package/dist/preview/rendering/ScaleConfig.js.map +1 -1
- package/dist/preview/rendering/loadImage.js.map +1 -1
- package/dist/preview/rendering/renderToImageNative.js.map +1 -1
- package/dist/preview/rendering/serializeTimelineDirect.js +1 -1
- package/dist/preview/rendering/serializeTimelineDirect.js.map +1 -1
- package/dist/preview/statsTrackingStrategy.js.map +1 -1
- package/dist/preview/workers/WorkerPool.js.map +1 -1
- package/dist/render/EFRenderAPI.js.map +1 -1
- package/dist/transcoding/cache/RequestDeduplicator.js.map +1 -1
- package/dist/utils/LRUCache.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"renderTimegroupToCanvas.js","names":["timeMs: number","timeoutMs: number","blankVideos: string[]","renderState: RenderState","options: CanvasPreviewOptions","pendingResolutionScale: number | null","captureCanvas: HTMLCanvasElement | null","captureCtx: HtmlInCanvasContext | null","originalParent: ParentNode | null","originalNextSibling: ChildNode | null"],"sources":["../../src/preview/renderTimegroupToCanvas.ts"],"sourcesContent":["import type { EFTimegroup } from \"../elements/EFTimegroup.js\";\nimport type {\n CaptureOptions,\n CaptureFromCloneOptions,\n GeneratedThumbnail,\n GenerateThumbnailsOptions,\n ThumbnailQueue,\n CanvasPreviewResult,\n CanvasPreviewOptions,\n} from \"./renderTimegroupToCanvas.types.js\";\nimport { RenderContext } from \"./RenderContext.js\";\nimport { FrameController } from \"./FrameController.js\";\nimport { captureTimelineToDataUri } from \"./rendering/serializeTimelineDirect.js\";\nimport {\n updateAnimations,\n type AnimatableElement,\n} from \"../elements/updateAnimations.js\";\n\n// Re-export renderer types for external use\nexport type { RenderOptions, RenderResult, Renderer } from \"./renderers.js\";\nexport { getEffectiveRenderMode, isCanvas, isImage } from \"./renderers.js\";\nimport {\n type TemporalElement,\n isVisibleAtTime,\n DEFAULT_WIDTH,\n DEFAULT_HEIGHT,\n DEFAULT_CAPTURE_SCALE,\n DEFAULT_BLOCKING_TIMEOUT_MS,\n} from \"./previewTypes.js\";\nimport { defaultProfiler } from \"./RenderProfiler.js\";\nimport { logger } from \"./logger.js\";\n\n// Import rendering modules\nimport { loadImageFromDataUri } from \"./rendering/loadImage.js\";\nimport {\n createDprCanvas,\n renderToImageNative,\n} from \"./rendering/renderToImageNative.js\";\nimport {\n clearInlineImageCache,\n getInlineImageCacheSize,\n} from \"./rendering/inlineImages.js\";\nimport {\n isNativeCanvasApiAvailable,\n getRenderMode,\n} from \"./previewSettings.js\";\nimport type {\n HtmlInCanvasContext,\n HtmlInCanvasElement,\n} from \"./rendering/types.js\";\n\n// Re-export rendering types and functions for external use\nexport { loadImageFromDataUri };\n\n// ============================================================================\n// Constants (module-specific, not shared)\n// ============================================================================\n\n/** Number of rows to sample when checking canvas content */\nconst CANVAS_SAMPLE_STRIP_HEIGHT = 4;\n\n// ============================================================================\n// Types\n// ============================================================================\n\n// Re-export types from type-only module (zero side effects)\nexport type {\n ContentReadyMode,\n CaptureOptions,\n CaptureFromCloneOptions,\n GeneratedThumbnail,\n GenerateThumbnailsOptions,\n ThumbnailQueue,\n CanvasPreviewResult,\n CanvasPreviewOptions,\n} from \"./renderTimegroupToCanvas.types.js\";\n\n/**\n * Error thrown when video content is not ready within the blocking timeout.\n */\nexport class ContentNotReadyError extends Error {\n constructor(\n public readonly timeMs: number,\n public readonly timeoutMs: number,\n public readonly blankVideos: string[],\n ) {\n super(\n `Video content not ready at ${timeMs}ms after ${timeoutMs}ms timeout. Blank videos: ${blankVideos.join(\", \")}`,\n );\n this.name = \"ContentNotReadyError\";\n }\n}\n\n// ============================================================================\n// Module State (reset via resetRenderState)\n// ============================================================================\n\n/**\n * Module-level render state including caches and reusable objects.\n */\ninterface RenderState {\n inlineImageCache: Map<string, string>;\n layoutInitializedCanvases: WeakSet<HTMLCanvasElement>;\n xmlSerializer: XMLSerializer | null;\n textEncoder: TextEncoder;\n metrics: {\n inlineImageCacheHits: number;\n inlineImageCacheMisses: number;\n inlineImageCacheEvictions: number;\n };\n}\n\n/**\n * Module-level state for render operations.\n * Note: xmlSerializer is lazy-initialized for Node.js compatibility\n */\nconst renderState: RenderState = {\n inlineImageCache: new Map(),\n layoutInitializedCanvases: new WeakSet(),\n xmlSerializer: null, // Lazy-initialized in browser context\n textEncoder: new TextEncoder(),\n metrics: {\n inlineImageCacheHits: 0,\n inlineImageCacheMisses: 0,\n inlineImageCacheEvictions: 0,\n },\n};\n\n/**\n * Get the current render state for testing and debugging.\n * @returns The module-level render state object\n */\nexport function getRenderState(): RenderState {\n return renderState;\n}\n\n/**\n * Get cache metrics for monitoring performance.\n * @returns Object with cache hit/miss/eviction counts\n */\nexport function getCacheMetrics(): RenderState[\"metrics\"] {\n return { ...renderState.metrics };\n}\n\n/**\n * Reset cache metrics to zero.\n */\nexport function resetCacheMetrics(): void {\n renderState.metrics.inlineImageCacheHits = 0;\n renderState.metrics.inlineImageCacheMisses = 0;\n renderState.metrics.inlineImageCacheEvictions = 0;\n}\n\n/**\n * Reset all module state including profiling counters, caches, and logging flags.\n * Call at the start of export sessions to ensure clean state.\n */\nexport function resetRenderState(): void {\n defaultProfiler.reset();\n clearInlineImageCache();\n resetCacheMetrics();\n}\n\n// Re-export cache management functions\nexport { clearInlineImageCache, getInlineImageCacheSize };\n\n/**\n * DEBUG: Capture a single thumbnail at the current time.\n * Call from console: window.debugCaptureThumbnail()\n */\nif (typeof window !== \"undefined\") {\n (window as any).debugCaptureThumbnail = async function () {\n const timegroup = document.querySelector(\"ef-timegroup\") as any;\n if (!timegroup) {\n console.error(\"No timegroup found\");\n return;\n }\n\n const currentTime = timegroup.currentTimeMs ?? 0;\n\n try {\n const result = await captureTimegroupAtTime(timegroup, {\n timeMs: currentTime,\n scale: 0.25,\n contentReadyMode: \"blocking\",\n blockingTimeoutMs: 1000,\n });\n\n // Create a temporary img element to display the result\n const img = document.createElement(\"img\");\n if (result instanceof HTMLCanvasElement) {\n img.src = result.toDataURL();\n } else if (result instanceof HTMLImageElement) {\n img.src = result.src;\n }\n img.style.cssText =\n \"position:fixed;top:10px;right:10px;border:2px solid red;z-index:99999;\";\n document.body.appendChild(img);\n\n return result;\n } catch (err) {\n console.error(\"[DEBUG] Capture failed:\", err);\n throw err;\n }\n };\n}\n\n// ============================================================================\n// Internal Helpers\n// ============================================================================\n\n/**\n * Wait for next animation frame (allows browser to complete layout)\n */\nfunction waitForFrame(): Promise<void> {\n return new Promise((resolve) => requestAnimationFrame(() => resolve()));\n}\n\n/**\n * Check if a canvas has any rendered content (not all transparent/uninitialized).\n * Returns true if there's ANY non-transparent pixel.\n */\nfunction canvasHasContent(canvas: HTMLCanvasElement): boolean {\n const ctx = canvas.getContext(\"2d\", { willReadFrequently: true });\n if (!ctx) return false;\n\n try {\n const width = canvas.width;\n const height = canvas.height;\n if (width === 0 || height === 0) return false;\n\n // Sample a horizontal strip across the middle of the canvas\n // This catches most video content even if edges are black\n const stripY = Math.floor(height / 2);\n const imageData = ctx.getImageData(\n 0,\n stripY,\n width,\n CANVAS_SAMPLE_STRIP_HEIGHT,\n );\n const data = imageData.data;\n\n // Check if ANY pixel has non-zero alpha (is not transparent)\n // A truly blank/uninitialized canvas has all pixels at [0,0,0,0]\n // A black video frame would have pixels at [0,0,0,255] (opaque black)\n for (let i = 3; i < data.length; i += 4) {\n if (data[i] !== 0) {\n return true;\n }\n }\n\n return false;\n } catch {\n // Canvas might be tainted, assume it has content\n return true;\n }\n}\n\ninterface WaitForVideoContentResult {\n ready: boolean;\n blankVideos: string[];\n}\n\n/**\n * Wait for video canvases within a timegroup to have content.\n * Only checks videos that should be visible at the current time.\n * Returns result with ready status and list of blank video names.\n */\nexport async function waitForVideoContent(\n timegroup: EFTimegroup,\n timeMs: number,\n maxWaitMs: number,\n): Promise<WaitForVideoContentResult> {\n const startTime = performance.now();\n\n // Find all video elements in the timegroup (including nested)\n const allVideos = timegroup.querySelectorAll(\"ef-video\");\n if (allVideos.length === 0) return { ready: true, blankVideos: [] };\n\n // Filter to only videos that should be visible at this time\n const visibleVideos = Array.from(allVideos).filter((video) => {\n // Check if video itself is in time range\n if (!isVisibleAtTime(video, timeMs)) return false;\n\n // Check if all ancestor timegroups are in time range\n let parent = video.parentElement;\n while (parent && parent !== timegroup) {\n if (\n parent.tagName === \"EF-TIMEGROUP\" &&\n !isVisibleAtTime(parent, timeMs)\n ) {\n return false;\n }\n parent = parent.parentElement;\n }\n return true;\n });\n\n if (visibleVideos.length === 0) return { ready: true, blankVideos: [] };\n\n const getBlankVideoNames = () =>\n visibleVideos\n .filter((video) => {\n const shadowCanvas = video.shadowRoot?.querySelector(\"canvas\");\n return shadowCanvas && !canvasHasContent(shadowCanvas);\n })\n .map((v) => (v as TemporalElement).src || v.id || \"unnamed\");\n\n while (performance.now() - startTime < maxWaitMs) {\n let allHaveContent = true;\n\n for (const video of visibleVideos) {\n const shadowCanvas = video.shadowRoot?.querySelector(\"canvas\");\n if (shadowCanvas && shadowCanvas.width > 0 && shadowCanvas.height > 0) {\n if (!canvasHasContent(shadowCanvas)) {\n allHaveContent = false;\n break;\n }\n }\n }\n\n if (allHaveContent) return { ready: true, blankVideos: [] };\n\n // Wait a bit and check again\n await waitForFrame();\n }\n\n return { ready: false, blankVideos: getBlankVideoNames() };\n}\n\n/**\n * Captures a frame from an already-seeked render clone.\n * Used internally by captureBatch for efficiency (reuses one clone across all captures).\n *\n * @param renderClone - A render clone that has already been seeked to the target time\n * @param renderContainer - The container holding the render clone (from createRenderClone)\n * @param options - Capture options\n * @returns Canvas or Image with the rendered frame (both are CanvasImageSource)\n */\nexport async function captureFromClone(\n renderClone: EFTimegroup,\n _renderContainer: HTMLElement,\n options: CaptureFromCloneOptions = {},\n): Promise<CanvasImageSource> {\n const {\n scale = DEFAULT_CAPTURE_SCALE,\n contentReadyMode = \"immediate\",\n blockingTimeoutMs = DEFAULT_BLOCKING_TIMEOUT_MS,\n originalTimegroup,\n timeMs: explicitTimeMs,\n canvasMode,\n } = options;\n\n // Use explicit time if provided, otherwise fall back to clone's currentTimeMs\n // CRITICAL: Using explicit time ensures temporal visibility checks are accurate\n // NOTE: Must be defined BEFORE any logging that references timeMs\n const timeMs = explicitTimeMs ?? renderClone.currentTimeMs;\n\n // Use original timegroup dimensions if available, otherwise clone dimensions\n const sourceForDimensions = originalTimegroup ?? renderClone;\n const width = sourceForDimensions.offsetWidth || DEFAULT_WIDTH;\n const height = sourceForDimensions.offsetHeight || DEFAULT_HEIGHT;\n\n // NOTE: seekForRender() has already:\n // 1. Called frameController.renderFrame() to coordinate FrameRenderable elements\n // 2. Awaited #executeCustomFrameTasks() so frame tasks are complete\n // No need to call frameController.renderFrame() again - it would fire tasks redundantly\n\n if (contentReadyMode === \"blocking\") {\n const result = await waitForVideoContent(\n renderClone,\n timeMs,\n blockingTimeoutMs,\n );\n if (!result.ready) {\n throw new ContentNotReadyError(\n timeMs,\n blockingTimeoutMs,\n result.blankVideos,\n );\n }\n }\n\n // Determine effective canvas mode:\n // 1. If explicitly specified, use that\n // 2. If \"native\" is requested but not available, fall back to foreignObject\n // 3. If not specified, default to foreignObject for compatibility\n const effectiveCanvasMode = (() => {\n if (!canvasMode) return \"foreignObject\";\n if (canvasMode === \"native\" && !isNativeCanvasApiAvailable()) {\n logger.debug(\n \"[captureFromClone] Native canvas mode requested but not available, falling back to foreignObject\",\n );\n return \"foreignObject\";\n }\n return canvasMode;\n })();\n\n // Create RenderContext for caching during this capture operation (only needed for foreignObject)\n const renderContext = new RenderContext();\n\n try {\n if (effectiveCanvasMode === \"native\") {\n // NATIVE PATH: Use drawElementImage API (~1.76x faster than foreignObject)\n // No DOM serialization, no canvas-to-dataURL encoding, no image loading\n // Direct browser-native rendering\n\n const t0 = performance.now();\n const canvas = await renderToImageNative(renderClone, width, height, {\n skipDprScaling: true, // Use 1x DPR for video export (4x fewer pixels!)\n });\n const renderTime = performance.now() - t0;\n\n logger.debug(\n `[captureFromClone] native render=${renderTime.toFixed(0)}ms (canvasScale=${scale})`,\n );\n\n return canvas;\n } else {\n // FOREIGNOBJECT PATH: Serialize DOM → SVG → Image → Canvas\n // More compatible but slower than native path\n\n // NOTE: seekForRender() has already ensured rendering is complete, including:\n // - Lit updates propagated\n // - All LitElement descendants updated\n // - frameController.renderFrame() called for FrameRenderable elements\n // - Layout stabilization complete\n // No additional RAF wait needed - can serialize immediately\n\n const t0 = performance.now();\n const dataUri = await captureTimelineToDataUri(\n renderClone,\n width,\n height,\n {\n renderContext,\n canvasScale: scale,\n timeMs,\n },\n );\n const serializeTime = performance.now() - t0;\n\n const t1 = performance.now();\n const image = await loadImageFromDataUri(dataUri);\n const loadTime = performance.now() - t1;\n\n logger.debug(\n `[captureFromClone] foreignObject serialize=${serializeTime.toFixed(0)}ms, load=${loadTime.toFixed(0)}ms (canvasScale=${scale})`,\n );\n\n // Return image directly - no copy needed!\n return image;\n }\n } finally {\n // Ensure RenderContext is disposed even if an error occurs\n renderContext.dispose();\n }\n}\n\n/**\n * Captures a single frame from a timegroup at a specific time.\n *\n * CLONE-TIMELINE ARCHITECTURE:\n * Creates an independent render clone, seeks it to the target time, and captures.\n * Prime-timeline is NEVER seeked - user can continue previewing/editing during capture.\n *\n * @param timegroup - The source timegroup\n * @param options - Capture options including timeMs, scale, contentReadyMode\n * @returns Canvas with the rendered frame\n * @throws ContentNotReadyError if blocking mode times out waiting for video content\n */\nexport async function captureTimegroupAtTime(\n timegroup: EFTimegroup,\n options: CaptureOptions,\n): Promise<CanvasImageSource> {\n const {\n timeMs,\n scale = DEFAULT_CAPTURE_SCALE,\n // skipRestore is deprecated with Clone-timeline (Prime is never seeked)\n contentReadyMode = \"immediate\",\n blockingTimeoutMs = DEFAULT_BLOCKING_TIMEOUT_MS,\n canvasMode,\n skipClone = false,\n } = options;\n\n if (skipClone) {\n // DIRECT RENDERING: Skip clone creation for headless server rendering\n // Seek prime timeline directly and capture from it\n // WARNING: This modifies the prime timeline! Only use in headless contexts.\n\n const seekStart = performance.now();\n await timegroup.seekForRender(timeMs);\n const seekMs = performance.now() - seekStart;\n\n const renderStart = performance.now();\n // Use timegroup's actual container (parentElement or document.body as fallback)\n const container = (timegroup.parentElement || document.body) as HTMLElement;\n const result = await captureFromClone(timegroup, container, {\n scale,\n contentReadyMode,\n blockingTimeoutMs,\n originalTimegroup: undefined, // No original since we're rendering the prime\n canvasMode,\n timeMs, // Pass explicit time since we're not using a clone\n });\n const renderMs = performance.now() - renderStart;\n\n // Store timing (no clone time since we skipped it)\n if (typeof result === \"object\" && result !== null) {\n (result as any).__perfTiming = { cloneMs: 0, seekMs, renderMs };\n }\n\n return result;\n }\n\n // CLONE-TIMELINE: Create a short-lived render clone for this capture\n // Prime-timeline is NEVER seeked - clone is fully independent\n const cloneStart = performance.now();\n const {\n clone: renderClone,\n container: renderContainer,\n cleanup: cleanupRenderClone,\n } = await timegroup.createRenderClone();\n const cloneMs = performance.now() - cloneStart;\n\n try {\n // Seek the clone to target time (Prime stays at user position)\n // Use seekForRender which bypasses duration clamping - render clones may have\n // zero duration initially until media durations are computed, but we still\n // want to seek to the requested time for capture purposes.\n const seekStart = performance.now();\n await renderClone.seekForRender(timeMs);\n const seekMs = performance.now() - seekStart;\n\n // Use the shared capture helper\n const renderStart = performance.now();\n const result = await captureFromClone(renderClone, renderContainer, {\n scale,\n contentReadyMode,\n blockingTimeoutMs,\n originalTimegroup: timegroup,\n canvasMode,\n });\n const renderMs = performance.now() - renderStart;\n\n // Store timing on the result for access by callers (if they need it)\n // Note: CanvasImageSource doesn't support custom properties, but we can attach them anyway\n if (typeof result === \"object\" && result !== null) {\n (result as any).__perfTiming = { cloneMs, seekMs, renderMs };\n }\n\n return result;\n } finally {\n // Clean up the render clone\n cleanupRenderClone();\n }\n}\n\n/**\n * Generate thumbnails using an existing render clone and mutable queue.\n * The queue can be modified while generation is in progress.\n *\n * @param renderClone - Pre-created render clone to use\n * @param renderContainer - Container for the render clone\n * @param queue - Mutable queue that provides timestamps\n * @param options - Capture options (scale, contentReadyMode, etc.)\n * @yields Objects with { timeMs, canvas } for each captured thumbnail\n *\n * @example\n * ```ts\n * const queue = new MutableTimestampQueue();\n * queue.reset([0, 100, 200]);\n *\n * for await (const { timeMs, canvas } of generateThumbnailsFromClone(clone, container, queue)) {\n * cache.set(timeMs, canvas);\n * // Queue can be modified here while generator continues\n * }\n * ```\n */\nexport async function* generateThumbnailsFromClone(\n renderClone: EFTimegroup,\n renderContainer: HTMLElement,\n queue: ThumbnailQueue,\n options: GenerateThumbnailsOptions = {},\n): AsyncGenerator<GeneratedThumbnail> {\n const {\n scale = DEFAULT_CAPTURE_SCALE,\n contentReadyMode = \"immediate\",\n blockingTimeoutMs = DEFAULT_BLOCKING_TIMEOUT_MS,\n signal,\n } = options;\n\n while (true) {\n // Check if aborted before starting work\n if (signal?.aborted) {\n break;\n }\n\n const timeMs = queue.shift();\n if (timeMs === undefined) {\n // Queue is empty, generator exits\n break;\n }\n\n // Seek the clone to the target time\n await renderClone.seekForRender(timeMs);\n\n // Check if aborted after seek (before expensive capture)\n if (signal?.aborted) {\n break;\n }\n\n // Capture from the seeked clone, passing explicit timeMs\n const canvas = await captureFromClone(renderClone, renderContainer, {\n scale,\n contentReadyMode,\n blockingTimeoutMs,\n timeMs, // CRITICAL: Pass explicit time for accurate temporal visibility\n });\n\n // Yield the result with explicit timestamp association\n yield { timeMs, canvas };\n }\n}\n\n/**\n * Generate thumbnails for multiple timestamps efficiently using a single render clone.\n * This avoids the overhead of creating/destroying a clone for each thumbnail.\n *\n * @param timegroup - The timegroup to capture\n * @param timestamps - Array of timestamps to capture (in milliseconds)\n * @param options - Capture options (scale, contentReadyMode, etc.)\n * @param signal - Optional AbortSignal to cancel generation\n * @yields Objects with { timeMs, canvas } for each captured thumbnail\n *\n * @example\n * ```ts\n * for await (const { timeMs, canvas } of generateThumbnails(tg, [0, 100, 200])) {\n * console.log(`Got thumbnail for ${timeMs}ms`);\n * thumbnailCache.set(timeMs, canvas);\n * }\n * ```\n */\nexport async function* generateThumbnails(\n timegroup: EFTimegroup,\n timestamps: number[],\n options: GenerateThumbnailsOptions = {},\n signal?: AbortSignal,\n): AsyncGenerator<GeneratedThumbnail> {\n const {\n scale = DEFAULT_CAPTURE_SCALE,\n contentReadyMode = \"immediate\",\n blockingTimeoutMs = DEFAULT_BLOCKING_TIMEOUT_MS,\n } = options;\n\n // Create a single render clone for all thumbnails\n const {\n clone: renderClone,\n container: renderContainer,\n cleanup: cleanupRenderClone,\n } = await timegroup.createRenderClone();\n\n try {\n for (const timeMs of timestamps) {\n // Check for abort before each capture\n signal?.throwIfAborted();\n\n // Seek the clone to the target time\n await renderClone.seekForRender(timeMs);\n\n // Capture from the seeked clone\n const canvas = await captureFromClone(renderClone, renderContainer, {\n scale,\n contentReadyMode,\n blockingTimeoutMs,\n originalTimegroup: timegroup,\n });\n\n // Yield the result with explicit timestamp association\n yield { timeMs, canvas };\n }\n } finally {\n // Always clean up the render clone\n cleanupRenderClone();\n }\n}\n\n/** Epsilon for comparing time values (ms) - times within this are considered equal */\nconst TIME_EPSILON_MS = 1;\n\n/** Default scale for preview rendering */\nconst DEFAULT_PREVIEW_SCALE = 1;\n\n/** Default resolution scale (full resolution) */\nconst DEFAULT_RESOLUTION_SCALE = 1;\n\n/**\n * Convert relative time to absolute time for a timegroup.\n * Nested timegroup children have ABSOLUTE startTimeMs values,\n * so relative capture times must be converted for temporal culling.\n */\nfunction toAbsoluteTime(\n timegroup: EFTimegroup,\n relativeTimeMs: number,\n): number {\n return relativeTimeMs + (timegroup.startTimeMs ?? 0);\n}\n\n/**\n * Renders a timegroup preview to a canvas using SVG foreignObject.\n *\n * Captures the prime timeline's current visual state including DOM changes\n * from frame tasks (SVG paths, canvas content, text updates, etc.).\n *\n * Optimized with:\n * - Passive clone structure rebuilt each frame from prime's current state\n * - Temporal bucketing for time-based culling\n * - RenderContext for canvas pixel caching across frames\n * - Resolution scaling for performance (renders at lower resolution, CSS upscales)\n *\n * @param timegroup - The source timegroup to preview (prime timeline)\n * @param scaleOrOptions - Scale factor (default 1) or options object\n * @returns Object with canvas and refresh function\n */\nexport function renderTimegroupToCanvas(\n timegroup: EFTimegroup,\n scaleOrOptions: number | CanvasPreviewOptions = DEFAULT_PREVIEW_SCALE,\n): CanvasPreviewResult {\n // Normalize options\n const options: CanvasPreviewOptions =\n typeof scaleOrOptions === \"number\"\n ? { scale: scaleOrOptions }\n : scaleOrOptions;\n\n const scale = options.scale ?? DEFAULT_PREVIEW_SCALE;\n // These are mutable to support dynamic resolution changes\n let currentResolutionScale =\n options.resolutionScale ?? DEFAULT_RESOLUTION_SCALE;\n\n const width = timegroup.offsetWidth || DEFAULT_WIDTH;\n const height = timegroup.offsetHeight || DEFAULT_HEIGHT;\n const dpr =\n (typeof window !== \"undefined\" ? window.devicePixelRatio : 1) || 1;\n\n // Calculate effective render dimensions (internal resolution) - mutable\n let renderWidth = Math.floor(width * currentResolutionScale);\n let renderHeight = Math.floor(height * currentResolutionScale);\n\n // Create canvas with proper DPR handling\n const canvas = createDprCanvas({\n renderWidth,\n renderHeight,\n scale,\n fullWidth: width,\n fullHeight: height,\n dpr,\n });\n\n // Return canvas directly - no wrapper needed\n const wrapperContainer = canvas;\n\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) {\n throw new Error(\"Failed to get canvas 2d context\");\n }\n\n // Track render state\n let rendering = false;\n let lastTimeMs = -1;\n let disposed = false;\n\n // Invalidate lastTimeMs when composition structure or attributes change so\n // refresh() re-renders even when currentTimeMs hasn't changed (e.g. paused edits).\n const compositionObserver = new MutationObserver(() => {\n if (!rendering) lastTimeMs = -1;\n });\n compositionObserver.observe(timegroup, {\n attributes: true,\n childList: true,\n subtree: true,\n });\n\n // Create RenderContext for caching across refresh calls (foreignObject only)\n const renderContext = new RenderContext();\n\n // Create FrameController for coordinating element rendering\n // Cached for the lifetime of this preview instance\n const frameController = new FrameController(timegroup);\n\n // Log resolution scale on first render for debugging\n let hasLoggedScale = false;\n\n // Pending resolution change - applied at start of next refresh to avoid blanking\n let pendingResolutionScale: number | null = null;\n\n // Use the user's render mode preference. Native requires the timegroup to be\n // inside a <canvas layoutsubtree> for drawElementImage to work.\n const useNative =\n getRenderMode() === \"native\" && isNativeCanvasApiAvailable();\n let captureCanvas: HTMLCanvasElement | null = null;\n let captureCtx: HtmlInCanvasContext | null = null;\n let originalParent: ParentNode | null = null;\n let originalNextSibling: ChildNode | null = null;\n let savedClipPath = \"\";\n let savedPointerEvents = \"\";\n\n if (useNative) {\n captureCanvas = document.createElement(\"canvas\");\n captureCanvas.setAttribute(\"layoutsubtree\", \"\");\n (captureCanvas as HtmlInCanvasElement).layoutSubtree = true;\n captureCanvas.width = renderWidth;\n captureCanvas.height = renderHeight;\n captureCanvas.style.cssText = `position:fixed;left:0;top:0;width:${width}px;height:${height}px;opacity:0;pointer-events:none;z-index:-9999;`;\n originalParent = timegroup.parentNode;\n originalNextSibling = timegroup.nextSibling;\n savedClipPath = timegroup.style.clipPath;\n savedPointerEvents = timegroup.style.pointerEvents;\n timegroup.style.clipPath = \"\";\n timegroup.style.pointerEvents = \"\";\n captureCanvas.appendChild(timegroup);\n document.body.appendChild(captureCanvas);\n captureCtx = captureCanvas.getContext(\"2d\") as HtmlInCanvasContext;\n void captureCanvas.offsetHeight;\n void timegroup.offsetHeight;\n }\n\n /**\n * Apply pending resolution scale changes.\n * Called at the start of refresh() before rendering, so the old content\n * stays visible until new content is ready to be drawn.\n */\n const applyPendingResolutionChange = (): void => {\n if (pendingResolutionScale === null) return;\n\n const newScale = pendingResolutionScale;\n pendingResolutionScale = null;\n\n currentResolutionScale = newScale;\n renderWidth = Math.floor(width * currentResolutionScale);\n renderHeight = Math.floor(height * currentResolutionScale);\n\n if (captureCanvas) {\n captureCanvas.width = renderWidth;\n captureCanvas.height = renderHeight;\n }\n };\n\n /**\n * Dynamically change resolution scale without rebuilding clone structure.\n * The actual change is deferred until next refresh() to avoid blanking -\n * old content stays visible until new content is ready.\n */\n const setResolutionScale = (newScale: number): void => {\n // Clamp to valid range\n newScale = Math.max(0.1, Math.min(1, newScale));\n\n if (newScale === currentResolutionScale && pendingResolutionScale === null)\n return;\n\n // Queue the change - will be applied at start of next refresh\n pendingResolutionScale = newScale;\n\n // Force re-render on next refresh by invalidating lastTimeMs\n lastTimeMs = -1;\n };\n\n const getResolutionScale = (): number =>\n pendingResolutionScale ?? currentResolutionScale;\n\n // Rolling timing stats for per-phase profiling\n let frameCount = 0;\n let totalFrameControllerMs = 0;\n let totalCaptureMs = 0;\n let totalCopyMs = 0;\n let totalFrameMs = 0;\n\n const refresh = async (): Promise<void> => {\n if (disposed) return;\n\n const sourceTimeMs = timegroup.currentTimeMs ?? 0;\n const userTimeMs = timegroup.userTimeMs ?? 0;\n\n if (Math.abs(sourceTimeMs - userTimeMs) > TIME_EPSILON_MS) return;\n if (userTimeMs === lastTimeMs) return;\n if (rendering) return;\n\n lastTimeMs = userTimeMs;\n rendering = true;\n\n applyPendingResolutionChange();\n\n if (!hasLoggedScale) {\n hasLoggedScale = true;\n const mode = useNative ? \"native\" : \"foreignObject\";\n logger.debug(\n `[renderTimegroupToCanvas] Resolution scale: ${currentResolutionScale} (${width}x${height} → ${renderWidth}x${renderHeight}), canvas buffer: ${canvas.width}x${canvas.height}, CSS size: ${canvas.style.width}x${canvas.style.height}, renderMode: ${mode}`,\n );\n }\n\n try {\n const tFrame = performance.now();\n\n const tFC0 = performance.now();\n await frameController.renderFrame(userTimeMs, {\n waitForLitUpdate: false,\n onAnimationsUpdate: (root) => {\n updateAnimations(root as AnimatableElement);\n },\n });\n const fcMs = performance.now() - tFC0;\n\n const tCapture0 = performance.now();\n\n if (useNative && captureCanvas && captureCtx) {\n if (captureCanvas.width !== width || captureCanvas.height !== height) {\n captureCtx.save();\n captureCtx.scale(\n captureCanvas.width / width,\n captureCanvas.height / height,\n );\n captureCtx.drawElementImage(timegroup, 0, 0);\n captureCtx.restore();\n } else {\n captureCtx.drawElementImage(timegroup, 0, 0);\n }\n const captureMs = performance.now() - tCapture0;\n\n const tCopy0 = performance.now();\n const targetWidth = Math.floor(renderWidth * scale * dpr);\n const targetHeight = Math.floor(renderHeight * scale * dpr);\n if (canvas.width !== targetWidth || canvas.height !== targetHeight) {\n canvas.width = targetWidth;\n canvas.height = targetHeight;\n } else {\n ctx.clearRect(0, 0, canvas.width, canvas.height);\n }\n ctx.drawImage(captureCanvas, 0, 0, canvas.width, canvas.height);\n const copyMs = performance.now() - tCopy0;\n\n const frameMs = performance.now() - tFrame;\n frameCount++;\n totalFrameControllerMs += fcMs;\n totalCaptureMs += captureMs;\n totalCopyMs += copyMs;\n totalFrameMs += frameMs;\n\n defaultProfiler.incrementRenderCount();\n if (defaultProfiler.shouldLogByFrameCount(60)) {\n frameCount = 0;\n totalFrameControllerMs = 0;\n totalCaptureMs = 0;\n totalCopyMs = 0;\n totalFrameMs = 0;\n }\n } else {\n const absoluteTimeMs = toAbsoluteTime(timegroup, userTimeMs);\n\n const dataUri = await captureTimelineToDataUri(\n timegroup,\n width,\n height,\n {\n renderContext,\n canvasScale: currentResolutionScale,\n timeMs: absoluteTimeMs,\n },\n );\n const captureMs = performance.now() - tCapture0;\n\n const tCopy0 = performance.now();\n const image = await loadImageFromDataUri(dataUri);\n const copyMs = performance.now() - tCopy0;\n\n const targetWidth = Math.floor(renderWidth * scale * dpr);\n const targetHeight = Math.floor(renderHeight * scale * dpr);\n if (canvas.width !== targetWidth || canvas.height !== targetHeight) {\n canvas.width = targetWidth;\n canvas.height = targetHeight;\n } else {\n ctx.clearRect(0, 0, canvas.width, canvas.height);\n }\n\n ctx.save();\n ctx.scale(dpr * scale, dpr * scale);\n ctx.drawImage(image, 0, 0, renderWidth, renderHeight);\n ctx.restore();\n\n const frameMs = performance.now() - tFrame;\n frameCount++;\n totalFrameControllerMs += fcMs;\n totalCaptureMs += captureMs;\n totalCopyMs += copyMs;\n totalFrameMs += frameMs;\n\n defaultProfiler.incrementRenderCount();\n if (defaultProfiler.shouldLogByFrameCount(60)) {\n frameCount = 0;\n totalFrameControllerMs = 0;\n totalCaptureMs = 0;\n totalCopyMs = 0;\n totalFrameMs = 0;\n }\n }\n } catch (e) {\n logger.error(\"Canvas preview render failed:\", e);\n } finally {\n rendering = false;\n }\n };\n\n /**\n * Dispose the preview and release resources.\n */\n const dispose = (): void => {\n if (disposed) return;\n disposed = true;\n compositionObserver.disconnect();\n frameController.abort();\n renderContext.dispose();\n\n // Restore timegroup to original DOM position if native mode moved it\n if (useNative && originalParent) {\n timegroup.style.clipPath = savedClipPath;\n timegroup.style.pointerEvents = savedPointerEvents;\n if (originalNextSibling) {\n originalParent.insertBefore(timegroup, originalNextSibling);\n } else {\n originalParent.appendChild(timegroup);\n }\n captureCanvas?.remove();\n }\n };\n\n // Do initial render\n refresh();\n\n return {\n container: wrapperContainer,\n canvas,\n refresh,\n setResolutionScale,\n getResolutionScale,\n dispose,\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"renderTimegroupToCanvas.js","names":["timeMs: number","timeoutMs: number","blankVideos: string[]","renderState: RenderState","options: CanvasPreviewOptions","pendingResolutionScale: number | null","captureCanvas: HTMLCanvasElement | null","captureCtx: HtmlInCanvasContext | null","originalParent: ParentNode | null","originalNextSibling: ChildNode | null"],"sources":["../../src/preview/renderTimegroupToCanvas.ts"],"sourcesContent":["import type { EFTimegroup } from \"../elements/EFTimegroup.js\";\nimport type {\n CaptureOptions,\n CaptureFromCloneOptions,\n GeneratedThumbnail,\n GenerateThumbnailsOptions,\n ThumbnailQueue,\n CanvasPreviewResult,\n CanvasPreviewOptions,\n} from \"./renderTimegroupToCanvas.types.js\";\nimport { RenderContext } from \"./RenderContext.js\";\nimport { FrameController } from \"./FrameController.js\";\nimport { captureTimelineToDataUri } from \"./rendering/serializeTimelineDirect.js\";\nimport { updateAnimations, type AnimatableElement } from \"../elements/updateAnimations.js\";\n\n// Re-export renderer types for external use\nexport type { RenderOptions, RenderResult, Renderer } from \"./renderers.js\";\nexport { getEffectiveRenderMode, isCanvas, isImage } from \"./renderers.js\";\nimport {\n type TemporalElement,\n isVisibleAtTime,\n DEFAULT_WIDTH,\n DEFAULT_HEIGHT,\n DEFAULT_CAPTURE_SCALE,\n DEFAULT_BLOCKING_TIMEOUT_MS,\n} from \"./previewTypes.js\";\nimport { defaultProfiler } from \"./RenderProfiler.js\";\nimport { logger } from \"./logger.js\";\n\n// Import rendering modules\nimport { loadImageFromDataUri } from \"./rendering/loadImage.js\";\nimport { createDprCanvas, renderToImageNative } from \"./rendering/renderToImageNative.js\";\nimport { clearInlineImageCache, getInlineImageCacheSize } from \"./rendering/inlineImages.js\";\nimport { isNativeCanvasApiAvailable, getRenderMode } from \"./previewSettings.js\";\nimport type { HtmlInCanvasContext, HtmlInCanvasElement } from \"./rendering/types.js\";\n\n// Re-export rendering types and functions for external use\nexport { loadImageFromDataUri };\n\n// ============================================================================\n// Constants (module-specific, not shared)\n// ============================================================================\n\n/** Number of rows to sample when checking canvas content */\nconst CANVAS_SAMPLE_STRIP_HEIGHT = 4;\n\n// ============================================================================\n// Types\n// ============================================================================\n\n// Re-export types from type-only module (zero side effects)\nexport type {\n ContentReadyMode,\n CaptureOptions,\n CaptureFromCloneOptions,\n GeneratedThumbnail,\n GenerateThumbnailsOptions,\n ThumbnailQueue,\n CanvasPreviewResult,\n CanvasPreviewOptions,\n} from \"./renderTimegroupToCanvas.types.js\";\n\n/**\n * Error thrown when video content is not ready within the blocking timeout.\n */\nexport class ContentNotReadyError extends Error {\n constructor(\n public readonly timeMs: number,\n public readonly timeoutMs: number,\n public readonly blankVideos: string[],\n ) {\n super(\n `Video content not ready at ${timeMs}ms after ${timeoutMs}ms timeout. Blank videos: ${blankVideos.join(\", \")}`,\n );\n this.name = \"ContentNotReadyError\";\n }\n}\n\n// ============================================================================\n// Module State (reset via resetRenderState)\n// ============================================================================\n\n/**\n * Module-level render state including caches and reusable objects.\n */\ninterface RenderState {\n inlineImageCache: Map<string, string>;\n layoutInitializedCanvases: WeakSet<HTMLCanvasElement>;\n xmlSerializer: XMLSerializer | null;\n textEncoder: TextEncoder;\n metrics: {\n inlineImageCacheHits: number;\n inlineImageCacheMisses: number;\n inlineImageCacheEvictions: number;\n };\n}\n\n/**\n * Module-level state for render operations.\n * Note: xmlSerializer is lazy-initialized for Node.js compatibility\n */\nconst renderState: RenderState = {\n inlineImageCache: new Map(),\n layoutInitializedCanvases: new WeakSet(),\n xmlSerializer: null, // Lazy-initialized in browser context\n textEncoder: new TextEncoder(),\n metrics: {\n inlineImageCacheHits: 0,\n inlineImageCacheMisses: 0,\n inlineImageCacheEvictions: 0,\n },\n};\n\n/**\n * Get the current render state for testing and debugging.\n * @returns The module-level render state object\n */\nexport function getRenderState(): RenderState {\n return renderState;\n}\n\n/**\n * Get cache metrics for monitoring performance.\n * @returns Object with cache hit/miss/eviction counts\n */\nexport function getCacheMetrics(): RenderState[\"metrics\"] {\n return { ...renderState.metrics };\n}\n\n/**\n * Reset cache metrics to zero.\n */\nexport function resetCacheMetrics(): void {\n renderState.metrics.inlineImageCacheHits = 0;\n renderState.metrics.inlineImageCacheMisses = 0;\n renderState.metrics.inlineImageCacheEvictions = 0;\n}\n\n/**\n * Reset all module state including profiling counters, caches, and logging flags.\n * Call at the start of export sessions to ensure clean state.\n */\nexport function resetRenderState(): void {\n defaultProfiler.reset();\n clearInlineImageCache();\n resetCacheMetrics();\n}\n\n// Re-export cache management functions\nexport { clearInlineImageCache, getInlineImageCacheSize };\n\n/**\n * DEBUG: Capture a single thumbnail at the current time.\n * Call from console: window.debugCaptureThumbnail()\n */\nif (typeof window !== \"undefined\") {\n (window as any).debugCaptureThumbnail = async function () {\n const timegroup = document.querySelector(\"ef-timegroup\") as any;\n if (!timegroup) {\n console.error(\"No timegroup found\");\n return;\n }\n\n const currentTime = timegroup.currentTimeMs ?? 0;\n\n try {\n const result = await captureTimegroupAtTime(timegroup, {\n timeMs: currentTime,\n scale: 0.25,\n contentReadyMode: \"blocking\",\n blockingTimeoutMs: 1000,\n });\n\n // Create a temporary img element to display the result\n const img = document.createElement(\"img\");\n if (result instanceof HTMLCanvasElement) {\n img.src = result.toDataURL();\n } else if (result instanceof HTMLImageElement) {\n img.src = result.src;\n }\n img.style.cssText = \"position:fixed;top:10px;right:10px;border:2px solid red;z-index:99999;\";\n document.body.appendChild(img);\n\n return result;\n } catch (err) {\n console.error(\"[DEBUG] Capture failed:\", err);\n throw err;\n }\n };\n}\n\n// ============================================================================\n// Internal Helpers\n// ============================================================================\n\n/**\n * Wait for next animation frame (allows browser to complete layout)\n */\nfunction waitForFrame(): Promise<void> {\n return new Promise((resolve) => requestAnimationFrame(() => resolve()));\n}\n\n/**\n * Check if a canvas has any rendered content (not all transparent/uninitialized).\n * Returns true if there's ANY non-transparent pixel.\n */\nfunction canvasHasContent(canvas: HTMLCanvasElement): boolean {\n const ctx = canvas.getContext(\"2d\", { willReadFrequently: true });\n if (!ctx) return false;\n\n try {\n const width = canvas.width;\n const height = canvas.height;\n if (width === 0 || height === 0) return false;\n\n // Sample a horizontal strip across the middle of the canvas\n // This catches most video content even if edges are black\n const stripY = Math.floor(height / 2);\n const imageData = ctx.getImageData(0, stripY, width, CANVAS_SAMPLE_STRIP_HEIGHT);\n const data = imageData.data;\n\n // Check if ANY pixel has non-zero alpha (is not transparent)\n // A truly blank/uninitialized canvas has all pixels at [0,0,0,0]\n // A black video frame would have pixels at [0,0,0,255] (opaque black)\n for (let i = 3; i < data.length; i += 4) {\n if (data[i] !== 0) {\n return true;\n }\n }\n\n return false;\n } catch {\n // Canvas might be tainted, assume it has content\n return true;\n }\n}\n\ninterface WaitForVideoContentResult {\n ready: boolean;\n blankVideos: string[];\n}\n\n/**\n * Wait for video canvases within a timegroup to have content.\n * Only checks videos that should be visible at the current time.\n * Returns result with ready status and list of blank video names.\n */\nexport async function waitForVideoContent(\n timegroup: EFTimegroup,\n timeMs: number,\n maxWaitMs: number,\n): Promise<WaitForVideoContentResult> {\n const startTime = performance.now();\n\n // Find all video elements in the timegroup (including nested)\n const allVideos = timegroup.querySelectorAll(\"ef-video\");\n if (allVideos.length === 0) return { ready: true, blankVideos: [] };\n\n // Filter to only videos that should be visible at this time\n const visibleVideos = Array.from(allVideos).filter((video) => {\n // Check if video itself is in time range\n if (!isVisibleAtTime(video, timeMs)) return false;\n\n // Check if all ancestor timegroups are in time range\n let parent = video.parentElement;\n while (parent && parent !== timegroup) {\n if (parent.tagName === \"EF-TIMEGROUP\" && !isVisibleAtTime(parent, timeMs)) {\n return false;\n }\n parent = parent.parentElement;\n }\n return true;\n });\n\n if (visibleVideos.length === 0) return { ready: true, blankVideos: [] };\n\n const getBlankVideoNames = () =>\n visibleVideos\n .filter((video) => {\n const shadowCanvas = video.shadowRoot?.querySelector(\"canvas\");\n return shadowCanvas && !canvasHasContent(shadowCanvas);\n })\n .map((v) => (v as TemporalElement).src || v.id || \"unnamed\");\n\n while (performance.now() - startTime < maxWaitMs) {\n let allHaveContent = true;\n\n for (const video of visibleVideos) {\n const shadowCanvas = video.shadowRoot?.querySelector(\"canvas\");\n if (shadowCanvas && shadowCanvas.width > 0 && shadowCanvas.height > 0) {\n if (!canvasHasContent(shadowCanvas)) {\n allHaveContent = false;\n break;\n }\n }\n }\n\n if (allHaveContent) return { ready: true, blankVideos: [] };\n\n // Wait a bit and check again\n await waitForFrame();\n }\n\n return { ready: false, blankVideos: getBlankVideoNames() };\n}\n\n/**\n * Captures a frame from an already-seeked render clone.\n * Used internally by captureBatch for efficiency (reuses one clone across all captures).\n *\n * @param renderClone - A render clone that has already been seeked to the target time\n * @param renderContainer - The container holding the render clone (from createRenderClone)\n * @param options - Capture options\n * @returns Canvas or Image with the rendered frame (both are CanvasImageSource)\n */\nexport async function captureFromClone(\n renderClone: EFTimegroup,\n _renderContainer: HTMLElement,\n options: CaptureFromCloneOptions = {},\n): Promise<CanvasImageSource> {\n const {\n scale = DEFAULT_CAPTURE_SCALE,\n contentReadyMode = \"immediate\",\n blockingTimeoutMs = DEFAULT_BLOCKING_TIMEOUT_MS,\n originalTimegroup,\n timeMs: explicitTimeMs,\n canvasMode,\n } = options;\n\n // Use explicit time if provided, otherwise fall back to clone's currentTimeMs\n // CRITICAL: Using explicit time ensures temporal visibility checks are accurate\n // NOTE: Must be defined BEFORE any logging that references timeMs\n const timeMs = explicitTimeMs ?? renderClone.currentTimeMs;\n\n // Use original timegroup dimensions if available, otherwise clone dimensions\n const sourceForDimensions = originalTimegroup ?? renderClone;\n const width = sourceForDimensions.offsetWidth || DEFAULT_WIDTH;\n const height = sourceForDimensions.offsetHeight || DEFAULT_HEIGHT;\n\n // NOTE: seekForRender() has already:\n // 1. Called frameController.renderFrame() to coordinate FrameRenderable elements\n // 2. Awaited #executeCustomFrameTasks() so frame tasks are complete\n // No need to call frameController.renderFrame() again - it would fire tasks redundantly\n\n if (contentReadyMode === \"blocking\") {\n const result = await waitForVideoContent(renderClone, timeMs, blockingTimeoutMs);\n if (!result.ready) {\n throw new ContentNotReadyError(timeMs, blockingTimeoutMs, result.blankVideos);\n }\n }\n\n // Determine effective canvas mode:\n // 1. If explicitly specified, use that\n // 2. If \"native\" is requested but not available, fall back to foreignObject\n // 3. If not specified, default to foreignObject for compatibility\n const effectiveCanvasMode = (() => {\n if (!canvasMode) return \"foreignObject\";\n if (canvasMode === \"native\" && !isNativeCanvasApiAvailable()) {\n logger.debug(\n \"[captureFromClone] Native canvas mode requested but not available, falling back to foreignObject\",\n );\n return \"foreignObject\";\n }\n return canvasMode;\n })();\n\n // Create RenderContext for caching during this capture operation (only needed for foreignObject)\n const renderContext = new RenderContext();\n\n try {\n if (effectiveCanvasMode === \"native\") {\n // NATIVE PATH: Use drawElementImage API (~1.76x faster than foreignObject)\n // No DOM serialization, no canvas-to-dataURL encoding, no image loading\n // Direct browser-native rendering\n\n const t0 = performance.now();\n const canvas = await renderToImageNative(renderClone, width, height, {\n skipDprScaling: true, // Use 1x DPR for video export (4x fewer pixels!)\n });\n const renderTime = performance.now() - t0;\n\n logger.debug(\n `[captureFromClone] native render=${renderTime.toFixed(0)}ms (canvasScale=${scale})`,\n );\n\n return canvas;\n } else {\n // FOREIGNOBJECT PATH: Serialize DOM → SVG → Image → Canvas\n // More compatible but slower than native path\n\n // NOTE: seekForRender() has already ensured rendering is complete, including:\n // - Lit updates propagated\n // - All LitElement descendants updated\n // - frameController.renderFrame() called for FrameRenderable elements\n // - Layout stabilization complete\n // No additional RAF wait needed - can serialize immediately\n\n const t0 = performance.now();\n const dataUri = await captureTimelineToDataUri(renderClone, width, height, {\n renderContext,\n canvasScale: scale,\n timeMs,\n });\n const serializeTime = performance.now() - t0;\n\n const t1 = performance.now();\n const image = await loadImageFromDataUri(dataUri);\n const loadTime = performance.now() - t1;\n\n logger.debug(\n `[captureFromClone] foreignObject serialize=${serializeTime.toFixed(0)}ms, load=${loadTime.toFixed(0)}ms (canvasScale=${scale})`,\n );\n\n // Return image directly - no copy needed!\n return image;\n }\n } finally {\n // Ensure RenderContext is disposed even if an error occurs\n renderContext.dispose();\n }\n}\n\n/**\n * Captures a single frame from a timegroup at a specific time.\n *\n * CLONE-TIMELINE ARCHITECTURE:\n * Creates an independent render clone, seeks it to the target time, and captures.\n * Prime-timeline is NEVER seeked - user can continue previewing/editing during capture.\n *\n * @param timegroup - The source timegroup\n * @param options - Capture options including timeMs, scale, contentReadyMode\n * @returns Canvas with the rendered frame\n * @throws ContentNotReadyError if blocking mode times out waiting for video content\n */\nexport async function captureTimegroupAtTime(\n timegroup: EFTimegroup,\n options: CaptureOptions,\n): Promise<CanvasImageSource> {\n const {\n timeMs,\n scale = DEFAULT_CAPTURE_SCALE,\n // skipRestore is deprecated with Clone-timeline (Prime is never seeked)\n contentReadyMode = \"immediate\",\n blockingTimeoutMs = DEFAULT_BLOCKING_TIMEOUT_MS,\n canvasMode,\n skipClone = false,\n } = options;\n\n if (skipClone) {\n // DIRECT RENDERING: Skip clone creation for headless server rendering\n // Seek prime timeline directly and capture from it\n // WARNING: This modifies the prime timeline! Only use in headless contexts.\n\n const seekStart = performance.now();\n await timegroup.seekForRender(timeMs);\n const seekMs = performance.now() - seekStart;\n\n const renderStart = performance.now();\n // Use timegroup's actual container (parentElement or document.body as fallback)\n const container = (timegroup.parentElement || document.body) as HTMLElement;\n const result = await captureFromClone(timegroup, container, {\n scale,\n contentReadyMode,\n blockingTimeoutMs,\n originalTimegroup: undefined, // No original since we're rendering the prime\n canvasMode,\n timeMs, // Pass explicit time since we're not using a clone\n });\n const renderMs = performance.now() - renderStart;\n\n // Store timing (no clone time since we skipped it)\n if (typeof result === \"object\" && result !== null) {\n (result as any).__perfTiming = { cloneMs: 0, seekMs, renderMs };\n }\n\n return result;\n }\n\n // CLONE-TIMELINE: Create a short-lived render clone for this capture\n // Prime-timeline is NEVER seeked - clone is fully independent\n const cloneStart = performance.now();\n const {\n clone: renderClone,\n container: renderContainer,\n cleanup: cleanupRenderClone,\n } = await timegroup.createRenderClone();\n const cloneMs = performance.now() - cloneStart;\n\n try {\n // Seek the clone to target time (Prime stays at user position)\n // Use seekForRender which bypasses duration clamping - render clones may have\n // zero duration initially until media durations are computed, but we still\n // want to seek to the requested time for capture purposes.\n const seekStart = performance.now();\n await renderClone.seekForRender(timeMs);\n const seekMs = performance.now() - seekStart;\n\n // Use the shared capture helper\n const renderStart = performance.now();\n const result = await captureFromClone(renderClone, renderContainer, {\n scale,\n contentReadyMode,\n blockingTimeoutMs,\n originalTimegroup: timegroup,\n canvasMode,\n });\n const renderMs = performance.now() - renderStart;\n\n // Store timing on the result for access by callers (if they need it)\n // Note: CanvasImageSource doesn't support custom properties, but we can attach them anyway\n if (typeof result === \"object\" && result !== null) {\n (result as any).__perfTiming = { cloneMs, seekMs, renderMs };\n }\n\n return result;\n } finally {\n // Clean up the render clone\n cleanupRenderClone();\n }\n}\n\n/**\n * Generate thumbnails using an existing render clone and mutable queue.\n * The queue can be modified while generation is in progress.\n *\n * @param renderClone - Pre-created render clone to use\n * @param renderContainer - Container for the render clone\n * @param queue - Mutable queue that provides timestamps\n * @param options - Capture options (scale, contentReadyMode, etc.)\n * @yields Objects with { timeMs, canvas } for each captured thumbnail\n *\n * @example\n * ```ts\n * const queue = new MutableTimestampQueue();\n * queue.reset([0, 100, 200]);\n *\n * for await (const { timeMs, canvas } of generateThumbnailsFromClone(clone, container, queue)) {\n * cache.set(timeMs, canvas);\n * // Queue can be modified here while generator continues\n * }\n * ```\n */\nexport async function* generateThumbnailsFromClone(\n renderClone: EFTimegroup,\n renderContainer: HTMLElement,\n queue: ThumbnailQueue,\n options: GenerateThumbnailsOptions = {},\n): AsyncGenerator<GeneratedThumbnail> {\n const {\n scale = DEFAULT_CAPTURE_SCALE,\n contentReadyMode = \"immediate\",\n blockingTimeoutMs = DEFAULT_BLOCKING_TIMEOUT_MS,\n signal,\n } = options;\n\n while (true) {\n // Check if aborted before starting work\n if (signal?.aborted) {\n break;\n }\n\n const timeMs = queue.shift();\n if (timeMs === undefined) {\n // Queue is empty, generator exits\n break;\n }\n\n // Seek the clone to the target time\n await renderClone.seekForRender(timeMs);\n\n // Check if aborted after seek (before expensive capture)\n if (signal?.aborted) {\n break;\n }\n\n // Capture from the seeked clone, passing explicit timeMs\n const canvas = await captureFromClone(renderClone, renderContainer, {\n scale,\n contentReadyMode,\n blockingTimeoutMs,\n timeMs, // CRITICAL: Pass explicit time for accurate temporal visibility\n });\n\n // Yield the result with explicit timestamp association\n yield { timeMs, canvas };\n }\n}\n\n/**\n * Generate thumbnails for multiple timestamps efficiently using a single render clone.\n * This avoids the overhead of creating/destroying a clone for each thumbnail.\n *\n * @param timegroup - The timegroup to capture\n * @param timestamps - Array of timestamps to capture (in milliseconds)\n * @param options - Capture options (scale, contentReadyMode, etc.)\n * @param signal - Optional AbortSignal to cancel generation\n * @yields Objects with { timeMs, canvas } for each captured thumbnail\n *\n * @example\n * ```ts\n * for await (const { timeMs, canvas } of generateThumbnails(tg, [0, 100, 200])) {\n * console.log(`Got thumbnail for ${timeMs}ms`);\n * thumbnailCache.set(timeMs, canvas);\n * }\n * ```\n */\nexport async function* generateThumbnails(\n timegroup: EFTimegroup,\n timestamps: number[],\n options: GenerateThumbnailsOptions = {},\n signal?: AbortSignal,\n): AsyncGenerator<GeneratedThumbnail> {\n const {\n scale = DEFAULT_CAPTURE_SCALE,\n contentReadyMode = \"immediate\",\n blockingTimeoutMs = DEFAULT_BLOCKING_TIMEOUT_MS,\n } = options;\n\n // Create a single render clone for all thumbnails\n const {\n clone: renderClone,\n container: renderContainer,\n cleanup: cleanupRenderClone,\n } = await timegroup.createRenderClone();\n\n try {\n for (const timeMs of timestamps) {\n // Check for abort before each capture\n signal?.throwIfAborted();\n\n // Seek the clone to the target time\n await renderClone.seekForRender(timeMs);\n\n // Capture from the seeked clone\n const canvas = await captureFromClone(renderClone, renderContainer, {\n scale,\n contentReadyMode,\n blockingTimeoutMs,\n originalTimegroup: timegroup,\n });\n\n // Yield the result with explicit timestamp association\n yield { timeMs, canvas };\n }\n } finally {\n // Always clean up the render clone\n cleanupRenderClone();\n }\n}\n\n/** Epsilon for comparing time values (ms) - times within this are considered equal */\nconst TIME_EPSILON_MS = 1;\n\n/** Default scale for preview rendering */\nconst DEFAULT_PREVIEW_SCALE = 1;\n\n/** Default resolution scale (full resolution) */\nconst DEFAULT_RESOLUTION_SCALE = 1;\n\n/**\n * Convert relative time to absolute time for a timegroup.\n * Nested timegroup children have ABSOLUTE startTimeMs values,\n * so relative capture times must be converted for temporal culling.\n */\nfunction toAbsoluteTime(timegroup: EFTimegroup, relativeTimeMs: number): number {\n return relativeTimeMs + (timegroup.startTimeMs ?? 0);\n}\n\n/**\n * Renders a timegroup preview to a canvas using SVG foreignObject.\n *\n * Captures the prime timeline's current visual state including DOM changes\n * from frame tasks (SVG paths, canvas content, text updates, etc.).\n *\n * Optimized with:\n * - Passive clone structure rebuilt each frame from prime's current state\n * - Temporal bucketing for time-based culling\n * - RenderContext for canvas pixel caching across frames\n * - Resolution scaling for performance (renders at lower resolution, CSS upscales)\n *\n * @param timegroup - The source timegroup to preview (prime timeline)\n * @param scaleOrOptions - Scale factor (default 1) or options object\n * @returns Object with canvas and refresh function\n */\nexport function renderTimegroupToCanvas(\n timegroup: EFTimegroup,\n scaleOrOptions: number | CanvasPreviewOptions = DEFAULT_PREVIEW_SCALE,\n): CanvasPreviewResult {\n // Normalize options\n const options: CanvasPreviewOptions =\n typeof scaleOrOptions === \"number\" ? { scale: scaleOrOptions } : scaleOrOptions;\n\n const scale = options.scale ?? DEFAULT_PREVIEW_SCALE;\n // These are mutable to support dynamic resolution changes\n let currentResolutionScale = options.resolutionScale ?? DEFAULT_RESOLUTION_SCALE;\n\n const width = timegroup.offsetWidth || DEFAULT_WIDTH;\n const height = timegroup.offsetHeight || DEFAULT_HEIGHT;\n const dpr = (typeof window !== \"undefined\" ? window.devicePixelRatio : 1) || 1;\n\n // Calculate effective render dimensions (internal resolution) - mutable\n let renderWidth = Math.floor(width * currentResolutionScale);\n let renderHeight = Math.floor(height * currentResolutionScale);\n\n // Create canvas with proper DPR handling\n const canvas = createDprCanvas({\n renderWidth,\n renderHeight,\n scale,\n fullWidth: width,\n fullHeight: height,\n dpr,\n });\n\n // Return canvas directly - no wrapper needed\n const wrapperContainer = canvas;\n\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) {\n throw new Error(\"Failed to get canvas 2d context\");\n }\n\n // Track render state\n let rendering = false;\n let lastTimeMs = -1;\n let disposed = false;\n\n // Invalidate lastTimeMs when composition structure or attributes change so\n // refresh() re-renders even when currentTimeMs hasn't changed (e.g. paused edits).\n const compositionObserver = new MutationObserver(() => {\n if (!rendering) lastTimeMs = -1;\n });\n compositionObserver.observe(timegroup, {\n attributes: true,\n childList: true,\n subtree: true,\n });\n\n // Create RenderContext for caching across refresh calls (foreignObject only)\n const renderContext = new RenderContext();\n\n // Create FrameController for coordinating element rendering\n // Cached for the lifetime of this preview instance\n const frameController = new FrameController(timegroup);\n\n // Log resolution scale on first render for debugging\n let hasLoggedScale = false;\n\n // Pending resolution change - applied at start of next refresh to avoid blanking\n let pendingResolutionScale: number | null = null;\n\n // Use the user's render mode preference. Native requires the timegroup to be\n // inside a <canvas layoutsubtree> for drawElementImage to work.\n const useNative = getRenderMode() === \"native\" && isNativeCanvasApiAvailable();\n let captureCanvas: HTMLCanvasElement | null = null;\n let captureCtx: HtmlInCanvasContext | null = null;\n let originalParent: ParentNode | null = null;\n let originalNextSibling: ChildNode | null = null;\n let savedClipPath = \"\";\n let savedPointerEvents = \"\";\n\n if (useNative) {\n captureCanvas = document.createElement(\"canvas\");\n captureCanvas.setAttribute(\"layoutsubtree\", \"\");\n (captureCanvas as HtmlInCanvasElement).layoutSubtree = true;\n captureCanvas.width = renderWidth;\n captureCanvas.height = renderHeight;\n captureCanvas.style.cssText = `position:fixed;left:0;top:0;width:${width}px;height:${height}px;opacity:0;pointer-events:none;z-index:-9999;`;\n originalParent = timegroup.parentNode;\n originalNextSibling = timegroup.nextSibling;\n savedClipPath = timegroup.style.clipPath;\n savedPointerEvents = timegroup.style.pointerEvents;\n timegroup.style.clipPath = \"\";\n timegroup.style.pointerEvents = \"\";\n captureCanvas.appendChild(timegroup);\n document.body.appendChild(captureCanvas);\n captureCtx = captureCanvas.getContext(\"2d\") as HtmlInCanvasContext;\n void captureCanvas.offsetHeight;\n void timegroup.offsetHeight;\n }\n\n /**\n * Apply pending resolution scale changes.\n * Called at the start of refresh() before rendering, so the old content\n * stays visible until new content is ready to be drawn.\n */\n const applyPendingResolutionChange = (): void => {\n if (pendingResolutionScale === null) return;\n\n const newScale = pendingResolutionScale;\n pendingResolutionScale = null;\n\n currentResolutionScale = newScale;\n renderWidth = Math.floor(width * currentResolutionScale);\n renderHeight = Math.floor(height * currentResolutionScale);\n\n if (captureCanvas) {\n captureCanvas.width = renderWidth;\n captureCanvas.height = renderHeight;\n }\n };\n\n /**\n * Dynamically change resolution scale without rebuilding clone structure.\n * The actual change is deferred until next refresh() to avoid blanking -\n * old content stays visible until new content is ready.\n */\n const setResolutionScale = (newScale: number): void => {\n // Clamp to valid range\n newScale = Math.max(0.1, Math.min(1, newScale));\n\n if (newScale === currentResolutionScale && pendingResolutionScale === null) return;\n\n // Queue the change - will be applied at start of next refresh\n pendingResolutionScale = newScale;\n\n // Force re-render on next refresh by invalidating lastTimeMs\n lastTimeMs = -1;\n };\n\n const getResolutionScale = (): number => pendingResolutionScale ?? currentResolutionScale;\n\n const refresh = async (): Promise<void> => {\n if (disposed) return;\n\n const sourceTimeMs = timegroup.currentTimeMs ?? 0;\n const userTimeMs = timegroup.userTimeMs ?? 0;\n\n if (Math.abs(sourceTimeMs - userTimeMs) > TIME_EPSILON_MS) return;\n if (userTimeMs === lastTimeMs) return;\n if (rendering) return;\n\n lastTimeMs = userTimeMs;\n rendering = true;\n\n applyPendingResolutionChange();\n\n if (!hasLoggedScale) {\n hasLoggedScale = true;\n const mode = useNative ? \"native\" : \"foreignObject\";\n logger.debug(\n `[renderTimegroupToCanvas] Resolution scale: ${currentResolutionScale} (${width}x${height} → ${renderWidth}x${renderHeight}), canvas buffer: ${canvas.width}x${canvas.height}, CSS size: ${canvas.style.width}x${canvas.style.height}, renderMode: ${mode}`,\n );\n }\n\n try {\n await frameController.renderFrame(userTimeMs, {\n waitForLitUpdate: false,\n onAnimationsUpdate: (root) => {\n updateAnimations(root as AnimatableElement);\n },\n });\n\n if (useNative && captureCanvas && captureCtx) {\n if (captureCanvas.width !== width || captureCanvas.height !== height) {\n captureCtx.save();\n captureCtx.scale(captureCanvas.width / width, captureCanvas.height / height);\n captureCtx.drawElementImage(timegroup, 0, 0);\n captureCtx.restore();\n } else {\n captureCtx.drawElementImage(timegroup, 0, 0);\n }\n const targetWidth = Math.floor(renderWidth * scale * dpr);\n const targetHeight = Math.floor(renderHeight * scale * dpr);\n if (canvas.width !== targetWidth || canvas.height !== targetHeight) {\n canvas.width = targetWidth;\n canvas.height = targetHeight;\n } else {\n ctx.clearRect(0, 0, canvas.width, canvas.height);\n }\n ctx.drawImage(captureCanvas, 0, 0, canvas.width, canvas.height);\n\n defaultProfiler.incrementRenderCount();\n } else {\n const absoluteTimeMs = toAbsoluteTime(timegroup, userTimeMs);\n\n const dataUri = await captureTimelineToDataUri(timegroup, width, height, {\n renderContext,\n canvasScale: currentResolutionScale,\n timeMs: absoluteTimeMs,\n });\n const image = await loadImageFromDataUri(dataUri);\n\n const targetWidth = Math.floor(renderWidth * scale * dpr);\n const targetHeight = Math.floor(renderHeight * scale * dpr);\n if (canvas.width !== targetWidth || canvas.height !== targetHeight) {\n canvas.width = targetWidth;\n canvas.height = targetHeight;\n } else {\n ctx.clearRect(0, 0, canvas.width, canvas.height);\n }\n\n ctx.save();\n ctx.scale(dpr * scale, dpr * scale);\n ctx.drawImage(image, 0, 0, renderWidth, renderHeight);\n ctx.restore();\n\n defaultProfiler.incrementRenderCount();\n }\n } catch (e) {\n logger.error(\"Canvas preview render failed:\", e);\n } finally {\n rendering = false;\n }\n };\n\n /**\n * Dispose the preview and release resources.\n */\n const dispose = (): void => {\n if (disposed) return;\n disposed = true;\n compositionObserver.disconnect();\n frameController.abort();\n renderContext.dispose();\n\n // Restore timegroup to original DOM position if native mode moved it\n if (useNative && originalParent) {\n timegroup.style.clipPath = savedClipPath;\n timegroup.style.pointerEvents = savedPointerEvents;\n if (originalNextSibling) {\n originalParent.insertBefore(timegroup, originalNextSibling);\n } else {\n originalParent.appendChild(timegroup);\n }\n captureCanvas?.remove();\n }\n };\n\n // Do initial render\n refresh();\n\n return {\n container: wrapperContainer,\n canvas,\n refresh,\n setResolutionScale,\n getResolutionScale,\n dispose,\n };\n}\n"],"mappings":"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"}
|
|
@@ -311,7 +311,7 @@ async function renderTimegroupToVideo(timegroup, options = {}) {
|
|
|
311
311
|
try {
|
|
312
312
|
const audioBuffer = await timegroup.renderAudio(lastRenderedAudioEndMs, chunkEndMs, signal);
|
|
313
313
|
if (audioBuffer && audioBuffer.length > 0) await audioSource.add(audioBuffer);
|
|
314
|
-
} catch (
|
|
314
|
+
} catch (_e) {}
|
|
315
315
|
lastRenderedAudioEndMs = chunkEndMs;
|
|
316
316
|
}
|
|
317
317
|
if (videoSource && output && encodingCtx) {
|
|
@@ -342,7 +342,7 @@ async function renderTimegroupToVideo(timegroup, options = {}) {
|
|
|
342
342
|
if (audioSource && lastRenderedAudioEndMs < config.endMs) try {
|
|
343
343
|
const audioBuffer = await timegroup.renderAudio(lastRenderedAudioEndMs, config.endMs, signal);
|
|
344
344
|
if (audioBuffer && audioBuffer.length > 0) await audioSource.add(audioBuffer);
|
|
345
|
-
} catch (
|
|
345
|
+
} catch (_e) {}
|
|
346
346
|
if (config.benchmarkMode) return;
|
|
347
347
|
await output.finalize();
|
|
348
348
|
if (typeof __EF_TELEMETRY_ENABLED__ !== "undefined" && __EF_TELEMETRY_ENABLED__) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"renderTimegroupToVideo.js","names":["timestamps: number[]","output: Output | null","videoSource: CanvasSource | null","audioSource: AudioBufferSource | null","target: BufferTarget | StreamTarget | null","fileStream: {\n writable: WritableStream<Uint8Array>;\n close: () => Promise<void>;\n } | null","encodingCanvas: OffscreenCanvas | null","encodingCtx: OffscreenCanvasRenderingContext2D | null","videoConfig: VideoEncodingConfig","thumbCanvas: HTMLCanvasElement | null","thumbCtx: CanvasRenderingContext2D | null","pendingFrames: PendingFrame[]","entry: PendingFrame","image","image: HTMLImageElement","headers: Record<string, string>"],"sources":["../../src/preview/renderTimegroupToVideo.ts"],"sourcesContent":["/**\n * Video rendering for timegroups using direct serialization.\n *\n * Architecture:\n * - Creates a render clone of the timeline\n * - For each frame:\n * 1. Seeks the clone to the target time\n * 2. Executes frame tasks (SVG updates, canvas draws, etc.)\n * 3. Serializes the live DOM directly to SVG+foreignObject data URI\n * 4. Renders to image and encodes to video\n *\n * RenderContext provides pixel caching across frames for performance.\n */\n\nimport { logger } from \"./logger.js\";\nimport {\n Output,\n Mp4OutputFormat,\n BufferTarget,\n StreamTarget,\n CanvasSource,\n AudioBufferSource,\n QUALITY_HIGH,\n canEncodeAudio,\n getEncodableAudioCodecs,\n type VideoEncodingConfig,\n type AudioEncodingConfig,\n type AudioCodec,\n} from \"mediabunny\";\nimport type { EFTimegroup } from \"../elements/EFTimegroup.js\";\nimport type { RenderToVideoOptions } from \"./renderTimegroupToVideo.types.js\";\nimport type { ContentReadyMode } from \"./renderTimegroupToCanvas.types.js\";\nimport {\n resetRenderState,\n waitForVideoContent,\n} from \"./renderTimegroupToCanvas.js\";\nimport { captureTimelineToDataUri } from \"./rendering/serializeTimelineDirect.js\";\nimport { renderToImageNative } from \"./rendering/renderToImageNative.js\";\nimport { isNativeCanvasApiAvailable } from \"./previewSettings.js\";\nimport { createPreviewContainer } from \"./previewTypes.js\";\nimport { RenderContext } from \"./RenderContext.js\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\n// Re-export types from type-only module (zero side effects)\nexport type {\n RenderProgress,\n RenderToVideoOptions,\n} from \"./renderTimegroupToVideo.types.js\";\n\n// ============================================================================\n// Errors\n// ============================================================================\n\nexport class NoSupportedAudioCodecError extends Error {\n constructor(requestedCodecs: AudioCodec[], availableCodecs: AudioCodec[]) {\n super(\n `No supported audio codec found. Requested: [${requestedCodecs.join(\", \")}], ` +\n `Available: [${availableCodecs.length > 0 ? availableCodecs.join(\", \") : \"none\"}]`,\n );\n this.name = \"NoSupportedAudioCodecError\";\n }\n}\n\nexport class RenderCancelledError extends Error {\n constructor() {\n super(\"Render cancelled\");\n this.name = \"RenderCancelledError\";\n }\n}\n\n// ============================================================================\n// Configuration\n// ============================================================================\n\ninterface ResolvedConfig {\n fps: number;\n codec: \"avc\" | \"hevc\" | \"vp9\" | \"av1\" | \"vp8\";\n bitrate: number;\n filename: string;\n scale: number;\n keyFrameInterval: number;\n startMs: number;\n endMs: number;\n renderDurationMs: number;\n width: number;\n height: number;\n videoWidth: number;\n videoHeight: number;\n totalFrames: number;\n frameDurationMs: number;\n frameDurationS: number;\n streaming: boolean;\n includeAudio: boolean;\n audioBitrate: number;\n contentReadyMode: ContentReadyMode;\n blockingTimeoutMs: number;\n returnBuffer: boolean;\n preferredAudioCodecs: AudioCodec[];\n benchmarkMode: boolean;\n progressPreviewInterval: number;\n canvasMode: \"native\" | \"foreignObject\";\n}\n\nfunction resolveConfig(\n timegroup: EFTimegroup,\n options: RenderToVideoOptions = {},\n): ResolvedConfig {\n const fps = options.fps ?? timegroup.effectiveFps ?? 30;\n const codec = options.codec ?? \"avc\";\n const bitrate = options.bitrate ?? 8_000_000;\n const filename = options.filename ?? \"timegroup-video.mp4\";\n const scale = options.scale ?? 1;\n const keyFrameInterval = options.keyFrameInterval ?? 2;\n const streaming = options.streaming ?? true;\n const includeAudio = options.includeAudio ?? true;\n const audioBitrate = options.audioBitrate ?? 128_000;\n const contentReadyMode = options.contentReadyMode ?? \"blocking\";\n const blockingTimeoutMs = options.blockingTimeoutMs ?? 5000;\n const returnBuffer = options.returnBuffer ?? false;\n const preferredAudioCodecs = options.preferredAudioCodecs ?? [\"aac\", \"opus\"];\n const benchmarkMode = options.benchmarkMode ?? false;\n // Preview generation now uses canvas reference (no encoding) - cheap to enable!\n // Defaults to 60 frames (every 2 seconds at 30fps). Set to 0 to disable.\n const progressPreviewInterval = options.progressPreviewInterval ?? 60;\n\n const totalDurationMs = timegroup.durationMs;\n if (!totalDurationMs || totalDurationMs <= 0) {\n throw new Error(\"Timegroup has no duration\");\n }\n\n const startMs = Math.max(0, options.fromMs ?? 0);\n const endMs =\n options.toMs !== undefined\n ? Math.min(options.toMs, totalDurationMs)\n : totalDurationMs;\n const renderDurationMs = endMs - startMs;\n\n if (renderDurationMs <= 0) {\n throw new Error(`Invalid render range: from ${startMs}ms to ${endMs}ms`);\n }\n\n // Force layout reflow before reading dimensions\n void timegroup.offsetHeight;\n\n // Try multiple sources for dimensions (offsetWidth can be 0 in headless browsers)\n let timegroupWidth = timegroup.offsetWidth;\n let timegroupHeight = timegroup.offsetHeight;\n\n if (!timegroupWidth || !timegroupHeight) {\n const rect = timegroup.getBoundingClientRect();\n if (rect.width > 0 && rect.height > 0) {\n timegroupWidth = rect.width;\n timegroupHeight = rect.height;\n }\n }\n\n if (!timegroupWidth || !timegroupHeight) {\n const computed = getComputedStyle(timegroup);\n const cw = parseFloat(computed.width);\n const ch = parseFloat(computed.height);\n if (cw > 0 && ch > 0) {\n timegroupWidth = cw;\n timegroupHeight = ch;\n }\n }\n\n if (!timegroupWidth || !timegroupHeight) {\n throw new Error(\n `Timegroup has no dimensions (${timegroupWidth}x${timegroupHeight}). ` +\n `Ensure the timegroup element is in the document and has explicit width/height styles ` +\n `(e.g., class=\"w-[1920px] h-[1080px]\")`,\n );\n }\n const width = Math.floor(timegroupWidth * scale);\n const height = Math.floor(timegroupHeight * scale);\n\n const videoWidth = width % 2 === 0 ? width : width - 1;\n const videoHeight = height % 2 === 0 ? height : height - 1;\n\n const frameDurationMs = 1000 / fps;\n const totalFrames = Math.ceil(renderDurationMs / frameDurationMs);\n const frameDurationS = frameDurationMs / 1000;\n\n // Determine effective canvas mode:\n // 1. If explicitly specified, use that (with fallback if native not available)\n // 2. If not specified, default to foreignObject for compatibility\n const canvasMode = (() => {\n const requested = options.canvasMode;\n if (!requested) return \"foreignObject\";\n if (requested === \"native\" && !isNativeCanvasApiAvailable()) {\n logger.debug(\n \"[renderTimegroupToVideo] Native canvas mode requested but not available, falling back to foreignObject\",\n );\n return \"foreignObject\";\n }\n return requested;\n })();\n\n return {\n fps,\n codec,\n bitrate,\n filename,\n scale,\n keyFrameInterval,\n startMs,\n endMs,\n renderDurationMs,\n width,\n height,\n videoWidth,\n videoHeight,\n totalFrames,\n frameDurationMs,\n frameDurationS,\n streaming,\n includeAudio,\n audioBitrate,\n contentReadyMode,\n blockingTimeoutMs,\n returnBuffer,\n preferredAudioCodecs,\n benchmarkMode,\n progressPreviewInterval,\n canvasMode,\n };\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nfunction isFileSystemAccessSupported(): boolean {\n return typeof window !== \"undefined\" && \"showSaveFilePicker\" in window;\n}\n\nasync function getFileWritableStream(filename: string): Promise<{\n writable: WritableStream<Uint8Array>;\n close: () => Promise<void>;\n} | null> {\n if (!isFileSystemAccessSupported()) {\n return null;\n }\n\n try {\n const fileHandle = await (window as any).showSaveFilePicker({\n suggestedName: filename,\n types: [{ description: \"MP4 Video\", accept: { \"video/mp4\": [\".mp4\"] } }],\n });\n const writable = await fileHandle.createWritable();\n return {\n writable,\n close: async () => {\n await writable.close();\n },\n };\n } catch (e) {\n if ((e as Error).name !== \"AbortError\") {\n logger.warn(\"[renderToVideo] File System Access failed:\", e);\n }\n return null;\n }\n}\n\nasync function selectAudioCodec(\n preferredCodecs: AudioCodec[],\n encodingOptions: {\n numberOfChannels: number;\n sampleRate: number;\n bitrate: number;\n },\n): Promise<AudioCodec> {\n for (const codec of preferredCodecs) {\n try {\n const isSupported = await canEncodeAudio(codec, encodingOptions);\n if (isSupported) return codec;\n } catch (e) {\n logger.warn(`[selectAudioCodec] Check failed for ${codec}:`, e);\n }\n }\n const availableCodecs = await getEncodableAudioCodecs(\n undefined,\n encodingOptions,\n );\n throw new NoSupportedAudioCodecError(preferredCodecs, availableCodecs);\n}\n\nfunction downloadBlob(blob: Blob, filename: string): void {\n const url = URL.createObjectURL(blob);\n const a = document.createElement(\"a\");\n a.href = url;\n a.download = filename;\n document.body.appendChild(a);\n a.click();\n document.body.removeChild(a);\n URL.revokeObjectURL(url);\n}\n\n// ============================================================================\n// Public API\n// ============================================================================\n\nexport async function getSupportedAudioCodecs(options?: {\n numberOfChannels?: number;\n sampleRate?: number;\n bitrate?: number;\n}): Promise<AudioCodec[]> {\n const {\n numberOfChannels = 2,\n sampleRate = 48000,\n bitrate = 128000,\n } = options ?? {};\n return getEncodableAudioCodecs(undefined, {\n numberOfChannels,\n sampleRate,\n bitrate,\n });\n}\n\n/**\n * Renders a timegroup to an MP4 video file.\n *\n * Uses the EXACT same code path as thumbnail generation (captureFromClone).\n * This ensures consistency - if thumbnails work, video export works.\n */\nexport async function renderTimegroupToVideo(\n timegroup: EFTimegroup,\n options: RenderToVideoOptions = {},\n): Promise<Uint8Array | undefined> {\n const config = resolveConfig(timegroup, options);\n const { signal, onProgress } = options;\n\n const checkCancelled = () => {\n if (signal?.aborted) throw new RenderCancelledError();\n };\n\n resetRenderState();\n\n // =========================================================================\n // Create render clone - EXACT same as captureBatch in EFTimegroup\n // =========================================================================\n const {\n clone: renderClone,\n container: cloneContainer,\n cleanup: cleanupRenderClone,\n } = await timegroup.createRenderClone();\n\n // Build timestamps array for frame loop\n const timestamps: number[] = [];\n for (let i = 0; i < config.totalFrames; i++) {\n timestamps.push(config.startMs + i * config.frameDurationMs);\n }\n\n // =========================================================================\n // Set up video encoding\n // =========================================================================\n let output: Output | null = null;\n let videoSource: CanvasSource | null = null;\n let audioSource: AudioBufferSource | null = null;\n let target: BufferTarget | StreamTarget | null = null;\n let fileStream: {\n writable: WritableStream<Uint8Array>;\n close: () => Promise<void>;\n } | null = null;\n let useStreaming = false;\n let encodingCanvas: OffscreenCanvas | null = null;\n let encodingCtx: OffscreenCanvasRenderingContext2D | null = null;\n\n if (!config.benchmarkMode) {\n // Check for custom writable stream first (for programmatic streaming)\n if (options.customWritableStream) {\n target = new StreamTarget(options.customWritableStream as any);\n output = new Output({\n format: new Mp4OutputFormat({ fastStart: \"fragmented\" }),\n target,\n });\n useStreaming = true;\n } else if (config.streaming) {\n fileStream = await getFileWritableStream(config.filename);\n useStreaming = fileStream !== null;\n\n if (useStreaming && fileStream) {\n target = new StreamTarget(fileStream.writable as any);\n output = new Output({\n format: new Mp4OutputFormat({ fastStart: \"fragmented\" }),\n target,\n });\n }\n }\n\n if (!target) {\n target = new BufferTarget();\n output = new Output({ format: new Mp4OutputFormat(), target });\n }\n\n encodingCanvas = new OffscreenCanvas(config.videoWidth, config.videoHeight);\n encodingCtx = encodingCanvas.getContext(\"2d\");\n if (!encodingCtx) {\n cleanupRenderClone();\n throw new Error(\"Failed to get encoding canvas context\");\n }\n\n if (!output) {\n throw new Error(\"Output not initialized\");\n }\n\n const videoConfig: VideoEncodingConfig = {\n codec: config.codec,\n bitrate: config.bitrate,\n keyFrameInterval: config.keyFrameInterval,\n };\n videoSource = new CanvasSource(encodingCanvas, videoConfig);\n output.addVideoTrack(videoSource);\n\n if (config.includeAudio) {\n const selectedCodec = await selectAudioCodec(\n config.preferredAudioCodecs,\n {\n numberOfChannels: 2,\n sampleRate: 48000,\n bitrate: config.audioBitrate,\n },\n );\n const audioConfig: AudioEncodingConfig = {\n codec: selectedCodec,\n bitrate: config.audioBitrate,\n };\n audioSource = new AudioBufferSource(audioConfig);\n output.addAudioTrack(audioSource);\n }\n\n await output.start();\n }\n\n // =========================================================================\n // Setup for per-frame passive structure rebuilding (like live preview)\n // =========================================================================\n // Create RenderContext for caching across all frames\n const renderContext = new RenderContext();\n\n // Create preview container with proper styling (reusable, content rebuilt each frame)\n // Use unscaled dimensions for the preview container (which holds the full-size clone)\n const containerWidth = timegroup.offsetWidth || 1920;\n const containerHeight = timegroup.offsetHeight || 1080;\n const previewContainer = createPreviewContainer({\n width: containerWidth,\n height: containerHeight,\n background: getComputedStyle(timegroup).background || \"#000\",\n });\n\n // Setup for direct serialization\n logger.debug(`[renderTimegroupToVideo] Using direct timeline serialization`);\n\n // Attach clone container (keeps renderClone in its React-managed DOM position)\n previewContainer.appendChild(cloneContainer);\n\n // Add ef-render-clone-container class for CSS selectors and debugging\n previewContainer.classList.add(\"ef-render-clone-container\");\n\n // CRITICAL: Attach container to document so getComputedStyle returns actual values\n // Without this, all computed styles are empty strings!\n // Hide the container OFF-SCREEN but do NOT use visibility:hidden because:\n // 1. visibility:hidden is inherited by all children\n // 2. seekForRender checks getComputedStyle().visibility and skips \"hidden\" subtrees\n // 3. This would cause FrameController to skip rendering all nested content\n previewContainer.style.cssText +=\n \";position:fixed;left:-99999px;top:-99999px;pointer-events:none;\";\n document.body.appendChild(previewContainer);\n\n // Force layout/reflow so getComputedStyle returns correct values\n void renderClone.offsetHeight;\n logger.debug(\n `[renderTimegroupToVideo] Attached previewContainer to document.body (off-screen) for style computation`,\n );\n\n // =========================================================================\n // Frame loop - DEEP PIPELINE: overlap encode + render + prepare\n // =========================================================================\n const renderStartTime = performance.now();\n let lastRenderedAudioEndMs = config.startMs;\n const audioChunkDurationMs = 2000;\n\n // Reusable thumbnail canvas for preview (no encoding, just draw to canvas)\n let thumbCanvas: HTMLCanvasElement | null = null;\n let thumbCtx: CanvasRenderingContext2D | null = null;\n if (onProgress && config.progressPreviewInterval > 0) {\n const previewWidth = 160;\n const previewHeight = Math.round(\n previewWidth * (config.videoHeight / config.videoWidth),\n );\n thumbCanvas = document.createElement(\"canvas\");\n thumbCanvas.width = previewWidth;\n thumbCanvas.height = previewHeight;\n thumbCtx = thumbCanvas.getContext(\"2d\");\n }\n\n try {\n // ========================================================================\n // OVERLAPPED PIPELINE: image loading runs parallel with seek+serialize\n // ========================================================================\n // The clone can only seek one frame at a time, and serialization must\n // capture the DOM before the next seek. But image loading (data URI →\n // Image) is independent of the clone and runs in the background.\n //\n // Per-frame timeline:\n // [seek(N)] → [serialize(N)] → [image.load(N) in background...]\n // └─ [seek(N+1)] → [serialize(N+1)] → ...\n // └─ encode(N) when image resolves\n\n type PendingFrame = {\n frameIndex: number;\n timeMs: number;\n timestampS: number;\n resolved: HTMLImageElement | null;\n promise: Promise<HTMLImageElement>;\n };\n\n const MAX_AHEAD = 2;\n const pendingFrames: PendingFrame[] = [];\n let nextSeekFrame = 0;\n let encodedFrames = 0;\n\n while (encodedFrames < config.totalFrames) {\n checkCancelled();\n\n // ==================================================================\n // PHASE 1: Fill pipeline — seek+serialize ahead while images load\n // ==================================================================\n while (\n nextSeekFrame < config.totalFrames &&\n pendingFrames.length < MAX_AHEAD\n ) {\n const fi = nextSeekFrame;\n const timeMs = timestamps[fi]!;\n const timestampS = (fi * config.frameDurationMs) / 1000;\n\n await renderClone.seekForRender(timeMs);\n\n const entry: PendingFrame = {\n frameIndex: fi,\n timeMs,\n timestampS,\n resolved: null,\n promise: null!,\n };\n\n // Wait for video content if using blocking mode\n if (config.contentReadyMode === \"blocking\") {\n await waitForVideoContent(\n renderClone,\n timeMs,\n config.blockingTimeoutMs,\n );\n }\n\n if (config.canvasMode === \"native\") {\n const canvas = await renderToImageNative(\n renderClone,\n config.width,\n config.height,\n {\n skipDprScaling: true,\n },\n );\n entry.resolved = canvas as any as HTMLImageElement;\n entry.promise = Promise.resolve(entry.resolved);\n } else {\n // Synchronous capture: walks DOM + snapshots canvas pixels.\n // Returns immediately — clone is free for next seek.\n // Encoding (canvas→base64, SVG assembly) and image loading\n // all resolve in the background.\n const dataUriPromise = captureTimelineToDataUri(\n renderClone,\n config.width,\n config.height,\n {\n renderContext,\n canvasScale: config.scale,\n timeMs,\n },\n );\n\n entry.promise = dataUriPromise.then((dataUri) => {\n return new Promise<HTMLImageElement>((resolve, reject) => {\n const image = new Image();\n image.onload = () => {\n entry.resolved = image;\n resolve(image);\n };\n image.onerror = (e) => {\n console.error(`[Render] frame ${fi} image load error:`, e);\n reject(new Error(`Failed to load image from data URI`));\n };\n image.src = dataUri;\n });\n });\n }\n\n pendingFrames.push(entry);\n nextSeekFrame++;\n }\n\n // ==================================================================\n // PHASE 2: Encode next frame in order (await if not yet loaded)\n // ==================================================================\n const head = pendingFrames.shift()!;\n const preloaded = head.resolved !== null;\n let image: HTMLImageElement;\n if (preloaded) {\n image = head.resolved!;\n } else {\n image = await head.promise;\n }\n\n if (\n audioSource &&\n head.timeMs >= lastRenderedAudioEndMs + audioChunkDurationMs\n ) {\n const chunkEndMs = Math.min(\n head.timeMs + audioChunkDurationMs,\n config.endMs,\n );\n try {\n const audioBuffer = await timegroup.renderAudio(\n lastRenderedAudioEndMs,\n chunkEndMs,\n signal,\n );\n if (audioBuffer && audioBuffer.length > 0) {\n await audioSource.add(audioBuffer);\n }\n } catch (e) {\n /* Audio render failures are non-fatal */\n }\n lastRenderedAudioEndMs = chunkEndMs;\n }\n\n if (videoSource && output && encodingCtx) {\n encodingCtx.drawImage(\n image,\n 0,\n 0,\n image.width,\n image.height,\n 0,\n 0,\n config.videoWidth,\n config.videoHeight,\n );\n await videoSource.add(head.timestampS, config.frameDurationS);\n }\n\n // ==================================================================\n // Progress reporting\n // ==================================================================\n encodedFrames++;\n const currentFrame = encodedFrames;\n const progress = currentFrame / config.totalFrames;\n const renderedMs = currentFrame * config.frameDurationMs;\n const elapsedMs = performance.now() - renderStartTime;\n const msPerFrame = elapsedMs / currentFrame;\n const remainingFrames = config.totalFrames - currentFrame;\n const estimatedRemainingMs = remainingFrames * msPerFrame;\n const speedMultiplier = renderedMs / elapsedMs;\n\n if (\n thumbCanvas &&\n thumbCtx &&\n head.frameIndex % config.progressPreviewInterval === 0\n ) {\n thumbCtx.drawImage(image, 0, 0, thumbCanvas.width, thumbCanvas.height);\n }\n\n onProgress?.({\n progress,\n currentFrame,\n totalFrames: config.totalFrames,\n renderedMs,\n totalDurationMs: config.renderDurationMs,\n elapsedMs,\n estimatedRemainingMs,\n speedMultiplier,\n framePreviewCanvas: thumbCanvas || undefined,\n });\n }\n\n // Render remaining audio\n if (audioSource && lastRenderedAudioEndMs < config.endMs) {\n try {\n const audioBuffer = await timegroup.renderAudio(\n lastRenderedAudioEndMs,\n config.endMs,\n signal,\n );\n if (audioBuffer && audioBuffer.length > 0) {\n await audioSource.add(audioBuffer);\n }\n } catch (e) {\n /* Audio render failures are non-fatal */\n }\n }\n\n if (config.benchmarkMode) {\n return undefined;\n }\n\n await output!.finalize();\n\n if (\n typeof __EF_TELEMETRY_ENABLED__ !== \"undefined\" &&\n __EF_TELEMETRY_ENABLED__\n ) {\n const elapsedMs = Math.round(performance.now() - renderStartTime);\n const endpoint = options.telemetryEndpoint ?? \"https://editframe.com\";\n const efMediaCount =\n timegroup.querySelectorAll(\"ef-video,ef-audio\").length;\n const efImageCount = timegroup.querySelectorAll(\"ef-image\").length;\n const efCaptionsCount = timegroup.querySelectorAll(\"ef-captions\").length;\n const efTextCount = timegroup.querySelectorAll(\"ef-text\").length;\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n };\n if (options.telemetryToken) {\n headers[\"Authorization\"] = `Bearer ${options.telemetryToken}`;\n }\n fetch(`${endpoint}/api/v1/telemetry`, {\n method: \"POST\",\n headers,\n body: JSON.stringify({\n event_type: \"render\",\n render_path: \"client\",\n duration_ms: elapsedMs,\n width: config.videoWidth,\n height: config.videoHeight,\n fps: config.fps,\n feature_usage: {\n efMediaCount,\n efImageCount,\n efCaptionsCount,\n efTextCount,\n },\n }),\n keepalive: true,\n }).catch(() => {\n // Telemetry errors must never surface to users.\n });\n }\n\n if (useStreaming) {\n // Streaming mode: chunks already sent via customWritableStream or file stream\n return undefined;\n } else {\n const bufferTarget = target as BufferTarget;\n const videoBuffer = bufferTarget.buffer;\n if (!videoBuffer) {\n throw new Error(\"Video encoding failed: no buffer produced\");\n }\n\n if (config.returnBuffer) {\n return new Uint8Array(videoBuffer);\n }\n\n const videoBlob = new Blob([videoBuffer], { type: \"video/mp4\" });\n downloadBlob(videoBlob, config.filename);\n return undefined;\n }\n } finally {\n renderContext.dispose();\n // Remove previewContainer first — renderClone was moved into it, so it must be\n // detached before cleanupRenderClone() unmounts the React root that owns renderClone.\n if (previewContainer.parentNode) {\n previewContainer.parentNode.removeChild(previewContainer);\n }\n cleanupRenderClone();\n }\n}\n\nexport { QUALITY_HIGH };\nexport type { AudioCodec };\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"renderTimegroupToVideo.js","names":["timestamps: number[]","output: Output | null","videoSource: CanvasSource | null","audioSource: AudioBufferSource | null","target: BufferTarget | StreamTarget | null","fileStream: {\n writable: WritableStream<Uint8Array>;\n close: () => Promise<void>;\n } | null","encodingCanvas: OffscreenCanvas | null","encodingCtx: OffscreenCanvasRenderingContext2D | null","videoConfig: VideoEncodingConfig","thumbCanvas: HTMLCanvasElement | null","thumbCtx: CanvasRenderingContext2D | null","pendingFrames: PendingFrame[]","entry: PendingFrame","image","image: HTMLImageElement","headers: Record<string, string>"],"sources":["../../src/preview/renderTimegroupToVideo.ts"],"sourcesContent":["/**\n * Video rendering for timegroups using direct serialization.\n *\n * Architecture:\n * - Creates a render clone of the timeline\n * - For each frame:\n * 1. Seeks the clone to the target time\n * 2. Executes frame tasks (SVG updates, canvas draws, etc.)\n * 3. Serializes the live DOM directly to SVG+foreignObject data URI\n * 4. Renders to image and encodes to video\n *\n * RenderContext provides pixel caching across frames for performance.\n */\n\nimport { logger } from \"./logger.js\";\nimport {\n Output,\n Mp4OutputFormat,\n BufferTarget,\n StreamTarget,\n CanvasSource,\n AudioBufferSource,\n QUALITY_HIGH,\n canEncodeAudio,\n getEncodableAudioCodecs,\n type VideoEncodingConfig,\n type AudioEncodingConfig,\n type AudioCodec,\n} from \"mediabunny\";\nimport type { EFTimegroup } from \"../elements/EFTimegroup.js\";\nimport type { RenderToVideoOptions } from \"./renderTimegroupToVideo.types.js\";\nimport type { ContentReadyMode } from \"./renderTimegroupToCanvas.types.js\";\nimport { resetRenderState, waitForVideoContent } from \"./renderTimegroupToCanvas.js\";\nimport { captureTimelineToDataUri } from \"./rendering/serializeTimelineDirect.js\";\nimport { renderToImageNative } from \"./rendering/renderToImageNative.js\";\nimport { isNativeCanvasApiAvailable } from \"./previewSettings.js\";\nimport { createPreviewContainer } from \"./previewTypes.js\";\nimport { RenderContext } from \"./RenderContext.js\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\n// Re-export types from type-only module (zero side effects)\nexport type { RenderProgress, RenderToVideoOptions } from \"./renderTimegroupToVideo.types.js\";\n\n// ============================================================================\n// Errors\n// ============================================================================\n\nexport class NoSupportedAudioCodecError extends Error {\n constructor(requestedCodecs: AudioCodec[], availableCodecs: AudioCodec[]) {\n super(\n `No supported audio codec found. Requested: [${requestedCodecs.join(\", \")}], ` +\n `Available: [${availableCodecs.length > 0 ? availableCodecs.join(\", \") : \"none\"}]`,\n );\n this.name = \"NoSupportedAudioCodecError\";\n }\n}\n\nexport class RenderCancelledError extends Error {\n constructor() {\n super(\"Render cancelled\");\n this.name = \"RenderCancelledError\";\n }\n}\n\n// ============================================================================\n// Configuration\n// ============================================================================\n\ninterface ResolvedConfig {\n fps: number;\n codec: \"avc\" | \"hevc\" | \"vp9\" | \"av1\" | \"vp8\";\n bitrate: number;\n filename: string;\n scale: number;\n keyFrameInterval: number;\n startMs: number;\n endMs: number;\n renderDurationMs: number;\n width: number;\n height: number;\n videoWidth: number;\n videoHeight: number;\n totalFrames: number;\n frameDurationMs: number;\n frameDurationS: number;\n streaming: boolean;\n includeAudio: boolean;\n audioBitrate: number;\n contentReadyMode: ContentReadyMode;\n blockingTimeoutMs: number;\n returnBuffer: boolean;\n preferredAudioCodecs: AudioCodec[];\n benchmarkMode: boolean;\n progressPreviewInterval: number;\n canvasMode: \"native\" | \"foreignObject\";\n}\n\nfunction resolveConfig(timegroup: EFTimegroup, options: RenderToVideoOptions = {}): ResolvedConfig {\n const fps = options.fps ?? timegroup.effectiveFps ?? 30;\n const codec = options.codec ?? \"avc\";\n const bitrate = options.bitrate ?? 8_000_000;\n const filename = options.filename ?? \"timegroup-video.mp4\";\n const scale = options.scale ?? 1;\n const keyFrameInterval = options.keyFrameInterval ?? 2;\n const streaming = options.streaming ?? true;\n const includeAudio = options.includeAudio ?? true;\n const audioBitrate = options.audioBitrate ?? 128_000;\n const contentReadyMode = options.contentReadyMode ?? \"blocking\";\n const blockingTimeoutMs = options.blockingTimeoutMs ?? 5000;\n const returnBuffer = options.returnBuffer ?? false;\n const preferredAudioCodecs = options.preferredAudioCodecs ?? [\"aac\", \"opus\"];\n const benchmarkMode = options.benchmarkMode ?? false;\n // Preview generation now uses canvas reference (no encoding) - cheap to enable!\n // Defaults to 60 frames (every 2 seconds at 30fps). Set to 0 to disable.\n const progressPreviewInterval = options.progressPreviewInterval ?? 60;\n\n const totalDurationMs = timegroup.durationMs;\n if (!totalDurationMs || totalDurationMs <= 0) {\n throw new Error(\"Timegroup has no duration\");\n }\n\n const startMs = Math.max(0, options.fromMs ?? 0);\n const endMs =\n options.toMs !== undefined ? Math.min(options.toMs, totalDurationMs) : totalDurationMs;\n const renderDurationMs = endMs - startMs;\n\n if (renderDurationMs <= 0) {\n throw new Error(`Invalid render range: from ${startMs}ms to ${endMs}ms`);\n }\n\n // Force layout reflow before reading dimensions\n void timegroup.offsetHeight;\n\n // Try multiple sources for dimensions (offsetWidth can be 0 in headless browsers)\n let timegroupWidth = timegroup.offsetWidth;\n let timegroupHeight = timegroup.offsetHeight;\n\n if (!timegroupWidth || !timegroupHeight) {\n const rect = timegroup.getBoundingClientRect();\n if (rect.width > 0 && rect.height > 0) {\n timegroupWidth = rect.width;\n timegroupHeight = rect.height;\n }\n }\n\n if (!timegroupWidth || !timegroupHeight) {\n const computed = getComputedStyle(timegroup);\n const cw = parseFloat(computed.width);\n const ch = parseFloat(computed.height);\n if (cw > 0 && ch > 0) {\n timegroupWidth = cw;\n timegroupHeight = ch;\n }\n }\n\n if (!timegroupWidth || !timegroupHeight) {\n throw new Error(\n `Timegroup has no dimensions (${timegroupWidth}x${timegroupHeight}). ` +\n `Ensure the timegroup element is in the document and has explicit width/height styles ` +\n `(e.g., class=\"w-[1920px] h-[1080px]\")`,\n );\n }\n const width = Math.floor(timegroupWidth * scale);\n const height = Math.floor(timegroupHeight * scale);\n\n const videoWidth = width % 2 === 0 ? width : width - 1;\n const videoHeight = height % 2 === 0 ? height : height - 1;\n\n const frameDurationMs = 1000 / fps;\n const totalFrames = Math.ceil(renderDurationMs / frameDurationMs);\n const frameDurationS = frameDurationMs / 1000;\n\n // Determine effective canvas mode:\n // 1. If explicitly specified, use that (with fallback if native not available)\n // 2. If not specified, default to foreignObject for compatibility\n const canvasMode = (() => {\n const requested = options.canvasMode;\n if (!requested) return \"foreignObject\";\n if (requested === \"native\" && !isNativeCanvasApiAvailable()) {\n logger.debug(\n \"[renderTimegroupToVideo] Native canvas mode requested but not available, falling back to foreignObject\",\n );\n return \"foreignObject\";\n }\n return requested;\n })();\n\n return {\n fps,\n codec,\n bitrate,\n filename,\n scale,\n keyFrameInterval,\n startMs,\n endMs,\n renderDurationMs,\n width,\n height,\n videoWidth,\n videoHeight,\n totalFrames,\n frameDurationMs,\n frameDurationS,\n streaming,\n includeAudio,\n audioBitrate,\n contentReadyMode,\n blockingTimeoutMs,\n returnBuffer,\n preferredAudioCodecs,\n benchmarkMode,\n progressPreviewInterval,\n canvasMode,\n };\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nfunction isFileSystemAccessSupported(): boolean {\n return typeof window !== \"undefined\" && \"showSaveFilePicker\" in window;\n}\n\nasync function getFileWritableStream(filename: string): Promise<{\n writable: WritableStream<Uint8Array>;\n close: () => Promise<void>;\n} | null> {\n if (!isFileSystemAccessSupported()) {\n return null;\n }\n\n try {\n const fileHandle = await (window as any).showSaveFilePicker({\n suggestedName: filename,\n types: [{ description: \"MP4 Video\", accept: { \"video/mp4\": [\".mp4\"] } }],\n });\n const writable = await fileHandle.createWritable();\n return {\n writable,\n close: async () => {\n await writable.close();\n },\n };\n } catch (e) {\n if ((e as Error).name !== \"AbortError\") {\n logger.warn(\"[renderToVideo] File System Access failed:\", e);\n }\n return null;\n }\n}\n\nasync function selectAudioCodec(\n preferredCodecs: AudioCodec[],\n encodingOptions: {\n numberOfChannels: number;\n sampleRate: number;\n bitrate: number;\n },\n): Promise<AudioCodec> {\n for (const codec of preferredCodecs) {\n try {\n const isSupported = await canEncodeAudio(codec, encodingOptions);\n if (isSupported) return codec;\n } catch (e) {\n logger.warn(`[selectAudioCodec] Check failed for ${codec}:`, e);\n }\n }\n const availableCodecs = await getEncodableAudioCodecs(undefined, encodingOptions);\n throw new NoSupportedAudioCodecError(preferredCodecs, availableCodecs);\n}\n\nfunction downloadBlob(blob: Blob, filename: string): void {\n const url = URL.createObjectURL(blob);\n const a = document.createElement(\"a\");\n a.href = url;\n a.download = filename;\n document.body.appendChild(a);\n a.click();\n document.body.removeChild(a);\n URL.revokeObjectURL(url);\n}\n\n// ============================================================================\n// Public API\n// ============================================================================\n\nexport async function getSupportedAudioCodecs(options?: {\n numberOfChannels?: number;\n sampleRate?: number;\n bitrate?: number;\n}): Promise<AudioCodec[]> {\n const { numberOfChannels = 2, sampleRate = 48000, bitrate = 128000 } = options ?? {};\n return getEncodableAudioCodecs(undefined, {\n numberOfChannels,\n sampleRate,\n bitrate,\n });\n}\n\n/**\n * Renders a timegroup to an MP4 video file.\n *\n * Uses the EXACT same code path as thumbnail generation (captureFromClone).\n * This ensures consistency - if thumbnails work, video export works.\n */\nexport async function renderTimegroupToVideo(\n timegroup: EFTimegroup,\n options: RenderToVideoOptions = {},\n): Promise<Uint8Array | undefined> {\n const config = resolveConfig(timegroup, options);\n const { signal, onProgress } = options;\n\n const checkCancelled = () => {\n if (signal?.aborted) throw new RenderCancelledError();\n };\n\n resetRenderState();\n\n // =========================================================================\n // Create render clone - EXACT same as captureBatch in EFTimegroup\n // =========================================================================\n const {\n clone: renderClone,\n container: cloneContainer,\n cleanup: cleanupRenderClone,\n } = await timegroup.createRenderClone();\n\n // Build timestamps array for frame loop\n const timestamps: number[] = [];\n for (let i = 0; i < config.totalFrames; i++) {\n timestamps.push(config.startMs + i * config.frameDurationMs);\n }\n\n // =========================================================================\n // Set up video encoding\n // =========================================================================\n let output: Output | null = null;\n let videoSource: CanvasSource | null = null;\n let audioSource: AudioBufferSource | null = null;\n let target: BufferTarget | StreamTarget | null = null;\n let fileStream: {\n writable: WritableStream<Uint8Array>;\n close: () => Promise<void>;\n } | null = null;\n let useStreaming = false;\n let encodingCanvas: OffscreenCanvas | null = null;\n let encodingCtx: OffscreenCanvasRenderingContext2D | null = null;\n\n if (!config.benchmarkMode) {\n // Check for custom writable stream first (for programmatic streaming)\n if (options.customWritableStream) {\n target = new StreamTarget(options.customWritableStream as any);\n output = new Output({\n format: new Mp4OutputFormat({ fastStart: \"fragmented\" }),\n target,\n });\n useStreaming = true;\n } else if (config.streaming) {\n fileStream = await getFileWritableStream(config.filename);\n useStreaming = fileStream !== null;\n\n if (useStreaming && fileStream) {\n target = new StreamTarget(fileStream.writable as any);\n output = new Output({\n format: new Mp4OutputFormat({ fastStart: \"fragmented\" }),\n target,\n });\n }\n }\n\n if (!target) {\n target = new BufferTarget();\n output = new Output({ format: new Mp4OutputFormat(), target });\n }\n\n encodingCanvas = new OffscreenCanvas(config.videoWidth, config.videoHeight);\n encodingCtx = encodingCanvas.getContext(\"2d\");\n if (!encodingCtx) {\n cleanupRenderClone();\n throw new Error(\"Failed to get encoding canvas context\");\n }\n\n if (!output) {\n throw new Error(\"Output not initialized\");\n }\n\n const videoConfig: VideoEncodingConfig = {\n codec: config.codec,\n bitrate: config.bitrate,\n keyFrameInterval: config.keyFrameInterval,\n };\n videoSource = new CanvasSource(encodingCanvas, videoConfig);\n output.addVideoTrack(videoSource);\n\n if (config.includeAudio) {\n const selectedCodec = await selectAudioCodec(config.preferredAudioCodecs, {\n numberOfChannels: 2,\n sampleRate: 48000,\n bitrate: config.audioBitrate,\n });\n const audioConfig: AudioEncodingConfig = {\n codec: selectedCodec,\n bitrate: config.audioBitrate,\n };\n audioSource = new AudioBufferSource(audioConfig);\n output.addAudioTrack(audioSource);\n }\n\n await output.start();\n }\n\n // =========================================================================\n // Setup for per-frame passive structure rebuilding (like live preview)\n // =========================================================================\n // Create RenderContext for caching across all frames\n const renderContext = new RenderContext();\n\n // Create preview container with proper styling (reusable, content rebuilt each frame)\n // Use unscaled dimensions for the preview container (which holds the full-size clone)\n const containerWidth = timegroup.offsetWidth || 1920;\n const containerHeight = timegroup.offsetHeight || 1080;\n const previewContainer = createPreviewContainer({\n width: containerWidth,\n height: containerHeight,\n background: getComputedStyle(timegroup).background || \"#000\",\n });\n\n // Setup for direct serialization\n logger.debug(`[renderTimegroupToVideo] Using direct timeline serialization`);\n\n // Attach clone container (keeps renderClone in its React-managed DOM position)\n previewContainer.appendChild(cloneContainer);\n\n // Add ef-render-clone-container class for CSS selectors and debugging\n previewContainer.classList.add(\"ef-render-clone-container\");\n\n // CRITICAL: Attach container to document so getComputedStyle returns actual values\n // Without this, all computed styles are empty strings!\n // Hide the container OFF-SCREEN but do NOT use visibility:hidden because:\n // 1. visibility:hidden is inherited by all children\n // 2. seekForRender checks getComputedStyle().visibility and skips \"hidden\" subtrees\n // 3. This would cause FrameController to skip rendering all nested content\n previewContainer.style.cssText +=\n \";position:fixed;left:-99999px;top:-99999px;pointer-events:none;\";\n document.body.appendChild(previewContainer);\n\n // Force layout/reflow so getComputedStyle returns correct values\n void renderClone.offsetHeight;\n logger.debug(\n `[renderTimegroupToVideo] Attached previewContainer to document.body (off-screen) for style computation`,\n );\n\n // =========================================================================\n // Frame loop - DEEP PIPELINE: overlap encode + render + prepare\n // =========================================================================\n const renderStartTime = performance.now();\n let lastRenderedAudioEndMs = config.startMs;\n const audioChunkDurationMs = 2000;\n\n // Reusable thumbnail canvas for preview (no encoding, just draw to canvas)\n let thumbCanvas: HTMLCanvasElement | null = null;\n let thumbCtx: CanvasRenderingContext2D | null = null;\n if (onProgress && config.progressPreviewInterval > 0) {\n const previewWidth = 160;\n const previewHeight = Math.round(previewWidth * (config.videoHeight / config.videoWidth));\n thumbCanvas = document.createElement(\"canvas\");\n thumbCanvas.width = previewWidth;\n thumbCanvas.height = previewHeight;\n thumbCtx = thumbCanvas.getContext(\"2d\");\n }\n\n try {\n // ========================================================================\n // OVERLAPPED PIPELINE: image loading runs parallel with seek+serialize\n // ========================================================================\n // The clone can only seek one frame at a time, and serialization must\n // capture the DOM before the next seek. But image loading (data URI →\n // Image) is independent of the clone and runs in the background.\n //\n // Per-frame timeline:\n // [seek(N)] → [serialize(N)] → [image.load(N) in background...]\n // └─ [seek(N+1)] → [serialize(N+1)] → ...\n // └─ encode(N) when image resolves\n\n type PendingFrame = {\n frameIndex: number;\n timeMs: number;\n timestampS: number;\n resolved: HTMLImageElement | null;\n promise: Promise<HTMLImageElement>;\n };\n\n const MAX_AHEAD = 2;\n const pendingFrames: PendingFrame[] = [];\n let nextSeekFrame = 0;\n let encodedFrames = 0;\n\n while (encodedFrames < config.totalFrames) {\n checkCancelled();\n\n // ==================================================================\n // PHASE 1: Fill pipeline — seek+serialize ahead while images load\n // ==================================================================\n while (nextSeekFrame < config.totalFrames && pendingFrames.length < MAX_AHEAD) {\n const fi = nextSeekFrame;\n const timeMs = timestamps[fi]!;\n const timestampS = (fi * config.frameDurationMs) / 1000;\n\n await renderClone.seekForRender(timeMs);\n\n const entry: PendingFrame = {\n frameIndex: fi,\n timeMs,\n timestampS,\n resolved: null,\n promise: null!,\n };\n\n // Wait for video content if using blocking mode\n if (config.contentReadyMode === \"blocking\") {\n await waitForVideoContent(renderClone, timeMs, config.blockingTimeoutMs);\n }\n\n if (config.canvasMode === \"native\") {\n const canvas = await renderToImageNative(renderClone, config.width, config.height, {\n skipDprScaling: true,\n });\n entry.resolved = canvas as any as HTMLImageElement;\n entry.promise = Promise.resolve(entry.resolved);\n } else {\n // Synchronous capture: walks DOM + snapshots canvas pixels.\n // Returns immediately — clone is free for next seek.\n // Encoding (canvas→base64, SVG assembly) and image loading\n // all resolve in the background.\n const dataUriPromise = captureTimelineToDataUri(\n renderClone,\n config.width,\n config.height,\n {\n renderContext,\n canvasScale: config.scale,\n timeMs,\n },\n );\n\n entry.promise = dataUriPromise.then((dataUri) => {\n return new Promise<HTMLImageElement>((resolve, reject) => {\n const image = new Image();\n image.onload = () => {\n entry.resolved = image;\n resolve(image);\n };\n image.onerror = (e) => {\n console.error(`[Render] frame ${fi} image load error:`, e);\n reject(new Error(`Failed to load image from data URI`));\n };\n image.src = dataUri;\n });\n });\n }\n\n pendingFrames.push(entry);\n nextSeekFrame++;\n }\n\n // ==================================================================\n // PHASE 2: Encode next frame in order (await if not yet loaded)\n // ==================================================================\n const head = pendingFrames.shift()!;\n const preloaded = head.resolved !== null;\n let image: HTMLImageElement;\n if (preloaded) {\n image = head.resolved!;\n } else {\n image = await head.promise;\n }\n\n if (audioSource && head.timeMs >= lastRenderedAudioEndMs + audioChunkDurationMs) {\n const chunkEndMs = Math.min(head.timeMs + audioChunkDurationMs, config.endMs);\n try {\n const audioBuffer = await timegroup.renderAudio(\n lastRenderedAudioEndMs,\n chunkEndMs,\n signal,\n );\n if (audioBuffer && audioBuffer.length > 0) {\n await audioSource.add(audioBuffer);\n }\n } catch (_e) {\n /* Audio render failures are non-fatal */\n }\n lastRenderedAudioEndMs = chunkEndMs;\n }\n\n if (videoSource && output && encodingCtx) {\n encodingCtx.drawImage(\n image,\n 0,\n 0,\n image.width,\n image.height,\n 0,\n 0,\n config.videoWidth,\n config.videoHeight,\n );\n await videoSource.add(head.timestampS, config.frameDurationS);\n }\n\n // ==================================================================\n // Progress reporting\n // ==================================================================\n encodedFrames++;\n const currentFrame = encodedFrames;\n const progress = currentFrame / config.totalFrames;\n const renderedMs = currentFrame * config.frameDurationMs;\n const elapsedMs = performance.now() - renderStartTime;\n const msPerFrame = elapsedMs / currentFrame;\n const remainingFrames = config.totalFrames - currentFrame;\n const estimatedRemainingMs = remainingFrames * msPerFrame;\n const speedMultiplier = renderedMs / elapsedMs;\n\n if (thumbCanvas && thumbCtx && head.frameIndex % config.progressPreviewInterval === 0) {\n thumbCtx.drawImage(image, 0, 0, thumbCanvas.width, thumbCanvas.height);\n }\n\n onProgress?.({\n progress,\n currentFrame,\n totalFrames: config.totalFrames,\n renderedMs,\n totalDurationMs: config.renderDurationMs,\n elapsedMs,\n estimatedRemainingMs,\n speedMultiplier,\n framePreviewCanvas: thumbCanvas || undefined,\n });\n }\n\n // Render remaining audio\n if (audioSource && lastRenderedAudioEndMs < config.endMs) {\n try {\n const audioBuffer = await timegroup.renderAudio(\n lastRenderedAudioEndMs,\n config.endMs,\n signal,\n );\n if (audioBuffer && audioBuffer.length > 0) {\n await audioSource.add(audioBuffer);\n }\n } catch (_e) {\n /* Audio render failures are non-fatal */\n }\n }\n\n if (config.benchmarkMode) {\n return undefined;\n }\n\n await output!.finalize();\n\n if (typeof __EF_TELEMETRY_ENABLED__ !== \"undefined\" && __EF_TELEMETRY_ENABLED__) {\n const elapsedMs = Math.round(performance.now() - renderStartTime);\n const endpoint = options.telemetryEndpoint ?? \"https://editframe.com\";\n const efMediaCount = timegroup.querySelectorAll(\"ef-video,ef-audio\").length;\n const efImageCount = timegroup.querySelectorAll(\"ef-image\").length;\n const efCaptionsCount = timegroup.querySelectorAll(\"ef-captions\").length;\n const efTextCount = timegroup.querySelectorAll(\"ef-text\").length;\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n };\n if (options.telemetryToken) {\n headers[\"Authorization\"] = `Bearer ${options.telemetryToken}`;\n }\n fetch(`${endpoint}/api/v1/telemetry`, {\n method: \"POST\",\n headers,\n body: JSON.stringify({\n event_type: \"render\",\n render_path: \"client\",\n duration_ms: elapsedMs,\n width: config.videoWidth,\n height: config.videoHeight,\n fps: config.fps,\n feature_usage: {\n efMediaCount,\n efImageCount,\n efCaptionsCount,\n efTextCount,\n },\n }),\n keepalive: true,\n }).catch(() => {\n // Telemetry errors must never surface to users.\n });\n }\n\n if (useStreaming) {\n // Streaming mode: chunks already sent via customWritableStream or file stream\n return undefined;\n } else {\n const bufferTarget = target as BufferTarget;\n const videoBuffer = bufferTarget.buffer;\n if (!videoBuffer) {\n throw new Error(\"Video encoding failed: no buffer produced\");\n }\n\n if (config.returnBuffer) {\n return new Uint8Array(videoBuffer);\n }\n\n const videoBlob = new Blob([videoBuffer], { type: \"video/mp4\" });\n downloadBlob(videoBlob, config.filename);\n return undefined;\n }\n } finally {\n renderContext.dispose();\n // Remove previewContainer first — renderClone was moved into it, so it must be\n // detached before cleanupRenderClone() unmounts the React root that owns renderClone.\n if (previewContainer.parentNode) {\n previewContainer.parentNode.removeChild(previewContainer);\n }\n cleanupRenderClone();\n }\n}\n\nexport { QUALITY_HIGH };\nexport type { AudioCodec };\n"],"mappings":";;;;;;;;;;AAkDA,IAAa,6BAAb,cAAgD,MAAM;CACpD,YAAY,iBAA+B,iBAA+B;AACxE,QACE,+CAA+C,gBAAgB,KAAK,KAAK,CAAC,iBACzD,gBAAgB,SAAS,IAAI,gBAAgB,KAAK,KAAK,GAAG,OAAO,GACnF;AACD,OAAK,OAAO;;;AAIhB,IAAa,uBAAb,cAA0C,MAAM;CAC9C,cAAc;AACZ,QAAM,mBAAmB;AACzB,OAAK,OAAO;;;AAqChB,SAAS,cAAc,WAAwB,UAAgC,EAAE,EAAkB;CACjG,MAAM,MAAM,QAAQ,OAAO,UAAU,gBAAgB;CACrD,MAAM,QAAQ,QAAQ,SAAS;CAC/B,MAAM,UAAU,QAAQ,WAAW;CACnC,MAAM,WAAW,QAAQ,YAAY;CACrC,MAAM,QAAQ,QAAQ,SAAS;CAC/B,MAAM,mBAAmB,QAAQ,oBAAoB;CACrD,MAAM,YAAY,QAAQ,aAAa;CACvC,MAAM,eAAe,QAAQ,gBAAgB;CAC7C,MAAM,eAAe,QAAQ,gBAAgB;CAC7C,MAAM,mBAAmB,QAAQ,oBAAoB;CACrD,MAAM,oBAAoB,QAAQ,qBAAqB;CACvD,MAAM,eAAe,QAAQ,gBAAgB;CAC7C,MAAM,uBAAuB,QAAQ,wBAAwB,CAAC,OAAO,OAAO;CAC5E,MAAM,gBAAgB,QAAQ,iBAAiB;CAG/C,MAAM,0BAA0B,QAAQ,2BAA2B;CAEnE,MAAM,kBAAkB,UAAU;AAClC,KAAI,CAAC,mBAAmB,mBAAmB,EACzC,OAAM,IAAI,MAAM,4BAA4B;CAG9C,MAAM,UAAU,KAAK,IAAI,GAAG,QAAQ,UAAU,EAAE;CAChD,MAAM,QACJ,QAAQ,SAAS,SAAY,KAAK,IAAI,QAAQ,MAAM,gBAAgB,GAAG;CACzE,MAAM,mBAAmB,QAAQ;AAEjC,KAAI,oBAAoB,EACtB,OAAM,IAAI,MAAM,8BAA8B,QAAQ,QAAQ,MAAM,IAAI;AAI1E,CAAK,UAAU;CAGf,IAAI,iBAAiB,UAAU;CAC/B,IAAI,kBAAkB,UAAU;AAEhC,KAAI,CAAC,kBAAkB,CAAC,iBAAiB;EACvC,MAAM,OAAO,UAAU,uBAAuB;AAC9C,MAAI,KAAK,QAAQ,KAAK,KAAK,SAAS,GAAG;AACrC,oBAAiB,KAAK;AACtB,qBAAkB,KAAK;;;AAI3B,KAAI,CAAC,kBAAkB,CAAC,iBAAiB;EACvC,MAAM,WAAW,iBAAiB,UAAU;EAC5C,MAAM,KAAK,WAAW,SAAS,MAAM;EACrC,MAAM,KAAK,WAAW,SAAS,OAAO;AACtC,MAAI,KAAK,KAAK,KAAK,GAAG;AACpB,oBAAiB;AACjB,qBAAkB;;;AAItB,KAAI,CAAC,kBAAkB,CAAC,gBACtB,OAAM,IAAI,MACR,gCAAgC,eAAe,GAAG,gBAAgB,+HAGnE;CAEH,MAAM,QAAQ,KAAK,MAAM,iBAAiB,MAAM;CAChD,MAAM,SAAS,KAAK,MAAM,kBAAkB,MAAM;CAElD,MAAM,aAAa,QAAQ,MAAM,IAAI,QAAQ,QAAQ;CACrD,MAAM,cAAc,SAAS,MAAM,IAAI,SAAS,SAAS;CAEzD,MAAM,kBAAkB,MAAO;AAmB/B,QAAO;EACL;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,aAhCkB,KAAK,KAAK,mBAAmB,gBAAgB;EAiC/D;EACA,gBAjCqB,kBAAkB;EAkCvC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,mBAtCwB;GACxB,MAAM,YAAY,QAAQ;AAC1B,OAAI,CAAC,UAAW,QAAO;AACvB,OAAI,cAAc,YAAY,CAAC,4BAA4B,EAAE;AAC3D,WAAO,MACL,yGACD;AACD,WAAO;;AAET,UAAO;MACL;EA6BH;;AAOH,SAAS,8BAAuC;AAC9C,QAAO,OAAO,WAAW,eAAe,wBAAwB;;AAGlE,eAAe,sBAAsB,UAG3B;AACR,KAAI,CAAC,6BAA6B,CAChC,QAAO;AAGT,KAAI;EAKF,MAAM,WAAW,OAJE,MAAO,OAAe,mBAAmB;GAC1D,eAAe;GACf,OAAO,CAAC;IAAE,aAAa;IAAa,QAAQ,EAAE,aAAa,CAAC,OAAO,EAAE;IAAE,CAAC;GACzE,CAAC,EACgC,gBAAgB;AAClD,SAAO;GACL;GACA,OAAO,YAAY;AACjB,UAAM,SAAS,OAAO;;GAEzB;UACM,GAAG;AACV,MAAK,EAAY,SAAS,aACxB,QAAO,KAAK,8CAA8C,EAAE;AAE9D,SAAO;;;AAIX,eAAe,iBACb,iBACA,iBAKqB;AACrB,MAAK,MAAM,SAAS,gBAClB,KAAI;AAEF,MADoB,MAAM,eAAe,OAAO,gBAAgB,CAC/C,QAAO;UACjB,GAAG;AACV,SAAO,KAAK,uCAAuC,MAAM,IAAI,EAAE;;AAInE,OAAM,IAAI,2BAA2B,iBADb,MAAM,wBAAwB,QAAW,gBAAgB,CACX;;AAGxE,SAAS,aAAa,MAAY,UAAwB;CACxD,MAAM,MAAM,IAAI,gBAAgB,KAAK;CACrC,MAAM,IAAI,SAAS,cAAc,IAAI;AACrC,GAAE,OAAO;AACT,GAAE,WAAW;AACb,UAAS,KAAK,YAAY,EAAE;AAC5B,GAAE,OAAO;AACT,UAAS,KAAK,YAAY,EAAE;AAC5B,KAAI,gBAAgB,IAAI;;AAO1B,eAAsB,wBAAwB,SAIpB;CACxB,MAAM,EAAE,mBAAmB,GAAG,aAAa,MAAO,UAAU,UAAW,WAAW,EAAE;AACpF,QAAO,wBAAwB,QAAW;EACxC;EACA;EACA;EACD,CAAC;;;;;;;;AASJ,eAAsB,uBACpB,WACA,UAAgC,EAAE,EACD;CACjC,MAAM,SAAS,cAAc,WAAW,QAAQ;CAChD,MAAM,EAAE,QAAQ,eAAe;CAE/B,MAAM,uBAAuB;AAC3B,MAAI,QAAQ,QAAS,OAAM,IAAI,sBAAsB;;AAGvD,mBAAkB;CAKlB,MAAM,EACJ,OAAO,aACP,WAAW,gBACX,SAAS,uBACP,MAAM,UAAU,mBAAmB;CAGvC,MAAMA,aAAuB,EAAE;AAC/B,MAAK,IAAI,IAAI,GAAG,IAAI,OAAO,aAAa,IACtC,YAAW,KAAK,OAAO,UAAU,IAAI,OAAO,gBAAgB;CAM9D,IAAIC,SAAwB;CAC5B,IAAIC,cAAmC;CACvC,IAAIC,cAAwC;CAC5C,IAAIC,SAA6C;CACjD,IAAIC,aAGO;CACX,IAAI,eAAe;CACnB,IAAIC,iBAAyC;CAC7C,IAAIC,cAAwD;AAE5D,KAAI,CAAC,OAAO,eAAe;AAEzB,MAAI,QAAQ,sBAAsB;AAChC,YAAS,IAAI,aAAa,QAAQ,qBAA4B;AAC9D,YAAS,IAAI,OAAO;IAClB,QAAQ,IAAI,gBAAgB,EAAE,WAAW,cAAc,CAAC;IACxD;IACD,CAAC;AACF,kBAAe;aACN,OAAO,WAAW;AAC3B,gBAAa,MAAM,sBAAsB,OAAO,SAAS;AACzD,kBAAe,eAAe;AAE9B,OAAI,gBAAgB,YAAY;AAC9B,aAAS,IAAI,aAAa,WAAW,SAAgB;AACrD,aAAS,IAAI,OAAO;KAClB,QAAQ,IAAI,gBAAgB,EAAE,WAAW,cAAc,CAAC;KACxD;KACD,CAAC;;;AAIN,MAAI,CAAC,QAAQ;AACX,YAAS,IAAI,cAAc;AAC3B,YAAS,IAAI,OAAO;IAAE,QAAQ,IAAI,iBAAiB;IAAE;IAAQ,CAAC;;AAGhE,mBAAiB,IAAI,gBAAgB,OAAO,YAAY,OAAO,YAAY;AAC3E,gBAAc,eAAe,WAAW,KAAK;AAC7C,MAAI,CAAC,aAAa;AAChB,uBAAoB;AACpB,SAAM,IAAI,MAAM,wCAAwC;;AAG1D,MAAI,CAAC,OACH,OAAM,IAAI,MAAM,yBAAyB;EAG3C,MAAMC,cAAmC;GACvC,OAAO,OAAO;GACd,SAAS,OAAO;GAChB,kBAAkB,OAAO;GAC1B;AACD,gBAAc,IAAI,aAAa,gBAAgB,YAAY;AAC3D,SAAO,cAAc,YAAY;AAEjC,MAAI,OAAO,cAAc;AAUvB,iBAAc,IAAI,kBAJuB;IACvC,OANoB,MAAM,iBAAiB,OAAO,sBAAsB;KACxE,kBAAkB;KAClB,YAAY;KACZ,SAAS,OAAO;KACjB,CAAC;IAGA,SAAS,OAAO;IACjB,CAC+C;AAChD,UAAO,cAAc,YAAY;;AAGnC,QAAM,OAAO,OAAO;;CAOtB,MAAM,gBAAgB,IAAI,eAAe;CAMzC,MAAM,mBAAmB,uBAAuB;EAC9C,OAHqB,UAAU,eAAe;EAI9C,QAHsB,UAAU,gBAAgB;EAIhD,YAAY,iBAAiB,UAAU,CAAC,cAAc;EACvD,CAAC;AAGF,QAAO,MAAM,+DAA+D;AAG5E,kBAAiB,YAAY,eAAe;AAG5C,kBAAiB,UAAU,IAAI,4BAA4B;AAQ3D,kBAAiB,MAAM,WACrB;AACF,UAAS,KAAK,YAAY,iBAAiB;AAG3C,CAAK,YAAY;AACjB,QAAO,MACL,yGACD;CAKD,MAAM,kBAAkB,YAAY,KAAK;CACzC,IAAI,yBAAyB,OAAO;CACpC,MAAM,uBAAuB;CAG7B,IAAIC,cAAwC;CAC5C,IAAIC,WAA4C;AAChD,KAAI,cAAc,OAAO,0BAA0B,GAAG;EACpD,MAAM,eAAe;EACrB,MAAM,gBAAgB,KAAK,MAAM,gBAAgB,OAAO,cAAc,OAAO,YAAY;AACzF,gBAAc,SAAS,cAAc,SAAS;AAC9C,cAAY,QAAQ;AACpB,cAAY,SAAS;AACrB,aAAW,YAAY,WAAW,KAAK;;AAGzC,KAAI;EAqBF,MAAM,YAAY;EAClB,MAAMC,gBAAgC,EAAE;EACxC,IAAI,gBAAgB;EACpB,IAAI,gBAAgB;AAEpB,SAAO,gBAAgB,OAAO,aAAa;AACzC,mBAAgB;AAKhB,UAAO,gBAAgB,OAAO,eAAe,cAAc,SAAS,WAAW;IAC7E,MAAM,KAAK;IACX,MAAM,SAAS,WAAW;IAC1B,MAAM,aAAc,KAAK,OAAO,kBAAmB;AAEnD,UAAM,YAAY,cAAc,OAAO;IAEvC,MAAMC,QAAsB;KAC1B,YAAY;KACZ;KACA;KACA,UAAU;KACV,SAAS;KACV;AAGD,QAAI,OAAO,qBAAqB,WAC9B,OAAM,oBAAoB,aAAa,QAAQ,OAAO,kBAAkB;AAG1E,QAAI,OAAO,eAAe,UAAU;AAIlC,WAAM,WAHS,MAAM,oBAAoB,aAAa,OAAO,OAAO,OAAO,QAAQ,EACjF,gBAAgB,MACjB,CAAC;AAEF,WAAM,UAAU,QAAQ,QAAQ,MAAM,SAAS;UAiB/C,OAAM,UAXiB,yBACrB,aACA,OAAO,OACP,OAAO,QACP;KACE;KACA,aAAa,OAAO;KACpB;KACD,CACF,CAE8B,MAAM,YAAY;AAC/C,YAAO,IAAI,SAA2B,SAAS,WAAW;MACxD,MAAMC,UAAQ,IAAI,OAAO;AACzB,cAAM,eAAe;AACnB,aAAM,WAAWA;AACjB,eAAQA,QAAM;;AAEhB,cAAM,WAAW,MAAM;AACrB,eAAQ,MAAM,kBAAkB,GAAG,qBAAqB,EAAE;AAC1D,8BAAO,IAAI,MAAM,qCAAqC,CAAC;;AAEzD,cAAM,MAAM;OACZ;MACF;AAGJ,kBAAc,KAAK,MAAM;AACzB;;GAMF,MAAM,OAAO,cAAc,OAAO;GAClC,MAAM,YAAY,KAAK,aAAa;GACpC,IAAIC;AACJ,OAAI,UACF,SAAQ,KAAK;OAEb,SAAQ,MAAM,KAAK;AAGrB,OAAI,eAAe,KAAK,UAAU,yBAAyB,sBAAsB;IAC/E,MAAM,aAAa,KAAK,IAAI,KAAK,SAAS,sBAAsB,OAAO,MAAM;AAC7E,QAAI;KACF,MAAM,cAAc,MAAM,UAAU,YAClC,wBACA,YACA,OACD;AACD,SAAI,eAAe,YAAY,SAAS,EACtC,OAAM,YAAY,IAAI,YAAY;aAE7B,IAAI;AAGb,6BAAyB;;AAG3B,OAAI,eAAe,UAAU,aAAa;AACxC,gBAAY,UACV,OACA,GACA,GACA,MAAM,OACN,MAAM,QACN,GACA,GACA,OAAO,YACP,OAAO,YACR;AACD,UAAM,YAAY,IAAI,KAAK,YAAY,OAAO,eAAe;;AAM/D;GACA,MAAM,eAAe;GACrB,MAAM,WAAW,eAAe,OAAO;GACvC,MAAM,aAAa,eAAe,OAAO;GACzC,MAAM,YAAY,YAAY,KAAK,GAAG;GACtC,MAAM,aAAa,YAAY;GAE/B,MAAM,wBADkB,OAAO,cAAc,gBACE;GAC/C,MAAM,kBAAkB,aAAa;AAErC,OAAI,eAAe,YAAY,KAAK,aAAa,OAAO,4BAA4B,EAClF,UAAS,UAAU,OAAO,GAAG,GAAG,YAAY,OAAO,YAAY,OAAO;AAGxE,gBAAa;IACX;IACA;IACA,aAAa,OAAO;IACpB;IACA,iBAAiB,OAAO;IACxB;IACA;IACA;IACA,oBAAoB,eAAe;IACpC,CAAC;;AAIJ,MAAI,eAAe,yBAAyB,OAAO,MACjD,KAAI;GACF,MAAM,cAAc,MAAM,UAAU,YAClC,wBACA,OAAO,OACP,OACD;AACD,OAAI,eAAe,YAAY,SAAS,EACtC,OAAM,YAAY,IAAI,YAAY;WAE7B,IAAI;AAKf,MAAI,OAAO,cACT;AAGF,QAAM,OAAQ,UAAU;AAExB,MAAI,OAAO,6BAA6B,eAAe,0BAA0B;GAC/E,MAAM,YAAY,KAAK,MAAM,YAAY,KAAK,GAAG,gBAAgB;GACjE,MAAM,WAAW,QAAQ,qBAAqB;GAC9C,MAAM,eAAe,UAAU,iBAAiB,oBAAoB,CAAC;GACrE,MAAM,eAAe,UAAU,iBAAiB,WAAW,CAAC;GAC5D,MAAM,kBAAkB,UAAU,iBAAiB,cAAc,CAAC;GAClE,MAAM,cAAc,UAAU,iBAAiB,UAAU,CAAC;GAC1D,MAAMC,UAAkC,EACtC,gBAAgB,oBACjB;AACD,OAAI,QAAQ,eACV,SAAQ,mBAAmB,UAAU,QAAQ;AAE/C,SAAM,GAAG,SAAS,oBAAoB;IACpC,QAAQ;IACR;IACA,MAAM,KAAK,UAAU;KACnB,YAAY;KACZ,aAAa;KACb,aAAa;KACb,OAAO,OAAO;KACd,QAAQ,OAAO;KACf,KAAK,OAAO;KACZ,eAAe;MACb;MACA;MACA;MACA;MACD;KACF,CAAC;IACF,WAAW;IACZ,CAAC,CAAC,YAAY,GAEb;;AAGJ,MAAI,aAEF;OACK;GAEL,MAAM,cADe,OACY;AACjC,OAAI,CAAC,YACH,OAAM,IAAI,MAAM,4CAA4C;AAG9D,OAAI,OAAO,aACT,QAAO,IAAI,WAAW,YAAY;AAIpC,gBADkB,IAAI,KAAK,CAAC,YAAY,EAAE,EAAE,MAAM,aAAa,CAAC,EACxC,OAAO,SAAS;AACxC;;WAEM;AACR,gBAAc,SAAS;AAGvB,MAAI,iBAAiB,WACnB,kBAAiB,WAAW,YAAY,iBAAiB;AAE3D,sBAAoB"}
|
|
@@ -230,7 +230,7 @@ async function renderVideoToVideo(video, options = {}) {
|
|
|
230
230
|
try {
|
|
231
231
|
const audioBuffer = await video.renderAudio(lastRenderedAudioEndMs, chunkEndMs);
|
|
232
232
|
if (audioBuffer && audioBuffer.length > 0) await audioSource.add(audioBuffer);
|
|
233
|
-
} catch (
|
|
233
|
+
} catch (_e) {}
|
|
234
234
|
lastRenderedAudioEndMs = chunkEndMs;
|
|
235
235
|
}
|
|
236
236
|
if (thumbCanvas && thumbCtx && frameIndex % config.progressPreviewInterval === 0) thumbCtx.drawImage(encodingCanvas, 0, 0, thumbCanvas.width, thumbCanvas.height);
|
|
@@ -256,7 +256,7 @@ async function renderVideoToVideo(video, options = {}) {
|
|
|
256
256
|
if (audioSource && lastRenderedAudioEndMs < config.endMs) try {
|
|
257
257
|
const audioBuffer = await video.renderAudio(lastRenderedAudioEndMs, config.endMs);
|
|
258
258
|
if (audioBuffer && audioBuffer.length > 0) await audioSource.add(audioBuffer);
|
|
259
|
-
} catch (
|
|
259
|
+
} catch (_e) {}
|
|
260
260
|
const totalElapsed = performance.now() - renderStartTime;
|
|
261
261
|
logger.debug(`[renderVideoToVideo] complete: ${config.totalFrames} frames in ${totalElapsed.toFixed(0)}ms (seek=${totalSeekMs.toFixed(0)}ms, draw=${totalDrawMs.toFixed(0)}ms, encode=${totalEncodeMs.toFixed(0)}ms) speed=${(config.renderDurationMs / totalElapsed).toFixed(1)}x`);
|
|
262
262
|
await output.finalize();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"renderVideoToVideo.js","names":["width: number","height: number","output: Output | null","videoSource: CanvasSource | null","audioSource: AudioBufferSource | null","target: BufferTarget | StreamTarget | null","fileStream: {\n writable: WritableStream<Uint8Array>;\n close: () => Promise<void>;\n } | null","thumbCanvas: HTMLCanvasElement | null","thumbCtx: CanvasRenderingContext2D | null"],"sources":["../../src/preview/renderVideoToVideo.ts"],"sourcesContent":["/**\n * Direct video-to-video rendering — fast path for single video elements.\n *\n * Bypasses the full DOM serialization pipeline (foreignObject/native canvas)\n * by decoding frames directly from the media engine and re-encoding to MP4.\n *\n * Supports CSS effects via canvas 2D context:\n * - filter (ctx.filter)\n * - opacity (ctx.globalAlpha)\n */\n\nimport {\n Output,\n Mp4OutputFormat,\n BufferTarget,\n StreamTarget,\n CanvasSource,\n AudioBufferSource,\n canEncodeAudio,\n getEncodableAudioCodecs,\n type VideoEncodingConfig,\n type AudioEncodingConfig,\n type AudioCodec,\n} from \"mediabunny\";\nimport type { EFVideo } from \"../elements/EFVideo.js\";\nimport {\n NoSupportedAudioCodecError,\n RenderCancelledError,\n} from \"./renderTimegroupToVideo.js\";\nimport type { RenderToVideoOptions } from \"./renderTimegroupToVideo.types.js\";\nimport { logger } from \"./logger.js\";\n\n// ============================================================================\n// Configuration\n// ============================================================================\n\ninterface ResolvedVideoConfig {\n fps: number;\n codec: \"avc\" | \"hevc\" | \"vp9\" | \"av1\" | \"vp8\";\n bitrate: number;\n filename: string;\n scale: number;\n keyFrameInterval: number;\n startMs: number;\n endMs: number;\n renderDurationMs: number;\n videoWidth: number;\n videoHeight: number;\n totalFrames: number;\n frameDurationMs: number;\n frameDurationS: number;\n streaming: boolean;\n includeAudio: boolean;\n audioBitrate: number;\n returnBuffer: boolean;\n preferredAudioCodecs: AudioCodec[];\n progressPreviewInterval: number;\n trimStartMs: number;\n}\n\nasync function resolveVideoConfig(\n video: EFVideo,\n options: RenderToVideoOptions = {},\n): Promise<ResolvedVideoConfig> {\n const fps = options.fps ?? 30;\n const codec = options.codec ?? \"avc\";\n const bitrate = options.bitrate ?? 8_000_000;\n const filename = options.filename ?? \"video-export.mp4\";\n const scale = options.scale ?? 1;\n const keyFrameInterval = options.keyFrameInterval ?? 2;\n const streaming = options.streaming ?? false;\n const includeAudio = options.includeAudio ?? true;\n const audioBitrate = options.audioBitrate ?? 128_000;\n const returnBuffer = options.returnBuffer ?? false;\n const preferredAudioCodecs = options.preferredAudioCodecs ?? [\"aac\", \"opus\"];\n const progressPreviewInterval = options.progressPreviewInterval ?? 60;\n\n const trimStartMs = video.trimStartMs ?? 0;\n const trimEndMs = video.trimEndMs ?? 0;\n const intrinsicDurationMs = video.intrinsicDurationMs;\n\n if (!intrinsicDurationMs || intrinsicDurationMs <= 0) {\n throw new Error(\n \"Video has no intrinsic duration. Ensure the media engine is loaded.\",\n );\n }\n\n const effectiveDurationMs = intrinsicDurationMs - trimStartMs - trimEndMs;\n if (effectiveDurationMs <= 0) {\n throw new Error(\n `Invalid trim range: trimStart=${trimStartMs}ms, trimEnd=${trimEndMs}ms, ` +\n `intrinsicDuration=${intrinsicDurationMs}ms leaves no content.`,\n );\n }\n\n const startMs =\n options.fromMs !== undefined ? Math.max(0, options.fromMs) : 0;\n const endMs =\n options.toMs !== undefined\n ? Math.min(options.toMs, effectiveDurationMs)\n : effectiveDurationMs;\n const renderDurationMs = endMs - startMs;\n\n if (renderDurationMs <= 0) {\n throw new Error(`Invalid render range: from ${startMs}ms to ${endMs}ms`);\n }\n\n let width: number;\n let height: number;\n\n // Decode first frame to determine dimensions\n {\n const firstFrame = await video.getVideoFrameAtSourceTime(trimStartMs, {\n quality: \"main\",\n });\n try {\n width = firstFrame.displayWidth;\n height = firstFrame.displayHeight;\n } finally {\n firstFrame.close();\n }\n }\n\n const videoWidth = Math.floor(width * scale);\n const videoHeight = Math.floor(height * scale);\n // Ensure even dimensions for video encoding\n const evenWidth = videoWidth % 2 === 0 ? videoWidth : videoWidth - 1;\n const evenHeight = videoHeight % 2 === 0 ? videoHeight : videoHeight - 1;\n\n const frameDurationMs = 1000 / fps;\n const totalFrames = Math.ceil(renderDurationMs / frameDurationMs);\n const frameDurationS = frameDurationMs / 1000;\n\n return {\n fps,\n codec,\n bitrate,\n filename,\n scale,\n keyFrameInterval,\n startMs,\n endMs,\n renderDurationMs,\n videoWidth: evenWidth,\n videoHeight: evenHeight,\n totalFrames,\n frameDurationMs,\n frameDurationS,\n streaming,\n includeAudio,\n audioBitrate,\n returnBuffer,\n preferredAudioCodecs,\n progressPreviewInterval,\n trimStartMs,\n };\n}\n\n// ============================================================================\n// Utilities (same as renderTimegroupToVideo — not exported from there)\n// ============================================================================\n\nfunction isFileSystemAccessSupported(): boolean {\n return typeof window !== \"undefined\" && \"showSaveFilePicker\" in window;\n}\n\nasync function getFileWritableStream(filename: string): Promise<{\n writable: WritableStream<Uint8Array>;\n close: () => Promise<void>;\n} | null> {\n if (!isFileSystemAccessSupported()) {\n return null;\n }\n\n try {\n const fileHandle = await (window as any).showSaveFilePicker({\n suggestedName: filename,\n types: [{ description: \"MP4 Video\", accept: { \"video/mp4\": [\".mp4\"] } }],\n });\n const writable = await fileHandle.createWritable();\n return {\n writable,\n close: async () => {\n await writable.close();\n },\n };\n } catch (e) {\n if ((e as Error).name !== \"AbortError\") {\n logger.warn(\"[renderVideoToVideo] File System Access failed:\", e);\n }\n return null;\n }\n}\n\nfunction downloadBlob(blob: Blob, filename: string): void {\n const url = URL.createObjectURL(blob);\n const a = document.createElement(\"a\");\n a.href = url;\n a.download = filename;\n document.body.appendChild(a);\n a.click();\n document.body.removeChild(a);\n URL.revokeObjectURL(url);\n}\n\nasync function selectAudioCodec(\n preferredCodecs: AudioCodec[],\n encodingOptions: {\n numberOfChannels: number;\n sampleRate: number;\n bitrate: number;\n },\n): Promise<AudioCodec> {\n for (const codec of preferredCodecs) {\n try {\n const isSupported = await canEncodeAudio(codec, encodingOptions);\n if (isSupported) return codec;\n } catch (e) {\n logger.warn(`[selectAudioCodec] Check failed for ${codec}:`, e);\n }\n }\n const availableCodecs = await getEncodableAudioCodecs(\n undefined,\n encodingOptions,\n );\n throw new NoSupportedAudioCodecError(preferredCodecs, availableCodecs);\n}\n\n// ============================================================================\n// Main render function\n// ============================================================================\n\n/**\n * Render a single EFVideo element directly to MP4.\n *\n * This is the fast path: frames are decoded from the media engine,\n * drawn to an encoding canvas (with CSS filter/opacity applied),\n * and encoded to video. No DOM serialization involved.\n */\nexport async function renderVideoToVideo(\n video: EFVideo,\n options: RenderToVideoOptions = {},\n): Promise<Uint8Array | undefined> {\n const { signal, onProgress } = options;\n\n const checkCancelled = () => {\n if (signal?.aborted) throw new RenderCancelledError();\n };\n\n // Ensure media engine is loaded\n await video.waitForMediaDurations(signal);\n checkCancelled();\n\n const config = await resolveVideoConfig(video, options);\n\n // Suspend the PlaybackController's self-render loop for the duration of\n // this render to prevent concurrent frame fetches from interfering.\n const pc = (video as any).playbackController;\n\n // Read CSS effects once before the frame loop (values don't change during rendering)\n const computedStyle = getComputedStyle(video);\n const cssFilter = computedStyle.filter;\n const cssOpacity = parseFloat(computedStyle.opacity);\n const hasFilter = cssFilter && cssFilter !== \"none\";\n const hasOpacity = cssOpacity < 1;\n\n logger.debug(\n `[renderVideoToVideo] starting: ${config.totalFrames} frames, ` +\n `${config.videoWidth}x${config.videoHeight} @ ${config.fps}fps, ` +\n `trim=[${config.trimStartMs}, -${video.trimEndMs ?? 0}], ` +\n `css: filter=${hasFilter ? cssFilter : \"none\"}, opacity=${cssOpacity}`,\n );\n\n // =========================================================================\n // Set up video encoding\n // =========================================================================\n let output: Output | null = null;\n let videoSource: CanvasSource | null = null;\n let audioSource: AudioBufferSource | null = null;\n let target: BufferTarget | StreamTarget | null = null;\n let fileStream: {\n writable: WritableStream<Uint8Array>;\n close: () => Promise<void>;\n } | null = null;\n let useStreaming = false;\n\n const encodingCanvas = new OffscreenCanvas(\n config.videoWidth,\n config.videoHeight,\n );\n const encodingCtx = encodingCanvas.getContext(\n \"2d\",\n hasFilter || hasOpacity ? { willReadFrequently: true } : undefined,\n );\n if (!encodingCtx) {\n throw new Error(\"Failed to get encoding canvas context\");\n }\n\n if (hasFilter) {\n encodingCtx.filter = cssFilter;\n }\n if (hasOpacity) {\n encodingCtx.globalAlpha = cssOpacity;\n }\n\n if (options.customWritableStream) {\n target = new StreamTarget(options.customWritableStream as any);\n output = new Output({\n format: new Mp4OutputFormat({ fastStart: \"fragmented\" }),\n target,\n });\n useStreaming = true;\n } else if (config.streaming) {\n fileStream = await getFileWritableStream(config.filename);\n useStreaming = fileStream !== null;\n\n if (useStreaming && fileStream) {\n target = new StreamTarget(fileStream.writable as any);\n output = new Output({\n format: new Mp4OutputFormat({ fastStart: \"fragmented\" }),\n target,\n });\n }\n }\n\n if (!target) {\n target = new BufferTarget();\n output = new Output({ format: new Mp4OutputFormat(), target });\n }\n\n if (!output) {\n throw new Error(\"Output not initialized\");\n }\n\n const videoConfig: VideoEncodingConfig = {\n codec: config.codec,\n bitrate: config.bitrate,\n keyFrameInterval: config.keyFrameInterval,\n };\n\n // Use CanvasSource directly - filter and opacity don't require special handling\n videoSource = new CanvasSource(encodingCanvas, videoConfig);\n output.addVideoTrack(videoSource);\n\n if (config.includeAudio) {\n try {\n const selectedCodec = await selectAudioCodec(\n config.preferredAudioCodecs,\n {\n numberOfChannels: 2,\n sampleRate: 48000,\n bitrate: config.audioBitrate,\n },\n );\n const audioConfig: AudioEncodingConfig = {\n codec: selectedCodec,\n bitrate: config.audioBitrate,\n };\n audioSource = new AudioBufferSource(audioConfig);\n output.addAudioTrack(audioSource);\n } catch (e) {\n logger.warn(\n \"[renderVideoToVideo] Audio codec selection failed, rendering without audio:\",\n e,\n );\n }\n }\n\n await output.start();\n\n // =========================================================================\n // Frame loop\n // =========================================================================\n const renderStartTime = performance.now();\n let lastRenderedAudioEndMs = config.startMs;\n const audioChunkDurationMs = 2000;\n\n let thumbCanvas: HTMLCanvasElement | null = null;\n let thumbCtx: CanvasRenderingContext2D | null = null;\n\n if (config.progressPreviewInterval > 0) {\n const thumbScale = 160 / config.videoWidth;\n thumbCanvas = document.createElement(\"canvas\");\n thumbCanvas.width = Math.round(config.videoWidth * thumbScale);\n thumbCanvas.height = Math.round(config.videoHeight * thumbScale);\n thumbCtx = thumbCanvas.getContext(\"2d\");\n }\n\n let totalSeekMs = 0;\n let totalDrawMs = 0;\n let totalEncodeMs = 0;\n\n pc?.suspendSelfRender();\n\n try {\n for (let frameIndex = 0; frameIndex < config.totalFrames; frameIndex++) {\n checkCancelled();\n\n const timelineTimeMs =\n config.startMs + frameIndex * config.frameDurationMs;\n const sourceTimeMs = timelineTimeMs + config.trimStartMs;\n const timestampS = (frameIndex * config.frameDurationMs) / 1000;\n\n // Decode frame\n const seekStart = performance.now();\n const videoFrame = await video.getVideoFrameAtSourceTime(sourceTimeMs, {\n quality: \"main\",\n signal,\n });\n totalSeekMs += performance.now() - seekStart;\n\n try {\n const drawStart = performance.now();\n\n encodingCtx.drawImage(\n videoFrame,\n 0,\n 0,\n videoFrame.displayWidth,\n videoFrame.displayHeight,\n 0,\n 0,\n config.videoWidth,\n config.videoHeight,\n );\n\n totalDrawMs += performance.now() - drawStart;\n } finally {\n videoFrame.close();\n }\n\n // Encode frame\n const encodeStart = performance.now();\n await videoSource!.add(timestampS, config.frameDurationS);\n totalEncodeMs += performance.now() - encodeStart;\n\n // Render audio in chunks\n if (\n audioSource &&\n timelineTimeMs >= lastRenderedAudioEndMs + audioChunkDurationMs\n ) {\n const chunkEndMs = Math.min(\n timelineTimeMs + audioChunkDurationMs,\n config.endMs,\n );\n try {\n const audioBuffer = await video.renderAudio(\n lastRenderedAudioEndMs,\n chunkEndMs,\n );\n if (audioBuffer && audioBuffer.length > 0) {\n await audioSource.add(audioBuffer);\n }\n } catch (e) {\n // Audio render failures are non-fatal\n }\n lastRenderedAudioEndMs = chunkEndMs;\n }\n\n // Progress preview thumbnail\n if (\n thumbCanvas &&\n thumbCtx &&\n frameIndex % config.progressPreviewInterval === 0\n ) {\n thumbCtx.drawImage(\n encodingCanvas as any,\n 0,\n 0,\n thumbCanvas.width,\n thumbCanvas.height,\n );\n }\n\n // Progress reporting\n const currentFrame = frameIndex + 1;\n const progress = currentFrame / config.totalFrames;\n const renderedMs = currentFrame * config.frameDurationMs;\n const elapsedMs = performance.now() - renderStartTime;\n const msPerFrame = elapsedMs / currentFrame;\n const remainingFrames = config.totalFrames - currentFrame;\n const estimatedRemainingMs = remainingFrames * msPerFrame;\n const speedMultiplier = renderedMs / elapsedMs;\n\n onProgress?.({\n progress,\n currentFrame,\n totalFrames: config.totalFrames,\n renderedMs,\n totalDurationMs: config.renderDurationMs,\n elapsedMs,\n estimatedRemainingMs,\n speedMultiplier,\n framePreviewCanvas: thumbCanvas || undefined,\n });\n }\n\n // Render remaining audio\n if (audioSource && lastRenderedAudioEndMs < config.endMs) {\n try {\n const audioBuffer = await video.renderAudio(\n lastRenderedAudioEndMs,\n config.endMs,\n );\n if (audioBuffer && audioBuffer.length > 0) {\n await audioSource.add(audioBuffer);\n }\n } catch (e) {\n // Audio render failures are non-fatal\n }\n }\n\n // =========================================================================\n // Finalize\n // =========================================================================\n const totalElapsed = performance.now() - renderStartTime;\n logger.debug(\n `[renderVideoToVideo] complete: ${config.totalFrames} frames in ${totalElapsed.toFixed(0)}ms ` +\n `(seek=${totalSeekMs.toFixed(0)}ms, draw=${totalDrawMs.toFixed(0)}ms, encode=${totalEncodeMs.toFixed(0)}ms) ` +\n `speed=${(config.renderDurationMs / totalElapsed).toFixed(1)}x`,\n );\n\n await output.finalize();\n\n if (useStreaming) {\n if (fileStream) {\n await fileStream.close();\n }\n return undefined;\n } else {\n const bufferTarget = target as BufferTarget;\n const videoBuffer = bufferTarget.buffer;\n if (!videoBuffer) {\n throw new Error(\"Video encoding failed: no buffer produced\");\n }\n\n if (config.returnBuffer) {\n return new Uint8Array(videoBuffer);\n }\n\n const videoBlob = new Blob([videoBuffer], { type: \"video/mp4\" });\n downloadBlob(videoBlob, config.filename);\n return undefined;\n }\n } catch (error) {\n // Clean up output on failure\n try {\n await output?.finalize();\n } catch {\n // Ignore finalize errors during cleanup\n }\n throw error;\n } finally {\n pc?.resumeSelfRender();\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"renderVideoToVideo.js","names":["width: number","height: number","output: Output | null","videoSource: CanvasSource | null","audioSource: AudioBufferSource | null","target: BufferTarget | StreamTarget | null","fileStream: {\n writable: WritableStream<Uint8Array>;\n close: () => Promise<void>;\n } | null","thumbCanvas: HTMLCanvasElement | null","thumbCtx: CanvasRenderingContext2D | null"],"sources":["../../src/preview/renderVideoToVideo.ts"],"sourcesContent":["/**\n * Direct video-to-video rendering — fast path for single video elements.\n *\n * Bypasses the full DOM serialization pipeline (foreignObject/native canvas)\n * by decoding frames directly from the media engine and re-encoding to MP4.\n *\n * Supports CSS effects via canvas 2D context:\n * - filter (ctx.filter)\n * - opacity (ctx.globalAlpha)\n */\n\nimport {\n Output,\n Mp4OutputFormat,\n BufferTarget,\n StreamTarget,\n CanvasSource,\n AudioBufferSource,\n canEncodeAudio,\n getEncodableAudioCodecs,\n type VideoEncodingConfig,\n type AudioEncodingConfig,\n type AudioCodec,\n} from \"mediabunny\";\nimport type { EFVideo } from \"../elements/EFVideo.js\";\nimport { NoSupportedAudioCodecError, RenderCancelledError } from \"./renderTimegroupToVideo.js\";\nimport type { RenderToVideoOptions } from \"./renderTimegroupToVideo.types.js\";\nimport { logger } from \"./logger.js\";\n\n// ============================================================================\n// Configuration\n// ============================================================================\n\ninterface ResolvedVideoConfig {\n fps: number;\n codec: \"avc\" | \"hevc\" | \"vp9\" | \"av1\" | \"vp8\";\n bitrate: number;\n filename: string;\n scale: number;\n keyFrameInterval: number;\n startMs: number;\n endMs: number;\n renderDurationMs: number;\n videoWidth: number;\n videoHeight: number;\n totalFrames: number;\n frameDurationMs: number;\n frameDurationS: number;\n streaming: boolean;\n includeAudio: boolean;\n audioBitrate: number;\n returnBuffer: boolean;\n preferredAudioCodecs: AudioCodec[];\n progressPreviewInterval: number;\n trimStartMs: number;\n}\n\nasync function resolveVideoConfig(\n video: EFVideo,\n options: RenderToVideoOptions = {},\n): Promise<ResolvedVideoConfig> {\n const fps = options.fps ?? 30;\n const codec = options.codec ?? \"avc\";\n const bitrate = options.bitrate ?? 8_000_000;\n const filename = options.filename ?? \"video-export.mp4\";\n const scale = options.scale ?? 1;\n const keyFrameInterval = options.keyFrameInterval ?? 2;\n const streaming = options.streaming ?? false;\n const includeAudio = options.includeAudio ?? true;\n const audioBitrate = options.audioBitrate ?? 128_000;\n const returnBuffer = options.returnBuffer ?? false;\n const preferredAudioCodecs = options.preferredAudioCodecs ?? [\"aac\", \"opus\"];\n const progressPreviewInterval = options.progressPreviewInterval ?? 60;\n\n const trimStartMs = video.trimStartMs ?? 0;\n const trimEndMs = video.trimEndMs ?? 0;\n const intrinsicDurationMs = video.intrinsicDurationMs;\n\n if (!intrinsicDurationMs || intrinsicDurationMs <= 0) {\n throw new Error(\"Video has no intrinsic duration. Ensure the media engine is loaded.\");\n }\n\n const effectiveDurationMs = intrinsicDurationMs - trimStartMs - trimEndMs;\n if (effectiveDurationMs <= 0) {\n throw new Error(\n `Invalid trim range: trimStart=${trimStartMs}ms, trimEnd=${trimEndMs}ms, ` +\n `intrinsicDuration=${intrinsicDurationMs}ms leaves no content.`,\n );\n }\n\n const startMs = options.fromMs !== undefined ? Math.max(0, options.fromMs) : 0;\n const endMs =\n options.toMs !== undefined ? Math.min(options.toMs, effectiveDurationMs) : effectiveDurationMs;\n const renderDurationMs = endMs - startMs;\n\n if (renderDurationMs <= 0) {\n throw new Error(`Invalid render range: from ${startMs}ms to ${endMs}ms`);\n }\n\n let width: number;\n let height: number;\n\n // Decode first frame to determine dimensions\n {\n const firstFrame = await video.getVideoFrameAtSourceTime(trimStartMs, {\n quality: \"main\",\n });\n try {\n width = firstFrame.displayWidth;\n height = firstFrame.displayHeight;\n } finally {\n firstFrame.close();\n }\n }\n\n const videoWidth = Math.floor(width * scale);\n const videoHeight = Math.floor(height * scale);\n // Ensure even dimensions for video encoding\n const evenWidth = videoWidth % 2 === 0 ? videoWidth : videoWidth - 1;\n const evenHeight = videoHeight % 2 === 0 ? videoHeight : videoHeight - 1;\n\n const frameDurationMs = 1000 / fps;\n const totalFrames = Math.ceil(renderDurationMs / frameDurationMs);\n const frameDurationS = frameDurationMs / 1000;\n\n return {\n fps,\n codec,\n bitrate,\n filename,\n scale,\n keyFrameInterval,\n startMs,\n endMs,\n renderDurationMs,\n videoWidth: evenWidth,\n videoHeight: evenHeight,\n totalFrames,\n frameDurationMs,\n frameDurationS,\n streaming,\n includeAudio,\n audioBitrate,\n returnBuffer,\n preferredAudioCodecs,\n progressPreviewInterval,\n trimStartMs,\n };\n}\n\n// ============================================================================\n// Utilities (same as renderTimegroupToVideo — not exported from there)\n// ============================================================================\n\nfunction isFileSystemAccessSupported(): boolean {\n return typeof window !== \"undefined\" && \"showSaveFilePicker\" in window;\n}\n\nasync function getFileWritableStream(filename: string): Promise<{\n writable: WritableStream<Uint8Array>;\n close: () => Promise<void>;\n} | null> {\n if (!isFileSystemAccessSupported()) {\n return null;\n }\n\n try {\n const fileHandle = await (window as any).showSaveFilePicker({\n suggestedName: filename,\n types: [{ description: \"MP4 Video\", accept: { \"video/mp4\": [\".mp4\"] } }],\n });\n const writable = await fileHandle.createWritable();\n return {\n writable,\n close: async () => {\n await writable.close();\n },\n };\n } catch (e) {\n if ((e as Error).name !== \"AbortError\") {\n logger.warn(\"[renderVideoToVideo] File System Access failed:\", e);\n }\n return null;\n }\n}\n\nfunction downloadBlob(blob: Blob, filename: string): void {\n const url = URL.createObjectURL(blob);\n const a = document.createElement(\"a\");\n a.href = url;\n a.download = filename;\n document.body.appendChild(a);\n a.click();\n document.body.removeChild(a);\n URL.revokeObjectURL(url);\n}\n\nasync function selectAudioCodec(\n preferredCodecs: AudioCodec[],\n encodingOptions: {\n numberOfChannels: number;\n sampleRate: number;\n bitrate: number;\n },\n): Promise<AudioCodec> {\n for (const codec of preferredCodecs) {\n try {\n const isSupported = await canEncodeAudio(codec, encodingOptions);\n if (isSupported) return codec;\n } catch (e) {\n logger.warn(`[selectAudioCodec] Check failed for ${codec}:`, e);\n }\n }\n const availableCodecs = await getEncodableAudioCodecs(undefined, encodingOptions);\n throw new NoSupportedAudioCodecError(preferredCodecs, availableCodecs);\n}\n\n// ============================================================================\n// Main render function\n// ============================================================================\n\n/**\n * Render a single EFVideo element directly to MP4.\n *\n * This is the fast path: frames are decoded from the media engine,\n * drawn to an encoding canvas (with CSS filter/opacity applied),\n * and encoded to video. No DOM serialization involved.\n */\nexport async function renderVideoToVideo(\n video: EFVideo,\n options: RenderToVideoOptions = {},\n): Promise<Uint8Array | undefined> {\n const { signal, onProgress } = options;\n\n const checkCancelled = () => {\n if (signal?.aborted) throw new RenderCancelledError();\n };\n\n // Ensure media engine is loaded\n await video.waitForMediaDurations(signal);\n checkCancelled();\n\n const config = await resolveVideoConfig(video, options);\n\n // Suspend the PlaybackController's self-render loop for the duration of\n // this render to prevent concurrent frame fetches from interfering.\n const pc = (video as any).playbackController;\n\n // Read CSS effects once before the frame loop (values don't change during rendering)\n const computedStyle = getComputedStyle(video);\n const cssFilter = computedStyle.filter;\n const cssOpacity = parseFloat(computedStyle.opacity);\n const hasFilter = cssFilter && cssFilter !== \"none\";\n const hasOpacity = cssOpacity < 1;\n\n logger.debug(\n `[renderVideoToVideo] starting: ${config.totalFrames} frames, ` +\n `${config.videoWidth}x${config.videoHeight} @ ${config.fps}fps, ` +\n `trim=[${config.trimStartMs}, -${video.trimEndMs ?? 0}], ` +\n `css: filter=${hasFilter ? cssFilter : \"none\"}, opacity=${cssOpacity}`,\n );\n\n // =========================================================================\n // Set up video encoding\n // =========================================================================\n let output: Output | null = null;\n let videoSource: CanvasSource | null = null;\n let audioSource: AudioBufferSource | null = null;\n let target: BufferTarget | StreamTarget | null = null;\n let fileStream: {\n writable: WritableStream<Uint8Array>;\n close: () => Promise<void>;\n } | null = null;\n let useStreaming = false;\n\n const encodingCanvas = new OffscreenCanvas(config.videoWidth, config.videoHeight);\n const encodingCtx = encodingCanvas.getContext(\n \"2d\",\n hasFilter || hasOpacity ? { willReadFrequently: true } : undefined,\n );\n if (!encodingCtx) {\n throw new Error(\"Failed to get encoding canvas context\");\n }\n\n if (hasFilter) {\n encodingCtx.filter = cssFilter;\n }\n if (hasOpacity) {\n encodingCtx.globalAlpha = cssOpacity;\n }\n\n if (options.customWritableStream) {\n target = new StreamTarget(options.customWritableStream as any);\n output = new Output({\n format: new Mp4OutputFormat({ fastStart: \"fragmented\" }),\n target,\n });\n useStreaming = true;\n } else if (config.streaming) {\n fileStream = await getFileWritableStream(config.filename);\n useStreaming = fileStream !== null;\n\n if (useStreaming && fileStream) {\n target = new StreamTarget(fileStream.writable as any);\n output = new Output({\n format: new Mp4OutputFormat({ fastStart: \"fragmented\" }),\n target,\n });\n }\n }\n\n if (!target) {\n target = new BufferTarget();\n output = new Output({ format: new Mp4OutputFormat(), target });\n }\n\n if (!output) {\n throw new Error(\"Output not initialized\");\n }\n\n const videoConfig: VideoEncodingConfig = {\n codec: config.codec,\n bitrate: config.bitrate,\n keyFrameInterval: config.keyFrameInterval,\n };\n\n // Use CanvasSource directly - filter and opacity don't require special handling\n videoSource = new CanvasSource(encodingCanvas, videoConfig);\n output.addVideoTrack(videoSource);\n\n if (config.includeAudio) {\n try {\n const selectedCodec = await selectAudioCodec(config.preferredAudioCodecs, {\n numberOfChannels: 2,\n sampleRate: 48000,\n bitrate: config.audioBitrate,\n });\n const audioConfig: AudioEncodingConfig = {\n codec: selectedCodec,\n bitrate: config.audioBitrate,\n };\n audioSource = new AudioBufferSource(audioConfig);\n output.addAudioTrack(audioSource);\n } catch (e) {\n logger.warn(\"[renderVideoToVideo] Audio codec selection failed, rendering without audio:\", e);\n }\n }\n\n await output.start();\n\n // =========================================================================\n // Frame loop\n // =========================================================================\n const renderStartTime = performance.now();\n let lastRenderedAudioEndMs = config.startMs;\n const audioChunkDurationMs = 2000;\n\n let thumbCanvas: HTMLCanvasElement | null = null;\n let thumbCtx: CanvasRenderingContext2D | null = null;\n\n if (config.progressPreviewInterval > 0) {\n const thumbScale = 160 / config.videoWidth;\n thumbCanvas = document.createElement(\"canvas\");\n thumbCanvas.width = Math.round(config.videoWidth * thumbScale);\n thumbCanvas.height = Math.round(config.videoHeight * thumbScale);\n thumbCtx = thumbCanvas.getContext(\"2d\");\n }\n\n let totalSeekMs = 0;\n let totalDrawMs = 0;\n let totalEncodeMs = 0;\n\n pc?.suspendSelfRender();\n\n try {\n for (let frameIndex = 0; frameIndex < config.totalFrames; frameIndex++) {\n checkCancelled();\n\n const timelineTimeMs = config.startMs + frameIndex * config.frameDurationMs;\n const sourceTimeMs = timelineTimeMs + config.trimStartMs;\n const timestampS = (frameIndex * config.frameDurationMs) / 1000;\n\n // Decode frame\n const seekStart = performance.now();\n const videoFrame = await video.getVideoFrameAtSourceTime(sourceTimeMs, {\n quality: \"main\",\n signal,\n });\n totalSeekMs += performance.now() - seekStart;\n\n try {\n const drawStart = performance.now();\n\n encodingCtx.drawImage(\n videoFrame,\n 0,\n 0,\n videoFrame.displayWidth,\n videoFrame.displayHeight,\n 0,\n 0,\n config.videoWidth,\n config.videoHeight,\n );\n\n totalDrawMs += performance.now() - drawStart;\n } finally {\n videoFrame.close();\n }\n\n // Encode frame\n const encodeStart = performance.now();\n await videoSource!.add(timestampS, config.frameDurationS);\n totalEncodeMs += performance.now() - encodeStart;\n\n // Render audio in chunks\n if (audioSource && timelineTimeMs >= lastRenderedAudioEndMs + audioChunkDurationMs) {\n const chunkEndMs = Math.min(timelineTimeMs + audioChunkDurationMs, config.endMs);\n try {\n const audioBuffer = await video.renderAudio(lastRenderedAudioEndMs, chunkEndMs);\n if (audioBuffer && audioBuffer.length > 0) {\n await audioSource.add(audioBuffer);\n }\n } catch (_e) {\n // Audio render failures are non-fatal\n }\n lastRenderedAudioEndMs = chunkEndMs;\n }\n\n // Progress preview thumbnail\n if (thumbCanvas && thumbCtx && frameIndex % config.progressPreviewInterval === 0) {\n thumbCtx.drawImage(encodingCanvas as any, 0, 0, thumbCanvas.width, thumbCanvas.height);\n }\n\n // Progress reporting\n const currentFrame = frameIndex + 1;\n const progress = currentFrame / config.totalFrames;\n const renderedMs = currentFrame * config.frameDurationMs;\n const elapsedMs = performance.now() - renderStartTime;\n const msPerFrame = elapsedMs / currentFrame;\n const remainingFrames = config.totalFrames - currentFrame;\n const estimatedRemainingMs = remainingFrames * msPerFrame;\n const speedMultiplier = renderedMs / elapsedMs;\n\n onProgress?.({\n progress,\n currentFrame,\n totalFrames: config.totalFrames,\n renderedMs,\n totalDurationMs: config.renderDurationMs,\n elapsedMs,\n estimatedRemainingMs,\n speedMultiplier,\n framePreviewCanvas: thumbCanvas || undefined,\n });\n }\n\n // Render remaining audio\n if (audioSource && lastRenderedAudioEndMs < config.endMs) {\n try {\n const audioBuffer = await video.renderAudio(lastRenderedAudioEndMs, config.endMs);\n if (audioBuffer && audioBuffer.length > 0) {\n await audioSource.add(audioBuffer);\n }\n } catch (_e) {\n // Audio render failures are non-fatal\n }\n }\n\n // =========================================================================\n // Finalize\n // =========================================================================\n const totalElapsed = performance.now() - renderStartTime;\n logger.debug(\n `[renderVideoToVideo] complete: ${config.totalFrames} frames in ${totalElapsed.toFixed(0)}ms ` +\n `(seek=${totalSeekMs.toFixed(0)}ms, draw=${totalDrawMs.toFixed(0)}ms, encode=${totalEncodeMs.toFixed(0)}ms) ` +\n `speed=${(config.renderDurationMs / totalElapsed).toFixed(1)}x`,\n );\n\n await output.finalize();\n\n if (useStreaming) {\n if (fileStream) {\n await fileStream.close();\n }\n return undefined;\n } else {\n const bufferTarget = target as BufferTarget;\n const videoBuffer = bufferTarget.buffer;\n if (!videoBuffer) {\n throw new Error(\"Video encoding failed: no buffer produced\");\n }\n\n if (config.returnBuffer) {\n return new Uint8Array(videoBuffer);\n }\n\n const videoBlob = new Blob([videoBuffer], { type: \"video/mp4\" });\n downloadBlob(videoBlob, config.filename);\n return undefined;\n }\n } catch (error) {\n // Clean up output on failure\n try {\n await output?.finalize();\n } catch {\n // Ignore finalize errors during cleanup\n }\n throw error;\n } finally {\n pc?.resumeSelfRender();\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"renderers.js","names":[],"sources":["../../src/preview/renderers.ts"],"sourcesContent":["/**\n * Renderer strategy pattern for HTML-to-image rendering.\n * Provides a unified interface for native (drawElementImage) and foreignObject paths.\n */\n\nimport {
|
|
1
|
+
{"version":3,"file":"renderers.js","names":[],"sources":["../../src/preview/renderers.ts"],"sourcesContent":["/**\n * Renderer strategy pattern for HTML-to-image rendering.\n * Provides a unified interface for native (drawElementImage) and foreignObject paths.\n */\n\nimport { isNativeCanvasApiAvailable, getRenderMode, type RenderMode } from \"./previewSettings.js\";\n\n/**\n * Options for rendering HTML to an image or canvas.\n */\nexport interface RenderOptions {\n /** Skip device pixel ratio scaling (render at logical pixels) */\n skipDprScaling?: boolean;\n /** Scale factor for encoding internal canvases (foreignObject only) */\n canvasScale?: number;\n /** Whether to reuse an existing canvas (native only) */\n reuseCanvas?: HTMLCanvasElement;\n}\n\n/**\n * Result of a render operation.\n * Native path returns a canvas, foreignObject path returns an image.\n */\nexport type RenderResult = HTMLCanvasElement | HTMLImageElement;\n\n/**\n * Renderer interface for HTML-to-image conversion.\n */\nexport interface Renderer {\n /** The render mode this renderer implements */\n readonly mode: RenderMode;\n\n /**\n * Render an HTML container to an image or canvas.\n * @param container - The HTML element to render\n * @param width - Target width in logical pixels\n * @param height - Target height in logical pixels\n * @param options - Rendering options\n * @returns Promise resolving to a canvas or image element\n */\n render(\n container: HTMLElement,\n width: number,\n height: number,\n options?: RenderOptions,\n ): Promise<RenderResult>;\n\n /**\n * Check if this renderer is available in the current environment.\n */\n isAvailable(): boolean;\n}\n\n/**\n * Get the effective render mode, validating that native is available when selected.\n * Falls back to foreignObject if native is selected but not available.\n */\nexport function getEffectiveRenderMode(): RenderMode {\n const mode = getRenderMode();\n\n if (mode === \"native\" && !isNativeCanvasApiAvailable()) {\n return \"foreignObject\";\n }\n\n return mode;\n}\n\n/**\n * Check if a render result is a canvas element.\n */\nexport function isCanvas(result: RenderResult): result is HTMLCanvasElement {\n return result instanceof HTMLCanvasElement;\n}\n\n/**\n * Check if a render result is an image element.\n */\nexport function isImage(result: RenderResult): result is HTMLImageElement {\n return result instanceof HTMLImageElement;\n}\n"],"mappings":";;;;;;;AAyDA,SAAgB,yBAAqC;CACnD,MAAM,OAAO,eAAe;AAE5B,KAAI,SAAS,YAAY,CAAC,4BAA4B,CACpD,QAAO;AAGT,QAAO;;;;;AAMT,SAAgB,SAAS,QAAmD;AAC1E,QAAO,kBAAkB;;;;;AAM3B,SAAgB,QAAQ,QAAkD;AACxE,QAAO,kBAAkB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ScaleConfig.js","names":[],"sources":["../../../src/preview/rendering/ScaleConfig.ts"],"sourcesContent":["/**\n * ScaleConfig - Unified scaling configuration for timeline serialization.\n *\n * Consolidates the multi-stage scaling architecture into a single,\n * well-defined abstraction with clear contracts.\n *\n * Previously, scaling was applied in 4 separate stages with implicit contracts:\n * 1. captureTimelineToDataUri: scaled output dimensions\n * 2. captureElementParts: CSS transform wrapper for DOM content\n * 3. serializeCanvas: independent optimalScale calculation per canvas\n * 4. encodeCanvasesInParallel: received pre-scaled snapshots\n *\n * Now, ScaleConfig centralizes all scaling logic and makes the contracts explicit.\n */\n\nexport interface CanvasScaleParams {\n /** Natural canvas pixel dimensions */\n naturalWidth: number;\n naturalHeight: number;\n /** CSS display dimensions (how big it appears) */\n displayWidth: number;\n displayHeight: number;\n}\n\n/**\n * Immutable scaling configuration for a serialization operation.\n *\n * All scaling decisions are computed once at construction and cached.\n * This ensures consistency across all stages of serialization.\n */\nexport class ScaleConfig {\n /** User-specified export scale (e.g., 0.25 for thumbnails, 1.0 for full resolution) */\n readonly exportScale: number;\n\n /** Input dimensions (before scaling) */\n readonly inputWidth: number;\n readonly inputHeight: number;\n\n /** Output SVG dimensions (after scaling) */\n readonly outputWidth: number;\n readonly outputHeight: number;\n\n /** Whether DOM content needs CSS transform:scale() wrapper */\n readonly needsDOMScaling: boolean;\n\n /** Quality multiplier for canvas encoding (1.5x for sharpness) */\n readonly qualityMultiplier: number = 1.5;\n\n constructor(width: number, height: number, exportScale: number) {\n this.inputWidth = width;\n this.inputHeight = height;\n this.exportScale = exportScale;\n\n // Compute output dimensions (Stage 1)\n this.outputWidth = Math.floor(width * exportScale);\n this.outputHeight = Math.floor(height * exportScale);\n\n // Determine if DOM needs CSS scaling (Stage 2)\n this.needsDOMScaling = exportScale < 1;\n\n // Freeze to ensure immutability\n Object.freeze(this);\n }\n\n /**\n * Compute optimal encoding scale for a canvas element.\n *\n * This is Stage 3 of the scaling architecture. Canvas pixels are scaled\n * independently from DOM content because they have intrinsic resolution.\n *\n * Algorithm:\n * 1. Calculate display scale (CSS size vs natural size)\n * 2. Multiply by export scale\n * 3. Multiply by quality multiplier (1.5x for sharpness)\n * 4. Cap at 1.0 (never upscale beyond natural resolution)\n *\n * @param params - Canvas dimensions (natural and display)\n * @returns Optimal scale for encoding (0.0 to 1.0)\n */\n computeCanvasScale(params: CanvasScaleParams): number {\n const { naturalWidth, naturalHeight, displayWidth, displayHeight } = params;\n\n // Calculate how much smaller the display is vs natural size\n const displayScaleX = displayWidth / naturalWidth;\n const displayScaleY = displayHeight / naturalHeight;\n const displayScale = Math.min(displayScaleX, displayScaleY);\n\n // Combine display scale, export scale, and quality multiplier\n // Cap at 1.0 to never upscale beyond natural resolution\n const optimalScale = Math.min(
|
|
1
|
+
{"version":3,"file":"ScaleConfig.js","names":[],"sources":["../../../src/preview/rendering/ScaleConfig.ts"],"sourcesContent":["/**\n * ScaleConfig - Unified scaling configuration for timeline serialization.\n *\n * Consolidates the multi-stage scaling architecture into a single,\n * well-defined abstraction with clear contracts.\n *\n * Previously, scaling was applied in 4 separate stages with implicit contracts:\n * 1. captureTimelineToDataUri: scaled output dimensions\n * 2. captureElementParts: CSS transform wrapper for DOM content\n * 3. serializeCanvas: independent optimalScale calculation per canvas\n * 4. encodeCanvasesInParallel: received pre-scaled snapshots\n *\n * Now, ScaleConfig centralizes all scaling logic and makes the contracts explicit.\n */\n\nexport interface CanvasScaleParams {\n /** Natural canvas pixel dimensions */\n naturalWidth: number;\n naturalHeight: number;\n /** CSS display dimensions (how big it appears) */\n displayWidth: number;\n displayHeight: number;\n}\n\n/**\n * Immutable scaling configuration for a serialization operation.\n *\n * All scaling decisions are computed once at construction and cached.\n * This ensures consistency across all stages of serialization.\n */\nexport class ScaleConfig {\n /** User-specified export scale (e.g., 0.25 for thumbnails, 1.0 for full resolution) */\n readonly exportScale: number;\n\n /** Input dimensions (before scaling) */\n readonly inputWidth: number;\n readonly inputHeight: number;\n\n /** Output SVG dimensions (after scaling) */\n readonly outputWidth: number;\n readonly outputHeight: number;\n\n /** Whether DOM content needs CSS transform:scale() wrapper */\n readonly needsDOMScaling: boolean;\n\n /** Quality multiplier for canvas encoding (1.5x for sharpness) */\n readonly qualityMultiplier: number = 1.5;\n\n constructor(width: number, height: number, exportScale: number) {\n this.inputWidth = width;\n this.inputHeight = height;\n this.exportScale = exportScale;\n\n // Compute output dimensions (Stage 1)\n this.outputWidth = Math.floor(width * exportScale);\n this.outputHeight = Math.floor(height * exportScale);\n\n // Determine if DOM needs CSS scaling (Stage 2)\n this.needsDOMScaling = exportScale < 1;\n\n // Freeze to ensure immutability\n Object.freeze(this);\n }\n\n /**\n * Compute optimal encoding scale for a canvas element.\n *\n * This is Stage 3 of the scaling architecture. Canvas pixels are scaled\n * independently from DOM content because they have intrinsic resolution.\n *\n * Algorithm:\n * 1. Calculate display scale (CSS size vs natural size)\n * 2. Multiply by export scale\n * 3. Multiply by quality multiplier (1.5x for sharpness)\n * 4. Cap at 1.0 (never upscale beyond natural resolution)\n *\n * @param params - Canvas dimensions (natural and display)\n * @returns Optimal scale for encoding (0.0 to 1.0)\n */\n computeCanvasScale(params: CanvasScaleParams): number {\n const { naturalWidth, naturalHeight, displayWidth, displayHeight } = params;\n\n // Calculate how much smaller the display is vs natural size\n const displayScaleX = displayWidth / naturalWidth;\n const displayScaleY = displayHeight / naturalHeight;\n const displayScale = Math.min(displayScaleX, displayScaleY);\n\n // Combine display scale, export scale, and quality multiplier\n // Cap at 1.0 to never upscale beyond natural resolution\n const optimalScale = Math.min(1.0, displayScale * this.exportScale * this.qualityMultiplier);\n\n return optimalScale;\n }\n\n /**\n * Get the CSS transform value for DOM scaling.\n * Returns null if no scaling is needed.\n */\n getDOMTransform(): string | null {\n return this.needsDOMScaling ? `scale(${this.exportScale})` : null;\n }\n\n /**\n * Get the wrapper dimensions for the CSS transform.\n * When DOM is scaled, the wrapper must be larger to accommodate\n * the scaled-down content.\n */\n getDOMWrapperDimensions(): { width: number; height: number } {\n if (!this.needsDOMScaling) {\n return { width: this.outputWidth, height: this.outputHeight };\n }\n\n return {\n width: Math.floor(this.outputWidth / this.exportScale),\n height: Math.floor(this.outputHeight / this.exportScale),\n };\n }\n\n /**\n * Create a ScaleConfig from legacy options.\n * Maintains backward compatibility with existing callsites.\n */\n static fromOptions(width: number, height: number, canvasScale: number): ScaleConfig {\n return new ScaleConfig(width, height, canvasScale);\n }\n}\n"],"mappings":";;;;;;;AA8BA,IAAa,cAAb,MAAa,YAAY;CAkBvB,YAAY,OAAe,QAAgB,aAAqB;2BAF3B;AAGnC,OAAK,aAAa;AAClB,OAAK,cAAc;AACnB,OAAK,cAAc;AAGnB,OAAK,cAAc,KAAK,MAAM,QAAQ,YAAY;AAClD,OAAK,eAAe,KAAK,MAAM,SAAS,YAAY;AAGpD,OAAK,kBAAkB,cAAc;AAGrC,SAAO,OAAO,KAAK;;;;;;;;;;;;;;;;;CAkBrB,mBAAmB,QAAmC;EACpD,MAAM,EAAE,cAAc,eAAe,cAAc,kBAAkB;EAGrE,MAAM,gBAAgB,eAAe;EACrC,MAAM,gBAAgB,gBAAgB;EACtC,MAAM,eAAe,KAAK,IAAI,eAAe,cAAc;AAM3D,SAFqB,KAAK,IAAI,GAAK,eAAe,KAAK,cAAc,KAAK,kBAAkB;;;;;;CAS9F,kBAAiC;AAC/B,SAAO,KAAK,kBAAkB,SAAS,KAAK,YAAY,KAAK;;;;;;;CAQ/D,0BAA6D;AAC3D,MAAI,CAAC,KAAK,gBACR,QAAO;GAAE,OAAO,KAAK;GAAa,QAAQ,KAAK;GAAc;AAG/D,SAAO;GACL,OAAO,KAAK,MAAM,KAAK,cAAc,KAAK,YAAY;GACtD,QAAQ,KAAK,MAAM,KAAK,eAAe,KAAK,YAAY;GACzD;;;;;;CAOH,OAAO,YAAY,OAAe,QAAgB,aAAkC;AAClF,SAAO,IAAI,YAAY,OAAO,QAAQ,YAAY"}
|