@editframe/elements 0.37.3-beta → 0.38.1
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/EF_FRAMEGEN.js +17 -14
- package/dist/EF_FRAMEGEN.js.map +1 -1
- package/dist/EF_RENDERING.js.map +1 -1
- package/dist/canvas/EFCanvas.d.ts +9 -2
- package/dist/canvas/EFCanvas.js +14 -4
- package/dist/canvas/EFCanvas.js.map +1 -1
- package/dist/canvas/EFCanvasItem.d.ts +2 -2
- package/dist/canvas/overlays/SelectionOverlay.d.ts +10 -2
- package/dist/canvas/overlays/SelectionOverlay.js +5 -12
- package/dist/canvas/overlays/SelectionOverlay.js.map +1 -1
- package/dist/canvas/overlays/overlayState.js.map +1 -1
- package/dist/canvas/selection/SelectionController.js.map +1 -1
- package/dist/elements/EFAudio.d.ts +1 -11
- package/dist/elements/EFAudio.js +2 -10
- package/dist/elements/EFAudio.js.map +1 -1
- package/dist/elements/EFCaptions.d.ts +5 -9
- package/dist/elements/EFCaptions.js +34 -11
- package/dist/elements/EFCaptions.js.map +1 -1
- package/dist/elements/EFImage.d.ts +10 -8
- package/dist/elements/EFImage.js +117 -32
- package/dist/elements/EFImage.js.map +1 -1
- package/dist/elements/EFMedia/AssetMediaEngine.js +2 -2
- package/dist/elements/EFMedia/AssetMediaEngine.js.map +1 -1
- package/dist/elements/EFMedia/BaseMediaEngine.js +15 -92
- package/dist/elements/EFMedia/BaseMediaEngine.js.map +1 -1
- package/dist/elements/EFMedia/BufferedSeekingInput.js +10 -11
- package/dist/elements/EFMedia/BufferedSeekingInput.js.map +1 -1
- package/dist/elements/EFMedia/{AssetIdMediaEngine.js → FileMediaEngine.js} +44 -24
- package/dist/elements/EFMedia/FileMediaEngine.js.map +1 -0
- package/dist/elements/EFMedia/JitMediaEngine.js +14 -13
- package/dist/elements/EFMedia/JitMediaEngine.js.map +1 -1
- package/dist/elements/EFMedia/shared/AudioSpanUtils.js +3 -3
- package/dist/elements/EFMedia/shared/AudioSpanUtils.js.map +1 -1
- package/dist/elements/EFMedia/shared/ThumbnailExtractor.js +12 -7
- package/dist/elements/EFMedia/shared/ThumbnailExtractor.js.map +1 -1
- package/dist/elements/EFMedia/shared/timeoutUtils.js +44 -0
- package/dist/elements/EFMedia/shared/timeoutUtils.js.map +1 -0
- package/dist/elements/EFMedia/videoTasks/MainVideoInputCache.js +1 -1
- package/dist/elements/EFMedia/videoTasks/MainVideoInputCache.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/ScrubInputCache.js +4 -4
- package/dist/elements/EFMedia/videoTasks/ScrubInputCache.js.map +1 -1
- package/dist/elements/EFMedia.d.ts +14 -8
- package/dist/elements/EFMedia.js +52 -19
- package/dist/elements/EFMedia.js.map +1 -1
- package/dist/elements/EFPanZoom.d.ts +2 -2
- package/dist/elements/EFPanZoom.js +1 -1
- package/dist/elements/EFPanZoom.js.map +1 -1
- package/dist/elements/EFSourceMixin.js +16 -8
- package/dist/elements/EFSourceMixin.js.map +1 -1
- package/dist/elements/EFSurface.d.ts +5 -8
- package/dist/elements/EFSurface.js +4 -43
- package/dist/elements/EFSurface.js.map +1 -1
- package/dist/elements/EFTemporal.d.ts +33 -8
- package/dist/elements/EFTemporal.js +92 -40
- package/dist/elements/EFTemporal.js.map +1 -1
- package/dist/elements/EFText.d.ts +3 -0
- package/dist/elements/EFText.js +54 -21
- package/dist/elements/EFText.js.map +1 -1
- package/dist/elements/EFTextSegment.js +8 -4
- package/dist/elements/EFTextSegment.js.map +1 -1
- package/dist/elements/EFTimegroup.d.ts +26 -43
- package/dist/elements/EFTimegroup.js +295 -314
- package/dist/elements/EFTimegroup.js.map +1 -1
- package/dist/elements/EFVideo.d.ts +44 -42
- package/dist/elements/EFVideo.js +259 -172
- package/dist/elements/EFVideo.js.map +1 -1
- package/dist/elements/EFWaveform.d.ts +3 -8
- package/dist/elements/EFWaveform.js +18 -13
- 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/TargetController.d.ts +0 -3
- package/dist/elements/TargetController.js +12 -35
- package/dist/elements/TargetController.js.map +1 -1
- package/dist/elements/TimegroupController.js.map +1 -1
- package/dist/elements/cloneFactoryRegistry.d.ts +14 -0
- package/dist/elements/cloneFactoryRegistry.js +15 -0
- package/dist/elements/cloneFactoryRegistry.js.map +1 -0
- package/dist/elements/renderTemporalAudio.js +8 -6
- package/dist/elements/renderTemporalAudio.js.map +1 -1
- package/dist/elements/setupTemporalHierarchy.js +62 -0
- package/dist/elements/setupTemporalHierarchy.js.map +1 -0
- package/dist/elements/updateAnimations.js +62 -87
- package/dist/elements/updateAnimations.js.map +1 -1
- package/dist/getRenderInfo.d.ts +3 -2
- package/dist/getRenderInfo.js +20 -4
- package/dist/getRenderInfo.js.map +1 -1
- package/dist/gui/ContextMixin.js +68 -12
- package/dist/gui/ContextMixin.js.map +1 -1
- package/dist/gui/Controllable.js +1 -1
- package/dist/gui/Controllable.js.map +1 -1
- package/dist/gui/EFActiveRootTemporal.d.ts +2 -2
- package/dist/gui/EFActiveRootTemporal.js.map +1 -1
- package/dist/gui/EFControls.d.ts +2 -2
- package/dist/gui/EFControls.js +2 -2
- package/dist/gui/EFControls.js.map +1 -1
- package/dist/gui/EFDial.d.ts +2 -2
- package/dist/gui/EFDial.js +12 -9
- package/dist/gui/EFDial.js.map +1 -1
- package/dist/gui/EFFilmstrip.d.ts +2 -0
- package/dist/gui/EFFilmstrip.js +18 -10
- package/dist/gui/EFFilmstrip.js.map +1 -1
- package/dist/gui/EFFitScale.d.ts +28 -4
- package/dist/gui/EFFitScale.js +88 -26
- package/dist/gui/EFFitScale.js.map +1 -1
- package/dist/gui/EFFocusOverlay.d.ts +2 -2
- package/dist/gui/EFFocusOverlay.js +3 -3
- package/dist/gui/EFFocusOverlay.js.map +1 -1
- package/dist/gui/EFOverlayItem.d.ts +2 -2
- package/dist/gui/EFOverlayLayer.d.ts +2 -2
- package/dist/gui/EFPause.d.ts +2 -2
- package/dist/gui/EFPause.js +1 -1
- package/dist/gui/EFPlay.d.ts +2 -2
- package/dist/gui/EFPlay.js +1 -1
- package/dist/gui/EFPreview.js +1 -1
- package/dist/gui/EFResizableBox.d.ts +2 -2
- package/dist/gui/EFResizableBox.js +5 -5
- package/dist/gui/EFResizableBox.js.map +1 -1
- package/dist/gui/EFScrubber.d.ts +2 -2
- package/dist/gui/EFScrubber.js +8 -13
- package/dist/gui/EFScrubber.js.map +1 -1
- package/dist/gui/EFTimeDisplay.d.ts +6 -2
- package/dist/gui/EFTimeDisplay.js +25 -7
- package/dist/gui/EFTimeDisplay.js.map +1 -1
- package/dist/gui/EFTimelineRuler.d.ts +2 -2
- package/dist/gui/EFTimelineRuler.js +3 -3
- package/dist/gui/EFTimelineRuler.js.map +1 -1
- package/dist/gui/EFToggleLoop.d.ts +2 -2
- package/dist/gui/EFToggleLoop.js +1 -1
- package/dist/gui/EFTogglePlay.d.ts +2 -2
- package/dist/gui/EFTogglePlay.js +1 -1
- package/dist/gui/EFTransformHandles.d.ts +2 -2
- package/dist/gui/EFTransformHandles.js +6 -6
- package/dist/gui/EFTransformHandles.js.map +1 -1
- package/dist/gui/EFWorkbench.d.ts +40 -36
- package/dist/gui/EFWorkbench.js +436 -822
- package/dist/gui/EFWorkbench.js.map +1 -1
- package/dist/gui/FitScaleHelpers.js.map +1 -1
- package/dist/gui/PlaybackController.d.ts +3 -8
- package/dist/gui/PlaybackController.js +59 -56
- package/dist/gui/PlaybackController.js.map +1 -1
- package/dist/gui/TWMixin.js +1 -1
- package/dist/gui/TWMixin.js.map +1 -1
- package/dist/gui/TargetOrContextMixin.js +43 -6
- package/dist/gui/TargetOrContextMixin.js.map +1 -1
- package/dist/gui/ef-theme.css +136 -0
- package/dist/gui/hierarchy/EFHierarchy.d.ts +2 -2
- package/dist/gui/hierarchy/EFHierarchy.js +14 -24
- package/dist/gui/hierarchy/EFHierarchy.js.map +1 -1
- package/dist/gui/hierarchy/EFHierarchyItem.d.ts +3 -3
- package/dist/gui/hierarchy/EFHierarchyItem.js +22 -10
- package/dist/gui/hierarchy/EFHierarchyItem.js.map +1 -1
- package/dist/gui/icons.js.map +1 -1
- package/dist/gui/previewSettingsContext.d.ts +18 -0
- package/dist/gui/previewSettingsContext.js.map +1 -1
- package/dist/gui/theme.js +34 -0
- package/dist/gui/theme.js.map +1 -0
- package/dist/gui/timeline/EFTimeline.d.ts +2 -2
- package/dist/gui/timeline/EFTimeline.js +70 -52
- package/dist/gui/timeline/EFTimeline.js.map +1 -1
- package/dist/gui/timeline/EFTimelineRow.d.ts +5 -3
- package/dist/gui/timeline/EFTimelineRow.js +55 -32
- package/dist/gui/timeline/EFTimelineRow.js.map +1 -1
- package/dist/gui/timeline/TrimHandles.d.ts +23 -9
- package/dist/gui/timeline/TrimHandles.js +224 -51
- package/dist/gui/timeline/TrimHandles.js.map +1 -1
- package/dist/gui/timeline/flattenHierarchy.js.map +1 -1
- package/dist/gui/timeline/timelineEditingContext.d.ts +34 -0
- package/dist/gui/timeline/timelineEditingContext.js +24 -0
- package/dist/gui/timeline/timelineEditingContext.js.map +1 -0
- package/dist/gui/timeline/timelineStateContext.js.map +1 -1
- package/dist/gui/timeline/tracks/AudioTrack.js +1 -1
- package/dist/gui/timeline/tracks/AudioTrack.js.map +1 -1
- package/dist/gui/timeline/tracks/CaptionsTrack.d.ts +2 -3
- package/dist/gui/timeline/tracks/CaptionsTrack.js +17 -75
- package/dist/gui/timeline/tracks/CaptionsTrack.js.map +1 -1
- package/dist/gui/timeline/tracks/EFThumbnailStrip.d.ts +52 -0
- package/dist/gui/timeline/tracks/EFThumbnailStrip.js +596 -0
- package/dist/gui/timeline/tracks/EFThumbnailStrip.js.map +1 -0
- package/dist/gui/timeline/tracks/HTMLTrack.js.map +1 -1
- package/dist/gui/timeline/tracks/ImageTrack.js.map +1 -1
- package/dist/gui/timeline/tracks/TextTrack.d.ts +3 -2
- package/dist/gui/timeline/tracks/TextTrack.js +17 -43
- package/dist/gui/timeline/tracks/TextTrack.js.map +1 -1
- package/dist/gui/timeline/tracks/TimegroupTrack.d.ts +3 -4
- package/dist/gui/timeline/tracks/TimegroupTrack.js +33 -23
- package/dist/gui/timeline/tracks/TimegroupTrack.js.map +1 -1
- package/dist/gui/timeline/tracks/TrackItem.d.ts +7 -9
- package/dist/gui/timeline/tracks/TrackItem.js +18 -17
- package/dist/gui/timeline/tracks/TrackItem.js.map +1 -1
- package/dist/gui/timeline/tracks/VideoTrack.d.ts +3 -3
- package/dist/gui/timeline/tracks/VideoTrack.js +11 -14
- package/dist/gui/timeline/tracks/VideoTrack.js.map +1 -1
- package/dist/gui/timeline/tracks/WaveformTrack.js.map +1 -1
- package/dist/gui/timeline/tracks/renderTrackChildren.js.map +1 -1
- package/dist/gui/timeline/tracks/waveformUtils.js +1 -1
- package/dist/gui/timeline/tracks/waveformUtils.js.map +1 -1
- package/dist/gui/tree/EFTree.d.ts +2 -2
- package/dist/gui/tree/EFTree.js +8 -14
- package/dist/gui/tree/EFTree.js.map +1 -1
- package/dist/gui/tree/EFTreeItem.d.ts +2 -2
- package/dist/gui/tree/EFTreeItem.js +3 -3
- package/dist/gui/tree/EFTreeItem.js.map +1 -1
- package/dist/gui/tree/treeContext.js.map +1 -1
- package/dist/index.d.ts +10 -8
- package/dist/index.js +6 -5
- package/dist/index.js.map +1 -1
- package/dist/node.d.ts +2 -2
- package/dist/node.js +2 -2
- package/dist/preview/AdaptiveResolutionTracker.js +3 -3
- package/dist/preview/AdaptiveResolutionTracker.js.map +1 -1
- package/dist/preview/FrameController.d.ts +2 -17
- package/dist/preview/FrameController.js +40 -63
- package/dist/preview/FrameController.js.map +1 -1
- package/dist/preview/QualityUpgradeScheduler.d.ts +76 -0
- package/dist/preview/QualityUpgradeScheduler.js +158 -0
- package/dist/preview/QualityUpgradeScheduler.js.map +1 -0
- package/dist/preview/RenderContext.d.ts +119 -1
- package/dist/preview/RenderContext.js +21 -3
- package/dist/preview/RenderContext.js.map +1 -1
- package/dist/preview/RenderProfiler.js.map +1 -1
- package/dist/preview/RenderStats.js +85 -0
- package/dist/preview/RenderStats.js.map +1 -0
- package/dist/preview/encoding/canvasEncoder.js +2 -52
- package/dist/preview/encoding/canvasEncoder.js.map +1 -1
- package/dist/preview/encoding/mainThreadEncoder.js.map +1 -1
- package/dist/preview/encoding/workerEncoder.js.map +1 -1
- package/dist/preview/logger.js.map +1 -1
- package/dist/preview/previewSettings.d.ts +34 -0
- package/dist/preview/previewSettings.js +29 -17
- package/dist/preview/previewSettings.js.map +1 -1
- package/dist/preview/previewTypes.js +4 -4
- package/dist/preview/previewTypes.js.map +1 -1
- package/dist/preview/renderElementToCanvas.d.ts +44 -0
- package/dist/preview/renderElementToCanvas.js +72 -0
- package/dist/preview/renderElementToCanvas.js.map +1 -0
- package/dist/preview/renderTimegroupToCanvas.d.ts +134 -32
- package/dist/preview/renderTimegroupToCanvas.js +321 -146
- package/dist/preview/renderTimegroupToCanvas.js.map +1 -1
- package/dist/preview/renderTimegroupToCanvas.types.d.ts +51 -0
- package/dist/preview/renderTimegroupToVideo.d.ts +20 -35
- package/dist/preview/renderTimegroupToVideo.js +94 -106
- package/dist/preview/renderTimegroupToVideo.js.map +1 -1
- package/dist/preview/renderTimegroupToVideo.types.d.ts +42 -0
- package/dist/preview/renderVideoToVideo.js +286 -0
- package/dist/preview/renderVideoToVideo.js.map +1 -0
- package/dist/preview/renderers.d.ts +56 -0
- package/dist/preview/renderers.js +13 -1
- package/dist/preview/renderers.js.map +1 -1
- package/dist/preview/rendering/ScaleConfig.js +74 -0
- package/dist/preview/rendering/ScaleConfig.js.map +1 -0
- package/dist/preview/rendering/inlineImages.d.ts +13 -0
- package/dist/preview/rendering/inlineImages.js +7 -44
- package/dist/preview/rendering/inlineImages.js.map +1 -1
- package/dist/preview/rendering/loadImage.d.ts +8 -0
- package/dist/preview/rendering/loadImage.js +22 -0
- package/dist/preview/rendering/loadImage.js.map +1 -0
- package/dist/preview/rendering/renderToImageNative.js +3 -3
- package/dist/preview/rendering/renderToImageNative.js.map +1 -1
- package/dist/preview/rendering/serializeTimelineDirect.js +224 -68
- package/dist/preview/rendering/serializeTimelineDirect.js.map +1 -1
- package/dist/preview/statsTrackingStrategy.js +1 -101
- package/dist/preview/statsTrackingStrategy.js.map +1 -1
- package/dist/preview/workers/WorkerPool.js +0 -1
- package/dist/preview/workers/WorkerPool.js.map +1 -1
- package/dist/preview/workers/encoderWorkerInline.js +21 -54
- package/dist/preview/workers/encoderWorkerInline.js.map +1 -1
- package/dist/render/EFRenderAPI.d.ts +2 -1
- package/dist/render/EFRenderAPI.js +12 -36
- package/dist/render/EFRenderAPI.js.map +1 -1
- package/dist/render/getRenderData.js +4 -4
- package/dist/render/getRenderData.js.map +1 -1
- package/dist/style.css +114 -163
- package/dist/transcoding/cache/RequestDeduplicator.js +1 -0
- package/dist/transcoding/cache/RequestDeduplicator.js.map +1 -1
- package/dist/transcoding/types/index.d.ts +1 -1
- package/dist/transcoding/utils/UrlGenerator.js +10 -3
- package/dist/transcoding/utils/UrlGenerator.js.map +1 -1
- package/dist/utils/LRUCache.js +1 -0
- package/dist/utils/LRUCache.js.map +1 -1
- package/dist/utils/frameTime.js +23 -1
- package/dist/utils/frameTime.js.map +1 -1
- package/package.json +45 -8
- package/scripts/build-css.js +8 -1
- package/test/setup.ts +0 -1
- package/test/useAssetMSW.ts +50 -0
- package/test/visualRegressionUtils.ts +23 -9
- package/tsdown.config.ts +6 -1
- package/dist/_virtual/rolldown_runtime.js +0 -27
- package/dist/elements/EFMedia/AssetIdMediaEngine.js.map +0 -1
- package/dist/elements/EFThumbnailStrip.d.ts +0 -167
- package/dist/elements/EFThumbnailStrip.js +0 -731
- package/dist/elements/EFThumbnailStrip.js.map +0 -1
- package/dist/elements/SessionThumbnailCache.js +0 -154
- package/dist/elements/SessionThumbnailCache.js.map +0 -1
- package/dist/node_modules/react/cjs/react-jsx-runtime.development.js +0 -688
- package/dist/node_modules/react/cjs/react-jsx-runtime.development.js.map +0 -1
- package/dist/node_modules/react/cjs/react.development.js +0 -1521
- package/dist/node_modules/react/cjs/react.development.js.map +0 -1
- package/dist/node_modules/react/index.js +0 -13
- package/dist/node_modules/react/index.js.map +0 -1
- package/dist/node_modules/react/jsx-runtime.js +0 -13
- package/dist/node_modules/react/jsx-runtime.js.map +0 -1
- package/dist/preview/encoding/types.d.ts +0 -1
- package/dist/preview/renderTimegroupPreview.js +0 -686
- package/dist/preview/renderTimegroupPreview.js.map +0 -1
- package/dist/preview/rendering/renderToImage.d.ts +0 -2
- package/dist/preview/rendering/renderToImage.js +0 -95
- package/dist/preview/rendering/renderToImage.js.map +0 -1
- package/dist/preview/rendering/renderToImageForeignObject.js +0 -163
- package/dist/preview/rendering/renderToImageForeignObject.js.map +0 -1
- package/dist/preview/rendering/renderToImageNative.d.ts +0 -1
- package/dist/preview/rendering/svgSerializer.js +0 -43
- package/dist/preview/rendering/svgSerializer.js.map +0 -1
- package/dist/preview/rendering/types.d.ts +0 -2
- package/dist/preview/thumbnailCacheSettings.js +0 -52
- package/dist/preview/thumbnailCacheSettings.js.map +0 -1
- package/dist/sandbox/PlaybackControls.d.ts +0 -1
- package/dist/sandbox/PlaybackControls.js +0 -10
- package/dist/sandbox/PlaybackControls.js.map +0 -1
- package/dist/sandbox/ScenarioRunner.d.ts +0 -1
- package/dist/sandbox/ScenarioRunner.js +0 -1
- package/dist/sandbox/defineSandbox.d.ts +0 -1
- package/dist/sandbox/index.d.ts +0 -3
- package/dist/sandbox/index.js +0 -2
- package/test/EFVideo.framegen.browsertest.ts +0 -80
- package/test/thumbnail-performance-test.html +0 -116
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"renderTimegroupToCanvas.js","names":["timeMs: number","timeoutMs: number","blankVideos: string[]","renderState: RenderState","scaleColors: Record<number, string>","image: HTMLCanvasElement | HTMLImageElement","container","options: CanvasPreviewOptions","pendingResolutionScale: number | null"],"sources":["../../src/preview/renderTimegroupToCanvas.ts"],"sourcesContent":["import type { EFTimegroup } from \"../elements/EFTimegroup.js\";\nimport {\n buildCloneStructure,\n syncStyles,\n collectDocumentStyles,\n overrideRootCloneStyles,\n removeHiddenNodesForSerialization,\n restoreHiddenNodes,\n type SyncState,\n} from \"./renderTimegroupPreview.js\";\nimport { getEffectiveRenderMode } from \"./renderers.js\";\nimport { RenderContext } from \"./RenderContext.js\";\nimport { FrameController } from \"./FrameController.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_THUMBNAIL_SCALE,\n DEFAULT_BLOCKING_TIMEOUT_MS,\n createPreviewContainer,\n} from \"./previewTypes.js\";\nimport { defaultProfiler } from \"./RenderProfiler.js\";\nimport { logger } from \"./logger.js\";\n\n// Import rendering modules\nimport {\n renderToImage,\n renderToImageDirect,\n prepareFrameDataUri,\n loadImageFromDataUri,\n} from \"./rendering/renderToImage.js\";\nimport { renderToImageNative, createDprCanvas } from \"./rendering/renderToImageNative.js\";\nimport { clearInlineImageCache, getInlineImageCacheSize } from \"./rendering/inlineImages.js\";\n\n// Re-export rendering types and functions for external use\nexport type {\n NativeRenderOptions,\n ForeignObjectRenderOptions,\n} from \"./rendering/types.js\";\nexport {\n renderToImage,\n renderToImageNative,\n renderToImageDirect,\n prepareFrameDataUri,\n loadImageFromDataUri,\n};\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/**\n * Content readiness strategy for capture operations.\n * - \"immediate\": Capture NOW, skip all waits. May have blank video frames.\n * - \"blocking\": Wait for video content to be ready. Throws on timeout.\n */\nexport type ContentReadyMode = \"immediate\" | \"blocking\";\n\n/**\n * Options for capturing a timegroup frame.\n */\nexport interface CaptureOptions {\n /** Time to capture at in milliseconds (required) */\n timeMs: number;\n /** Scale factor (default: 0.25 for captureTimegroupAtTime) */\n scale?: number;\n /** Skip restoring original time after capture (for batch operations) */\n skipRestore?: boolean;\n /** Content readiness strategy (default: \"immediate\") */\n contentReadyMode?: ContentReadyMode;\n /** Max wait time for blocking mode before throwing (default: 5000ms) */\n blockingTimeoutMs?: number;\n}\n\n/**\n * Options for batch capture operations, excluding timeMs which is provided per-timestamp.\n */\nexport interface CaptureBatchOptions {\n /** Scale factor for thumbnails (default: 0.25) */\n scale?: number;\n /** Content readiness strategy (default: \"immediate\") */\n contentReadyMode?: ContentReadyMode;\n /** Max wait time for blocking mode before throwing (default: 5000ms) */\n blockingTimeoutMs?: number;\n}\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(`Video content not ready at ${timeMs}ms after ${timeoutMs}ms timeout. Blank videos: ${blankVideos.join(', ')}`);\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// Internal Helpers\n// ============================================================================\n\n/**\n * Create a debug label for showing render info.\n */\nfunction createDebugLabel(): HTMLDivElement {\n const debugLabel = document.createElement(\"div\");\n debugLabel.style.cssText = `\n position: absolute;\n top: -24px;\n left: 0;\n padding: 2px 8px;\n font: bold 12px monospace;\n background: rgba(0, 0, 0, 0.8);\n border-radius: 3px;\n white-space: nowrap;\n z-index: 1000;\n pointer-events: none;\n `;\n return debugLabel;\n}\n\n/**\n * Update debug label with resolution info.\n */\nfunction updateDebugLabel(\n label: HTMLDivElement,\n renderWidth: number,\n renderHeight: number,\n resolutionScale: number,\n): void {\n const scaleColors: Record<number, string> = {\n 1: \"#00ff00\",\n 0.75: \"#ffff00\",\n 0.5: \"#ff8800\",\n 0.25: \"#ff0000\",\n };\n label.style.color = scaleColors[resolutionScale] || \"#ffffff\";\n label.textContent = `Render: ${renderWidth}x${renderHeight} (${Math.round(resolutionScale * 100)}%)`;\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\");\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 */\nasync 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 = () => 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 * Options for capturing from an existing render clone.\n */\nexport interface CaptureFromCloneOptions {\n /** Scale factor for the output canvas (default: 0.25) */\n scale?: number;\n /** Content readiness strategy (default: \"immediate\") */\n contentReadyMode?: ContentReadyMode;\n /** Max wait time for blocking mode before throwing (default: 5000ms) */\n blockingTimeoutMs?: number;\n /** Original timegroup (for dimension and background reference) */\n originalTimegroup?: EFTimegroup;\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 with the rendered frame\n */\nexport async function captureFromClone(\n renderClone: EFTimegroup,\n renderContainer: HTMLElement,\n options: CaptureFromCloneOptions = {},\n): Promise<HTMLCanvasElement> {\n const {\n scale = DEFAULT_THUMBNAIL_SCALE,\n contentReadyMode = \"immediate\",\n blockingTimeoutMs = DEFAULT_BLOCKING_TIMEOUT_MS,\n originalTimegroup,\n } = options;\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 // Create canvas at scaled size\n const dpr = window.devicePixelRatio || 1;\n const canvas = document.createElement(\"canvas\");\n canvas.width = Math.floor(width * scale * dpr);\n canvas.height = Math.floor(height * scale * dpr);\n canvas.style.width = `${Math.floor(width * scale)}px`;\n canvas.style.height = `${Math.floor(height * scale)}px`;\n\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) {\n throw new Error(\"Failed to get canvas 2d context\");\n }\n\n // Handle content readiness based on mode\n const timeMs = renderClone.currentTimeMs;\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 // Create RenderContext for caching during this capture operation\n const renderContext = new RenderContext();\n \n try {\n let image: HTMLCanvasElement | HTMLImageElement;\n const renderMode = getEffectiveRenderMode();\n \n if (renderMode === \"native\") {\n // NATIVE PATH: Render the seeked renderClone directly from live DOM\n // The clone is already at the correct time, so drawElementImage captures its current\n // visual state including video frames at the correct position.\n // \n // Position render container properly for capture\n renderContainer.style.cssText = `\n position: fixed;\n left: 0;\n top: 0;\n width: ${width}px;\n height: ${height}px;\n pointer-events: none;\n overflow: hidden;\n `;\n \n // OPTIMIZATION: Always skip DPR scaling for captures (thumbnails and video export).\n // Retina quality isn't needed for captured frames, and DPR=2 means 4x more pixels.\n // Live preview uses a different code path (renderTimegroupToCanvas) which handles DPR properly.\n image = await renderToImageNative(renderContainer, width, height, { skipDprScaling: true });\n } else {\n // FOREIGNOBJECT PATH: Build passive structure from the SEEKED render clone\n // The clone is already at the correct time, so getComputedStyle captures the right values.\n // Styles are synced during clone building in a single pass.\n const t0 = performance.now();\n const { container, syncState } = buildCloneStructure(renderClone, timeMs);\n const buildTime = performance.now() - t0;\n\n // Create wrapper using shared helper\n const bgSource = originalTimegroup ?? renderClone;\n const previewContainer = createPreviewContainer({\n width,\n height,\n background: getComputedStyle(bgSource).background || \"#000\",\n });\n \n const t1 = performance.now();\n const styleEl = document.createElement(\"style\");\n styleEl.textContent = collectDocumentStyles();\n const stylesTime = performance.now() - t1;\n previewContainer.appendChild(styleEl);\n previewContainer.appendChild(container);\n \n // Ensure clone root is visible\n overrideRootCloneStyles(syncState, true);\n\n // Render using foreignObject serialization\n // Pass scale, renderContext, and sourceMap for caching optimization\n const t2 = performance.now();\n image = await renderToImage(previewContainer, width, height, { \n canvasScale: scale,\n renderContext,\n sourceMap: syncState.canvasSourceMap,\n });\n const renderTime = performance.now() - t2;\n \n logger.debug(`[captureFromClone] build=${buildTime.toFixed(0)}ms, styles=${stylesTime.toFixed(0)}ms, render=${renderTime.toFixed(0)}ms (canvasScale=${scale})`);\n }\n\n // Draw to canvas (may need scaling for native path which is at DPR)\n const srcWidth = image.width;\n const srcHeight = image.height;\n ctx.drawImage(\n image,\n 0, 0, srcWidth, srcHeight,\n 0, 0, canvas.width, canvas.height\n );\n\n return canvas;\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<HTMLCanvasElement> {\n const {\n timeMs,\n scale = DEFAULT_THUMBNAIL_SCALE,\n // skipRestore is deprecated with Clone-timeline (Prime is never seeked)\n contentReadyMode = \"immediate\",\n blockingTimeoutMs = DEFAULT_BLOCKING_TIMEOUT_MS,\n } = options;\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 { clone: renderClone, container: renderContainer, cleanup: cleanupRenderClone } = \n await timegroup.createRenderClone();\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 await renderClone.seekForRender(timeMs);\n \n // Use the shared capture helper\n return await captureFromClone(renderClone, renderContainer, {\n scale,\n contentReadyMode,\n blockingTimeoutMs,\n originalTimegroup: timegroup,\n });\n } finally {\n // 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\nexport interface CanvasPreviewResult {\n /**\n * Wrapper container holding the canvas and debug label.\n * Append this to your DOM - the canvas inside will receive transforms.\n */\n container: HTMLDivElement;\n canvas: HTMLCanvasElement;\n /**\n * Call this to re-render the timegroup to canvas at current visual state.\n * Returns a promise that resolves when rendering is complete.\n */\n refresh: () => Promise<void>;\n /**\n * Dynamically change the resolution scale without rebuilding the clone structure.\n * This is nearly instant - just updates CSS and internal variables.\n * The next refresh() call will render at the new resolution.\n */\n setResolutionScale: (scale: number) => void;\n /**\n * Get the current resolution scale.\n */\n getResolutionScale: () => number;\n /**\n * Dispose the preview and release resources.\n * Call this when the preview is no longer needed.\n */\n dispose: () => void;\n}\n\n/**\n * Options for canvas preview rendering.\n */\nexport interface CanvasPreviewOptions {\n /**\n * Output scale factor (default: 1).\n * Scales the final canvas size.\n */\n scale?: number;\n \n /**\n * Resolution scale for internal rendering (default: 1).\n * Reduces the internal render resolution for better performance.\n * The canvas CSS size remains the same (browser upscales).\n * - 1: Full resolution\n * - 0.75: 3/4 resolution\n * - 0.5: Half resolution\n * - 0.25: Quarter resolution\n */\n resolutionScale?: number;\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 = 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 = options.resolutionScale ?? DEFAULT_RESOLUTION_SCALE;\n \n const width = timegroup.offsetWidth || DEFAULT_WIDTH;\n const height = timegroup.offsetHeight || DEFAULT_HEIGHT;\n const dpr = window.devicePixelRatio || 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 // Create wrapper container with debug label\n const wrapperContainer = document.createElement(\"div\");\n wrapperContainer.style.cssText = \"position: relative; display: inline-block;\";\n const debugLabel = createDebugLabel();\n wrapperContainer.appendChild(debugLabel);\n wrapperContainer.appendChild(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 // Create RenderContext for caching across refresh calls\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 // Reusable preview container and style element\n // Container gets new content each frame but CSS styles are cached\n const previewContainer = createPreviewContainer({\n width: renderWidth,\n height: renderHeight,\n background: getComputedStyle(timegroup).background || \"#000\",\n });\n \n const styleEl = document.createElement(\"style\");\n styleEl.textContent = collectDocumentStyles();\n previewContainer.appendChild(styleEl);\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 /**\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 // Update previewContainer dimensions (affects what renderToImage produces)\n previewContainer.style.width = `${renderWidth}px`;\n previewContainer.style.height = `${renderHeight}px`;\n \n // Update clone transform\n if (currentResolutionScale < 1) {\n container.style.transform = `scale(${currentResolutionScale})`;\n container.style.transformOrigin = \"top left\";\n } else {\n container.style.transform = \"\";\n }\n \n // Canvas dimensions will be updated right before drawing (in refresh)\n // to avoid clearing the canvas until new content is ready\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 (rendering || disposed) return;\n // Clone-timeline: captures use separate clones, Prime-timeline is never locked\n \n const sourceTimeMs = timegroup.currentTimeMs ?? 0;\n const userTimeMs = timegroup.userTimeMs ?? 0;\n if (Math.abs(sourceTimeMs - userTimeMs) > TIME_EPSILON_MS) return;\n \n if (userTimeMs === lastTimeMs) return;\n lastTimeMs = userTimeMs;\n \n rendering = true;\n \n // Apply any pending resolution changes before rendering\n // This updates previewContainer and clone transform, but NOT canvas dimensions yet\n applyPendingResolutionChange();\n \n // Log scale info once per initialization\n if (!hasLoggedScale) {\n hasLoggedScale = true;\n const mode = getEffectiveRenderMode();\n logger.debug(`[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 try {\n // Use FrameController to ensure all FrameRenderable elements are ready\n // This coordinates prepare → render phases before we capture their state\n await frameController.renderFrame(userTimeMs);\n \n // Build passive structure from prime timeline's CURRENT state\n // Frame tasks have already run above, so DOM changes are captured\n const absoluteTimeMs = toAbsoluteTime(timegroup, userTimeMs);\n const { container, syncState } = buildCloneStructure(timegroup, absoluteTimeMs);\n \n // Apply CSS transform to scale down the content within the container\n // This makes the clone render at reduced complexity\n if (currentResolutionScale < 1) {\n container.style.transform = `scale(${currentResolutionScale})`;\n container.style.transformOrigin = \"top left\";\n }\n \n // Clear previous container content and add new structure\n while (previewContainer.firstChild !== styleEl && previewContainer.firstChild) {\n previewContainer.removeChild(previewContainer.firstChild);\n }\n previewContainer.appendChild(container);\n overrideRootCloneStyles(syncState);\n\n // Remove hidden nodes from DOM for serialization - they won't be serialized\n // or have their canvases encoded. This is a significant optimization.\n const removedNodes = removeHiddenNodesForSerialization(syncState);\n\n // Render at scaled dimensions with canvas scaling for internal video frames\n // Pass renderContext and sourceMap for caching optimization\n const t0 = performance.now();\n const image = await renderToImage(previewContainer, renderWidth, renderHeight, {\n canvasScale: currentResolutionScale,\n renderContext,\n sourceMap: syncState.canvasSourceMap,\n });\n const renderTime = performance.now() - t0;\n \n // Restore hidden nodes for next frame's delta tracking\n restoreHiddenNodes(removedNodes);\n\n // Update canvas buffer dimensions NOW, right before drawing\n // This clears the canvas, but we immediately draw new content\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);\n ctx.restore();\n \n // Log render time periodically (every 60 frames)\n defaultProfiler.incrementRenderCount();\n if (defaultProfiler.shouldLogByFrameCount(60)) {\n logger.debug(`[renderTimegroupToCanvas] Frame render: ${renderTime.toFixed(1)}ms (resolutionScale=${currentResolutionScale}, image=${image.width}x${image.height})`);\n }\n \n // Update debug label\n updateDebugLabel(debugLabel, renderWidth, renderHeight, currentResolutionScale);\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 frameController.abort();\n renderContext.dispose();\n };\n\n // Do initial render\n refresh();\n\n return { container: wrapperContainer, canvas, refresh, setResolutionScale, getResolutionScale, dispose };\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 {\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 // 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 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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"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
//#region src/preview/renderTimegroupToCanvas.types.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Type definitions for canvas rendering.
|
|
4
|
+
* This file has ZERO imports and ZERO side effects - safe for SSR.
|
|
5
|
+
*/
|
|
6
|
+
type ContentReadyMode = "immediate" | "blocking";
|
|
7
|
+
interface CaptureOptions {
|
|
8
|
+
timeMs: number;
|
|
9
|
+
scale?: number;
|
|
10
|
+
skipRestore?: boolean;
|
|
11
|
+
contentReadyMode?: ContentReadyMode;
|
|
12
|
+
blockingTimeoutMs?: number;
|
|
13
|
+
canvasMode?: "native" | "foreignObject";
|
|
14
|
+
skipClone?: boolean;
|
|
15
|
+
}
|
|
16
|
+
interface CaptureFromCloneOptions {
|
|
17
|
+
scale?: number;
|
|
18
|
+
contentReadyMode?: ContentReadyMode;
|
|
19
|
+
blockingTimeoutMs?: number;
|
|
20
|
+
originalTimegroup?: any;
|
|
21
|
+
timeMs?: number;
|
|
22
|
+
canvasMode?: "native" | "foreignObject";
|
|
23
|
+
}
|
|
24
|
+
interface GeneratedThumbnail {
|
|
25
|
+
timeMs: number;
|
|
26
|
+
canvas: CanvasImageSource;
|
|
27
|
+
}
|
|
28
|
+
interface GenerateThumbnailsOptions {
|
|
29
|
+
scale?: number;
|
|
30
|
+
contentReadyMode?: ContentReadyMode;
|
|
31
|
+
blockingTimeoutMs?: number;
|
|
32
|
+
signal?: AbortSignal;
|
|
33
|
+
}
|
|
34
|
+
interface ThumbnailQueue {
|
|
35
|
+
shift(): number | undefined;
|
|
36
|
+
}
|
|
37
|
+
interface CanvasPreviewResult {
|
|
38
|
+
container: HTMLCanvasElement;
|
|
39
|
+
canvas: HTMLCanvasElement;
|
|
40
|
+
refresh: () => Promise<void>;
|
|
41
|
+
setResolutionScale: (scale: number) => void;
|
|
42
|
+
getResolutionScale: () => number;
|
|
43
|
+
dispose: () => void;
|
|
44
|
+
}
|
|
45
|
+
interface CanvasPreviewOptions {
|
|
46
|
+
scale?: number;
|
|
47
|
+
resolutionScale?: number;
|
|
48
|
+
}
|
|
49
|
+
//#endregion
|
|
50
|
+
export { CanvasPreviewOptions, CanvasPreviewResult, CaptureFromCloneOptions, CaptureOptions, ContentReadyMode, GenerateThumbnailsOptions, GeneratedThumbnail, ThumbnailQueue };
|
|
51
|
+
//# sourceMappingURL=renderTimegroupToCanvas.types.d.ts.map
|
|
@@ -1,42 +1,27 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import "../elements/EFTimegroup.js";
|
|
3
|
-
import { AudioCodec } from "mediabunny";
|
|
1
|
+
import { RenderProgress, RenderToVideoOptions } from "./renderTimegroupToVideo.types.js";
|
|
2
|
+
import { EFTimegroup } from "../elements/EFTimegroup.js";
|
|
3
|
+
import { AudioCodec, QUALITY_HIGH } from "mediabunny";
|
|
4
4
|
|
|
5
5
|
//#region src/preview/renderTimegroupToVideo.d.ts
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
currentFrame: number;
|
|
10
|
-
totalFrames: number;
|
|
11
|
-
renderedMs: number;
|
|
12
|
-
totalDurationMs: number;
|
|
13
|
-
elapsedMs: number;
|
|
14
|
-
estimatedRemainingMs: number;
|
|
15
|
-
speedMultiplier: number;
|
|
16
|
-
framePreviewCanvas?: HTMLCanvasElement;
|
|
7
|
+
declare class NoSupportedAudioCodecError extends Error {
|
|
8
|
+
constructor(requestedCodecs: AudioCodec[], availableCodecs: AudioCodec[]);
|
|
17
9
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
codec?: "avc" | "hevc" | "vp9" | "av1" | "vp8";
|
|
21
|
-
bitrate?: number;
|
|
22
|
-
filename?: string;
|
|
23
|
-
scale?: number;
|
|
24
|
-
keyFrameInterval?: number;
|
|
25
|
-
fromMs?: number;
|
|
26
|
-
toMs?: number;
|
|
27
|
-
onProgress?: (progress: RenderProgress) => void;
|
|
28
|
-
streaming?: boolean;
|
|
29
|
-
signal?: AbortSignal;
|
|
30
|
-
includeAudio?: boolean;
|
|
31
|
-
audioBitrate?: number;
|
|
32
|
-
contentReadyMode?: ContentReadyMode;
|
|
33
|
-
blockingTimeoutMs?: number;
|
|
34
|
-
returnBuffer?: boolean;
|
|
35
|
-
preferredAudioCodecs?: AudioCodec[];
|
|
36
|
-
benchmarkMode?: boolean;
|
|
37
|
-
customWritableStream?: WritableStream<Uint8Array>;
|
|
38
|
-
progressPreviewInterval?: number;
|
|
10
|
+
declare class RenderCancelledError extends Error {
|
|
11
|
+
constructor();
|
|
39
12
|
}
|
|
13
|
+
declare function getSupportedAudioCodecs(options?: {
|
|
14
|
+
numberOfChannels?: number;
|
|
15
|
+
sampleRate?: number;
|
|
16
|
+
bitrate?: number;
|
|
17
|
+
}): Promise<AudioCodec[]>;
|
|
18
|
+
/**
|
|
19
|
+
* Renders a timegroup to an MP4 video file.
|
|
20
|
+
*
|
|
21
|
+
* Uses the EXACT same code path as thumbnail generation (captureFromClone).
|
|
22
|
+
* This ensures consistency - if thumbnails work, video export works.
|
|
23
|
+
*/
|
|
24
|
+
declare function renderTimegroupToVideo(timegroup: EFTimegroup, options?: RenderToVideoOptions): Promise<Uint8Array | undefined>;
|
|
40
25
|
//#endregion
|
|
41
|
-
export { RenderProgress, RenderToVideoOptions };
|
|
26
|
+
export { type AudioCodec, NoSupportedAudioCodecError, QUALITY_HIGH, RenderCancelledError, type RenderProgress, type RenderToVideoOptions, getSupportedAudioCodecs, renderTimegroupToVideo };
|
|
42
27
|
//# sourceMappingURL=renderTimegroupToVideo.d.ts.map
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { logger } from "./logger.js";
|
|
2
|
-
import { createPreviewContainer } from "./previewTypes.js";
|
|
3
2
|
import { RenderContext } from "./RenderContext.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
3
|
+
import { createPreviewContainer } from "./previewTypes.js";
|
|
4
|
+
import { captureTimelineToDataUri } from "./rendering/serializeTimelineDirect.js";
|
|
5
|
+
import { isNativeCanvasApiAvailable } from "./previewSettings.js";
|
|
6
|
+
import { renderToImageNative } from "./rendering/renderToImageNative.js";
|
|
7
|
+
import { resetRenderState, waitForVideoContent } from "./renderTimegroupToCanvas.js";
|
|
8
|
+
import { AudioBufferSource, BufferTarget, CanvasSource, Mp4OutputFormat, Output, QUALITY_HIGH, StreamTarget, canEncodeAudio, getEncodableAudioCodecs } from "mediabunny";
|
|
7
9
|
|
|
8
10
|
//#region src/preview/renderTimegroupToVideo.ts
|
|
9
11
|
var NoSupportedAudioCodecError = class extends Error {
|
|
@@ -18,7 +20,7 @@ var RenderCancelledError = class extends Error {
|
|
|
18
20
|
this.name = "RenderCancelledError";
|
|
19
21
|
}
|
|
20
22
|
};
|
|
21
|
-
function resolveConfig(timegroup, options) {
|
|
23
|
+
function resolveConfig(timegroup, options = {}) {
|
|
22
24
|
const fps = options.fps ?? timegroup.effectiveFps ?? 30;
|
|
23
25
|
const codec = options.codec ?? "avc";
|
|
24
26
|
const bitrate = options.bitrate ?? 8e6;
|
|
@@ -41,11 +43,24 @@ function resolveConfig(timegroup, options) {
|
|
|
41
43
|
const renderDurationMs = endMs - startMs;
|
|
42
44
|
if (renderDurationMs <= 0) throw new Error(`Invalid render range: from ${startMs}ms to ${endMs}ms`);
|
|
43
45
|
timegroup.offsetHeight;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
let timegroupWidth = timegroup.offsetWidth;
|
|
47
|
+
let timegroupHeight = timegroup.offsetHeight;
|
|
48
|
+
if (!timegroupWidth || !timegroupHeight) {
|
|
49
|
+
const rect = timegroup.getBoundingClientRect();
|
|
50
|
+
if (rect.width > 0 && rect.height > 0) {
|
|
51
|
+
timegroupWidth = rect.width;
|
|
52
|
+
timegroupHeight = rect.height;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (!timegroupWidth || !timegroupHeight) {
|
|
56
|
+
const computed = getComputedStyle(timegroup);
|
|
57
|
+
const cw = parseFloat(computed.width);
|
|
58
|
+
const ch = parseFloat(computed.height);
|
|
59
|
+
if (cw > 0 && ch > 0) {
|
|
60
|
+
timegroupWidth = cw;
|
|
61
|
+
timegroupHeight = ch;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
49
64
|
if (!timegroupWidth || !timegroupHeight) throw new Error(`Timegroup has no dimensions (${timegroupWidth}x${timegroupHeight}). Ensure the timegroup element is in the document and has explicit width/height styles (e.g., class="w-[1920px] h-[1080px]")`);
|
|
50
65
|
const width = Math.floor(timegroupWidth * scale);
|
|
51
66
|
const height = Math.floor(timegroupHeight * scale);
|
|
@@ -77,7 +92,16 @@ function resolveConfig(timegroup, options) {
|
|
|
77
92
|
returnBuffer,
|
|
78
93
|
preferredAudioCodecs,
|
|
79
94
|
benchmarkMode,
|
|
80
|
-
progressPreviewInterval
|
|
95
|
+
progressPreviewInterval,
|
|
96
|
+
canvasMode: (() => {
|
|
97
|
+
const requested = options.canvasMode;
|
|
98
|
+
if (!requested) return "foreignObject";
|
|
99
|
+
if (requested === "native" && !isNativeCanvasApiAvailable()) {
|
|
100
|
+
logger.debug("[renderTimegroupToVideo] Native canvas mode requested but not available, falling back to foreignObject");
|
|
101
|
+
return "foreignObject";
|
|
102
|
+
}
|
|
103
|
+
return requested;
|
|
104
|
+
})()
|
|
81
105
|
};
|
|
82
106
|
}
|
|
83
107
|
function isFileSystemAccessSupported() {
|
|
@@ -122,9 +146,17 @@ function downloadBlob(blob, filename) {
|
|
|
122
146
|
document.body.removeChild(a);
|
|
123
147
|
URL.revokeObjectURL(url);
|
|
124
148
|
}
|
|
149
|
+
async function getSupportedAudioCodecs(options) {
|
|
150
|
+
const { numberOfChannels = 2, sampleRate = 48e3, bitrate = 128e3 } = options ?? {};
|
|
151
|
+
return getEncodableAudioCodecs(void 0, {
|
|
152
|
+
numberOfChannels,
|
|
153
|
+
sampleRate,
|
|
154
|
+
bitrate
|
|
155
|
+
});
|
|
156
|
+
}
|
|
125
157
|
/**
|
|
126
158
|
* Renders a timegroup to an MP4 video file.
|
|
127
|
-
*
|
|
159
|
+
*
|
|
128
160
|
* Uses the EXACT same code path as thumbnail generation (captureFromClone).
|
|
129
161
|
* This ensures consistency - if thumbnails work, video export works.
|
|
130
162
|
*/
|
|
@@ -138,12 +170,6 @@ async function renderTimegroupToVideo(timegroup, options = {}) {
|
|
|
138
170
|
const { clone: renderClone, cleanup: cleanupRenderClone } = await timegroup.createRenderClone();
|
|
139
171
|
const timestamps = [];
|
|
140
172
|
for (let i = 0; i < config.totalFrames; i++) timestamps.push(config.startMs + i * config.frameDurationMs);
|
|
141
|
-
const videoElements = renderClone.querySelectorAll("ef-video");
|
|
142
|
-
if (videoElements.length > 0) {
|
|
143
|
-
logger.debug(`[renderTimegroupToVideo] Prefetching main video segments for ${videoElements.length} video(s)...`);
|
|
144
|
-
await Promise.all(Array.from(videoElements).map((video) => video.prefetchMainVideoSegments(timestamps)));
|
|
145
|
-
logger.debug(`[renderTimegroupToVideo] Prefetch complete`);
|
|
146
|
-
}
|
|
147
173
|
let output = null;
|
|
148
174
|
let videoSource = null;
|
|
149
175
|
let audioSource = null;
|
|
@@ -211,13 +237,13 @@ async function renderTimegroupToVideo(timegroup, options = {}) {
|
|
|
211
237
|
height: timegroup.offsetHeight || 1080,
|
|
212
238
|
background: getComputedStyle(timegroup).background || "#000"
|
|
213
239
|
});
|
|
214
|
-
|
|
240
|
+
logger.debug(`[renderTimegroupToVideo] Using direct timeline serialization`);
|
|
215
241
|
previewContainer.appendChild(renderClone);
|
|
216
242
|
previewContainer.classList.add("ef-render-clone-container");
|
|
217
243
|
previewContainer.style.cssText += ";position:fixed;left:-99999px;top:-99999px;pointer-events:none;";
|
|
218
244
|
document.body.appendChild(previewContainer);
|
|
219
245
|
renderClone.offsetHeight;
|
|
220
|
-
|
|
246
|
+
logger.debug(`[renderTimegroupToVideo] Attached previewContainer to document.body (off-screen) for style computation`);
|
|
221
247
|
const renderStartTime = performance.now();
|
|
222
248
|
let lastRenderedAudioEndMs = config.startMs;
|
|
223
249
|
const audioChunkDurationMs = 2e3;
|
|
@@ -231,95 +257,76 @@ async function renderTimegroupToVideo(timegroup, options = {}) {
|
|
|
231
257
|
thumbCanvas.height = previewHeight;
|
|
232
258
|
thumbCtx = thumbCanvas.getContext("2d");
|
|
233
259
|
}
|
|
234
|
-
let totalSeekMs = 0;
|
|
235
|
-
let totalSyncMs = 0;
|
|
236
|
-
let totalRenderMs = 0;
|
|
237
|
-
let totalEncodeMs = 0;
|
|
238
260
|
try {
|
|
239
|
-
const
|
|
240
|
-
const
|
|
241
|
-
const MAX_SEEK = 1;
|
|
242
|
-
const MAX_RENDER = 4;
|
|
261
|
+
const MAX_AHEAD = 2;
|
|
262
|
+
const pendingFrames = [];
|
|
243
263
|
let nextSeekFrame = 0;
|
|
244
|
-
let
|
|
245
|
-
|
|
264
|
+
let encodedFrames = 0;
|
|
265
|
+
while (encodedFrames < config.totalFrames) {
|
|
246
266
|
checkCancelled();
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
await new Promise((resolve, reject) => {
|
|
275
|
-
image$1.onload = () => resolve();
|
|
267
|
+
while (nextSeekFrame < config.totalFrames && pendingFrames.length < MAX_AHEAD) {
|
|
268
|
+
const fi = nextSeekFrame;
|
|
269
|
+
const timeMs = timestamps[fi];
|
|
270
|
+
const timestampS = fi * config.frameDurationMs / 1e3;
|
|
271
|
+
await renderClone.seekForRender(timeMs);
|
|
272
|
+
const entry = {
|
|
273
|
+
frameIndex: fi,
|
|
274
|
+
timeMs,
|
|
275
|
+
timestampS,
|
|
276
|
+
resolved: null,
|
|
277
|
+
promise: null
|
|
278
|
+
};
|
|
279
|
+
if (config.contentReadyMode === "blocking") await waitForVideoContent(renderClone, timeMs, config.blockingTimeoutMs);
|
|
280
|
+
if (config.canvasMode === "native") {
|
|
281
|
+
entry.resolved = await renderToImageNative(renderClone, config.width, config.height, { skipDprScaling: true });
|
|
282
|
+
entry.promise = Promise.resolve(entry.resolved);
|
|
283
|
+
} else entry.promise = captureTimelineToDataUri(renderClone, config.width, config.height, {
|
|
284
|
+
renderContext,
|
|
285
|
+
canvasScale: config.scale,
|
|
286
|
+
timeMs
|
|
287
|
+
}).then((dataUri) => {
|
|
288
|
+
return new Promise((resolve, reject) => {
|
|
289
|
+
const image$1 = new Image();
|
|
290
|
+
image$1.onload = () => {
|
|
291
|
+
entry.resolved = image$1;
|
|
292
|
+
resolve(image$1);
|
|
293
|
+
};
|
|
276
294
|
image$1.onerror = (e) => {
|
|
277
|
-
console.error(`[
|
|
278
|
-
console.error(`[Frame ${renderFrameIndex}] Data URI preview:`, dataUri.substring(0, 200) + "...");
|
|
295
|
+
console.error(`[Render] frame ${fi} image load error:`, e);
|
|
279
296
|
reject(/* @__PURE__ */ new Error(`Failed to load image from data URI`));
|
|
280
297
|
};
|
|
281
298
|
image$1.src = dataUri;
|
|
282
299
|
});
|
|
283
|
-
const renderTime = performance.now() - renderStart;
|
|
284
|
-
totalRenderMs += renderTime;
|
|
285
|
-
if (renderFrameIndex % 30 === 0) console.log(`[Frame ${renderFrameIndex}] Image loaded: ${image$1.width}x${image$1.height}`);
|
|
286
|
-
if (renderFrameIndex % 30 === 0) console.log(`[Frame ${renderFrameIndex}] serialize=${syncTime.toFixed(1)}ms`);
|
|
287
|
-
return image$1;
|
|
288
300
|
});
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
timeMs: renderTimeMs,
|
|
292
|
-
timestampS: renderTimestampS,
|
|
293
|
-
promise: renderPromise
|
|
294
|
-
});
|
|
295
|
-
nextRenderFrame++;
|
|
301
|
+
pendingFrames.push(entry);
|
|
302
|
+
nextSeekFrame++;
|
|
296
303
|
}
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
304
|
+
const head = pendingFrames.shift();
|
|
305
|
+
const preloaded = head.resolved !== null;
|
|
306
|
+
let image;
|
|
307
|
+
if (preloaded) image = head.resolved;
|
|
308
|
+
else image = await head.promise;
|
|
309
|
+
if (audioSource && head.timeMs >= lastRenderedAudioEndMs + audioChunkDurationMs) {
|
|
310
|
+
const chunkEndMs = Math.min(head.timeMs + audioChunkDurationMs, config.endMs);
|
|
303
311
|
try {
|
|
304
|
-
const audioBuffer = await timegroup.renderAudio(lastRenderedAudioEndMs, chunkEndMs);
|
|
312
|
+
const audioBuffer = await timegroup.renderAudio(lastRenderedAudioEndMs, chunkEndMs, signal);
|
|
305
313
|
if (audioBuffer && audioBuffer.length > 0) await audioSource.add(audioBuffer);
|
|
306
314
|
} catch (e) {}
|
|
307
315
|
lastRenderedAudioEndMs = chunkEndMs;
|
|
308
316
|
}
|
|
309
317
|
if (videoSource && output && encodingCtx) {
|
|
310
|
-
const encodeStart = performance.now();
|
|
311
318
|
encodingCtx.drawImage(image, 0, 0, image.width, image.height, 0, 0, config.videoWidth, config.videoHeight);
|
|
312
|
-
await videoSource.add(timestampS, config.frameDurationS);
|
|
313
|
-
totalEncodeMs += performance.now() - encodeStart;
|
|
319
|
+
await videoSource.add(head.timestampS, config.frameDurationS);
|
|
314
320
|
}
|
|
315
|
-
|
|
321
|
+
encodedFrames++;
|
|
322
|
+
const currentFrame = encodedFrames;
|
|
316
323
|
const progress = currentFrame / config.totalFrames;
|
|
317
324
|
const renderedMs = currentFrame * config.frameDurationMs;
|
|
318
325
|
const elapsedMs = performance.now() - renderStartTime;
|
|
319
326
|
const msPerFrame = elapsedMs / currentFrame;
|
|
320
327
|
const estimatedRemainingMs = (config.totalFrames - currentFrame) * msPerFrame;
|
|
321
328
|
const speedMultiplier = renderedMs / elapsedMs;
|
|
322
|
-
if (thumbCanvas && thumbCtx && frameIndex % config.progressPreviewInterval === 0) thumbCtx.drawImage(image, 0, 0, thumbCanvas.width, thumbCanvas.height);
|
|
329
|
+
if (thumbCanvas && thumbCtx && head.frameIndex % config.progressPreviewInterval === 0) thumbCtx.drawImage(image, 0, 0, thumbCanvas.width, thumbCanvas.height);
|
|
323
330
|
onProgress?.({
|
|
324
331
|
progress,
|
|
325
332
|
currentFrame,
|
|
@@ -333,28 +340,9 @@ async function renderTimegroupToVideo(timegroup, options = {}) {
|
|
|
333
340
|
});
|
|
334
341
|
}
|
|
335
342
|
if (audioSource && lastRenderedAudioEndMs < config.endMs) try {
|
|
336
|
-
const audioBuffer = await timegroup.renderAudio(lastRenderedAudioEndMs, config.endMs);
|
|
343
|
+
const audioBuffer = await timegroup.renderAudio(lastRenderedAudioEndMs, config.endMs, signal);
|
|
337
344
|
if (audioBuffer && audioBuffer.length > 0) await audioSource.add(audioBuffer);
|
|
338
345
|
} catch (e) {}
|
|
339
|
-
const totalTime = performance.now() - renderStartTime;
|
|
340
|
-
const avgSeek = totalSeekMs / config.totalFrames;
|
|
341
|
-
const avgSync = totalSyncMs / config.totalFrames;
|
|
342
|
-
const avgRender = totalRenderMs / config.totalFrames;
|
|
343
|
-
const avgEncode = totalEncodeMs / config.totalFrames;
|
|
344
|
-
const avgTotal = totalTime / config.totalFrames;
|
|
345
|
-
const untracked = totalTime - (totalSeekMs + totalSyncMs + totalRenderMs + totalEncodeMs);
|
|
346
|
-
console.log(`\n=== Video Export Performance Breakdown ===`);
|
|
347
|
-
console.log(`Mode: Direct Serialization`);
|
|
348
|
-
console.log(`Total frames: ${config.totalFrames}`);
|
|
349
|
-
console.log(`Total time: ${totalTime.toFixed(0)}ms (${avgTotal.toFixed(1)}ms/frame)`);
|
|
350
|
-
console.log(`\nPer-stage totals:`);
|
|
351
|
-
console.log(` Seek: ${totalSeekMs.toFixed(0)}ms (${(totalSeekMs / totalTime * 100).toFixed(1)}%) - avg ${avgSeek.toFixed(1)}ms/frame`);
|
|
352
|
-
console.log(` Serialize: ${totalSyncMs.toFixed(0)}ms (${(totalSyncMs / totalTime * 100).toFixed(1)}%) - avg ${avgSync.toFixed(1)}ms/frame`);
|
|
353
|
-
console.log(` Render: ${totalRenderMs.toFixed(0)}ms (${(totalRenderMs / totalTime * 100).toFixed(1)}%) - avg ${avgRender.toFixed(1)}ms/frame`);
|
|
354
|
-
console.log(` Encode: ${totalEncodeMs.toFixed(0)}ms (${(totalEncodeMs / totalTime * 100).toFixed(1)}%) - avg ${avgEncode.toFixed(1)}ms/frame`);
|
|
355
|
-
console.log(` Other: ${untracked.toFixed(0)}ms (${(untracked / totalTime * 100).toFixed(1)}%)`);
|
|
356
|
-
console.log(`==========================================\n`);
|
|
357
|
-
logger.debug(`[renderTimegroupToVideo] ${config.totalFrames} frames: seek=${totalSeekMs.toFixed(0)}ms, sync=${totalSyncMs.toFixed(0)}ms, render=${totalRenderMs.toFixed(0)}ms, encode=${totalEncodeMs.toFixed(0)}ms, total=${totalTime.toFixed(0)}ms`);
|
|
358
346
|
if (config.benchmarkMode) return;
|
|
359
347
|
await output.finalize();
|
|
360
348
|
if (useStreaming) return;
|
|
@@ -373,5 +361,5 @@ async function renderTimegroupToVideo(timegroup, options = {}) {
|
|
|
373
361
|
}
|
|
374
362
|
|
|
375
363
|
//#endregion
|
|
376
|
-
export { RenderCancelledError, renderTimegroupToVideo };
|
|
364
|
+
export { NoSupportedAudioCodecError, QUALITY_HIGH, RenderCancelledError, getSupportedAudioCodecs, renderTimegroupToVideo };
|
|
377
365
|
//# sourceMappingURL=renderTimegroupToVideo.js.map
|