@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.
Files changed (324) 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 +12 -2
  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 +118 -56
  117. package/dist/elements/EFThumbnailStrip.js +522 -358
  118. package/dist/elements/EFThumbnailStrip.js.map +1 -1
  119. package/dist/elements/EFTimegroup.d.ts +223 -27
  120. package/dist/elements/EFTimegroup.js +850 -147
  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 +152 -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 +11 -5
  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 +182 -4
  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 +840 -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/sandbox/PlaybackControls.js +10 -0
  304. package/dist/sandbox/PlaybackControls.js.map +1 -0
  305. package/dist/sandbox/ScenarioRunner.js +1 -0
  306. package/dist/sandbox/index.js +2 -0
  307. package/dist/style.css +68 -67
  308. package/dist/transcoding/types/index.d.ts +2 -1
  309. package/dist/transcoding/utils/UrlGenerator.d.ts +6 -1
  310. package/dist/transcoding/utils/UrlGenerator.js +12 -3
  311. package/dist/transcoding/utils/UrlGenerator.js.map +1 -1
  312. package/dist/utils/LRUCache.js +1 -375
  313. package/dist/utils/LRUCache.js.map +1 -1
  314. package/dist/utils/frameTime.js +14 -0
  315. package/dist/utils/frameTime.js.map +1 -0
  316. package/package.json +3 -3
  317. package/test/profilingPlugin.ts +223 -0
  318. package/test/recordReplayProxyPlugin.js +22 -27
  319. package/test/thumbnail-performance-test.html +116 -0
  320. package/test/visualRegressionUtils.ts +286 -0
  321. package/types.json +1 -1
  322. package/dist/elements/TimegroupController.d.ts +0 -18
  323. package/dist/msToTimeCode.js +0 -17
  324. 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,69 @@ 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;
125
311
  #currentTime = void 0;
312
+ #userTimeMs = 0;
126
313
  #seekInProgress = false;
127
314
  #pendingSeekTime;
128
315
  #processingPendingSeek = false;
316
+ #restoringFromLocalStorage = false;
317
+ /** @internal */
318
+ isRestoringFromLocalStorage() {
319
+ return this.#restoringFromLocalStorage;
320
+ }
321
+ /** @internal - Used by PlaybackController to set restoration state */
322
+ setRestoringFromLocalStorage(value) {
323
+ this.#restoringFromLocalStorage = value;
324
+ }
129
325
  #customFrameTasks = /* @__PURE__ */ new Set();
326
+ #onFrameCallback = null;
327
+ #onFrameCleanup = null;
328
+ #playbackListener = null;
130
329
  /**
131
330
  * Get the effective FPS for this timegroup.
132
331
  * During rendering, uses the render options FPS if available.
133
332
  * Otherwise uses the configured fps property.
333
+ * @public
134
334
  */
135
335
  get effectiveFps() {
136
336
  if (typeof window !== "undefined" && window.EF_FRAMEGEN?.renderOptions) return window.EF_FRAMEGEN.renderOptions.encoderOptions.video.framerate;
137
337
  return this.fps;
138
338
  }
139
- /**
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
143
- */
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;
149
- }
150
- async runThrottledFrameTask() {
339
+ async #runThrottledFrameTask() {
151
340
  if (this.playbackController) return this.playbackController.runThrottledFrameTask();
152
341
  await this.frameTask.run();
153
342
  }
