@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 +1 @@
1
- {"version":3,"file":"EFThumbnailStrip.js","names":["timestamps: number[]","EFThumbnailStrip","effectiveStartMs","effectiveEndMs","allThumbnails: ThumbnailRenderInfo[]","status: ThumbnailRenderInfo[\"status\"]","nearHitKey: string | undefined","drawWidth: number","drawHeight: number","drawX: number","drawY: number"],"sources":["../../src/elements/EFThumbnailStrip.ts"],"sourcesContent":["import { Task } from \"@lit/task\";\nimport { css, html, LitElement } from \"lit\";\nimport { customElement, property, state } from \"lit/decorators.js\";\nimport { createRef, ref } from \"lit/directives/ref.js\";\nimport type { MediaEngine as ImportedMediaEngine } from \"../transcoding/types/index.js\";\nimport { OrderedLRUCache } from \"../utils/LRUCache.js\";\nimport type { EFVideo } from \"./EFVideo.js\";\nimport { TargetController } from \"./TargetController.ts\";\n\n/**\n * Global thumbnail image cache for smooth resize performance\n * Shared across all thumbnail strip instances\n * Uses OrderedLRUCache for efficient timestamp-based searching\n */\nconst thumbnailImageCache = new OrderedLRUCache<string, ImageData>(\n 200,\n (a, b) => {\n // Extract timestamp from cache key for ordered searching (take last part after splitting on ':')\n const partsA = a.split(\":\");\n const partsB = b.split(\":\");\n const timeA = Number.parseFloat(partsA[partsA.length - 1] || \"0\");\n const timeB = Number.parseFloat(partsB[partsB.length - 1] || \"0\");\n return timeA - timeB;\n },\n);\n\n// Export for debugging (works in both browser and server)\n(\n globalThis as typeof globalThis & {\n debugThumbnailCache: typeof thumbnailImageCache;\n }\n).debugThumbnailCache = thumbnailImageCache;\n\n/**\n * Quantize timestamp to 30fps frame boundaries for consistent caching\n * This eliminates cache misses from floating point precision differences\n */\nfunction quantizeTimestamp(timeMs: number): number {\n const frameIntervalMs = 1000 / 30; // 33.33ms at 30fps\n return Math.round(timeMs / frameIntervalMs) * frameIntervalMs;\n}\n\n/**\n * Generate cache key for thumbnail image data (dimension-independent, quantized)\n */\nfunction getThumbnailCacheKey(videoSrc: string, timeMs: number): string {\n const quantizedTimeMs = quantizeTimestamp(timeMs);\n return `${videoSrc}:${quantizedTimeMs}`;\n}\n\n// Constants for consistent thumbnail layout\nconst THUMBNAIL_GAP = 1; // 1px gap between thumbnails\nconst STRIP_BORDER_PADDING = 4; // Account for border/padding in available height\n\ninterface ThumbnailSegment {\n segmentId: number;\n thumbnails: Array<{\n timeMs: number;\n }>;\n}\n\ninterface ThumbnailLayout {\n count: number;\n segments: readonly ThumbnailSegment[];\n}\n\n// Use the imported MediaEngine type and mediabunny types\n\ninterface ThumbnailRenderInfo {\n timeMs: number;\n segmentId: number;\n x: number;\n width: number;\n height: number;\n status: \"exact-hit\" | \"near-hit\" | \"missing\" | \"loading\";\n imageData?: ImageData;\n nearHitKey?: string;\n}\n\n/**\n * Calculate optimal thumbnail count and timestamps for the strip\n * Groups thumbnails by scrub segment ID for efficient caching\n */\nfunction calculateThumbnailLayout(\n stripWidth: number,\n thumbnailWidth: number,\n startTimeMs: number,\n endTimeMs: number,\n scrubSegmentDurationMs?: number,\n): ThumbnailLayout {\n // Must have positive width and valid time range\n if (stripWidth <= 0 || thumbnailWidth <= 0 || endTimeMs <= startTimeMs) {\n return { count: 0, segments: [] };\n }\n\n // Simple calculation: how many full thumbnails fit, plus one more to fill the width\n const thumbnailPitch = thumbnailWidth + THUMBNAIL_GAP;\n const baseFitCount = Math.floor(stripWidth / thumbnailPitch);\n const count = Math.max(1, baseFitCount + 1); // Always one extra to fill width\n\n // Generate timestamps evenly distributed across time range\n const timestamps: number[] = [];\n const timeRange = endTimeMs - startTimeMs;\n\n for (let i = 0; i < count; i++) {\n const timeMs =\n count === 1\n ? (startTimeMs + endTimeMs) / 2\n : startTimeMs + (i * timeRange) / (count - 1);\n timestamps.push(timeMs);\n }\n\n // Group by segment ID\n const segmentMap = new Map<number, Array<{ timeMs: number }>>();\n for (const timeMs of timestamps) {\n const segmentId = scrubSegmentDurationMs\n ? Math.floor(timeMs / scrubSegmentDurationMs)\n : 0;\n if (!segmentMap.has(segmentId)) {\n segmentMap.set(segmentId, []);\n }\n // biome-ignore lint/style/noNonNullAssertion: Set in line above\n segmentMap.get(segmentId)!.push({ timeMs });\n }\n\n const segments = Array.from(segmentMap.entries())\n .sort(([a], [b]) => a - b)\n .map(([segmentId, thumbnails]) => ({ segmentId, thumbnails }));\n\n return { count, segments };\n}\n\n@customElement(\"ef-thumbnail-strip\")\nexport class EFThumbnailStrip extends LitElement {\n static styles = [\n css`\n :host {\n display: block;\n position: relative;\n width: 100%;\n height: 48px; /* Default filmstrip height */\n background: #2a2a2a;\n border: 2px solid #333;\n border-radius: 6px;\n overflow: hidden;\n box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.3);\n }\n canvas {\n display: block;\n position: absolute;\n top: 0;\n left: 0;\n image-rendering: pixelated; /* Keep thumbnails crisp */\n /* Width and height set programmatically to prevent CSS scaling */\n }\n .loading-overlay {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: rgba(42, 42, 42, 0.9);\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 11px;\n color: #ccc;\n font-weight: 500;\n }\n `,\n ];\n\n canvasRef = createRef<HTMLCanvasElement>();\n\n // Target video element using the same pattern as EFSurface\n // @ts-expect-error controller is intentionally not referenced directly to prevent GC\n // biome-ignore lint/correctness/noUnusedPrivateClassMembers: Used for side effects\n private _targetController: TargetController = new TargetController(\n this as any,\n );\n\n private _targetElement: EFVideo | null = null;\n\n @state()\n get targetElement(): EFVideo | null {\n return this._targetElement;\n }\n\n set targetElement(value: EFVideo | null) {\n const oldValue = this._targetElement;\n this._targetElement = value;\n\n // Clean up previous video property observer\n this._videoPropertyObserver?.disconnect();\n\n // When target element changes, set up property watching and media engine listener\n if (value && value !== oldValue) {\n // Watch for video property changes that affect thumbnails\n this._videoPropertyObserver = new MutationObserver((mutations) => {\n let shouldUpdate = false;\n for (const mutation of mutations) {\n if (mutation.type === \"attributes\" && mutation.attributeName) {\n const attr = mutation.attributeName;\n if (\n attr === \"trimstart\" ||\n attr === \"trimend\" ||\n attr === \"sourcein\" ||\n attr === \"sourceout\" ||\n attr === \"src\"\n ) {\n shouldUpdate = true;\n break;\n }\n }\n }\n if (shouldUpdate) {\n this.runThumbnailUpdate();\n }\n });\n\n this._videoPropertyObserver.observe(value, {\n attributes: true,\n attributeFilter: [\n \"trimstart\",\n \"trimend\",\n \"sourcein\",\n \"sourceout\",\n \"src\",\n ],\n });\n\n // Listen for media engine ready\n if (value.mediaEngineTask) {\n value.mediaEngineTask.taskComplete\n .then(() => {\n // When media engine is ready, retrigger thumbnails if we have width\n if (this._stripWidth > 0) {\n this.thumbnailLayoutTask.run();\n }\n })\n .catch(() => {\n // Ignore media engine errors\n });\n }\n }\n\n this.requestUpdate(\"targetElement\", oldValue);\n }\n\n @property({ type: String })\n target = \"\";\n\n /**\n * Desired thumbnail width in pixels (height is determined by aspect ratio)\n * Number of thumbnails is derived from this and available strip width\n */\n @property({ type: Number, attribute: \"thumbnail-width\" })\n thumbnailWidth = 80;\n\n /**\n * Custom start time in milliseconds relative to trimmed timeline (0 = start of trimmed portion)\n * In trimmed mode: 0ms = sourceStartMs, 1000ms = sourceStartMs + 1000ms\n * In intrinsic mode: 0ms = 0ms in source media\n */\n @property({ type: Number, attribute: \"start-time-ms\" })\n startTimeMs?: number;\n\n /**\n * Custom end time in milliseconds relative to trimmed timeline\n * In trimmed mode: relative to sourceStartMs\n * In intrinsic mode: relative to source media start (0ms)\n */\n @property({ type: Number, attribute: \"end-time-ms\" })\n endTimeMs?: number;\n\n /**\n * Use intrinsic duration instead of trimmed duration\n * Accepts \"true\"/\"false\" string values or boolean\n */\n @property({\n type: Boolean,\n attribute: \"use-intrinsic-duration\",\n reflect: true,\n converter: {\n fromAttribute: (value: string | null) => {\n if (value === null) return false;\n return value === \"true\";\n },\n toAttribute: (value: boolean) => (value ? \"true\" : null),\n },\n })\n useIntrinsicDuration = false;\n\n private _stripWidth = 0;\n private _stripHeight = 48; // Default height, updated by ResizeObserver\n private _pendingStripWidth: number | undefined;\n private _thumbnailLayoutTask: Promise<ThumbnailRenderInfo[]> | undefined;\n @state()\n private set stripWidth(value: number) {\n if (this._thumbnailLayoutTask) {\n this._pendingStripWidth = value;\n return;\n }\n this._stripWidth = value;\n\n if (value > 0) {\n this._thumbnailLayoutTask = this.thumbnailLayoutTask\n .run()\n .then(async () => {\n // Use taskComplete and .value instead of promise return value\n await this.thumbnailLayoutTask.taskComplete;\n const layout = this.thumbnailLayoutTask.value;\n return layout ? this.runThumbnailRenderTask(layout) : [];\n })\n .finally(() => {\n this._thumbnailLayoutTask = undefined;\n if (this._pendingStripWidth) {\n this.stripWidth = this._pendingStripWidth;\n this._pendingStripWidth = undefined;\n }\n });\n }\n }\n private get stripWidth() {\n return this._stripWidth;\n }\n\n /**\n * Run thumbnail render task directly with provided layout (bypasses task args dependency)\n */\n private async runThumbnailRenderTask(\n layout: ThumbnailLayout,\n ): Promise<ThumbnailRenderInfo[]> {\n if (!layout || !this.targetElement || layout.count === 0) {\n return [];\n }\n\n // Run the thumbnail render task logic directly\n return this.renderThumbnails(\n layout,\n this.targetElement,\n this.thumbnailWidth,\n );\n }\n\n private resizeObserver?: ResizeObserver;\n private _thumbnailUpdateInProgress = false;\n private _pendingThumbnailUpdate = false;\n private _videoPropertyObserver?: MutationObserver;\n\n updated(changedProperties: Map<string | number | symbol, unknown>) {\n super.updated(changedProperties);\n\n // IMPLEMENTATION GUIDELINES: Fix for initial loading bug - ensure width is detected\n if (this._stripWidth === 0) {\n const width = this.clientWidth;\n if (width > 0) {\n this.stripWidth = width;\n }\n }\n\n // IMPLEMENTATION GUIDELINES: Responsive debouncing for thumbnail property changes using EFTimegroup pattern\n if (\n changedProperties.has(\"thumbnailWidth\") ||\n changedProperties.has(\"startTimeMs\") ||\n changedProperties.has(\"endTimeMs\") ||\n changedProperties.has(\"useIntrinsicDuration\")\n ) {\n this.runThumbnailUpdate();\n }\n }\n\n /**\n * Run thumbnail update with responsive debouncing (based on EFTimegroup currentTime pattern)\n */\n private runThumbnailUpdate() {\n // If update already in progress, just flag that another update is needed\n if (this._thumbnailUpdateInProgress) {\n this._pendingThumbnailUpdate = true;\n return;\n }\n\n this._thumbnailUpdateInProgress = true;\n\n // Trigger full layout→render pipeline immediately for responsiveness\n this.thumbnailLayoutTask\n .run()\n .then(async () => {\n await this.thumbnailLayoutTask.taskComplete;\n const layout = this.thumbnailLayoutTask.value;\n if (layout) {\n await this.runThumbnailRenderTask(layout);\n }\n })\n .catch(() => {\n // Ignore errors - thumbnails will show as placeholders\n })\n .finally(() => {\n this._thumbnailUpdateInProgress = false;\n\n // If more property changes came in while we were processing, run another update\n if (this._pendingThumbnailUpdate) {\n this._pendingThumbnailUpdate = false;\n this.runThumbnailUpdate();\n }\n });\n }\n\n private thumbnailLayoutTask = new Task(this, {\n autoRun: false,\n task: async ([\n stripWidth,\n thumbnailWidth,\n targetElement,\n startTimeMs,\n endTimeMs,\n useIntrinsicDuration,\n mediaEngine,\n ]: readonly [\n number,\n number,\n EFVideo | null,\n number | undefined,\n number | undefined,\n boolean,\n ImportedMediaEngine | null | undefined,\n ]) => {\n // Need valid dimensions and target element\n if (stripWidth <= 0 || !targetElement) {\n return { count: 0, segments: [] };\n }\n\n // IMPLEMENTATION GUIDELINES: Wait for media engine to be ready before generating thumbnails\n if (!mediaEngine) {\n // If no media engine yet, wait for it to be ready\n if (targetElement.mediaEngineTask) {\n await targetElement.mediaEngineTask.taskComplete;\n // Get the media engine after it's ready\n const readyMediaEngine = targetElement.mediaEngineTask.value;\n if (!readyMediaEngine) {\n return { count: 0, segments: [] };\n }\n // Continue with the ready media engine\n return this.calculateLayoutWithMediaEngine(\n stripWidth,\n thumbnailWidth,\n targetElement,\n startTimeMs,\n endTimeMs,\n useIntrinsicDuration,\n readyMediaEngine,\n );\n }\n return { count: 0, segments: [] };\n }\n\n // Media engine is ready, proceed with layout calculation\n return this.calculateLayoutWithMediaEngine(\n stripWidth,\n thumbnailWidth,\n targetElement,\n startTimeMs,\n endTimeMs,\n useIntrinsicDuration,\n mediaEngine,\n );\n },\n args: () =>\n [\n this.stripWidth,\n this.thumbnailWidth,\n this.targetElement,\n this.startTimeMs,\n this.endTimeMs,\n this.useIntrinsicDuration,\n this.targetElement?.mediaEngineTask?.value,\n ] as const,\n });\n\n /**\n * Calculate layout with a ready media engine\n */\n private calculateLayoutWithMediaEngine(\n stripWidth: number,\n thumbnailWidth: number,\n targetElement: EFVideo,\n startTimeMs: number | undefined,\n endTimeMs: number | undefined,\n useIntrinsicDuration: boolean,\n mediaEngine: ImportedMediaEngine,\n ) {\n // Determine time range for thumbnails with correct timeline coordinate handling\n if (useIntrinsicDuration) {\n // INTRINSIC MODE: start-time-ms/end-time-ms are relative to source timeline (0 = source start)\n const effectiveStartMs = startTimeMs ?? 0;\n const effectiveEndMs =\n endTimeMs ?? targetElement.intrinsicDurationMs ?? 0;\n\n return this.generateLayoutFromTimeRange(\n stripWidth,\n thumbnailWidth,\n effectiveStartMs,\n effectiveEndMs,\n mediaEngine,\n );\n }\n // TRIMMED MODE: start-time-ms/end-time-ms are relative to trimmed timeline (0 = trim start)\n const sourceStart = targetElement.sourceStartMs ?? 0;\n const trimmedDuration = targetElement.durationMs ?? 0;\n\n // Convert trimmed timeline coordinates to source timeline coordinates\n const effectiveStartMs =\n startTimeMs !== undefined\n ? sourceStart + startTimeMs // Convert from trimmed timeline to source timeline\n : sourceStart; // Default: start of trimmed portion\n\n const effectiveEndMs =\n endTimeMs !== undefined\n ? sourceStart + endTimeMs // Convert from trimmed timeline to source timeline\n : sourceStart + trimmedDuration; // Default: end of trimmed portion\n\n return this.generateLayoutFromTimeRange(\n stripWidth,\n thumbnailWidth,\n effectiveStartMs,\n effectiveEndMs,\n mediaEngine,\n );\n }\n\n /**\n * Generate layout from calculated time range\n */\n private generateLayoutFromTimeRange(\n stripWidth: number,\n thumbnailWidth: number,\n effectiveStartMs: number,\n effectiveEndMs: number,\n mediaEngine: ImportedMediaEngine,\n ) {\n // Get scrub segment duration from media engine if available\n const scrubSegmentDurationMs =\n mediaEngine && typeof mediaEngine.getScrubVideoRendition === \"function\"\n ? mediaEngine.getScrubVideoRendition()?.segmentDurationMs\n : undefined;\n\n // Generate layout using our algorithm with segment alignment\n const layout = calculateThumbnailLayout(\n stripWidth,\n thumbnailWidth,\n effectiveStartMs,\n effectiveEndMs,\n scrubSegmentDurationMs,\n );\n\n return layout;\n }\n\n private thumbnailRenderTask = new Task(this, {\n autoRun: false,\n task: async ([layout, targetElement, thumbnailWidth]: readonly [\n ThumbnailLayout | null,\n EFVideo | null,\n number,\n ]) => {\n // Simplified task that delegates to renderThumbnails method\n if (!layout || !targetElement) {\n return [];\n }\n return this.renderThumbnails(layout, targetElement, thumbnailWidth);\n },\n args: () =>\n [\n this.thumbnailLayoutTask.value || null,\n this.targetElement,\n this.thumbnailWidth,\n ] as const,\n });\n\n /**\n * Render thumbnails with provided layout (main rendering logic)\n */\n private async renderThumbnails(\n layout: ThumbnailLayout,\n targetElement: EFVideo,\n thumbnailWidth: number,\n ): Promise<ThumbnailRenderInfo[]> {\n if (!layout || !targetElement || layout.count === 0) {\n return [];\n }\n\n const videoSrc = targetElement.src;\n const availableHeight = this._stripHeight - STRIP_BORDER_PADDING; // Account for border/padding\n\n const allThumbnails: ThumbnailRenderInfo[] = [];\n let thumbnailIndex = 0; // Track ordinal position\n\n // Process each segment\n for (const segment of layout.segments) {\n for (const thumbnail of segment.thumbnails) {\n const cacheKey = getThumbnailCacheKey(videoSrc, thumbnail.timeMs);\n\n // Try exact cache hit first\n let imageData = thumbnailImageCache.get(cacheKey);\n let status: ThumbnailRenderInfo[\"status\"] = \"exact-hit\";\n let nearHitKey: string | undefined;\n\n if (!imageData) {\n // Try near cache hit within 5 seconds using proper range search\n const timeMinus = Math.max(0, thumbnail.timeMs - 5000);\n const timePlus = thumbnail.timeMs + 5000;\n\n // For range bounds, use raw timestamps (don't quantize the search range)\n const rangeStartKey = `${videoSrc}:${timeMinus}`;\n const rangeEndKey = `${videoSrc}:${timePlus}`;\n\n // Use findRange to find any cached items in this time window\n const nearHits = thumbnailImageCache.findRange(\n rangeStartKey,\n rangeEndKey,\n );\n\n // Filter to only include the same video source\n const sameVideoHits = nearHits.filter((hit) =>\n hit.key.startsWith(`${videoSrc}:`),\n );\n\n if (sameVideoHits.length > 0) {\n // Get the closest match by time from same video\n const nearestHit = sameVideoHits.reduce((closest, current) => {\n const currentParts = current.key.split(\":\");\n const closestParts = closest.key.split(\":\");\n const currentTime = Number.parseFloat(\n currentParts[currentParts.length - 1] || \"0\",\n );\n const closestTime = Number.parseFloat(\n closestParts[closestParts.length - 1] || \"0\",\n );\n const currentDiff = Math.abs(currentTime - thumbnail.timeMs);\n const closestDiff = Math.abs(closestTime - thumbnail.timeMs);\n return currentDiff < closestDiff ? current : closest;\n });\n\n imageData = nearestHit.value;\n status = \"near-hit\";\n nearHitKey = nearestHit.key;\n } else {\n status = \"missing\";\n }\n }\n\n // Fixed integer positioning - no floating point\n const x = thumbnailIndex * (thumbnailWidth + THUMBNAIL_GAP);\n\n allThumbnails.push({\n timeMs: thumbnail.timeMs,\n segmentId: segment.segmentId,\n x,\n width: thumbnailWidth, // Always exactly 80px\n height: availableHeight, // Always exactly 44px\n status,\n imageData,\n nearHitKey,\n });\n\n thumbnailIndex++; // Increment ordinal position\n }\n }\n\n // Draw current state (cache hits and placeholders)\n await this.drawThumbnails(allThumbnails);\n\n // Load missing thumbnails from scrub tracks\n await this.loadMissingThumbnails(allThumbnails, targetElement);\n\n return allThumbnails;\n }\n\n connectedCallback() {\n super.connectedCallback();\n\n // Set up ResizeObserver to track element dimensions\n this.resizeObserver = new ResizeObserver((entries) => {\n for (const entry of entries) {\n // Use borderBoxSize for accurate dimensions including borders/padding\n const width =\n entry.borderBoxSize && entry.borderBoxSize.length > 0\n ? entry.borderBoxSize[0]?.inlineSize\n : entry.contentRect.width;\n\n const height =\n entry.borderBoxSize && entry.borderBoxSize.length > 0\n ? entry.borderBoxSize[0]?.blockSize\n : entry.contentRect.height;\n\n this._stripHeight = height ?? 0;\n this.stripWidth = width ?? 0; // This triggers thumbnail layout update\n }\n });\n\n this.resizeObserver.observe(this);\n\n // Force initial width calculation after element is fully connected\n this.updateComplete.then(() => {\n if (this._stripWidth === 0) {\n const width = this.clientWidth;\n if (width > 0) {\n this.stripWidth = width ?? 0;\n }\n }\n });\n }\n\n disconnectedCallback() {\n super.disconnectedCallback();\n this.resizeObserver?.disconnect();\n this.resizeObserver = undefined;\n\n // Clean up video property observer\n this._videoPropertyObserver?.disconnect();\n this._videoPropertyObserver = undefined;\n }\n\n /**\n * Draw thumbnails to the canvas with cache hits and placeholders\n */\n private async drawThumbnails(\n thumbnails: ThumbnailRenderInfo[],\n ): Promise<void> {\n const canvas = this.canvasRef.value;\n if (!canvas) {\n return;\n }\n\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) {\n return;\n }\n\n // Set canvas to exact size we're drawing - prevents CSS scaling\n const dpr = window.devicePixelRatio || 1;\n\n // Set canvas buffer size for high DPI rendering\n canvas.width = this._stripWidth * dpr;\n canvas.height = this._stripHeight * dpr;\n\n // Set canvas DOM size to exactly what we're drawing - no CSS scaling\n canvas.style.width = `${this._stripWidth}px`;\n canvas.style.height = `${this._stripHeight}px`;\n\n // Scale the drawing context to match device pixel ratio\n ctx.scale(dpr, dpr);\n\n // Clear canvas (use logical pixel dimensions since context is scaled)\n ctx.fillStyle = \"#2a2a2a\";\n ctx.fillRect(0, 0, this._stripWidth, this._stripHeight);\n\n // Draw each thumbnail with proper aspect ratio and centering\n for (const thumb of thumbnails) {\n if (thumb.imageData) {\n // Draw cached thumbnail with aspect ratio preservation\n const tempCanvas = document.createElement(\"canvas\");\n tempCanvas.width = thumb.imageData.width;\n tempCanvas.height = thumb.imageData.height;\n const tempCtx = tempCanvas.getContext(\"2d\");\n if (!tempCtx) {\n continue;\n }\n tempCtx.putImageData(thumb.imageData, 0, 0);\n\n // Preserve aspect ratio within fixed container bounds\n const sourceAspect = thumb.imageData.width / thumb.imageData.height;\n const containerAspect = thumb.width / thumb.height;\n\n // Calculate aspect-ratio-preserving dimensions with integer coordinates\n let drawWidth: number;\n let drawHeight: number;\n let drawX: number;\n let drawY: number;\n\n if (sourceAspect > containerAspect) {\n // Source is wider - fit to container width, letterbox top/bottom\n drawWidth = thumb.width;\n drawHeight = Math.round(thumb.width / sourceAspect);\n drawX = thumb.x;\n drawY = Math.round((this._stripHeight - drawHeight) / 2);\n } else {\n // Source is taller - fit to container height, pillarbox left/right\n drawWidth = Math.round(thumb.height * sourceAspect);\n drawHeight = thumb.height;\n drawX = thumb.x + Math.round((thumb.width - drawWidth) / 2);\n drawY = Math.round((this._stripHeight - drawHeight) / 2);\n }\n\n // Draw with proper aspect ratio preservation\n ctx.drawImage(tempCanvas, drawX, drawY, drawWidth, drawHeight);\n\n // Add subtle indicator for near hits\n if (thumb.status === \"near-hit\") {\n ctx.fillStyle = \"rgba(255, 165, 0, 0.3)\";\n ctx.fillRect(thumb.x, 0, thumb.width, 2);\n }\n } else {\n // Draw placeholder - center vertically in strip with integer positioning\n const placeholderY = Math.round((this._stripHeight - thumb.height) / 2);\n ctx.fillStyle = \"#404040\";\n ctx.fillRect(thumb.x, placeholderY, thumb.width, thumb.height);\n\n // Add subtle loading indicator with integer positioning\n ctx.strokeStyle = \"#666\";\n ctx.lineWidth = 1;\n ctx.setLineDash([2, 2]);\n ctx.strokeRect(thumb.x, placeholderY, thumb.width, thumb.height);\n ctx.setLineDash([]);\n }\n }\n }\n\n /**\n * Load missing thumbnails using MediaEngine batch extraction\n */\n private async loadMissingThumbnails(\n thumbnails: ThumbnailRenderInfo[],\n targetElement: EFVideo,\n ): Promise<void> {\n const mediaEngine = targetElement.mediaEngineTask?.value;\n if (!mediaEngine) {\n return;\n }\n\n // Get all missing thumbnails\n const missingThumbnails = thumbnails.filter(\n (t) => t.status === \"missing\" || t.status === \"near-hit\",\n );\n\n if (missingThumbnails.length === 0) {\n return;\n }\n\n // Update status to loading\n for (const thumb of missingThumbnails) {\n thumb.status = \"loading\";\n }\n\n // Batch extract all missing thumbnails using MediaEngine\n const timestamps = missingThumbnails.map((t) => t.timeMs);\n\n const thumbnailResults = await mediaEngine.extractThumbnails(timestamps);\n\n // Convert canvases to ImageData and update thumbnails\n for (let i = 0; i < missingThumbnails.length; i++) {\n const thumb = missingThumbnails[i];\n const thumbnailResult = thumbnailResults[i];\n\n if (thumb && thumbnailResult) {\n // Convert canvas to ImageData\n const imageData = this.canvasToImageData(thumbnailResult.thumbnail);\n\n if (imageData) {\n const cacheKey = getThumbnailCacheKey(\n targetElement.src,\n thumb.timeMs,\n );\n thumbnailImageCache.set(cacheKey, imageData);\n thumb.imageData = imageData;\n thumb.status = \"exact-hit\";\n }\n }\n }\n\n // Redraw with newly loaded thumbnails\n await this.drawThumbnails(thumbnails);\n }\n\n /**\n * Convert Canvas to ImageData for caching\n */\n private canvasToImageData(\n canvas: HTMLCanvasElement | OffscreenCanvas,\n ): ImageData | null {\n // Extract ImageData from canvas\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) {\n return null;\n }\n\n return ctx.getImageData(0, 0, canvas.width, canvas.height);\n }\n\n render() {\n return html`\n <canvas ${ref(this.canvasRef)}></canvas>\n ${this.thumbnailRenderTask.render({\n pending: () => html``,\n complete: () => html``,\n error: (e) =>\n html`<div class=\"error\">Error loading thumbnails: ${e}</div>`,\n })}\n `;\n }\n}\ndeclare global {\n interface HTMLElementTagNameMap {\n \"ef-thumbnail-strip\": EFThumbnailStrip;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"EFThumbnailStrip.js","names":["EFThumbnailStrip","node: Node | null","parentNode: Node | null","slots: ThumbnailSlot[]"],"sources":["../../src/elements/EFThumbnailStrip.ts"],"sourcesContent":["import { consume } from \"@lit/context\";\nimport { css, html, LitElement } from \"lit\";\nimport { customElement, property, state } from \"lit/decorators.js\";\nimport { createRef, ref } from \"lit/directives/ref.js\";\nimport type { EFVideo } from \"./EFVideo.js\";\nimport type { EFTimegroup } from \"./EFTimegroup.js\";\nimport { TargetController } from \"./TargetController.ts\";\nimport { timelineStateContext, type TimelineState } from \"../gui/timeline/timelineStateContext.js\";\nimport { sessionThumbnailCache, getCacheKey } from \"./SessionThumbnailCache.js\";\nimport { findRootTemporal } from \"./findRootTemporal.js\";\n\n/** Type guard to check if element is EFVideo */\nfunction isEFVideo(element: Element | null): element is EFVideo {\n return element?.tagName.toLowerCase() === \"ef-video\";\n}\n\n/** Type guard to check if element is EFTimegroup */\nfunction isEFTimegroup(element: Element | null): element is EFTimegroup {\n return element?.tagName.toLowerCase() === \"ef-timegroup\";\n}\n\n/**\n * Get identifiers for cache key generation.\n * Returns rootId (for cache isolation), elementId (for element-specific caching), and epoch (for content versioning).\n */\nfunction getCacheIdentifiers(element: EFVideo | EFTimegroup): { rootId: string; elementId: string; epoch: number } {\n // Get root timegroup for cache isolation between projects\n const rootTemporal = findRootTemporal(element);\n const rootTimegroup = rootTemporal && isEFTimegroup(rootTemporal) ? rootTemporal : null;\n const rootId = rootTimegroup?.id || \"default\";\n const epoch = rootTimegroup?.contentEpoch ?? 0;\n\n // Element identifier\n const elementId = isEFVideo(element)\n ? element.src || element.id || \"video\"\n : element.id || \"timegroup\";\n\n return { rootId, elementId, epoch };\n}\n\n/** Padding in pixels for virtual rendering (render extra thumbnails beyond viewport) */\nconst VIRTUAL_RENDER_PADDING_PX = 200;\n\n/** Default gap between thumbnails */\nconst DEFAULT_GAP = 4;\n\n/** Default aspect ratio if unknown */\nconst DEFAULT_ASPECT_RATIO = 16 / 9;\n\n/** Max canvas width for thumbnail captures */\nconst MAX_CAPTURE_WIDTH = 480;\n\n/** Thumbnails to capture per batch */\nconst BATCH_SIZE = 10;\n\ninterface ThumbnailSlot {\n timeMs: number;\n x: number; // Absolute position (not scroll-adjusted)\n width: number;\n imageData?: ImageData;\n status: \"cached\" | \"loading\" | \"pending\";\n}\n\n@customElement(\"ef-thumbnail-strip\")\nexport class EFThumbnailStrip extends LitElement {\n static styles = [\n css`\n :host {\n display: block;\n position: relative;\n background: #1a1a2e;\n overflow: hidden;\n width: 100%;\n height: 100%;\n }\n canvas {\n display: block;\n /* Absolute positioning - we manually position at visible region */\n position: absolute;\n top: 0;\n /* Left and width set programmatically based on visible portion */\n height: 100%;\n image-rendering: auto;\n }\n `,\n ];\n\n canvasRef = createRef<HTMLCanvasElement>();\n\n // ─────────────────────────────────────────────────────────────────────────\n // Public Properties\n // ─────────────────────────────────────────────────────────────────────────\n\n @property({ type: String })\n target = \"\";\n\n @property({ type: Number, attribute: \"thumbnail-width\" })\n thumbnailWidth = 0; // 0 = auto (calculate from height using aspect ratio)\n\n @property({ type: Number, attribute: \"gap\" })\n gap = DEFAULT_GAP;\n\n @property({ type: Number, attribute: \"start-time-ms\" })\n startTimeMs?: number;\n\n @property({ type: Number, attribute: \"end-time-ms\" })\n endTimeMs?: number;\n\n @property({\n type: Boolean,\n attribute: \"use-intrinsic-duration\",\n reflect: true,\n converter: {\n fromAttribute: (value: string | null) => value === \"true\",\n toAttribute: (value: boolean) => (value ? \"true\" : null),\n },\n })\n useIntrinsicDuration = false;\n\n @property({ type: Number, attribute: \"pixels-per-ms\" })\n pixelsPerMs = 0.1;\n\n // ─────────────────────────────────────────────────────────────────────────\n // Internal State\n // ─────────────────────────────────────────────────────────────────────────\n\n /** Timeline state context for viewport info */\n @consume({ context: timelineStateContext, subscribe: true })\n @state()\n private _timelineState?: TimelineState;\n\n /** Target element controller */\n // @ts-expect-error controller used for side effects\n // biome-ignore lint/correctness/noUnusedPrivateClassMembers: Used for side effects\n private _targetController: TargetController = new TargetController(this as any);\n\n private _targetElement: EFVideo | EFTimegroup | null = null;\n\n @state()\n get targetElement(): EFVideo | EFTimegroup | null {\n return this._targetElement;\n }\n\n set targetElement(value: EFVideo | EFTimegroup | null) {\n const oldValue = this._targetElement;\n this._targetElement = value;\n\n // Clean up old observer\n this._mutationObserver?.disconnect();\n\n // Reset ready state when target changes\n if (value !== oldValue) {\n this._hasLoadedThumbnails = false;\n this._lastLoadedEpoch = null;\n this._lastLayoutParams = null;\n }\n\n if (value && value !== oldValue) {\n this._setupTargetObserver(value);\n }\n\n this.requestUpdate(\"targetElement\", oldValue);\n }\n\n /** Host element dimensions */\n private _width = 0;\n private _height = 0;\n\n /** Scroll container reference */\n private _scrollContainer: HTMLElement | null = null;\n private _currentScrollLeft = 0;\n \n /** \n * Offset from scroll container's left edge to this element's track.\n * Used for sticky positioning when track doesn't start at x=0 (e.g., labels column).\n */\n private _trackLeftOffset = 0;\n\n /** Current thumbnail slots */\n private _thumbnailSlots: ThumbnailSlot[] = [];\n\n /** Capture in progress flag */\n private _captureInProgress = false;\n\n /** Resize observer */\n private _resizeObserver?: ResizeObserver;\n\n /** Mutation observer for target element changes */\n private _mutationObserver?: MutationObserver;\n\n /** Animation frame for scroll updates */\n private _scrollFrame?: number;\n\n /** Render request tracking */\n private _renderRequested = false;\n\n /** Track if any thumbnails have been loaded (for ready event) */\n private _hasLoadedThumbnails = false;\n\n /** Track the last epoch we loaded thumbnails for */\n private _lastLoadedEpoch: number | null = null;\n \n /** Track layout parameters to avoid unnecessary slot recreation */\n private _lastLayoutParams: {\n width: number;\n height: number;\n startTimeMs: number;\n endTimeMs: number;\n thumbWidth: number;\n gap: number;\n } | null = null;\n\n // ─────────────────────────────────────────────────────────────────────────\n // Lifecycle\n // ─────────────────────────────────────────────────────────────────────────\n\n connectedCallback() {\n super.connectedCallback();\n\n // Set up resize observer\n this._resizeObserver = new ResizeObserver((entries) => {\n for (const entry of entries) {\n const box = entry.borderBoxSize?.[0];\n this._width = box?.inlineSize ?? entry.contentRect.width;\n this._height = box?.blockSize ?? entry.contentRect.height;\n // Recalculate track offset in case layout changed\n this._calculateTrackOffset();\n this._scheduleRender();\n }\n });\n this._resizeObserver.observe(this);\n\n // Find scroll container after element is ready\n this.updateComplete.then(() => {\n this._findScrollContainer();\n this._scheduleRender();\n });\n }\n\n disconnectedCallback() {\n super.disconnectedCallback();\n this._resizeObserver?.disconnect();\n this._mutationObserver?.disconnect();\n this._detachScrollListener();\n\n if (this._scrollFrame) {\n cancelAnimationFrame(this._scrollFrame);\n }\n }\n\n updated(changedProperties: Map<string | number | symbol, unknown>) {\n super.updated(changedProperties);\n\n // Re-render on property changes\n if (\n changedProperties.has(\"thumbnailWidth\") ||\n changedProperties.has(\"gap\") ||\n changedProperties.has(\"startTimeMs\") ||\n changedProperties.has(\"endTimeMs\") ||\n changedProperties.has(\"useIntrinsicDuration\") ||\n changedProperties.has(\"pixelsPerMs\") ||\n changedProperties.has(\"targetElement\")\n ) {\n this._scheduleRender();\n }\n\n // Handle timeline context scroll changes\n if (changedProperties.has(\"_timelineState\")) {\n this._onContextScroll();\n }\n }\n\n // ─────────────────────────────────────────────────────────────────────────\n // Scroll Handling\n // ─────────────────────────────────────────────────────────────────────────\n\n private _findScrollContainer(): void {\n // Walk up the DOM tree, crossing shadow boundaries\n let node: Node | null = this.parentNode;\n \n while (node) {\n // Check if this node is an element with overflow-x: auto or scroll\n if (node instanceof HTMLElement) {\n const style = getComputedStyle(node);\n if (style.overflowX === \"auto\" || style.overflowX === \"scroll\") {\n this._scrollContainer = node;\n this._calculateTrackOffset();\n this._attachScrollListener();\n return;\n }\n }\n \n // Move to parent, crossing shadow DOM boundaries\n if (node.parentNode) {\n node = node.parentNode;\n } else if (node instanceof ShadowRoot) {\n // Cross shadow boundary to the host element\n node = node.host;\n } else {\n break;\n }\n }\n }\n\n /**\n * Calculate the horizontal offset from scroll container's left edge to this element's track.\n * This accounts for sticky labels or other elements that precede the track area.\n * \n * We look for our specific timeline elements (ef-timeline-row) and measure their label width.\n */\n private _calculateTrackOffset(): void {\n if (!this._scrollContainer) {\n this._trackLeftOffset = 0;\n return;\n }\n \n // Find ef-timeline-row ancestor and get its label width\n const timelineRow = this._findTimelineRow();\n if (timelineRow) {\n const labelWidth = this._getTimelineRowLabelWidth(timelineRow);\n if (labelWidth > 0) {\n this._trackLeftOffset = labelWidth;\n return;\n }\n }\n \n // No timeline row found - track starts at scroll container's left edge\n this._trackLeftOffset = 0;\n }\n \n /**\n * Find the ef-timeline-row ancestor by walking up through shadow DOM boundaries.\n */\n private _findTimelineRow(): Element | null {\n let node: Node | null = this;\n \n while (node) {\n // Check if this is ef-timeline-row\n if (node instanceof Element && node.tagName.toLowerCase() === 'ef-timeline-row') {\n return node;\n }\n \n // Move up through shadow DOM boundaries\n const parentNode: Node | null = node.parentNode;\n if (parentNode instanceof ShadowRoot) {\n node = parentNode.host;\n } else {\n node = parentNode;\n }\n }\n \n return null;\n }\n \n /**\n * Get the label width from an ef-timeline-row element.\n * Queries the shadow root for .row-label and returns its width.\n */\n private _getTimelineRowLabelWidth(timelineRow: Element): number {\n const shadowRoot = timelineRow.shadowRoot;\n if (!shadowRoot) return 0;\n \n const rowLabel = shadowRoot.querySelector('.row-label');\n if (!rowLabel) return 0;\n \n return rowLabel.getBoundingClientRect().width;\n }\n \n /**\n * Get this strip's absolute position in the timeline (pixels from timeline origin).\n * Uses the target element's startTimeMs to determine position.\n */\n private _getStripTimelinePosition(): number {\n const target = this._targetElement;\n if (!target) return 0;\n \n // For videos, use their startTimeMs to get timeline position\n if (isEFVideo(target)) {\n const startTimeMs = target.startTimeMs ?? 0;\n const pixelsPerMs = this._timelineState?.pixelsPerMs ?? 0.1;\n return startTimeMs * pixelsPerMs;\n }\n \n // For root timegroup, position is 0\n return 0;\n }\n\n private _attachScrollListener(): void {\n if (!this._scrollContainer) return;\n this._scrollContainer.addEventListener(\"scroll\", this._onScroll, { passive: true });\n this._currentScrollLeft = this._scrollContainer.scrollLeft;\n }\n\n private _detachScrollListener(): void {\n if (this._scrollContainer) {\n this._scrollContainer.removeEventListener(\"scroll\", this._onScroll);\n this._scrollContainer = null;\n }\n }\n\n private _onScroll = (): void => {\n if (!this._scrollContainer) return;\n this._currentScrollLeft = this._scrollContainer.scrollLeft;\n this._drawCanvas();\n\n // Schedule loading of newly visible thumbnails\n if (!this._scrollFrame) {\n this._scrollFrame = requestAnimationFrame(() => {\n this._scrollFrame = undefined;\n this._loadVisibleThumbnails();\n });\n }\n };\n\n private _onContextScroll(): void {\n if (!this._timelineState || this._scrollContainer) return;\n this._currentScrollLeft = this._timelineState.viewportScrollLeft;\n this._drawCanvas();\n }\n\n private get _viewportWidth(): number {\n if (this._timelineState?.viewportWidth) {\n return this._timelineState.viewportWidth;\n }\n if (this._scrollContainer) {\n // Subtract track offset to get actual viewport width available for the track\n return this._scrollContainer.clientWidth - this._trackLeftOffset;\n }\n return this._width;\n }\n\n // ─────────────────────────────────────────────────────────────────────────\n // Target Observer\n // ─────────────────────────────────────────────────────────────────────────\n\n /**\n * Watch for async content loading from child media elements.\n * When media finishes loading, increment the epoch to invalidate cached thumbnails.\n */\n private _watchChildContentLoading(target: EFTimegroup): void {\n const mediaElements = target.querySelectorAll('ef-video, ef-image, ef-audio');\n \n for (const el of mediaElements) {\n // Watch EFVideo/EFAudio mediaEngineTask completion\n const mediaEngine = (el as any).mediaEngineTask;\n if (mediaEngine?.taskComplete) {\n mediaEngine.taskComplete.then(() => {\n if (this._targetElement === target) {\n target.incrementContentEpoch();\n // Reset layout params to force slot recreation with new epoch\n this._lastLayoutParams = null;\n this._scheduleRender();\n }\n }).catch(() => {\n // Ignore abort errors\n });\n }\n \n // Watch EFImage fetchImage completion \n const fetchTask = (el as any).fetchImage;\n if (fetchTask?.taskComplete) {\n fetchTask.taskComplete.then(() => {\n if (this._targetElement === target) {\n target.incrementContentEpoch();\n // Reset layout params to force slot recreation with new epoch\n this._lastLayoutParams = null;\n this._scheduleRender();\n }\n }).catch(() => {\n // Ignore abort errors\n });\n }\n }\n }\n\n private _setupTargetObserver(target: EFVideo | EFTimegroup): void {\n if (isEFVideo(target)) {\n // Watch video property changes\n this._mutationObserver = new MutationObserver(() => this._scheduleRender());\n this._mutationObserver.observe(target, {\n attributes: true,\n attributeFilter: [\"trimstart\", \"trimend\", \"sourcein\", \"sourceout\", \"src\"],\n });\n\n // Wait for media engine\n target.updateComplete.then(() => {\n if (this._targetElement !== target) return;\n target.mediaEngineTask?.taskComplete.then(() => {\n if (this._targetElement !== target) return;\n this._scheduleRender();\n });\n });\n } else if (isEFTimegroup(target)) {\n // Watch timegroup structure and content changes\n this._mutationObserver = new MutationObserver((mutations) => {\n // Double-check that mutations are actually content-changing\n // (defensive check in case attributeFilter doesn't catch everything)\n const hasContentChange = mutations.some((mutation) => {\n if (mutation.type === \"childList\") {\n return mutation.addedNodes.length > 0 || mutation.removedNodes.length > 0;\n }\n if (mutation.type === \"attributes\") {\n const attrName = mutation.attributeName;\n // Skip time/playback attributes that might slip through\n if (attrName === \"currenttime\" || attrName === \"current-time\" || \n attrName === \"playing\" || attrName === \"loop\") {\n return false;\n }\n // Only count visual content attributes\n return attrName === \"src\" || attrName === \"asset-id\" || \n attrName === \"style\" || attrName === \"transform\";\n }\n return false;\n });\n \n // Only increment epoch and schedule render if content actually changed\n if (hasContentChange) {\n const epochBefore = target.contentEpoch;\n target.incrementContentEpoch();\n const epochAfter = target.contentEpoch;\n \n // Only schedule render if epoch actually changed\n // (defensive check in case incrementContentEpoch was called elsewhere)\n if (epochAfter !== epochBefore) {\n // Reset layout params to force slot recreation with new epoch\n this._lastLayoutParams = null;\n \n // Check if new children were added\n const hasNewChildren = mutations.some(m => m.addedNodes.length > 0);\n \n // Re-watch content loading for new children\n if (hasNewChildren) {\n this._watchChildContentLoading(target);\n }\n \n this._scheduleRender();\n }\n }\n });\n this._mutationObserver.observe(target, {\n childList: true,\n subtree: true,\n attributes: true,\n // Only watch attributes that affect visual content\n // Exclude time/playback attributes (currenttime, playing, loop) and trim/source attributes\n // (those affect which part of content is shown, not the content itself)\n attributeFilter: [\"src\", \"asset-id\", \"style\", \"transform\"],\n });\n\n // Watch for async content loading from child media elements\n this._watchChildContentLoading(target);\n\n // Watch for duration becoming available\n if (target.durationMs === 0) {\n const checkDuration = () => {\n if (this._targetElement !== target) return;\n if (target.durationMs > 0) {\n this._scheduleRender();\n } else {\n requestAnimationFrame(checkDuration);\n }\n };\n requestAnimationFrame(checkDuration);\n }\n }\n }\n\n // ─────────────────────────────────────────────────────────────────────────\n // Rendering Pipeline\n // ─────────────────────────────────────────────────────────────────────────\n\n private _scheduleRender(): void {\n if (this._renderRequested) return;\n this._renderRequested = true;\n\n requestAnimationFrame(() => {\n this._renderRequested = false;\n \n this._calculateLayout();\n this._checkCache();\n this._drawCanvas();\n \n // Load any pending thumbnails (has its own epoch and state checks)\n this._loadVisibleThumbnails();\n\n // Check if we should dispatch ready event\n // (e.g., all thumbnails were already cached, or nothing to load)\n this._checkAndDispatchReady();\n });\n }\n\n /**\n * Check if thumbnails are ready and dispatch event if not already done.\n */\n private _checkAndDispatchReady(): void {\n if (this._hasLoadedThumbnails) return;\n\n // Consider ready if we have layout and either:\n // 1. Have some cached thumbnails, or\n // 2. Have no pending thumbnails (nothing to load)\n const hasLayout = this._thumbnailSlots.length > 0;\n const hasAnyCached = this._thumbnailSlots.some((s) => s.status === \"cached\");\n const hasPending = this._thumbnailSlots.some((s) => s.status === \"pending\");\n\n if (hasLayout && (hasAnyCached || !hasPending)) {\n this._hasLoadedThumbnails = true;\n this.dispatchEvent(new CustomEvent(\"thumbnails-ready\", { bubbles: true }));\n }\n }\n\n /**\n * Calculate thumbnail layout based on current dimensions and time range.\n * Only recreates slots if layout parameters have actually changed.\n */\n private _calculateLayout(): void {\n if (this._width <= 0 || this._height <= 0 || !this._targetElement) {\n this._thumbnailSlots = [];\n this._lastLayoutParams = null;\n return;\n }\n\n const timeRange = this._getTimeRange();\n if (timeRange.endMs <= timeRange.startMs) {\n this._thumbnailSlots = [];\n this._lastLayoutParams = null;\n return;\n }\n\n // Calculate thumbnail dimensions\n const thumbWidth = this._getEffectiveThumbnailWidth();\n const gap = this.gap;\n\n // Check if layout parameters have changed\n const currentParams = {\n width: this._width,\n height: this._height,\n startTimeMs: timeRange.startMs,\n endTimeMs: timeRange.endMs,\n thumbWidth,\n gap,\n };\n\n // If layout parameters haven't changed, preserve existing slots\n if (this._lastLayoutParams &&\n this._lastLayoutParams.width === currentParams.width &&\n this._lastLayoutParams.height === currentParams.height &&\n this._lastLayoutParams.startTimeMs === currentParams.startTimeMs &&\n this._lastLayoutParams.endTimeMs === currentParams.endTimeMs &&\n this._lastLayoutParams.thumbWidth === currentParams.thumbWidth &&\n this._lastLayoutParams.gap === currentParams.gap) {\n // Layout hasn't changed, keep existing slots\n return;\n }\n\n // Layout changed - recreate slots\n this._lastLayoutParams = currentParams;\n\n // Calculate how many thumbnails fit\n const count = Math.max(1, Math.floor((this._width + gap) / (thumbWidth + gap)));\n\n // Calculate pitch (spacing) for edge-to-edge fill\n const pitch = count > 1 ? (this._width - thumbWidth) / (count - 1) : 0;\n\n // Generate slots with timestamps\n const slots: ThumbnailSlot[] = [];\n const duration = timeRange.endMs - timeRange.startMs;\n\n for (let i = 0; i < count; i++) {\n const timeMs = count === 1\n ? (timeRange.startMs + timeRange.endMs) / 2\n : timeRange.startMs + (i * duration) / (count - 1);\n\n slots.push({\n timeMs,\n x: Math.round(i * pitch),\n width: thumbWidth,\n status: \"pending\",\n });\n }\n\n this._thumbnailSlots = slots;\n }\n\n /**\n * Get effective time range for thumbnails.\n */\n private _getTimeRange(): { startMs: number; endMs: number } {\n const target = this._targetElement;\n if (!target) return { startMs: 0, endMs: 0 };\n\n if (isEFVideo(target)) {\n if (this.useIntrinsicDuration) {\n // Intrinsic mode: 0 to full source duration\n return {\n startMs: this.startTimeMs ?? 0,\n endMs: this.endTimeMs ?? target.intrinsicDurationMs ?? 0,\n };\n }\n // Trimmed mode: source coordinates\n const sourceStart = target.sourceStartMs ?? 0;\n const trimmedDuration = target.durationMs ?? 0;\n return {\n startMs: this.startTimeMs !== undefined ? sourceStart + this.startTimeMs : sourceStart,\n endMs: this.endTimeMs !== undefined ? sourceStart + this.endTimeMs : sourceStart + trimmedDuration,\n };\n }\n\n // Timegroup\n return {\n startMs: this.startTimeMs ?? 0,\n endMs: (this.endTimeMs && this.endTimeMs > 0) ? this.endTimeMs : target.durationMs ?? 0,\n };\n }\n\n /**\n * Calculate effective thumbnail width (auto or specified).\n */\n private _getEffectiveThumbnailWidth(): number {\n if (this.thumbnailWidth > 0) return this.thumbnailWidth;\n\n const target = this._targetElement;\n let aspectRatio = DEFAULT_ASPECT_RATIO;\n\n if (isEFVideo(target)) {\n const w = (target as any).videoWidth || 1920;\n const h = (target as any).videoHeight || 1080;\n aspectRatio = w / h;\n } else if (isEFTimegroup(target)) {\n const w = target.offsetWidth || 1920;\n const h = target.offsetHeight || 1080;\n aspectRatio = w / h;\n }\n\n return Math.round(this._height * aspectRatio);\n }\n\n /**\n * Check cache for existing thumbnails.\n */\n private _checkCache(): void {\n if (!this._targetElement) return;\n\n const { rootId, elementId, epoch } = getCacheIdentifiers(this._targetElement);\n\n for (const slot of this._thumbnailSlots) {\n const key = getCacheKey(rootId, elementId, slot.timeMs, epoch);\n if (sessionThumbnailCache.has(key)) {\n slot.imageData = sessionThumbnailCache.get(key);\n slot.status = \"cached\";\n } else {\n // Mark as pending if not in cache\n slot.status = \"pending\";\n }\n }\n }\n\n /**\n * Draw the canvas with current thumbnail state.\n * Canvas is absolutely positioned at the visible portion of the strip.\n * Uses virtual rendering - only draws thumbnails in the visible region.\n */\n private _drawCanvas(): void {\n const canvas = this.canvasRef.value;\n if (!canvas) return;\n\n const ctx = canvas.getContext(\"2d\", { willReadFrequently: true });\n if (!ctx) return;\n\n const stripWidth = this._width;\n const height = this._height;\n\n if (stripWidth <= 0 || height <= 0) return;\n\n const dpr = window.devicePixelRatio || 1;\n \n // Get scroll and viewport info\n const scrollLeft = this._currentScrollLeft;\n const viewportWidth = this._viewportWidth;\n \n // Get this strip's absolute position in the timeline\n const stripStartPx = this._getStripTimelinePosition();\n const stripEndPx = stripStartPx + stripWidth;\n \n // Calculate visible region in timeline coordinates (with padding)\n const visibleLeftPx = scrollLeft - VIRTUAL_RENDER_PADDING_PX;\n const visibleRightPx = scrollLeft + viewportWidth + VIRTUAL_RENDER_PADDING_PX;\n \n // Check if strip is visible at all\n if (stripEndPx < visibleLeftPx || stripStartPx > visibleRightPx) {\n canvas.style.display = \"none\";\n return;\n }\n canvas.style.display = \"block\";\n \n // Calculate the intersection: what part of the strip is visible\n // Coordinates relative to strip's left edge (0 = strip start)\n const visibleStartInStrip = Math.max(0, visibleLeftPx - stripStartPx);\n const visibleEndInStrip = Math.min(stripWidth, visibleRightPx - stripStartPx);\n const visibleWidthPx = visibleEndInStrip - visibleStartInStrip;\n \n if (visibleWidthPx <= 0) {\n canvas.style.display = \"none\";\n return;\n }\n\n // Set canvas size with DPR\n const targetWidth = Math.ceil(visibleWidthPx * dpr);\n const targetHeight = Math.ceil(height * dpr);\n\n if (canvas.width !== targetWidth || canvas.height !== targetHeight) {\n canvas.width = targetWidth;\n canvas.height = targetHeight;\n }\n \n // Position canvas at the visible portion within the strip\n canvas.style.left = `${visibleStartInStrip}px`;\n canvas.style.width = `${visibleWidthPx}px`;\n canvas.style.height = `${height}px`;\n\n // Reset transform\n ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n\n // Clear with background\n ctx.fillStyle = \"#1a1a2e\";\n ctx.fillRect(0, 0, visibleWidthPx, height);\n\n // Draw each visible thumbnail\n for (const slot of this._thumbnailSlots) {\n const slotRight = slot.x + slot.width;\n\n // Skip if slot is outside visible strip region\n if (slotRight < visibleStartInStrip || slot.x > visibleEndInStrip) continue;\n\n // Draw position relative to canvas (canvas starts at visibleStartInStrip)\n const drawX = slot.x - visibleStartInStrip;\n\n // Skip if outside canvas bounds\n if (drawX + slot.width < 0 || drawX > visibleWidthPx) continue;\n\n if (slot.imageData) {\n this._drawThumbnailImage(ctx, slot.imageData, drawX, slot.width, height);\n } else {\n // Placeholder\n ctx.fillStyle = slot.status === \"loading\" ? \"#2d2d50\" : \"#2d2d44\";\n ctx.fillRect(drawX, 0, slot.width, height);\n\n // Loading indicator\n if (slot.status === \"loading\") {\n ctx.fillStyle = \"rgba(59, 130, 246, 0.3)\";\n ctx.fillRect(drawX, 0, slot.width, 2);\n }\n }\n }\n }\n\n /**\n * Draw a thumbnail image with cover mode scaling.\n */\n private _drawThumbnailImage(\n ctx: CanvasRenderingContext2D,\n imageData: ImageData,\n x: number,\n width: number,\n height: number,\n ): void {\n // Create temp canvas for ImageData\n const tempCanvas = document.createElement(\"canvas\");\n tempCanvas.width = imageData.width;\n tempCanvas.height = imageData.height;\n const tempCtx = tempCanvas.getContext(\"2d\");\n if (!tempCtx) return;\n tempCtx.putImageData(imageData, 0, 0);\n\n // Cover mode: crop to fill destination\n const srcAspect = imageData.width / imageData.height;\n const dstAspect = width / height;\n\n let srcX = 0, srcY = 0, srcW = imageData.width, srcH = imageData.height;\n\n if (srcAspect > dstAspect) {\n srcW = imageData.height * dstAspect;\n srcX = (imageData.width - srcW) / 2;\n } else {\n srcH = imageData.width / dstAspect;\n srcY = (imageData.height - srcH) / 2;\n }\n\n ctx.drawImage(tempCanvas, srcX, srcY, srcW, srcH, x, 0, width, height);\n }\n\n // ─────────────────────────────────────────────────────────────────────────\n // Thumbnail Loading\n // ─────────────────────────────────────────────────────────────────────────\n\n /**\n * Load thumbnails that are visible in the current viewport.\n * Skips loading if the epoch hasn't changed since last load.\n */\n private async _loadVisibleThumbnails(): Promise<void> {\n if (this._captureInProgress || !this._targetElement) return;\n\n // For timegroups, check if epoch has changed since last load\n // If not, don't reload - the cache should already have the thumbnails\n if (isEFTimegroup(this._targetElement)) {\n const currentEpoch = this._targetElement.contentEpoch;\n if (this._lastLoadedEpoch !== null && this._lastLoadedEpoch === currentEpoch) {\n // Epoch hasn't changed, check if all visible slots are already cached\n // Only proceed if there are actually pending slots that need loading\n const hasPendingSlots = this._thumbnailSlots.some(s => s.status === \"pending\");\n if (!hasPendingSlots) {\n return;\n }\n }\n }\n\n const viewportWidth = this._viewportWidth;\n const scrollOffset = this._currentScrollLeft;\n const stripWidth = this._width;\n \n // Get strip's timeline position\n const stripStartPx = this._getStripTimelinePosition();\n \n // Calculate visible region in timeline coordinates\n const visibleLeftPx = scrollOffset - VIRTUAL_RENDER_PADDING_PX;\n const visibleRightPx = scrollOffset + viewportWidth + VIRTUAL_RENDER_PADDING_PX;\n \n // Convert to strip-local coordinates\n const visibleStartInStrip = Math.max(0, visibleLeftPx - stripStartPx);\n const visibleEndInStrip = Math.min(stripWidth, visibleRightPx - stripStartPx);\n\n // Find pending slots in visible range (using strip-local coordinates)\n const pending = this._thumbnailSlots.filter((slot) => {\n if (slot.status !== \"pending\") return false;\n const slotRight = slot.x + slot.width;\n return slotRight >= visibleStartInStrip && slot.x <= visibleEndInStrip;\n });\n\n if (pending.length === 0) return;\n\n this._captureInProgress = true;\n\n // Mark as loading\n for (const slot of pending) {\n slot.status = \"loading\";\n }\n this._drawCanvas();\n\n try {\n if (isEFTimegroup(this._targetElement)) {\n await this._captureTimegroupThumbnails(pending);\n } else if (isEFVideo(this._targetElement)) {\n await this._captureVideoThumbnails(pending);\n }\n } catch (error) {\n console.warn(\"Failed to capture thumbnails:\", error);\n // Reset failed slots\n for (const slot of pending) {\n if (slot.status === \"loading\") {\n slot.status = \"pending\";\n }\n }\n } finally {\n this._captureInProgress = false;\n this._drawCanvas();\n\n // Update last loaded epoch for timegroups\n if (isEFTimegroup(this._targetElement)) {\n this._lastLoadedEpoch = this._targetElement.contentEpoch;\n }\n\n // Dispatch ready event when thumbnails are first loaded\n const hasAnyLoaded = this._thumbnailSlots.some((s) => s.status === \"cached\");\n if (hasAnyLoaded && !this._hasLoadedThumbnails) {\n this._hasLoadedThumbnails = true;\n this.dispatchEvent(new CustomEvent(\"thumbnails-ready\", { bubbles: true }));\n }\n }\n }\n\n /**\n * Capture thumbnails from a timegroup target.\n */\n private async _captureTimegroupThumbnails(slots: ThumbnailSlot[]): Promise<void> {\n const target = this._targetElement as EFTimegroup;\n const { rootId, elementId, epoch } = getCacheIdentifiers(target);\n\n // Calculate capture scale\n const timegroupWidth = target.offsetWidth || 1920;\n const timegroupHeight = target.offsetHeight || 1080;\n const scale = Math.min(1, this._height / timegroupHeight, MAX_CAPTURE_WIDTH / timegroupWidth);\n\n // Process in batches\n for (let i = 0; i < slots.length; i += BATCH_SIZE) {\n const batch = slots.slice(i, i + BATCH_SIZE);\n const timestamps = batch.map((s) => s.timeMs);\n\n try {\n const canvases = await target.captureBatch(timestamps, {\n scale,\n contentReadyMode: \"immediate\",\n });\n\n for (let j = 0; j < batch.length; j++) {\n const slot = batch[j]!;\n const canvas = canvases[j];\n\n if (canvas) {\n const imageData = this._canvasToImageData(canvas);\n if (imageData) {\n const key = getCacheKey(rootId, elementId, slot.timeMs, epoch);\n sessionThumbnailCache.set(key, imageData, slot.timeMs, elementId);\n slot.imageData = imageData;\n slot.status = \"cached\";\n }\n }\n }\n\n // Redraw after each batch for progressive feedback\n this._drawCanvas();\n\n // Yield to main thread between batches\n if (i + BATCH_SIZE < slots.length) {\n await new Promise((r) => requestAnimationFrame(r));\n }\n } catch (error) {\n console.warn(\"Batch capture failed:\", error);\n }\n }\n }\n\n /**\n * Capture thumbnails from a video target using MediaEngine.\n */\n private async _captureVideoThumbnails(slots: ThumbnailSlot[]): Promise<void> {\n const target = this._targetElement as EFVideo;\n const { rootId, elementId, epoch } = getCacheIdentifiers(target);\n\n // Wait for media engine\n if (target.mediaEngineTask) {\n await target.mediaEngineTask.taskComplete;\n }\n\n const mediaEngine = target.mediaEngineTask?.value;\n if (!mediaEngine) return;\n\n // Check for video rendition\n const videoRendition = mediaEngine.getVideoRendition();\n const scrubRendition = mediaEngine.getScrubVideoRendition();\n if (!videoRendition && !scrubRendition) return;\n\n const timestamps = slots.map((s) => s.timeMs);\n\n // Create an abort controller for this thumbnail extraction\n // ThumbnailExtractor requires a signal to properly handle cleanup\n const abortController = new AbortController();\n\n try {\n const results = await mediaEngine.extractThumbnails(timestamps, abortController.signal);\n\n for (let i = 0; i < slots.length; i++) {\n const slot = slots[i]!;\n const result = results[i];\n\n if (result?.thumbnail) {\n const imageData = this._canvasToImageData(result.thumbnail);\n if (imageData) {\n const key = getCacheKey(rootId, elementId, slot.timeMs, epoch);\n sessionThumbnailCache.set(key, imageData, slot.timeMs, elementId);\n slot.imageData = imageData;\n slot.status = \"cached\";\n }\n }\n }\n } catch (error) {\n // Abort on error to clean up any in-flight requests\n abortController.abort();\n console.warn(\"Video thumbnail extraction failed:\", error);\n }\n }\n\n /**\n * Convert canvas to ImageData.\n */\n private _canvasToImageData(canvas: HTMLCanvasElement | OffscreenCanvas): ImageData | null {\n const ctx = canvas.getContext(\"2d\", { willReadFrequently: true }) as\n | CanvasRenderingContext2D\n | OffscreenCanvasRenderingContext2D\n | null;\n if (!ctx) return null;\n return ctx.getImageData(0, 0, canvas.width, canvas.height);\n }\n\n // ─────────────────────────────────────────────────────────────────────────\n // Public API\n // ─────────────────────────────────────────────────────────────────────────\n\n /**\n * Returns a promise that resolves when thumbnails are ready.\n * Resolves immediately if thumbnails are already loaded.\n */\n whenReady(): Promise<void> {\n if (this._hasLoadedThumbnails) {\n return Promise.resolve();\n }\n return new Promise((resolve) => {\n this.addEventListener(\"thumbnails-ready\", () => resolve(), { once: true });\n });\n }\n\n /**\n * Check if thumbnails have been loaded.\n */\n get isReady(): boolean {\n return this._hasLoadedThumbnails;\n }\n\n // ─────────────────────────────────────────────────────────────────────────\n // Cache Invalidation\n // ─────────────────────────────────────────────────────────────────────────\n\n /**\n * Invalidate cached thumbnails for this element within a time range.\n * Call this when content changes at specific times.\n */\n invalidateTimeRange(startTimeMs: number, endTimeMs: number): void {\n if (!this._targetElement) return;\n\n const { rootId, elementId } = getCacheIdentifiers(this._targetElement);\n sessionThumbnailCache.invalidateTimeRange(rootId, elementId, startTimeMs, endTimeMs);\n\n // Reset affected slots\n for (const slot of this._thumbnailSlots) {\n if (slot.timeMs >= startTimeMs && slot.timeMs <= endTimeMs) {\n slot.imageData = undefined;\n slot.status = \"pending\";\n }\n }\n\n this._scheduleRender();\n }\n\n /**\n * Invalidate all cached thumbnails for this element.\n */\n invalidateAll(): void {\n if (!this._targetElement) return;\n\n const { rootId, elementId } = getCacheIdentifiers(this._targetElement);\n sessionThumbnailCache.invalidateElement(rootId, elementId);\n\n // Reset all slots\n for (const slot of this._thumbnailSlots) {\n slot.imageData = undefined;\n slot.status = \"pending\";\n }\n\n this._scheduleRender();\n }\n\n // ─────────────────────────────────────────────────────────────────────────\n // Render\n // ─────────────────────────────────────────────────────────────────────────\n\n render() {\n return html`<canvas ${ref(this.canvasRef)}></canvas>`;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"ef-thumbnail-strip\": EFThumbnailStrip;\n }\n}\n\n// Re-export cache for backwards compatibility and debugging\nexport { sessionThumbnailCache as thumbnailImageCache } from \"./SessionThumbnailCache.js\";\n"],"mappings":"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"}
@@ -1,15 +1,16 @@
1
1
  import { TemporalMixinInterface } from "./EFTemporal.js";
