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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (324) hide show
  1. package/dist/EF_FRAMEGEN.d.ts +5 -0
  2. package/dist/EF_FRAMEGEN.js +20 -4
  3. package/dist/EF_FRAMEGEN.js.map +1 -1
  4. package/dist/EF_INTERACTIVE.js.map +1 -1
  5. package/dist/_virtual/rolldown_runtime.js +27 -0
  6. package/dist/canvas/EFCanvas.d.ts +311 -0
  7. package/dist/canvas/EFCanvas.js +1089 -0
  8. package/dist/canvas/EFCanvas.js.map +1 -0
  9. package/dist/canvas/EFCanvasItem.d.ts +55 -0
  10. package/dist/canvas/EFCanvasItem.js +72 -0
  11. package/dist/canvas/EFCanvasItem.js.map +1 -0
  12. package/dist/canvas/api/CanvasAPI.d.ts +115 -0
  13. package/dist/canvas/api/CanvasAPI.js +182 -0
  14. package/dist/canvas/api/CanvasAPI.js.map +1 -0
  15. package/dist/canvas/api/types.d.ts +42 -0
  16. package/dist/canvas/coordinateTransform.js +90 -0
  17. package/dist/canvas/coordinateTransform.js.map +1 -0
  18. package/dist/canvas/getElementBounds.js +40 -0
  19. package/dist/canvas/getElementBounds.js.map +1 -0
  20. package/dist/canvas/overlays/SelectionOverlay.js +265 -0
  21. package/dist/canvas/overlays/SelectionOverlay.js.map +1 -0
  22. package/dist/canvas/overlays/overlayState.js +153 -0
  23. package/dist/canvas/overlays/overlayState.js.map +1 -0
  24. package/dist/canvas/selection/SelectionController.js +105 -0
  25. package/dist/canvas/selection/SelectionController.js.map +1 -0
  26. package/dist/canvas/selection/SelectionModel.d.ts +98 -0
  27. package/dist/canvas/selection/SelectionModel.js +229 -0
  28. package/dist/canvas/selection/SelectionModel.js.map +1 -0
  29. package/dist/canvas/selection/selectionContext.d.ts +31 -0
  30. package/dist/canvas/selection/selectionContext.js +12 -0
  31. package/dist/canvas/selection/selectionContext.js.map +1 -0
  32. package/dist/elements/ContainerInfo.d.ts +29 -0
  33. package/dist/elements/ContainerInfo.js +30 -0
  34. package/dist/elements/ContainerInfo.js.map +1 -0
  35. package/dist/elements/EFAudio.d.ts +13 -3
  36. package/dist/elements/EFAudio.js +64 -10
  37. package/dist/elements/EFAudio.js.map +1 -1
  38. package/dist/elements/EFCaptions.d.ts +18 -16
  39. package/dist/elements/EFCaptions.js +110 -19
  40. package/dist/elements/EFCaptions.js.map +1 -1
  41. package/dist/elements/EFImage.d.ts +12 -2
  42. package/dist/elements/EFImage.js +79 -9
  43. package/dist/elements/EFImage.js.map +1 -1
  44. package/dist/elements/EFMedia/AssetIdMediaEngine.js +51 -4
  45. package/dist/elements/EFMedia/AssetIdMediaEngine.js.map +1 -1
  46. package/dist/elements/EFMedia/AssetMediaEngine.js +125 -52
  47. package/dist/elements/EFMedia/AssetMediaEngine.js.map +1 -1
  48. package/dist/elements/EFMedia/BaseMediaEngine.js +24 -6
  49. package/dist/elements/EFMedia/BaseMediaEngine.js.map +1 -1
  50. package/dist/elements/EFMedia/JitMediaEngine.js +12 -8
  51. package/dist/elements/EFMedia/JitMediaEngine.js.map +1 -1
  52. package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.js +46 -7
  53. package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.js.map +1 -1
  54. package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.js +98 -73
  55. package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.js.map +1 -1
  56. package/dist/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.js +28 -5
  57. package/dist/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.js.map +1 -1
  58. package/dist/elements/EFMedia/audioTasks/makeAudioInputTask.js +18 -6
  59. package/dist/elements/EFMedia/audioTasks/makeAudioInputTask.js.map +1 -1
  60. package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.js +8 -2
  61. package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.js.map +1 -1
  62. package/dist/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.js +31 -6
  63. package/dist/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.js.map +1 -1
  64. package/dist/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.js +28 -5
  65. package/dist/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.js.map +1 -1
  66. package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.js +97 -72
  67. package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.js.map +1 -1
  68. package/dist/elements/EFMedia/shared/AudioSpanUtils.js +3 -1
  69. package/dist/elements/EFMedia/shared/AudioSpanUtils.js.map +1 -1
  70. package/dist/elements/EFMedia/shared/BufferUtils.js +1 -1
  71. package/dist/elements/EFMedia/shared/BufferUtils.js.map +1 -1
  72. package/dist/elements/EFMedia/shared/ThumbnailExtractor.js +25 -14
  73. package/dist/elements/EFMedia/shared/ThumbnailExtractor.js.map +1 -1
  74. package/dist/elements/EFMedia/tasks/makeMediaEngineTask.js +47 -16
  75. package/dist/elements/EFMedia/tasks/makeMediaEngineTask.js.map +1 -1
  76. package/dist/elements/EFMedia/videoTasks/MainVideoInputCache.js +37 -19
  77. package/dist/elements/EFMedia/videoTasks/MainVideoInputCache.js.map +1 -1
  78. package/dist/elements/EFMedia/videoTasks/ScrubInputCache.js +65 -21
  79. package/dist/elements/EFMedia/videoTasks/ScrubInputCache.js.map +1 -1
  80. package/dist/elements/EFMedia/videoTasks/makeScrubVideoBufferTask.js +8 -3
  81. package/dist/elements/EFMedia/videoTasks/makeScrubVideoBufferTask.js.map +1 -1
  82. package/dist/elements/EFMedia/videoTasks/makeScrubVideoInitSegmentFetchTask.js +32 -9
  83. package/dist/elements/EFMedia/videoTasks/makeScrubVideoInitSegmentFetchTask.js.map +1 -1
  84. package/dist/elements/EFMedia/videoTasks/makeScrubVideoInputTask.js +33 -10
  85. package/dist/elements/EFMedia/videoTasks/makeScrubVideoInputTask.js.map +1 -1
  86. package/dist/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.js +23 -8
  87. package/dist/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.js.map +1 -1
  88. package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentFetchTask.js +34 -10
  89. package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentFetchTask.js.map +1 -1
  90. package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentIdTask.js +31 -8
  91. package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentIdTask.js.map +1 -1
  92. package/dist/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.js +31 -114
  93. package/dist/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.js.map +1 -1
  94. package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.js +44 -8
  95. package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.js.map +1 -1
  96. package/dist/elements/EFMedia.d.ts +18 -7
  97. package/dist/elements/EFMedia.js +23 -3
  98. package/dist/elements/EFMedia.js.map +1 -1
  99. package/dist/elements/EFPanZoom.d.ts +96 -0
  100. package/dist/elements/EFPanZoom.js +290 -0
  101. package/dist/elements/EFPanZoom.js.map +1 -0
  102. package/dist/elements/EFSourceMixin.js +7 -6
  103. package/dist/elements/EFSourceMixin.js.map +1 -1
  104. package/dist/elements/EFSurface.d.ts +6 -6
  105. package/dist/elements/EFSurface.js +7 -2
  106. package/dist/elements/EFSurface.js.map +1 -1
  107. package/dist/elements/EFTemporal.d.ts +2 -1
  108. package/dist/elements/EFTemporal.js +192 -71
  109. package/dist/elements/EFTemporal.js.map +1 -1
  110. package/dist/elements/EFText.d.ts +5 -4
  111. package/dist/elements/EFText.js +102 -13
  112. package/dist/elements/EFText.js.map +1 -1
  113. package/dist/elements/EFTextSegment.d.ts +32 -6
  114. package/dist/elements/EFTextSegment.js +53 -15
  115. package/dist/elements/EFTextSegment.js.map +1 -1
  116. package/dist/elements/EFThumbnailStrip.d.ts +118 -56
  117. package/dist/elements/EFThumbnailStrip.js +522 -358
  118. package/dist/elements/EFThumbnailStrip.js.map +1 -1
  119. package/dist/elements/EFTimegroup.d.ts +223 -27
  120. package/dist/elements/EFTimegroup.js +850 -147
  121. package/dist/elements/EFTimegroup.js.map +1 -1
  122. package/dist/elements/EFVideo.d.ts +42 -5
  123. package/dist/elements/EFVideo.js +165 -11
  124. package/dist/elements/EFVideo.js.map +1 -1
  125. package/dist/elements/EFWaveform.d.ts +6 -6
  126. package/dist/elements/EFWaveform.js +2 -1
  127. package/dist/elements/EFWaveform.js.map +1 -1
  128. package/dist/elements/ElementPositionInfo.d.ts +35 -0
  129. package/dist/elements/ElementPositionInfo.js +49 -0
  130. package/dist/elements/ElementPositionInfo.js.map +1 -0
  131. package/dist/elements/FetchMixin.js +16 -1
  132. package/dist/elements/FetchMixin.js.map +1 -1
  133. package/dist/elements/SessionThumbnailCache.js +152 -0
  134. package/dist/elements/SessionThumbnailCache.js.map +1 -0
  135. package/dist/elements/TargetController.js +3 -1
  136. package/dist/elements/TargetController.js.map +1 -1
  137. package/dist/elements/TimegroupController.js +9 -3
  138. package/dist/elements/TimegroupController.js.map +1 -1
  139. package/dist/elements/findRootTemporal.js +30 -0
  140. package/dist/elements/findRootTemporal.js.map +1 -0
  141. package/dist/elements/renderTemporalAudio.js +18 -5
  142. package/dist/elements/renderTemporalAudio.js.map +1 -1
  143. package/dist/elements/updateAnimations.js +171 -28
  144. package/dist/elements/updateAnimations.js.map +1 -1
  145. package/dist/getRenderInfo.d.ts +2 -2
  146. package/dist/gui/ContextMixin.js +4 -2
  147. package/dist/gui/ContextMixin.js.map +1 -1
  148. package/dist/gui/Controllable.js +74 -1
  149. package/dist/gui/Controllable.js.map +1 -1
  150. package/dist/gui/EFActiveRootTemporal.d.ts +50 -0
  151. package/dist/gui/EFActiveRootTemporal.js +94 -0
  152. package/dist/gui/EFActiveRootTemporal.js.map +1 -0
  153. package/dist/gui/EFConfiguration.d.ts +11 -5
  154. package/dist/gui/EFConfiguration.js.map +1 -1
  155. package/dist/gui/EFControls.d.ts +2 -2
  156. package/dist/gui/EFControls.js +109 -13
  157. package/dist/gui/EFControls.js.map +1 -1
  158. package/dist/gui/EFDial.d.ts +4 -4
  159. package/dist/gui/EFFilmstrip.d.ts +11 -214
  160. package/dist/gui/EFFilmstrip.js +53 -1152
  161. package/dist/gui/EFFilmstrip.js.map +1 -1
  162. package/dist/gui/EFFitScale.d.ts +3 -3
  163. package/dist/gui/EFFitScale.js +39 -12
  164. package/dist/gui/EFFitScale.js.map +1 -1
  165. package/dist/gui/EFFocusOverlay.d.ts +4 -4
  166. package/dist/gui/EFOverlayItem.d.ts +48 -0
  167. package/dist/gui/EFOverlayItem.js +97 -0
  168. package/dist/gui/EFOverlayItem.js.map +1 -0
  169. package/dist/gui/EFOverlayLayer.d.ts +70 -0
  170. package/dist/gui/EFOverlayLayer.js +104 -0
  171. package/dist/gui/EFOverlayLayer.js.map +1 -0
  172. package/dist/gui/EFPause.d.ts +4 -4
  173. package/dist/gui/EFPlay.d.ts +4 -4
  174. package/dist/gui/EFResizableBox.d.ts +12 -16
  175. package/dist/gui/EFResizableBox.js +109 -451
  176. package/dist/gui/EFResizableBox.js.map +1 -1
  177. package/dist/gui/EFScrubber.d.ts +30 -5
  178. package/dist/gui/EFScrubber.js +224 -31
  179. package/dist/gui/EFScrubber.js.map +1 -1
  180. package/dist/gui/EFTimeDisplay.d.ts +4 -4
  181. package/dist/gui/EFTimeDisplay.js +4 -1
  182. package/dist/gui/EFTimeDisplay.js.map +1 -1
  183. package/dist/gui/EFTimelineRuler.d.ts +71 -0
  184. package/dist/gui/EFTimelineRuler.js +320 -0
  185. package/dist/gui/EFTimelineRuler.js.map +1 -0
  186. package/dist/gui/EFToggleLoop.d.ts +4 -4
  187. package/dist/gui/EFTogglePlay.d.ts +4 -4
  188. package/dist/gui/EFTransformHandles.d.ts +91 -0
  189. package/dist/gui/EFTransformHandles.js +393 -0
  190. package/dist/gui/EFTransformHandles.js.map +1 -0
  191. package/dist/gui/EFWorkbench.d.ts +182 -4
  192. package/dist/gui/EFWorkbench.js +2067 -22
  193. package/dist/gui/EFWorkbench.js.map +1 -1
  194. package/dist/gui/FitScaleHelpers.d.ts +31 -0
  195. package/dist/gui/FitScaleHelpers.js +41 -0
  196. package/dist/gui/FitScaleHelpers.js.map +1 -0
  197. package/dist/gui/PlaybackController.d.ts +2 -1
  198. package/dist/gui/PlaybackController.js +46 -15
  199. package/dist/gui/PlaybackController.js.map +1 -1
  200. package/dist/gui/TWMixin.js +1 -1
  201. package/dist/gui/TWMixin.js.map +1 -1
  202. package/dist/gui/hierarchy/EFHierarchy.d.ts +65 -0
  203. package/dist/gui/hierarchy/EFHierarchy.js +338 -0
  204. package/dist/gui/hierarchy/EFHierarchy.js.map +1 -0
  205. package/dist/gui/hierarchy/EFHierarchyItem.d.ts +118 -0
  206. package/dist/gui/hierarchy/EFHierarchyItem.js +551 -0
  207. package/dist/gui/hierarchy/EFHierarchyItem.js.map +1 -0
  208. package/dist/gui/hierarchy/hierarchyContext.d.ts +38 -0
  209. package/dist/gui/hierarchy/hierarchyContext.js +8 -0
  210. package/dist/gui/hierarchy/hierarchyContext.js.map +1 -0
  211. package/dist/gui/icons.js +34 -0
  212. package/dist/gui/icons.js.map +1 -0
  213. package/dist/gui/panZoomTransformContext.js +12 -0
  214. package/dist/gui/panZoomTransformContext.js.map +1 -0
  215. package/dist/gui/previewSettingsContext.js +12 -0
  216. package/dist/gui/previewSettingsContext.js.map +1 -0
  217. package/dist/gui/timeline/EFTimeline.d.ts +270 -0
  218. package/dist/gui/timeline/EFTimeline.js +1369 -0
  219. package/dist/gui/timeline/EFTimeline.js.map +1 -0
  220. package/dist/gui/timeline/EFTimelineRow.js +374 -0
  221. package/dist/gui/timeline/EFTimelineRow.js.map +1 -0
  222. package/dist/gui/timeline/TrimHandles.d.ts +36 -0
  223. package/dist/gui/timeline/TrimHandles.js +204 -0
  224. package/dist/gui/timeline/TrimHandles.js.map +1 -0
  225. package/dist/gui/timeline/flattenHierarchy.js +31 -0
  226. package/dist/gui/timeline/flattenHierarchy.js.map +1 -0
  227. package/dist/gui/timeline/timelineStateContext.d.ts +26 -0
  228. package/dist/gui/timeline/timelineStateContext.js +42 -0
  229. package/dist/gui/timeline/timelineStateContext.js.map +1 -0
  230. package/dist/gui/timeline/tracks/AudioTrack.js +264 -0
  231. package/dist/gui/timeline/tracks/AudioTrack.js.map +1 -0
  232. package/dist/gui/timeline/tracks/CaptionsTrack.js +595 -0
  233. package/dist/gui/timeline/tracks/CaptionsTrack.js.map +1 -0
  234. package/dist/gui/timeline/tracks/HTMLTrack.js +19 -0
  235. package/dist/gui/timeline/tracks/HTMLTrack.js.map +1 -0
  236. package/dist/gui/timeline/tracks/ImageTrack.js +53 -0
  237. package/dist/gui/timeline/tracks/ImageTrack.js.map +1 -0
  238. package/dist/gui/timeline/tracks/TextTrack.js +250 -0
  239. package/dist/gui/timeline/tracks/TextTrack.js.map +1 -0
  240. package/dist/gui/timeline/tracks/TimegroupTrack.js +143 -0
  241. package/dist/gui/timeline/tracks/TimegroupTrack.js.map +1 -0
  242. package/dist/gui/timeline/tracks/TrackItem.js +269 -0
  243. package/dist/gui/timeline/tracks/TrackItem.js.map +1 -0
  244. package/dist/gui/timeline/tracks/VideoTrack.js +265 -0
  245. package/dist/gui/timeline/tracks/VideoTrack.js.map +1 -0
  246. package/dist/gui/timeline/tracks/WaveformTrack.js +19 -0
  247. package/dist/gui/timeline/tracks/WaveformTrack.js.map +1 -0
  248. package/dist/gui/timeline/tracks/ensureTrackItemInit.js +1 -0
  249. package/dist/gui/timeline/tracks/preloadTracks.js +9 -0
  250. package/dist/gui/timeline/tracks/renderTrackChildren.js +119 -0
  251. package/dist/gui/timeline/tracks/renderTrackChildren.js.map +1 -0
  252. package/dist/gui/timeline/tracks/waveformUtils.js +80 -0
  253. package/dist/gui/timeline/tracks/waveformUtils.js.map +1 -0
  254. package/dist/gui/transformCalculations.js +217 -0
  255. package/dist/gui/transformCalculations.js.map +1 -0
  256. package/dist/gui/transformUtils.d.ts +37 -0
  257. package/dist/gui/transformUtils.js +77 -0
  258. package/dist/gui/transformUtils.js.map +1 -0
  259. package/dist/gui/tree/EFTree.d.ts +59 -0
  260. package/dist/gui/tree/EFTree.js +174 -0
  261. package/dist/gui/tree/EFTree.js.map +1 -0
  262. package/dist/gui/tree/EFTreeItem.d.ts +38 -0
  263. package/dist/gui/tree/EFTreeItem.js +146 -0
  264. package/dist/gui/tree/EFTreeItem.js.map +1 -0
  265. package/dist/gui/tree/treeContext.d.ts +60 -0
  266. package/dist/gui/tree/treeContext.js +23 -0
  267. package/dist/gui/tree/treeContext.js.map +1 -0
  268. package/dist/index.d.ts +32 -8
  269. package/dist/index.js +30 -6
  270. package/dist/index.js.map +1 -1
  271. package/dist/node_modules/react/cjs/react-jsx-runtime.development.js +688 -0
  272. package/dist/node_modules/react/cjs/react-jsx-runtime.development.js.map +1 -0
  273. package/dist/node_modules/react/cjs/react.development.js +1521 -0
  274. package/dist/node_modules/react/cjs/react.development.js.map +1 -0
  275. package/dist/node_modules/react/index.js +13 -0
  276. package/dist/node_modules/react/index.js.map +1 -0
  277. package/dist/node_modules/react/jsx-runtime.js +13 -0
  278. package/dist/node_modules/react/jsx-runtime.js.map +1 -0
  279. package/dist/preview/AdaptiveResolutionTracker.js +228 -0
  280. package/dist/preview/AdaptiveResolutionTracker.js.map +1 -0
  281. package/dist/preview/RenderProfiler.js +135 -0
  282. package/dist/preview/RenderProfiler.js.map +1 -0
  283. package/dist/preview/previewSettings.js +131 -0
  284. package/dist/preview/previewSettings.js.map +1 -0
  285. package/dist/preview/previewTypes.js +64 -0
  286. package/dist/preview/previewTypes.js.map +1 -0
  287. package/dist/preview/renderTimegroupPreview.js +656 -0
  288. package/dist/preview/renderTimegroupPreview.js.map +1 -0
  289. package/dist/preview/renderTimegroupToCanvas.d.ts +37 -0
  290. package/dist/preview/renderTimegroupToCanvas.js +840 -0
  291. package/dist/preview/renderTimegroupToCanvas.js.map +1 -0
  292. package/dist/preview/renderTimegroupToVideo.d.ts +39 -0
  293. package/dist/preview/renderTimegroupToVideo.js +274 -0
  294. package/dist/preview/renderTimegroupToVideo.js.map +1 -0
  295. package/dist/preview/renderers.js +16 -0
  296. package/dist/preview/renderers.js.map +1 -0
  297. package/dist/preview/statsTrackingStrategy.js +201 -0
  298. package/dist/preview/statsTrackingStrategy.js.map +1 -0
  299. package/dist/preview/thumbnailCacheSettings.js +52 -0
  300. package/dist/preview/thumbnailCacheSettings.js.map +1 -0
  301. package/dist/preview/workers/WorkerPool.js +178 -0
  302. package/dist/preview/workers/WorkerPool.js.map +1 -0
  303. package/dist/sandbox/PlaybackControls.js +10 -0
  304. package/dist/sandbox/PlaybackControls.js.map +1 -0
  305. package/dist/sandbox/ScenarioRunner.js +1 -0
  306. package/dist/sandbox/index.js +2 -0
  307. package/dist/style.css +68 -67
  308. package/dist/transcoding/types/index.d.ts +2 -1
  309. package/dist/transcoding/utils/UrlGenerator.d.ts +6 -1
  310. package/dist/transcoding/utils/UrlGenerator.js +12 -3
  311. package/dist/transcoding/utils/UrlGenerator.js.map +1 -1
  312. package/dist/utils/LRUCache.js +1 -375
  313. package/dist/utils/LRUCache.js.map +1 -1
  314. package/dist/utils/frameTime.js +14 -0
  315. package/dist/utils/frameTime.js.map +1 -0
  316. package/package.json +3 -3
  317. package/test/profilingPlugin.ts +223 -0
  318. package/test/recordReplayProxyPlugin.js +22 -27
  319. package/test/thumbnail-performance-test.html +116 -0
  320. package/test/visualRegressionUtils.ts +286 -0
  321. package/types.json +1 -1
  322. package/dist/elements/TimegroupController.d.ts +0 -18
  323. package/dist/msToTimeCode.js +0 -17
  324. package/dist/msToTimeCode.js.map +0 -1
