@editframe/elements 0.30.2-beta.0 → 0.31.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (326) hide show
  1. package/dist/EF_FRAMEGEN.d.ts +5 -0
  2. package/dist/EF_FRAMEGEN.js +20 -4
  3. package/dist/EF_FRAMEGEN.js.map +1 -1
  4. package/dist/EF_INTERACTIVE.js.map +1 -1
  5. package/dist/_virtual/rolldown_runtime.js +27 -0
  6. package/dist/canvas/EFCanvas.d.ts +311 -0
  7. package/dist/canvas/EFCanvas.js +1089 -0
  8. package/dist/canvas/EFCanvas.js.map +1 -0
  9. package/dist/canvas/EFCanvasItem.d.ts +55 -0
  10. package/dist/canvas/EFCanvasItem.js +72 -0
  11. package/dist/canvas/EFCanvasItem.js.map +1 -0
  12. package/dist/canvas/api/CanvasAPI.d.ts +115 -0
  13. package/dist/canvas/api/CanvasAPI.js +182 -0
  14. package/dist/canvas/api/CanvasAPI.js.map +1 -0
  15. package/dist/canvas/api/types.d.ts +42 -0
  16. package/dist/canvas/coordinateTransform.js +90 -0
  17. package/dist/canvas/coordinateTransform.js.map +1 -0
  18. package/dist/canvas/getElementBounds.js +40 -0
  19. package/dist/canvas/getElementBounds.js.map +1 -0
  20. package/dist/canvas/overlays/SelectionOverlay.js +265 -0
  21. package/dist/canvas/overlays/SelectionOverlay.js.map +1 -0
  22. package/dist/canvas/overlays/overlayState.js +153 -0
  23. package/dist/canvas/overlays/overlayState.js.map +1 -0
  24. package/dist/canvas/selection/SelectionController.js +105 -0
  25. package/dist/canvas/selection/SelectionController.js.map +1 -0
  26. package/dist/canvas/selection/SelectionModel.d.ts +98 -0
  27. package/dist/canvas/selection/SelectionModel.js +229 -0
  28. package/dist/canvas/selection/SelectionModel.js.map +1 -0
  29. package/dist/canvas/selection/selectionContext.d.ts +31 -0
  30. package/dist/canvas/selection/selectionContext.js +12 -0
  31. package/dist/canvas/selection/selectionContext.js.map +1 -0
  32. package/dist/elements/ContainerInfo.d.ts +29 -0
  33. package/dist/elements/ContainerInfo.js +30 -0
  34. package/dist/elements/ContainerInfo.js.map +1 -0
  35. package/dist/elements/EFAudio.d.ts +13 -3
  36. package/dist/elements/EFAudio.js +64 -10
  37. package/dist/elements/EFAudio.js.map +1 -1
  38. package/dist/elements/EFCaptions.d.ts +18 -16
  39. package/dist/elements/EFCaptions.js +110 -19
  40. package/dist/elements/EFCaptions.js.map +1 -1
  41. package/dist/elements/EFImage.d.ts +16 -6
  42. package/dist/elements/EFImage.js +79 -9
  43. package/dist/elements/EFImage.js.map +1 -1
  44. package/dist/elements/EFMedia/AssetIdMediaEngine.js +51 -4
  45. package/dist/elements/EFMedia/AssetIdMediaEngine.js.map +1 -1
  46. package/dist/elements/EFMedia/AssetMediaEngine.js +125 -52
  47. package/dist/elements/EFMedia/AssetMediaEngine.js.map +1 -1
  48. package/dist/elements/EFMedia/BaseMediaEngine.js +24 -6
  49. package/dist/elements/EFMedia/BaseMediaEngine.js.map +1 -1
  50. package/dist/elements/EFMedia/JitMediaEngine.js +12 -8
  51. package/dist/elements/EFMedia/JitMediaEngine.js.map +1 -1
  52. package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.js +46 -7
  53. package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.js.map +1 -1
  54. package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.js +98 -73
  55. package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.js.map +1 -1
  56. package/dist/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.js +28 -5
  57. package/dist/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.js.map +1 -1
  58. package/dist/elements/EFMedia/audioTasks/makeAudioInputTask.js +18 -6
  59. package/dist/elements/EFMedia/audioTasks/makeAudioInputTask.js.map +1 -1
  60. package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.js +8 -2
  61. package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.js.map +1 -1
  62. package/dist/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.js +31 -6
  63. package/dist/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.js.map +1 -1
  64. package/dist/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.js +28 -5
  65. package/dist/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.js.map +1 -1
  66. package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.js +97 -72
  67. package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.js.map +1 -1
  68. package/dist/elements/EFMedia/shared/AudioSpanUtils.js +3 -1
  69. package/dist/elements/EFMedia/shared/AudioSpanUtils.js.map +1 -1
  70. package/dist/elements/EFMedia/shared/BufferUtils.js +1 -1
  71. package/dist/elements/EFMedia/shared/BufferUtils.js.map +1 -1
  72. package/dist/elements/EFMedia/shared/ThumbnailExtractor.js +25 -14
  73. package/dist/elements/EFMedia/shared/ThumbnailExtractor.js.map +1 -1
  74. package/dist/elements/EFMedia/tasks/makeMediaEngineTask.js +47 -16
  75. package/dist/elements/EFMedia/tasks/makeMediaEngineTask.js.map +1 -1
  76. package/dist/elements/EFMedia/videoTasks/MainVideoInputCache.js +37 -19
  77. package/dist/elements/EFMedia/videoTasks/MainVideoInputCache.js.map +1 -1
  78. package/dist/elements/EFMedia/videoTasks/ScrubInputCache.js +65 -21
  79. package/dist/elements/EFMedia/videoTasks/ScrubInputCache.js.map +1 -1
  80. package/dist/elements/EFMedia/videoTasks/makeScrubVideoBufferTask.js +8 -3
  81. package/dist/elements/EFMedia/videoTasks/makeScrubVideoBufferTask.js.map +1 -1
  82. package/dist/elements/EFMedia/videoTasks/makeScrubVideoInitSegmentFetchTask.js +32 -9
  83. package/dist/elements/EFMedia/videoTasks/makeScrubVideoInitSegmentFetchTask.js.map +1 -1
  84. package/dist/elements/EFMedia/videoTasks/makeScrubVideoInputTask.js +33 -10
  85. package/dist/elements/EFMedia/videoTasks/makeScrubVideoInputTask.js.map +1 -1
  86. package/dist/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.js +23 -8
  87. package/dist/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.js.map +1 -1
  88. package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentFetchTask.js +34 -10
  89. package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentFetchTask.js.map +1 -1
  90. package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentIdTask.js +31 -8
  91. package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentIdTask.js.map +1 -1
  92. package/dist/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.js +31 -114
  93. package/dist/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.js.map +1 -1
  94. package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.js +44 -8
  95. package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.js.map +1 -1
  96. package/dist/elements/EFMedia.d.ts +18 -7
  97. package/dist/elements/EFMedia.js +23 -3
  98. package/dist/elements/EFMedia.js.map +1 -1
  99. package/dist/elements/EFPanZoom.d.ts +96 -0
  100. package/dist/elements/EFPanZoom.js +290 -0
  101. package/dist/elements/EFPanZoom.js.map +1 -0
  102. package/dist/elements/EFSourceMixin.js +7 -6
  103. package/dist/elements/EFSourceMixin.js.map +1 -1
  104. package/dist/elements/EFSurface.d.ts +6 -6
  105. package/dist/elements/EFSurface.js +7 -2
  106. package/dist/elements/EFSurface.js.map +1 -1
  107. package/dist/elements/EFTemporal.d.ts +2 -1
  108. package/dist/elements/EFTemporal.js +192 -71
  109. package/dist/elements/EFTemporal.js.map +1 -1
  110. package/dist/elements/EFText.d.ts +5 -4
  111. package/dist/elements/EFText.js +102 -13
  112. package/dist/elements/EFText.js.map +1 -1
  113. package/dist/elements/EFTextSegment.d.ts +32 -6
  114. package/dist/elements/EFTextSegment.js +53 -15
  115. package/dist/elements/EFTextSegment.js.map +1 -1
  116. package/dist/elements/EFThumbnailStrip.d.ts +129 -56
  117. package/dist/elements/EFThumbnailStrip.js +605 -359
  118. package/dist/elements/EFThumbnailStrip.js.map +1 -1
  119. package/dist/elements/EFTimegroup.d.ts +233 -25
  120. package/dist/elements/EFTimegroup.js +865 -144
  121. package/dist/elements/EFTimegroup.js.map +1 -1
  122. package/dist/elements/EFVideo.d.ts +42 -5
  123. package/dist/elements/EFVideo.js +165 -11
  124. package/dist/elements/EFVideo.js.map +1 -1
  125. package/dist/elements/EFWaveform.d.ts +6 -6
  126. package/dist/elements/EFWaveform.js +2 -1
  127. package/dist/elements/EFWaveform.js.map +1 -1
  128. package/dist/elements/ElementPositionInfo.d.ts +35 -0
  129. package/dist/elements/ElementPositionInfo.js +49 -0
  130. package/dist/elements/ElementPositionInfo.js.map +1 -0
  131. package/dist/elements/FetchMixin.js +16 -1
  132. package/dist/elements/FetchMixin.js.map +1 -1
  133. package/dist/elements/SessionThumbnailCache.js +154 -0
  134. package/dist/elements/SessionThumbnailCache.js.map +1 -0
  135. package/dist/elements/TargetController.js +3 -1
  136. package/dist/elements/TargetController.js.map +1 -1
  137. package/dist/elements/TimegroupController.js +9 -3
  138. package/dist/elements/TimegroupController.js.map +1 -1
  139. package/dist/elements/findRootTemporal.js +30 -0
  140. package/dist/elements/findRootTemporal.js.map +1 -0
  141. package/dist/elements/renderTemporalAudio.js +18 -5
  142. package/dist/elements/renderTemporalAudio.js.map +1 -1
  143. package/dist/elements/updateAnimations.js +171 -28
  144. package/dist/elements/updateAnimations.js.map +1 -1
  145. package/dist/getRenderInfo.d.ts +2 -2
  146. package/dist/gui/ContextMixin.js +4 -2
  147. package/dist/gui/ContextMixin.js.map +1 -1
  148. package/dist/gui/Controllable.js +74 -1
  149. package/dist/gui/Controllable.js.map +1 -1
  150. package/dist/gui/EFActiveRootTemporal.d.ts +50 -0
  151. package/dist/gui/EFActiveRootTemporal.js +94 -0
  152. package/dist/gui/EFActiveRootTemporal.js.map +1 -0
  153. package/dist/gui/EFConfiguration.d.ts +7 -1
  154. package/dist/gui/EFConfiguration.js.map +1 -1
  155. package/dist/gui/EFControls.d.ts +2 -2
  156. package/dist/gui/EFControls.js +109 -13
  157. package/dist/gui/EFControls.js.map +1 -1
  158. package/dist/gui/EFDial.d.ts +4 -4
  159. package/dist/gui/EFFilmstrip.d.ts +11 -214
  160. package/dist/gui/EFFilmstrip.js +53 -1152
  161. package/dist/gui/EFFilmstrip.js.map +1 -1
  162. package/dist/gui/EFFitScale.d.ts +3 -3
  163. package/dist/gui/EFFitScale.js +39 -12
  164. package/dist/gui/EFFitScale.js.map +1 -1
  165. package/dist/gui/EFFocusOverlay.d.ts +4 -4
  166. package/dist/gui/EFOverlayItem.d.ts +48 -0
  167. package/dist/gui/EFOverlayItem.js +97 -0
  168. package/dist/gui/EFOverlayItem.js.map +1 -0
  169. package/dist/gui/EFOverlayLayer.d.ts +70 -0
  170. package/dist/gui/EFOverlayLayer.js +104 -0
  171. package/dist/gui/EFOverlayLayer.js.map +1 -0
  172. package/dist/gui/EFPause.d.ts +4 -4
  173. package/dist/gui/EFPlay.d.ts +4 -4
  174. package/dist/gui/EFResizableBox.d.ts +12 -16
  175. package/dist/gui/EFResizableBox.js +109 -451
  176. package/dist/gui/EFResizableBox.js.map +1 -1
  177. package/dist/gui/EFScrubber.d.ts +30 -5
  178. package/dist/gui/EFScrubber.js +224 -31
  179. package/dist/gui/EFScrubber.js.map +1 -1
  180. package/dist/gui/EFTimeDisplay.d.ts +4 -4
  181. package/dist/gui/EFTimeDisplay.js +4 -1
  182. package/dist/gui/EFTimeDisplay.js.map +1 -1
  183. package/dist/gui/EFTimelineRuler.d.ts +71 -0
  184. package/dist/gui/EFTimelineRuler.js +320 -0
  185. package/dist/gui/EFTimelineRuler.js.map +1 -0
  186. package/dist/gui/EFToggleLoop.d.ts +4 -4
  187. package/dist/gui/EFTogglePlay.d.ts +4 -4
  188. package/dist/gui/EFTransformHandles.d.ts +91 -0
  189. package/dist/gui/EFTransformHandles.js +393 -0
  190. package/dist/gui/EFTransformHandles.js.map +1 -0
  191. package/dist/gui/EFWorkbench.d.ts +178 -0
  192. package/dist/gui/EFWorkbench.js +2067 -22
  193. package/dist/gui/EFWorkbench.js.map +1 -1
  194. package/dist/gui/FitScaleHelpers.d.ts +31 -0
  195. package/dist/gui/FitScaleHelpers.js +41 -0
  196. package/dist/gui/FitScaleHelpers.js.map +1 -0
  197. package/dist/gui/PlaybackController.d.ts +2 -1
  198. package/dist/gui/PlaybackController.js +46 -15
  199. package/dist/gui/PlaybackController.js.map +1 -1
  200. package/dist/gui/TWMixin.js +1 -1
  201. package/dist/gui/TWMixin.js.map +1 -1
  202. package/dist/gui/hierarchy/EFHierarchy.d.ts +65 -0
  203. package/dist/gui/hierarchy/EFHierarchy.js +338 -0
  204. package/dist/gui/hierarchy/EFHierarchy.js.map +1 -0
  205. package/dist/gui/hierarchy/EFHierarchyItem.d.ts +118 -0
  206. package/dist/gui/hierarchy/EFHierarchyItem.js +551 -0
  207. package/dist/gui/hierarchy/EFHierarchyItem.js.map +1 -0
  208. package/dist/gui/hierarchy/hierarchyContext.d.ts +38 -0
  209. package/dist/gui/hierarchy/hierarchyContext.js +8 -0
  210. package/dist/gui/hierarchy/hierarchyContext.js.map +1 -0
  211. package/dist/gui/icons.js +34 -0
  212. package/dist/gui/icons.js.map +1 -0
  213. package/dist/gui/panZoomTransformContext.js +12 -0
  214. package/dist/gui/panZoomTransformContext.js.map +1 -0
  215. package/dist/gui/previewSettingsContext.js +12 -0
  216. package/dist/gui/previewSettingsContext.js.map +1 -0
  217. package/dist/gui/timeline/EFTimeline.d.ts +270 -0
  218. package/dist/gui/timeline/EFTimeline.js +1369 -0
  219. package/dist/gui/timeline/EFTimeline.js.map +1 -0
  220. package/dist/gui/timeline/EFTimelineRow.js +374 -0
  221. package/dist/gui/timeline/EFTimelineRow.js.map +1 -0
  222. package/dist/gui/timeline/TrimHandles.d.ts +36 -0
  223. package/dist/gui/timeline/TrimHandles.js +204 -0
  224. package/dist/gui/timeline/TrimHandles.js.map +1 -0
  225. package/dist/gui/timeline/flattenHierarchy.js +31 -0
  226. package/dist/gui/timeline/flattenHierarchy.js.map +1 -0
  227. package/dist/gui/timeline/timelineStateContext.d.ts +26 -0
  228. package/dist/gui/timeline/timelineStateContext.js +42 -0
  229. package/dist/gui/timeline/timelineStateContext.js.map +1 -0
  230. package/dist/gui/timeline/tracks/AudioTrack.js +264 -0
  231. package/dist/gui/timeline/tracks/AudioTrack.js.map +1 -0
  232. package/dist/gui/timeline/tracks/CaptionsTrack.js +595 -0
  233. package/dist/gui/timeline/tracks/CaptionsTrack.js.map +1 -0
  234. package/dist/gui/timeline/tracks/HTMLTrack.js +19 -0
  235. package/dist/gui/timeline/tracks/HTMLTrack.js.map +1 -0
  236. package/dist/gui/timeline/tracks/ImageTrack.js +53 -0
  237. package/dist/gui/timeline/tracks/ImageTrack.js.map +1 -0
  238. package/dist/gui/timeline/tracks/TextTrack.js +250 -0
  239. package/dist/gui/timeline/tracks/TextTrack.js.map +1 -0
  240. package/dist/gui/timeline/tracks/TimegroupTrack.js +143 -0
  241. package/dist/gui/timeline/tracks/TimegroupTrack.js.map +1 -0
  242. package/dist/gui/timeline/tracks/TrackItem.js +269 -0
  243. package/dist/gui/timeline/tracks/TrackItem.js.map +1 -0
  244. package/dist/gui/timeline/tracks/VideoTrack.js +265 -0
  245. package/dist/gui/timeline/tracks/VideoTrack.js.map +1 -0
  246. package/dist/gui/timeline/tracks/WaveformTrack.js +19 -0
  247. package/dist/gui/timeline/tracks/WaveformTrack.js.map +1 -0
  248. package/dist/gui/timeline/tracks/ensureTrackItemInit.js +1 -0
  249. package/dist/gui/timeline/tracks/preloadTracks.js +9 -0
  250. package/dist/gui/timeline/tracks/renderTrackChildren.js +119 -0
  251. package/dist/gui/timeline/tracks/renderTrackChildren.js.map +1 -0
  252. package/dist/gui/timeline/tracks/waveformUtils.js +80 -0
  253. package/dist/gui/timeline/tracks/waveformUtils.js.map +1 -0
  254. package/dist/gui/transformCalculations.js +217 -0
  255. package/dist/gui/transformCalculations.js.map +1 -0
  256. package/dist/gui/transformUtils.d.ts +37 -0
  257. package/dist/gui/transformUtils.js +77 -0
  258. package/dist/gui/transformUtils.js.map +1 -0
  259. package/dist/gui/tree/EFTree.d.ts +59 -0
  260. package/dist/gui/tree/EFTree.js +174 -0
  261. package/dist/gui/tree/EFTree.js.map +1 -0
  262. package/dist/gui/tree/EFTreeItem.d.ts +38 -0
  263. package/dist/gui/tree/EFTreeItem.js +146 -0
  264. package/dist/gui/tree/EFTreeItem.js.map +1 -0
  265. package/dist/gui/tree/treeContext.d.ts +60 -0
  266. package/dist/gui/tree/treeContext.js +23 -0
  267. package/dist/gui/tree/treeContext.js.map +1 -0
  268. package/dist/index.d.ts +32 -8
  269. package/dist/index.js +30 -6
  270. package/dist/index.js.map +1 -1
  271. package/dist/node_modules/react/cjs/react-jsx-runtime.development.js +688 -0
  272. package/dist/node_modules/react/cjs/react-jsx-runtime.development.js.map +1 -0
  273. package/dist/node_modules/react/cjs/react.development.js +1521 -0
  274. package/dist/node_modules/react/cjs/react.development.js.map +1 -0
  275. package/dist/node_modules/react/index.js +13 -0
  276. package/dist/node_modules/react/index.js.map +1 -0
  277. package/dist/node_modules/react/jsx-runtime.js +13 -0
  278. package/dist/node_modules/react/jsx-runtime.js.map +1 -0
  279. package/dist/preview/AdaptiveResolutionTracker.js +228 -0
  280. package/dist/preview/AdaptiveResolutionTracker.js.map +1 -0
  281. package/dist/preview/RenderProfiler.js +135 -0
  282. package/dist/preview/RenderProfiler.js.map +1 -0
  283. package/dist/preview/previewSettings.js +131 -0
  284. package/dist/preview/previewSettings.js.map +1 -0
  285. package/dist/preview/previewTypes.js +64 -0
  286. package/dist/preview/previewTypes.js.map +1 -0
  287. package/dist/preview/renderTimegroupPreview.js +656 -0
  288. package/dist/preview/renderTimegroupPreview.js.map +1 -0
  289. package/dist/preview/renderTimegroupToCanvas.d.ts +37 -0
  290. package/dist/preview/renderTimegroupToCanvas.js +833 -0
  291. package/dist/preview/renderTimegroupToCanvas.js.map +1 -0
  292. package/dist/preview/renderTimegroupToVideo.d.ts +39 -0
  293. package/dist/preview/renderTimegroupToVideo.js +274 -0
  294. package/dist/preview/renderTimegroupToVideo.js.map +1 -0
  295. package/dist/preview/renderers.js +16 -0
  296. package/dist/preview/renderers.js.map +1 -0
  297. package/dist/preview/statsTrackingStrategy.js +201 -0
  298. package/dist/preview/statsTrackingStrategy.js.map +1 -0
  299. package/dist/preview/thumbnailCacheSettings.js +52 -0
  300. package/dist/preview/thumbnailCacheSettings.js.map +1 -0
  301. package/dist/preview/workers/WorkerPool.js +178 -0
  302. package/dist/preview/workers/WorkerPool.js.map +1 -0
  303. package/dist/preview/workers/encoderWorkerInline.js +103 -0
  304. package/dist/preview/workers/encoderWorkerInline.js.map +1 -0
  305. package/dist/sandbox/PlaybackControls.js +10 -0
  306. package/dist/sandbox/PlaybackControls.js.map +1 -0
  307. package/dist/sandbox/ScenarioRunner.js +1 -0
  308. package/dist/sandbox/index.js +2 -0
  309. package/dist/style.css +71 -67
  310. package/dist/transcoding/types/index.d.ts +2 -1
  311. package/dist/transcoding/utils/UrlGenerator.d.ts +6 -1
  312. package/dist/transcoding/utils/UrlGenerator.js +12 -3
  313. package/dist/transcoding/utils/UrlGenerator.js.map +1 -1
  314. package/dist/utils/LRUCache.js +1 -375
  315. package/dist/utils/LRUCache.js.map +1 -1
  316. package/dist/utils/frameTime.js +14 -0
  317. package/dist/utils/frameTime.js.map +1 -0
  318. package/package.json +3 -3
  319. package/test/profilingPlugin.ts +223 -0
  320. package/test/recordReplayProxyPlugin.js +22 -27
  321. package/test/thumbnail-performance-test.html +116 -0
  322. package/test/visualRegressionUtils.ts +286 -0
  323. package/types.json +1 -1
  324. package/dist/elements/TimegroupController.d.ts +0 -18
  325. package/dist/msToTimeCode.js +0 -17
  326. package/dist/msToTimeCode.js.map +0 -1