2
- import { ContextMixinInterface } from "../gui/ContextMixin.js";
3
2
  import { EFMedia } from "./EFMedia.js";
3
+ import { ContainerInfo } from "./ContainerInfo.js";
4
+ import { ElementPositionInfo } from "./ElementPositionInfo.js";
5
+ import { CaptureBatchOptions, CaptureOptions } from "../preview/renderTimegroupToCanvas.js";
6
+ import { RenderToVideoOptions } from "../preview/renderTimegroupToVideo.js";
4
7
  import { Task } from "@lit/task";
5
8
  import * as lit0 from "lit";
6
9
  import { LitElement, PropertyValues } from "lit";
7
10
  import * as lit_html0 from "lit-html";
8
11
 
9
12
  //#region src/elements/EFTimegroup.d.ts
10
- declare global {
11
- var EF_DEV_WORKBENCH: boolean | undefined;
12
- }
13
+
13
14
  type FrameTaskCallback = (info: {
14
15
  ownCurrentTimeMs: number;
15
16
  currentTimeMs: number;
@@ -17,47 +18,175 @@ type FrameTaskCallback = (info: {
17
18
  percentComplete: number;
18
19
  element: EFTimegroup;
19
20
  }) => void | Promise<void>;
21
+ /**
22
+ * Result of createRenderClone() - contains the clone, its container, and cleanup function.
23
+ */
24
+ interface RenderCloneResult {
25
+ /** The cloned timegroup, fully functional with its own time state */
26
+ clone: EFTimegroup;
27
+ /** The offscreen container holding the clone */
28
+ container: HTMLElement;
29
+ /** Call this to remove the clone from DOM and clean up */
30
+ cleanup: () => void;
31
+ }
32
+ /**
33
+ * Initializer function type for setting up JavaScript behavior on timegroup instances.
34
+ * This function is called on both the prime timeline and each render clone.
35
+ *
36
+ * CONSTRAINTS:
37
+ * - MUST be synchronous (no async/await, no Promise return)
38
+ * - MUST complete in <100ms (error) or <10ms (warning)
39
+ * - Should only register callbacks and set up behavior, not do expensive work
40
+ */
41
+ type TimegroupInitializer = (timegroup: EFTimegroup) => void;
42
+ /**
43
+ * The four timegroup modes define how duration is calculated:
44
+ * - "fit": Inherits duration from parent timegroup
45
+ * - "fixed": Uses explicit duration attribute
46
+ * - "sequence": Sum of child durations minus overlaps
47
+ * - "contain": Maximum of child durations
48
+ */
49
+ type TimeMode = "fit" | "fixed" | "sequence" | "contain";
20
50
  declare const EFTimegroup_base: (new (...args: any[]) => TemporalMixinInterface) & typeof LitElement;
21
51
  declare class EFTimegroup extends EFTimegroup_base {
22
52
  #private;
23
53
  static get observedAttributes(): string[];
24
54
  static styles: lit0.CSSResult;
55
+ /** @internal */
25
56
  _timeGroupContext: this;
57
+ /** @internal */
26
58
  efContext: this;
27
- mode: "fit" | "fixed" | "sequence" | "contain";
59
+ /** @public */
60
+ mode: TimeMode;
61
+ /** @public */
28
62
  overlapMs: number;
63
+ /**
64
+ * Initializer function for setting up JavaScript behavior on this timegroup.
65
+ * This function is called on both the prime timeline and each render clone.
66
+ *
67
+ * REQUIRED for render operations (captureBatch, renderToVideo, createRenderClone).
68
+ *
69
+ * CONSTRAINTS:
70
+ * - MUST be synchronous (no async/await, no Promise return)
71
+ * - MUST complete in <100ms (error thrown) or <10ms (warning logged)
72
+ * - Should only register callbacks and set up behavior, not do expensive work
73
+ *
74
+ * @example
75
+ * ```javascript
76
+ * const tg = document.querySelector('ef-timegroup');
77
+ * tg.initializer = (instance) => {
78
+ * instance.addFrameCallback((time) => {
79
+ * // Update content based on time
80
+ * });
81
+ * };
82
+ * ```
83
+ * @public
84
+ */
85
+ initializer?: TimegroupInitializer;
86
+ /** @public */
29
87
  fps: number;
88
+ /**
89
+ * When true, automatically seeks to frame 0 after media durations are loaded.
90
+ * Only applies to root timegroups (timegroups that are not nested inside another timegroup).
91
+ * This ensures the first frame is rendered immediately on initialization.
92
+ */
93
+ autoInit: boolean;
94
+ /**
95
+ * When true, automatically wraps this root timegroup with an ef-workbench element.
96
+ * The workbench provides development UI including hierarchy panel, timeline, and playback controls.
97
+ * Only applies to root timegroups.
98
+ * @public
99
+ */
100
+ workbench: boolean;
30
101
  attributeChangedCallback(name: string, old: string | null, value: string | null): void;
102
+ /** @public */
31
103
  fit: "none" | "contain" | "cover";
104
+ /** @internal */
105
+ isRestoringFromLocalStorage(): boolean;
106
+ /** @internal - Used by PlaybackController to set restoration state */
107
+ setRestoringFromLocalStorage(value: boolean): void;
32
108
  /**
33
109
  * Get the effective FPS for this timegroup.
34
110
  * During rendering, uses the render options FPS if available.
35
111
  * Otherwise uses the configured fps property.
112
+ * @public
36
113
  */
37
114
  get effectiveFps(): number;
38
115
  /**
39
- * Quantize a time value to the nearest frame boundary based on effectiveFps.
40
- * @param timeSeconds - Time in seconds
41
- * @returns Time quantized to frame boundaries in seconds
116
+ * Get the current content epoch (used by thumbnail cache).
117
+ * The epoch increments whenever visual content changes.
118
+ * @public
42
119
  */
43
- private quantizeToFrameTime;
44
- private runThrottledFrameTask;
120
+ get contentEpoch(): number;
121
+ /**
122
+ * Increment content epoch (called when visual content changes).
123
+ * This invalidates cached thumbnails by changing their cache keys.
124
+ * @public
125
+ */
126
+ incrementContentEpoch(): void;
127
+ /** @public */
45
128
  set currentTime(time: number);
129
+ /** @public */
46
130
  get currentTime(): number;
131
+ /** @public */
47
132
  set currentTimeMs(ms: number);
133
+ /** @public */
48
134
  get currentTimeMs(): number;
135
+ /**
136
+ * The time the user last requested via seek/scrub.
137
+ * Preview systems should use this instead of currentTimeMs to avoid
138
+ * seeing intermediate times during batch operations (thumbnails, export).
139
+ * @public
140
+ */
141
+ get userTimeMs(): number;
49
142
  /**
50
143
  * Seek to a specific time and wait for all frames to be ready.
51
144
  * This is the recommended way to seek in tests and programmatic control.
52
145
  *
146
+ * Combines seeking (Purpose 3) with frame rendering (Purpose 4) to ensure
147
+ * all visible elements are ready after the seek completes.
148
+ *
149
+ * Updates both the source time AND userTimeMs (what the preview displays).
150
+ *
53
151
  * @param timeMs - Time in milliseconds to seek to
54
152
  * @returns Promise that resolves when the seek is complete and all visible children are ready
153
+ * @public
55
154
  */
56
155
  seek(timeMs: number): Promise<void>;
156
+ /**
157
+ * Optimized seek for render loops.
158
+ * Unlike `seek()`, this:
159
+ * - Skips waitForMediaDurations (already loaded at render setup)
160
+ * - Skips localStorage persistence
161
+ * - Consolidates awaits to reduce event loop yields
162
+ *
163
+ * Still waits for all content to be ready (Lit updates, frame tasks, video frames).
164
+ *
165
+ * @param timeMs - Time in milliseconds to seek to
166
+ * @internal
167
+ */
168
+ seekForRender(timeMs: number): Promise<void>;
57
169
  /**
58
170
  * Determines if this is a root timegroup (no parent timegroups)
171
+ * @public
59
172
  */
60
173
  get isRootTimegroup(): boolean;
174
+ /**
175
+ * Property-based frame task callback for React integration.
176
+ * When set, automatically registers the callback as a frame task.
177
+ * Setting a new value automatically cleans up the previous callback.
178
+ * Set to null or undefined to remove the callback.
179
+ *
180
+ * @example
181
+ * // React usage:
182
+ * <Timegroup onFrame={({ ownCurrentTimeMs, percentComplete }) => {
183
+ * // Per-frame updates
184
+ * }} />
185
+ *
186
+ * @public
187
+ */
188
+ get onFrame(): FrameTaskCallback | null;
189
+ set onFrame(callback: FrameTaskCallback | null | undefined);
61
190
  /**
62
191
  * Register a custom frame task callback that will be executed during frame rendering.
63
192
  * The callback receives timing information and can be async or sync.
@@ -65,64 +194,143 @@ declare class EFTimegroup extends EFTimegroup_base {
65
194
  *
66
195
  * @param callback - Function to execute on each frame
67
196
  * @returns A cleanup function that removes the callback when called
197
+ * @public
68
198
  */
69
199
  addFrameTask(callback: FrameTaskCallback): () => void;
70
200
  /**
71
201
  * Remove a previously registered custom frame task callback.
72
202
  *
73
203
  * @param callback - The callback function to remove
204
+ * @public
74
205
  */
75
206
  removeFrameTask(callback: FrameTaskCallback): void;
207
+ /** @internal */
76
208
  saveTimeToLocalStorage(time: number): void;
77
209
  render(): lit_html0.TemplateResult<1>;
210
+ /** @internal */
78
211
  loadTimeFromLocalStorage(): number | undefined;
79
212
  connectedCallback(): void;
213
+ /**
214
+ * Called when this timegroup becomes a root (no parent timegroup).
215
+ * Sets up the playback listener after PlaybackController is created.
216
+ * @internal
217
+ */
218
+ didBecomeRoot(): void;
80
219
  protected updated(changedProperties: PropertyValues): void;
81
220
  disconnectedCallback(): void;
221
+ /**
222
+ * Capture the timegroup at a specific timestamp as a canvas.
223
+ * Does NOT modify currentTimeMs - captures are rendered independently.
224
+ *
225
+ * @param options - Capture options including timeMs, scale, contentReadyMode
226
+ * @returns Promise resolving to an HTMLCanvasElement with the captured frame
227
+ * @public
228
+ */
229
+ captureAtTime(options: CaptureOptions): Promise<HTMLCanvasElement>;
230
+ /**
231
+ * Capture multiple timestamps as canvas thumbnails in a single batch.
232
+ *
233
+ * CLONE-TIMELINE ARCHITECTURE:
234
+ * Creates a single render clone and reuses it across all captures.
235
+ * Prime-timeline is NEVER seeked - user can continue previewing/editing during capture.
236
+ *
237
+ * @param timestamps - Array of timestamps (in milliseconds) to capture
238
+ * @param options - Capture options (scale, contentReadyMode, blockingTimeoutMs)
239
+ * @returns Promise resolving to array of HTMLCanvasElements
240
+ * @public
241
+ */
242
+ captureBatch(timestamps: number[], options?: CaptureBatchOptions): Promise<HTMLCanvasElement[]>;
243
+ /**
244
+ * Render the timegroup to an MP4 video file and trigger download.
245
+ * Captures each frame at the specified fps, encodes using WebCodecs via
246
+ * MediaBunny, and downloads the resulting video.
247
+ *
248
+ * @param options - Rendering options (fps, codec, bitrate, filename, etc.)
249
+ * @returns Promise that resolves when video is downloaded
250
+ * @public
251
+ */
252
+ renderToVideo(options?: RenderToVideoOptions): Promise<Uint8Array | undefined>;
253
+ /**
254
+ * Create an independent clone of this timegroup for rendering.
255
+ * The clone is a fully functional ef-timegroup with its own animations
256
+ * and time state, isolated from the original (Prime-timeline).
257
+ *
258
+ * OPTIONAL: An initializer can be set via `timegroup.initializer = (tg) => { ... }`
259
+ * to re-run JavaScript setup (frame callbacks, React components) on each clone.
260
+ *
261
+ * This enables:
262
+ * - Rendering without affecting user's preview position
263
+ * - Concurrent renders with different clones
264
+ * - Re-running JavaScript setup on each clone (if initializer is provided)
265
+ *
266
+ * @returns Promise resolving to clone, container, and cleanup function
267
+ * @throws Error if initializer is async or takes too long
268
+ * @public
269
+ */
270
+ createRenderClone(): Promise<RenderCloneResult>;
271
+ /** @internal */
82
272
  get storageKey(): string;
273
+ /** @internal */
83
274
  get intrinsicDurationMs(): number | undefined;
275
+ /** @internal */
84
276
  get hasOwnDuration(): boolean;
277
+ /** @public */
85
278
  get durationMs(): number;
86
- getPendingFrameTasks(signal?: AbortSignal): Promise<Task<readonly unknown[], unknown>[]>;
87
- waitForNestedUpdates(signal?: AbortSignal): Promise<void>;
88
- waitForFrameTasks(): Promise<void>;
89
- mediaDurationsPromise: Promise<void> | undefined;
90
- waitForMediaDurations(): Promise<void>;
279
+ /** @internal */
280
+ waitForFrameTasks(signal?: AbortSignal): Promise<void>;
281
+ /** @internal */
282
+ waitForMediaDurations(signal?: AbortSignal): Promise<void>;
283
+ /** @internal */
91
284
  get childTemporals(): TemporalMixinInterface[];
92
- get contextProvider(): ContextMixinInterface | null;
93
285
  /**
94
286
  * Returns true if the timegroup should be wrapped with a workbench.
95
287
  *
96
288
  * A timegroup should be wrapped with a workbench if:
97
289
  * - It's being rendered (EF_RENDERING), OR
98
- * - It's in interactive mode (EF_INTERACTIVE) with the dev workbench flag set
290
+ * - The workbench property is set to true
99
291
  *
100
292
  * If the timegroup is already wrapped in a context provider like ef-preview,
101
293
  * it should NOT be wrapped in a workbench.
294
+ * @internal
102
295
  */
103
296
  shouldWrapWithWorkbench(): boolean;
297
+ /** @internal */
104
298
  wrapWithWorkbench(): void;
105
- get efElements(): Element[];
106
299
  /**
107
300
  * Returns media elements for playback audio rendering
108
301
  * For standalone media, returns [this]; for timegroups, returns all descendants
109
302
  * Used by PlaybackController for audio-driven playback
303
+ * @internal
110
304
  */
111
305
  getMediaElements(): EFMedia[];
112
306
  /**
113
307
  * Render audio buffer for playback
114
308
  * Called by PlaybackController during live playback
115
309
  * Delegates to shared renderTemporalAudio utility for consistent behavior
310
+ * @internal
116
311
  */
117
- renderAudio(fromMs: number, toMs: number): Promise<AudioBuffer>;
118
- /**
119
- * TEMPORARY TEST METHOD: Renders audio and immediately plays it back
120
- * Usage: timegroup.testPlayAudio(0, 5000) // Play first 5 seconds
121
- */
122
- testPlayAudio(fromMs: number, toMs: number): Promise<void>;
123
- loadMd5Sums(): Promise<void>;
312
+ renderAudio(fromMs: number, toMs: number, signal?: AbortSignal): Promise<AudioBuffer>;
313
+ static readonly TIMEGROUP_FRAME_TASK_THRESHOLD = 100;
314
+ static readonly TIMEGROUP_FRAME_TASK_RESET_MS = 1000;
315
+ /** @internal */
124
316
  frameTask: Task<readonly [number, number], void>;
317
+ /** @internal */
125
318
  seekTask: Task<readonly [number | undefined], number | undefined>;
319
+ /**
320
+ * Get container information for this timegroup.
321
+ * Timegroups are always containers and can contain children.
322
+ * Display mode is determined from computed styles.
323
+ *
324
+ * @public
325
+ */
326
+ getContainerInfo(): ContainerInfo;
327
+ /**
328
+ * Get position information for this timegroup.
329
+ * Returns computed bounds, transform, and rotation.
330
+ *
331
+ * @public
332
+ */
333
+ getPositionInfo(): ElementPositionInfo | null;
126
334
  }
127
335
  declare global {
128
336
  interface HTMLElementTagNameMap {