@editframe/elements 0.30.2-beta.0 → 0.31.0-beta.0
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.d.ts +5 -0
- package/dist/EF_FRAMEGEN.js +20 -4
- package/dist/EF_FRAMEGEN.js.map +1 -1
- package/dist/EF_INTERACTIVE.js.map +1 -1
- package/dist/_virtual/rolldown_runtime.js +27 -0
- package/dist/canvas/EFCanvas.d.ts +311 -0
- package/dist/canvas/EFCanvas.js +1089 -0
- package/dist/canvas/EFCanvas.js.map +1 -0
- package/dist/canvas/EFCanvasItem.d.ts +55 -0
- package/dist/canvas/EFCanvasItem.js +72 -0
- package/dist/canvas/EFCanvasItem.js.map +1 -0
- package/dist/canvas/api/CanvasAPI.d.ts +115 -0
- package/dist/canvas/api/CanvasAPI.js +182 -0
- package/dist/canvas/api/CanvasAPI.js.map +1 -0
- package/dist/canvas/api/types.d.ts +42 -0
- package/dist/canvas/coordinateTransform.js +90 -0
- package/dist/canvas/coordinateTransform.js.map +1 -0
- package/dist/canvas/getElementBounds.js +40 -0
- package/dist/canvas/getElementBounds.js.map +1 -0
- package/dist/canvas/overlays/SelectionOverlay.js +265 -0
- package/dist/canvas/overlays/SelectionOverlay.js.map +1 -0
- package/dist/canvas/overlays/overlayState.js +153 -0
- package/dist/canvas/overlays/overlayState.js.map +1 -0
- package/dist/canvas/selection/SelectionController.js +105 -0
- package/dist/canvas/selection/SelectionController.js.map +1 -0
- package/dist/canvas/selection/SelectionModel.d.ts +98 -0
- package/dist/canvas/selection/SelectionModel.js +229 -0
- package/dist/canvas/selection/SelectionModel.js.map +1 -0
- package/dist/canvas/selection/selectionContext.d.ts +31 -0
- package/dist/canvas/selection/selectionContext.js +12 -0
- package/dist/canvas/selection/selectionContext.js.map +1 -0
- package/dist/elements/ContainerInfo.d.ts +29 -0
- package/dist/elements/ContainerInfo.js +30 -0
- package/dist/elements/ContainerInfo.js.map +1 -0
- package/dist/elements/EFAudio.d.ts +13 -3
- package/dist/elements/EFAudio.js +64 -10
- package/dist/elements/EFAudio.js.map +1 -1
- package/dist/elements/EFCaptions.d.ts +18 -16
- package/dist/elements/EFCaptions.js +110 -19
- package/dist/elements/EFCaptions.js.map +1 -1
- package/dist/elements/EFImage.d.ts +12 -2
- package/dist/elements/EFImage.js +79 -9
- package/dist/elements/EFImage.js.map +1 -1
- package/dist/elements/EFMedia/AssetIdMediaEngine.js +51 -4
- package/dist/elements/EFMedia/AssetIdMediaEngine.js.map +1 -1
- package/dist/elements/EFMedia/AssetMediaEngine.js +125 -52
- package/dist/elements/EFMedia/AssetMediaEngine.js.map +1 -1
- package/dist/elements/EFMedia/BaseMediaEngine.js +24 -6
- package/dist/elements/EFMedia/BaseMediaEngine.js.map +1 -1
- package/dist/elements/EFMedia/JitMediaEngine.js +12 -8
- package/dist/elements/EFMedia/JitMediaEngine.js.map +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.js +46 -7
- package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.js.map +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.js +98 -73
- package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.js.map +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.js +28 -5
- package/dist/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.js.map +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioInputTask.js +18 -6
- package/dist/elements/EFMedia/audioTasks/makeAudioInputTask.js.map +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.js +8 -2
- package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.js.map +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.js +31 -6
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.js.map +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.js +28 -5
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.js.map +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.js +97 -72
- package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.js.map +1 -1
- package/dist/elements/EFMedia/shared/AudioSpanUtils.js +3 -1
- package/dist/elements/EFMedia/shared/AudioSpanUtils.js.map +1 -1
- package/dist/elements/EFMedia/shared/BufferUtils.js +1 -1
- package/dist/elements/EFMedia/shared/BufferUtils.js.map +1 -1
- package/dist/elements/EFMedia/shared/ThumbnailExtractor.js +25 -14
- package/dist/elements/EFMedia/shared/ThumbnailExtractor.js.map +1 -1
- package/dist/elements/EFMedia/tasks/makeMediaEngineTask.js +47 -16
- package/dist/elements/EFMedia/tasks/makeMediaEngineTask.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/MainVideoInputCache.js +37 -19
- package/dist/elements/EFMedia/videoTasks/MainVideoInputCache.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/ScrubInputCache.js +65 -21
- package/dist/elements/EFMedia/videoTasks/ScrubInputCache.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoBufferTask.js +8 -3
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoBufferTask.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoInitSegmentFetchTask.js +32 -9
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoInitSegmentFetchTask.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoInputTask.js +33 -10
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoInputTask.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.js +23 -8
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentFetchTask.js +34 -10
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentFetchTask.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentIdTask.js +31 -8
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentIdTask.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.js +31 -114
- package/dist/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.js +44 -8
- package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.js.map +1 -1
- package/dist/elements/EFMedia.d.ts +18 -7
- package/dist/elements/EFMedia.js +23 -3
- package/dist/elements/EFMedia.js.map +1 -1
- package/dist/elements/EFPanZoom.d.ts +96 -0
- package/dist/elements/EFPanZoom.js +290 -0
- package/dist/elements/EFPanZoom.js.map +1 -0
- package/dist/elements/EFSourceMixin.js +7 -6
- package/dist/elements/EFSourceMixin.js.map +1 -1
- package/dist/elements/EFSurface.d.ts +6 -6
- package/dist/elements/EFSurface.js +7 -2
- package/dist/elements/EFSurface.js.map +1 -1
- package/dist/elements/EFTemporal.d.ts +2 -1
- package/dist/elements/EFTemporal.js +192 -71
- package/dist/elements/EFTemporal.js.map +1 -1
- package/dist/elements/EFText.d.ts +5 -4
- package/dist/elements/EFText.js +102 -13
- package/dist/elements/EFText.js.map +1 -1
- package/dist/elements/EFTextSegment.d.ts +32 -6
- package/dist/elements/EFTextSegment.js +53 -15
- package/dist/elements/EFTextSegment.js.map +1 -1
- package/dist/elements/EFThumbnailStrip.d.ts +118 -56
- package/dist/elements/EFThumbnailStrip.js +522 -358
- package/dist/elements/EFThumbnailStrip.js.map +1 -1
- package/dist/elements/EFTimegroup.d.ts +223 -27
- package/dist/elements/EFTimegroup.js +850 -147
- package/dist/elements/EFTimegroup.js.map +1 -1
- package/dist/elements/EFVideo.d.ts +42 -5
- package/dist/elements/EFVideo.js +165 -11
- package/dist/elements/EFVideo.js.map +1 -1
- package/dist/elements/EFWaveform.d.ts +6 -6
- package/dist/elements/EFWaveform.js +2 -1
- package/dist/elements/EFWaveform.js.map +1 -1
- package/dist/elements/ElementPositionInfo.d.ts +35 -0
- package/dist/elements/ElementPositionInfo.js +49 -0
- package/dist/elements/ElementPositionInfo.js.map +1 -0
- package/dist/elements/FetchMixin.js +16 -1
- package/dist/elements/FetchMixin.js.map +1 -1
- package/dist/elements/SessionThumbnailCache.js +152 -0
- package/dist/elements/SessionThumbnailCache.js.map +1 -0
- package/dist/elements/TargetController.js +3 -1
- package/dist/elements/TargetController.js.map +1 -1
- package/dist/elements/TimegroupController.js +9 -3
- package/dist/elements/TimegroupController.js.map +1 -1
- package/dist/elements/findRootTemporal.js +30 -0
- package/dist/elements/findRootTemporal.js.map +1 -0
- package/dist/elements/renderTemporalAudio.js +18 -5
- package/dist/elements/renderTemporalAudio.js.map +1 -1
- package/dist/elements/updateAnimations.js +171 -28
- package/dist/elements/updateAnimations.js.map +1 -1
- package/dist/getRenderInfo.d.ts +2 -2
- package/dist/gui/ContextMixin.js +4 -2
- package/dist/gui/ContextMixin.js.map +1 -1
- package/dist/gui/Controllable.js +74 -1
- package/dist/gui/Controllable.js.map +1 -1
- package/dist/gui/EFActiveRootTemporal.d.ts +50 -0
- package/dist/gui/EFActiveRootTemporal.js +94 -0
- package/dist/gui/EFActiveRootTemporal.js.map +1 -0
- package/dist/gui/EFConfiguration.d.ts +11 -5
- package/dist/gui/EFConfiguration.js.map +1 -1
- package/dist/gui/EFControls.d.ts +2 -2
- package/dist/gui/EFControls.js +109 -13
- package/dist/gui/EFControls.js.map +1 -1
- package/dist/gui/EFDial.d.ts +4 -4
- package/dist/gui/EFFilmstrip.d.ts +11 -214
- package/dist/gui/EFFilmstrip.js +53 -1152
- package/dist/gui/EFFilmstrip.js.map +1 -1
- package/dist/gui/EFFitScale.d.ts +3 -3
- package/dist/gui/EFFitScale.js +39 -12
- package/dist/gui/EFFitScale.js.map +1 -1
- package/dist/gui/EFFocusOverlay.d.ts +4 -4
- package/dist/gui/EFOverlayItem.d.ts +48 -0
- package/dist/gui/EFOverlayItem.js +97 -0
- package/dist/gui/EFOverlayItem.js.map +1 -0
- package/dist/gui/EFOverlayLayer.d.ts +70 -0
- package/dist/gui/EFOverlayLayer.js +104 -0
- package/dist/gui/EFOverlayLayer.js.map +1 -0
- package/dist/gui/EFPause.d.ts +4 -4
- package/dist/gui/EFPlay.d.ts +4 -4
- package/dist/gui/EFResizableBox.d.ts +12 -16
- package/dist/gui/EFResizableBox.js +109 -451
- package/dist/gui/EFResizableBox.js.map +1 -1
- package/dist/gui/EFScrubber.d.ts +30 -5
- package/dist/gui/EFScrubber.js +224 -31
- package/dist/gui/EFScrubber.js.map +1 -1
- package/dist/gui/EFTimeDisplay.d.ts +4 -4
- package/dist/gui/EFTimeDisplay.js +4 -1
- package/dist/gui/EFTimeDisplay.js.map +1 -1
- package/dist/gui/EFTimelineRuler.d.ts +71 -0
- package/dist/gui/EFTimelineRuler.js +320 -0
- package/dist/gui/EFTimelineRuler.js.map +1 -0
- package/dist/gui/EFToggleLoop.d.ts +4 -4
- package/dist/gui/EFTogglePlay.d.ts +4 -4
- package/dist/gui/EFTransformHandles.d.ts +91 -0
- package/dist/gui/EFTransformHandles.js +393 -0
- package/dist/gui/EFTransformHandles.js.map +1 -0
- package/dist/gui/EFWorkbench.d.ts +182 -4
- package/dist/gui/EFWorkbench.js +2067 -22
- package/dist/gui/EFWorkbench.js.map +1 -1
- package/dist/gui/FitScaleHelpers.d.ts +31 -0
- package/dist/gui/FitScaleHelpers.js +41 -0
- package/dist/gui/FitScaleHelpers.js.map +1 -0
- package/dist/gui/PlaybackController.d.ts +2 -1
- package/dist/gui/PlaybackController.js +46 -15
- 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/hierarchy/EFHierarchy.d.ts +65 -0
- package/dist/gui/hierarchy/EFHierarchy.js +338 -0
- package/dist/gui/hierarchy/EFHierarchy.js.map +1 -0
- package/dist/gui/hierarchy/EFHierarchyItem.d.ts +118 -0
- package/dist/gui/hierarchy/EFHierarchyItem.js +551 -0
- package/dist/gui/hierarchy/EFHierarchyItem.js.map +1 -0
- package/dist/gui/hierarchy/hierarchyContext.d.ts +38 -0
- package/dist/gui/hierarchy/hierarchyContext.js +8 -0
- package/dist/gui/hierarchy/hierarchyContext.js.map +1 -0
- package/dist/gui/icons.js +34 -0
- package/dist/gui/icons.js.map +1 -0
- package/dist/gui/panZoomTransformContext.js +12 -0
- package/dist/gui/panZoomTransformContext.js.map +1 -0
- package/dist/gui/previewSettingsContext.js +12 -0
- package/dist/gui/previewSettingsContext.js.map +1 -0
- package/dist/gui/timeline/EFTimeline.d.ts +270 -0
- package/dist/gui/timeline/EFTimeline.js +1369 -0
- package/dist/gui/timeline/EFTimeline.js.map +1 -0
- package/dist/gui/timeline/EFTimelineRow.js +374 -0
- package/dist/gui/timeline/EFTimelineRow.js.map +1 -0
- package/dist/gui/timeline/TrimHandles.d.ts +36 -0
- package/dist/gui/timeline/TrimHandles.js +204 -0
- package/dist/gui/timeline/TrimHandles.js.map +1 -0
- package/dist/gui/timeline/flattenHierarchy.js +31 -0
- package/dist/gui/timeline/flattenHierarchy.js.map +1 -0
- package/dist/gui/timeline/timelineStateContext.d.ts +26 -0
- package/dist/gui/timeline/timelineStateContext.js +42 -0
- package/dist/gui/timeline/timelineStateContext.js.map +1 -0
- package/dist/gui/timeline/tracks/AudioTrack.js +264 -0
- package/dist/gui/timeline/tracks/AudioTrack.js.map +1 -0
- package/dist/gui/timeline/tracks/CaptionsTrack.js +595 -0
- package/dist/gui/timeline/tracks/CaptionsTrack.js.map +1 -0
- package/dist/gui/timeline/tracks/HTMLTrack.js +19 -0
- package/dist/gui/timeline/tracks/HTMLTrack.js.map +1 -0
- package/dist/gui/timeline/tracks/ImageTrack.js +53 -0
- package/dist/gui/timeline/tracks/ImageTrack.js.map +1 -0
- package/dist/gui/timeline/tracks/TextTrack.js +250 -0
- package/dist/gui/timeline/tracks/TextTrack.js.map +1 -0
- package/dist/gui/timeline/tracks/TimegroupTrack.js +143 -0
- package/dist/gui/timeline/tracks/TimegroupTrack.js.map +1 -0
- package/dist/gui/timeline/tracks/TrackItem.js +269 -0
- package/dist/gui/timeline/tracks/TrackItem.js.map +1 -0
- package/dist/gui/timeline/tracks/VideoTrack.js +265 -0
- package/dist/gui/timeline/tracks/VideoTrack.js.map +1 -0
- package/dist/gui/timeline/tracks/WaveformTrack.js +19 -0
- package/dist/gui/timeline/tracks/WaveformTrack.js.map +1 -0
- package/dist/gui/timeline/tracks/ensureTrackItemInit.js +1 -0
- package/dist/gui/timeline/tracks/preloadTracks.js +9 -0
- package/dist/gui/timeline/tracks/renderTrackChildren.js +119 -0
- package/dist/gui/timeline/tracks/renderTrackChildren.js.map +1 -0
- package/dist/gui/timeline/tracks/waveformUtils.js +80 -0
- package/dist/gui/timeline/tracks/waveformUtils.js.map +1 -0
- package/dist/gui/transformCalculations.js +217 -0
- package/dist/gui/transformCalculations.js.map +1 -0
- package/dist/gui/transformUtils.d.ts +37 -0
- package/dist/gui/transformUtils.js +77 -0
- package/dist/gui/transformUtils.js.map +1 -0
- package/dist/gui/tree/EFTree.d.ts +59 -0
- package/dist/gui/tree/EFTree.js +174 -0
- package/dist/gui/tree/EFTree.js.map +1 -0
- package/dist/gui/tree/EFTreeItem.d.ts +38 -0
- package/dist/gui/tree/EFTreeItem.js +146 -0
- package/dist/gui/tree/EFTreeItem.js.map +1 -0
- package/dist/gui/tree/treeContext.d.ts +60 -0
- package/dist/gui/tree/treeContext.js +23 -0
- package/dist/gui/tree/treeContext.js.map +1 -0
- package/dist/index.d.ts +32 -8
- package/dist/index.js +30 -6
- package/dist/index.js.map +1 -1
- package/dist/node_modules/react/cjs/react-jsx-runtime.development.js +688 -0
- package/dist/node_modules/react/cjs/react-jsx-runtime.development.js.map +1 -0
- package/dist/node_modules/react/cjs/react.development.js +1521 -0
- package/dist/node_modules/react/cjs/react.development.js.map +1 -0
- package/dist/node_modules/react/index.js +13 -0
- package/dist/node_modules/react/index.js.map +1 -0
- package/dist/node_modules/react/jsx-runtime.js +13 -0
- package/dist/node_modules/react/jsx-runtime.js.map +1 -0
- package/dist/preview/AdaptiveResolutionTracker.js +228 -0
- package/dist/preview/AdaptiveResolutionTracker.js.map +1 -0
- package/dist/preview/RenderProfiler.js +135 -0
- package/dist/preview/RenderProfiler.js.map +1 -0
- package/dist/preview/previewSettings.js +131 -0
- package/dist/preview/previewSettings.js.map +1 -0
- package/dist/preview/previewTypes.js +64 -0
- package/dist/preview/previewTypes.js.map +1 -0
- package/dist/preview/renderTimegroupPreview.js +656 -0
- package/dist/preview/renderTimegroupPreview.js.map +1 -0
- package/dist/preview/renderTimegroupToCanvas.d.ts +37 -0
- package/dist/preview/renderTimegroupToCanvas.js +840 -0
- package/dist/preview/renderTimegroupToCanvas.js.map +1 -0
- package/dist/preview/renderTimegroupToVideo.d.ts +39 -0
- package/dist/preview/renderTimegroupToVideo.js +274 -0
- package/dist/preview/renderTimegroupToVideo.js.map +1 -0
- package/dist/preview/renderers.js +16 -0
- package/dist/preview/renderers.js.map +1 -0
- package/dist/preview/statsTrackingStrategy.js +201 -0
- package/dist/preview/statsTrackingStrategy.js.map +1 -0
- package/dist/preview/thumbnailCacheSettings.js +52 -0
- package/dist/preview/thumbnailCacheSettings.js.map +1 -0
- package/dist/preview/workers/WorkerPool.js +178 -0
- package/dist/preview/workers/WorkerPool.js.map +1 -0
- package/dist/sandbox/PlaybackControls.js +10 -0
- package/dist/sandbox/PlaybackControls.js.map +1 -0
- package/dist/sandbox/ScenarioRunner.js +1 -0
- package/dist/sandbox/index.js +2 -0
- package/dist/style.css +68 -67
- package/dist/transcoding/types/index.d.ts +2 -1
- package/dist/transcoding/utils/UrlGenerator.d.ts +6 -1
- package/dist/transcoding/utils/UrlGenerator.js +12 -3
- package/dist/transcoding/utils/UrlGenerator.js.map +1 -1
- package/dist/utils/LRUCache.js +1 -375
- package/dist/utils/LRUCache.js.map +1 -1
- package/dist/utils/frameTime.js +14 -0
- package/dist/utils/frameTime.js.map +1 -0
- package/package.json +3 -3
- package/test/profilingPlugin.ts +223 -0
- package/test/recordReplayProxyPlugin.js +22 -27
- package/test/thumbnail-performance-test.html +116 -0
- package/test/visualRegressionUtils.ts +286 -0
- package/types.json +1 -1
- package/dist/elements/TimegroupController.d.ts +0 -18
- package/dist/msToTimeCode.js +0 -17
- package/dist/msToTimeCode.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"renderTimegroupToCanvas.js","names":["timeMs: number","timeoutMs: number","blankVideos: string[]","_xmlSerializer: XMLSerializer | null","_textEncoder: TextEncoder | null","_workerPool: WorkerPool | null","workerUrl: string","dataUrl: string","results: Array<{ canvas: HTMLCanvasElement; dataUrl: string; preserveAlpha: boolean }>","scaleColors: Record<number, string>","canvasRestoreInfo: CanvasRestoreInfo[]","base64: string","captureCanvas: HTMLCanvasElement","dpr","image: HTMLCanvasElement | HTMLImageElement","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 type SyncState,\n} from \"./renderTimegroupPreview.js\";\nimport { getEffectiveRenderMode } from \"./renderers.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 { WorkerPool, encodeCanvasInWorker } from \"./workers/WorkerPool.js\";\nimport {\n type TemporalElement,\n isVisibleAtTime,\n DEFAULT_WIDTH,\n DEFAULT_HEIGHT,\n DEFAULT_THUMBNAIL_SCALE,\n DEFAULT_BLOCKING_TIMEOUT_MS,\n JPEG_QUALITY_HIGH,\n JPEG_QUALITY_MEDIUM,\n createPreviewContainer,\n} from \"./previewTypes.js\";\nimport { defaultProfiler } from \"./RenderProfiler.js\";\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/** Interval between profiling log outputs (ms) */\nconst PROFILING_LOG_INTERVAL_MS = 2000;\n\n/** Maximum number of cached inline images before eviction */\nconst MAX_INLINE_IMAGE_CACHE_SIZE = 100;\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 * Extended CanvasRenderingContext2D with HTML-in-Canvas API support.\n * @see https://github.com/WICG/html-in-canvas\n */\ninterface HtmlInCanvasContext extends CanvasRenderingContext2D {\n drawElementImage(element: HTMLElement, x: number, y: number): void;\n}\n\n/**\n * Extended HTMLCanvasElement with layoutSubtree property for HTML-in-Canvas.\n */\ninterface HtmlInCanvasElement extends HTMLCanvasElement {\n layoutSubtree?: boolean;\n}\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/** Image cache for inlining external images as data URIs (foreignObject path) */\nconst _inlineImageCache = new Map<string, string>();\n\n/** Track canvases that have been initialized for layoutsubtree (only need to wait once) */\nconst _layoutInitializedCanvases = new WeakSet<HTMLCanvasElement>();\n\n// Reusable instances for better performance (avoid creating new instances every frame)\nlet _xmlSerializer: XMLSerializer | null = null;\nlet _textEncoder: TextEncoder | null = null;\n\n// Worker pool for parallel canvas encoding (lazy initialization)\nlet _workerPool: WorkerPool | null = null;\nlet _workerPoolWarningLogged = false;\n\n/**\n * Get or create the worker pool for canvas encoding.\n * Returns null if workers are not available.\n */\nfunction getWorkerPool(): WorkerPool | null {\n if (_workerPool) {\n return _workerPool;\n }\n\n // Check if workers are available\n if (\n typeof Worker === \"undefined\" ||\n typeof OffscreenCanvas === \"undefined\" ||\n typeof createImageBitmap === \"undefined\"\n ) {\n if (!_workerPoolWarningLogged) {\n _workerPoolWarningLogged = true;\n console.warn(\n \"[renderTimegroupToCanvas] Web Workers or OffscreenCanvas not available, using main thread fallback\",\n );\n }\n return null;\n }\n\n try {\n // Create worker URL - Vite processes worker files when using ?worker_file suffix\n // In browser environments (Vite), import.meta.url is available at runtime\n let workerUrl: string;\n try {\n // TypeScript may not recognize import.meta in CommonJS mode, but it works at runtime in Vite\n // Access it dynamically to avoid TypeScript compilation errors\n // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n // @ts-expect-error - import.meta.url works at runtime in Vite even if TS doesn't recognize it\n const metaUrl = import.meta?.url;\n if (metaUrl) {\n // Use ?worker_file&type=module - this is what Vite uses internally when processing workers\n // The ?worker suffix creates a wrapper, but ?worker_file gives us the actual worker\n workerUrl = new URL(\"./workers/encoderWorker.ts?worker_file&type=module\", metaUrl).href;\n } else {\n workerUrl = \"./workers/encoderWorker.ts?worker_file&type=module\";\n }\n } catch {\n // Fallback: use relative path\n workerUrl = \"./workers/encoderWorker.ts?worker_file&type=module\";\n }\n \n _workerPool = new WorkerPool(workerUrl);\n \n // Check if workers were actually created\n if (!_workerPool.isAvailable()) {\n const reason = _workerPool.workerCount === 0 \n ? \"no workers created (check console for errors)\" \n : \"workers not available\";\n _workerPool = null;\n if (!_workerPoolWarningLogged) {\n _workerPoolWarningLogged = true;\n console.warn(\n `[renderTimegroupToCanvas] Worker pool initialization failed (${reason}), using main thread fallback`,\n );\n }\n }\n } catch (error) {\n _workerPool = null;\n if (!_workerPoolWarningLogged) {\n _workerPoolWarningLogged = true;\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.warn(\n `[renderTimegroupToCanvas] Failed to create worker pool: ${errorMessage} - using main thread fallback`,\n );\n }\n }\n\n return _workerPool;\n}\n\n/**\n * Encode a single canvas to a data URL (fallback implementation for main thread).\n */\nfunction encodeCanvasOnMainThread(\n canvas: HTMLCanvasElement,\n canvasScale: number,\n): { dataUrl: string; preserveAlpha: boolean } | null {\n try {\n if (canvas.width === 0 || canvas.height === 0) {\n return null;\n }\n\n const preserveAlpha = canvas.dataset.preserveAlpha === \"true\";\n let dataUrl: string;\n\n if (canvasScale < 1) {\n // Scale down canvas before encoding\n const scaledWidth = Math.floor(canvas.width * canvasScale);\n const scaledHeight = Math.floor(canvas.height * canvasScale);\n const scaledCanvas = document.createElement(\"canvas\");\n scaledCanvas.width = scaledWidth;\n scaledCanvas.height = scaledHeight;\n const scaledCtx = scaledCanvas.getContext(\"2d\");\n if (scaledCtx) {\n scaledCtx.drawImage(canvas, 0, 0, scaledWidth, scaledHeight);\n const quality = canvasScale < 0.5 ? JPEG_QUALITY_MEDIUM : JPEG_QUALITY_HIGH;\n dataUrl = preserveAlpha\n ? scaledCanvas.toDataURL(\"image/png\")\n : scaledCanvas.toDataURL(\"image/jpeg\", quality);\n } else {\n dataUrl = preserveAlpha\n ? canvas.toDataURL(\"image/png\")\n : canvas.toDataURL(\"image/jpeg\", JPEG_QUALITY_HIGH);\n }\n } else {\n dataUrl = preserveAlpha\n ? canvas.toDataURL(\"image/png\")\n : canvas.toDataURL(\"image/jpeg\", JPEG_QUALITY_HIGH);\n }\n\n return { dataUrl, preserveAlpha };\n } catch (e) {\n // Cross-origin canvas or other error - skip\n return null;\n }\n}\n\n/**\n * Encode canvases to data URLs in parallel using worker pool.\n * Falls back to main thread encoding if workers are unavailable.\n */\nasync function encodeCanvasesInParallel(\n canvases: HTMLCanvasElement[],\n canvasScale: number = 1,\n): Promise<Array<{ canvas: HTMLCanvasElement; dataUrl: string; preserveAlpha: boolean }>> {\n const workerPool = getWorkerPool();\n\n // If no worker pool available, fall back to main thread\n if (!workerPool) {\n const results: Array<{ canvas: HTMLCanvasElement; dataUrl: string; preserveAlpha: boolean }> = [];\n for (const canvas of canvases) {\n const encoded = encodeCanvasOnMainThread(canvas, canvasScale);\n if (encoded) {\n results.push({ canvas, ...encoded });\n }\n }\n return results;\n }\n\n // Use worker pool for parallel encoding\n const encodingTasks = canvases.map(async (canvas) => {\n try {\n if (canvas.width === 0 || canvas.height === 0) {\n return null;\n }\n\n const preserveAlpha = canvas.dataset.preserveAlpha === \"true\";\n let sourceCanvas = canvas;\n\n // Handle canvas scaling on main thread before encoding\n if (canvasScale < 1) {\n const scaledWidth = Math.floor(canvas.width * canvasScale);\n const scaledHeight = Math.floor(canvas.height * canvasScale);\n const scaledCanvas = document.createElement(\"canvas\");\n scaledCanvas.width = scaledWidth;\n scaledCanvas.height = scaledHeight;\n const scaledCtx = scaledCanvas.getContext(\"2d\");\n if (scaledCtx) {\n scaledCtx.drawImage(canvas, 0, 0, scaledWidth, scaledHeight);\n sourceCanvas = scaledCanvas;\n }\n }\n \n // Encode in worker\n const dataUrl = await workerPool.execute((worker) =>\n encodeCanvasInWorker(worker, sourceCanvas, preserveAlpha),\n );\n\n return { canvas, dataUrl, preserveAlpha };\n } catch (error) {\n // Fallback to main thread if worker encoding fails\n const encoded = encodeCanvasOnMainThread(canvas, canvasScale);\n if (encoded) {\n return { canvas, ...encoded };\n }\n \n // Cross-origin canvas or other error - skip\n return null;\n }\n });\n\n const encodedResults = await Promise.all(encodingTasks);\n const validResults = encodedResults.filter(\n (r): r is { canvas: HTMLCanvasElement; dataUrl: string; preserveAlpha: boolean } => r !== null,\n );\n return validResults;\n}\n\n/**\n * Fast base64 encoding directly from Uint8Array.\n * Avoids the overhead of converting to binary string first.\n * Uses lookup table for optimal performance.\n */\nfunction encodeBase64Fast(bytes: Uint8Array): string {\n const base64Chars = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/\";\n let result = \"\";\n let i = 0;\n const len = bytes.length;\n \n // Process 3 bytes at a time (produces 4 base64 chars)\n while (i < len - 2) {\n const byte1 = bytes[i++]!;\n const byte2 = bytes[i++]!;\n const byte3 = bytes[i++]!;\n \n const bitmap = (byte1 << 16) | (byte2 << 8) | byte3;\n \n result += base64Chars.charAt((bitmap >> 18) & 63);\n result += base64Chars.charAt((bitmap >> 12) & 63);\n result += base64Chars.charAt((bitmap >> 6) & 63);\n result += base64Chars.charAt(bitmap & 63);\n }\n \n // Handle remaining bytes (1 or 2)\n if (i < len) {\n const byte1 = bytes[i++]!;\n const bitmap = byte1 << 16;\n \n result += base64Chars.charAt((bitmap >> 18) & 63);\n result += base64Chars.charAt((bitmap >> 12) & 63);\n \n if (i < len) {\n const byte2 = bytes[i++]!;\n const bitmap2 = (byte1 << 16) | (byte2 << 8);\n result += base64Chars.charAt((bitmap2 >> 6) & 63);\n result += \"=\";\n } else {\n result += \"==\";\n }\n }\n \n return result;\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 _inlineImageCache.clear();\n}\n\n/**\n * Clear the inline image cache. Useful for memory management in long-running sessions.\n */\nexport function clearInlineImageCache(): void {\n _inlineImageCache.clear();\n}\n\n/**\n * Get current inline image cache size for diagnostics.\n */\nexport function getInlineImageCacheSize(): number {\n return _inlineImageCache.size;\n}\n\n// ============================================================================\n// Internal Helpers\n// ============================================================================\n\n/**\n * Options for creating a DPR-aware canvas.\n */\ninterface CanvasOptions {\n /** Render width (internal resolution) */\n renderWidth: number;\n /** Render height (internal resolution) */\n renderHeight: number;\n /** Display scale factor */\n scale: number;\n /** Device pixel ratio (defaults to window.devicePixelRatio) */\n dpr?: number;\n /** Full logical width (for CSS sizing) */\n fullWidth: number;\n /** Full logical height (for CSS sizing) */\n fullHeight: number;\n}\n\n/**\n * Create a canvas element with proper DPR handling.\n * Buffer size is based on renderWidth/renderHeight (internal resolution).\n * CSS size is based on fullWidth/fullHeight (logical display size).\n */\nfunction createDprCanvas(options: CanvasOptions): HTMLCanvasElement {\n const { renderWidth, renderHeight, scale, fullWidth, fullHeight } = options;\n const dpr = options.dpr ?? window.devicePixelRatio ?? 1;\n \n const canvas = document.createElement(\"canvas\");\n canvas.width = Math.floor(renderWidth * scale * dpr);\n canvas.height = Math.floor(renderHeight * scale * dpr);\n canvas.style.width = `${Math.floor(fullWidth * scale)}px`;\n canvas.style.height = `${Math.floor(fullHeight * scale)}px`;\n \n return canvas;\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 * Information needed to restore canvases after serialization.\n */\ninterface CanvasRestoreInfo {\n canvas: HTMLCanvasElement;\n parent: Node;\n nextSibling: Node | null;\n img: HTMLImageElement;\n}\n\n/**\n * Options for SVG serialization.\n */\ninterface SerializeToSvgOptions {\n /** Scale factor for encoding canvases (default: 1) */\n canvasScale?: number;\n /** Whether to inline external images (default: false for cloned containers) */\n inlineImages?: boolean;\n /** Whether to log early render info (default: false) */\n logEarlyRenders?: boolean;\n}\n\n/**\n * Result of SVG serialization.\n */\ninterface SerializationResult {\n dataUri: string;\n /** Call this to restore canvases if they were modified in-place */\n restore: () => void;\n}\n\n/**\n * Common SVG foreignObject serialization pipeline.\n * Handles canvas encoding, serialization, and base64 encoding.\n * \n * @param container - The HTML element to serialize\n * @param width - Output width\n * @param height - Output height\n * @param options - Serialization options\n * @returns Serialization result with data URI and restore function\n */\nasync function serializeToSvgDataUri(\n container: HTMLElement,\n width: number,\n height: number,\n options: SerializeToSvgOptions = {},\n): Promise<SerializationResult> {\n const { canvasScale = 1, inlineImages: shouldInlineImages = false, logEarlyRenders = false } = options;\n \n // Store info for restoration (only used if modifying in-place)\n const canvasRestoreInfo: CanvasRestoreInfo[] = [];\n \n // Phase 1: Encode canvases to data URLs (parallel)\n const canvasStart = performance.now();\n const canvases = Array.from(container.querySelectorAll(\"canvas\"));\n const encodedResults = await encodeCanvasesInParallel(canvases, canvasScale);\n \n // Replace canvases with images\n for (const { canvas, dataUrl } of encodedResults) {\n try {\n const img = document.createElement(\"img\");\n img.src = dataUrl;\n img.width = canvas.width;\n img.height = canvas.height;\n const style = canvas.getAttribute(\"style\");\n if (style) img.setAttribute(\"style\", style);\n \n const parent = canvas.parentNode;\n if (parent) {\n const nextSibling = canvas.nextSibling;\n parent.replaceChild(img, canvas);\n canvasRestoreInfo.push({ canvas, parent, nextSibling, img });\n }\n } catch {\n // Cross-origin canvas - leave as-is\n }\n }\n defaultProfiler.addTime(\"canvasEncode\", performance.now() - canvasStart);\n \n // Phase 2: Inline external images (if requested)\n if (shouldInlineImages) {\n const inlineStart = performance.now();\n await inlineImages(container);\n defaultProfiler.addTime(\"inline\", performance.now() - inlineStart);\n }\n \n // Phase 3: Serialize to XHTML\n const serializeStart = performance.now();\n const wrapper = document.createElement(\"div\");\n wrapper.setAttribute(\"xmlns\", \"http://www.w3.org/1999/xhtml\");\n wrapper.setAttribute(\"style\", `width:${width}px;height:${height}px;overflow:hidden;position:relative;`);\n wrapper.appendChild(container);\n \n if (!_xmlSerializer) {\n _xmlSerializer = new XMLSerializer();\n }\n const serialized = _xmlSerializer.serializeToString(wrapper);\n defaultProfiler.addTime(\"serialize\", performance.now() - serializeStart);\n \n // Prepare restore function (removes container from wrapper, restores canvases)\n const restore = (): void => {\n const restoreStart = performance.now();\n wrapper.removeChild(container);\n \n for (const { canvas, parent, nextSibling, img } of canvasRestoreInfo) {\n if (img.parentNode === parent) {\n if (nextSibling) {\n parent.insertBefore(canvas, nextSibling);\n parent.removeChild(img);\n } else {\n parent.replaceChild(canvas, img);\n }\n }\n }\n defaultProfiler.addTime(\"restore\", performance.now() - restoreStart);\n };\n \n // DEBUG: Log serialized HTML size for early renders\n if (logEarlyRenders && defaultProfiler.isEarlyRender(2)) {\n console.log(`[serializeToSvgDataUri] FO serialized: ${serialized.length} chars`);\n }\n \n // Phase 4: Create SVG and encode to base64\n const base64Start = performance.now();\n const svg = `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${width}\" height=\"${height}\"><foreignObject width=\"100%\" height=\"100%\">${serialized}</foreignObject></svg>`;\n \n if (!_textEncoder) {\n _textEncoder = new TextEncoder();\n }\n const utf8Bytes = _textEncoder.encode(svg);\n \n let base64: string;\n if (typeof (Uint8Array.prototype as any).toBase64 === \"function\") {\n base64 = (utf8Bytes as any).toBase64();\n } else {\n base64 = encodeBase64Fast(utf8Bytes);\n }\n const dataUri = `data:image/svg+xml;base64,${base64}`;\n defaultProfiler.addTime(\"base64\", performance.now() - base64Start);\n \n return { dataUri, restore };\n}\n\n/**\n * Inline all images in a container as base64 data URIs.\n * SVG foreignObject can't load external images due to security restrictions.\n * Uses an LRU-style cache with size limits to prevent memory leaks.\n */\nasync function inlineImages(container: HTMLElement): Promise<void> {\n const images = container.querySelectorAll(\"img\");\n for (const image of images) {\n const src = image.getAttribute(\"src\");\n if (!src || src.startsWith(\"data:\")) continue;\n\n const cached = _inlineImageCache.get(src);\n if (cached) {\n image.setAttribute(\"src\", cached);\n continue;\n }\n\n try {\n const response = await fetch(src);\n const blob = await response.blob();\n const dataUrl = await blobToDataURL(blob);\n image.setAttribute(\"src\", dataUrl);\n \n // Evict oldest entries if cache is full (simple FIFO eviction)\n if (_inlineImageCache.size >= MAX_INLINE_IMAGE_CACHE_SIZE) {\n const firstKey = _inlineImageCache.keys().next().value;\n if (firstKey) _inlineImageCache.delete(firstKey);\n }\n _inlineImageCache.set(src, dataUrl);\n } catch (e) {\n console.warn(\"Failed to inline image:\", src, e);\n }\n }\n}\n\n\n/**\n * Convert a Blob to a data URL.\n */\nfunction blobToDataURL(blob: Blob): Promise<string> {\n return new Promise((resolve, reject) => {\n const reader = new FileReader();\n reader.onload = () => resolve(reader.result as string);\n reader.onerror = reject;\n reader.readAsDataURL(blob);\n });\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 * Wait for multiple animation frames to ensure all paints are flushed.\n * This is necessary because video frame decoding and canvas painting may\n * happen asynchronously even after seek() returns.\n */\nfunction waitForPaintFlush(): Promise<void> {\n return new Promise(resolve => {\n // Double RAF ensures we wait for:\n // 1. First RAF: Any pending paints are scheduled\n // 2. Second RAF: Those paints have completed\n requestAnimationFrame(() => {\n requestAnimationFrame(() => resolve());\n });\n });\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 native rendering.\n */\nexport interface NativeRenderOptions {\n /**\n * Wait for RAF before capturing. Only needed if content hasn't been laid out yet.\n * Default: false (capture immediately - frame tasks should already be complete)\n * \n * Set to true only for edge cases where you're rendering content that was just\n * added to the DOM and hasn't had a chance to layout yet.\n */\n waitForPaint?: boolean;\n \n /**\n * Reuse an existing canvas instead of creating a new one.\n * The canvas must have layoutsubtree enabled and be in the DOM.\n */\n reuseCanvas?: HTMLCanvasElement;\n \n /**\n * Skip device pixel ratio scaling. When true, renders at 1x regardless of display DPR.\n * Default: false (respects display DPR for crisp rendering)\n * \n * Set to true for video export where retina resolution isn't needed.\n * This can provide a 4x speedup on 2x DPR displays!\n */\n skipDprScaling?: boolean;\n}\n\n/**\n * Render HTML content to canvas using native HTML-in-Canvas API (drawElementImage).\n * This is much faster than the foreignObject approach and avoids canvas tainting.\n * \n * Note: The native API renders at device pixel ratio, so we capture at DPR scale\n * and then downsample to logical pixels to match the foreignObject path's output.\n * \n * @param container - The HTML element to render\n * @param width - Target width in logical pixels\n * @param height - Target height in logical pixels\n * @param options - Rendering options (skipWait for batch mode)\n * \n * @see https://github.com/WICG/html-in-canvas\n */\nexport async function renderToImageNative(\n container: HTMLElement,\n width: number,\n height: number,\n options: NativeRenderOptions = {},\n): Promise<HTMLCanvasElement> {\n const t0 = performance.now();\n const { waitForPaint = false, reuseCanvas, skipDprScaling = false } = options;\n // Use 1x DPR when skipDprScaling is true (for video export) - 4x fewer pixels!\n const dpr = skipDprScaling ? 1 : (window.devicePixelRatio || 1);\n \n // Use provided canvas or create new one\n let captureCanvas: HTMLCanvasElement;\n let shouldCleanup = false;\n \n if (reuseCanvas) {\n captureCanvas = reuseCanvas;\n \n // Ensure canvas dimensions match (both attribute and CSS)\n const dpr = skipDprScaling ? 1 : (window.devicePixelRatio || 1);\n const targetWidth = Math.floor(width * dpr);\n const targetHeight = Math.floor(height * dpr);\n \n // Set attribute dimensions (pixel buffer size)\n if (captureCanvas.width !== targetWidth) {\n captureCanvas.width = targetWidth;\n }\n if (captureCanvas.height !== targetHeight) {\n captureCanvas.height = targetHeight;\n }\n \n // Ensure CSS dimensions match logical size (required for layoutsubtree)\n captureCanvas.style.width = `${width}px`;\n captureCanvas.style.height = `${height}px`;\n \n // Ensure layoutsubtree is set (required for drawElementImage)\n if (!captureCanvas.hasAttribute(\"layoutsubtree\")) {\n captureCanvas.setAttribute(\"layoutsubtree\", \"\");\n (captureCanvas as HtmlInCanvasElement).layoutSubtree = true;\n }\n \n // Ensure canvas is in DOM (required for drawElementImage layout)\n if (!captureCanvas.parentNode) {\n document.body.appendChild(captureCanvas);\n }\n \n // Ensure container is child of canvas\n if (container.parentElement !== captureCanvas) {\n captureCanvas.appendChild(container);\n }\n \n // Ensure container is visible (not display: none) for layout\n // drawElementImage requires the element to be laid out\n const containerStyle = getComputedStyle(container);\n if (containerStyle.display === 'none') {\n container.style.display = 'block';\n }\n \n // Force synchronous layout by reading layout properties\n // This ensures both canvas and container are laid out (required for drawElementImage)\n // Reading offsetHeight forces a synchronous layout recalculation\n void captureCanvas.offsetHeight;\n void container.offsetHeight;\n getComputedStyle(captureCanvas).opacity;\n getComputedStyle(container).opacity;\n } else {\n captureCanvas = document.createElement(\"canvas\");\n captureCanvas.width = Math.floor(width * dpr);\n captureCanvas.height = Math.floor(height * dpr);\n \n // Enable HTML-in-Canvas mode via layoutsubtree attribute/property\n captureCanvas.setAttribute(\"layoutsubtree\", \"\");\n (captureCanvas as HtmlInCanvasElement).layoutSubtree = true;\n \n captureCanvas.appendChild(container);\n \n captureCanvas.style.cssText = `\n position: fixed;\n left: 0;\n top: 0;\n width: ${width}px;\n height: ${height}px;\n opacity: 0;\n pointer-events: none;\n z-index: -9999;\n `;\n document.body.appendChild(captureCanvas);\n shouldCleanup = true;\n }\n \n const t1 = performance.now();\n defaultProfiler.addTime(\"setup\", t1 - t0);\n \n try {\n // Force style calculation to ensure CSS is computed before capture\n // This ensures both canvas and container are laid out (required for drawElementImage)\n getComputedStyle(container).opacity;\n \n // When reusing canvas with layoutsubtree, wait for initial layout (first use only)\n // Use a WeakSet to track canvases that have been initialized\n if (reuseCanvas && (captureCanvas as any).layoutSubtree && !_layoutInitializedCanvases.has(captureCanvas)) {\n await waitForFrame();\n _layoutInitializedCanvases.add(captureCanvas);\n \n // Canvas may have been detached during async wait (e.g., test cleanup)\n if (!captureCanvas.parentNode) {\n return captureCanvas;\n }\n }\n \n // Only wait for paint in rare edge cases where content was just added to DOM\n if (waitForPaint) {\n await waitForPaintFlush();\n \n if (!captureCanvas.parentNode) {\n return captureCanvas;\n }\n }\n \n const ctx = captureCanvas.getContext(\"2d\") as HtmlInCanvasContext;\n ctx.drawElementImage(container, 0, 0);\n } finally {\n // Only clean up if we created the canvas\n if (shouldCleanup && captureCanvas.parentNode) {\n captureCanvas.parentNode.removeChild(captureCanvas);\n }\n }\n \n const t2 = performance.now();\n defaultProfiler.addTime(\"draw\", t2 - t1);\n \n // If DPR is 1, no downsampling needed - return as-is\n if (dpr === 1) {\n defaultProfiler.incrementRenderCount();\n return captureCanvas;\n }\n \n // Downsample to logical pixel dimensions to match foreignObject path output\n // This ensures consistent behavior regardless of which rendering path is used\n const outputCanvas = document.createElement(\"canvas\");\n outputCanvas.width = width;\n outputCanvas.height = height;\n \n const outputCtx = outputCanvas.getContext(\"2d\")!;\n // Draw the DPR-scaled capture onto the 1x output canvas\n outputCtx.drawImage(\n captureCanvas,\n 0, 0, captureCanvas.width, captureCanvas.height, // source (full DPR capture)\n 0, 0, width, height // destination (logical pixels)\n );\n \n const t3 = performance.now();\n defaultProfiler.addTime(\"downsample\", t3 - t2);\n defaultProfiler.incrementRenderCount();\n \n // Log timing periodically\n defaultProfiler.shouldLogByTime(PROFILING_LOG_INTERVAL_MS);\n \n return outputCanvas;\n}\n\n/**\n * Options for foreignObject rendering path.\n */\nexport interface ForeignObjectRenderOptions extends NativeRenderOptions {\n /**\n * Scale factor for encoding internal canvases.\n * When set, canvases are scaled down before encoding to data URLs,\n * dramatically reducing encoding time for thumbnails.\n * Default: 1 (no scaling - encode at full resolution)\n */\n canvasScale?: number;\n}\n\n/**\n * Render HTML content to an image (or canvas) for drawing.\n * \n * Supports two rendering modes (configurable via previewSettings):\n * - \"native\": Chrome's experimental drawElementImage API (fastest when available)\n * - \"foreignObject\": SVG foreignObject serialization (fallback, works everywhere)\n * \n * @param container - The HTML element to render\n * @param width - Target width in logical pixels\n * @param height - Target height in logical pixels\n * @param options - Rendering options\n * @returns HTMLCanvasElement when using native, HTMLImageElement when using foreignObject\n */\nexport async function renderToImage(\n container: HTMLElement,\n width: number,\n height: number,\n options?: ForeignObjectRenderOptions,\n): Promise<HTMLImageElement | HTMLCanvasElement> {\n const renderMode = getEffectiveRenderMode();\n \n // Native HTML-in-Canvas API path (fastest, requires Chrome flag)\n if (renderMode === \"native\") {\n return renderToImageNative(container, width, height, options);\n }\n \n // Fallback: SVG foreignObject serialization\n // Clone the container first (don't modify original)\n // Note: cloneNode doesn't copy canvas pixels, so we encode from original canvases\n const originalCanvases = Array.from(container.querySelectorAll(\"canvas\"));\n const clone = container.cloneNode(true) as HTMLElement;\n const clonedCanvases = clone.querySelectorAll(\"canvas\");\n \n // Encode original canvases and map to cloned elements\n const canvasScale = options?.canvasScale ?? 1;\n const canvasStart = performance.now();\n const encodedResults = await encodeCanvasesInParallel(originalCanvases, canvasScale);\n \n for (let i = 0; i < originalCanvases.length; i++) {\n const srcCanvas = originalCanvases[i];\n const dstCanvas = clonedCanvases[i];\n const encoded = encodedResults.find((r) => r.canvas === srcCanvas);\n \n if (!srcCanvas || !dstCanvas || !encoded) continue;\n \n try {\n const img = document.createElement(\"img\");\n img.src = encoded.dataUrl;\n img.width = srcCanvas.width;\n img.height = srcCanvas.height;\n const style = dstCanvas.getAttribute(\"style\");\n if (style) img.setAttribute(\"style\", style);\n dstCanvas.parentNode?.replaceChild(img, dstCanvas);\n } catch {\n // Cross-origin or other error - skip\n }\n }\n defaultProfiler.addTime(\"canvasEncode\", performance.now() - canvasStart);\n\n // Inline external images in the clone\n const inlineStart = performance.now();\n await inlineImages(clone);\n defaultProfiler.addTime(\"inline\", performance.now() - inlineStart);\n\n // Use common serialization pipeline (no restore needed since we're working on a clone)\n const { dataUri } = await serializeToSvgDataUri(clone, width, height);\n \n // Load as image\n return loadImageFromDataUri(dataUri);\n}\n\n\n/**\n * Render a pre-built clone container to an image WITHOUT cloning it again.\n * This is the fast path for reusing clone structures across frames.\n * \n * Key difference from renderToImage:\n * - Does NOT call cloneNode (avoids expensive DOM duplication)\n * - Converts canvases to images in-place, then restores them after serialization\n * - Assumes the container already has refreshed canvas content\n * \n * @param container - Pre-built clone container with refreshed canvas content\n * @param width - Output width\n * @param height - Output height\n * @returns Promise resolving to an HTMLImageElement\n */\nexport async function renderToImageDirect(\n container: HTMLElement,\n width: number,\n height: number,\n): Promise<HTMLImageElement> {\n defaultProfiler.incrementRenderCount();\n \n // Use common serialization pipeline (modifies in-place, restores after)\n const { dataUri, restore } = await serializeToSvgDataUri(container, width, height, {\n inlineImages: true,\n logEarlyRenders: true,\n });\n restore();\n \n // Load as image\n const image = await loadImageFromDataUri(dataUri);\n \n // Log timing breakdown periodically\n defaultProfiler.shouldLogByFrameCount(100);\n \n return image;\n}\n\n/**\n * Prepare a frame's data URI without waiting for image load.\n * Returns the data URI asynchronously (after parallel canvas encoding and serialization) for pipelined loading.\n * The DOM is restored before this function returns.\n */\nexport async function prepareFrameDataUri(\n container: HTMLElement,\n width: number,\n height: number,\n): Promise<string> {\n defaultProfiler.incrementRenderCount();\n \n // Use common serialization pipeline (modifies in-place, restores after)\n const { dataUri, restore } = await serializeToSvgDataUri(container, width, height);\n restore();\n \n return dataUri;\n}\n\n/**\n * Load an image from a data URI. Returns a Promise that resolves when loaded.\n */\nexport function loadImageFromDataUri(dataUri: string): Promise<HTMLImageElement> {\n const img = new Image();\n const imageLoadStart = performance.now();\n \n return new Promise<HTMLImageElement>((resolve, reject) => {\n img.onload = () => {\n defaultProfiler.addTime(\"imageLoad\", performance.now() - imageLoadStart);\n resolve(img);\n };\n img.onerror = reject;\n img.src = dataUri;\n });\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 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 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: Skip DPR scaling for thumbnails - retina quality isn't needed\n // and DPR=2 means 4x more pixels to render.\n const skipDpr = scale < 1;\n image = await renderToImageNative(renderContainer, width, height, { skipDprScaling: skipDpr });\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 so canvases are encoded at thumbnail size (MUCH faster)\n const t2 = performance.now();\n image = await renderToImage(previewContainer, width, height, { canvasScale: scale });\n const renderTime = performance.now() - t2;\n \n console.log(`[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}\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 syncState: SyncState;\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\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 * Optimized with:\n * - Persistent clone structure (built once)\n * - Temporal bucketing for time-based culling\n * - Property split (static vs animated)\n * - Parent index for O(1) visibility checks\n * - Resolution scaling for performance (renders at lower resolution, CSS upscales)\n *\n * @param timegroup - The source timegroup to preview\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 // Build clone structure ONCE with optimized sync state\n // Initial sync happens during clone building in a single pass\n const initialTimeMs = toAbsoluteTime(timegroup, timegroup.currentTimeMs ?? 0);\n const { container, syncState } = buildCloneStructure(timegroup, initialTimeMs);\n\n // Create a wrapper div with scaled dimensions\n // When resolutionScale < 1, we render at a smaller size and CSS transform scales the content\n const previewContainer = createPreviewContainer({\n width: renderWidth,\n height: renderHeight,\n background: getComputedStyle(timegroup).background || \"#000\",\n });\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 // Inject document styles so CSS rules work in SVG foreignObject\n const styleEl = document.createElement(\"style\");\n styleEl.textContent = collectDocumentStyles();\n previewContainer.appendChild(styleEl);\n \n previewContainer.appendChild(container);\n overrideRootCloneStyles(syncState);\n\n // Track render state\n let rendering = false;\n let lastTimeMs = -1;\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) 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 console.log(`[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 syncStyles(syncState, toAbsoluteTime(timegroup, userTimeMs));\n overrideRootCloneStyles(syncState);\n\n // Render at scaled dimensions with canvas scaling for internal video frames\n const t0 = performance.now();\n const image = await renderToImage(previewContainer, renderWidth, renderHeight, {\n canvasScale: currentResolutionScale,\n });\n const renderTime = performance.now() - t0;\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 console.log(`[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 console.error(\"Canvas preview render failed:\", e);\n } finally {\n rendering = false;\n }\n };\n\n // Do initial render\n refresh();\n\n return { container: wrapperContainer, canvas, refresh, syncState, setResolutionScale, getResolutionScale };\n}\n\n"],"mappings":"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"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { ContentReadyMode } from "./renderTimegroupToCanvas.js";
|
|
2
|
+
import { AudioCodec } from "mediabunny";
|
|
3
|
+
|
|
4
|
+
//#region src/preview/renderTimegroupToVideo.d.ts
|
|
5
|
+
|
|
6
|
+
interface RenderProgress {
|
|
7
|
+
progress: number;
|
|
8
|
+
currentFrame: number;
|
|
9
|
+
totalFrames: number;
|
|
10
|
+
renderedMs: number;
|
|
11
|
+
totalDurationMs: number;
|
|
12
|
+
elapsedMs: number;
|
|
13
|
+
estimatedRemainingMs: number;
|
|
14
|
+
speedMultiplier: number;
|
|
15
|
+
framePreviewUrl?: string;
|
|
16
|
+
}
|
|
17
|
+
interface RenderToVideoOptions {
|
|
18
|
+
fps?: number;
|
|
19
|
+
codec?: "avc" | "hevc" | "vp9" | "av1" | "vp8";
|
|
20
|
+
bitrate?: number;
|
|
21
|
+
filename?: string;
|
|
22
|
+
scale?: number;
|
|
23
|
+
keyFrameInterval?: number;
|
|
24
|
+
fromMs?: number;
|
|
25
|
+
toMs?: number;
|
|
26
|
+
onProgress?: (progress: RenderProgress) => void;
|
|
27
|
+
streaming?: boolean;
|
|
28
|
+
signal?: AbortSignal;
|
|
29
|
+
includeAudio?: boolean;
|
|
30
|
+
audioBitrate?: number;
|
|
31
|
+
contentReadyMode?: ContentReadyMode;
|
|
32
|
+
blockingTimeoutMs?: number;
|
|
33
|
+
returnBuffer?: boolean;
|
|
34
|
+
preferredAudioCodecs?: AudioCodec[];
|
|
35
|
+
benchmarkMode?: boolean;
|
|
36
|
+
}
|
|
37
|
+
//#endregion
|
|
38
|
+
export { RenderToVideoOptions };
|
|
39
|
+
//# sourceMappingURL=renderTimegroupToVideo.d.ts.map
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { captureFromClone, resetRenderState } from "./renderTimegroupToCanvas.js";
|
|
2
|
+
import { AudioBufferSource, BufferTarget, CanvasSource, Mp4OutputFormat, Output, StreamTarget, canEncodeAudio, getEncodableAudioCodecs } from "mediabunny";
|
|
3
|
+
|
|
4
|
+
//#region src/preview/renderTimegroupToVideo.ts
|
|
5
|
+
var NoSupportedAudioCodecError = class extends Error {
|
|
6
|
+
constructor(requestedCodecs, availableCodecs) {
|
|
7
|
+
super(`No supported audio codec found. Requested: [${requestedCodecs.join(", ")}], Available: [${availableCodecs.length > 0 ? availableCodecs.join(", ") : "none"}]`);
|
|
8
|
+
this.name = "NoSupportedAudioCodecError";
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
var RenderCancelledError = class extends Error {
|
|
12
|
+
constructor() {
|
|
13
|
+
super("Render cancelled");
|
|
14
|
+
this.name = "RenderCancelledError";
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
function resolveConfig(timegroup, options) {
|
|
18
|
+
const fps = options.fps ?? timegroup.effectiveFps ?? 30;
|
|
19
|
+
const codec = options.codec ?? "avc";
|
|
20
|
+
const bitrate = options.bitrate ?? 8e6;
|
|
21
|
+
const filename = options.filename ?? "timegroup-video.mp4";
|
|
22
|
+
const scale = options.scale ?? 1;
|
|
23
|
+
const keyFrameInterval = options.keyFrameInterval ?? 2;
|
|
24
|
+
const streaming = options.streaming ?? true;
|
|
25
|
+
const includeAudio = options.includeAudio ?? true;
|
|
26
|
+
const audioBitrate = options.audioBitrate ?? 128e3;
|
|
27
|
+
const contentReadyMode = options.contentReadyMode ?? "blocking";
|
|
28
|
+
const blockingTimeoutMs = options.blockingTimeoutMs ?? 5e3;
|
|
29
|
+
const returnBuffer = options.returnBuffer ?? false;
|
|
30
|
+
const preferredAudioCodecs = options.preferredAudioCodecs ?? ["aac", "opus"];
|
|
31
|
+
const benchmarkMode = options.benchmarkMode ?? false;
|
|
32
|
+
const totalDurationMs = timegroup.durationMs;
|
|
33
|
+
if (!totalDurationMs || totalDurationMs <= 0) throw new Error("Timegroup has no duration");
|
|
34
|
+
const startMs = Math.max(0, options.fromMs ?? 0);
|
|
35
|
+
const endMs = options.toMs !== void 0 ? Math.min(options.toMs, totalDurationMs) : totalDurationMs;
|
|
36
|
+
const renderDurationMs = endMs - startMs;
|
|
37
|
+
if (renderDurationMs <= 0) throw new Error(`Invalid render range: from ${startMs}ms to ${endMs}ms`);
|
|
38
|
+
const timegroupWidth = timegroup.offsetWidth || 1920;
|
|
39
|
+
const timegroupHeight = timegroup.offsetHeight || 1080;
|
|
40
|
+
const width = Math.floor(timegroupWidth * scale);
|
|
41
|
+
const height = Math.floor(timegroupHeight * scale);
|
|
42
|
+
const videoWidth = width % 2 === 0 ? width : width - 1;
|
|
43
|
+
const videoHeight = height % 2 === 0 ? height : height - 1;
|
|
44
|
+
const frameDurationMs = 1e3 / fps;
|
|
45
|
+
return {
|
|
46
|
+
fps,
|
|
47
|
+
codec,
|
|
48
|
+
bitrate,
|
|
49
|
+
filename,
|
|
50
|
+
scale,
|
|
51
|
+
keyFrameInterval,
|
|
52
|
+
startMs,
|
|
53
|
+
endMs,
|
|
54
|
+
renderDurationMs,
|
|
55
|
+
videoWidth,
|
|
56
|
+
videoHeight,
|
|
57
|
+
totalFrames: Math.ceil(renderDurationMs / frameDurationMs),
|
|
58
|
+
frameDurationMs,
|
|
59
|
+
frameDurationS: frameDurationMs / 1e3,
|
|
60
|
+
streaming,
|
|
61
|
+
includeAudio,
|
|
62
|
+
audioBitrate,
|
|
63
|
+
contentReadyMode,
|
|
64
|
+
blockingTimeoutMs,
|
|
65
|
+
returnBuffer,
|
|
66
|
+
preferredAudioCodecs,
|
|
67
|
+
benchmarkMode
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function isFileSystemAccessSupported() {
|
|
71
|
+
return typeof window !== "undefined" && "showSaveFilePicker" in window;
|
|
72
|
+
}
|
|
73
|
+
async function getFileWritableStream(filename) {
|
|
74
|
+
if (!isFileSystemAccessSupported()) return null;
|
|
75
|
+
try {
|
|
76
|
+
const writable = await (await window.showSaveFilePicker({
|
|
77
|
+
suggestedName: filename,
|
|
78
|
+
types: [{
|
|
79
|
+
description: "MP4 Video",
|
|
80
|
+
accept: { "video/mp4": [".mp4"] }
|
|
81
|
+
}]
|
|
82
|
+
})).createWritable();
|
|
83
|
+
return {
|
|
84
|
+
writable,
|
|
85
|
+
close: async () => {
|
|
86
|
+
await writable.close();
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
} catch (e) {
|
|
90
|
+
if (e.name !== "AbortError") console.warn("[renderToVideo] File System Access failed:", e);
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
async function selectAudioCodec(preferredCodecs, encodingOptions) {
|
|
95
|
+
for (const codec of preferredCodecs) try {
|
|
96
|
+
if (await canEncodeAudio(codec, encodingOptions)) return codec;
|
|
97
|
+
} catch (e) {
|
|
98
|
+
console.warn(`[selectAudioCodec] Check failed for ${codec}:`, e);
|
|
99
|
+
}
|
|
100
|
+
throw new NoSupportedAudioCodecError(preferredCodecs, await getEncodableAudioCodecs(void 0, encodingOptions));
|
|
101
|
+
}
|
|
102
|
+
function downloadBlob(blob, filename) {
|
|
103
|
+
const url = URL.createObjectURL(blob);
|
|
104
|
+
const a = document.createElement("a");
|
|
105
|
+
a.href = url;
|
|
106
|
+
a.download = filename;
|
|
107
|
+
document.body.appendChild(a);
|
|
108
|
+
a.click();
|
|
109
|
+
document.body.removeChild(a);
|
|
110
|
+
URL.revokeObjectURL(url);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Renders a timegroup to an MP4 video file.
|
|
114
|
+
*
|
|
115
|
+
* Uses the EXACT same code path as thumbnail generation (captureFromClone).
|
|
116
|
+
* This ensures consistency - if thumbnails work, video export works.
|
|
117
|
+
*/
|
|
118
|
+
async function renderTimegroupToVideo(timegroup, options = {}) {
|
|
119
|
+
const config = resolveConfig(timegroup, options);
|
|
120
|
+
const { signal, onProgress } = options;
|
|
121
|
+
const checkCancelled = () => {
|
|
122
|
+
if (signal?.aborted) throw new RenderCancelledError();
|
|
123
|
+
};
|
|
124
|
+
resetRenderState();
|
|
125
|
+
const { clone: renderClone, container: renderContainer, cleanup: cleanupRenderClone } = await timegroup.createRenderClone();
|
|
126
|
+
const timestamps = [];
|
|
127
|
+
for (let i = 0; i < config.totalFrames; i++) timestamps.push(config.startMs + i * config.frameDurationMs);
|
|
128
|
+
const videoElements = renderClone.querySelectorAll("ef-video");
|
|
129
|
+
if (videoElements.length > 0) {
|
|
130
|
+
console.log(`[renderTimegroupToVideo] Prefetching main video segments for ${videoElements.length} video(s)...`);
|
|
131
|
+
await Promise.all(Array.from(videoElements).map((video) => video.prefetchMainVideoSegments(timestamps)));
|
|
132
|
+
console.log(`[renderTimegroupToVideo] Prefetch complete`);
|
|
133
|
+
}
|
|
134
|
+
let output = null;
|
|
135
|
+
let videoSource = null;
|
|
136
|
+
let audioSource = null;
|
|
137
|
+
let target = null;
|
|
138
|
+
let fileStream = null;
|
|
139
|
+
let useStreaming = false;
|
|
140
|
+
let encodingCanvas = null;
|
|
141
|
+
let encodingCtx = null;
|
|
142
|
+
if (!config.benchmarkMode) {
|
|
143
|
+
if (config.streaming) {
|
|
144
|
+
fileStream = await getFileWritableStream(config.filename);
|
|
145
|
+
useStreaming = fileStream !== null;
|
|
146
|
+
}
|
|
147
|
+
if (useStreaming && fileStream) {
|
|
148
|
+
target = new StreamTarget(fileStream.writable);
|
|
149
|
+
output = new Output({
|
|
150
|
+
format: new Mp4OutputFormat({ fastStart: "fragmented" }),
|
|
151
|
+
target
|
|
152
|
+
});
|
|
153
|
+
} else {
|
|
154
|
+
target = new BufferTarget();
|
|
155
|
+
output = new Output({
|
|
156
|
+
format: new Mp4OutputFormat(),
|
|
157
|
+
target
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
encodingCanvas = new OffscreenCanvas(config.videoWidth, config.videoHeight);
|
|
161
|
+
encodingCtx = encodingCanvas.getContext("2d");
|
|
162
|
+
if (!encodingCtx) {
|
|
163
|
+
cleanupRenderClone();
|
|
164
|
+
throw new Error("Failed to get encoding canvas context");
|
|
165
|
+
}
|
|
166
|
+
const videoConfig = {
|
|
167
|
+
codec: config.codec,
|
|
168
|
+
bitrate: config.bitrate,
|
|
169
|
+
keyFrameInterval: config.keyFrameInterval
|
|
170
|
+
};
|
|
171
|
+
videoSource = new CanvasSource(encodingCanvas, videoConfig);
|
|
172
|
+
output.addVideoTrack(videoSource);
|
|
173
|
+
if (config.includeAudio) {
|
|
174
|
+
audioSource = new AudioBufferSource({
|
|
175
|
+
codec: await selectAudioCodec(config.preferredAudioCodecs, {
|
|
176
|
+
numberOfChannels: 2,
|
|
177
|
+
sampleRate: 48e3,
|
|
178
|
+
bitrate: config.audioBitrate
|
|
179
|
+
}),
|
|
180
|
+
bitrate: config.audioBitrate
|
|
181
|
+
});
|
|
182
|
+
output.addAudioTrack(audioSource);
|
|
183
|
+
}
|
|
184
|
+
await output.start();
|
|
185
|
+
}
|
|
186
|
+
const renderStartTime = performance.now();
|
|
187
|
+
let lastFramePreviewUrl;
|
|
188
|
+
let lastRenderedAudioEndMs = config.startMs;
|
|
189
|
+
const audioChunkDurationMs = 2e3;
|
|
190
|
+
let totalSeekMs = 0;
|
|
191
|
+
let totalCaptureMs = 0;
|
|
192
|
+
let totalEncodeMs = 0;
|
|
193
|
+
try {
|
|
194
|
+
for (let frameIndex = 0; frameIndex < config.totalFrames; frameIndex++) {
|
|
195
|
+
checkCancelled();
|
|
196
|
+
const timeMs = timestamps[frameIndex];
|
|
197
|
+
const timestampS = frameIndex * config.frameDurationMs / 1e3;
|
|
198
|
+
if (audioSource && timeMs >= lastRenderedAudioEndMs + audioChunkDurationMs) {
|
|
199
|
+
const chunkEndMs = Math.min(timeMs + audioChunkDurationMs, config.endMs);
|
|
200
|
+
try {
|
|
201
|
+
const audioBuffer = await timegroup.renderAudio(lastRenderedAudioEndMs, chunkEndMs);
|
|
202
|
+
if (audioBuffer && audioBuffer.length > 0) await audioSource.add(audioBuffer);
|
|
203
|
+
} catch (e) {}
|
|
204
|
+
lastRenderedAudioEndMs = chunkEndMs;
|
|
205
|
+
}
|
|
206
|
+
const seekStart = performance.now();
|
|
207
|
+
await renderClone.seekForRender(timeMs);
|
|
208
|
+
totalSeekMs += performance.now() - seekStart;
|
|
209
|
+
const captureStart = performance.now();
|
|
210
|
+
const canvas = await captureFromClone(renderClone, renderContainer, {
|
|
211
|
+
scale: config.scale,
|
|
212
|
+
contentReadyMode: config.contentReadyMode,
|
|
213
|
+
blockingTimeoutMs: config.blockingTimeoutMs,
|
|
214
|
+
originalTimegroup: timegroup
|
|
215
|
+
});
|
|
216
|
+
totalCaptureMs += performance.now() - captureStart;
|
|
217
|
+
if (videoSource && output && encodingCtx) {
|
|
218
|
+
const encodeStart = performance.now();
|
|
219
|
+
encodingCtx.drawImage(canvas, 0, 0, canvas.width, canvas.height, 0, 0, config.videoWidth, config.videoHeight);
|
|
220
|
+
await videoSource.add(timestampS, config.frameDurationS);
|
|
221
|
+
totalEncodeMs += performance.now() - encodeStart;
|
|
222
|
+
}
|
|
223
|
+
const currentFrame = frameIndex + 1;
|
|
224
|
+
const progress = currentFrame / config.totalFrames;
|
|
225
|
+
const renderedMs = currentFrame * config.frameDurationMs;
|
|
226
|
+
const elapsedMs = performance.now() - renderStartTime;
|
|
227
|
+
const msPerFrame = elapsedMs / currentFrame;
|
|
228
|
+
const estimatedRemainingMs = (config.totalFrames - currentFrame) * msPerFrame;
|
|
229
|
+
const speedMultiplier = renderedMs / elapsedMs;
|
|
230
|
+
if (onProgress && frameIndex % 10 === 0) {
|
|
231
|
+
const previewWidth = 160;
|
|
232
|
+
const previewHeight = Math.round(previewWidth * (config.videoHeight / config.videoWidth));
|
|
233
|
+
const thumbCanvas = document.createElement("canvas");
|
|
234
|
+
thumbCanvas.width = previewWidth;
|
|
235
|
+
thumbCanvas.height = previewHeight;
|
|
236
|
+
thumbCanvas.getContext("2d").drawImage(canvas, 0, 0, previewWidth, previewHeight);
|
|
237
|
+
lastFramePreviewUrl = thumbCanvas.toDataURL("image/jpeg", .7);
|
|
238
|
+
}
|
|
239
|
+
onProgress?.({
|
|
240
|
+
progress,
|
|
241
|
+
currentFrame,
|
|
242
|
+
totalFrames: config.totalFrames,
|
|
243
|
+
renderedMs,
|
|
244
|
+
totalDurationMs: config.renderDurationMs,
|
|
245
|
+
elapsedMs,
|
|
246
|
+
estimatedRemainingMs,
|
|
247
|
+
speedMultiplier,
|
|
248
|
+
framePreviewUrl: lastFramePreviewUrl
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
if (audioSource && lastRenderedAudioEndMs < config.endMs) try {
|
|
252
|
+
const audioBuffer = await timegroup.renderAudio(lastRenderedAudioEndMs, config.endMs);
|
|
253
|
+
if (audioBuffer && audioBuffer.length > 0) await audioSource.add(audioBuffer);
|
|
254
|
+
} catch (e) {}
|
|
255
|
+
const totalTime = performance.now() - renderStartTime;
|
|
256
|
+
console.log(`[renderTimegroupToVideo] ${config.totalFrames} frames: seek=${totalSeekMs.toFixed(0)}ms, capture=${totalCaptureMs.toFixed(0)}ms, encode=${totalEncodeMs.toFixed(0)}ms, total=${totalTime.toFixed(0)}ms`);
|
|
257
|
+
if (config.benchmarkMode) return;
|
|
258
|
+
await output.finalize();
|
|
259
|
+
if (useStreaming) return;
|
|
260
|
+
else {
|
|
261
|
+
const videoBuffer = target.buffer;
|
|
262
|
+
if (!videoBuffer) throw new Error("Video encoding failed: no buffer produced");
|
|
263
|
+
if (config.returnBuffer) return new Uint8Array(videoBuffer);
|
|
264
|
+
downloadBlob(new Blob([videoBuffer], { type: "video/mp4" }), config.filename);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
} finally {
|
|
268
|
+
cleanupRenderClone();
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
//#endregion
|
|
273
|
+
export { RenderCancelledError, renderTimegroupToVideo };
|
|
274
|
+
//# sourceMappingURL=renderTimegroupToVideo.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"renderTimegroupToVideo.js","names":["timestamps: number[]","output: Output | null","videoSource: CanvasSource | null","audioSource: AudioBufferSource | null","target: BufferTarget | StreamTarget | null","fileStream: { writable: WritableStream<Uint8Array>; close: () => Promise<void> } | null","encodingCanvas: OffscreenCanvas | null","encodingCtx: OffscreenCanvasRenderingContext2D | null","videoConfig: VideoEncodingConfig","lastFramePreviewUrl: string | undefined"],"sources":["../../src/preview/renderTimegroupToVideo.ts"],"sourcesContent":["/**\n * Video rendering for timegroups.\n * \n * Uses the EXACT same rendering path as thumbnail generation (captureFromClone),\n * ensuring consistency between preview thumbnails and exported video.\n */\n\nimport {\n Output,\n Mp4OutputFormat,\n BufferTarget,\n StreamTarget,\n CanvasSource,\n AudioBufferSource,\n QUALITY_HIGH,\n canEncodeAudio,\n getEncodableAudioCodecs,\n type VideoEncodingConfig,\n type AudioEncodingConfig,\n type AudioCodec,\n} from \"mediabunny\";\nimport type { EFTimegroup } from \"../elements/EFTimegroup.js\";\nimport type { EFVideo } from \"../elements/EFVideo.js\";\nimport {\n resetRenderState,\n captureFromClone,\n type ContentReadyMode,\n} from \"./renderTimegroupToCanvas.js\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface RenderProgress {\n progress: number;\n currentFrame: number;\n totalFrames: number;\n renderedMs: number;\n totalDurationMs: number;\n elapsedMs: number;\n estimatedRemainingMs: number;\n speedMultiplier: number;\n framePreviewUrl?: string;\n}\n\nexport interface RenderToVideoOptions {\n fps?: number;\n codec?: \"avc\" | \"hevc\" | \"vp9\" | \"av1\" | \"vp8\";\n bitrate?: number;\n filename?: string;\n scale?: number;\n keyFrameInterval?: number;\n fromMs?: number;\n toMs?: number;\n onProgress?: (progress: RenderProgress) => void;\n streaming?: boolean;\n signal?: AbortSignal;\n includeAudio?: boolean;\n audioBitrate?: number;\n contentReadyMode?: ContentReadyMode;\n blockingTimeoutMs?: number;\n returnBuffer?: boolean;\n preferredAudioCodecs?: AudioCodec[];\n benchmarkMode?: boolean;\n}\n\n// ============================================================================\n// Errors\n// ============================================================================\n\nexport class NoSupportedAudioCodecError extends Error {\n constructor(requestedCodecs: AudioCodec[], availableCodecs: AudioCodec[]) {\n super(\n `No supported audio codec found. Requested: [${requestedCodecs.join(\", \")}], ` +\n `Available: [${availableCodecs.length > 0 ? availableCodecs.join(\", \") : \"none\"}]`\n );\n this.name = \"NoSupportedAudioCodecError\";\n }\n}\n\nexport class RenderCancelledError extends Error {\n constructor() {\n super(\"Render cancelled\");\n this.name = \"RenderCancelledError\";\n }\n}\n\n// ============================================================================\n// Configuration\n// ============================================================================\n\ninterface ResolvedConfig {\n fps: number;\n codec: \"avc\" | \"hevc\" | \"vp9\" | \"av1\" | \"vp8\";\n bitrate: number;\n filename: string;\n scale: number;\n keyFrameInterval: number;\n startMs: number;\n endMs: number;\n renderDurationMs: number;\n videoWidth: number;\n videoHeight: number;\n totalFrames: number;\n frameDurationMs: number;\n frameDurationS: number;\n streaming: boolean;\n includeAudio: boolean;\n audioBitrate: number;\n contentReadyMode: ContentReadyMode;\n blockingTimeoutMs: number;\n returnBuffer: boolean;\n preferredAudioCodecs: AudioCodec[];\n benchmarkMode: boolean;\n}\n\nfunction resolveConfig(\n timegroup: EFTimegroup,\n options: RenderToVideoOptions,\n): ResolvedConfig {\n const fps = options.fps ?? timegroup.effectiveFps ?? 30;\n const codec = options.codec ?? \"avc\";\n const bitrate = options.bitrate ?? 8_000_000;\n const filename = options.filename ?? \"timegroup-video.mp4\";\n const scale = options.scale ?? 1;\n const keyFrameInterval = options.keyFrameInterval ?? 2;\n const streaming = options.streaming ?? true;\n const includeAudio = options.includeAudio ?? true;\n const audioBitrate = options.audioBitrate ?? 128_000;\n const contentReadyMode = options.contentReadyMode ?? \"blocking\";\n const blockingTimeoutMs = options.blockingTimeoutMs ?? 5000;\n const returnBuffer = options.returnBuffer ?? false;\n const preferredAudioCodecs = options.preferredAudioCodecs ?? [\"aac\", \"opus\"];\n const benchmarkMode = options.benchmarkMode ?? false;\n\n const totalDurationMs = timegroup.durationMs;\n if (!totalDurationMs || totalDurationMs <= 0) {\n throw new Error(\"Timegroup has no duration\");\n }\n\n const startMs = Math.max(0, options.fromMs ?? 0);\n const endMs = options.toMs !== undefined ? Math.min(options.toMs, totalDurationMs) : totalDurationMs;\n const renderDurationMs = endMs - startMs;\n \n if (renderDurationMs <= 0) {\n throw new Error(`Invalid render range: from ${startMs}ms to ${endMs}ms`);\n }\n\n const timegroupWidth = timegroup.offsetWidth || 1920;\n const timegroupHeight = timegroup.offsetHeight || 1080;\n const width = Math.floor(timegroupWidth * scale);\n const height = Math.floor(timegroupHeight * scale);\n\n const videoWidth = width % 2 === 0 ? width : width - 1;\n const videoHeight = height % 2 === 0 ? height : height - 1;\n\n const frameDurationMs = 1000 / fps;\n const totalFrames = Math.ceil(renderDurationMs / frameDurationMs);\n const frameDurationS = frameDurationMs / 1000;\n\n return {\n fps,\n codec,\n bitrate,\n filename,\n scale,\n keyFrameInterval,\n startMs,\n endMs,\n renderDurationMs,\n videoWidth,\n videoHeight,\n totalFrames,\n frameDurationMs,\n frameDurationS,\n streaming,\n includeAudio,\n audioBitrate,\n contentReadyMode,\n blockingTimeoutMs,\n returnBuffer,\n preferredAudioCodecs,\n benchmarkMode,\n };\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nfunction isFileSystemAccessSupported(): boolean {\n return typeof window !== \"undefined\" && \"showSaveFilePicker\" in window;\n}\n\nasync function getFileWritableStream(\n filename: string,\n): Promise<{ writable: WritableStream<Uint8Array>; close: () => Promise<void> } | null> {\n if (!isFileSystemAccessSupported()) {\n return null;\n }\n\n try {\n const fileHandle = await (window as any).showSaveFilePicker({\n suggestedName: filename,\n types: [{ description: \"MP4 Video\", accept: { \"video/mp4\": [\".mp4\"] } }],\n });\n const writable = await fileHandle.createWritable();\n return { writable, close: async () => { await writable.close(); } };\n } catch (e) {\n if ((e as Error).name !== \"AbortError\") {\n console.warn(\"[renderToVideo] File System Access failed:\", e);\n }\n return null;\n }\n}\n\nasync function selectAudioCodec(\n preferredCodecs: AudioCodec[],\n encodingOptions: { numberOfChannels: number; sampleRate: number; bitrate: number },\n): Promise<AudioCodec> {\n for (const codec of preferredCodecs) {\n try {\n const isSupported = await canEncodeAudio(codec, encodingOptions);\n if (isSupported) return codec;\n } catch (e) {\n console.warn(`[selectAudioCodec] Check failed for ${codec}:`, e);\n }\n }\n const availableCodecs = await getEncodableAudioCodecs(undefined, encodingOptions);\n throw new NoSupportedAudioCodecError(preferredCodecs, availableCodecs);\n}\n\nfunction downloadBlob(blob: Blob, filename: string): void {\n const url = URL.createObjectURL(blob);\n const a = document.createElement(\"a\");\n a.href = url;\n a.download = filename;\n document.body.appendChild(a);\n a.click();\n document.body.removeChild(a);\n URL.revokeObjectURL(url);\n}\n\n// ============================================================================\n// Public API\n// ============================================================================\n\nexport async function getSupportedAudioCodecs(options?: {\n numberOfChannels?: number;\n sampleRate?: number;\n bitrate?: number;\n}): Promise<AudioCodec[]> {\n const { numberOfChannels = 2, sampleRate = 48000, bitrate = 128000 } = options ?? {};\n return getEncodableAudioCodecs(undefined, { numberOfChannels, sampleRate, bitrate });\n}\n\n/**\n * Renders a timegroup to an MP4 video file.\n * \n * Uses the EXACT same code path as thumbnail generation (captureFromClone).\n * This ensures consistency - if thumbnails work, video export works.\n */\nexport async function renderTimegroupToVideo(\n timegroup: EFTimegroup,\n options: RenderToVideoOptions = {},\n): Promise<Uint8Array | undefined> {\n const config = resolveConfig(timegroup, options);\n const { signal, onProgress } = options;\n \n const checkCancelled = () => {\n if (signal?.aborted) throw new RenderCancelledError();\n };\n \n resetRenderState();\n \n // =========================================================================\n // Create render clone - EXACT same as captureBatch in EFTimegroup\n // =========================================================================\n const { clone: renderClone, container: renderContainer, cleanup: cleanupRenderClone } =\n await timegroup.createRenderClone();\n \n // Pre-fetch main video segments for all timestamps\n // This ensures all segments are cached before rendering starts,\n // avoiding network delays during the frame loop\n const timestamps: number[] = [];\n for (let i = 0; i < config.totalFrames; i++) {\n timestamps.push(config.startMs + i * config.frameDurationMs);\n }\n \n const videoElements = renderClone.querySelectorAll(\"ef-video\");\n if (videoElements.length > 0) {\n console.log(`[renderTimegroupToVideo] Prefetching main video segments for ${videoElements.length} video(s)...`);\n await Promise.all(\n Array.from(videoElements).map((video) =>\n (video as EFVideo).prefetchMainVideoSegments(timestamps),\n ),\n );\n console.log(`[renderTimegroupToVideo] Prefetch complete`);\n }\n \n // =========================================================================\n // Set up video encoding\n // =========================================================================\n let output: Output | null = null;\n let videoSource: CanvasSource | null = null;\n let audioSource: AudioBufferSource | null = null;\n let target: BufferTarget | StreamTarget | null = null;\n let fileStream: { writable: WritableStream<Uint8Array>; close: () => Promise<void> } | null = null;\n let useStreaming = false;\n let encodingCanvas: OffscreenCanvas | null = null;\n let encodingCtx: OffscreenCanvasRenderingContext2D | null = null;\n \n if (!config.benchmarkMode) {\n if (config.streaming) {\n fileStream = await getFileWritableStream(config.filename);\n useStreaming = fileStream !== null;\n }\n \n if (useStreaming && fileStream) {\n target = new StreamTarget(fileStream.writable as any);\n output = new Output({\n format: new Mp4OutputFormat({ fastStart: \"fragmented\" }),\n target,\n });\n } else {\n target = new BufferTarget();\n output = new Output({ format: new Mp4OutputFormat(), target });\n }\n \n encodingCanvas = new OffscreenCanvas(config.videoWidth, config.videoHeight);\n encodingCtx = encodingCanvas.getContext(\"2d\");\n if (!encodingCtx) {\n cleanupRenderClone();\n throw new Error(\"Failed to get encoding canvas context\");\n }\n \n const videoConfig: VideoEncodingConfig = {\n codec: config.codec,\n bitrate: config.bitrate,\n keyFrameInterval: config.keyFrameInterval,\n };\n videoSource = new CanvasSource(encodingCanvas, videoConfig);\n output.addVideoTrack(videoSource);\n \n if (config.includeAudio) {\n const selectedCodec = await selectAudioCodec(config.preferredAudioCodecs, {\n numberOfChannels: 2,\n sampleRate: 48000,\n bitrate: config.audioBitrate,\n });\n const audioConfig: AudioEncodingConfig = {\n codec: selectedCodec,\n bitrate: config.audioBitrate,\n };\n audioSource = new AudioBufferSource(audioConfig);\n output.addAudioTrack(audioSource);\n }\n \n await output.start();\n }\n \n // =========================================================================\n // Frame loop - using EXACT same code path as captureBatch\n // =========================================================================\n const renderStartTime = performance.now();\n let lastFramePreviewUrl: string | undefined;\n let lastRenderedAudioEndMs = config.startMs;\n const audioChunkDurationMs = 2000;\n \n let totalSeekMs = 0;\n let totalCaptureMs = 0;\n let totalEncodeMs = 0;\n \n try {\n for (let frameIndex = 0; frameIndex < config.totalFrames; frameIndex++) {\n checkCancelled();\n \n const timeMs = timestamps[frameIndex]!;\n const timestampS = (frameIndex * config.frameDurationMs) / 1000;\n \n // Render audio chunk if needed\n if (audioSource && timeMs >= lastRenderedAudioEndMs + audioChunkDurationMs) {\n const chunkEndMs = Math.min(timeMs + audioChunkDurationMs, config.endMs);\n try {\n const audioBuffer = await timegroup.renderAudio(lastRenderedAudioEndMs, chunkEndMs);\n if (audioBuffer && audioBuffer.length > 0) {\n await audioSource.add(audioBuffer);\n }\n } catch (e) { /* Audio render failures are non-fatal */ }\n lastRenderedAudioEndMs = chunkEndMs;\n }\n \n // =====================================================================\n // EXACT same pattern as captureBatch: seekForRender + captureFromClone\n // =====================================================================\n const seekStart = performance.now();\n await renderClone.seekForRender(timeMs);\n totalSeekMs += performance.now() - seekStart;\n \n const captureStart = performance.now();\n const canvas = await captureFromClone(renderClone, renderContainer, {\n scale: config.scale,\n contentReadyMode: config.contentReadyMode,\n blockingTimeoutMs: config.blockingTimeoutMs,\n originalTimegroup: timegroup,\n });\n totalCaptureMs += performance.now() - captureStart;\n \n // Encode frame\n if (videoSource && output && encodingCtx) {\n const encodeStart = performance.now();\n encodingCtx.drawImage(\n canvas,\n 0, 0, canvas.width, canvas.height,\n 0, 0, config.videoWidth, config.videoHeight,\n );\n await videoSource.add(timestampS, config.frameDurationS);\n totalEncodeMs += performance.now() - encodeStart;\n }\n \n // Progress\n const currentFrame = frameIndex + 1;\n const progress = currentFrame / config.totalFrames;\n const renderedMs = currentFrame * config.frameDurationMs;\n const elapsedMs = performance.now() - renderStartTime;\n const msPerFrame = elapsedMs / currentFrame;\n const remainingFrames = config.totalFrames - currentFrame;\n const estimatedRemainingMs = remainingFrames * msPerFrame;\n const speedMultiplier = renderedMs / elapsedMs;\n \n if (onProgress && frameIndex % 10 === 0) {\n const previewWidth = 160;\n const previewHeight = Math.round(previewWidth * (config.videoHeight / config.videoWidth));\n const thumbCanvas = document.createElement(\"canvas\");\n thumbCanvas.width = previewWidth;\n thumbCanvas.height = previewHeight;\n const thumbCtx = thumbCanvas.getContext(\"2d\")!;\n thumbCtx.drawImage(canvas, 0, 0, previewWidth, previewHeight);\n lastFramePreviewUrl = thumbCanvas.toDataURL(\"image/jpeg\", 0.7);\n }\n \n onProgress?.({\n progress,\n currentFrame,\n totalFrames: config.totalFrames,\n renderedMs,\n totalDurationMs: config.renderDurationMs,\n elapsedMs,\n estimatedRemainingMs,\n speedMultiplier,\n framePreviewUrl: lastFramePreviewUrl,\n });\n }\n \n // Render remaining audio\n if (audioSource && lastRenderedAudioEndMs < config.endMs) {\n try {\n const audioBuffer = await timegroup.renderAudio(lastRenderedAudioEndMs, config.endMs);\n if (audioBuffer && audioBuffer.length > 0) {\n await audioSource.add(audioBuffer);\n }\n } catch (e) { /* Audio render failures are non-fatal */ }\n }\n \n const totalTime = performance.now() - renderStartTime;\n console.log(\n `[renderTimegroupToVideo] ${config.totalFrames} frames: ` +\n `seek=${totalSeekMs.toFixed(0)}ms, capture=${totalCaptureMs.toFixed(0)}ms, ` +\n `encode=${totalEncodeMs.toFixed(0)}ms, total=${totalTime.toFixed(0)}ms`\n );\n \n if (config.benchmarkMode) {\n return undefined;\n }\n \n await output!.finalize();\n \n if (useStreaming) {\n return undefined;\n } else {\n const bufferTarget = target as BufferTarget;\n const videoBuffer = bufferTarget.buffer;\n if (!videoBuffer) {\n throw new Error(\"Video encoding failed: no buffer produced\");\n }\n \n if (config.returnBuffer) {\n return new Uint8Array(videoBuffer);\n }\n \n const videoBlob = new Blob([videoBuffer], { type: \"video/mp4\" });\n downloadBlob(videoBlob, config.filename);\n return undefined;\n }\n \n } finally {\n cleanupRenderClone();\n }\n}\n\nexport { QUALITY_HIGH };\nexport type { AudioCodec };\n"],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { getRenderMode, isNativeCanvasApiAvailable } from "./previewSettings.js";
|
|
2
|
+
|
|
3
|
+
//#region src/preview/renderers.ts
|
|
4
|
+
/**
|
|
5
|
+
* Get the effective render mode, validating that native is available when selected.
|
|
6
|
+
* Falls back to foreignObject if native is selected but not available.
|
|
7
|
+
*/
|
|
8
|
+
function getEffectiveRenderMode() {
|
|
9
|
+
const mode = getRenderMode();
|
|
10
|
+
if (mode === "native" && !isNativeCanvasApiAvailable()) return "foreignObject";
|
|
11
|
+
return mode;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
//#endregion
|
|
15
|
+
export { getEffectiveRenderMode };
|
|
16
|
+
//# sourceMappingURL=renderers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"renderers.js","names":[],"sources":["../../src/preview/renderers.ts"],"sourcesContent":["/**\n * Renderer strategy pattern for HTML-to-image rendering.\n * Provides a unified interface for native (drawElementImage) and foreignObject paths.\n */\n\nimport { isNativeCanvasApiAvailable, getRenderMode, type RenderMode } from \"./previewSettings.js\";\n\n/**\n * Options for rendering HTML to an image or canvas.\n */\nexport interface RenderOptions {\n /** Skip device pixel ratio scaling (render at logical pixels) */\n skipDprScaling?: boolean;\n /** Scale factor for encoding internal canvases (foreignObject only) */\n canvasScale?: number;\n /** Whether to reuse an existing canvas (native only) */\n reuseCanvas?: HTMLCanvasElement;\n /** Whether to wait for an extra paint cycle (native only) */\n waitForPaint?: boolean;\n}\n\n/**\n * Result of a render operation.\n * Native path returns a canvas, foreignObject path returns an image.\n */\nexport type RenderResult = HTMLCanvasElement | HTMLImageElement;\n\n/**\n * Renderer interface for HTML-to-image conversion.\n */\nexport interface Renderer {\n /** The render mode this renderer implements */\n readonly mode: RenderMode;\n \n /**\n * Render an HTML container to an image or canvas.\n * @param container - The HTML element to render\n * @param width - Target width in logical pixels\n * @param height - Target height in logical pixels\n * @param options - Rendering options\n * @returns Promise resolving to a canvas or image element\n */\n render(\n container: HTMLElement,\n width: number,\n height: number,\n options?: RenderOptions,\n ): Promise<RenderResult>;\n \n /**\n * Check if this renderer is available in the current environment.\n */\n isAvailable(): boolean;\n}\n\n/**\n * Get the effective render mode, validating that native is available when selected.\n * Falls back to foreignObject if native is selected but not available.\n */\nexport function getEffectiveRenderMode(): RenderMode {\n const mode = getRenderMode();\n \n if (mode === \"native\" && !isNativeCanvasApiAvailable()) {\n return \"foreignObject\";\n }\n \n return mode;\n}\n\n/**\n * Check if a render result is a canvas element.\n */\nexport function isCanvas(result: RenderResult): result is HTMLCanvasElement {\n return result instanceof HTMLCanvasElement;\n}\n\n/**\n * Check if a render result is an image element.\n */\nexport function isImage(result: RenderResult): result is HTMLImageElement {\n return result instanceof HTMLImageElement;\n}\n"],"mappings":";;;;;;;AA2DA,SAAgB,yBAAqC;CACnD,MAAM,OAAO,eAAe;AAE5B,KAAI,SAAS,YAAY,CAAC,4BAA4B,CACpD,QAAO;AAGT,QAAO"}
|