@@ -1,8 +1,9 @@
1
1
  import { EF_INTERACTIVE } from "../EF_INTERACTIVE.js";
2
+ import { quantizeToFrameTimeS } from "../utils/frameTime.js";
2
3
  import { EF_RENDERING } from "../EF_RENDERING.js";
3
4
  import { parseTimeToMs } from "./parseTimeToMs.js";
4
5
  import { __decorate } from "../_virtual/_@oxc-project_runtime@0.94.0/helpers/decorate.js";
5
- import { EFTemporal, deepGetElementsWithFrameTasks, flushStartTimeMsCache, resetTemporalCache, shallowGetTemporalElements, timegroupContext } from "./EFTemporal.js";
6
+ import { EFTemporal, deepGetElementsWithFrameTasks, flushStartTimeMsCache, registerIsTimegroupCalculatingDuration, resetTemporalCache, shallowGetTemporalElements, timegroupContext } from "./EFTemporal.js";
6
7
  import { efContext } from "../gui/efContext.js";
7
8
  import { isContextMixin } from "../gui/ContextMixin.js";
8
9
  import { TWMixin } from "../gui/TWMixin2.js";
@@ -12,6 +13,16 @@ import { EFTargetable } from "./TargetController.js";
12
13
  import { deepGetMediaElements } from "./EFMedia.js";