@@ -1,165 +1,91 @@
1
1
  import { __decorate } from "../_virtual/_@oxc-project_runtime@0.94.0/helpers/decorate.js";
2
- import { OrderedLRUCache } from "../utils/LRUCache.js";
3
2
  import { TargetController } from "./TargetController.js";
4
- import { Task } from "@lit/task";
3
+ import { findRootTemporal } from "./findRootTemporal.js";
4
+ import { timelineStateContext } from "../gui/timeline/timelineStateContext.js";
5
+ import { getCacheKey, sessionThumbnailCache } from "./SessionThumbnailCache.js";
6
+ import { consume } from "@lit/context";
5
7
  import { LitElement, css, html } from "lit";
6
8
  import { customElement, property, state } from "lit/decorators.js";
7
9
  import { createRef, ref } from "lit/directives/ref.js";
8
10
 
9
11
  //#region src/elements/EFThumbnailStrip.ts
10
- /**
11
- * Global thumbnail image cache for smooth resize performance
12
- * Shared across all thumbnail strip instances
13
- * Uses OrderedLRUCache for efficient timestamp-based searching
14
- */
15
- const thumbnailImageCache = new OrderedLRUCache(200, (a, b) => {
16
- const partsA = a.split(":");
17
- const partsB = b.split(":");
18
- return Number.parseFloat(partsA[partsA.length - 1] || "0") - Number.parseFloat(partsB[partsB.length - 1] || "0");
19
- });
20
- globalThis.debugThumbnailCache = thumbnailImageCache;
21
- /**
22
- * Quantize timestamp to 30fps frame boundaries for consistent caching
23
- * This eliminates cache misses from floating point precision differences
24
- */
25
- function quantizeTimestamp(timeMs) {
26
- const frameIntervalMs = 1e3 / 30;
27
- return Math.round(timeMs / frameIntervalMs) * frameIntervalMs;
12
+ /** Type guard to check if element is EFVideo */
13
+ function isEFVideo(element) {
14
+ return element?.tagName.toLowerCase() === "ef-video";
28
15
  }
29
- /**
30
- * Generate cache key for thumbnail image data (dimension-independent, quantized)
31
- */
32
- function getThumbnailCacheKey(videoSrc, timeMs) {
33
- return `${videoSrc}:${quantizeTimestamp(timeMs)}`;
16
+ /** Type guard to check if element is EFTimegroup */
17
+ function isEFTimegroup(element) {
18
+ return element?.tagName.toLowerCase() === "ef-timegroup";
34
19
  }
35
- const THUMBNAIL_GAP = 1;
36
- const STRIP_BORDER_PADDING = 4;
37
20
  /**
38
- * Calculate optimal thumbnail count and timestamps for the strip
39
- * Groups thumbnails by scrub segment ID for efficient caching
21
+ * Get identifiers for cache key generation.
22
+ * Returns rootId (for cache isolation) and elementId (for element-specific caching).
40
23
  */
41
- function calculateThumbnailLayout(stripWidth, thumbnailWidth, startTimeMs, endTimeMs, scrubSegmentDurationMs) {
42
- if (stripWidth <= 0 || thumbnailWidth <= 0 || endTimeMs <= startTimeMs) return {
43
- count: 0,
44
- segments: []
45
- };
46
- const thumbnailPitch = thumbnailWidth + THUMBNAIL_GAP;
47
- const baseFitCount = Math.floor(stripWidth / thumbnailPitch);
48
- const count = Math.max(1, baseFitCount + 1);
49
- const timestamps = [];
50
- const timeRange = endTimeMs - startTimeMs;
51
- for (let i = 0; i < count; i++) {
52
- const timeMs = count === 1 ? (startTimeMs + endTimeMs) / 2 : startTimeMs + i * timeRange / (count - 1);
53
- timestamps.push(timeMs);
54
- }
55
- const segmentMap = /* @__PURE__ */ new Map();
56
- for (const timeMs of timestamps) {
57
- const segmentId = scrubSegmentDurationMs ? Math.floor(timeMs / scrubSegmentDurationMs) : 0;
58
- if (!segmentMap.has(segmentId)) segmentMap.set(segmentId, []);
59
- segmentMap.get(segmentId).push({ timeMs });
60
- }
24
+ function getCacheIdentifiers(element) {
25
+ const rootTemporal = findRootTemporal(element);
61
26
  return {
62
- count,
63
- segments: Array.from(segmentMap.entries()).sort(([a], [b]) => a - b).map(([segmentId, thumbnails]) => ({
64
- segmentId,
65
- thumbnails
66
- }))
27
+ rootId: (rootTemporal && isEFTimegroup(rootTemporal) ? rootTemporal : null)?.id || "default",
28
+ elementId: isEFVideo(element) ? element.src || element.id || "video" : element.id || "timegroup"
67
29
  };
68
30
  }
31
+ /** Padding in pixels for virtual rendering (render extra thumbnails beyond viewport) */
32
+ const VIRTUAL_RENDER_PADDING_PX = 200;
33
+ /** Default gap between thumbnails */
34
+ const DEFAULT_GAP = 4;
35
+ /** Default aspect ratio if unknown */
36
+ const DEFAULT_ASPECT_RATIO = 16 / 9;
37
+ /** Max canvas width for thumbnail captures */
38
+ const MAX_CAPTURE_WIDTH = 480;
39
+ /** Thumbnails to capture per batch */
40
+ const BATCH_SIZE = 10;
69
41
  let EFThumbnailStrip = class EFThumbnailStrip$1 extends LitElement {
70
42
  constructor(..._args) {
71
43
  super(..._args);
72
44
  this.canvasRef = createRef();
73
- this._targetController = new TargetController(this);
74
- this._targetElement = null;
75
45
  this.target = "";
76
- this.thumbnailWidth = 80;
46
+ this.thumbnailWidth = 0;
47
+ this.gap = DEFAULT_GAP;
77
48
  this.useIntrinsicDuration = false;
78
- this._stripWidth = 0;
79
- this._stripHeight = 48;
80
- this._thumbnailUpdateInProgress = false;
81
- this._pendingThumbnailUpdate = false;
82
- this.thumbnailLayoutTask = new Task(this, {
83
- autoRun: false,
84
- task: async ([stripWidth, thumbnailWidth, targetElement, startTimeMs, endTimeMs, useIntrinsicDuration, mediaEngine]) => {
85
- if (stripWidth <= 0 || !targetElement) return {
86
- count: 0,
87
- segments: []
88
- };
89
- if (!mediaEngine) {
90
- if (targetElement.mediaEngineTask) {
91
- await targetElement.mediaEngineTask.taskComplete;
92
- const readyMediaEngine = targetElement.mediaEngineTask.value;
93
- if (!readyMediaEngine) return {
94
- count: 0,
95
- segments: []
96
- };
97
- return this.calculateLayoutWithMediaEngine(stripWidth, thumbnailWidth, targetElement, startTimeMs, endTimeMs, useIntrinsicDuration, readyMediaEngine);
98
- }
99
- return {
100
- count: 0,
101
- segments: []
102
- };
103
- }
104
- return this.calculateLayoutWithMediaEngine(stripWidth, thumbnailWidth, targetElement, startTimeMs, endTimeMs, useIntrinsicDuration, mediaEngine);
105
- },
106
- args: () => [
107
- this.stripWidth,
108
- this.thumbnailWidth,
109
- this.targetElement,
110
- this.startTimeMs,
111
- this.endTimeMs,
112
- this.useIntrinsicDuration,
113
- this.targetElement?.mediaEngineTask?.value
114
- ]
115
- });
116
- this.thumbnailRenderTask = new Task(this, {
117
- autoRun: false,
118
- task: async ([layout, targetElement, thumbnailWidth]) => {
119
- if (!layout || !targetElement) return [];
120
- return this.renderThumbnails(layout, targetElement, thumbnailWidth);
121
- },
122
- args: () => [
123
- this.thumbnailLayoutTask.value || null,
124
- this.targetElement,
125
- this.thumbnailWidth
126
- ]
127
- });
49
+ this.pixelsPerMs = .1;
50
+ this._targetController = new TargetController(this);
51
+ this._targetElement = null;
52
+ this._width = 0;
53
+ this._height = 0;
54
+ this._scrollContainer = null;
55
+ this._currentScrollLeft = 0;
56
+ this._trackLeftOffset = 0;
57
+ this._thumbnailSlots = [];
58
+ this._captureInProgress = false;
59
+ this._renderRequested = false;
60
+ this._hasLoadedThumbnails = false;
61
+ this._onScroll = () => {
62
+ if (!this._scrollContainer) return;
63
+ this._currentScrollLeft = this._scrollContainer.scrollLeft;
64
+ this._drawCanvas();
65
+ if (!this._scrollFrame) this._scrollFrame = requestAnimationFrame(() => {
66
+ this._scrollFrame = void 0;
67
+ this._loadVisibleThumbnails();
68
+ });
69
+ };
128
70
  }
129
71
  static {
130
72
  this.styles = [css`
131
73
  :host {
132
74
  display: block;
133
75
  position: relative;
134
- width: 100%;
135
- height: 48px; /* Default filmstrip height */
136
- background: #2a2a2a;
137
- border: 2px solid #333;
138
- border-radius: 6px;
76
+ background: #1a1a2e;
139
77
  overflow: hidden;
140
- box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.3);
78
+ width: 100%;
79
+ height: 100%;
141
80
  }
142
81
  canvas {
143
82
  display: block;
83
+ /* Absolute positioning - we manually position at visible region */
144
84
  position: absolute;
145
85
  top: 0;
146
- left: 0;
147
- image-rendering: pixelated; /* Keep thumbnails crisp */
148
- /* Width and height set programmatically to prevent CSS scaling */
149
- }
150
- .loading-overlay {
151
- position: absolute;
152
- top: 0;
153
- left: 0;
154
- right: 0;
155
- bottom: 0;
156
- background: rgba(42, 42, 42, 0.9);
157
- display: flex;
158
- align-items: center;
159
- justify-content: center;
160
- font-size: 11px;
161
- color: #ccc;
162
- font-weight: 500;
86
+ /* Left and width set programmatically based on visible portion */
87
+ height: 100%;
88
+ image-rendering: auto;
163
89
  }
164
90
  `];
165
91
  }
@@ -169,20 +95,137 @@ let EFThumbnailStrip = class EFThumbnailStrip$1 extends LitElement {
169
95
  set targetElement(value) {
170
96
  const oldValue = this._targetElement;
171
97
  this._targetElement = value;
172
- this._videoPropertyObserver?.disconnect();
173
- if (value && value !== oldValue) {
174
- this._videoPropertyObserver = new MutationObserver((mutations) => {
175
- let shouldUpdate = false;
176
- for (const mutation of mutations) if (mutation.type === "attributes" && mutation.attributeName) {
177
- const attr = mutation.attributeName;
178
- if (attr === "trimstart" || attr === "trimend" || attr === "sourcein" || attr === "sourceout" || attr === "src") {
179
- shouldUpdate = true;
180
- break;
181
- }
98
+ this._mutationObserver?.disconnect();
99
+ if (value !== oldValue) this._hasLoadedThumbnails = false;
100
+ if (value && value !== oldValue) this._setupTargetObserver(value);
101
+ this.requestUpdate("targetElement", oldValue);
102
+ }
103
+ connectedCallback() {
104
+ super.connectedCallback();
105
+ this._resizeObserver = new ResizeObserver((entries) => {
106
+ for (const entry of entries) {
107
+ const box = entry.borderBoxSize?.[0];
108
+ this._width = box?.inlineSize ?? entry.contentRect.width;
109
+ this._height = box?.blockSize ?? entry.contentRect.height;
110
+ this._calculateTrackOffset();
111
+ this._scheduleRender();
112
+ }
113
+ });
114
+ this._resizeObserver.observe(this);
115
+ this.updateComplete.then(() => {
116
+ this._findScrollContainer();
117
+ this._scheduleRender();
118
+ });
119
+ }
120
+ disconnectedCallback() {
121
+ super.disconnectedCallback();
122
+ this._resizeObserver?.disconnect();
123
+ this._mutationObserver?.disconnect();
124
+ this._detachScrollListener();
125
+ if (this._scrollFrame) cancelAnimationFrame(this._scrollFrame);
126
+ }
127
+ updated(changedProperties) {
128
+ super.updated(changedProperties);
129
+ if (changedProperties.has("thumbnailWidth") || changedProperties.has("gap") || changedProperties.has("startTimeMs") || changedProperties.has("endTimeMs") || changedProperties.has("useIntrinsicDuration") || changedProperties.has("pixelsPerMs") || changedProperties.has("targetElement")) this._scheduleRender();
130
+ if (changedProperties.has("_timelineState")) this._onContextScroll();
131
+ }
132
+ _findScrollContainer() {
133
+ let node = this.parentNode;
134
+ while (node) {
135
+ if (node instanceof HTMLElement) {
136
+ const style = getComputedStyle(node);
137
+ if (style.overflowX === "auto" || style.overflowX === "scroll") {
138
+ this._scrollContainer = node;
139
+ this._calculateTrackOffset();
140
+ this._attachScrollListener();
141
+ return;
182
142
  }
183
- if (shouldUpdate) this.runThumbnailUpdate();
184
- });
185
- this._videoPropertyObserver.observe(value, {
143
+ }
144
+ if (node.parentNode) node = node.parentNode;
145
+ else if (node instanceof ShadowRoot) node = node.host;
146
+ else break;
147
+ }
148
+ }
149
+ /**
150
+ * Calculate the horizontal offset from scroll container's left edge to this element's track.
151
+ * This accounts for sticky labels or other elements that precede the track area.
152
+ *
153
+ * We look for our specific timeline elements (ef-timeline-row) and measure their label width.
154
+ */
155
+ _calculateTrackOffset() {
156
+ if (!this._scrollContainer) {
157
+ this._trackLeftOffset = 0;
158
+ return;
159
+ }
160
+ const timelineRow = this._findTimelineRow();
161
+ if (timelineRow) {
162
+ const labelWidth = this._getTimelineRowLabelWidth(timelineRow);
163
+ if (labelWidth > 0) {
164
+ this._trackLeftOffset = labelWidth;
165
+ return;
166
+ }
167
+ }
168
+ this._trackLeftOffset = 0;
169
+ }
170
+ /**
171
+ * Find the ef-timeline-row ancestor by walking up through shadow DOM boundaries.
172
+ */
173
+ _findTimelineRow() {
174
+ let node = this;
175
+ while (node) {
176
+ if (node instanceof Element && node.tagName.toLowerCase() === "ef-timeline-row") return node;
177
+ const parentNode = node.parentNode;
178
+ if (parentNode instanceof ShadowRoot) node = parentNode.host;
179
+ else node = parentNode;
180
+ }
181
+ return null;
182
+ }
183
+ /**
184
+ * Get the label width from an ef-timeline-row element.
185
+ * Queries the shadow root for .row-label and returns its width.
186
+ */
187
+ _getTimelineRowLabelWidth(timelineRow) {
188
+ const shadowRoot = timelineRow.shadowRoot;
189
+ if (!shadowRoot) return 0;
190
+ const rowLabel = shadowRoot.querySelector(".row-label");
191
+ if (!rowLabel) return 0;
192
+ return rowLabel.getBoundingClientRect().width;
193
+ }
194
+ /**
195
+ * Get this strip's absolute position in the timeline (pixels from timeline origin).
196
+ * Uses the target element's startTimeMs to determine position.
197
+ */
198
+ _getStripTimelinePosition() {
199
+ const target = this._targetElement;
200
+ if (!target) return 0;
201
+ if (isEFVideo(target)) return (target.startTimeMs ?? 0) * (this._timelineState?.pixelsPerMs ?? .1);
202
+ return 0;
203
+ }
204
+ _attachScrollListener() {
205
+ if (!this._scrollContainer) return;
206
+ this._scrollContainer.addEventListener("scroll", this._onScroll, { passive: true });
207
+ this._currentScrollLeft = this._scrollContainer.scrollLeft;
208
+ }
209
+ _detachScrollListener() {
210
+ if (this._scrollContainer) {
211
+ this._scrollContainer.removeEventListener("scroll", this._onScroll);
212
+ this._scrollContainer = null;
213
+ }
214
+ }
215
+ _onContextScroll() {
216
+ if (!this._timelineState || this._scrollContainer) return;
217
+ this._currentScrollLeft = this._timelineState.viewportScrollLeft;
218
+ this._drawCanvas();
219
+ }
220
+ get _viewportWidth() {
221
+ if (this._timelineState?.viewportWidth) return this._timelineState.viewportWidth;
222
+ if (this._scrollContainer) return this._scrollContainer.clientWidth - this._trackLeftOffset;
223
+ return this._width;
224
+ }
225
+ _setupTargetObserver(target) {
226
+ if (isEFVideo(target)) {
227
+ this._mutationObserver = new MutationObserver(() => this._scheduleRender());
228
+ this._mutationObserver.observe(target, {
186
229
  attributes: true,
187
230
  attributeFilter: [
188
231
  "trimstart",
@@ -192,271 +235,387 @@ let EFThumbnailStrip = class EFThumbnailStrip$1 extends LitElement {
192
235
  "src"
193
236
  ]
194
237
  });
195
- if (value.mediaEngineTask) value.mediaEngineTask.taskComplete.then(() => {
196
- if (this._stripWidth > 0) this.thumbnailLayoutTask.run();
197
- }).catch(() => {});
238
+ target.updateComplete.then(() => {
239
+ if (this._targetElement !== target) return;
240
+ target.mediaEngineTask?.taskComplete.then(() => {
241
+ if (this._targetElement !== target) return;
242
+ this._scheduleRender();
243
+ });
244
+ });
245
+ } else if (isEFTimegroup(target)) {
246
+ this._mutationObserver = new MutationObserver(() => this._scheduleRender());
247
+ this._mutationObserver.observe(target, {
248
+ childList: true,
249
+ subtree: true
250
+ });
251
+ if (target.durationMs === 0) {
252
+ const checkDuration = () => {
253
+ if (this._targetElement !== target) return;
254
+ if (target.durationMs > 0) this._scheduleRender();
255
+ else requestAnimationFrame(checkDuration);
256
+ };
257
+ requestAnimationFrame(checkDuration);
258
+ }
198
259
  }
199
- this.requestUpdate("targetElement", oldValue);
200
260
  }
201
- set stripWidth(value) {
202
- if (this._thumbnailLayoutTask) {
203
- this._pendingStripWidth = value;
204
- return;
205
- }
206
- this._stripWidth = value;
207
- if (value > 0) this._thumbnailLayoutTask = this.thumbnailLayoutTask.run().then(async () => {
208
- await this.thumbnailLayoutTask.taskComplete;
209
- const layout = this.thumbnailLayoutTask.value;
210
- return layout ? this.runThumbnailRenderTask(layout) : [];
211
- }).finally(() => {
212
- this._thumbnailLayoutTask = void 0;
213
- if (this._pendingStripWidth) {
214
- this.stripWidth = this._pendingStripWidth;
215
- this._pendingStripWidth = void 0;
216
- }
261
+ _scheduleRender() {
262
+ if (this._renderRequested) return;
263
+ this._renderRequested = true;
264
+ requestAnimationFrame(() => {
265
+ this._renderRequested = false;
266
+ this._calculateLayout();
267
+ this._checkCache();
268
+ this._drawCanvas();
269
+ this._loadVisibleThumbnails();
270
+ this._checkAndDispatchReady();
217
271
  });
218
272
  }
219
- get stripWidth() {
220
- return this._stripWidth;
221
- }
222
273
  /**
223
- * Run thumbnail render task directly with provided layout (bypasses task args dependency)
274
+ * Check if thumbnails are ready and dispatch event if not already done.
224
275
  */
225
- async runThumbnailRenderTask(layout) {
226
- if (!layout || !this.targetElement || layout.count === 0) return [];
227
- return this.renderThumbnails(layout, this.targetElement, this.thumbnailWidth);
228
- }
229
- updated(changedProperties) {
230
- super.updated(changedProperties);
231
- if (this._stripWidth === 0) {
232
- const width = this.clientWidth;
233
- if (width > 0) this.stripWidth = width;
276
+ _checkAndDispatchReady() {
277
+ if (this._hasLoadedThumbnails) return;
278
+ const hasLayout = this._thumbnailSlots.length > 0;
279
+ const hasAnyCached = this._thumbnailSlots.some((s) => s.status === "cached");
280
+ const hasPending = this._thumbnailSlots.some((s) => s.status === "pending");
281
+ if (hasLayout && (hasAnyCached || !hasPending)) {
282
+ this._hasLoadedThumbnails = true;
283
+ this.dispatchEvent(new CustomEvent("thumbnails-ready", { bubbles: true }));
234
284
  }
235
- if (changedProperties.has("thumbnailWidth") || changedProperties.has("startTimeMs") || changedProperties.has("endTimeMs") || changedProperties.has("useIntrinsicDuration")) this.runThumbnailUpdate();
236
285
  }
237
286
  /**
238
- * Run thumbnail update with responsive debouncing (based on EFTimegroup currentTime pattern)
287
+ * Calculate thumbnail layout based on current dimensions and time range.
239
288
  */
240
- runThumbnailUpdate() {
241
- if (this._thumbnailUpdateInProgress) {
242
- this._pendingThumbnailUpdate = true;
289
+ _calculateLayout() {
290
+ if (this._width <= 0 || this._height <= 0 || !this._targetElement) {
291
+ this._thumbnailSlots = [];
243
292
  return;
244
293
  }
245
- this._thumbnailUpdateInProgress = true;
246
- this.thumbnailLayoutTask.run().then(async () => {
247
- await this.thumbnailLayoutTask.taskComplete;
248
- const layout = this.thumbnailLayoutTask.value;
249
- if (layout) await this.runThumbnailRenderTask(layout);
250
- }).catch(() => {}).finally(() => {
251
- this._thumbnailUpdateInProgress = false;
252
- if (this._pendingThumbnailUpdate) {
253
- this._pendingThumbnailUpdate = false;
254
- this.runThumbnailUpdate();
255
- }
256
- });
294
+ const timeRange = this._getTimeRange();
295
+ if (timeRange.endMs <= timeRange.startMs) {
296
+ this._thumbnailSlots = [];
297
+ return;
298
+ }
299
+ const thumbWidth = this._getEffectiveThumbnailWidth();
300
+ const gap = this.gap;
301
+ const count = Math.max(1, Math.floor((this._width + gap) / (thumbWidth + gap)));
302
+ const pitch = count > 1 ? (this._width - thumbWidth) / (count - 1) : 0;
303
+ const slots = [];
304
+ const duration = timeRange.endMs - timeRange.startMs;
305
+ for (let i = 0; i < count; i++) {
306
+ const timeMs = count === 1 ? (timeRange.startMs + timeRange.endMs) / 2 : timeRange.startMs + i * duration / (count - 1);
307
+ slots.push({
308
+ timeMs,
309
+ x: Math.round(i * pitch),
310
+ width: thumbWidth,
311
+ status: "pending"
312
+ });
313
+ }
314
+ this._thumbnailSlots = slots;
257
315
  }
258
316
  /**
259
- * Calculate layout with a ready media engine
317
+ * Get effective time range for thumbnails.
260
318
  */
261
- calculateLayoutWithMediaEngine(stripWidth, thumbnailWidth, targetElement, startTimeMs, endTimeMs, useIntrinsicDuration, mediaEngine) {
262
- if (useIntrinsicDuration) {
263
- const effectiveStartMs$1 = startTimeMs ?? 0;
264
- const effectiveEndMs$1 = endTimeMs ?? targetElement.intrinsicDurationMs ?? 0;
265
- return this.generateLayoutFromTimeRange(stripWidth, thumbnailWidth, effectiveStartMs$1, effectiveEndMs$1, mediaEngine);
319
+ _getTimeRange() {
320
+ const target = this._targetElement;
321
+ if (!target) return {
322
+ startMs: 0,
323
+ endMs: 0
324
+ };
325
+ if (isEFVideo(target)) {
326
+ if (this.useIntrinsicDuration) return {
327
+ startMs: this.startTimeMs ?? 0,
328
+ endMs: this.endTimeMs ?? target.intrinsicDurationMs ?? 0
329
+ };
330
+ const sourceStart = target.sourceStartMs ?? 0;
331
+ const trimmedDuration = target.durationMs ?? 0;
332
+ return {
333
+ startMs: this.startTimeMs !== void 0 ? sourceStart + this.startTimeMs : sourceStart,
334
+ endMs: this.endTimeMs !== void 0 ? sourceStart + this.endTimeMs : sourceStart + trimmedDuration
335
+ };
266
336
  }
267
- const sourceStart = targetElement.sourceStartMs ?? 0;
268
- const trimmedDuration = targetElement.durationMs ?? 0;
269
- const effectiveStartMs = startTimeMs !== void 0 ? sourceStart + startTimeMs : sourceStart;
270
- const effectiveEndMs = endTimeMs !== void 0 ? sourceStart + endTimeMs : sourceStart + trimmedDuration;
271
- return this.generateLayoutFromTimeRange(stripWidth, thumbnailWidth, effectiveStartMs, effectiveEndMs, mediaEngine);
337
+ return {
338
+ startMs: this.startTimeMs ?? 0,
339
+ endMs: this.endTimeMs && this.endTimeMs > 0 ? this.endTimeMs : target.durationMs ?? 0
340
+ };
272
341
  }
273
342
  /**
274
- * Generate layout from calculated time range
343
+ * Calculate effective thumbnail width (auto or specified).
275
344
  */
276
- generateLayoutFromTimeRange(stripWidth, thumbnailWidth, effectiveStartMs, effectiveEndMs, mediaEngine) {
277
- return calculateThumbnailLayout(stripWidth, thumbnailWidth, effectiveStartMs, effectiveEndMs, mediaEngine && typeof mediaEngine.getScrubVideoRendition === "function" ? mediaEngine.getScrubVideoRendition()?.segmentDurationMs : void 0);
345
+ _getEffectiveThumbnailWidth() {
346
+ if (this.thumbnailWidth > 0) return this.thumbnailWidth;
347
+ const target = this._targetElement;
348
+ let aspectRatio = DEFAULT_ASPECT_RATIO;
349
+ if (isEFVideo(target)) aspectRatio = (target.videoWidth || 1920) / (target.videoHeight || 1080);
350
+ else if (isEFTimegroup(target)) aspectRatio = (target.offsetWidth || 1920) / (target.offsetHeight || 1080);
351
+ return Math.round(this._height * aspectRatio);
278
352
  }
279
353
  /**
280
- * Render thumbnails with provided layout (main rendering logic)
354
+ * Check cache for existing thumbnails.
281
355
  */
282
- async renderThumbnails(layout, targetElement, thumbnailWidth) {
283
- if (!layout || !targetElement || layout.count === 0) return [];
284
- const videoSrc = targetElement.src;
285
- const availableHeight = this._stripHeight - STRIP_BORDER_PADDING;
286
- const allThumbnails = [];
287
- let thumbnailIndex = 0;
288
- for (const segment of layout.segments) for (const thumbnail of segment.thumbnails) {
289
- const cacheKey = getThumbnailCacheKey(videoSrc, thumbnail.timeMs);
290
- let imageData = thumbnailImageCache.get(cacheKey);
291
- let status = "exact-hit";
292
- let nearHitKey;
293
- if (!imageData) {
294
- const timeMinus = Math.max(0, thumbnail.timeMs - 5e3);
295
- const timePlus = thumbnail.timeMs + 5e3;
296
- const rangeStartKey = `${videoSrc}:${timeMinus}`;
297
- const rangeEndKey = `${videoSrc}:${timePlus}`;
298
- const sameVideoHits = thumbnailImageCache.findRange(rangeStartKey, rangeEndKey).filter((hit) => hit.key.startsWith(`${videoSrc}:`));
299
- if (sameVideoHits.length > 0) {
300
- const nearestHit = sameVideoHits.reduce((closest, current) => {
301
- const currentParts = current.key.split(":");
302
- const closestParts = closest.key.split(":");
303
- const currentTime = Number.parseFloat(currentParts[currentParts.length - 1] || "0");
304
- const closestTime = Number.parseFloat(closestParts[closestParts.length - 1] || "0");
305
- return Math.abs(currentTime - thumbnail.timeMs) < Math.abs(closestTime - thumbnail.timeMs) ? current : closest;
306
- });
307
- imageData = nearestHit.value;
308
- status = "near-hit";
309
- nearHitKey = nearestHit.key;
310
- } else status = "missing";
356
+ _checkCache() {
357
+ if (!this._targetElement) return;
358
+ const { rootId, elementId } = getCacheIdentifiers(this._targetElement);
359
+ for (const slot of this._thumbnailSlots) {
360
+ const key = getCacheKey(rootId, elementId, slot.timeMs);
361
+ if (sessionThumbnailCache.has(key)) {
362
+ slot.imageData = sessionThumbnailCache.get(key);
363
+ slot.status = "cached";
311
364
  }
312
- const x = thumbnailIndex * (thumbnailWidth + THUMBNAIL_GAP);
313
- allThumbnails.push({
314
- timeMs: thumbnail.timeMs,
315
- segmentId: segment.segmentId,
316
- x,
317
- width: thumbnailWidth,
318
- height: availableHeight,
319
- status,
320
- imageData,
321
- nearHitKey
322
- });
323
- thumbnailIndex++;
324
365
  }
325
- await this.drawThumbnails(allThumbnails);
326
- await this.loadMissingThumbnails(allThumbnails, targetElement);
327
- return allThumbnails;
328
- }
329
- connectedCallback() {
330
- super.connectedCallback();
331
- this.resizeObserver = new ResizeObserver((entries) => {
332
- for (const entry of entries) {
333
- const width = entry.borderBoxSize && entry.borderBoxSize.length > 0 ? entry.borderBoxSize[0]?.inlineSize : entry.contentRect.width;
334
- this._stripHeight = (entry.borderBoxSize && entry.borderBoxSize.length > 0 ? entry.borderBoxSize[0]?.blockSize : entry.contentRect.height) ?? 0;
335
- this.stripWidth = width ?? 0;
336
- }
337
- });
338
- this.resizeObserver.observe(this);
339
- this.updateComplete.then(() => {
340
- if (this._stripWidth === 0) {
341
- const width = this.clientWidth;
342
- if (width > 0) this.stripWidth = width ?? 0;
343
- }
344
- });
345
- }
346
- disconnectedCallback() {
347
- super.disconnectedCallback();
348
- this.resizeObserver?.disconnect();
349
- this.resizeObserver = void 0;
350
- this._videoPropertyObserver?.disconnect();
351
- this._videoPropertyObserver = void 0;
352
366
  }
353
367
  /**
354
- * Draw thumbnails to the canvas with cache hits and placeholders
368
+ * Draw the canvas with current thumbnail state.
369
+ * Canvas is absolutely positioned at the visible portion of the strip.
370
+ * Uses virtual rendering - only draws thumbnails in the visible region.
355
371
  */
356
- async drawThumbnails(thumbnails) {
372
+ _drawCanvas() {
357
373
  const canvas = this.canvasRef.value;
358
374
  if (!canvas) return;
359
- const ctx = canvas.getContext("2d");
375
+ const ctx = canvas.getContext("2d", { willReadFrequently: true });
360
376
  if (!ctx) return;
377
+ const stripWidth = this._width;
378
+ const height = this._height;
379
+ if (stripWidth <= 0 || height <= 0) return;
361
380
  const dpr = window.devicePixelRatio || 1;
362
- canvas.width = this._stripWidth * dpr;
363
- canvas.height = this._stripHeight * dpr;
364
- canvas.style.width = `${this._stripWidth}px`;
365
- canvas.style.height = `${this._stripHeight}px`;
366
- ctx.scale(dpr, dpr);
367
- ctx.fillStyle = "#2a2a2a";
368
- ctx.fillRect(0, 0, this._stripWidth, this._stripHeight);
369
- for (const thumb of thumbnails) if (thumb.imageData) {
370
- const tempCanvas = document.createElement("canvas");
371
- tempCanvas.width = thumb.imageData.width;
372
- tempCanvas.height = thumb.imageData.height;
373
- const tempCtx = tempCanvas.getContext("2d");
374
- if (!tempCtx) continue;
375
- tempCtx.putImageData(thumb.imageData, 0, 0);
376
- const sourceAspect = thumb.imageData.width / thumb.imageData.height;
377
- const containerAspect = thumb.width / thumb.height;
378
- let drawWidth;
379
- let drawHeight;
380
- let drawX;
381
- let drawY;
382
- if (sourceAspect > containerAspect) {
383
- drawWidth = thumb.width;
384
- drawHeight = Math.round(thumb.width / sourceAspect);
385
- drawX = thumb.x;
386
- drawY = Math.round((this._stripHeight - drawHeight) / 2);
387
- } else {
388
- drawWidth = Math.round(thumb.height * sourceAspect);
389
- drawHeight = thumb.height;
390
- drawX = thumb.x + Math.round((thumb.width - drawWidth) / 2);
391
- drawY = Math.round((this._stripHeight - drawHeight) / 2);
392
- }
393
- ctx.drawImage(tempCanvas, drawX, drawY, drawWidth, drawHeight);
394
- if (thumb.status === "near-hit") {
395
- ctx.fillStyle = "rgba(255, 165, 0, 0.3)";
396
- ctx.fillRect(thumb.x, 0, thumb.width, 2);
381
+ const scrollLeft = this._currentScrollLeft;
382
+ const viewportWidth = this._viewportWidth;
383
+ const stripStartPx = this._getStripTimelinePosition();
384
+ const stripEndPx = stripStartPx + stripWidth;
385
+ const visibleLeftPx = scrollLeft - VIRTUAL_RENDER_PADDING_PX;
386
+ const visibleRightPx = scrollLeft + viewportWidth + VIRTUAL_RENDER_PADDING_PX;
387
+ if (stripEndPx < visibleLeftPx || stripStartPx > visibleRightPx) {
388
+ canvas.style.display = "none";
389
+ return;
390
+ }
391
+ canvas.style.display = "block";
392
+ const visibleStartInStrip = Math.max(0, visibleLeftPx - stripStartPx);
393
+ const visibleEndInStrip = Math.min(stripWidth, visibleRightPx - stripStartPx);
394
+ const visibleWidthPx = visibleEndInStrip - visibleStartInStrip;
395
+ if (visibleWidthPx <= 0) {
396
+ canvas.style.display = "none";
397
+ return;
398
+ }
399
+ const targetWidth = Math.ceil(visibleWidthPx * dpr);
400
+ const targetHeight = Math.ceil(height * dpr);
401
+ if (canvas.width !== targetWidth || canvas.height !== targetHeight) {
402
+ canvas.width = targetWidth;
403
+ canvas.height = targetHeight;
404
+ }
405
+ canvas.style.left = `${visibleStartInStrip}px`;
406
+ canvas.style.width = `${visibleWidthPx}px`;
407
+ canvas.style.height = `${height}px`;
408
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
409
+ ctx.fillStyle = "#1a1a2e";
410
+ ctx.fillRect(0, 0, visibleWidthPx, height);
411
+ for (const slot of this._thumbnailSlots) {
412
+ if (slot.x + slot.width < visibleStartInStrip || slot.x > visibleEndInStrip) continue;
413
+ const drawX = slot.x - visibleStartInStrip;
414
+ if (drawX + slot.width < 0 || drawX > visibleWidthPx) continue;
415
+ if (slot.imageData) this._drawThumbnailImage(ctx, slot.imageData, drawX, slot.width, height);
416
+ else {
417
+ ctx.fillStyle = slot.status === "loading" ? "#2d2d50" : "#2d2d44";
418
+ ctx.fillRect(drawX, 0, slot.width, height);
419
+ if (slot.status === "loading") {
420
+ ctx.fillStyle = "rgba(59, 130, 246, 0.3)";
421
+ ctx.fillRect(drawX, 0, slot.width, 2);
422
+ }
397
423
  }
424
+ }
425
+ }
426
+ /**
427
+ * Draw a thumbnail image with cover mode scaling.
428
+ */
429
+ _drawThumbnailImage(ctx, imageData, x, width, height) {
430
+ const tempCanvas = document.createElement("canvas");
431
+ tempCanvas.width = imageData.width;
432
+ tempCanvas.height = imageData.height;
433
+ const tempCtx = tempCanvas.getContext("2d");
434
+ if (!tempCtx) return;
435
+ tempCtx.putImageData(imageData, 0, 0);
436
+ const srcAspect = imageData.width / imageData.height;
437
+ const dstAspect = width / height;
438
+ let srcX = 0, srcY = 0, srcW = imageData.width, srcH = imageData.height;
439
+ if (srcAspect > dstAspect) {
440
+ srcW = imageData.height * dstAspect;
441
+ srcX = (imageData.width - srcW) / 2;
398
442
  } else {
399
- const placeholderY = Math.round((this._stripHeight - thumb.height) / 2);
400
- ctx.fillStyle = "#404040";
401
- ctx.fillRect(thumb.x, placeholderY, thumb.width, thumb.height);
402
- ctx.strokeStyle = "#666";
403
- ctx.lineWidth = 1;
404
- ctx.setLineDash([2, 2]);
405
- ctx.strokeRect(thumb.x, placeholderY, thumb.width, thumb.height);
406
- ctx.setLineDash([]);
443
+ srcH = imageData.width / dstAspect;
444
+ srcY = (imageData.height - srcH) / 2;
407
445
  }
446
+ ctx.drawImage(tempCanvas, srcX, srcY, srcW, srcH, x, 0, width, height);
408
447
  }
409
448
  /**
410
- * Load missing thumbnails using MediaEngine batch extraction
449
+ * Load thumbnails that are visible in the current viewport.
411
450
  */
412
- async loadMissingThumbnails(thumbnails, targetElement) {
413
- const mediaEngine = targetElement.mediaEngineTask?.value;
451
+ async _loadVisibleThumbnails() {
452
+ if (this._captureInProgress || !this._targetElement) return;
453
+ const viewportWidth = this._viewportWidth;
454
+ const scrollOffset = this._currentScrollLeft;
455
+ const stripWidth = this._width;
456
+ const stripStartPx = this._getStripTimelinePosition();
457
+ const visibleLeftPx = scrollOffset - VIRTUAL_RENDER_PADDING_PX;
458
+ const visibleRightPx = scrollOffset + viewportWidth + VIRTUAL_RENDER_PADDING_PX;
459
+ const visibleStartInStrip = Math.max(0, visibleLeftPx - stripStartPx);
460
+ const visibleEndInStrip = Math.min(stripWidth, visibleRightPx - stripStartPx);
461
+ const pending = this._thumbnailSlots.filter((slot) => {
462
+ if (slot.status !== "pending") return false;
463
+ return slot.x + slot.width >= visibleStartInStrip && slot.x <= visibleEndInStrip;
464
+ });
465
+ if (pending.length === 0) return;
466
+ this._captureInProgress = true;
467
+ for (const slot of pending) slot.status = "loading";
468
+ this._drawCanvas();
469
+ try {
470
+ if (isEFTimegroup(this._targetElement)) await this._captureTimegroupThumbnails(pending);
471
+ else if (isEFVideo(this._targetElement)) await this._captureVideoThumbnails(pending);
472
+ } catch (error) {
473
+ console.warn("Failed to capture thumbnails:", error);
474
+ for (const slot of pending) if (slot.status === "loading") slot.status = "pending";
475
+ } finally {
476
+ this._captureInProgress = false;
477
+ this._drawCanvas();
478
+ if (this._thumbnailSlots.some((s) => s.status === "cached") && !this._hasLoadedThumbnails) {
479
+ this._hasLoadedThumbnails = true;
480
+ this.dispatchEvent(new CustomEvent("thumbnails-ready", { bubbles: true }));
481
+ }
482
+ }
483
+ }
484
+ /**
485
+ * Capture thumbnails from a timegroup target.
486
+ */
487
+ async _captureTimegroupThumbnails(slots) {
488
+ const target = this._targetElement;
489
+ const { rootId, elementId } = getCacheIdentifiers(target);
490
+ const timegroupWidth = target.offsetWidth || 1920;
491
+ const timegroupHeight = target.offsetHeight || 1080;
492
+ const scale = Math.min(1, this._height / timegroupHeight, MAX_CAPTURE_WIDTH / timegroupWidth);
493
+ for (let i = 0; i < slots.length; i += BATCH_SIZE) {
494
+ const batch = slots.slice(i, i + BATCH_SIZE);
495
+ const timestamps = batch.map((s) => s.timeMs);
496
+ try {
497
+ const canvases = await target.captureBatch(timestamps, {
498
+ scale,
499
+ contentReadyMode: "immediate"
500
+ });
501
+ for (let j = 0; j < batch.length; j++) {
502
+ const slot = batch[j];
503
+ const canvas = canvases[j];
504
+ if (canvas) {
505
+ const imageData = this._canvasToImageData(canvas);
506
+ if (imageData) {
507
+ const key = getCacheKey(rootId, elementId, slot.timeMs);
508
+ sessionThumbnailCache.set(key, imageData, slot.timeMs, elementId);
509
+ slot.imageData = imageData;
510
+ slot.status = "cached";
511
+ }
512
+ }
513
+ }
514
+ this._drawCanvas();
515
+ if (i + BATCH_SIZE < slots.length) await new Promise((r) => requestAnimationFrame(r));
516
+ } catch (error) {
517
+ console.warn("Batch capture failed:", error);
518
+ }
519
+ }
520
+ }
521
+ /**
522
+ * Capture thumbnails from a video target using MediaEngine.
523
+ */
524
+ async _captureVideoThumbnails(slots) {
525
+ const target = this._targetElement;
526
+ const { rootId, elementId } = getCacheIdentifiers(target);
527
+ if (target.mediaEngineTask) await target.mediaEngineTask.taskComplete;
528
+ const mediaEngine = target.mediaEngineTask?.value;
414
529
  if (!mediaEngine) return;
415
- const missingThumbnails = thumbnails.filter((t) => t.status === "missing" || t.status === "near-hit");
416
- if (missingThumbnails.length === 0) return;
417
- for (const thumb of missingThumbnails) thumb.status = "loading";
418
- const timestamps = missingThumbnails.map((t) => t.timeMs);
419
- const thumbnailResults = await mediaEngine.extractThumbnails(timestamps);
420
- for (let i = 0; i < missingThumbnails.length; i++) {
421
- const thumb = missingThumbnails[i];
422
- const thumbnailResult = thumbnailResults[i];
423
- if (thumb && thumbnailResult) {
424
- const imageData = this.canvasToImageData(thumbnailResult.thumbnail);
425
- if (imageData) {
426
- const cacheKey = getThumbnailCacheKey(targetElement.src, thumb.timeMs);
427
- thumbnailImageCache.set(cacheKey, imageData);
428
- thumb.imageData = imageData;
429
- thumb.status = "exact-hit";
530
+ const videoRendition = mediaEngine.getVideoRendition();
531
+ const scrubRendition = mediaEngine.getScrubVideoRendition();
532
+ if (!videoRendition && !scrubRendition) return;
533
+ const timestamps = slots.map((s) => s.timeMs);
534
+ const abortController = new AbortController();
535
+ try {
536
+ const results = await mediaEngine.extractThumbnails(timestamps, abortController.signal);
537
+ for (let i = 0; i < slots.length; i++) {
538
+ const slot = slots[i];
539
+ const result = results[i];
540
+ if (result?.thumbnail) {
541
+ const imageData = this._canvasToImageData(result.thumbnail);
542
+ if (imageData) {
543
+ const key = getCacheKey(rootId, elementId, slot.timeMs);
544
+ sessionThumbnailCache.set(key, imageData, slot.timeMs, elementId);
545
+ slot.imageData = imageData;
546
+ slot.status = "cached";
547
+ }
430
548
  }
431
549
  }
550
+ } catch (error) {
551
+ abortController.abort();
552
+ console.warn("Video thumbnail extraction failed:", error);
432
553
  }
433
- await this.drawThumbnails(thumbnails);
434
554
  }
435
555
  /**
436
- * Convert Canvas to ImageData for caching
556
+ * Convert canvas to ImageData.
437
557
  */
438
- canvasToImageData(canvas) {
439
- const ctx = canvas.getContext("2d");
558
+ _canvasToImageData(canvas) {
559
+ const ctx = canvas.getContext("2d", { willReadFrequently: true });
440
560
  if (!ctx) return null;
441
561
  return ctx.getImageData(0, 0, canvas.width, canvas.height);
442
562
  }
563
+ /**
564
+ * Returns a promise that resolves when thumbnails are ready.
565
+ * Resolves immediately if thumbnails are already loaded.
566
+ */
567
+ whenReady() {
568
+ if (this._hasLoadedThumbnails) return Promise.resolve();
569
+ return new Promise((resolve) => {
570
+ this.addEventListener("thumbnails-ready", () => resolve(), { once: true });
571
+ });
572
+ }
573
+ /**
574
+ * Check if thumbnails have been loaded.
575
+ */
576
+ get isReady() {
577
+ return this._hasLoadedThumbnails;
578
+ }
579
+ /**
580
+ * Invalidate cached thumbnails for this element within a time range.
581
+ * Call this when content changes at specific times.
582
+ */
583
+ invalidateTimeRange(startTimeMs, endTimeMs) {
584
+ if (!this._targetElement) return;
585
+ const { rootId, elementId } = getCacheIdentifiers(this._targetElement);
586
+ sessionThumbnailCache.invalidateTimeRange(rootId, elementId, startTimeMs, endTimeMs);
587
+ for (const slot of this._thumbnailSlots) if (slot.timeMs >= startTimeMs && slot.timeMs <= endTimeMs) {
588
+ slot.imageData = void 0;
589
+ slot.status = "pending";
590
+ }
591
+ this._scheduleRender();
592
+ }
593
+ /**
594
+ * Invalidate all cached thumbnails for this element.
595
+ */
596
+ invalidateAll() {
597
+ if (!this._targetElement) return;
598
+ const { rootId, elementId } = getCacheIdentifiers(this._targetElement);
599
+ sessionThumbnailCache.invalidateElement(rootId, elementId);
600
+ for (const slot of this._thumbnailSlots) {
601
+ slot.imageData = void 0;
602
+ slot.status = "pending";
603
+ }
604
+ this._scheduleRender();
605
+ }
443
606
  render() {
444
- return html`
445
- <canvas ${ref(this.canvasRef)}></canvas>
446
- ${this.thumbnailRenderTask.render({
447
- pending: () => html``,
448
- complete: () => html``,
449
- error: (e) => html`<div class="error">Error loading thumbnails: ${e}</div>`
450
- })}
451
- `;
607
+ return html`<canvas ${ref(this.canvasRef)}></canvas>`;
452
608
  }
453
609
  };
454
- __decorate([state()], EFThumbnailStrip.prototype, "targetElement", null);
455
610
  __decorate([property({ type: String })], EFThumbnailStrip.prototype, "target", void 0);
456
611
  __decorate([property({
457
612
  type: Number,
458
613
  attribute: "thumbnail-width"
459
614
  })], EFThumbnailStrip.prototype, "thumbnailWidth", void 0);
615
+ __decorate([property({
616
+ type: Number,
617
+ attribute: "gap"
618
+ })], EFThumbnailStrip.prototype, "gap", void 0);
460
619
  __decorate([property({
461
620
  type: Number,
462
621
  attribute: "start-time-ms"
@@ -470,14 +629,19 @@ __decorate([property({
470
629
  attribute: "use-intrinsic-duration",
471
630
  reflect: true,
472
631
  converter: {
473
- fromAttribute: (value) => {
474
- if (value === null) return false;
475
- return value === "true";
476
- },
632
+ fromAttribute: (value) => value === "true",
477
633
  toAttribute: (value) => value ? "true" : null
478
634
  }
479
635
  })], EFThumbnailStrip.prototype, "useIntrinsicDuration", void 0);
480
- __decorate([state()], EFThumbnailStrip.prototype, "stripWidth", null);
636
+ __decorate([property({
637
+ type: Number,
638
+ attribute: "pixels-per-ms"
639
+ })], EFThumbnailStrip.prototype, "pixelsPerMs", void 0);
640
+ __decorate([consume({
641
+ context: timelineStateContext,
642
+ subscribe: true
643
+ }), state()], EFThumbnailStrip.prototype, "_timelineState", void 0);
644
+ __decorate([state()], EFThumbnailStrip.prototype, "targetElement", null);
481
645
  EFThumbnailStrip = __decorate([customElement("ef-thumbnail-strip")], EFThumbnailStrip);
482
646
 
483
647
  //#endregion