343
+ /** @public */
154
344
  set currentTime(time) {
155
- time = this.quantizeToFrameTime(time);
345
+ const seekTarget = evaluateSeekTarget(time, this.durationMs, this.effectiveFps);
156
346
  if (this.playbackController) {
157
- this.playbackController.currentTime = time;
347
+ this.playbackController.currentTime = seekTarget;
348
+ this.#userTimeMs = seekTarget * 1e3;
158
349
  return;
159
350
  }
160
- time = Math.max(0, Math.min(this.durationMs / 1e3, time));
161
351
  if (!this.isRootTimegroup) return;
162
- if (Number.isNaN(time)) return;
163
- if (time === this.#currentTime && !this.#processingPendingSeek) return;
164
- if (this.#pendingSeekTime === time) return;
352
+ if (Number.isNaN(seekTarget)) return;
353
+ if (seekTarget === this.#currentTime && !this.#processingPendingSeek && !this.#restoringFromLocalStorage) return;
354
+ if (this.#pendingSeekTime === seekTarget) return;
355
+ if (this.#restoringFromLocalStorage && seekTarget !== this.#currentTime) {}
165
356
  if (this.#seekInProgress) {
166
- this.#pendingSeekTime = time;
167
- this.#currentTime = time;
357
+ this.#pendingSeekTime = seekTarget;
358
+ this.#currentTime = seekTarget;
359
+ this.#userTimeMs = seekTarget * 1e3;
168
360
  return;
169
361
  }
170
- this.#currentTime = time;
362
+ this.#currentTime = seekTarget;
363
+ this.#userTimeMs = seekTarget * 1e3;
171
364
  this.#seekInProgress = true;
172
- this.seekTask.run().finally(() => {
173
- if (this.#pendingSeekTime !== void 0 && this.#pendingSeekTime !== time) {
365
+ this.seekTask.run().catch(() => {}).finally(() => {
366
+ this.#seekInProgress = false;
367
+ if (this.#pendingSeekTime !== void 0 && this.#pendingSeekTime !== seekTarget) {
174
368
  const pendingTime = this.#pendingSeekTime;
175
369
  this.#pendingSeekTime = void 0;
176
370
  this.#processingPendingSeek = true;
@@ -182,49 +376,141 @@ let EFTimegroup = class EFTimegroup$1 extends EFTargetable(EFTemporal(TWMixin(Li
182
376
  } else this.#pendingSeekTime = void 0;
183
377
  });
184
378
  }
379
+ /** @public */
185
380
  get currentTime() {
186
381
  if (this.playbackController) return this.playbackController.currentTime;
187
382
  return this.#currentTime ?? 0;
188
383
  }
384
+ /** @public */
189
385
  set currentTimeMs(ms) {
190
386
  this.currentTime = ms / 1e3;
191
387
  }
388
+ /** @public */
192
389
  get currentTimeMs() {
193
390
  return this.currentTime * 1e3;
194
391
  }
195
392
  /**
393
+ * The time the user last requested via seek/scrub.
394
+ * Preview systems should use this instead of currentTimeMs to avoid
395
+ * seeing intermediate times during batch operations (thumbnails, export).
396
+ * @public
397
+ */
398
+ get userTimeMs() {
399
+ return this.#userTimeMs;
400
+ }
401
+ /**
196
402
  * Seek to a specific time and wait for all frames to be ready.
197
403
  * This is the recommended way to seek in tests and programmatic control.
198
404
  *
405
+ * Combines seeking (Purpose 3) with frame rendering (Purpose 4) to ensure
406
+ * all visible elements are ready after the seek completes.
407
+ *
408
+ * Updates both the source time AND userTimeMs (what the preview displays).
409
+ *
199
410
  * @param timeMs - Time in milliseconds to seek to
200
411
  * @returns Promise that resolves when the seek is complete and all visible children are ready
412
+ * @public
201
413
  */
202
414
  async seek(timeMs) {
415
+ this.#userTimeMs = timeMs;
203
416
  this.currentTimeMs = timeMs;
204
417
  await this.seekTask.taskComplete;
205
418
  if (this.playbackController) this.saveTimeToLocalStorage(this.currentTime);
206
419
  await this.frameTask.taskComplete;
207
- const visibleElements = deepGetElementsWithFrameTasks(this).filter((element) => {
208
- return evaluateAnimationVisibilityState(element).isVisible;
209
- });
420
+ const visibleElements = this.#evaluateVisibleElementsForFrame();
210
421
  await Promise.all(visibleElements.map(async (element) => {
211
422
  if ("waitForFrameReady" in element && typeof element.waitForFrameReady === "function") await element.waitForFrameReady();
212
423
  else await element.updateComplete;
213
424
  }));
214
425
  }
215
426
  /**
427
+ * Optimized seek for render loops.
428
+ * Unlike `seek()`, this:
429
+ * - Skips waitForMediaDurations (already loaded at render setup)
430
+ * - Skips localStorage persistence
431
+ * - Consolidates awaits to reduce event loop yields
432
+ *
433
+ * Still waits for all content to be ready (Lit updates, frame tasks, video frames).
434
+ *
435
+ * @param timeMs - Time in milliseconds to seek to
436
+ * @internal
437
+ */
438
+ async seekForRender(timeMs) {
439
+ const newTime = timeMs / 1e3;
440
+ this.#userTimeMs = timeMs;
441
+ this.#currentTime = newTime;
442
+ this.requestUpdate("currentTime");
443
+ await this.updateComplete;
444
+ const allLitElements = this.#getAllLitElementDescendants();
445
+ await Promise.all(allLitElements.map((el) => el.updateComplete));
446
+ const textElements = allLitElements.filter((el) => el.tagName === "EF-TEXT");
447
+ if (textElements.length > 0) await Promise.all(textElements.map((el) => {
448
+ if ("whenSegmentsReady" in el && typeof el.whenSegmentsReady === "function") return el.whenSegmentsReady();
449
+ return Promise.resolve();
450
+ }));
451
+ await new Promise((resolve) => requestAnimationFrame(resolve));
452
+ const visibleElements = this.#evaluateVisibleElementsForFrame();
453
+ await Promise.all([this.frameTask.run(), ...visibleElements.map((element) => {
454
+ if ("waitForFrameReady" in element && typeof element.waitForFrameReady === "function") return element.waitForFrameReady();
455
+ else return element.updateComplete;
456
+ })]);
457
+ this.offsetWidth;
458
+ }
459
+ /**
460
+ * Collects all LitElement descendants recursively.
461
+ * Used by seekForRender to ensure all reactive elements have updated.
462
+ */
463
+ #getAllLitElementDescendants() {
464
+ const result = [];
465
+ const walk = (el) => {
466
+ for (const child of el.children) {
467
+ if (child instanceof LitElement) result.push(child);
468
+ walk(child);
469
+ }
470
+ };
471
+ walk(this);
472
+ return result;
473
+ }
474
+ /**
216
475
  * Determines if this is a root timegroup (no parent timegroups)
476
+ * @public
217
477
  */
218
478
  get isRootTimegroup() {
219
479
  return !this.parentTimegroup;
220
480
  }
221
481
  /**
482
+ * Property-based frame task callback for React integration.
483
+ * When set, automatically registers the callback as a frame task.
484
+ * Setting a new value automatically cleans up the previous callback.
485
+ * Set to null or undefined to remove the callback.
486
+ *
487
+ * @example
488
+ * // React usage:
489
+ * <Timegroup onFrame={({ ownCurrentTimeMs, percentComplete }) => {
490
+ * // Per-frame updates
491
+ * }} />
492
+ *
493
+ * @public
494
+ */
495
+ get onFrame() {
496
+ return this.#onFrameCallback;
497
+ }
498
+ set onFrame(callback) {
499
+ if (this.#onFrameCleanup) {
500
+ this.#onFrameCleanup();
501
+ this.#onFrameCleanup = null;
502
+ }
503
+ this.#onFrameCallback = callback ?? null;
504
+ if (callback) this.#onFrameCleanup = this.addFrameTask(callback);
505
+ }
506
+ /**
222
507
  * Register a custom frame task callback that will be executed during frame rendering.
223
508
  * The callback receives timing information and can be async or sync.
224
509
  * Multiple callbacks can be registered and will execute in parallel.
225
510
  *
226
511
  * @param callback - Function to execute on each frame
227
512
  * @returns A cleanup function that removes the callback when called
513
+ * @public
228
514
  */
229
515
  addFrameTask(callback) {
230
516
  if (typeof callback !== "function") throw new Error("Frame task callback must be a function");
@@ -237,10 +523,12 @@ let EFTimegroup = class EFTimegroup$1 extends EFTargetable(EFTemporal(TWMixin(Li
237
523
  * Remove a previously registered custom frame task callback.
238
524
  *
239
525
  * @param callback - The callback function to remove
526
+ * @public
240
527
  */
241
528
  removeFrameTask(callback) {
242
529
  this.#customFrameTasks.delete(callback);
243
530
  }
531
+ /** @internal */
244
532
  saveTimeToLocalStorage(time) {
245
533
  try {
246
534
  if (this.id && this.isConnected && !Number.isNaN(time)) localStorage.setItem(this.storageKey, time.toString());
@@ -257,130 +545,397 @@ let EFTimegroup = class EFTimegroup$1 extends EFTargetable(EFTemporal(TWMixin(Li
257
545
  flushStartTimeMsCache();
258
546
  this.requestUpdate();
259
547
  };
548
+ /** @internal */
260
549
  loadTimeFromLocalStorage() {
261
550
  if (this.id) try {
262
551
  const storedValue = localStorage.getItem(this.storageKey);
263
552
  if (storedValue === null) return;
264
- return Number.parseFloat(storedValue);
553
+ const parsedValue = Number.parseFloat(storedValue);
554
+ if (Number.isNaN(parsedValue) || !Number.isFinite(parsedValue)) return;
555
+ return parsedValue;
265
556
  } catch (error) {
266
557
  log("Failed to load time from localStorage", error);
267
558
  }
268
559
  }
269
560
  connectedCallback() {
270
561
  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();
562
+ requestAnimationFrame(() => {
563
+ requestAnimationFrame(() => {
564
+ if (this.parentTimegroup) new TimegroupController(this.parentTimegroup, this);
565
+ if (this.shouldWrapWithWorkbench()) this.wrapWithWorkbench();
566
+ });
282
567
  });
283
- if (this.parentTimegroup) new TimegroupController(this.parentTimegroup, this);
284
- if (this.shouldWrapWithWorkbench()) this.wrapWithWorkbench();
568
+ }
569
+ /**
570
+ * Called when this timegroup becomes a root (no parent timegroup).
571
+ * Sets up the playback listener after PlaybackController is created.
572
+ * @internal
573
+ */
574
+ didBecomeRoot() {
575
+ super.didBecomeRoot();
576
+ this.#setupPlaybackListener();
577
+ }
578
+ /**
579
+ * Setup listener on playbackController to sync userTimeMs during playback.
580
+ */
581
+ #setupPlaybackListener() {
582
+ if (this.#playbackListener || !this.playbackController) return;
583
+ this.#playbackListener = (event) => {
584
+ if (event.property === "currentTimeMs" && typeof event.value === "number") {
585
+ if (this.playing) this.#userTimeMs = event.value;
586
+ }
587
+ };
588
+ this.playbackController.addListener(this.#playbackListener);
589
+ }
590
+ /**
591
+ * Remove playback listener on disconnect.
592
+ */
593
+ #removePlaybackListener() {
594
+ if (this.#playbackListener && this.playbackController) this.playbackController.removeListener(this.#playbackListener);
595
+ this.#playbackListener = null;
285
596
  }
286
597
  #previousDurationMs = 0;
287
598
  updated(changedProperties) {
288
599
  super.updated(changedProperties);
289
- if (changedProperties.has("mode") || changedProperties.has("overlapMs")) sequenceDurationCache.delete(this);
600
+ if (changedProperties.has("mode") || changedProperties.has("overlapMs")) durationCache.delete(this);
290
601
  if (this.#previousDurationMs !== this.durationMs) {
291
602
  this.#previousDurationMs = this.durationMs;
292
- this.runThrottledFrameTask();
603
+ this.#runThrottledFrameTask();
293
604
  }
294
605
  }
295
606
  disconnectedCallback() {
296
607
  super.disconnectedCallback();
297
608
  this.#resizeObserver?.disconnect();
609
+ this.#removePlaybackListener();
610
+ }
611
+ /**
612
+ * Capture the timegroup at a specific timestamp as a canvas.
613
+ * Does NOT modify currentTimeMs - captures are rendered independently.
614
+ *
615
+ * @param options - Capture options including timeMs, scale, contentReadyMode
616
+ * @returns Promise resolving to an HTMLCanvasElement with the captured frame
617
+ * @public
618
+ */
619
+ async captureAtTime(options) {
620
+ return captureTimegroupAtTime(this, options);
621
+ }
622
+ /**
623
+ * Capture multiple timestamps as canvas thumbnails in a single batch.
624
+ *
625
+ * CLONE-TIMELINE ARCHITECTURE:
626
+ * Creates a single render clone and reuses it across all captures.
627
+ * Prime-timeline is NEVER seeked - user can continue previewing/editing during capture.
628
+ *
629
+ * @param timestamps - Array of timestamps (in milliseconds) to capture
630
+ * @param options - Capture options (scale, contentReadyMode, blockingTimeoutMs)
631
+ * @returns Promise resolving to array of HTMLCanvasElements
632
+ * @public
633
+ */
634
+ async captureBatch(timestamps, options = {}) {
635
+ if (timestamps.length === 0) return [];
636
+ const { scale = .25, contentReadyMode = "immediate", blockingTimeoutMs = 5e3 } = options;
637
+ const batchStartTime = performance.now();
638
+ const cloneStartTime = performance.now();
639
+ const { clone: renderClone, container: renderContainer, cleanup: cleanupRenderClone } = await this.createRenderClone();
640
+ const cloneTime = performance.now() - cloneStartTime;
641
+ const prefetchStartTime = performance.now();
642
+ const videoElements = renderClone.querySelectorAll("ef-video");
643
+ if (videoElements.length > 0) await Promise.all(Array.from(videoElements).map((video) => video.prefetchScrubSegments(timestamps)));
644
+ const prefetchTime = performance.now() - prefetchStartTime;
645
+ const canvases = [];
646
+ let totalSeekTime = 0;
647
+ let totalCaptureTime = 0;
648
+ try {
649
+ for (let i = 0; i < timestamps.length; i++) {
650
+ const timeMs = timestamps[i];
651
+ const seekStart = performance.now();
652
+ await renderClone.seekForRender(timeMs);
653
+ totalSeekTime += performance.now() - seekStart;
654
+ const captureStart = performance.now();
655
+ const canvas = await captureFromClone(renderClone, renderContainer, {
656
+ scale,
657
+ contentReadyMode,
658
+ blockingTimeoutMs,
659
+ originalTimegroup: this
660
+ });
661
+ totalCaptureTime += performance.now() - captureStart;
662
+ canvases.push(canvas);
663
+ }
664
+ return canvases;
665
+ } finally {
666
+ const totalTime = performance.now() - batchStartTime;
667
+ 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`);
668
+ cleanupRenderClone();
669
+ }
670
+ }
671
+ /**
672
+ * Render the timegroup to an MP4 video file and trigger download.
673
+ * Captures each frame at the specified fps, encodes using WebCodecs via
674
+ * MediaBunny, and downloads the resulting video.
675
+ *
676
+ * @param options - Rendering options (fps, codec, bitrate, filename, etc.)
677
+ * @returns Promise that resolves when video is downloaded
678
+ * @public
679
+ */
680
+ async renderToVideo(options) {
681
+ return renderTimegroupToVideo(this, options);
682
+ }
683
+ /**
684
+ * Runs the initializer function with validation for synchronous execution and time budget.
685
+ * @throws Error if no initializer is set
686
+ * @throws Error if initializer returns a Promise (async not allowed)
687
+ * @throws Error if initializer takes more than INITIALIZER_ERROR_THRESHOLD_MS
688
+ * @internal
689
+ */
690
+ #runInitializer(cloneEl) {
691
+ if (!this.initializer) return;
692
+ const startTime = performance.now();
693
+ const result = this.initializer(cloneEl);
694
+ const elapsed = performance.now() - startTime;
695
+ 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.");
696
+ 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.`);
697
+ 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
698
  }
699
+ /**
700
+ * Copy captionsData property from original to clone.
701
+ * cloneNode() only copies attributes, not JavaScript properties.
702
+ * captionsData is often set via JS (e.g., captionsEl.captionsData = {...}),
703
+ * so we must manually copy it to the cloned elements.
704
+ * @internal
705
+ */
706
+ #copyCaptionsData(original, clone) {
707
+ const originalCaptions = original.querySelectorAll("ef-captions");
708
+ const cloneCaptions = clone.querySelectorAll("ef-captions");
709
+ for (let i = 0; i < originalCaptions.length && i < cloneCaptions.length; i++) {
710
+ const origCap = originalCaptions[i];
711
+ const cloneCap = cloneCaptions[i];
712
+ if (origCap.captionsData) cloneCap.captionsData = origCap.captionsData;
713
+ }
714
+ }
715
+ /**
716
+ * Copy ef-text _textContent property from original to cloned elements.
717
+ * This MUST be called BEFORE elements upgrade (before updateComplete)
718
+ * because splitText() runs in connectedCallback and will clear segments
719
+ * if _textContent is null/empty.
720
+ * @internal
721
+ */
722
+ #copyTextContent(original, clone) {
723
+ const originalTexts = original.querySelectorAll("ef-text");
724
+ const cloneTexts = clone.querySelectorAll("ef-text");
725
+ for (let i = 0; i < originalTexts.length && i < cloneTexts.length; i++) {
726
+ const origText = originalTexts[i];
727
+ const cloneText = cloneTexts[i];
728
+ if (origText._textContent !== void 0) cloneText._textContent = origText._textContent;
729
+ if (origText._templateElement !== void 0) cloneText._templateElement = origText._templateElement;
730
+ }
731
+ }
732
+ /**
733
+ * Copy ef-text-segment properties from original to cloned elements.
734
+ * segmentText and other properties are set via JS, not attributes,
735
+ * so we must manually copy them to the cloned elements.
736
+ * @internal
737
+ */
738
+ #copyTextSegmentData(original, clone) {
739
+ const originalSegments = original.querySelectorAll("ef-text-segment");
740
+ const cloneSegments = clone.querySelectorAll("ef-text-segment");
741
+ for (let i = 0; i < originalSegments.length && i < cloneSegments.length; i++) {
742
+ const origSeg = originalSegments[i];
743
+ const cloneSeg = cloneSegments[i];
744
+ if (origSeg.segmentText !== void 0) cloneSeg.segmentText = origSeg.segmentText;
745
+ if (origSeg.segmentIndex !== void 0) cloneSeg.segmentIndex = origSeg.segmentIndex;
746
+ if (origSeg.staggerOffsetMs !== void 0) cloneSeg.staggerOffsetMs = origSeg.staggerOffsetMs;
747
+ if (origSeg.segmentStartMs !== void 0) cloneSeg.segmentStartMs = origSeg.segmentStartMs;
748
+ if (origSeg.segmentEndMs !== void 0) cloneSeg.segmentEndMs = origSeg.segmentEndMs;
749
+ }
750
+ }
751
+ /**
752
+ * Wait for all ef-captions elements to have their data loaded.
753
+ * This is needed because EFCaptions is not an EFMedia, so waitForMediaDurations doesn't cover it.
754
+ * Used by createRenderClone to ensure captions are ready before rendering.
755
+ * @internal
756
+ */
757
+ async #waitForCaptionsData(root) {
758
+ const captionsElements = root.querySelectorAll("ef-captions");
759
+ if (captionsElements.length === 0) return;
760
+ const waitPromises = [];
761
+ for (const el of captionsElements) {
762
+ const task = el.unifiedCaptionsDataTask;
763
+ if (!task) continue;
764
+ if (task.status === TaskStatus.COMPLETE || task.status === TaskStatus.ERROR) continue;
765
+ if (task.status === TaskStatus.INITIAL) task.run().catch(() => {});
766
+ if (task.taskComplete) waitPromises.push(task.taskComplete);
767
+ }
768
+ if (waitPromises.length > 0) await Promise.all(waitPromises);
769
+ }
770
+ /**
771
+ * Create an independent clone of this timegroup for rendering.
772
+ * The clone is a fully functional ef-timegroup with its own animations
773
+ * and time state, isolated from the original (Prime-timeline).
774
+ *
775
+ * OPTIONAL: An initializer can be set via `timegroup.initializer = (tg) => { ... }`
776
+ * to re-run JavaScript setup (frame callbacks, React components) on each clone.
777
+ *
778
+ * This enables:
779
+ * - Rendering without affecting user's preview position
780
+ * - Concurrent renders with different clones
781
+ * - Re-running JavaScript setup on each clone (if initializer is provided)
782
+ *
783
+ * @returns Promise resolving to clone, container, and cleanup function
784
+ * @throws Error if initializer is async or takes too long
785
+ * @public
786
+ */
787
+ async createRenderClone() {
788
+ const container = document.createElement("div");
789
+ container.className = "ef-render-clone-container";
790
+ container.style.cssText = `
791
+ position: fixed;
792
+ left: -9999px;
793
+ top: 0;
794
+ width: ${this.offsetWidth || 1920}px;
795
+ height: ${this.offsetHeight || 1080}px;
796
+ pointer-events: none;
797
+ overflow: hidden;
798
+ `;
799
+ const cloneEl = this.cloneNode(true);
800
+ cloneEl.removeAttribute("id");
801
+ this.#copyCaptionsData(this, cloneEl);
802
+ this.#copyTextContent(this, cloneEl);
803
+ const originalConfig = this.closest("ef-configuration");
804
+ if (originalConfig) {
805
+ const configClone = originalConfig.cloneNode(false);
806
+ configClone.appendChild(cloneEl);
807
+ container.appendChild(configClone);
808
+ } else container.appendChild(cloneEl);
809
+ document.body.appendChild(container);
810
+ await cloneEl.updateComplete;
811
+ this.#copyTextSegmentData(this, cloneEl);
812
+ this.#runInitializer(cloneEl);
813
+ let actualClone = container.querySelector("ef-timegroup");
814
+ 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).");
815
+ await customElements.whenDefined("ef-timegroup");
816
+ customElements.upgrade(container);
817
+ actualClone = container.querySelector("ef-timegroup");
818
+ if (!actualClone) throw new Error("ef-timegroup element lost after upgrade");
819
+ await actualClone.updateComplete;
820
+ const setupParentChildRelationships = (parent, root) => {
821
+ for (const child of parent.children) if (child.tagName === "EF-TIMEGROUP") {
822
+ const childTG = child;
823
+ childTG.parentTimegroup = parent;
824
+ childTG.rootTimegroup = root;
825
+ childTG.lockRootTimegroup();
826
+ setupParentChildRelationships(childTG, root);
827
+ } else if ("parentTimegroup" in child && "rootTimegroup" in child) {
828
+ const temporal = child;
829
+ temporal.parentTimegroup = parent;
830
+ temporal.rootTimegroup = root;
831
+ if ("lockRootTimegroup" in temporal && typeof temporal.lockRootTimegroup === "function") temporal.lockRootTimegroup();
832
+ } else if (child instanceof Element) setupParentChildRelationshipsInContainer(child, parent, root);
833
+ };
834
+ const setupParentChildRelationshipsInContainer = (container$1, nearestParentTG, root) => {
835
+ for (const child of container$1.children) if (child.tagName === "EF-TIMEGROUP") {
836
+ const childTG = child;
837
+ childTG.parentTimegroup = nearestParentTG;
838
+ childTG.rootTimegroup = root;
839
+ childTG.lockRootTimegroup();
840
+ setupParentChildRelationships(childTG, root);
841
+ } else if ("parentTimegroup" in child && "rootTimegroup" in child) {
842
+ const temporal = child;
843
+ temporal.parentTimegroup = nearestParentTG;
844
+ temporal.rootTimegroup = root;
845
+ if ("lockRootTimegroup" in temporal && typeof temporal.lockRootTimegroup === "function") temporal.lockRootTimegroup();
846
+ } else if (child instanceof Element) setupParentChildRelationshipsInContainer(child, nearestParentTG, root);
847
+ };
848
+ actualClone.rootTimegroup = actualClone;
849
+ setupParentChildRelationships(actualClone, actualClone);
850
+ await actualClone.updateComplete;
851
+ actualClone.rootTimegroup = actualClone;
852
+ actualClone.lockRootTimegroup();
853
+ const finalizeRootTimegroup = (el) => {
854
+ if ("rootTimegroup" in el && "lockRootTimegroup" in el) {
855
+ el.rootTimegroup = actualClone;
856
+ el.lockRootTimegroup();
857
+ }
858
+ for (const child of el.children) finalizeRootTimegroup(child);
859
+ };
860
+ finalizeRootTimegroup(actualClone);
861
+ await actualClone.waitForMediaDurations();
862
+ await this.#waitForCaptionsData(actualClone);
863
+ if (actualClone.playbackController) {
864
+ actualClone.playbackController.remove();
865
+ actualClone.playbackController = void 0;
866
+ }
867
+ await actualClone.seek(0);
868
+ return {
869
+ clone: actualClone,
870
+ container,
871
+ cleanup: () => {
872
+ container.remove();
873
+ const reactRoot = actualClone._reactRoot;
874
+ if (reactRoot) queueMicrotask(() => {
875
+ reactRoot.unmount();
876
+ });
877
+ }
878
+ };
879
+ }
880
+ /** @internal */
299
881
  get storageKey() {
300
882
  if (!this.id) throw new Error("Timegroup must have an id to use localStorage.");
301
883
  return `ef-timegroup-${this.id}`;
302
884
  }
885
+ /** @internal */
303
886
  get intrinsicDurationMs() {
304
887
  if (this.hasExplicitDuration) return this.explicitDurationMs;
305
888
  }
889
+ /** @internal */
306
890
  get hasOwnDuration() {
307
- return this.mode === "contain" || this.mode === "sequence" || this.mode === "fixed" && this.hasExplicitDuration;
891
+ return hasOwnDurationForMode(this.mode, this.hasExplicitDuration);
308
892
  }
893
+ /** @public */
309
894
  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
- }
895
+ if (this.mode === "fixed") return super.durationMs;
896
+ const childTemporalsAsElements = this.childTemporals;
897
+ return evaluateDurationForMode(this, this.mode, childTemporalsAsElements);
338
898
  }
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();
899
+ /**
900
+ * Evaluates which elements should be rendered in the current frame.
901
+ * Filters to only include temporally visible elements for frame processing.
902
+ * Uses animation-friendly visibility to prevent animation jumps at exact boundaries.
903
+ */
904
+ #evaluateVisibleElementsForFrame() {
905
+ return deepGetElementsWithFrameTasks(this).filter((element) => {
906
+ return evaluateAnimationVisibilityState(element).isVisible;
355
907
  });
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
908
  }
370
- async waitForFrameTasks() {
909
+ /** @internal */
910
+ async waitForFrameTasks(signal) {
371
911
  return await withSpan("timegroup.waitForFrameTasks", {
372
912
  timegroupId: this.id || "unknown",
373
913
  mode: this.mode
374
914
  }, void 0, async (span) => {
915
+ signal?.throwIfAborted();
375
916
  const innerStart = performance.now();
376
917
  const temporalElements = deepGetElementsWithFrameTasks(this);
377
918
  if (isTracingEnabled()) span.setAttribute("temporalElementsCount", temporalElements.length);
378
- const visibleElements = temporalElements.filter((element) => {
379
- return evaluateAnimationVisibilityState(element).isVisible;
380
- });
919
+ signal?.throwIfAborted();
920
+ const visibleElements = this.#evaluateVisibleElementsForFrame();
381
921
  if (isTracingEnabled()) span.setAttribute("visibleElementsCount", visibleElements.length);
382
922
  const promiseStart = performance.now();
383
- await Promise.all(visibleElements.map((element) => element.frameTask.run()));
923
+ await Promise.all(visibleElements.map(async (element) => {
924
+ signal?.throwIfAborted();
925
+ try {
926
+ if ("waitForFrameReady" in element && typeof element.waitForFrameReady === "function") await element.waitForFrameReady();
927
+ else {
928
+ await element.updateComplete;
929
+ await element.frameTask.run();
930
+ }
931
+ } catch (error) {
932
+ 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"))) {
933
+ signal?.throwIfAborted();
934
+ return;
935
+ }
936
+ throw error;
937
+ }
938
+ }));
384
939
  const promiseEnd = performance.now();
385
940
  const innerEnd = performance.now();
386
941
  if (isTracingEnabled()) {
@@ -389,9 +944,21 @@ let EFTimegroup = class EFTimegroup$1 extends EFTargetable(EFTemporal(TWMixin(Li
389
944
  }
390
945
  });
391
946
  }
392
- async waitForMediaDurations() {
393
- if (!this.mediaDurationsPromise) this.mediaDurationsPromise = this.#waitForMediaDurations();
394
- return this.mediaDurationsPromise;
947
+ #mediaDurationsPromise = void 0;
948
+ /** @internal */
949
+ async waitForMediaDurations(signal) {
950
+ signal?.throwIfAborted();
951
+ if (!this.#mediaDurationsPromise) this.#mediaDurationsPromise = this.#waitForMediaDurations(signal).catch((err) => {
952
+ if (err instanceof DOMException && err.name === "AbortError") {
953
+ this.#mediaDurationsPromise = void 0;
954
+ throw err;
955
+ }
956
+ console.error(`[EFTimegroup] waitForMediaDurations failed for ${this.id || "unnamed"}:`, err);
957
+ this.#mediaDurationsPromise = void 0;
958
+ throw err;
959
+ });
960
+ if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
961
+ return this.#mediaDurationsPromise;
395
962
  }
396
963
  /**
397
964
  * Wait for all media elements to load their initial segments.
@@ -399,25 +966,96 @@ let EFTimegroup = class EFTimegroup$1 extends EFTargetable(EFTemporal(TWMixin(Li
399
966
  * that caused issues with constructing audio data. We had negative durations
400
967
  * in calculations and it was not clear why.
401
968
  */
402
- async #waitForMediaDurations() {
969
+ async #waitForMediaDurations(signal) {
403
970
  return withSpan("timegroup.waitForMediaDurations", {
404
971
  timegroupId: this.id || "unknown",
405
972
  mode: this.mode
406
973
  }, void 0, async (span) => {
407
- await this.updateComplete;
974
+ signal?.throwIfAborted();
975
+ await new Promise((resolve, reject) => {
976
+ if (signal?.aborted) {
977
+ reject(new DOMException("Aborted", "AbortError"));
978
+ return;
979
+ }
980
+ const abortHandler = () => {
981
+ clearTimeout(timeoutId);
982
+ cancelAnimationFrame(rafId2);
983
+ cancelAnimationFrame(rafId1);
984
+ reject(new DOMException("Aborted", "AbortError"));
985
+ };
986
+ signal?.addEventListener("abort", abortHandler, { once: true });
987
+ let rafId1;
988
+ let rafId2;
989
+ let timeoutId;
990
+ rafId1 = requestAnimationFrame(() => {
991
+ if (signal?.aborted) {
992
+ reject(new DOMException("Aborted", "AbortError"));
993
+ return;
994
+ }
995
+ rafId2 = requestAnimationFrame(() => {
996
+ if (signal?.aborted) {
997
+ reject(new DOMException("Aborted", "AbortError"));
998
+ return;
999
+ }
1000
+ timeoutId = setTimeout(() => {
1001
+ signal?.removeEventListener("abort", abortHandler);
1002
+ resolve();
1003
+ }, 10);
1004
+ });
1005
+ });
1006
+ });
1007
+ signal?.throwIfAborted();
408
1008
  const mediaElements = deepGetMediaElements(this);
409
1009
  if (isTracingEnabled()) span.setAttribute("mediaElementsCount", mediaElements.length);
410
- await Promise.all(mediaElements.map((m) => m.mediaEngineTask.value ? Promise.resolve() : m.mediaEngineTask.run()));
1010
+ signal?.throwIfAborted();
1011
+ const mediaLoadStart = Date.now();
1012
+ const MEDIA_LOAD_TIMEOUT_MS = 3e4;
1013
+ const loadPromises = mediaElements.map(async (m, index) => {
1014
+ signal?.throwIfAborted();
1015
+ const elementStart = Date.now();
1016
+ try {
1017
+ const status = m.mediaEngineTask.status;
1018
+ if (status === TaskStatus.COMPLETE || status === TaskStatus.ERROR) return;
1019
+ if (status === TaskStatus.INITIAL) m.mediaEngineTask.run().catch(() => {});
1020
+ const timeoutPromise = new Promise((_, reject) => {
1021
+ if (signal?.aborted) {
1022
+ reject(new DOMException("Aborted", "AbortError"));
1023
+ return;
1024
+ }
1025
+ const timeoutId = setTimeout(() => reject(/* @__PURE__ */ new Error(`Media element ${index} load timeout after ${MEDIA_LOAD_TIMEOUT_MS}ms`)), MEDIA_LOAD_TIMEOUT_MS);
1026
+ signal?.addEventListener("abort", () => {
1027
+ clearTimeout(timeoutId);
1028
+ reject(new DOMException("Aborted", "AbortError"));
1029
+ }, { once: true });
1030
+ });
1031
+ const taskPromise = m.mediaEngineTask.taskComplete;
1032
+ taskPromise.catch(() => {});
1033
+ await Promise.race([taskPromise, timeoutPromise]);
1034
+ } catch (error) {
1035
+ if (error instanceof DOMException && error.name === "AbortError") throw error;
1036
+ if (isTracingEnabled()) {
1037
+ const elementElapsed = Date.now() - elementStart;
1038
+ console.error(`[EFTimegroup] Media element ${index} failed after ${elementElapsed}ms:`, error);
1039
+ }
1040
+ }
1041
+ });
1042
+ const results = await Promise.allSettled(loadPromises);
1043
+ if (results.some((r) => r.status === "rejected" && r.reason instanceof DOMException && r.reason.name === "AbortError")) throw new DOMException("Aborted", "AbortError");
1044
+ const failures = results.filter((r) => r.status === "rejected");
1045
+ if (failures.length > 0 && isTracingEnabled()) {
1046
+ const mediaLoadElapsed = Date.now() - mediaLoadStart;
1047
+ console.warn(`[EFTimegroup] ${failures.length} media elements failed to load in ${mediaLoadElapsed}ms:`, failures.map((r) => r.status === "rejected" ? r.reason : null));
1048
+ }
411
1049
  flushStartTimeMsCache();
412
1050
  flushSequenceDurationCache();
413
- this.requestUpdate("currentTime");
414
- await this.updateComplete;
1051
+ setTimeout(() => this.requestUpdate("currentTime"), 0);
415
1052
  });
416
1053
  }
1054
+ /** @internal */
417
1055
  get childTemporals() {
418
1056
  return shallowGetTemporalElements(this);
419
1057
  }
420
- get contextProvider() {
1058
+ get #contextProvider() {
421
1059
  let parent = this.parentNode;
422
1060
  while (parent) {
423
1061
  if (isContextMixin(parent)) return parent;
@@ -430,34 +1068,59 @@ let EFTimegroup = class EFTimegroup$1 extends EFTargetable(EFTemporal(TWMixin(Li
430
1068
  *
431
1069
  * A timegroup should be wrapped with a workbench if:
432
1070
  * - It's being rendered (EF_RENDERING), OR
433
- * - It's in interactive mode (EF_INTERACTIVE) with the dev workbench flag set
1071
+ * - The workbench property is set to true
434
1072
  *
435
1073
  * If the timegroup is already wrapped in a context provider like ef-preview,
436
1074
  * it should NOT be wrapped in a workbench.
1075
+ * @internal
437
1076
  */
438
1077
  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;
1078
+ if (!this.isRootTimegroup) return false;
1079
+ if (this.closest("ef-canvas") !== null) return false;
1080
+ if (this.closest("ef-preview") !== null || this.closest("ef-workbench") !== null || this.closest("ef-preview-context") !== null) return false;
1081
+ if (this.closest("test-context") !== null) return false;
1082
+ if (this.closest(".ef-render-clone-container") !== null) return false;
1083
+ if (EF_RENDERING?.() === true) return true;
1084
+ return this.workbench;
442
1085
  }
1086
+ /** @internal */
443
1087
  wrapWithWorkbench() {
444
1088
  const workbench = document.createElement("ef-workbench");
445
1089
  this.parentElement?.append(workbench);
446
- if (!this.hasAttribute("id")) this.setAttribute("id", "root-this");
447
- this.setAttribute("slot", "canvas");
448
- workbench.append(this);
1090
+ if (!this.hasAttribute("id")) this.setAttribute("id", "root-timegroup");
1091
+ const panZoom = document.createElement("ef-pan-zoom");
1092
+ panZoom.id = "workbench-panzoom";
1093
+ panZoom.setAttribute("slot", "canvas");
1094
+ panZoom.setAttribute("auto-fit", "");
1095
+ panZoom.style.width = "100%";
1096
+ panZoom.style.height = "100%";
1097
+ const rect = this.getBoundingClientRect();
1098
+ const canvas = document.createElement("ef-canvas");
1099
+ canvas.id = "workbench-canvas";
1100
+ canvas.style.width = `${Math.max(rect.width, 1920)}px`;
1101
+ canvas.style.height = `${Math.max(rect.height, 1080)}px`;
1102
+ canvas.style.display = "block";
1103
+ canvas.append(this);
1104
+ panZoom.append(canvas);
1105
+ workbench.append(panZoom);
1106
+ const hierarchy = document.createElement("ef-hierarchy");
1107
+ hierarchy.setAttribute("slot", "hierarchy");
1108
+ hierarchy.setAttribute("target", "workbench-canvas");
1109
+ hierarchy.setAttribute("header", "Scenes");
1110
+ workbench.append(hierarchy);
449
1111
  const filmstrip = document.createElement("ef-filmstrip");
450
1112
  filmstrip.setAttribute("slot", "timeline");
451
1113
  filmstrip.setAttribute("target", this.id);
452
1114
  workbench.append(filmstrip);
453
1115
  }
454
- get efElements() {
1116
+ get #efElements() {
455
1117
  return Array.from(this.querySelectorAll("ef-audio, ef-video, ef-image, ef-captions, ef-waveform"));
456
1118
  }
457
1119
  /**
458
1120
  * Returns media elements for playback audio rendering
459
1121
  * For standalone media, returns [this]; for timegroups, returns all descendants
460
1122
  * Used by PlaybackController for audio-driven playback
1123
+ * @internal
461
1124
  */
462
1125
  getMediaElements() {
463
1126
  return deepGetMediaElements(this);
@@ -466,15 +1129,16 @@ let EFTimegroup = class EFTimegroup$1 extends EFTargetable(EFTemporal(TWMixin(Li
466
1129
  * Render audio buffer for playback
467
1130
  * Called by PlaybackController during live playback
468
1131
  * Delegates to shared renderTemporalAudio utility for consistent behavior
1132
+ * @internal
469
1133
  */
470
- async renderAudio(fromMs, toMs) {
471
- return renderTemporalAudio(this, fromMs, toMs);
1134
+ async renderAudio(fromMs, toMs, signal) {
1135
+ return renderTemporalAudio(this, fromMs, toMs, signal);
472
1136
  }
473
1137
  /**
474
1138
  * TEMPORARY TEST METHOD: Renders audio and immediately plays it back
475
1139
  * Usage: timegroup.testPlayAudio(0, 5000) // Play first 5 seconds
476
1140
  */
477
- async testPlayAudio(fromMs, toMs) {
1141
+ async #testPlayAudio(fromMs, toMs) {
478
1142
  const renderedBuffer = await this.renderAudio(fromMs, toMs);
479
1143
  const playbackContext = new AudioContext();
480
1144
  const bufferSource = playbackContext.createBufferSource();
@@ -488,13 +1152,13 @@ let EFTimegroup = class EFTimegroup$1 extends EFTargetable(EFTemporal(TWMixin(Li
488
1152
  };
489
1153
  });
490
1154
  }
491
- async loadMd5Sums() {
492
- const efElements = this.efElements;
1155
+ async #loadMd5Sums() {
1156
+ const efElements = this.#efElements;
493
1157
  const loaderTasks = [];
494
1158
  for (const el of efElements) {
495
1159
  const md5SumLoader = el.md5SumLoader;
496
1160
  if (md5SumLoader instanceof Task) {
497
- md5SumLoader.run();
1161
+ md5SumLoader.run().catch(() => {});
498
1162
  loaderTasks.push(md5SumLoader.taskComplete);
499
1163
  }
500
1164
  }
@@ -503,6 +1167,14 @@ let EFTimegroup = class EFTimegroup$1 extends EFTargetable(EFTemporal(TWMixin(Li
503
1167
  if ("productionSrc" in el && el.productionSrc instanceof Function) el.setAttribute("src", el.productionSrc());
504
1168
  });
505
1169
  }
1170
+ #timegroupFrameTaskCount = 0;
1171
+ #timegroupFrameTaskLastReset = Date.now();
1172
+ static {
1173
+ this.TIMEGROUP_FRAME_TASK_THRESHOLD = 100;
1174
+ }
1175
+ static {
1176
+ this.TIMEGROUP_FRAME_TASK_RESET_MS = 1e3;
1177
+ }
506
1178
  async #executeCustomFrameTasks() {
507
1179
  if (this.#customFrameTasks.size > 0) {
508
1180
  const percentComplete = this.durationMs > 0 ? this.ownCurrentTimeMs / this.durationMs : 0;
@@ -516,10 +1188,41 @@ let EFTimegroup = class EFTimegroup$1 extends EFTargetable(EFTemporal(TWMixin(Li
516
1188
  await Promise.all(Array.from(this.#customFrameTasks).map((callback) => Promise.resolve(callback(frameInfo))));
517
1189
  }
518
1190
  }
1191
+ /**
1192
+ * Get container information for this timegroup.
1193
+ * Timegroups are always containers and can contain children.
1194
+ * Display mode is determined from computed styles.
1195
+ *
1196
+ * @public
1197
+ */
1198
+ getContainerInfo() {
1199
+ return {
1200
+ ...getContainerInfoFromElement(this),
1201
+ isContainer: true,
1202
+ canContainChildren: true
1203
+ };
1204
+ }
1205
+ /**
1206
+ * Get position information for this timegroup.
1207
+ * Returns computed bounds, transform, and rotation.
1208
+ *
1209
+ * @public
1210
+ */
1211
+ getPositionInfo() {
1212
+ return getPositionInfoFromElement(this);
1213
+ }
519
1214
  };
520
1215
  __decorate([provide({ context: timegroupContext })], EFTimegroup.prototype, "_timeGroupContext", void 0);
521
1216
  __decorate([provide({ context: efContext })], EFTimegroup.prototype, "efContext", void 0);
522
1217
  __decorate([property({ type: Number })], EFTimegroup.prototype, "fps", void 0);
1218
+ __decorate([property({
1219
+ type: Boolean,
1220
+ attribute: "auto-init"
1221
+ })], EFTimegroup.prototype, "autoInit", void 0);
1222
+ __decorate([property({
1223
+ type: Boolean,
1224
+ reflect: true
1225
+ })], EFTimegroup.prototype, "workbench", void 0);
523
1226
  __decorate([property({ type: String })], EFTimegroup.prototype, "fit", void 0);
524
1227
  __decorate([property({
525
1228
  type: Number,