13
14
  import { TimegroupController } from "./TimegroupController.js";
14
15
  import { evaluateAnimationVisibilityState, updateAnimations } from "./updateAnimations.js";
16
+ import { getContainerInfoFromElement } from "./ContainerInfo.js";
17
+ import { getPositionInfoFromElement } from "./ElementPositionInfo.js";
18
+ import { captureFromClone, captureTimegroupAtTime } from "../preview/renderTimegroupToCanvas.js";
19
+ import { renderTimegroupToVideo } from "../preview/renderTimegroupToVideo.js";
20
+ import "../canvas/EFCanvas.js";
21
+ import "../gui/hierarchy/EFHierarchy.js";
22
+ import "../gui/EFFilmstrip.js";
23
+ import "../gui/EFFitScale.js";
24
+ import "../gui/EFWorkbench.js";
25
+ import "./EFPanZoom.js";
15
26
  import { provide } from "@lit/context";
16
27
  import { Task, TaskStatus } from "@lit/task";
17
28
  import debug from "debug";
@@ -21,15 +32,150 @@ import { customElement, property } from "lit/decorators.js";
21
32
  //#region src/elements/EFTimegroup.ts
22
33
  var _EFTimegroup;
23
34
  const log = debug("ef:elements:EFTimegroup");
24
- let sequenceDurationCache = /* @__PURE__ */ new WeakMap();
25
- const flushSequenceDurationCache = () => {
26
- sequenceDurationCache = /* @__PURE__ */ new WeakMap();
35
+ const INITIALIZER_ERROR_THRESHOLD_MS = 100;
36
+ const INITIALIZER_WARN_THRESHOLD_MS = 10;
37
+ let durationCache = /* @__PURE__ */ new WeakMap();
38
+ const flushDurationCache = () => {
39
+ durationCache = /* @__PURE__ */ new WeakMap();
27
40
  };
41
+ const flushSequenceDurationCache = flushDurationCache;
42
+ const durationCalculationInProgress = /* @__PURE__ */ new WeakSet();
43
+ const isTimegroupCalculatingDuration = (timegroup) => {
44
+ return timegroup !== void 0 && durationCalculationInProgress.has(timegroup);
45
+ };
46
+ registerIsTimegroupCalculatingDuration(isTimegroupCalculatingDuration);
47
+ /**
48
+ * Determines if a timegroup has its own duration based on its mode.
49
+ * This is the semantic rule: which modes produce independent durations.
50
+ */
51
+ function hasOwnDurationForMode(mode, hasExplicitDuration) {
52
+ return mode === "contain" || mode === "sequence" || mode === "fixed" && hasExplicitDuration;
53
+ }
54
+ /**
55
+ * Determines if a child temporal element should participate in parent duration calculation.
56
+ *
57
+ * Semantic rule: Fit-mode children inherit from parent, so they don't contribute to parent's
58
+ * duration calculation (to avoid circular dependencies). Children without own duration
59
+ * also don't contribute.
60
+ */
61
+ function shouldParticipateInDurationCalculation(child) {
62
+ if (child instanceof EFTimegroup && child.mode === "fit") return false;
63
+ if (!child.hasOwnDuration) return false;
64
+ return true;
65
+ }
66
+ /**
67
+ * Evaluates duration for "fit" mode: inherits from parent.
68
+ * Semantic rule: fit mode always matches parent duration, or 0 if no parent.
69
+ */
70
+ function evaluateFitDuration(parentTimegroup) {
71
+ if (!parentTimegroup) return 0;
72
+ return parentTimegroup.durationMs;
73
+ }
74
+ /**
75
+ * Evaluates duration for "sequence" mode: sum of children minus overlaps.
76
+ * Semantic rule: sequence mode sums child durations, subtracting overlap between consecutive items.
77
+ * Fit-mode children are excluded to avoid circular dependencies.
78
+ */
79
+ function evaluateSequenceDuration(timegroup, childTemporals, overlapMs) {
80
+ const cachedDuration = durationCache.get(timegroup);
81
+ if (cachedDuration !== void 0) return cachedDuration;
82
+ let duration = 0;
83
+ let participatingIndex = 0;
84
+ childTemporals.forEach((child) => {
85
+ if (!shouldParticipateInDurationCalculation(child)) return;
86
+ if (child instanceof EFTimegroup && durationCalculationInProgress.has(child)) return;
87
+ if (child instanceof EFTimegroup) {
88
+ let ancestor = child.parentNode;
89
+ let shouldSkip = false;
90
+ while (ancestor) {
91
+ if (ancestor === timegroup) break;
92
+ if (ancestor instanceof EFTimegroup && durationCalculationInProgress.has(ancestor)) {
93
+ shouldSkip = true;
94
+ break;
95
+ }
96
+ ancestor = ancestor.parentNode;
97
+ }
98
+ if (shouldSkip) return;
99
+ }
100
+ if (participatingIndex > 0) duration -= overlapMs;
101
+ duration += child.durationMs;
102
+ participatingIndex++;
103
+ });
104
+ duration = Math.max(0, duration);
105
+ durationCache.set(timegroup, duration);
106
+ return duration;
107
+ }
108
+ /**
109
+ * Evaluates duration for "contain" mode: maximum of children.
110
+ * Semantic rule: contain mode takes the maximum child duration.
111
+ * Fit-mode children and children without own duration are excluded.
112
+ */
113
+ function evaluateContainDuration(timegroup, childTemporals) {
114
+ const cachedDuration = durationCache.get(timegroup);
115
+ if (cachedDuration !== void 0) return cachedDuration;
116
+ let maxDuration = 0;
117
+ for (const child of childTemporals) {
118
+ if (!shouldParticipateInDurationCalculation(child)) continue;
119
+ if (child instanceof EFTimegroup && durationCalculationInProgress.has(child)) continue;
120
+ if (child instanceof EFTimegroup) {
121
+ let ancestor = child.parentNode;
122
+ let shouldSkip = false;
123
+ while (ancestor) {
124
+ if (ancestor === timegroup) break;
125
+ if (ancestor instanceof EFTimegroup && durationCalculationInProgress.has(ancestor)) {
126
+ shouldSkip = true;
127
+ break;
128
+ }
129
+ ancestor = ancestor.parentNode;
130
+ }
131
+ if (shouldSkip) continue;
132
+ }
133
+ maxDuration = Math.max(maxDuration, child.durationMs);
134
+ }
135
+ const duration = Math.max(0, maxDuration);
136
+ durationCache.set(timegroup, duration);
137
+ return duration;
138
+ }
139
+ /**
140
+ * Evaluates duration based on timegroup mode.
141
+ * This is the semantic evaluation function - it determines what duration should be.
142
+ *
143
+ * Note: Fixed mode is handled inline in the getter because it needs to call super.durationMs
144
+ * which requires the class context. The other modes are extracted for clarity.
145
+ */
146
+ function evaluateDurationForMode(timegroup, mode, childTemporals) {
147
+ switch (mode) {
148
+ case "fit": return evaluateFitDuration(timegroup.parentTimegroup);
149
+ case "sequence":
150
+ durationCalculationInProgress.add(timegroup);
151
+ try {
152
+ return evaluateSequenceDuration(timegroup, childTemporals, timegroup.overlapMs);
153
+ } finally {
154
+ durationCalculationInProgress.delete(timegroup);
155
+ }
156
+ case "contain":
157
+ durationCalculationInProgress.add(timegroup);
158
+ try {
159
+ return evaluateContainDuration(timegroup, childTemporals);
160
+ } finally {
161
+ durationCalculationInProgress.delete(timegroup);
162
+ }
163
+ default: throw new Error(`Invalid time mode: ${mode}`);
164
+ }
165
+ }
28
166
  const shallowGetTimegroups = (element, groups = []) => {
29
167
  for (const child of Array.from(element.children)) if (child instanceof EFTimegroup) groups.push(child);
30
168
  else shallowGetTimegroups(child, groups);
31
169
  return groups;
32
170
  };
171
+ /**
172
+ * Evaluates the target time for a seek operation.
173
+ * Applies quantization and clamping to determine the valid seek target.
174
+ */
175
+ function evaluateSeekTarget(requestedTime, durationMs, fps) {
176
+ const quantizedTime = quantizeToFrameTimeS(requestedTime, fps);
177
+ return Math.max(0, Math.min(quantizedTime, durationMs / 1e3));
178
+ }
33
179
  let EFTimegroup = class EFTimegroup$1 extends EFTargetable(EFTemporal(TWMixin(LitElement))) {
34
180
  static {
35
181
  _EFTimegroup = this;
@@ -41,47 +187,83 @@ let EFTimegroup = class EFTimegroup$1 extends EFTargetable(EFTemporal(TWMixin(Li
41
187
  this.mode = "contain";
42
188
  this.overlapMs = 0;
43
189
  this.fps = 30;
190
+ this.autoInit = false;
191
+ this.workbench = false;
44
192
  this.fit = "none";
45
- this.mediaDurationsPromise = void 0;
46
193
  this.frameTask = new Task(this, {
47
- autoRun: false,
194
+ autoRun: EF_INTERACTIVE,
48
195
  args: () => [this.ownCurrentTimeMs, this.currentTimeMs],
49
- task: async ([ownCurrentTimeMs, currentTimeMs]) => {
50
- if (this.isRootTimegroup) await withSpan("timegroup.frameTask", {
196
+ onError: (error) => {
197
+ this.frameTask.taskComplete.catch(() => {});
198
+ if (error instanceof DOMException && error.name === "AbortError" || error instanceof Error && (error.name === "AbortError" || error.message?.includes("signal is aborted") || error.message?.includes("The user aborted a request"))) return;
199
+ console.error("EFTimegroup frameTask error", error);
200
+ },
201
+ task: async ([ownCurrentTimeMs, currentTimeMs], { signal }) => {
202
+ const now = Date.now();
203
+ if (now - this.#timegroupFrameTaskLastReset > _EFTimegroup.TIMEGROUP_FRAME_TASK_RESET_MS) {
204
+ this.#timegroupFrameTaskCount = 0;
205
+ this.#timegroupFrameTaskLastReset = now;
206
+ }
207
+ this.#timegroupFrameTaskCount++;
208
+ if (this.#timegroupFrameTaskCount > _EFTimegroup.TIMEGROUP_FRAME_TASK_THRESHOLD) return;
209
+ signal?.throwIfAborted();
210
+ if (this.isRootTimegroup) return withSpan("timegroup.frameTask", {
51
211
  timegroupId: this.id || "unknown",
52
212
  ownCurrentTimeMs,
53
213
  currentTimeMs
54
214
  }, void 0, async () => {
55
- await this.waitForFrameTasks();
215
+ await this.waitForFrameTasks(signal);
216
+ signal?.throwIfAborted();
56
217
  await this.#executeCustomFrameTasks();
218
+ signal?.throwIfAborted();
57
219
  updateAnimations(this);
58
220
  });
59
- else await this.#executeCustomFrameTasks();
221
+ else return this.#executeCustomFrameTasks();
60
222
  }
61
223
  });
62
224
  this.seekTask = new Task(this, {
63
225
  autoRun: false,
64
226
  args: () => [this.#pendingSeekTime ?? this.#currentTime],
65
227
  onComplete: () => {},
66
- task: async ([targetTime]) => {
67
- if (this.playbackController) {
68
- await this.playbackController.seekTask.taskComplete;
228
+ task: async ([targetTime], { signal }) => {
229
+ signal?.throwIfAborted();
230
+ if (this.playbackController) return this.playbackController.seekTask.taskComplete.then(() => {
231
+ signal?.throwIfAborted();
69
232
  return this.currentTime;
70
- }
233
+ });
71
234
  if (!this.isRootTimegroup) return;
72
235
  return withSpan("timegroup.seekTask", {
73
236
  timegroupId: this.id || "unknown",
74
237
  targetTime: targetTime ?? 0,
75
238
  durationMs: this.durationMs
76
239
  }, void 0, async (span) => {
77
- await this.waitForMediaDurations();
78
- const newTime = Math.max(0, Math.min(targetTime ?? 0, this.durationMs / 1e3));
240
+ try {
241
+ await Promise.race([this.waitForMediaDurations(signal), new Promise((_, reject) => {
242
+ if (signal?.aborted) {
243
+ reject(new DOMException("Aborted", "AbortError"));
244
+ return;
245
+ }
246
+ const timeoutId = setTimeout(() => reject(/* @__PURE__ */ new Error("waitForMediaDurations timeout")), 1e4);
247
+ signal?.addEventListener("abort", () => {
248
+ clearTimeout(timeoutId);
249
+ reject(new DOMException("Aborted", "AbortError"));
250
+ });
251
+ })]);
252
+ } catch (error) {
253
+ if (error instanceof DOMException && error.name === "AbortError") throw error;
254
+ }
255
+ signal?.throwIfAborted();
256
+ const newTime = evaluateSeekTarget(targetTime ?? 0, this.durationMs, this.effectiveFps);
79
257
  if (isTracingEnabled()) span.setAttribute("newTime", newTime);
80
258
  this.#currentTime = newTime;
81
259
  this.requestUpdate("currentTime");
82
- await this.runThrottledFrameTask();
83
- this.saveTimeToLocalStorage(this.#currentTime);
260
+ await this.updateComplete;
261
+ signal?.throwIfAborted();
262
+ await this.#runThrottledFrameTask();
263
+ signal?.throwIfAborted();
264
+ if (!this.#restoringFromLocalStorage) this.saveTimeToLocalStorage(this.#currentTime);
84
265
  this.#seekInProgress = false;
266
+ if (this.#restoringFromLocalStorage) this.#restoringFromLocalStorage = false;
85
267
  return newTime;
86
268
  });
87
269
  }
@@ -94,7 +276,9 @@ let EFTimegroup = class EFTimegroup$1 extends EFTargetable(EFTemporal(TWMixin(Li
94
276
  "overlap",
95
277
  "currenttime",
96
278
  "fit",
97
- "fps"
279
+ "fps",
280
+ "auto-init",
281
+ "workbench"
98
282
  ];
99
283
  }
100
284
  static {
@@ -118,59 +302,87 @@ let EFTimegroup = class EFTimegroup$1 extends EFTargetable(EFTemporal(TWMixin(Li
118
302
  attributeChangedCallback(name, old, value) {
119
303
  if (name === "mode" && value) this.mode = value;
120
304
  if (name === "overlap" && value) this.overlapMs = parseTimeToMs(value);
305
+ if (name === "auto-init") this.autoInit = value !== null;
121
306
  if (name === "fps" && value) this.fps = Number.parseFloat(value);
307
+ if (name === "workbench") this.workbench = value !== null;
122
308
  super.attributeChangedCallback(name, old, value);
123
309
  }
124
310
  #resizeObserver;
311
+ /** Content epoch - increments when visual content changes (used by thumbnail cache) */
312
+ #contentEpoch = 0;
125
313
  #currentTime = void 0;
314
+ #userTimeMs = 0;
126
315
  #seekInProgress = false;
127
316
  #pendingSeekTime;
128
317
  #processingPendingSeek = false;
318
+ #restoringFromLocalStorage = false;
319
+ /** @internal */
320
+ isRestoringFromLocalStorage() {
321
+ return this.#restoringFromLocalStorage;
322
+ }
323
+ /** @internal - Used by PlaybackController to set restoration state */
324
+ setRestoringFromLocalStorage(value) {
325
+ this.#restoringFromLocalStorage = value;
326
+ }
129
327
  #customFrameTasks = /* @__PURE__ */ new Set();
328
+ #onFrameCallback = null;
329
+ #onFrameCleanup = null;
330
+ #playbackListener = null;
130
331
  /**
131
332
  * Get the effective FPS for this timegroup.
132
333
  * During rendering, uses the render options FPS if available.
133
334
  * Otherwise uses the configured fps property.
335
+ * @public
134
336
  */
135
337
  get effectiveFps() {
136
338
  if (typeof window !== "undefined" && window.EF_FRAMEGEN?.renderOptions) return window.EF_FRAMEGEN.renderOptions.encoderOptions.video.framerate;
137
339
  return this.fps;
138
340
  }
139
341
  /**
140
- * Quantize a time value to the nearest frame boundary based on effectiveFps.
141
- * @param timeSeconds - Time in seconds
142
- * @returns Time quantized to frame boundaries in seconds
342
+ * Get the current content epoch (used by thumbnail cache).
343
+ * The epoch increments whenever visual content changes.
344
+ * @public
143
345
  */
144
- quantizeToFrameTime(timeSeconds) {
145
- const fps = this.effectiveFps;
146
- if (!fps || fps <= 0) return timeSeconds;
147
- const frameDurationS = 1 / fps;
148
- return Math.round(timeSeconds / frameDurationS) * frameDurationS;
346
+ get contentEpoch() {
347
+ return this.#contentEpoch;
149
348
  }
150
- async runThrottledFrameTask() {
349
+ /**
350
+ * Increment content epoch (called when visual content changes).
351
+ * This invalidates cached thumbnails by changing their cache keys.
352
+ * @public
353
+ */
354
+ incrementContentEpoch() {
355
+ this.#contentEpoch++;
356
+ }
357
+ async #runThrottledFrameTask() {
151
358
  if (this.playbackController) return this.playbackController.runThrottledFrameTask();
152
359
  await this.frameTask.run();
153
360
  }
361
+ /** @public */
154
362
  set currentTime(time) {
155
- time = this.quantizeToFrameTime(time);
363
+ const seekTarget = evaluateSeekTarget(time, this.durationMs, this.effectiveFps);
156
364
  if (this.playbackController) {
157
- this.playbackController.currentTime = time;
365
+ this.playbackController.currentTime = seekTarget;
366
+ this.#userTimeMs = seekTarget * 1e3;
158
367
  return;
159
368
  }
160
- time = Math.max(0, Math.min(this.durationMs / 1e3, time));
161
369
  if (!this.isRootTimegroup) return;
162
- if (Number.isNaN(time)) return;
163
- if (time === this.#currentTime && !this.#processingPendingSeek) return;
164
- if (this.#pendingSeekTime === time) return;
370
+ if (Number.isNaN(seekTarget)) return;
371
+ if (seekTarget === this.#currentTime && !this.#processingPendingSeek && !this.#restoringFromLocalStorage) return;
372
+ if (this.#pendingSeekTime === seekTarget) return;
373
+ if (this.#restoringFromLocalStorage && seekTarget !== this.#currentTime) {}
165
374
  if (this.#seekInProgress) {
166
- this.#pendingSeekTime = time;
167
- this.#currentTime = time;
375
+ this.#pendingSeekTime = seekTarget;
376
+ this.#currentTime = seekTarget;
377
+ this.#userTimeMs = seekTarget * 1e3;
168
378
  return;
169
379
  }
170
- this.#currentTime = time;
380
+ this.#currentTime = seekTarget;
381
+ this.#userTimeMs = seekTarget * 1e3;
171
382
  this.#seekInProgress = true;
172
- this.seekTask.run().finally(() => {
173
- if (this.#pendingSeekTime !== void 0 && this.#pendingSeekTime !== time) {
383
+ this.seekTask.run().catch(() => {}).finally(() => {
384
+ this.#seekInProgress = false;
385
+ if (this.#pendingSeekTime !== void 0 && this.#pendingSeekTime !== seekTarget) {
174
386
  const pendingTime = this.#pendingSeekTime;
175
387
  this.#pendingSeekTime = void 0;
176
388
  this.#processingPendingSeek = true;
@@ -182,49 +394,141 @@ let EFTimegroup = class EFTimegroup$1 extends EFTargetable(EFTemporal(TWMixin(Li
182
394
  } else this.#pendingSeekTime = void 0;
183
395
  });
184
396
  }
397
+ /** @public */
185
398
  get currentTime() {
186
399
  if (this.playbackController) return this.playbackController.currentTime;
187
400
  return this.#currentTime ?? 0;
188
401
  }
402
+ /** @public */
189
403
  set currentTimeMs(ms) {
190
404
  this.currentTime = ms / 1e3;
191
405
  }
406
+ /** @public */
192
407
  get currentTimeMs() {
193
408
  return this.currentTime * 1e3;
194
409
  }
195
410
  /**
411
+ * The time the user last requested via seek/scrub.
412
+ * Preview systems should use this instead of currentTimeMs to avoid
413
+ * seeing intermediate times during batch operations (thumbnails, export).
414
+ * @public
415
+ */
416
+ get userTimeMs() {
417
+ return this.#userTimeMs;
418
+ }
419
+ /**
196
420
  * Seek to a specific time and wait for all frames to be ready.
197
421
  * This is the recommended way to seek in tests and programmatic control.
198
422
  *
423
+ * Combines seeking (Purpose 3) with frame rendering (Purpose 4) to ensure
424
+ * all visible elements are ready after the seek completes.
425
+ *
426
+ * Updates both the source time AND userTimeMs (what the preview displays).
427
+ *
199
428
  * @param timeMs - Time in milliseconds to seek to
200
429
  * @returns Promise that resolves when the seek is complete and all visible children are ready
430
+ * @public
201
431
  */
202
432
  async seek(timeMs) {
433
+ this.#userTimeMs = timeMs;
203
434
  this.currentTimeMs = timeMs;
204
435
  await this.seekTask.taskComplete;
205
436
  if (this.playbackController) this.saveTimeToLocalStorage(this.currentTime);
206
437
  await this.frameTask.taskComplete;
207
- const visibleElements = deepGetElementsWithFrameTasks(this).filter((element) => {
208
- return evaluateAnimationVisibilityState(element).isVisible;
209
- });
438
+ const visibleElements = this.#evaluateVisibleElementsForFrame();
210
439
  await Promise.all(visibleElements.map(async (element) => {
211
440
  if ("waitForFrameReady" in element && typeof element.waitForFrameReady === "function") await element.waitForFrameReady();
212
441
  else await element.updateComplete;
213
442
  }));
214
443
  }
215
444
  /**
445
+ * Optimized seek for render loops.
446
+ * Unlike `seek()`, this:
447
+ * - Skips waitForMediaDurations (already loaded at render setup)
448
+ * - Skips localStorage persistence
449
+ * - Consolidates awaits to reduce event loop yields
450
+ *
451
+ * Still waits for all content to be ready (Lit updates, frame tasks, video frames).
452
+ *
453
+ * @param timeMs - Time in milliseconds to seek to
454
+ * @internal
455
+ */
456
+ async seekForRender(timeMs) {
457
+ const newTime = timeMs / 1e3;
458
+ this.#userTimeMs = timeMs;
459
+ this.#currentTime = newTime;
460
+ this.requestUpdate("currentTime");
461
+ await this.updateComplete;
462
+ const allLitElements = this.#getAllLitElementDescendants();
463
+ await Promise.all(allLitElements.map((el) => el.updateComplete));
464
+ const textElements = allLitElements.filter((el) => el.tagName === "EF-TEXT");
465
+ if (textElements.length > 0) await Promise.all(textElements.map((el) => {
466
+ if ("whenSegmentsReady" in el && typeof el.whenSegmentsReady === "function") return el.whenSegmentsReady();
467
+ return Promise.resolve();
468
+ }));
469
+ await new Promise((resolve) => requestAnimationFrame(resolve));
470
+ const visibleElements = this.#evaluateVisibleElementsForFrame();
471
+ await Promise.all([this.frameTask.run(), ...visibleElements.map((element) => {
472
+ if ("waitForFrameReady" in element && typeof element.waitForFrameReady === "function") return element.waitForFrameReady();
473
+ else return element.updateComplete;
474
+ })]);
475
+ this.offsetWidth;
476
+ }
477
+ /**
478
+ * Collects all LitElement descendants recursively.
479
+ * Used by seekForRender to ensure all reactive elements have updated.
480
+ */
481
+ #getAllLitElementDescendants() {
482
+ const result = [];
483
+ const walk = (el) => {
484
+ for (const child of el.children) {
485
+ if (child instanceof LitElement) result.push(child);
486
+ walk(child);
487
+ }
488
+ };
489
+ walk(this);
490
+ return result;
491
+ }
492
+ /**
216
493
  * Determines if this is a root timegroup (no parent timegroups)
494
+ * @public
217
495
  */
218
496
  get isRootTimegroup() {
219
497
  return !this.parentTimegroup;
220
498
  }
221
499
  /**
500
+ * Property-based frame task callback for React integration.
501
+ * When set, automatically registers the callback as a frame task.
502
+ * Setting a new value automatically cleans up the previous callback.
503
+ * Set to null or undefined to remove the callback.
504
+ *
505
+ * @example
506
+ * // React usage:
507
+ * <Timegroup onFrame={({ ownCurrentTimeMs, percentComplete }) => {
508
+ * // Per-frame updates
509
+ * }} />
510
+ *
511
+ * @public
512
+ */
513
+ get onFrame() {
514
+ return this.#onFrameCallback;
515
+ }
516
+ set onFrame(callback) {
517
+ if (this.#onFrameCleanup) {
518
+ this.#onFrameCleanup();
519
+ this.#onFrameCleanup = null;
520
+ }
521
+ this.#onFrameCallback = callback ?? null;
522
+ if (callback) this.#onFrameCleanup = this.addFrameTask(callback);
523
+ }
524
+ /**
222
525
  * Register a custom frame task callback that will be executed during frame rendering.
223
526
  * The callback receives timing information and can be async or sync.
224
527
  * Multiple callbacks can be registered and will execute in parallel.
225
528
  *
226
529
  * @param callback - Function to execute on each frame
227
530
  * @returns A cleanup function that removes the callback when called
531
+ * @public
228
532
  */
229
533
  addFrameTask(callback) {
230
534
  if (typeof callback !== "function") throw new Error("Frame task callback must be a function");
@@ -237,10 +541,12 @@ let EFTimegroup = class EFTimegroup$1 extends EFTargetable(EFTemporal(TWMixin(Li
237
541
  * Remove a previously registered custom frame task callback.
238
542
  *
239
543
  * @param callback - The callback function to remove
544
+ * @public
240
545
  */
241
546
  removeFrameTask(callback) {
242
547
  this.#customFrameTasks.delete(callback);
243
548
  }
549
+ /** @internal */
244
550
  saveTimeToLocalStorage(time) {
245
551
  try {
246
552
  if (this.id && this.isConnected && !Number.isNaN(time)) localStorage.setItem(this.storageKey, time.toString());
@@ -257,130 +563,397 @@ let EFTimegroup = class EFTimegroup$1 extends EFTargetable(EFTemporal(TWMixin(Li
257
563
  flushStartTimeMsCache();
258
564
  this.requestUpdate();
259
565
  };
566
+ /** @internal */
260
567
  loadTimeFromLocalStorage() {
261
568
  if (this.id) try {
262
569
  const storedValue = localStorage.getItem(this.storageKey);
263
570
  if (storedValue === null) return;
264
- return Number.parseFloat(storedValue);
571
+ const parsedValue = Number.parseFloat(storedValue);
572
+ if (Number.isNaN(parsedValue) || !Number.isFinite(parsedValue)) return;
573
+ return parsedValue;
265
574
  } catch (error) {
266
575
  log("Failed to load time from localStorage", error);
267
576
  }
268
577
  }
269
578
  connectedCallback() {
270
579
  super.connectedCallback();
271
- if (!this.playbackController) this.waitForMediaDurations().then(async () => {
272
- let didLoadFromStorage = false;
273
- if (this.id) {
274
- const maybeLoadedTime = this.loadTimeFromLocalStorage();
275
- if (maybeLoadedTime !== void 0) {
276
- this.currentTime = maybeLoadedTime;
277
- didLoadFromStorage = true;
278
- }
279
- }
280
- if (EF_INTERACTIVE && this.seekTask.status === TaskStatus.INITIAL) this.seekTask.run();
281
- else if (didLoadFromStorage) await this.seekTask.run();
580
+ requestAnimationFrame(() => {
581
+ requestAnimationFrame(() => {
582
+ if (this.parentTimegroup) new TimegroupController(this.parentTimegroup, this);
583
+ if (this.shouldWrapWithWorkbench()) this.wrapWithWorkbench();
584
+ });
282
585
  });
283
- if (this.parentTimegroup) new TimegroupController(this.parentTimegroup, this);
284
- if (this.shouldWrapWithWorkbench()) this.wrapWithWorkbench();
586
+ }
587
+ /**
588
+ * Called when this timegroup becomes a root (no parent timegroup).
589
+ * Sets up the playback listener after PlaybackController is created.
590
+ * @internal
591
+ */
592
+ didBecomeRoot() {
593
+ super.didBecomeRoot();
594
+ this.#setupPlaybackListener();
595
+ }
596
+ /**
597
+ * Setup listener on playbackController to sync userTimeMs during playback.
598
+ */
599
+ #setupPlaybackListener() {
600
+ if (this.#playbackListener || !this.playbackController) return;
601
+ this.#playbackListener = (event) => {
602
+ if (event.property === "currentTimeMs" && typeof event.value === "number") {
603
+ if (this.playing) this.#userTimeMs = event.value;
604
+ }
605
+ };
606
+ this.playbackController.addListener(this.#playbackListener);
607
+ }
608
+ /**
609
+ * Remove playback listener on disconnect.
610
+ */
611
+ #removePlaybackListener() {
612
+ if (this.#playbackListener && this.playbackController) this.playbackController.removeListener(this.#playbackListener);
613
+ this.#playbackListener = null;
285
614
  }
286
615
  #previousDurationMs = 0;
287
616
  updated(changedProperties) {
288
617
  super.updated(changedProperties);
289
- if (changedProperties.has("mode") || changedProperties.has("overlapMs")) sequenceDurationCache.delete(this);
618
+ if (changedProperties.has("mode") || changedProperties.has("overlapMs")) durationCache.delete(this);
290
619
  if (this.#previousDurationMs !== this.durationMs) {
291
620
  this.#previousDurationMs = this.durationMs;
292
- this.runThrottledFrameTask();
621
+ this.#runThrottledFrameTask();
293
622
  }
294
623
  }
295
624
  disconnectedCallback() {
296
625
  super.disconnectedCallback();
297
626
  this.#resizeObserver?.disconnect();
627
+ this.#removePlaybackListener();
628
+ }
629
+ /**
630
+ * Capture the timegroup at a specific timestamp as a canvas.
631
+ * Does NOT modify currentTimeMs - captures are rendered independently.
632
+ *
633
+ * @param options - Capture options including timeMs, scale, contentReadyMode
634
+ * @returns Promise resolving to an HTMLCanvasElement with the captured frame
635
+ * @public
636
+ */
637
+ async captureAtTime(options) {
638
+ return captureTimegroupAtTime(this, options);
639
+ }
640
+ /**
641
+ * Capture multiple timestamps as canvas thumbnails in a single batch.
642
+ *
643
+ * CLONE-TIMELINE ARCHITECTURE:
644
+ * Creates a single render clone and reuses it across all captures.
645
+ * Prime-timeline is NEVER seeked - user can continue previewing/editing during capture.
646
+ *
647
+ * @param timestamps - Array of timestamps (in milliseconds) to capture
648
+ * @param options - Capture options (scale, contentReadyMode, blockingTimeoutMs)
649
+ * @returns Promise resolving to array of HTMLCanvasElements
650
+ * @public
651
+ */
652
+ async captureBatch(timestamps, options = {}) {
653
+ if (timestamps.length === 0) return [];
654
+ const { scale = .25, contentReadyMode = "immediate", blockingTimeoutMs = 5e3 } = options;
655
+ const batchStartTime = performance.now();
656
+ const cloneStartTime = performance.now();
657
+ const { clone: renderClone, container: renderContainer, cleanup: cleanupRenderClone } = await this.createRenderClone();
658
+ const cloneTime = performance.now() - cloneStartTime;
659
+ const prefetchStartTime = performance.now();
660
+ const videoElements = renderClone.querySelectorAll("ef-video");
661
+ if (videoElements.length > 0) await Promise.all(Array.from(videoElements).map((video) => video.prefetchScrubSegments(timestamps)));
662
+ const prefetchTime = performance.now() - prefetchStartTime;
663
+ const canvases = [];
664
+ let totalSeekTime = 0;
665
+ let totalCaptureTime = 0;
666
+ try {
667
+ for (let i = 0; i < timestamps.length; i++) {
668
+ const timeMs = timestamps[i];
669
+ const seekStart = performance.now();
670
+ await renderClone.seekForRender(timeMs);
671
+ totalSeekTime += performance.now() - seekStart;
672
+ const captureStart = performance.now();
673
+ const canvas = await captureFromClone(renderClone, renderContainer, {
674
+ scale,
675
+ contentReadyMode,
676
+ blockingTimeoutMs,
677
+ originalTimegroup: this
678
+ });
679
+ totalCaptureTime += performance.now() - captureStart;
680
+ canvases.push(canvas);
681
+ }
682
+ return canvases;
683
+ } finally {
684
+ const totalTime = performance.now() - batchStartTime;
685
+ console.log(`[captureBatch] ${timestamps.length} frames: clone=${cloneTime.toFixed(0)}ms, prefetch=${prefetchTime.toFixed(0)}ms, seek=${totalSeekTime.toFixed(0)}ms, capture=${totalCaptureTime.toFixed(0)}ms, total=${totalTime.toFixed(0)}ms`);
686
+ cleanupRenderClone();
687
+ }
688
+ }
689
+ /**
690
+ * Render the timegroup to an MP4 video file and trigger download.
691
+ * Captures each frame at the specified fps, encodes using WebCodecs via
692
+ * MediaBunny, and downloads the resulting video.
693
+ *
694
+ * @param options - Rendering options (fps, codec, bitrate, filename, etc.)
695
+ * @returns Promise that resolves when video is downloaded
696
+ * @public
697
+ */
698
+ async renderToVideo(options) {
699
+ return renderTimegroupToVideo(this, options);
700
+ }
701
+ /**
702
+ * Runs the initializer function with validation for synchronous execution and time budget.
703
+ * @throws Error if no initializer is set
704
+ * @throws Error if initializer returns a Promise (async not allowed)
705
+ * @throws Error if initializer takes more than INITIALIZER_ERROR_THRESHOLD_MS
706
+ * @internal
707
+ */
708
+ #runInitializer(cloneEl) {
709
+ if (!this.initializer) return;
710
+ const startTime = performance.now();
711
+ const result = this.initializer(cloneEl);
712
+ const elapsed = performance.now() - startTime;
713
+ if (result !== void 0 && result !== null && typeof result.then === "function") throw new Error("Timeline initializer must be synchronous. Do not return a Promise from the initializer function.");
714
+ if (elapsed > INITIALIZER_ERROR_THRESHOLD_MS) throw new Error(`Timeline initializer took ${elapsed.toFixed(1)}ms, exceeding the ${INITIALIZER_ERROR_THRESHOLD_MS}ms limit. Initializers must be fast - move expensive work outside the initializer.`);
715
+ if (elapsed > INITIALIZER_WARN_THRESHOLD_MS) console.warn(`[ef-timegroup] Initializer took ${elapsed.toFixed(1)}ms, exceeding ${INITIALIZER_WARN_THRESHOLD_MS}ms. Consider optimizing for better render performance.`);
298
716
  }
717
+ /**
718
+ * Copy captionsData property from original to clone.
719
+ * cloneNode() only copies attributes, not JavaScript properties.
720
+ * captionsData is often set via JS (e.g., captionsEl.captionsData = {...}),
721
+ * so we must manually copy it to the cloned elements.
722
+ * @internal
723
+ */
724
+ #copyCaptionsData(original, clone) {
725
+ const originalCaptions = original.querySelectorAll("ef-captions");
726
+ const cloneCaptions = clone.querySelectorAll("ef-captions");
727
+ for (let i = 0; i < originalCaptions.length && i < cloneCaptions.length; i++) {
728
+ const origCap = originalCaptions[i];
729
+ const cloneCap = cloneCaptions[i];
730
+ if (origCap.captionsData) cloneCap.captionsData = origCap.captionsData;
731
+ }
732
+ }
733
+ /**
734
+ * Copy ef-text _textContent property from original to cloned elements.
735
+ * This MUST be called BEFORE elements upgrade (before updateComplete)
736
+ * because splitText() runs in connectedCallback and will clear segments
737
+ * if _textContent is null/empty.
738
+ * @internal
739
+ */
740
+ #copyTextContent(original, clone) {
741
+ const originalTexts = original.querySelectorAll("ef-text");
742
+ const cloneTexts = clone.querySelectorAll("ef-text");
743
+ for (let i = 0; i < originalTexts.length && i < cloneTexts.length; i++) {
744
+ const origText = originalTexts[i];
745
+ const cloneText = cloneTexts[i];
746
+ if (origText._textContent !== void 0) cloneText._textContent = origText._textContent;
747
+ if (origText._templateElement !== void 0) cloneText._templateElement = origText._templateElement;
748
+ }
749
+ }
750
+ /**
751
+ * Copy ef-text-segment properties from original to cloned elements.
752
+ * segmentText and other properties are set via JS, not attributes,
753
+ * so we must manually copy them to the cloned elements.
754
+ * @internal
755
+ */
756
+ #copyTextSegmentData(original, clone) {
757
+ const originalSegments = original.querySelectorAll("ef-text-segment");
758
+ const cloneSegments = clone.querySelectorAll("ef-text-segment");
759
+ for (let i = 0; i < originalSegments.length && i < cloneSegments.length; i++) {
760
+ const origSeg = originalSegments[i];
761
+ const cloneSeg = cloneSegments[i];
762
+ if (origSeg.segmentText !== void 0) cloneSeg.segmentText = origSeg.segmentText;
763
+ if (origSeg.segmentIndex !== void 0) cloneSeg.segmentIndex = origSeg.segmentIndex;
764
+ if (origSeg.staggerOffsetMs !== void 0) cloneSeg.staggerOffsetMs = origSeg.staggerOffsetMs;
765
+ if (origSeg.segmentStartMs !== void 0) cloneSeg.segmentStartMs = origSeg.segmentStartMs;
766
+ if (origSeg.segmentEndMs !== void 0) cloneSeg.segmentEndMs = origSeg.segmentEndMs;
767
+ }
768
+ }
769
+ /**
770
+ * Wait for all ef-captions elements to have their data loaded.
771
+ * This is needed because EFCaptions is not an EFMedia, so waitForMediaDurations doesn't cover it.
772
+ * Used by createRenderClone to ensure captions are ready before rendering.
773
+ * @internal
774
+ */
775
+ async #waitForCaptionsData(root) {
776
+ const captionsElements = root.querySelectorAll("ef-captions");
777
+ if (captionsElements.length === 0) return;
778
+ const waitPromises = [];
779
+ for (const el of captionsElements) {
780
+ const task = el.unifiedCaptionsDataTask;
781
+ if (!task) continue;
782
+ if (task.status === TaskStatus.COMPLETE || task.status === TaskStatus.ERROR) continue;
783
+ if (task.status === TaskStatus.INITIAL) task.run().catch(() => {});
784
+ if (task.taskComplete) waitPromises.push(task.taskComplete);
785
+ }
786
+ if (waitPromises.length > 0) await Promise.all(waitPromises);
787
+ }
788
+ /**
789
+ * Create an independent clone of this timegroup for rendering.
790
+ * The clone is a fully functional ef-timegroup with its own animations
791
+ * and time state, isolated from the original (Prime-timeline).
792
+ *
793
+ * OPTIONAL: An initializer can be set via `timegroup.initializer = (tg) => { ... }`
794
+ * to re-run JavaScript setup (frame callbacks, React components) on each clone.
795
+ *
796
+ * This enables:
797
+ * - Rendering without affecting user's preview position
798
+ * - Concurrent renders with different clones
799
+ * - Re-running JavaScript setup on each clone (if initializer is provided)
800
+ *
801
+ * @returns Promise resolving to clone, container, and cleanup function
802
+ * @throws Error if initializer is async or takes too long
803
+ * @public
804
+ */
805
+ async createRenderClone() {
806
+ const container = document.createElement("div");
807
+ container.className = "ef-render-clone-container";
808
+ container.style.cssText = `
809
+ position: fixed;
810
+ left: -9999px;
811
+ top: 0;
812
+ width: ${this.offsetWidth || 1920}px;
813
+ height: ${this.offsetHeight || 1080}px;
814
+ pointer-events: none;
815
+ overflow: hidden;
816
+ `;
817
+ const cloneEl = this.cloneNode(true);
818
+ cloneEl.removeAttribute("id");
819
+ this.#copyCaptionsData(this, cloneEl);
820
+ this.#copyTextContent(this, cloneEl);
821
+ const originalConfig = this.closest("ef-configuration");
822
+ if (originalConfig) {
823
+ const configClone = originalConfig.cloneNode(false);
824
+ configClone.appendChild(cloneEl);
825
+ container.appendChild(configClone);
826
+ } else container.appendChild(cloneEl);
827
+ document.body.appendChild(container);
828
+ await cloneEl.updateComplete;
829
+ this.#copyTextSegmentData(this, cloneEl);
830
+ this.#runInitializer(cloneEl);
831
+ let actualClone = container.querySelector("ef-timegroup");
832
+ if (!actualClone) throw new Error("No ef-timegroup found after initializer. Ensure your initializer renders a Timegroup (React) or does not remove the cloned element (vanilla JS).");
833
+ await customElements.whenDefined("ef-timegroup");
834
+ customElements.upgrade(container);
835
+ actualClone = container.querySelector("ef-timegroup");
836
+ if (!actualClone) throw new Error("ef-timegroup element lost after upgrade");
837
+ await actualClone.updateComplete;
838
+ const setupParentChildRelationships = (parent, root) => {
839
+ for (const child of parent.children) if (child.tagName === "EF-TIMEGROUP") {
840
+ const childTG = child;
841
+ childTG.parentTimegroup = parent;
842
+ childTG.rootTimegroup = root;
843
+ childTG.lockRootTimegroup();
844
+ setupParentChildRelationships(childTG, root);
845
+ } else if ("parentTimegroup" in child && "rootTimegroup" in child) {
846
+ const temporal = child;
847
+ temporal.parentTimegroup = parent;
848
+ temporal.rootTimegroup = root;
849
+ if ("lockRootTimegroup" in temporal && typeof temporal.lockRootTimegroup === "function") temporal.lockRootTimegroup();
850
+ } else if (child instanceof Element) setupParentChildRelationshipsInContainer(child, parent, root);
851
+ };
852
+ const setupParentChildRelationshipsInContainer = (container$1, nearestParentTG, root) => {
853
+ for (const child of container$1.children) if (child.tagName === "EF-TIMEGROUP") {
854
+ const childTG = child;
855
+ childTG.parentTimegroup = nearestParentTG;
856
+ childTG.rootTimegroup = root;
857
+ childTG.lockRootTimegroup();
858
+ setupParentChildRelationships(childTG, root);
859
+ } else if ("parentTimegroup" in child && "rootTimegroup" in child) {
860
+ const temporal = child;
861
+ temporal.parentTimegroup = nearestParentTG;
862
+ temporal.rootTimegroup = root;
863
+ if ("lockRootTimegroup" in temporal && typeof temporal.lockRootTimegroup === "function") temporal.lockRootTimegroup();
864
+ } else if (child instanceof Element) setupParentChildRelationshipsInContainer(child, nearestParentTG, root);
865
+ };
866
+ actualClone.rootTimegroup = actualClone;
867
+ setupParentChildRelationships(actualClone, actualClone);
868
+ await actualClone.updateComplete;
869
+ actualClone.rootTimegroup = actualClone;
870
+ actualClone.lockRootTimegroup();
871
+ const finalizeRootTimegroup = (el) => {
872
+ if ("rootTimegroup" in el && "lockRootTimegroup" in el) {
873
+ el.rootTimegroup = actualClone;
874
+ el.lockRootTimegroup();
875
+ }
876
+ for (const child of el.children) finalizeRootTimegroup(child);
877
+ };
878
+ finalizeRootTimegroup(actualClone);
879
+ await actualClone.waitForMediaDurations();
880
+ await this.#waitForCaptionsData(actualClone);
881
+ if (actualClone.playbackController) {
882
+ actualClone.playbackController.remove();
883
+ actualClone.playbackController = void 0;
884
+ }
885
+ await actualClone.seek(0);
886
+ return {
887
+ clone: actualClone,
888
+ container,
889
+ cleanup: () => {
890
+ container.remove();
891
+ const reactRoot = actualClone._reactRoot;
892
+ if (reactRoot) queueMicrotask(() => {
893
+ reactRoot.unmount();
894
+ });
895
+ }
896
+ };
897
+ }
898
+ /** @internal */
299
899
  get storageKey() {
300
900
  if (!this.id) throw new Error("Timegroup must have an id to use localStorage.");
301
901
  return `ef-timegroup-${this.id}`;
302
902
  }
903
+ /** @internal */
303
904
  get intrinsicDurationMs() {
304
905
  if (this.hasExplicitDuration) return this.explicitDurationMs;
305
906
  }
907
+ /** @internal */
306
908
  get hasOwnDuration() {
307
- return this.mode === "contain" || this.mode === "sequence" || this.mode === "fixed" && this.hasExplicitDuration;
909
+ return hasOwnDurationForMode(this.mode, this.hasExplicitDuration);
308
910
  }
911
+ /** @public */
309
912
  get durationMs() {
310
- switch (this.mode) {
311
- case "fit":
312
- if (!this.parentTimegroup) return 0;
313
- return this.parentTimegroup.durationMs;
314
- case "fixed": return super.durationMs;
315
- case "sequence": {
316
- const cachedDuration = sequenceDurationCache.get(this);
317
- if (cachedDuration !== void 0) return cachedDuration;
318
- let duration = 0;
319
- this.childTemporals.forEach((child, index) => {
320
- if (child instanceof _EFTimegroup && child.mode === "fit") return;
321
- if (index > 0) duration -= this.overlapMs;
322
- duration += child.durationMs;
323
- });
324
- sequenceDurationCache.set(this, duration);
325
- return duration;
326
- }
327
- case "contain": {
328
- let maxDuration = 0;
329
- for (const child of this.childTemporals) {
330
- if (child instanceof _EFTimegroup && child.mode === "fit") continue;
331
- if (!child.hasOwnDuration) continue;
332
- maxDuration = Math.max(maxDuration, child.durationMs);
333
- }
334
- return maxDuration;
335
- }
336
- default: throw new Error(`Invalid time mode: ${this.mode}`);
337
- }
913
+ if (this.mode === "fixed") return super.durationMs;
914
+ const childTemporalsAsElements = this.childTemporals;
915
+ return evaluateDurationForMode(this, this.mode, childTemporalsAsElements);
338
916
  }
339
- async getPendingFrameTasks(signal) {
340
- await this.waitForNestedUpdates(signal);
341
- signal?.throwIfAborted();
342
- const temporals = deepGetElementsWithFrameTasks(this);
343
- const timelineTimeMs = (this.#pendingSeekTime ?? this.#currentTime ?? 0) * 1e3;
344
- const frameTasks = temporals.filter((temporal) => {
345
- if (!("startTimeMs" in temporal) || !("endTimeMs" in temporal)) return true;
346
- const epsilon = .001;
347
- const startTimeMs = temporal.startTimeMs;
348
- const endTimeMs = temporal.endTimeMs;
349
- const elementStartsBeforeEnd = startTimeMs <= timelineTimeMs + epsilon;
350
- const elementEndsAfterStart = temporal.tagName.toLowerCase() === "ef-timegroup" && !temporal.parentTimegroup ? endTimeMs >= timelineTimeMs : endTimeMs > timelineTimeMs;
351
- return elementStartsBeforeEnd && elementEndsAfterStart;
352
- }).map((temporal) => temporal.frameTask);
353
- frameTasks.forEach((task) => {
354
- task.run();
917
+ /**
918
+ * Evaluates which elements should be rendered in the current frame.
919
+ * Filters to only include temporally visible elements for frame processing.
920
+ * Uses animation-friendly visibility to prevent animation jumps at exact boundaries.
921
+ */
922
+ #evaluateVisibleElementsForFrame() {
923
+ return deepGetElementsWithFrameTasks(this).filter((element) => {
924
+ return evaluateAnimationVisibilityState(element).isVisible;
355
925
  });
356
- return frameTasks.filter((task) => task.status < TaskStatus.COMPLETE);
357
- }
358
- async waitForNestedUpdates(signal) {
359
- const limit = 10;
360
- let steps = 0;
361
- let isComplete = true;
362
- while (true) {
363
- steps++;
364
- if (steps > limit) throw new Error("Reached update depth limit.");
365
- isComplete = await this.updateComplete;
366
- signal?.throwIfAborted();
367
- if (isComplete) break;
368
- }
369
926
  }
370
- async waitForFrameTasks() {
927
+ /** @internal */
928
+ async waitForFrameTasks(signal) {
371
929
  return await withSpan("timegroup.waitForFrameTasks", {
372
930
  timegroupId: this.id || "unknown",
373
931
  mode: this.mode
374
932
  }, void 0, async (span) => {
933
+ signal?.throwIfAborted();
375
934
  const innerStart = performance.now();
376
935
  const temporalElements = deepGetElementsWithFrameTasks(this);
377
936
  if (isTracingEnabled()) span.setAttribute("temporalElementsCount", temporalElements.length);
378
- const visibleElements = temporalElements.filter((element) => {
379
- return evaluateAnimationVisibilityState(element).isVisible;
380
- });
937
+ signal?.throwIfAborted();
938
+ const visibleElements = this.#evaluateVisibleElementsForFrame();
381
939
  if (isTracingEnabled()) span.setAttribute("visibleElementsCount", visibleElements.length);
382
940
  const promiseStart = performance.now();
383
- await Promise.all(visibleElements.map((element) => element.frameTask.run()));
941
+ await Promise.all(visibleElements.map(async (element) => {
942
+ signal?.throwIfAborted();
943
+ try {
944
+ if ("waitForFrameReady" in element && typeof element.waitForFrameReady === "function") await element.waitForFrameReady();
945
+ else {
946
+ await element.updateComplete;
947
+ await element.frameTask.run();
948
+ }
949
+ } catch (error) {
950
+ if (error instanceof DOMException && error.name === "AbortError" || error instanceof Error && (error.name === "AbortError" || error.message?.includes("signal is aborted") || error.message?.includes("The user aborted a request"))) {
951
+ signal?.throwIfAborted();
952
+ return;
953
+ }
954
+ throw error;
955
+ }
956
+ }));
384
957
  const promiseEnd = performance.now();
385
958
  const innerEnd = performance.now();
386
959
  if (isTracingEnabled()) {
@@ -389,9 +962,21 @@ let EFTimegroup = class EFTimegroup$1 extends EFTargetable(EFTemporal(TWMixin(Li
389
962
  }
390
963
  });
391
964
  }
392
- async waitForMediaDurations() {
393
- if (!this.mediaDurationsPromise) this.mediaDurationsPromise = this.#waitForMediaDurations();
394
- return this.mediaDurationsPromise;
965
+ #mediaDurationsPromise = void 0;
966
+ /** @internal */
967
+ async waitForMediaDurations(signal) {
968
+ signal?.throwIfAborted();
969
+ if (!this.#mediaDurationsPromise) this.#mediaDurationsPromise = this.#waitForMediaDurations(signal).catch((err) => {
970
+ if (err instanceof DOMException && err.name === "AbortError") {
971
+ this.#mediaDurationsPromise = void 0;
972
+ throw err;
973
+ }
974
+ console.error(`[EFTimegroup] waitForMediaDurations failed for ${this.id || "unnamed"}:`, err);
975
+ this.#mediaDurationsPromise = void 0;
976
+ throw err;
977
+ });
978
+ if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
979
+ return this.#mediaDurationsPromise;
395
980
  }
396
981
  /**
397
982
  * Wait for all media elements to load their initial segments.
@@ -399,25 +984,96 @@ let EFTimegroup = class EFTimegroup$1 extends EFTargetable(EFTemporal(TWMixin(Li
399
984
  * that caused issues with constructing audio data. We had negative durations
400
985
  * in calculations and it was not clear why.
401
986
  */
402
- async #waitForMediaDurations() {
987
+ async #waitForMediaDurations(signal) {
403
988
  return withSpan("timegroup.waitForMediaDurations", {
404
989
  timegroupId: this.id || "unknown",
405
990
  mode: this.mode
406
991
  }, void 0, async (span) => {
407
- await this.updateComplete;
992
+ signal?.throwIfAborted();
993
+ await new Promise((resolve, reject) => {
994
+ if (signal?.aborted) {
995
+ reject(new DOMException("Aborted", "AbortError"));
996
+ return;
997
+ }
998
+ const abortHandler = () => {
999
+ clearTimeout(timeoutId);
1000
+ cancelAnimationFrame(rafId2);
1001
+ cancelAnimationFrame(rafId1);
1002
+ reject(new DOMException("Aborted", "AbortError"));
1003
+ };
1004
+ signal?.addEventListener("abort", abortHandler, { once: true });
1005
+ let rafId1;
1006
+ let rafId2;
1007
+ let timeoutId;
1008
+ rafId1 = requestAnimationFrame(() => {
1009
+ if (signal?.aborted) {
1010
+ reject(new DOMException("Aborted", "AbortError"));
1011
+ return;
1012
+ }
1013
+ rafId2 = requestAnimationFrame(() => {
1014
+ if (signal?.aborted) {
1015
+ reject(new DOMException("Aborted", "AbortError"));
1016
+ return;
1017
+ }
1018
+ timeoutId = setTimeout(() => {
1019
+ signal?.removeEventListener("abort", abortHandler);
1020
+ resolve();
1021
+ }, 10);
1022
+ });
1023
+ });
1024
+ });
1025
+ signal?.throwIfAborted();
408
1026
  const mediaElements = deepGetMediaElements(this);
409
1027
  if (isTracingEnabled()) span.setAttribute("mediaElementsCount", mediaElements.length);
410
- await Promise.all(mediaElements.map((m) => m.mediaEngineTask.value ? Promise.resolve() : m.mediaEngineTask.run()));
1028
+ signal?.throwIfAborted();
1029
+ const mediaLoadStart = Date.now();
1030
+ const MEDIA_LOAD_TIMEOUT_MS = 3e4;
1031
+ const loadPromises = mediaElements.map(async (m, index) => {
1032
+ signal?.throwIfAborted();
1033
+ const elementStart = Date.now();
1034
+ try {
1035
+ const status = m.mediaEngineTask.status;
1036
+ if (status === TaskStatus.COMPLETE || status === TaskStatus.ERROR) return;
1037
+ if (status === TaskStatus.INITIAL) m.mediaEngineTask.run().catch(() => {});
1038
+ const timeoutPromise = new Promise((_, reject) => {
1039
+ if (signal?.aborted) {
1040
+ reject(new DOMException("Aborted", "AbortError"));
1041
+ return;
1042
+ }
1043
+ const timeoutId = setTimeout(() => reject(/* @__PURE__ */ new Error(`Media element ${index} load timeout after ${MEDIA_LOAD_TIMEOUT_MS}ms`)), MEDIA_LOAD_TIMEOUT_MS);
1044
+ signal?.addEventListener("abort", () => {
1045
+ clearTimeout(timeoutId);
1046
+ reject(new DOMException("Aborted", "AbortError"));
1047
+ }, { once: true });
1048
+ });
1049
+ const taskPromise = m.mediaEngineTask.taskComplete;
1050
+ taskPromise.catch(() => {});
1051
+ await Promise.race([taskPromise, timeoutPromise]);
1052
+ } catch (error) {
1053
+ if (error instanceof DOMException && error.name === "AbortError") throw error;
1054
+ if (isTracingEnabled()) {
1055
+ const elementElapsed = Date.now() - elementStart;
1056
+ console.error(`[EFTimegroup] Media element ${index} failed after ${elementElapsed}ms:`, error);
1057
+ }
1058
+ }
1059
+ });
1060
+ const results = await Promise.allSettled(loadPromises);
1061
+ if (results.some((r) => r.status === "rejected" && r.reason instanceof DOMException && r.reason.name === "AbortError")) throw new DOMException("Aborted", "AbortError");
1062
+ const failures = results.filter((r) => r.status === "rejected");
1063
+ if (failures.length > 0 && isTracingEnabled()) {
1064
+ const mediaLoadElapsed = Date.now() - mediaLoadStart;
1065
+ console.warn(`[EFTimegroup] ${failures.length} media elements failed to load in ${mediaLoadElapsed}ms:`, failures.map((r) => r.status === "rejected" ? r.reason : null));
1066
+ }
411
1067
  flushStartTimeMsCache();
412
1068
  flushSequenceDurationCache();
413
- this.requestUpdate("currentTime");
414
- await this.updateComplete;
1069
+ setTimeout(() => this.requestUpdate("currentTime"), 0);
415
1070
  });
416
1071
  }
1072
+ /** @internal */
417
1073
  get childTemporals() {
418
1074
  return shallowGetTemporalElements(this);
419
1075
  }
420
- get contextProvider() {
1076
+ get #contextProvider() {
421
1077
  let parent = this.parentNode;
422
1078
  while (parent) {
423
1079
  if (isContextMixin(parent)) return parent;
@@ -430,34 +1086,59 @@ let EFTimegroup = class EFTimegroup$1 extends EFTargetable(EFTemporal(TWMixin(Li
430
1086
  *
431
1087
  * A timegroup should be wrapped with a workbench if:
432
1088
  * - It's being rendered (EF_RENDERING), OR
433
- * - It's in interactive mode (EF_INTERACTIVE) with the dev workbench flag set
1089
+ * - The workbench property is set to true
434
1090
  *
435
1091
  * If the timegroup is already wrapped in a context provider like ef-preview,
436
1092
  * it should NOT be wrapped in a workbench.
1093
+ * @internal
437
1094
  */
438
1095
  shouldWrapWithWorkbench() {
439
- if (EF_RENDERING?.() === true) return this.closest("ef-timegroup") === this && this.closest("ef-preview") === null && this.closest("ef-workbench") === null && this.closest("test-context") === null;
440
- if (!globalThis.EF_DEV_WORKBENCH) return false;
441
- return EF_INTERACTIVE && this.closest("ef-timegroup") === this && this.closest("ef-preview") === null && this.closest("ef-workbench") === null && this.closest("test-context") === null;
1096
+ if (!this.isRootTimegroup) return false;
1097
+ if (this.closest("ef-canvas") !== null) return false;
1098
+ if (this.closest("ef-preview") !== null || this.closest("ef-workbench") !== null || this.closest("ef-preview-context") !== null) return false;
1099
+ if (this.closest("test-context") !== null) return false;
1100
+ if (this.closest(".ef-render-clone-container") !== null) return false;
1101
+ if (EF_RENDERING?.() === true) return true;
1102
+ return this.workbench;
442
1103
  }
1104
+ /** @internal */
443
1105
  wrapWithWorkbench() {
444
1106
  const workbench = document.createElement("ef-workbench");
445
1107
  this.parentElement?.append(workbench);
446
- if (!this.hasAttribute("id")) this.setAttribute("id", "root-this");
447
- this.setAttribute("slot", "canvas");
448
- workbench.append(this);
1108
+ if (!this.hasAttribute("id")) this.setAttribute("id", "root-timegroup");
1109
+ const panZoom = document.createElement("ef-pan-zoom");
1110
+ panZoom.id = "workbench-panzoom";
1111
+ panZoom.setAttribute("slot", "canvas");
1112
+ panZoom.setAttribute("auto-fit", "");
1113
+ panZoom.style.width = "100%";
1114
+ panZoom.style.height = "100%";
1115
+ const rect = this.getBoundingClientRect();
1116
+ const canvas = document.createElement("ef-canvas");
1117
+ canvas.id = "workbench-canvas";
1118
+ canvas.style.width = `${Math.max(rect.width, 1920)}px`;
1119
+ canvas.style.height = `${Math.max(rect.height, 1080)}px`;
1120
+ canvas.style.display = "block";
1121
+ canvas.append(this);
1122
+ panZoom.append(canvas);
1123
+ workbench.append(panZoom);
1124
+ const hierarchy = document.createElement("ef-hierarchy");
1125
+ hierarchy.setAttribute("slot", "hierarchy");
1126
+ hierarchy.setAttribute("target", "workbench-canvas");
1127
+ hierarchy.setAttribute("header", "Scenes");
1128
+ workbench.append(hierarchy);
449
1129
  const filmstrip = document.createElement("ef-filmstrip");
450
1130
  filmstrip.setAttribute("slot", "timeline");
451
1131
  filmstrip.setAttribute("target", this.id);
452
1132
  workbench.append(filmstrip);
453
1133
  }
454
- get efElements() {
1134
+ get #efElements() {
455
1135
  return Array.from(this.querySelectorAll("ef-audio, ef-video, ef-image, ef-captions, ef-waveform"));
456
1136
  }
457
1137
  /**
458
1138
  * Returns media elements for playback audio rendering
459
1139
  * For standalone media, returns [this]; for timegroups, returns all descendants
460
1140
  * Used by PlaybackController for audio-driven playback
1141
+ * @internal
461
1142
  */
462
1143
  getMediaElements() {
463
1144
  return deepGetMediaElements(this);
@@ -466,15 +1147,16 @@ let EFTimegroup = class EFTimegroup$1 extends EFTargetable(EFTemporal(TWMixin(Li
466
1147
  * Render audio buffer for playback
467
1148
  * Called by PlaybackController during live playback
468
1149
  * Delegates to shared renderTemporalAudio utility for consistent behavior
1150
+ * @internal
469
1151
  */
470
- async renderAudio(fromMs, toMs) {
471
- return renderTemporalAudio(this, fromMs, toMs);
1152
+ async renderAudio(fromMs, toMs, signal) {
1153
+ return renderTemporalAudio(this, fromMs, toMs, signal);
472
1154
  }
473
1155
  /**
474
1156
  * TEMPORARY TEST METHOD: Renders audio and immediately plays it back
475
1157
  * Usage: timegroup.testPlayAudio(0, 5000) // Play first 5 seconds
476
1158
  */
477
- async testPlayAudio(fromMs, toMs) {
1159
+ async #testPlayAudio(fromMs, toMs) {
478
1160
  const renderedBuffer = await this.renderAudio(fromMs, toMs);
479
1161
  const playbackContext = new AudioContext();
480
1162
  const bufferSource = playbackContext.createBufferSource();
@@ -488,13 +1170,13 @@ let EFTimegroup = class EFTimegroup$1 extends EFTargetable(EFTemporal(TWMixin(Li
488
1170
  };
489
1171
  });
490
1172
  }
491
- async loadMd5Sums() {
492
- const efElements = this.efElements;
1173
+ async #loadMd5Sums() {
1174
+ const efElements = this.#efElements;
493
1175
  const loaderTasks = [];
494
1176
  for (const el of efElements) {
495
1177
  const md5SumLoader = el.md5SumLoader;
496
1178
  if (md5SumLoader instanceof Task) {
497
- md5SumLoader.run();
1179
+ md5SumLoader.run().catch(() => {});
498
1180
  loaderTasks.push(md5SumLoader.taskComplete);
499
1181
  }
500
1182
  }
@@ -503,6 +1185,14 @@ let EFTimegroup = class EFTimegroup$1 extends EFTargetable(EFTemporal(TWMixin(Li
503
1185
  if ("productionSrc" in el && el.productionSrc instanceof Function) el.setAttribute("src", el.productionSrc());
504
1186
  });
505
1187
  }
1188
+ #timegroupFrameTaskCount = 0;
1189
+ #timegroupFrameTaskLastReset = Date.now();
1190
+ static {
1191
+ this.TIMEGROUP_FRAME_TASK_THRESHOLD = 100;
1192
+ }
1193
+ static {
1194
+ this.TIMEGROUP_FRAME_TASK_RESET_MS = 1e3;
1195
+ }
506
1196
  async #executeCustomFrameTasks() {
507
1197
  if (this.#customFrameTasks.size > 0) {
508
1198
  const percentComplete = this.durationMs > 0 ? this.ownCurrentTimeMs / this.durationMs : 0;
@@ -516,10 +1206,41 @@ let EFTimegroup = class EFTimegroup$1 extends EFTargetable(EFTemporal(TWMixin(Li
516
1206
  await Promise.all(Array.from(this.#customFrameTasks).map((callback) => Promise.resolve(callback(frameInfo))));
517
1207
  }
518
1208
  }
1209
+ /**
1210
+ * Get container information for this timegroup.
1211
+ * Timegroups are always containers and can contain children.
1212
+ * Display mode is determined from computed styles.
1213
+ *
1214
+ * @public
1215
+ */
1216
+ getContainerInfo() {
1217
+ return {
1218
+ ...getContainerInfoFromElement(this),
1219
+ isContainer: true,
1220
+ canContainChildren: true
1221
+ };
1222
+ }
1223
+ /**
1224
+ * Get position information for this timegroup.
1225
+ * Returns computed bounds, transform, and rotation.
1226
+ *
1227
+ * @public
1228
+ */
1229
+ getPositionInfo() {
1230
+ return getPositionInfoFromElement(this);
1231
+ }
519
1232
  };
520
1233
  __decorate([provide({ context: timegroupContext })], EFTimegroup.prototype, "_timeGroupContext", void 0);
521
1234
  __decorate([provide({ context: efContext })], EFTimegroup.prototype, "efContext", void 0);
522
1235
  __decorate([property({ type: Number })], EFTimegroup.prototype, "fps", void 0);
1236
+ __decorate([property({
1237
+ type: Boolean,
1238
+ attribute: "auto-init"
1239
+ })], EFTimegroup.prototype, "autoInit", void 0);
1240
+ __decorate([property({
1241
+ type: Boolean,
1242
+ reflect: true
1243
+ })], EFTimegroup.prototype, "workbench", void 0);
523
1244
  __decorate([property({ type: String })], EFTimegroup.prototype, "fit", void 0);
524
1245
  __decorate([property({
525
1246
  type: Number,