@editframe/elements 0.37.3-beta → 0.38.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 (327) hide show
  1. package/dist/EF_FRAMEGEN.js +17 -14
  2. package/dist/EF_FRAMEGEN.js.map +1 -1
  3. package/dist/EF_RENDERING.js.map +1 -1
  4. package/dist/canvas/EFCanvas.d.ts +9 -2
  5. package/dist/canvas/EFCanvas.js +14 -4
  6. package/dist/canvas/EFCanvas.js.map +1 -1
  7. package/dist/canvas/EFCanvasItem.d.ts +2 -2
  8. package/dist/canvas/overlays/SelectionOverlay.d.ts +10 -2
  9. package/dist/canvas/overlays/SelectionOverlay.js +5 -12
  10. package/dist/canvas/overlays/SelectionOverlay.js.map +1 -1
  11. package/dist/canvas/overlays/overlayState.js.map +1 -1
  12. package/dist/canvas/selection/SelectionController.js.map +1 -1
  13. package/dist/elements/EFAudio.d.ts +1 -11
  14. package/dist/elements/EFAudio.js +2 -10
  15. package/dist/elements/EFAudio.js.map +1 -1
  16. package/dist/elements/EFCaptions.d.ts +5 -9
  17. package/dist/elements/EFCaptions.js +34 -11
  18. package/dist/elements/EFCaptions.js.map +1 -1
  19. package/dist/elements/EFImage.d.ts +10 -8
  20. package/dist/elements/EFImage.js +117 -32
  21. package/dist/elements/EFImage.js.map +1 -1
  22. package/dist/elements/EFMedia/AssetMediaEngine.js +2 -2
  23. package/dist/elements/EFMedia/AssetMediaEngine.js.map +1 -1
  24. package/dist/elements/EFMedia/BaseMediaEngine.js +15 -92
  25. package/dist/elements/EFMedia/BaseMediaEngine.js.map +1 -1
  26. package/dist/elements/EFMedia/BufferedSeekingInput.js +10 -11
  27. package/dist/elements/EFMedia/BufferedSeekingInput.js.map +1 -1
  28. package/dist/elements/EFMedia/{AssetIdMediaEngine.js → FileMediaEngine.js} +44 -24
  29. package/dist/elements/EFMedia/FileMediaEngine.js.map +1 -0
  30. package/dist/elements/EFMedia/JitMediaEngine.js +14 -13
  31. package/dist/elements/EFMedia/JitMediaEngine.js.map +1 -1
  32. package/dist/elements/EFMedia/shared/AudioSpanUtils.js +3 -3
  33. package/dist/elements/EFMedia/shared/AudioSpanUtils.js.map +1 -1
  34. package/dist/elements/EFMedia/shared/ThumbnailExtractor.js +12 -7
  35. package/dist/elements/EFMedia/shared/ThumbnailExtractor.js.map +1 -1
  36. package/dist/elements/EFMedia/shared/timeoutUtils.js +44 -0
  37. package/dist/elements/EFMedia/shared/timeoutUtils.js.map +1 -0
  38. package/dist/elements/EFMedia/videoTasks/MainVideoInputCache.js +1 -1
  39. package/dist/elements/EFMedia/videoTasks/MainVideoInputCache.js.map +1 -1
  40. package/dist/elements/EFMedia/videoTasks/ScrubInputCache.js +4 -4
  41. package/dist/elements/EFMedia/videoTasks/ScrubInputCache.js.map +1 -1
  42. package/dist/elements/EFMedia.d.ts +14 -8
  43. package/dist/elements/EFMedia.js +52 -19
  44. package/dist/elements/EFMedia.js.map +1 -1
  45. package/dist/elements/EFPanZoom.d.ts +2 -2
  46. package/dist/elements/EFPanZoom.js +1 -1
  47. package/dist/elements/EFPanZoom.js.map +1 -1
  48. package/dist/elements/EFSourceMixin.js +16 -8
  49. package/dist/elements/EFSourceMixin.js.map +1 -1
  50. package/dist/elements/EFSurface.d.ts +5 -8
  51. package/dist/elements/EFSurface.js +4 -43
  52. package/dist/elements/EFSurface.js.map +1 -1
  53. package/dist/elements/EFTemporal.d.ts +33 -8
  54. package/dist/elements/EFTemporal.js +92 -40
  55. package/dist/elements/EFTemporal.js.map +1 -1
  56. package/dist/elements/EFText.d.ts +3 -0
  57. package/dist/elements/EFText.js +54 -21
  58. package/dist/elements/EFText.js.map +1 -1
  59. package/dist/elements/EFTextSegment.js +8 -4
  60. package/dist/elements/EFTextSegment.js.map +1 -1
  61. package/dist/elements/EFTimegroup.d.ts +26 -43
  62. package/dist/elements/EFTimegroup.js +295 -314
  63. package/dist/elements/EFTimegroup.js.map +1 -1
  64. package/dist/elements/EFVideo.d.ts +44 -42
  65. package/dist/elements/EFVideo.js +259 -172
  66. package/dist/elements/EFVideo.js.map +1 -1
  67. package/dist/elements/EFWaveform.d.ts +3 -8
  68. package/dist/elements/EFWaveform.js +18 -13
  69. package/dist/elements/EFWaveform.js.map +1 -1
  70. package/dist/elements/ElementPositionInfo.js.map +1 -1
  71. package/dist/elements/FetchMixin.js.map +1 -1
  72. package/dist/elements/TargetController.d.ts +0 -3
  73. package/dist/elements/TargetController.js +12 -35
  74. package/dist/elements/TargetController.js.map +1 -1
  75. package/dist/elements/TimegroupController.js.map +1 -1
  76. package/dist/elements/cloneFactoryRegistry.d.ts +14 -0
  77. package/dist/elements/cloneFactoryRegistry.js +15 -0
  78. package/dist/elements/cloneFactoryRegistry.js.map +1 -0
  79. package/dist/elements/renderTemporalAudio.js +8 -6
  80. package/dist/elements/renderTemporalAudio.js.map +1 -1
  81. package/dist/elements/setupTemporalHierarchy.js +62 -0
  82. package/dist/elements/setupTemporalHierarchy.js.map +1 -0
  83. package/dist/elements/updateAnimations.js +62 -87
  84. package/dist/elements/updateAnimations.js.map +1 -1
  85. package/dist/getRenderInfo.d.ts +3 -2
  86. package/dist/getRenderInfo.js +20 -4
  87. package/dist/getRenderInfo.js.map +1 -1
  88. package/dist/gui/ContextMixin.js +68 -12
  89. package/dist/gui/ContextMixin.js.map +1 -1
  90. package/dist/gui/Controllable.js +1 -1
  91. package/dist/gui/Controllable.js.map +1 -1
  92. package/dist/gui/EFActiveRootTemporal.d.ts +2 -2
  93. package/dist/gui/EFActiveRootTemporal.js.map +1 -1
  94. package/dist/gui/EFControls.d.ts +2 -2
  95. package/dist/gui/EFControls.js +2 -2
  96. package/dist/gui/EFControls.js.map +1 -1
  97. package/dist/gui/EFDial.d.ts +2 -2
  98. package/dist/gui/EFDial.js +12 -9
  99. package/dist/gui/EFDial.js.map +1 -1
  100. package/dist/gui/EFFilmstrip.d.ts +2 -0
  101. package/dist/gui/EFFilmstrip.js +18 -10
  102. package/dist/gui/EFFilmstrip.js.map +1 -1
  103. package/dist/gui/EFFitScale.d.ts +28 -4
  104. package/dist/gui/EFFitScale.js +88 -26
  105. package/dist/gui/EFFitScale.js.map +1 -1
  106. package/dist/gui/EFFocusOverlay.d.ts +2 -2
  107. package/dist/gui/EFFocusOverlay.js +3 -3
  108. package/dist/gui/EFFocusOverlay.js.map +1 -1
  109. package/dist/gui/EFOverlayItem.d.ts +2 -2
  110. package/dist/gui/EFOverlayLayer.d.ts +2 -2
  111. package/dist/gui/EFPause.d.ts +2 -2
  112. package/dist/gui/EFPause.js +1 -1
  113. package/dist/gui/EFPlay.d.ts +2 -2
  114. package/dist/gui/EFPlay.js +1 -1
  115. package/dist/gui/EFPreview.js +1 -1
  116. package/dist/gui/EFResizableBox.d.ts +2 -2
  117. package/dist/gui/EFResizableBox.js +5 -5
  118. package/dist/gui/EFResizableBox.js.map +1 -1
  119. package/dist/gui/EFScrubber.d.ts +2 -2
  120. package/dist/gui/EFScrubber.js +8 -13
  121. package/dist/gui/EFScrubber.js.map +1 -1
  122. package/dist/gui/EFTimeDisplay.d.ts +6 -2
  123. package/dist/gui/EFTimeDisplay.js +25 -7
  124. package/dist/gui/EFTimeDisplay.js.map +1 -1
  125. package/dist/gui/EFTimelineRuler.d.ts +2 -2
  126. package/dist/gui/EFTimelineRuler.js +3 -3
  127. package/dist/gui/EFTimelineRuler.js.map +1 -1
  128. package/dist/gui/EFToggleLoop.d.ts +2 -2
  129. package/dist/gui/EFToggleLoop.js +1 -1
  130. package/dist/gui/EFTogglePlay.d.ts +2 -2
  131. package/dist/gui/EFTogglePlay.js +1 -1
  132. package/dist/gui/EFTransformHandles.d.ts +2 -2
  133. package/dist/gui/EFTransformHandles.js +6 -6
  134. package/dist/gui/EFTransformHandles.js.map +1 -1
  135. package/dist/gui/EFWorkbench.d.ts +40 -36
  136. package/dist/gui/EFWorkbench.js +436 -822
  137. package/dist/gui/EFWorkbench.js.map +1 -1
  138. package/dist/gui/FitScaleHelpers.js.map +1 -1
  139. package/dist/gui/PlaybackController.d.ts +3 -8
  140. package/dist/gui/PlaybackController.js +59 -56
  141. package/dist/gui/PlaybackController.js.map +1 -1
  142. package/dist/gui/TWMixin.js +1 -1
  143. package/dist/gui/TWMixin.js.map +1 -1
  144. package/dist/gui/TargetOrContextMixin.js +43 -6
  145. package/dist/gui/TargetOrContextMixin.js.map +1 -1
  146. package/dist/gui/ef-theme.css +136 -0
  147. package/dist/gui/hierarchy/EFHierarchy.d.ts +2 -2
  148. package/dist/gui/hierarchy/EFHierarchy.js +14 -24
  149. package/dist/gui/hierarchy/EFHierarchy.js.map +1 -1
  150. package/dist/gui/hierarchy/EFHierarchyItem.d.ts +3 -3
  151. package/dist/gui/hierarchy/EFHierarchyItem.js +22 -10
  152. package/dist/gui/hierarchy/EFHierarchyItem.js.map +1 -1
  153. package/dist/gui/icons.js.map +1 -1
  154. package/dist/gui/previewSettingsContext.d.ts +18 -0
  155. package/dist/gui/previewSettingsContext.js.map +1 -1
  156. package/dist/gui/theme.js +34 -0
  157. package/dist/gui/theme.js.map +1 -0
  158. package/dist/gui/timeline/EFTimeline.d.ts +2 -2
  159. package/dist/gui/timeline/EFTimeline.js +70 -52
  160. package/dist/gui/timeline/EFTimeline.js.map +1 -1
  161. package/dist/gui/timeline/EFTimelineRow.d.ts +5 -3
  162. package/dist/gui/timeline/EFTimelineRow.js +55 -32
  163. package/dist/gui/timeline/EFTimelineRow.js.map +1 -1
  164. package/dist/gui/timeline/TrimHandles.d.ts +23 -9
  165. package/dist/gui/timeline/TrimHandles.js +224 -51
  166. package/dist/gui/timeline/TrimHandles.js.map +1 -1
  167. package/dist/gui/timeline/flattenHierarchy.js.map +1 -1
  168. package/dist/gui/timeline/timelineEditingContext.d.ts +34 -0
  169. package/dist/gui/timeline/timelineEditingContext.js +24 -0
  170. package/dist/gui/timeline/timelineEditingContext.js.map +1 -0
  171. package/dist/gui/timeline/timelineStateContext.js.map +1 -1
  172. package/dist/gui/timeline/tracks/AudioTrack.js +1 -1
  173. package/dist/gui/timeline/tracks/AudioTrack.js.map +1 -1
  174. package/dist/gui/timeline/tracks/CaptionsTrack.d.ts +2 -3
  175. package/dist/gui/timeline/tracks/CaptionsTrack.js +17 -75
  176. package/dist/gui/timeline/tracks/CaptionsTrack.js.map +1 -1
  177. package/dist/gui/timeline/tracks/EFThumbnailStrip.d.ts +52 -0
  178. package/dist/gui/timeline/tracks/EFThumbnailStrip.js +596 -0
  179. package/dist/gui/timeline/tracks/EFThumbnailStrip.js.map +1 -0
  180. package/dist/gui/timeline/tracks/HTMLTrack.js.map +1 -1
  181. package/dist/gui/timeline/tracks/ImageTrack.js.map +1 -1
  182. package/dist/gui/timeline/tracks/TextTrack.d.ts +3 -2
  183. package/dist/gui/timeline/tracks/TextTrack.js +17 -43
  184. package/dist/gui/timeline/tracks/TextTrack.js.map +1 -1
  185. package/dist/gui/timeline/tracks/TimegroupTrack.d.ts +3 -4
  186. package/dist/gui/timeline/tracks/TimegroupTrack.js +33 -23
  187. package/dist/gui/timeline/tracks/TimegroupTrack.js.map +1 -1
  188. package/dist/gui/timeline/tracks/TrackItem.d.ts +7 -9
  189. package/dist/gui/timeline/tracks/TrackItem.js +18 -17
  190. package/dist/gui/timeline/tracks/TrackItem.js.map +1 -1
  191. package/dist/gui/timeline/tracks/VideoTrack.d.ts +3 -3
  192. package/dist/gui/timeline/tracks/VideoTrack.js +11 -14
  193. package/dist/gui/timeline/tracks/VideoTrack.js.map +1 -1
  194. package/dist/gui/timeline/tracks/WaveformTrack.js.map +1 -1
  195. package/dist/gui/timeline/tracks/renderTrackChildren.js.map +1 -1
  196. package/dist/gui/timeline/tracks/waveformUtils.js +1 -1
  197. package/dist/gui/timeline/tracks/waveformUtils.js.map +1 -1
  198. package/dist/gui/tree/EFTree.d.ts +2 -2
  199. package/dist/gui/tree/EFTree.js +8 -14
  200. package/dist/gui/tree/EFTree.js.map +1 -1
  201. package/dist/gui/tree/EFTreeItem.d.ts +2 -2
  202. package/dist/gui/tree/EFTreeItem.js +3 -3
  203. package/dist/gui/tree/EFTreeItem.js.map +1 -1
  204. package/dist/gui/tree/treeContext.js.map +1 -1
  205. package/dist/index.d.ts +10 -8
  206. package/dist/index.js +6 -5
  207. package/dist/index.js.map +1 -1
  208. package/dist/node.d.ts +2 -2
  209. package/dist/node.js +2 -2
  210. package/dist/preview/AdaptiveResolutionTracker.js +3 -3
  211. package/dist/preview/AdaptiveResolutionTracker.js.map +1 -1
  212. package/dist/preview/FrameController.d.ts +2 -17
  213. package/dist/preview/FrameController.js +40 -63
  214. package/dist/preview/FrameController.js.map +1 -1
  215. package/dist/preview/QualityUpgradeScheduler.d.ts +76 -0
  216. package/dist/preview/QualityUpgradeScheduler.js +158 -0
  217. package/dist/preview/QualityUpgradeScheduler.js.map +1 -0
  218. package/dist/preview/RenderContext.d.ts +119 -1
  219. package/dist/preview/RenderContext.js +21 -3
  220. package/dist/preview/RenderContext.js.map +1 -1
  221. package/dist/preview/RenderProfiler.js.map +1 -1
  222. package/dist/preview/RenderStats.js +85 -0
  223. package/dist/preview/RenderStats.js.map +1 -0
  224. package/dist/preview/encoding/canvasEncoder.js +2 -52
  225. package/dist/preview/encoding/canvasEncoder.js.map +1 -1
  226. package/dist/preview/encoding/mainThreadEncoder.js.map +1 -1
  227. package/dist/preview/encoding/workerEncoder.js.map +1 -1
  228. package/dist/preview/logger.js.map +1 -1
  229. package/dist/preview/previewSettings.d.ts +34 -0
  230. package/dist/preview/previewSettings.js +29 -17
  231. package/dist/preview/previewSettings.js.map +1 -1
  232. package/dist/preview/previewTypes.js +4 -4
  233. package/dist/preview/previewTypes.js.map +1 -1
  234. package/dist/preview/renderElementToCanvas.d.ts +44 -0
  235. package/dist/preview/renderElementToCanvas.js +72 -0
  236. package/dist/preview/renderElementToCanvas.js.map +1 -0
  237. package/dist/preview/renderTimegroupToCanvas.d.ts +134 -32
  238. package/dist/preview/renderTimegroupToCanvas.js +321 -146
  239. package/dist/preview/renderTimegroupToCanvas.js.map +1 -1
  240. package/dist/preview/renderTimegroupToCanvas.types.d.ts +51 -0
  241. package/dist/preview/renderTimegroupToVideo.d.ts +20 -35
  242. package/dist/preview/renderTimegroupToVideo.js +94 -106
  243. package/dist/preview/renderTimegroupToVideo.js.map +1 -1
  244. package/dist/preview/renderTimegroupToVideo.types.d.ts +42 -0
  245. package/dist/preview/renderVideoToVideo.js +286 -0
  246. package/dist/preview/renderVideoToVideo.js.map +1 -0
  247. package/dist/preview/renderers.d.ts +56 -0
  248. package/dist/preview/renderers.js +13 -1
  249. package/dist/preview/renderers.js.map +1 -1
  250. package/dist/preview/rendering/ScaleConfig.js +74 -0
  251. package/dist/preview/rendering/ScaleConfig.js.map +1 -0
  252. package/dist/preview/rendering/inlineImages.d.ts +13 -0
  253. package/dist/preview/rendering/inlineImages.js +7 -44
  254. package/dist/preview/rendering/inlineImages.js.map +1 -1
  255. package/dist/preview/rendering/loadImage.d.ts +8 -0
  256. package/dist/preview/rendering/loadImage.js +22 -0
  257. package/dist/preview/rendering/loadImage.js.map +1 -0
  258. package/dist/preview/rendering/renderToImageNative.js +3 -3
  259. package/dist/preview/rendering/renderToImageNative.js.map +1 -1
  260. package/dist/preview/rendering/serializeTimelineDirect.js +224 -68
  261. package/dist/preview/rendering/serializeTimelineDirect.js.map +1 -1
  262. package/dist/preview/statsTrackingStrategy.js +1 -101
  263. package/dist/preview/statsTrackingStrategy.js.map +1 -1
  264. package/dist/preview/workers/WorkerPool.js +0 -1
  265. package/dist/preview/workers/WorkerPool.js.map +1 -1
  266. package/dist/preview/workers/encoderWorkerInline.js +21 -54
  267. package/dist/preview/workers/encoderWorkerInline.js.map +1 -1
  268. package/dist/render/EFRenderAPI.d.ts +2 -1
  269. package/dist/render/EFRenderAPI.js +12 -36
  270. package/dist/render/EFRenderAPI.js.map +1 -1
  271. package/dist/render/getRenderData.js +4 -4
  272. package/dist/render/getRenderData.js.map +1 -1
  273. package/dist/style.css +114 -163
  274. package/dist/transcoding/cache/RequestDeduplicator.js +1 -0
  275. package/dist/transcoding/cache/RequestDeduplicator.js.map +1 -1
  276. package/dist/transcoding/types/index.d.ts +1 -1
  277. package/dist/transcoding/utils/UrlGenerator.js +10 -3
  278. package/dist/transcoding/utils/UrlGenerator.js.map +1 -1
  279. package/dist/utils/LRUCache.js +1 -0
  280. package/dist/utils/LRUCache.js.map +1 -1
  281. package/dist/utils/frameTime.js +23 -1
  282. package/dist/utils/frameTime.js.map +1 -1
  283. package/package.json +45 -8
  284. package/scripts/build-css.js +8 -1
  285. package/test/setup.ts +0 -1
  286. package/test/useAssetMSW.ts +50 -0
  287. package/test/visualRegressionUtils.ts +23 -9
  288. package/tsdown.config.ts +6 -1
  289. package/dist/_virtual/rolldown_runtime.js +0 -27
  290. package/dist/elements/EFMedia/AssetIdMediaEngine.js.map +0 -1
  291. package/dist/elements/EFThumbnailStrip.d.ts +0 -167
  292. package/dist/elements/EFThumbnailStrip.js +0 -731
  293. package/dist/elements/EFThumbnailStrip.js.map +0 -1
  294. package/dist/elements/SessionThumbnailCache.js +0 -154
  295. package/dist/elements/SessionThumbnailCache.js.map +0 -1
  296. package/dist/node_modules/react/cjs/react-jsx-runtime.development.js +0 -688
  297. package/dist/node_modules/react/cjs/react-jsx-runtime.development.js.map +0 -1
  298. package/dist/node_modules/react/cjs/react.development.js +0 -1521
  299. package/dist/node_modules/react/cjs/react.development.js.map +0 -1
  300. package/dist/node_modules/react/index.js +0 -13
  301. package/dist/node_modules/react/index.js.map +0 -1
  302. package/dist/node_modules/react/jsx-runtime.js +0 -13
  303. package/dist/node_modules/react/jsx-runtime.js.map +0 -1
  304. package/dist/preview/encoding/types.d.ts +0 -1
  305. package/dist/preview/renderTimegroupPreview.js +0 -686
  306. package/dist/preview/renderTimegroupPreview.js.map +0 -1
  307. package/dist/preview/rendering/renderToImage.d.ts +0 -2
  308. package/dist/preview/rendering/renderToImage.js +0 -95
  309. package/dist/preview/rendering/renderToImage.js.map +0 -1
  310. package/dist/preview/rendering/renderToImageForeignObject.js +0 -163
  311. package/dist/preview/rendering/renderToImageForeignObject.js.map +0 -1
  312. package/dist/preview/rendering/renderToImageNative.d.ts +0 -1
  313. package/dist/preview/rendering/svgSerializer.js +0 -43
  314. package/dist/preview/rendering/svgSerializer.js.map +0 -1
  315. package/dist/preview/rendering/types.d.ts +0 -2
  316. package/dist/preview/thumbnailCacheSettings.js +0 -52
  317. package/dist/preview/thumbnailCacheSettings.js.map +0 -1
  318. package/dist/sandbox/PlaybackControls.d.ts +0 -1
  319. package/dist/sandbox/PlaybackControls.js +0 -10
  320. package/dist/sandbox/PlaybackControls.js.map +0 -1
  321. package/dist/sandbox/ScenarioRunner.d.ts +0 -1
  322. package/dist/sandbox/ScenarioRunner.js +0 -1
  323. package/dist/sandbox/defineSandbox.d.ts +0 -1
  324. package/dist/sandbox/index.d.ts +0 -3
  325. package/dist/sandbox/index.js +0 -2
  326. package/test/EFVideo.framegen.browsertest.ts +0 -80
  327. package/test/thumbnail-performance-test.html +0 -116
@@ -1 +1 @@
1
- {"version":3,"file":"EFCaptions.js","names":["EFCaptionsActiveWord","#wordText","#wordIndex","EFCaptionsSegment","#segmentText","EFCaptionsBeforeActiveWord","EFCaptionsAfterActiveWord","EFCaptions","#captionsDataLoaded","#captionsDataValue","#captionsDataPromise","#doLoadCaptionsData","#transcriptionData","#loadTranscriptionFragment","#rootTimegroupUpdateController","#cachedIntrinsicDurationMs","captionsData: Caption | null","result: number"],"sources":["../../src/elements/EFCaptions.ts"],"sourcesContent":["import { css, html, LitElement, type PropertyValueMap } from \"lit\";\nimport { customElement, property } from \"lit/decorators.js\";\nimport type { ReactiveController } from \"lit\";\nimport type { GetISOBMFFFileTranscriptionResult } from \"../../../api/src/index.js\";\nimport {\n type FrameRenderable,\n type FrameState,\n createFrameTaskWrapper,\n PRIORITY_CAPTIONS,\n} from \"../preview/FrameController.js\";\nimport { AsyncValue } from \"./EFMedia.js\";\nimport { CrossUpdateController } from \"./CrossUpdateController.js\";\nimport { EFAudio } from \"./EFAudio.js\";\nimport { EFSourceMixin } from \"./EFSourceMixin.js\";\nimport { EFTemporal, flushStartTimeMsCache } from \"./EFTemporal.js\";\nimport {\n flushSequenceDurationCache,\n EFTimegroup,\n} from \"./EFTimegroup.js\";\nimport { EFVideo } from \"./EFVideo.js\";\nimport { FetchMixin } from \"./FetchMixin.js\";\n\nexport interface WordSegment {\n text: string;\n start: number;\n end: number;\n}\n\nexport interface Segment {\n start: number;\n end: number;\n text: string;\n}\n\nexport interface Caption {\n segments: Segment[];\n word_segments: WordSegment[];\n}\n\nconst stopWords = new Set([\"\", \".\", \"!\", \"?\", \",\"]);\n\n/**\n * Caption active word element - displays the currently spoken word.\n * Uses light DOM for simplicity - parent sets textContent directly.\n */\n@customElement(\"ef-captions-active-word\")\nexport class EFCaptionsActiveWord extends HTMLElement {\n #wordText = \"\";\n #wordIndex = 0;\n \n set wordText(text: string) {\n this.#wordText = text;\n // Hide element if no content or only stop words\n if (!text || stopWords.has(text)) {\n this.hidden = true;\n this.textContent = \"\";\n } else {\n this.hidden = false;\n // Add trailing space to maintain consistent spacing with surrounding words\n this.textContent = text + \" \";\n }\n }\n \n get wordText(): string {\n return this.#wordText;\n }\n \n set wordIndex(index: number) {\n this.#wordIndex = index;\n // Set deterministic --ef-word-seed value based on word index\n const seed = (index * 9007) % 233; // Prime numbers for better distribution\n const seedValue = seed / 233; // Normalize to 0-1 range\n this.style.setProperty(\"--ef-word-seed\", seedValue.toString());\n }\n \n get wordIndex(): number {\n return this.#wordIndex;\n }\n}\n\n/**\n * Caption segment element - displays a full caption segment.\n * Uses light DOM for simplicity - parent sets textContent directly.\n */\n@customElement(\"ef-captions-segment\")\nexport class EFCaptionsSegment extends HTMLElement {\n #segmentText = \"\";\n \n set segmentText(text: string) {\n this.#segmentText = text;\n // Hide element if no content or only stop words\n if (!text || stopWords.has(text)) {\n this.hidden = true;\n this.textContent = \"\";\n } else {\n this.hidden = false;\n this.textContent = text;\n }\n }\n \n get segmentText(): string {\n return this.#segmentText;\n }\n}\n\n/**\n * Caption before-active-word element - displays words before the current word.\n * Uses light DOM for simplicity - parent sets textContent directly.\n */\n@customElement(\"ef-captions-before-active-word\")\nexport class EFCaptionsBeforeActiveWord extends EFCaptionsSegment {\n set segmentText(text: string) {\n // Check if there's an active word by looking for sibling active word element\n const activeWord = this.closest(\"ef-captions\")?.querySelector(\n \"ef-captions-active-word\",\n ) as EFCaptionsActiveWord;\n const hasActiveWord = activeWord?.wordText;\n \n // Add trailing space if there's an active word coming after us\n const finalText = text && hasActiveWord ? text + \" \" : text;\n \n // Hide element if no content or only stop words\n if (!finalText || stopWords.has(finalText)) {\n this.hidden = true;\n this.textContent = \"\";\n } else {\n this.hidden = false;\n this.textContent = finalText;\n }\n }\n}\n\n/**\n * Caption after-active-word element - displays words after the current word.\n * Uses light DOM for simplicity - parent sets textContent directly.\n */\n@customElement(\"ef-captions-after-active-word\")\nexport class EFCaptionsAfterActiveWord extends EFCaptionsSegment {\n set segmentText(text: string) {\n // No leading space - active word will add trailing space\n const finalText = text;\n \n // Hide element if no content or only stop words\n if (!finalText || stopWords.has(finalText)) {\n this.hidden = true;\n this.textContent = \"\";\n } else {\n this.hidden = false;\n this.textContent = finalText;\n }\n }\n}\n\n@customElement(\"ef-captions\")\nexport class EFCaptions extends EFSourceMixin(\n EFTemporal(FetchMixin(LitElement)),\n { assetType: \"caption_files\" },\n) implements FrameRenderable {\n static styles = [\n css`\n :host {\n display: block;\n white-space: normal;\n line-height: 1;\n gap: 0;\n }\n ::slotted(*) {\n display: inline;\n margin: 0;\n padding: 0;\n }\n `,\n ];\n\n @property({ type: String, attribute: \"target\", reflect: true })\n targetSelector = \"\";\n\n set target(value: string) {\n this.targetSelector = value;\n }\n\n @property({ attribute: \"word-style\" })\n wordStyle = \"\";\n\n /**\n * URL or path to a JSON file containing custom captions data.\n * The JSON should conform to the Caption interface with 'segments' and 'word_segments' arrays.\n */\n @property({ type: String, attribute: \"captions-src\", reflect: true })\n captionsSrc = \"\";\n\n /**\n * Direct captions data object. Takes priority over captions-src and captions-script.\n * Should conform to the Caption interface with 'segments' and 'word_segments' arrays.\n */\n @property({ type: Object, attribute: false })\n captionsData: Caption | null = null;\n\n /**\n * ID of a <script> element containing JSON captions data.\n * The script's textContent should be valid JSON conforming to the Caption interface.\n */\n @property({ type: String, attribute: \"captions-script\", reflect: true })\n captionsScript = \"\";\n\n activeWordContainers = this.getElementsByTagName(\"ef-captions-active-word\");\n segmentContainers = this.getElementsByTagName(\"ef-captions-segment\");\n beforeActiveWordContainers = this.getElementsByTagName(\n \"ef-captions-before-active-word\",\n );\n afterActiveWordContainers = this.getElementsByTagName(\n \"ef-captions-after-active-word\",\n );\n\n // Cache for intrinsicDurationMs to avoid expensive O(n) recalculation every frame\n #cachedIntrinsicDurationMs: number | undefined | null = null; // null = not computed, undefined = no duration\n\n render() {\n return html`<slot></slot>`;\n }\n\n transcriptionsPath() {\n if (!this.targetElement) {\n return null;\n }\n if (this.targetElement.assetId) {\n return `${this.apiHost}/api/v1/isobmff_files/${this.targetElement.assetId}/transcription`;\n }\n return null;\n }\n\n captionsPath() {\n if (!this.targetElement) {\n return null;\n }\n if (this.targetElement.assetId) {\n return `${this.apiHost}/api/v1/caption_files/${this.targetElement.assetId}`;\n }\n const targetSrc = this.targetElement.src;\n // Normalize the path: remove leading slash and any double slashes\n let normalizedSrc = targetSrc.startsWith(\"/\")\n ? targetSrc.slice(1)\n : targetSrc;\n normalizedSrc = normalizedSrc.replace(/^\\/+/, \"\");\n // Use production API format for local files\n return `/api/v1/assets/local/captions?src=${encodeURIComponent(normalizedSrc)}`;\n }\n\n // ============================================================================\n // Captions Data Loading - async methods instead of Tasks\n // ============================================================================\n\n #captionsDataLoaded = false;\n #captionsDataPromise: Promise<Caption | null> | null = null;\n #captionsDataValue: Caption | null = null;\n #transcriptionData: GetISOBMFFFileTranscriptionResult | null = null;\n\n /**\n * AsyncValue wrapper for backwards compatibility\n */\n unifiedCaptionsDataTask = new AsyncValue<Caption | null>();\n\n /**\n * Load captions data from all possible sources\n */\n async loadCaptionsData(signal?: AbortSignal): Promise<Caption | null> {\n // Return cached if already loaded\n if (this.#captionsDataLoaded && this.#captionsDataValue) {\n return this.#captionsDataValue;\n }\n\n // Return in-flight promise\n if (this.#captionsDataPromise) {\n return this.#captionsDataPromise;\n }\n\n this.unifiedCaptionsDataTask.startPending();\n this.#captionsDataPromise = this.#doLoadCaptionsData(signal);\n\n try {\n this.#captionsDataValue = await this.#captionsDataPromise;\n this.#captionsDataLoaded = true;\n if (this.#captionsDataValue) {\n this.unifiedCaptionsDataTask.setValue(this.#captionsDataValue);\n }\n return this.#captionsDataValue;\n } catch (error) {\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n console.error(\"Failed to load captions data:\", error);\n return null;\n } finally {\n this.#captionsDataPromise = null;\n }\n }\n\n async #doLoadCaptionsData(signal?: AbortSignal): Promise<Caption | null> {\n // Priority 1: Direct captionsData property\n if (this.captionsData) {\n return this.captionsData;\n }\n\n // Priority 2: Script element reference\n if (this.captionsScript) {\n const scriptElement = document.getElementById(this.captionsScript);\n if (scriptElement?.textContent) {\n try {\n return JSON.parse(scriptElement.textContent) as Caption;\n } catch (error) {\n console.error(`Failed to parse captions from script #${this.captionsScript}:`, error);\n }\n }\n }\n\n // Priority 3: External captions file\n if (this.captionsSrc) {\n try {\n const response = await this.fetch(this.captionsSrc, { signal });\n return await response.json() as Caption;\n } catch (error) {\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n console.error(`Failed to load captions from ${this.captionsSrc}:`, error);\n }\n }\n\n // Priority 4: Transcription from target element\n if (this.targetElement && !this.hasCustomCaptionsData) {\n const transcriptionPath = this.transcriptionsPath();\n if (transcriptionPath) {\n try {\n const response = await this.fetch(transcriptionPath, { signal });\n this.#transcriptionData = await response.json() as GetISOBMFFFileTranscriptionResult;\n signal?.throwIfAborted();\n\n // Load fragment for current time\n if (this.#transcriptionData) {\n return this.#loadTranscriptionFragment(signal);\n }\n } catch (error) {\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n // Transcription not available - not an error\n }\n }\n }\n\n return null;\n }\n\n async #loadTranscriptionFragment(signal?: AbortSignal): Promise<Caption | null> {\n if (!this.#transcriptionData) return null;\n\n const fragmentIndex = Math.floor(this.ownCurrentTimeMs / this.#transcriptionData.work_slice_ms);\n const fragmentPath = `${this.apiHost}/api/v1/transcriptions/${this.#transcriptionData.id}/fragments/${fragmentIndex}`;\n\n try {\n const response = await this.fetch(fragmentPath, { signal });\n return await response.json() as Caption;\n } catch (error) {\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n console.error(\"Failed to load transcription fragment:\", error);\n return null;\n }\n }\n\n /**\n * @deprecated Use FrameRenderable methods (prepareFrame, renderFrame) via FrameController instead.\n * This is a compatibility wrapper that delegates to the new system.\n */\n frameTask = createFrameTaskWrapper(this);\n\n // ============================================================================\n // FrameRenderable Implementation\n // Centralized frame control - no Lit Tasks\n // ============================================================================\n\n /**\n * Query readiness state for a given time.\n * @implements FrameRenderable\n */\n getFrameState(_timeMs: number): FrameState {\n // Check if captions data is loaded\n const hasData = this.#captionsDataLoaded && this.#captionsDataValue !== null;\n\n return {\n needsPreparation: !hasData,\n isReady: hasData,\n priority: PRIORITY_CAPTIONS,\n };\n }\n\n /**\n * Async preparation - waits for captions data to load.\n * @implements FrameRenderable\n */\n async prepareFrame(_timeMs: number, signal: AbortSignal): Promise<void> {\n await this.loadCaptionsData(signal);\n signal.throwIfAborted();\n }\n\n /**\n * Synchronous render - updates caption text containers.\n * Sets textContent directly on child elements (light DOM).\n * @implements FrameRenderable\n */\n renderFrame(_timeMs: number): void {\n // Update text containers by setting properties\n // Child elements update their textContent directly (light DOM)\n this.updateTextContainers();\n }\n\n // ============================================================================\n // End FrameRenderable Implementation\n // ============================================================================\n\n #rootTimegroupUpdateController?: ReactiveController;\n\n connectedCallback() {\n super.connectedCallback();\n\n // Start loading captions data\n this.loadCaptionsData().catch(() => {});\n\n // Try to get target element safely\n const target = this.targetSelector\n ? document.getElementById(this.targetSelector)\n : null;\n if (target && (target instanceof EFAudio || target instanceof EFVideo)) {\n new CrossUpdateController(target, this);\n }\n // For standalone captions with custom data, ensure proper timeline sync\n else if (this.hasCustomCaptionsData && this.rootTimegroup) {\n new CrossUpdateController(this.rootTimegroup, this);\n }\n\n // Ensure captions update when root timegroup's currentTimeMs changes\n if (this.rootTimegroup) {\n this.#rootTimegroupUpdateController = {\n hostUpdated: () => {\n Promise.resolve().then(() => {\n this.updateTextContainers();\n });\n },\n hostDisconnected: () => {\n this.#rootTimegroupUpdateController = undefined;\n },\n };\n this.rootTimegroup.addController(this.#rootTimegroupUpdateController);\n }\n\n // Prevent display:none from being set on the parent caption element.\n // IMPORTANT: This only applies to the parent <ef-captions> element, NOT to\n // caption child elements (<ef-captions-segment>, <ef-captions-active-word>, etc.).\n // Child elements MUST respect display:none for proper temporal visibility\n // in video rendering. Video export relies on display:none to hide elements\n // outside their time range.\n const observer = new MutationObserver(() => {\n if (this.style.display === \"none\") {\n this.style.removeProperty(\"display\");\n this.style.opacity = \"0\";\n this.style.pointerEvents = \"none\";\n } else if (!this.style.display || this.style.display === \"\") {\n this.style.removeProperty(\"opacity\");\n this.style.removeProperty(\"pointer-events\");\n }\n });\n observer.observe(this, { attributes: true, attributeFilter: [\"style\"] });\n }\n\n disconnectedCallback() {\n super.disconnectedCallback();\n if (this.#rootTimegroupUpdateController && this.rootTimegroup) {\n this.rootTimegroup.removeController(this.#rootTimegroupUpdateController);\n this.#rootTimegroupUpdateController = undefined;\n }\n }\n\n protected updated(\n changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>,\n ): void {\n // Set up root timegroup controller if rootTimegroup is now available\n if (this.rootTimegroup && !this.#rootTimegroupUpdateController) {\n this.#rootTimegroupUpdateController = {\n hostUpdated: () => {\n Promise.resolve().then(() => {\n this.updateTextContainers();\n });\n },\n hostDisconnected: () => {\n this.#rootTimegroupUpdateController = undefined;\n },\n };\n this.rootTimegroup.addController(this.#rootTimegroupUpdateController);\n }\n\n // Clean up controller if rootTimegroup changed\n if (\n changedProperties.has(\"rootTimegroup\") &&\n this.#rootTimegroupUpdateController\n ) {\n const oldRootTimegroup = changedProperties.get(\"rootTimegroup\") as\n | EFTimegroup\n | undefined;\n if (oldRootTimegroup && oldRootTimegroup !== this.rootTimegroup) {\n oldRootTimegroup.removeController(this.#rootTimegroupUpdateController);\n this.#rootTimegroupUpdateController = undefined;\n }\n }\n\n this.updateTextContainers();\n\n // Force duration recalculation when custom captions data changes\n if (\n changedProperties.has(\"captionsData\") ||\n changedProperties.has(\"captionsSrc\") ||\n changedProperties.has(\"captionsScript\")\n ) {\n // Invalidate caches and reload\n this.#cachedIntrinsicDurationMs = null;\n this.#captionsDataLoaded = false;\n this.#captionsDataValue = null;\n this.loadCaptionsData().catch(() => {});\n\n this.requestUpdate(\"intrinsicDurationMs\");\n\n flushSequenceDurationCache();\n flushStartTimeMsCache();\n\n if (this.parentTimegroup) {\n this.parentTimegroup.requestUpdate(\"durationMs\");\n this.parentTimegroup.requestUpdate(\"currentTime\");\n }\n }\n\n // Update captions when timeline position changes\n if (changedProperties.has(\"ownCurrentTimeMs\")) {\n this.updateTextContainers();\n }\n }\n\n updateTextContainers() {\n const captionsData = this.#captionsDataValue;\n if (!captionsData) {\n return;\n }\n\n // For captions with custom data, try to use the video's source time\n let currentTimeMs = this.ownCurrentTimeMs;\n if (this.hasCustomCaptionsData && this.parentTimegroup) {\n const videoElement = Array.from(this.parentTimegroup.children).find(\n (child): child is EFVideo => child instanceof EFVideo,\n );\n if (videoElement) {\n const sourceInMs = videoElement.sourceInMs ?? 0;\n currentTimeMs = videoElement.currentSourceTimeMs - sourceInMs;\n currentTimeMs = Math.max(0, Math.min(currentTimeMs, this.durationMs));\n }\n }\n\n const currentTimeSec = currentTimeMs / 1000;\n\n // Find the current word from word_segments\n const currentWord = captionsData.word_segments.find(\n (word) => currentTimeSec >= word.start && currentTimeSec < word.end,\n );\n\n // Find the current segment\n const currentSegment = captionsData.segments.find(\n (segment) =>\n currentTimeSec >= segment.start && currentTimeSec < segment.end,\n );\n\n for (const wordContainer of this.activeWordContainers) {\n if (currentWord) {\n const wordIndex = captionsData.word_segments.findIndex(\n (w) =>\n w.start === currentWord.start &&\n w.end === currentWord.end &&\n w.text === currentWord.text,\n );\n wordContainer.wordIndex = wordIndex >= 0 ? wordIndex : 0;\n wordContainer.wordText = currentWord.text; // Sets textContent directly\n } else {\n wordContainer.wordText = \"\"; // Hides element\n }\n }\n\n for (const segmentContainer of this.segmentContainers) {\n if (currentSegment) {\n segmentContainer.segmentText = currentSegment.text; // Sets textContent directly\n } else {\n segmentContainer.segmentText = \"\"; // Hides element\n }\n }\n\n // Process context for both word and segment cases\n if (currentWord && currentSegment) {\n const segmentWords = captionsData.word_segments.filter(\n (word) =>\n word.start >= currentSegment.start && word.end <= currentSegment.end,\n );\n\n const currentWordIndex = segmentWords.findIndex(\n (word) =>\n word.start === currentWord.start && word.end === currentWord.end,\n );\n\n if (currentWordIndex !== -1) {\n const beforeWords = segmentWords\n .slice(0, currentWordIndex)\n .map((w) => w.text.trim())\n .join(\" \");\n\n const afterWords = segmentWords\n .slice(currentWordIndex + 1)\n .map((w) => w.text.trim())\n .join(\" \");\n\n for (const container of this.beforeActiveWordContainers) {\n container.segmentText = beforeWords; // Sets textContent directly\n }\n\n for (const container of this.afterActiveWordContainers) {\n container.segmentText = afterWords; // Sets textContent directly\n }\n }\n } else if (currentSegment) {\n const segmentWords = captionsData.word_segments.filter(\n (word) =>\n word.start >= currentSegment.start && word.end <= currentSegment.end,\n );\n\n const firstWord = segmentWords[0];\n const isBeforeFirstWord = firstWord && currentTimeSec < firstWord.start;\n\n if (isBeforeFirstWord) {\n const allWords = segmentWords.map((w) => w.text.trim()).join(\" \");\n\n for (const container of this.beforeActiveWordContainers) {\n container.segmentText = \"\"; // Hides element\n }\n\n for (const container of this.afterActiveWordContainers) {\n container.segmentText = allWords; // Sets textContent directly\n }\n } else {\n const allCompletedWords = segmentWords\n .map((w) => w.text.trim())\n .join(\" \");\n\n for (const container of this.beforeActiveWordContainers) {\n container.segmentText = allCompletedWords; // Sets textContent directly\n }\n\n for (const container of this.afterActiveWordContainers) {\n container.segmentText = \"\"; // Hides element\n }\n }\n } else {\n for (const container of this.beforeActiveWordContainers) {\n container.segmentText = \"\"; // Hides element\n }\n\n for (const container of this.afterActiveWordContainers) {\n container.segmentText = \"\"; // Hides element\n }\n }\n }\n\n get targetElement() {\n const target = document.getElementById(this.targetSelector ?? \"\");\n if (target instanceof EFAudio || target instanceof EFVideo) {\n return target;\n }\n if (this.hasCustomCaptionsData) {\n return null;\n }\n return null;\n }\n\n get hasCustomCaptionsData(): boolean {\n return !!(this.captionsData || this.captionsSrc || this.captionsScript);\n }\n\n get intrinsicDurationMs(): number | undefined {\n if (this.#cachedIntrinsicDurationMs !== null) {\n return this.#cachedIntrinsicDurationMs;\n }\n\n let captionsData: Caption | null = null;\n\n if (this.captionsData) {\n captionsData = this.captionsData;\n } else if (this.captionsScript) {\n const scriptElement = document.getElementById(this.captionsScript);\n if (scriptElement?.textContent) {\n try {\n captionsData = JSON.parse(scriptElement.textContent) as Caption;\n } catch {\n // Invalid JSON\n }\n }\n } else if (this.#captionsDataValue) {\n captionsData = this.#captionsDataValue;\n }\n\n if (!captionsData) {\n if (!this.captionsData && !this.captionsScript && !this.captionsSrc) {\n this.#cachedIntrinsicDurationMs = undefined;\n }\n return undefined;\n }\n\n let result: number;\n if (\n captionsData.segments.length === 0 &&\n captionsData.word_segments.length === 0\n ) {\n result = 0;\n } else {\n const maxSegmentEnd =\n captionsData.segments.length > 0\n ? captionsData.segments.reduce(\n (max, s) => (s.end > max ? s.end : max),\n 0,\n )\n : 0;\n const maxWordEnd =\n captionsData.word_segments.length > 0\n ? captionsData.word_segments.reduce(\n (max, w) => (w.end > max ? w.end : max),\n 0,\n )\n : 0;\n\n result = Math.max(maxSegmentEnd, maxWordEnd) * 1000;\n }\n\n this.#cachedIntrinsicDurationMs = result;\n return result;\n }\n\n get hasOwnDuration(): boolean {\n return !!(\n this.captionsData ||\n this.captionsScript ||\n this.#captionsDataValue\n );\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"ef-captions\": EFCaptions;\n \"ef-captions-active-word\": EFCaptionsActiveWord;\n \"ef-captions-segment\": EFCaptionsSegment;\n \"ef-captions-before-active-word\": EFCaptionsBeforeActiveWord;\n \"ef-captions-after-active-word\": EFCaptionsAfterActiveWord;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"EFCaptions.js","names":["EFCaptionsActiveWord","#wordText","#wordIndex","EFCaptionsSegment","#segmentText","EFCaptionsBeforeActiveWord","EFCaptionsAfterActiveWord","EFCaptions","#captionsDataLoaded","#captionsDataValue","#captionsDataPromise","#doLoadCaptionsData","#findElementById","#transcriptionData","#loadTranscriptionFragment","#rootTimegroupUpdateController","#cachedIntrinsicDurationMs","captionsData: Caption | null","result: number"],"sources":["../../src/elements/EFCaptions.ts"],"sourcesContent":["import { css, html, LitElement, type PropertyValueMap } from \"lit\";\nimport { customElement, property } from \"lit/decorators.js\";\nimport type { ReactiveController } from \"lit\";\nimport type { GetISOBMFFFileTranscriptionResult } from \"../../../api/src/index.js\";\nimport {\n type FrameRenderable,\n type FrameState,\n PRIORITY_CAPTIONS,\n} from \"../preview/FrameController.js\";\nimport { AsyncValue } from \"./EFMedia.js\";\nimport { CrossUpdateController } from \"./CrossUpdateController.js\";\nimport { EFAudio } from \"./EFAudio.js\";\nimport { EFSourceMixin } from \"./EFSourceMixin.js\";\nimport { EFTemporal, flushStartTimeMsCache } from \"./EFTemporal.js\";\nimport { flushSequenceDurationCache, EFTimegroup } from \"./EFTimegroup.js\";\nimport { EFVideo } from \"./EFVideo.js\";\nimport { FetchMixin } from \"./FetchMixin.js\";\n\nexport interface WordSegment {\n text: string;\n start: number;\n end: number;\n}\n\nexport interface Segment {\n start: number;\n end: number;\n text: string;\n}\n\nexport interface Caption {\n segments: Segment[];\n word_segments: WordSegment[];\n}\n\nconst stopWords = new Set([\"\", \".\", \"!\", \"?\", \",\"]);\n\n/**\n * Caption active word element - displays the currently spoken word.\n * Uses light DOM for simplicity - parent sets textContent directly.\n */\n@customElement(\"ef-captions-active-word\")\nexport class EFCaptionsActiveWord extends LitElement {\n #wordText = \"\";\n #wordIndex = 0;\n\n set wordText(text: string) {\n this.#wordText = text;\n // Hide element if no content or only stop words\n if (!text || stopWords.has(text)) {\n this.hidden = true;\n this.textContent = \"\";\n } else {\n this.hidden = false;\n // Add trailing space to maintain consistent spacing with surrounding words\n this.textContent = text + \" \";\n }\n }\n\n get wordText(): string {\n return this.#wordText;\n }\n\n set wordIndex(index: number) {\n this.#wordIndex = index;\n // Set deterministic --ef-word-seed value based on word index\n const seed = (index * 9007) % 233; // Prime numbers for better distribution\n const seedValue = seed / 233; // Normalize to 0-1 range\n this.style.setProperty(\"--ef-word-seed\", seedValue.toString());\n }\n\n get wordIndex(): number {\n return this.#wordIndex;\n }\n}\n\n/**\n * Caption segment element - displays a full caption segment.\n * Uses light DOM for simplicity - parent sets textContent directly.\n */\n@customElement(\"ef-captions-segment\")\nexport class EFCaptionsSegment extends LitElement {\n #segmentText = \"\";\n\n set segmentText(text: string) {\n this.#segmentText = text;\n // Hide element if no content or only stop words\n if (!text || stopWords.has(text)) {\n this.hidden = true;\n this.textContent = \"\";\n } else {\n this.hidden = false;\n this.textContent = text;\n }\n }\n\n get segmentText(): string {\n return this.#segmentText;\n }\n}\n\n/**\n * Caption before-active-word element - displays words before the current word.\n * Uses light DOM for simplicity - parent sets textContent directly.\n */\n@customElement(\"ef-captions-before-active-word\")\nexport class EFCaptionsBeforeActiveWord extends EFCaptionsSegment {\n set segmentText(text: string) {\n // Check if there's an active word by looking for sibling active word element\n const activeWord = this.closest(\"ef-captions\")?.querySelector(\n \"ef-captions-active-word\",\n ) as EFCaptionsActiveWord;\n const hasActiveWord = activeWord?.wordText;\n\n // Add trailing space if there's an active word coming after us\n const finalText = text && hasActiveWord ? text + \" \" : text;\n\n // Hide element if no content or only stop words\n if (!finalText || stopWords.has(finalText)) {\n this.hidden = true;\n this.textContent = \"\";\n } else {\n this.hidden = false;\n this.textContent = finalText;\n }\n }\n}\n\n/**\n * Caption after-active-word element - displays words after the current word.\n * Uses light DOM for simplicity - parent sets textContent directly.\n */\n@customElement(\"ef-captions-after-active-word\")\nexport class EFCaptionsAfterActiveWord extends EFCaptionsSegment {\n set segmentText(text: string) {\n // No leading space - active word will add trailing space\n const finalText = text;\n\n // Hide element if no content or only stop words\n if (!finalText || stopWords.has(finalText)) {\n this.hidden = true;\n this.textContent = \"\";\n } else {\n this.hidden = false;\n this.textContent = finalText;\n }\n }\n}\n\n@customElement(\"ef-captions\")\nexport class EFCaptions\n extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {\n assetType: \"caption_files\",\n })\n implements FrameRenderable\n{\n static styles = [\n css`\n :host {\n display: block;\n white-space: normal;\n line-height: 1;\n gap: 0;\n }\n ::slotted(*) {\n display: inline;\n margin: 0;\n padding: 0;\n }\n `,\n ];\n\n @property({ type: String, attribute: \"target\", reflect: true })\n targetSelector = \"\";\n\n set target(value: string) {\n this.targetSelector = value;\n }\n\n @property({ attribute: \"word-style\" })\n wordStyle = \"\";\n\n /**\n * URL or path to a JSON file containing custom captions data.\n * The JSON should conform to the Caption interface with 'segments' and 'word_segments' arrays.\n */\n @property({ type: String, attribute: \"captions-src\", reflect: true })\n captionsSrc = \"\";\n\n /**\n * Direct captions data object. Takes priority over captions-src and captions-script.\n * Should conform to the Caption interface with 'segments' and 'word_segments' arrays.\n */\n @property({ type: Object, attribute: false })\n captionsData: Caption | null = null;\n\n /**\n * ID of a <script> element containing JSON captions data.\n * The script's textContent should be valid JSON conforming to the Caption interface.\n */\n @property({ type: String, attribute: \"captions-script\", reflect: true })\n captionsScript = \"\";\n\n activeWordContainers = this.getElementsByTagName(\"ef-captions-active-word\");\n segmentContainers = this.getElementsByTagName(\"ef-captions-segment\");\n beforeActiveWordContainers = this.getElementsByTagName(\n \"ef-captions-before-active-word\",\n );\n afterActiveWordContainers = this.getElementsByTagName(\n \"ef-captions-after-active-word\",\n );\n\n // Cache for intrinsicDurationMs to avoid expensive O(n) recalculation every frame\n #cachedIntrinsicDurationMs: number | undefined | null = null; // null = not computed, undefined = no duration\n\n render() {\n return html`<slot></slot>`;\n }\n\n transcriptionsPath() {\n if (!this.targetElement) {\n return null;\n }\n const fileId = this.targetElement.fileId ?? this.targetElement.assetId;\n if (fileId) {\n return `${this.apiHost}/api/v1/files/${fileId}/transcription`;\n }\n return null;\n }\n\n captionsPath() {\n if (!this.targetElement) {\n return null;\n }\n const fileId = this.targetElement.fileId ?? this.targetElement.assetId;\n if (fileId) {\n return `${this.apiHost}/api/v1/files/${fileId}`;\n }\n const targetSrc = this.targetElement.src;\n // Normalize the path: remove leading slash and any double slashes\n let normalizedSrc = targetSrc.startsWith(\"/\")\n ? targetSrc.slice(1)\n : targetSrc;\n normalizedSrc = normalizedSrc.replace(/^\\/+/, \"\");\n // Use production API format for local files\n return `/api/v1/assets/local/captions?src=${encodeURIComponent(normalizedSrc)}`;\n }\n\n // ============================================================================\n // Captions Data Loading - async methods instead of Tasks\n // ============================================================================\n\n #captionsDataLoaded = false;\n #captionsDataPromise: Promise<Caption | null> | null = null;\n #captionsDataValue: Caption | null = null;\n #transcriptionData: GetISOBMFFFileTranscriptionResult | null = null;\n\n /**\n * AsyncValue wrapper for backwards compatibility\n */\n unifiedCaptionsDataTask = new AsyncValue<Caption | null>();\n\n override shouldAutoReady(): boolean {\n return false;\n }\n\n /**\n * Load captions data from all possible sources\n */\n async loadCaptionsData(signal?: AbortSignal): Promise<Caption | null> {\n // Return cached if already loaded\n if (this.#captionsDataLoaded && this.#captionsDataValue) {\n this.setContentReadyState(\"ready\");\n return this.#captionsDataValue;\n }\n\n // Return in-flight promise\n if (this.#captionsDataPromise) {\n return this.#captionsDataPromise;\n }\n\n this.unifiedCaptionsDataTask.startPending();\n this.setContentReadyState(\"loading\");\n this.#captionsDataPromise = this.#doLoadCaptionsData(signal);\n\n try {\n this.#captionsDataValue = await this.#captionsDataPromise;\n this.#captionsDataLoaded = true;\n if (this.#captionsDataValue) {\n this.unifiedCaptionsDataTask.setValue(this.#captionsDataValue);\n }\n this.setContentReadyState(\"ready\");\n return this.#captionsDataValue;\n } catch (error) {\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n console.error(\"Failed to load captions data:\", error);\n this.setContentReadyState(\"error\");\n return null;\n } finally {\n this.#captionsDataPromise = null;\n }\n }\n\n async #doLoadCaptionsData(signal?: AbortSignal): Promise<Caption | null> {\n // Priority 1: Direct captionsData property\n if (this.captionsData) {\n return this.captionsData;\n }\n\n // Priority 2: Script element reference\n if (this.captionsScript) {\n const scriptElement = this.#findElementById(this.captionsScript);\n if (scriptElement?.textContent) {\n try {\n return JSON.parse(scriptElement.textContent) as Caption;\n } catch (error) {\n console.error(\n `Failed to parse captions from script #${this.captionsScript}:`,\n error,\n );\n }\n }\n }\n\n // Priority 3: External captions file\n if (this.captionsSrc) {\n try {\n const response = await this.fetch(this.captionsSrc, { signal });\n return (await response.json()) as Caption;\n } catch (error) {\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n console.error(\n `Failed to load captions from ${this.captionsSrc}:`,\n error,\n );\n }\n }\n\n // Priority 4: Transcription from target element\n if (this.targetElement && !this.hasCustomCaptionsData) {\n const transcriptionPath = this.transcriptionsPath();\n if (transcriptionPath) {\n try {\n const response = await this.fetch(transcriptionPath, { signal });\n this.#transcriptionData =\n (await response.json()) as GetISOBMFFFileTranscriptionResult;\n signal?.throwIfAborted();\n\n // Load fragment for current time\n if (this.#transcriptionData) {\n return this.#loadTranscriptionFragment(signal);\n }\n } catch (error) {\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n // Transcription not available - not an error\n }\n }\n }\n\n return null;\n }\n\n async #loadTranscriptionFragment(\n signal?: AbortSignal,\n ): Promise<Caption | null> {\n if (!this.#transcriptionData) return null;\n\n const fragmentIndex = Math.floor(\n this.ownCurrentTimeMs / this.#transcriptionData.work_slice_ms,\n );\n const fragmentPath = `${this.apiHost}/api/v1/transcriptions/${this.#transcriptionData.id}/fragments/${fragmentIndex}`;\n\n try {\n const response = await this.fetch(fragmentPath, { signal });\n return (await response.json()) as Caption;\n } catch (error) {\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n console.error(\"Failed to load transcription fragment:\", error);\n return null;\n }\n }\n\n // ============================================================================\n // FrameRenderable Implementation\n // ============================================================================\n\n /**\n * Query readiness state for a given time.\n * @implements FrameRenderable\n */\n getFrameState(_timeMs: number): FrameState {\n // Check if captions data is loaded\n const hasData =\n this.#captionsDataLoaded && this.#captionsDataValue !== null;\n\n return {\n needsPreparation: !hasData,\n isReady: hasData,\n priority: PRIORITY_CAPTIONS,\n };\n }\n\n /**\n * Async preparation - waits for captions data to load.\n * @implements FrameRenderable\n */\n async prepareFrame(_timeMs: number, signal: AbortSignal): Promise<void> {\n await this.loadCaptionsData(signal);\n signal.throwIfAborted();\n }\n\n /**\n * Synchronous render - updates caption text containers.\n * Sets textContent directly on child elements (light DOM).\n * @implements FrameRenderable\n */\n renderFrame(_timeMs: number): void {\n // Update text containers by setting properties\n // Child elements update their textContent directly (light DOM)\n this.updateTextContainers();\n }\n\n // ============================================================================\n // End FrameRenderable Implementation\n // ============================================================================\n\n #rootTimegroupUpdateController?: ReactiveController;\n\n connectedCallback() {\n super.connectedCallback();\n\n // Start loading captions data\n this.loadCaptionsData().catch(() => {});\n\n // Try to get target element safely\n const target = this.targetSelector\n ? this.#findElementById(this.targetSelector)\n : null;\n if (target && (target instanceof EFAudio || target instanceof EFVideo)) {\n new CrossUpdateController(target, this);\n }\n // For standalone captions with custom data, ensure proper timeline sync\n else if (this.hasCustomCaptionsData && this.rootTimegroup) {\n new CrossUpdateController(this.rootTimegroup, this);\n }\n\n // Ensure captions update when root timegroup's currentTimeMs changes\n if (this.rootTimegroup) {\n this.#rootTimegroupUpdateController = {\n hostUpdated: () => {\n Promise.resolve().then(() => {\n this.updateTextContainers();\n });\n },\n hostDisconnected: () => {\n this.#rootTimegroupUpdateController = undefined;\n },\n };\n this.rootTimegroup.addController(this.#rootTimegroupUpdateController);\n }\n\n // Prevent display:none from being set on the parent caption element.\n // IMPORTANT: This only applies to the parent <ef-captions> element, NOT to\n // caption child elements (<ef-captions-segment>, <ef-captions-active-word>, etc.).\n // Child elements MUST respect display:none for proper temporal visibility\n // in video rendering. Video export relies on display:none to hide elements\n // outside their time range.\n const observer = new MutationObserver(() => {\n if (this.style.display === \"none\") {\n this.style.removeProperty(\"display\");\n this.style.opacity = \"0\";\n this.style.pointerEvents = \"none\";\n } else if (!this.style.display || this.style.display === \"\") {\n this.style.removeProperty(\"opacity\");\n this.style.removeProperty(\"pointer-events\");\n }\n });\n observer.observe(this, { attributes: true, attributeFilter: [\"style\"] });\n }\n\n disconnectedCallback() {\n super.disconnectedCallback();\n if (this.#rootTimegroupUpdateController && this.rootTimegroup) {\n this.rootTimegroup.removeController(this.#rootTimegroupUpdateController);\n this.#rootTimegroupUpdateController = undefined;\n }\n }\n\n protected updated(\n changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>,\n ): void {\n // Set up root timegroup controller if rootTimegroup is now available\n if (this.rootTimegroup && !this.#rootTimegroupUpdateController) {\n this.#rootTimegroupUpdateController = {\n hostUpdated: () => {\n Promise.resolve().then(() => {\n this.updateTextContainers();\n });\n },\n hostDisconnected: () => {\n this.#rootTimegroupUpdateController = undefined;\n },\n };\n this.rootTimegroup.addController(this.#rootTimegroupUpdateController);\n }\n\n // Clean up controller if rootTimegroup changed\n if (\n changedProperties.has(\"rootTimegroup\") &&\n this.#rootTimegroupUpdateController\n ) {\n const oldRootTimegroup = changedProperties.get(\"rootTimegroup\") as\n | EFTimegroup\n | undefined;\n if (oldRootTimegroup && oldRootTimegroup !== this.rootTimegroup) {\n oldRootTimegroup.removeController(this.#rootTimegroupUpdateController);\n this.#rootTimegroupUpdateController = undefined;\n }\n }\n\n this.updateTextContainers();\n\n // Force duration recalculation when custom captions data changes\n if (\n changedProperties.has(\"captionsData\") ||\n changedProperties.has(\"captionsSrc\") ||\n changedProperties.has(\"captionsScript\")\n ) {\n this.emitContentChange(\"source\");\n this.#cachedIntrinsicDurationMs = null;\n this.#captionsDataLoaded = false;\n this.#captionsDataValue = null;\n this.loadCaptionsData().catch(() => {});\n\n this.requestUpdate(\"intrinsicDurationMs\");\n\n flushSequenceDurationCache();\n flushStartTimeMsCache();\n\n if (this.parentTimegroup) {\n this.parentTimegroup.requestUpdate(\"durationMs\");\n this.parentTimegroup.requestUpdate(\"currentTime\");\n }\n }\n\n // Update captions when timeline position changes\n if (changedProperties.has(\"ownCurrentTimeMs\")) {\n this.updateTextContainers();\n }\n }\n\n updateTextContainers() {\n const captionsData = this.#captionsDataValue;\n if (!captionsData) {\n return;\n }\n\n // For captions with custom data, try to use the video's source time\n let currentTimeMs = this.ownCurrentTimeMs;\n if (this.hasCustomCaptionsData && this.parentTimegroup) {\n const videoElement = Array.from(this.parentTimegroup.children).find(\n (child): child is EFVideo => child instanceof EFVideo,\n );\n if (videoElement) {\n const sourceInMs = videoElement.sourceInMs ?? 0;\n currentTimeMs = videoElement.currentSourceTimeMs - sourceInMs;\n currentTimeMs = Math.max(0, Math.min(currentTimeMs, this.durationMs));\n }\n }\n\n const currentTimeSec = currentTimeMs / 1000;\n\n // Find the current word from word_segments\n const currentWord = captionsData.word_segments.find(\n (word) => currentTimeSec >= word.start && currentTimeSec < word.end,\n );\n\n // Find the current segment\n const currentSegment = captionsData.segments.find(\n (segment) =>\n currentTimeSec >= segment.start && currentTimeSec < segment.end,\n );\n\n for (const wordContainer of this.activeWordContainers) {\n if (currentWord) {\n const wordIndex = captionsData.word_segments.findIndex(\n (w) =>\n w.start === currentWord.start &&\n w.end === currentWord.end &&\n w.text === currentWord.text,\n );\n wordContainer.wordIndex = wordIndex >= 0 ? wordIndex : 0;\n wordContainer.wordText = currentWord.text; // Sets textContent directly\n } else {\n wordContainer.wordText = \"\"; // Hides element\n }\n }\n\n for (const segmentContainer of this.segmentContainers) {\n if (currentSegment) {\n segmentContainer.segmentText = currentSegment.text; // Sets textContent directly\n } else {\n segmentContainer.segmentText = \"\"; // Hides element\n }\n }\n\n // Process context for both word and segment cases\n if (currentWord && currentSegment) {\n const segmentWords = captionsData.word_segments.filter(\n (word) =>\n word.start >= currentSegment.start && word.end <= currentSegment.end,\n );\n\n const currentWordIndex = segmentWords.findIndex(\n (word) =>\n word.start === currentWord.start && word.end === currentWord.end,\n );\n\n if (currentWordIndex !== -1) {\n const beforeWords = segmentWords\n .slice(0, currentWordIndex)\n .map((w) => w.text.trim())\n .join(\" \");\n\n const afterWords = segmentWords\n .slice(currentWordIndex + 1)\n .map((w) => w.text.trim())\n .join(\" \");\n\n for (const container of this.beforeActiveWordContainers) {\n container.segmentText = beforeWords; // Sets textContent directly\n }\n\n for (const container of this.afterActiveWordContainers) {\n container.segmentText = afterWords; // Sets textContent directly\n }\n }\n } else if (currentSegment) {\n const segmentWords = captionsData.word_segments.filter(\n (word) =>\n word.start >= currentSegment.start && word.end <= currentSegment.end,\n );\n\n const firstWord = segmentWords[0];\n const isBeforeFirstWord = firstWord && currentTimeSec < firstWord.start;\n\n if (isBeforeFirstWord) {\n const allWords = segmentWords.map((w) => w.text.trim()).join(\" \");\n\n for (const container of this.beforeActiveWordContainers) {\n container.segmentText = \"\"; // Hides element\n }\n\n for (const container of this.afterActiveWordContainers) {\n container.segmentText = allWords; // Sets textContent directly\n }\n } else {\n const allCompletedWords = segmentWords\n .map((w) => w.text.trim())\n .join(\" \");\n\n for (const container of this.beforeActiveWordContainers) {\n container.segmentText = allCompletedWords; // Sets textContent directly\n }\n\n for (const container of this.afterActiveWordContainers) {\n container.segmentText = \"\"; // Hides element\n }\n }\n } else {\n for (const container of this.beforeActiveWordContainers) {\n container.segmentText = \"\"; // Hides element\n }\n\n for (const container of this.afterActiveWordContainers) {\n container.segmentText = \"\"; // Hides element\n }\n }\n }\n\n get targetElement() {\n const target = this.targetSelector\n ? this.#findElementById(this.targetSelector)\n : null;\n if (target instanceof EFAudio || target instanceof EFVideo) {\n return target;\n }\n if (this.hasCustomCaptionsData) {\n return null;\n }\n return null;\n }\n\n get hasCustomCaptionsData(): boolean {\n return !!(this.captionsData || this.captionsSrc || this.captionsScript);\n }\n\n /**\n * Find element by ID, searching within clone scope first to avoid cross-boundary references.\n * @private\n */\n #findElementById(id: string): Element | null {\n // Search within nearest timegroup or configuration container first\n const container = this.closest(\"ef-timegroup, ef-configuration\");\n if (container) {\n const result = container.querySelector(`#${CSS.escape(id)}`);\n if (result) return result;\n }\n\n // Fall back to document-wide search\n return document.getElementById(id);\n }\n\n get intrinsicDurationMs(): number | undefined {\n if (this.#cachedIntrinsicDurationMs !== null) {\n return this.#cachedIntrinsicDurationMs;\n }\n\n let captionsData: Caption | null = null;\n\n if (this.captionsData) {\n captionsData = this.captionsData;\n } else if (this.captionsScript) {\n const scriptElement = this.#findElementById(this.captionsScript);\n if (scriptElement?.textContent) {\n try {\n captionsData = JSON.parse(scriptElement.textContent) as Caption;\n } catch {\n // Invalid JSON\n }\n }\n } else if (this.#captionsDataValue) {\n captionsData = this.#captionsDataValue;\n }\n\n if (!captionsData) {\n if (!this.captionsData && !this.captionsScript && !this.captionsSrc) {\n this.#cachedIntrinsicDurationMs = undefined;\n }\n return undefined;\n }\n\n let result: number;\n if (\n captionsData.segments.length === 0 &&\n captionsData.word_segments.length === 0\n ) {\n result = 0;\n } else {\n const maxSegmentEnd =\n captionsData.segments.length > 0\n ? captionsData.segments.reduce(\n (max, s) => (s.end > max ? s.end : max),\n 0,\n )\n : 0;\n const maxWordEnd =\n captionsData.word_segments.length > 0\n ? captionsData.word_segments.reduce(\n (max, w) => (w.end > max ? w.end : max),\n 0,\n )\n : 0;\n\n result = Math.max(maxSegmentEnd, maxWordEnd) * 1000;\n }\n\n this.#cachedIntrinsicDurationMs = result;\n return result;\n }\n\n get hasOwnDuration(): boolean {\n return !!(\n this.captionsData ||\n this.captionsScript ||\n this.#captionsDataValue\n );\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"ef-captions\": EFCaptions;\n \"ef-captions-active-word\": EFCaptionsActiveWord;\n \"ef-captions-segment\": EFCaptionsSegment;\n \"ef-captions-before-active-word\": EFCaptionsBeforeActiveWord;\n \"ef-captions-after-active-word\": EFCaptionsAfterActiveWord;\n }\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { FrameRenderable, FrameState, FrameTask } from "../preview/FrameController.js";
1
+ import { FrameRenderable, FrameState } from "../preview/FrameController.js";
2
2
  import { EFSourceMixinInterface } from "./EFSourceMixin.js";
3
3
  import { TemporalMixinInterface } from "./EFTemporal.js";
4
4
  import { FetchMixinInterface } from "./FetchMixin.js";
@@ -12,11 +12,13 @@ declare const EFImage_base: (new (...args: any[]) => TemporalMixinInterface) & (
12
12
  declare class EFImage extends EFImage_base implements FrameRenderable {
13
13
  #private;
14
14
  static styles: lit2.CSSResult[];
15
+ static get observedAttributes(): string[];
16
+ attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
15
17
  imageRef: lit_html_directives_ref_js0.Ref<HTMLImageElement>;
16
18
  canvasRef: lit_html_directives_ref_js0.Ref<HTMLCanvasElement>;
17
19
  /**
18
20
  * Get the current render version.
19
- * Version increments when src or assetId changes.
21
+ * Version increments when src or fileId changes.
20
22
  * @public
21
23
  */
22
24
  get renderVersion(): number;
@@ -26,21 +28,20 @@ declare class EFImage extends EFImage_base implements FrameRenderable {
26
28
  * @public
27
29
  */
28
30
  get hasAlpha(): boolean;
29
- set assetId(value: string | null);
31
+ set fileId(value: string | null);
32
+ get fileId(): string | null;
33
+ /** @deprecated Use fileId instead */
30
34
  get assetId(): string | null;
35
+ set assetId(value: string | null);
31
36
  render(): lit_html1.TemplateResult<1>;
32
37
  private isDirectUrl;
33
38
  assetPath(): string;
34
39
  get hasOwnDuration(): boolean;
40
+ shouldAutoReady(): boolean;
35
41
  /**
36
42
  * Load image from the configured source
37
43
  */
38
44
  loadImage(signal?: AbortSignal): Promise<void>;
39
- /**
40
- * @deprecated Use FrameRenderable methods (prepareFrame, renderFrame) via FrameController instead.
41
- * This is a compatibility wrapper that delegates to the new system.
42
- */
43
- frameTask: FrameTask;
44
45
  /**
45
46
  * Query readiness state for a given time.
46
47
  * @implements FrameRenderable
@@ -57,6 +58,7 @@ declare class EFImage extends EFImage_base implements FrameRenderable {
57
58
  */
58
59
  renderFrame(_timeMs: number): void;
59
60
  protected updated(changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): void;
61
+ disconnectedCallback(): void;
60
62
  /**
61
63
  * Get the natural dimensions of the image.
62
64
  * Returns null if the image hasn't loaded yet.
@@ -1,6 +1,6 @@
1
+ import { PRIORITY_IMAGE } from "../preview/FrameController.js";
1
2
  import { __decorate } from "../_virtual/_@oxc-project_runtime@0.95.0/helpers/decorate.js";
2
3
  import { EFTemporal } from "./EFTemporal.js";
3
- import { PRIORITY_IMAGE, createFrameTaskWrapper } from "../preview/FrameController.js";
4
4
  import { EFSourceMixin } from "./EFSourceMixin.js";
5
5
  import { FetchMixin } from "./FetchMixin.js";
6
6
  import { LitElement, css, html } from "lit";
@@ -13,24 +13,33 @@ let EFImage = class EFImage$1 extends EFTemporal(EFSourceMixin(FetchMixin(LitEle
13
13
  super(..._args);
14
14
  this.imageRef = createRef();
15
15
  this.canvasRef = createRef();
16
- this.frameTask = createFrameTaskWrapper(this);
17
16
  }
18
17
  static {
19
18
  this.styles = [css`
20
19
  :host {
21
20
  display: block;
22
- display: flex;
23
- align-items: center;
24
- justify-content: center;
21
+ position: relative;
22
+ object-fit: contain;
23
+ object-position: center;
25
24
  }
26
25
  canvas, img {
27
26
  width: 100%;
28
27
  height: 100%;
29
- object-fit: var(--object-fit, contain);
30
- object-position: var(--object-position, center);
28
+ object-fit: inherit;
29
+ object-position: inherit;
31
30
  }
32
31
  `];
33
32
  }
33
+ static get observedAttributes() {
34
+ return [...super.observedAttributes || [], "asset-id"];
35
+ }
36
+ attributeChangedCallback(name, oldValue, newValue) {
37
+ if (name === "asset-id") {
38
+ this.fileId = newValue;
39
+ return;
40
+ }
41
+ super.attributeChangedCallback(name, oldValue, newValue);
42
+ }
34
43
  /**
35
44
  * Render version counter - increments when visual content changes.
36
45
  * Used by RenderContext to cache rendered dataURLs.
@@ -38,7 +47,7 @@ let EFImage = class EFImage$1 extends EFTemporal(EFSourceMixin(FetchMixin(LitEle
38
47
  #renderVersion = 0;
39
48
  /**
40
49
  * Get the current render version.
41
- * Version increments when src or assetId changes.
50
+ * Version increments when src or fileId changes.
42
51
  * @public
43
52
  */
44
53
  get renderVersion() {
@@ -57,22 +66,30 @@ let EFImage = class EFImage$1 extends EFTemporal(EFSourceMixin(FetchMixin(LitEle
57
66
  get hasAlpha() {
58
67
  return this.#hasAlpha;
59
68
  }
60
- #assetId = null;
61
- set assetId(value) {
62
- this.#assetId = value;
69
+ #fileId = null;
70
+ set fileId(value) {
71
+ this.#fileId = value;
63
72
  }
73
+ get fileId() {
74
+ return this.#fileId ?? this.getAttribute("file-id") ?? this.getAttribute("asset-id");
75
+ }
76
+ /** @deprecated Use fileId instead */
64
77
  get assetId() {
65
- return this.#assetId ?? this.getAttribute("asset-id");
78
+ return this.fileId;
79
+ }
80
+ set assetId(value) {
81
+ this.fileId = value;
66
82
  }
67
83
  render() {
68
84
  const assetPath = this.assetPath();
69
85
  return this.isDirectUrl(assetPath) ? html`<img ${ref(this.imageRef)} src=${assetPath} />` : html`<canvas ${ref(this.canvasRef)}></canvas>`;
70
86
  }
71
87
  isDirectUrl(src) {
88
+ if (this.fileId) return false;
72
89
  return src.startsWith("http://") || src.startsWith("https://") || src.startsWith("data:");
73
90
  }
74
91
  assetPath() {
75
- if (this.assetId) return `${this.apiHost}/api/v1/image_files/${this.assetId}`;
92
+ if (this.fileId) return `${this.apiHost}/api/v1/files/${this.fileId}`;
76
93
  if (this.isDirectUrl(this.src)) return this.src;
77
94
  let normalizedSrc = this.src.startsWith("/") ? this.src.slice(1) : this.src;
78
95
  normalizedSrc = normalizedSrc.replace(/^\/+/, "");
@@ -84,17 +101,37 @@ let EFImage = class EFImage$1 extends EFTemporal(EFSourceMixin(FetchMixin(LitEle
84
101
  #imageLoaded = false;
85
102
  #imageLoadPromise = null;
86
103
  #lastLoadedPath = null;
104
+ #currentObjectUrl = null;
105
+ shouldAutoReady() {
106
+ return !this.src && !this.fileId;
107
+ }
87
108
  /**
88
109
  * Load image from the configured source
89
110
  */
90
111
  async loadImage(signal) {
91
112
  const assetPath = this.assetPath();
92
- if (!this.src && !this.assetId) return;
93
- if (this.#imageLoaded && this.#lastLoadedPath === assetPath) return;
113
+ if (!this.src && !this.fileId) return;
114
+ if (this.#imageLoaded && this.#lastLoadedPath === assetPath) {
115
+ this.setContentReadyState("ready");
116
+ return;
117
+ }
94
118
  if (this.#imageLoadPromise && this.#lastLoadedPath === assetPath) return this.#imageLoadPromise;
119
+ this.setContentReadyState("loading");
95
120
  if (this.isDirectUrl(assetPath)) {
96
- this.#imageLoaded = true;
97
121
  this.#lastLoadedPath = assetPath;
122
+ this.#imageLoadPromise = this.#waitForImageElement(signal);
123
+ try {
124
+ await this.#imageLoadPromise;
125
+ this.#imageLoaded = true;
126
+ this.setContentReadyState("ready");
127
+ } catch (error) {
128
+ if (error instanceof DOMException && error.name === "AbortError") throw error;
129
+ console.error("EFImage img element load error", error);
130
+ this.setContentReadyState("error");
131
+ throw error;
132
+ } finally {
133
+ this.#imageLoadPromise = null;
134
+ }
98
135
  return;
99
136
  }
100
137
  this.#lastLoadedPath = assetPath;
@@ -102,14 +139,39 @@ let EFImage = class EFImage$1 extends EFTemporal(EFSourceMixin(FetchMixin(LitEle
102
139
  try {
103
140
  await this.#imageLoadPromise;
104
141
  this.#imageLoaded = true;
142
+ this.setContentReadyState("ready");
105
143
  } catch (error) {
106
144
  if (error instanceof DOMException && error.name === "AbortError") throw error;
107
145
  if (error instanceof Error && error.message === "Canvas not ready") return;
108
146
  console.error("EFImage load error", error);
147
+ this.setContentReadyState("error");
109
148
  } finally {
110
149
  this.#imageLoadPromise = null;
111
150
  }
112
151
  }
152
+ async #waitForImageElement(signal) {
153
+ if (!this.imageRef.value) throw new Error("Image element not ready");
154
+ const img = this.imageRef.value;
155
+ if (img.complete && img.naturalHeight !== 0) return;
156
+ return new Promise((resolve, reject) => {
157
+ if (signal?.aborted) {
158
+ reject(new DOMException("Aborted", "AbortError"));
159
+ return;
160
+ }
161
+ const abortHandler = () => {
162
+ reject(new DOMException("Aborted", "AbortError"));
163
+ };
164
+ signal?.addEventListener("abort", abortHandler, { once: true });
165
+ img.onload = () => {
166
+ signal?.removeEventListener("abort", abortHandler);
167
+ resolve();
168
+ };
169
+ img.onerror = (error) => {
170
+ signal?.removeEventListener("abort", abortHandler);
171
+ reject(error);
172
+ };
173
+ });
174
+ }
113
175
  async #doLoadImage(assetPath, signal) {
114
176
  const response = await this.fetch(assetPath, { signal });
115
177
  signal?.throwIfAborted();
@@ -118,7 +180,6 @@ let EFImage = class EFImage$1 extends EFTemporal(EFSourceMixin(FetchMixin(LitEle
118
180
  signal?.throwIfAborted();
119
181
  const mimeType = blob.type.toLowerCase();
120
182
  this.#hasAlpha = !mimeType.includes("jpeg") && !mimeType.includes("jpg");
121
- console.log(`[EFImage] Loaded image: mimeType=${mimeType}, hasAlpha=${this.#hasAlpha}`);
122
183
  image.src = URL.createObjectURL(blob);
123
184
  await new Promise((resolve, reject) => {
124
185
  if (signal?.aborted) {
@@ -143,12 +204,36 @@ let EFImage = class EFImage$1 extends EFTemporal(EFSourceMixin(FetchMixin(LitEle
143
204
  });
144
205
  signal?.throwIfAborted();
145
206
  if (!this.canvasRef.value) throw new Error("Canvas not ready");
146
- const ctx = this.canvasRef.value.getContext("2d");
207
+ const ctx = this.canvasRef.value.getContext("2d", { willReadFrequently: true });
147
208
  if (!ctx) throw new Error("Canvas 2d context not ready");
148
- this.canvasRef.value.width = image.width;
149
- this.canvasRef.value.height = image.height;
150
- ctx.drawImage(image, 0, 0);
151
- URL.revokeObjectURL(image.src);
209
+ let canvasWidth = image.width || image.naturalWidth;
210
+ let canvasHeight = image.height || image.naturalHeight;
211
+ if (canvasWidth === 0 || canvasHeight === 0) {
212
+ const computedStyle = getComputedStyle(this);
213
+ const elementWidth = parseFloat(computedStyle.width);
214
+ const elementHeight = parseFloat(computedStyle.height);
215
+ if (elementWidth > 0 && elementHeight > 0) {
216
+ canvasWidth = elementWidth;
217
+ canvasHeight = elementHeight;
218
+ } else {
219
+ canvasWidth = 300;
220
+ canvasHeight = 150;
221
+ }
222
+ }
223
+ this.canvasRef.value.width = canvasWidth;
224
+ this.canvasRef.value.height = canvasHeight;
225
+ try {
226
+ await image.decode();
227
+ } catch (decodeError) {}
228
+ ctx.clearRect(0, 0, canvasWidth, canvasHeight);
229
+ try {
230
+ ctx.drawImage(image, 0, 0, canvasWidth, canvasHeight);
231
+ } catch (drawError) {
232
+ console.error(`[EFImage] drawImage failed:`, drawError);
233
+ throw drawError;
234
+ }
235
+ if (this.#currentObjectUrl && this.#currentObjectUrl !== image.src) URL.revokeObjectURL(this.#currentObjectUrl);
236
+ this.#currentObjectUrl = image.src;
152
237
  }
153
238
  /**
154
239
  * Query readiness state for a given time.
@@ -176,19 +261,19 @@ let EFImage = class EFImage$1 extends EFTemporal(EFSourceMixin(FetchMixin(LitEle
176
261
  renderFrame(_timeMs) {}
177
262
  updated(changedProperties) {
178
263
  super.updated(changedProperties);
179
- this.#syncObjectFitStyles();
180
- if (changedProperties.has("src") || changedProperties.has("assetId")) {
264
+ if (changedProperties.has("src") || changedProperties.has("fileId")) {
181
265
  this.#imageLoaded = false;
266
+ if (changedProperties.get("src") !== void 0 || changedProperties.get("fileId") !== void 0) this.emitContentChange("source");
182
267
  this.loadImage().catch(() => {});
183
268
  this.#renderVersion++;
184
269
  }
185
270
  }
186
- #syncObjectFitStyles() {
187
- const computedStyle = getComputedStyle(this);
188
- const objectFit = computedStyle.objectFit;
189
- const objectPosition = computedStyle.objectPosition;
190
- if (objectFit && objectFit !== "fill") this.style.setProperty("--object-fit", objectFit);
191
- if (objectPosition) this.style.setProperty("--object-position", objectPosition);
271
+ disconnectedCallback() {
272
+ super.disconnectedCallback();
273
+ if (this.#currentObjectUrl) {
274
+ URL.revokeObjectURL(this.#currentObjectUrl);
275
+ this.#currentObjectUrl = null;
276
+ }
192
277
  }
193
278
  /**
194
279
  * Get the natural dimensions of the image.
@@ -212,9 +297,9 @@ let EFImage = class EFImage$1 extends EFTemporal(EFSourceMixin(FetchMixin(LitEle
212
297
  };
213
298
  __decorate([property({
214
299
  type: String,
215
- attribute: "asset-id",
300
+ attribute: "file-id",
216
301
  reflect: true
217
- })], EFImage.prototype, "assetId", null);
302
+ })], EFImage.prototype, "fileId", null);
218
303
  EFImage = __decorate([customElement("ef-image")], EFImage);
219
304
 
220
305
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"EFImage.js","names":["EFImage","#renderVersion","#hasAlpha","#assetId","#imageLoaded","#lastLoadedPath","#imageLoadPromise","#doLoadImage","#syncObjectFitStyles"],"sources":["../../src/elements/EFImage.ts"],"sourcesContent":["import { css, html, LitElement, type PropertyValueMap } from \"lit\";\nimport { customElement, property } from \"lit/decorators.js\";\nimport { createRef, ref } from \"lit/directives/ref.js\";\nimport {\n type FrameRenderable,\n type FrameState,\n createFrameTaskWrapper,\n PRIORITY_IMAGE,\n} from \"../preview/FrameController.js\";\nimport { EFSourceMixin } from \"./EFSourceMixin.js\";\nimport { EFTemporal } from \"./EFTemporal.js\";\nimport { FetchMixin } from \"./FetchMixin.js\";\n\n@customElement(\"ef-image\")\nexport class EFImage extends EFTemporal(\n EFSourceMixin(FetchMixin(LitElement), {\n assetType: \"image_files\",\n }),\n) implements FrameRenderable {\n static styles = [\n css`\n :host {\n display: block;\n display: flex;\n align-items: center;\n justify-content: center;\n }\n canvas, img {\n width: 100%;\n height: 100%;\n object-fit: var(--object-fit, contain);\n object-position: var(--object-position, center);\n }\n `,\n ];\n\n imageRef = createRef<HTMLImageElement>();\n canvasRef = createRef<HTMLCanvasElement>();\n\n /**\n * Render version counter - increments when visual content changes.\n * Used by RenderContext to cache rendered dataURLs.\n */\n #renderVersion = 0;\n\n /**\n * Get the current render version.\n * Version increments when src or assetId changes.\n * @public\n */\n get renderVersion(): number {\n return this.#renderVersion;\n }\n\n /**\n * Whether the loaded image has an alpha channel.\n * JPEG images don't have alpha, PNG/WebP may have alpha.\n */\n #hasAlpha = true; // Default to true (preserve alpha) until we know otherwise\n\n /**\n * Get whether the image has an alpha channel.\n * Used to determine if we should encode as PNG (alpha) or JPEG (no alpha).\n * @public\n */\n get hasAlpha(): boolean {\n return this.#hasAlpha;\n }\n\n #assetId: string | null = null;\n @property({ type: String, attribute: \"asset-id\", reflect: true })\n set assetId(value: string | null) {\n this.#assetId = value;\n }\n\n get assetId() {\n return this.#assetId ?? this.getAttribute(\"asset-id\");\n }\n\n render() {\n const assetPath = this.assetPath();\n const isDirectUrl = this.isDirectUrl(assetPath);\n return isDirectUrl\n ? html`<img ${ref(this.imageRef)} src=${assetPath} />`\n : html`<canvas ${ref(this.canvasRef)}></canvas>`;\n }\n\n private isDirectUrl(src: string): boolean {\n return src.startsWith(\"http://\") || src.startsWith(\"https://\") || src.startsWith(\"data:\");\n }\n\n assetPath() {\n if (this.assetId) {\n return `${this.apiHost}/api/v1/image_files/${this.assetId}`;\n }\n if (this.isDirectUrl(this.src)) {\n return this.src;\n }\n // Normalize the path: remove leading slash and any double slashes\n let normalizedSrc = this.src.startsWith(\"/\")\n ? this.src.slice(1)\n : this.src;\n normalizedSrc = normalizedSrc.replace(/^\\/+/, \"\");\n // Use production API format for local files\n return `/api/v1/assets/local/image?src=${encodeURIComponent(normalizedSrc)}`;\n }\n\n get hasOwnDuration() {\n return this.hasExplicitDuration;\n }\n\n // ============================================================================\n // Image Loading - async method instead of Task\n // ============================================================================\n\n #imageLoaded = false;\n #imageLoadPromise: Promise<void> | null = null;\n #lastLoadedPath: string | null = null;\n\n /**\n * Load image from the configured source\n */\n async loadImage(signal?: AbortSignal): Promise<void> {\n const assetPath = this.assetPath();\n\n // Skip if no source\n if (!this.src && !this.assetId) {\n return;\n }\n\n // Return cached if path hasn't changed\n if (this.#imageLoaded && this.#lastLoadedPath === assetPath) {\n return;\n }\n\n // Return in-flight promise\n if (this.#imageLoadPromise && this.#lastLoadedPath === assetPath) {\n return this.#imageLoadPromise;\n }\n\n // For direct URLs, the img element handles loading\n if (this.isDirectUrl(assetPath)) {\n this.#imageLoaded = true;\n this.#lastLoadedPath = assetPath;\n return;\n }\n\n this.#lastLoadedPath = assetPath;\n this.#imageLoadPromise = this.#doLoadImage(assetPath, signal);\n\n try {\n await this.#imageLoadPromise;\n this.#imageLoaded = true;\n } catch (error) {\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n // Canvas not ready errors are expected during lifecycle\n if (error instanceof Error && error.message === \"Canvas not ready\") {\n return;\n }\n console.error(\"EFImage load error\", error);\n } finally {\n this.#imageLoadPromise = null;\n }\n }\n\n async #doLoadImage(assetPath: string, signal?: AbortSignal): Promise<void> {\n const response = await this.fetch(assetPath, { signal });\n signal?.throwIfAborted();\n \n const image = new Image();\n const blob = await response.blob();\n signal?.throwIfAborted();\n \n // Detect if image has alpha channel based on MIME type\n // JPEG images don't have alpha, PNG/WebP may have alpha\n const mimeType = blob.type.toLowerCase();\n this.#hasAlpha = !mimeType.includes(\"jpeg\") && !mimeType.includes(\"jpg\");\n console.log(`[EFImage] Loaded image: mimeType=${mimeType}, hasAlpha=${this.#hasAlpha}`);\n \n image.src = URL.createObjectURL(blob);\n\n await new Promise<void>((resolve, reject) => {\n if (signal?.aborted) {\n URL.revokeObjectURL(image.src);\n reject(new DOMException(\"Aborted\", \"AbortError\"));\n return;\n }\n \n const abortHandler = () => {\n URL.revokeObjectURL(image.src);\n reject(new DOMException(\"Aborted\", \"AbortError\"));\n };\n signal?.addEventListener(\"abort\", abortHandler, { once: true });\n \n image.onload = () => {\n signal?.removeEventListener(\"abort\", abortHandler);\n resolve();\n };\n image.onerror = (error) => {\n signal?.removeEventListener(\"abort\", abortHandler);\n URL.revokeObjectURL(image.src);\n reject(error);\n };\n });\n\n signal?.throwIfAborted();\n\n if (!this.canvasRef.value) throw new Error(\"Canvas not ready\");\n const ctx = this.canvasRef.value.getContext(\"2d\");\n if (!ctx) throw new Error(\"Canvas 2d context not ready\");\n this.canvasRef.value.width = image.width;\n this.canvasRef.value.height = image.height;\n ctx.drawImage(image, 0, 0);\n \n URL.revokeObjectURL(image.src);\n }\n\n /**\n * @deprecated Use FrameRenderable methods (prepareFrame, renderFrame) via FrameController instead.\n * This is a compatibility wrapper that delegates to the new system.\n */\n frameTask = createFrameTaskWrapper(this);\n\n // ============================================================================\n // FrameRenderable Implementation\n // Centralized frame control - no Lit Tasks\n // ============================================================================\n\n /**\n * Query readiness state for a given time.\n * @implements FrameRenderable\n */\n getFrameState(_timeMs: number): FrameState {\n return {\n needsPreparation: !this.#imageLoaded,\n isReady: this.#imageLoaded,\n priority: PRIORITY_IMAGE,\n };\n }\n\n /**\n * Async preparation - waits for image to load.\n * @implements FrameRenderable\n */\n async prepareFrame(_timeMs: number, signal: AbortSignal): Promise<void> {\n await this.loadImage(signal);\n signal.throwIfAborted();\n }\n\n /**\n * Synchronous render - image is already displayed via img element or canvas.\n * @implements FrameRenderable\n */\n renderFrame(_timeMs: number): void {\n // Image is already displayed - no explicit render action needed\n }\n\n // ============================================================================\n // End FrameRenderable Implementation\n // ============================================================================\n\n protected updated(changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): void {\n super.updated(changedProperties);\n \n // Sync object-fit styles from host element\n this.#syncObjectFitStyles();\n \n // Trigger image load when src or assetId changes\n if (changedProperties.has(\"src\") || changedProperties.has(\"assetId\")) {\n this.#imageLoaded = false;\n this.loadImage().catch(() => {});\n // Increment render version only when actual image content changes\n this.#renderVersion++;\n }\n }\n\n #syncObjectFitStyles() {\n const computedStyle = getComputedStyle(this);\n const objectFit = computedStyle.objectFit;\n const objectPosition = computedStyle.objectPosition;\n \n if (objectFit && objectFit !== 'fill') {\n this.style.setProperty('--object-fit', objectFit);\n }\n if (objectPosition) {\n this.style.setProperty('--object-position', objectPosition);\n }\n }\n\n /**\n * Get the natural dimensions of the image.\n * Returns null if the image hasn't loaded yet.\n *\n * @public\n */\n getNaturalDimensions(): { width: number; height: number } | null {\n // For direct URLs, check img element\n const img = this.imageRef.value;\n if (img && img.naturalWidth > 0 && img.naturalHeight > 0) {\n return {\n width: img.naturalWidth,\n height: img.naturalHeight,\n };\n }\n\n // For canvas-based images, check canvas dimensions\n const canvas = this.canvasRef.value;\n if (canvas && canvas.width > 0 && canvas.height > 0) {\n return {\n width: canvas.width,\n height: canvas.height,\n };\n }\n\n return null;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"ef-image\": EFImage;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"EFImage.js","names":["EFImage","#renderVersion","#hasAlpha","#fileId","#imageLoaded","#lastLoadedPath","#imageLoadPromise","#waitForImageElement","#doLoadImage","#currentObjectUrl"],"sources":["../../src/elements/EFImage.ts"],"sourcesContent":["import { css, html, LitElement, type PropertyValueMap } from \"lit\";\nimport { customElement, property } from \"lit/decorators.js\";\nimport { createRef, ref } from \"lit/directives/ref.js\";\nimport {\n type FrameRenderable,\n type FrameState,\n PRIORITY_IMAGE,\n} from \"../preview/FrameController.js\";\nimport { EFSourceMixin } from \"./EFSourceMixin.js\";\nimport { EFTemporal } from \"./EFTemporal.js\";\nimport { FetchMixin } from \"./FetchMixin.js\";\n\n@customElement(\"ef-image\")\nexport class EFImage\n extends EFTemporal(\n EFSourceMixin(FetchMixin(LitElement), {\n assetType: \"image_files\",\n }),\n )\n implements FrameRenderable\n{\n static styles = [\n css`\n :host {\n display: block;\n position: relative;\n object-fit: contain;\n object-position: center;\n }\n canvas, img {\n width: 100%;\n height: 100%;\n object-fit: inherit;\n object-position: inherit;\n }\n `,\n ];\n\n static get observedAttributes() {\n // biome-ignore lint/complexity/noThisInStatic: We need to access super\n const parentAttributes = super.observedAttributes || [];\n return [...parentAttributes, \"asset-id\"];\n }\n\n attributeChangedCallback(\n name: string,\n oldValue: string | null,\n newValue: string | null,\n ): void {\n if (name === \"asset-id\") {\n this.fileId = newValue;\n return;\n }\n super.attributeChangedCallback(name, oldValue, newValue);\n }\n\n imageRef = createRef<HTMLImageElement>();\n canvasRef = createRef<HTMLCanvasElement>();\n\n /**\n * Render version counter - increments when visual content changes.\n * Used by RenderContext to cache rendered dataURLs.\n */\n #renderVersion = 0;\n\n /**\n * Get the current render version.\n * Version increments when src or fileId changes.\n * @public\n */\n get renderVersion(): number {\n return this.#renderVersion;\n }\n\n /**\n * Whether the loaded image has an alpha channel.\n * JPEG images don't have alpha, PNG/WebP may have alpha.\n */\n #hasAlpha = true; // Default to true (preserve alpha) until we know otherwise\n\n /**\n * Get whether the image has an alpha channel.\n * Used to determine if we should encode as PNG (alpha) or JPEG (no alpha).\n * @public\n */\n get hasAlpha(): boolean {\n return this.#hasAlpha;\n }\n\n #fileId: string | null = null;\n\n @property({ type: String, attribute: \"file-id\", reflect: true })\n set fileId(value: string | null) {\n this.#fileId = value;\n }\n\n get fileId() {\n return (\n this.#fileId ??\n this.getAttribute(\"file-id\") ??\n this.getAttribute(\"asset-id\")\n );\n }\n\n /** @deprecated Use fileId instead */\n get assetId(): string | null {\n return this.fileId;\n }\n set assetId(value: string | null) {\n this.fileId = value;\n }\n\n render() {\n const assetPath = this.assetPath();\n const isDirectUrl = this.isDirectUrl(assetPath);\n return isDirectUrl\n ? html`<img ${ref(this.imageRef)} src=${assetPath} />`\n : html`<canvas ${ref(this.canvasRef)}></canvas>`;\n }\n\n private isDirectUrl(src: string): boolean {\n // For file-id based URLs (via apiHost), always use fetch+canvas instead of img element\n // This ensures proper rendering in all contexts (server, browser-full-video, browser-frame-by-frame)\n if (this.fileId) {\n return false;\n }\n return (\n src.startsWith(\"http://\") ||\n src.startsWith(\"https://\") ||\n src.startsWith(\"data:\")\n );\n }\n\n assetPath() {\n if (this.fileId) {\n const path = `${this.apiHost}/api/v1/files/${this.fileId}`;\n return path;\n }\n if (this.isDirectUrl(this.src)) {\n return this.src;\n }\n // Normalize the path: remove leading slash and any double slashes\n let normalizedSrc = this.src.startsWith(\"/\") ? this.src.slice(1) : this.src;\n normalizedSrc = normalizedSrc.replace(/^\\/+/, \"\");\n // Use production API format for local files\n return `/api/v1/assets/local/image?src=${encodeURIComponent(normalizedSrc)}`;\n }\n\n get hasOwnDuration() {\n return this.hasExplicitDuration;\n }\n\n // ============================================================================\n // Image Loading - async method instead of Task\n // ============================================================================\n\n #imageLoaded = false;\n #imageLoadPromise: Promise<void> | null = null;\n #lastLoadedPath: string | null = null;\n #currentObjectUrl: string | null = null;\n\n override shouldAutoReady(): boolean {\n return !this.src && !this.fileId;\n }\n\n /**\n * Load image from the configured source\n */\n async loadImage(signal?: AbortSignal): Promise<void> {\n const assetPath = this.assetPath();\n\n // Skip if no source\n if (!this.src && !this.fileId) {\n return;\n }\n\n // Return cached if path hasn't changed\n if (this.#imageLoaded && this.#lastLoadedPath === assetPath) {\n this.setContentReadyState(\"ready\");\n return;\n }\n\n // Return in-flight promise\n if (this.#imageLoadPromise && this.#lastLoadedPath === assetPath) {\n return this.#imageLoadPromise;\n }\n\n this.setContentReadyState(\"loading\");\n\n // For direct URLs, wait for the img element to load\n if (this.isDirectUrl(assetPath)) {\n this.#lastLoadedPath = assetPath;\n this.#imageLoadPromise = this.#waitForImageElement(signal);\n\n try {\n await this.#imageLoadPromise;\n this.#imageLoaded = true;\n this.setContentReadyState(\"ready\");\n } catch (error) {\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n console.error(\"EFImage img element load error\", error);\n this.setContentReadyState(\"error\");\n throw error;\n } finally {\n this.#imageLoadPromise = null;\n }\n return;\n }\n\n this.#lastLoadedPath = assetPath;\n this.#imageLoadPromise = this.#doLoadImage(assetPath, signal);\n\n try {\n await this.#imageLoadPromise;\n this.#imageLoaded = true;\n this.setContentReadyState(\"ready\");\n } catch (error) {\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n // Canvas not ready errors are expected during lifecycle\n if (error instanceof Error && error.message === \"Canvas not ready\") {\n return;\n }\n console.error(\"EFImage load error\", error);\n this.setContentReadyState(\"error\");\n } finally {\n this.#imageLoadPromise = null;\n }\n }\n\n async #waitForImageElement(signal?: AbortSignal): Promise<void> {\n if (!this.imageRef.value) {\n throw new Error(\"Image element not ready\");\n }\n\n const img = this.imageRef.value;\n\n // If already loaded (cached), return immediately\n if (img.complete && img.naturalHeight !== 0) {\n return;\n }\n\n return new Promise<void>((resolve, reject) => {\n if (signal?.aborted) {\n reject(new DOMException(\"Aborted\", \"AbortError\"));\n return;\n }\n\n const abortHandler = () => {\n reject(new DOMException(\"Aborted\", \"AbortError\"));\n };\n signal?.addEventListener(\"abort\", abortHandler, { once: true });\n\n img.onload = () => {\n signal?.removeEventListener(\"abort\", abortHandler);\n resolve();\n };\n img.onerror = (error) => {\n signal?.removeEventListener(\"abort\", abortHandler);\n reject(error);\n };\n });\n }\n\n async #doLoadImage(assetPath: string, signal?: AbortSignal): Promise<void> {\n const response = await this.fetch(assetPath, { signal });\n signal?.throwIfAborted();\n\n const image = new Image();\n const blob = await response.blob();\n signal?.throwIfAborted();\n\n // Detect if image has alpha channel based on MIME type\n // JPEG images don't have alpha, PNG/WebP may have alpha\n const mimeType = blob.type.toLowerCase();\n this.#hasAlpha = !mimeType.includes(\"jpeg\") && !mimeType.includes(\"jpg\");\n\n image.src = URL.createObjectURL(blob);\n\n await new Promise<void>((resolve, reject) => {\n if (signal?.aborted) {\n URL.revokeObjectURL(image.src);\n reject(new DOMException(\"Aborted\", \"AbortError\"));\n return;\n }\n\n const abortHandler = () => {\n URL.revokeObjectURL(image.src);\n reject(new DOMException(\"Aborted\", \"AbortError\"));\n };\n signal?.addEventListener(\"abort\", abortHandler, { once: true });\n\n image.onload = () => {\n signal?.removeEventListener(\"abort\", abortHandler);\n resolve();\n };\n image.onerror = (error) => {\n signal?.removeEventListener(\"abort\", abortHandler);\n URL.revokeObjectURL(image.src);\n reject(error);\n };\n });\n\n signal?.throwIfAborted();\n\n if (!this.canvasRef.value) throw new Error(\"Canvas not ready\");\n const ctx = this.canvasRef.value.getContext(\"2d\", {\n willReadFrequently: true,\n });\n if (!ctx) throw new Error(\"Canvas 2d context not ready\");\n\n // Determine canvas dimensions\n // For SVG images without explicit dimensions, image.width/height may be 0\n // In that case, fall back to naturalWidth/naturalHeight or element's computed size\n let canvasWidth = image.width || image.naturalWidth;\n let canvasHeight = image.height || image.naturalHeight;\n\n // If still zero (common with SVGs that only have viewBox), use element's computed size\n if (canvasWidth === 0 || canvasHeight === 0) {\n const computedStyle = getComputedStyle(this);\n const elementWidth = parseFloat(computedStyle.width);\n const elementHeight = parseFloat(computedStyle.height);\n\n // Use element dimensions if available, otherwise use a reasonable default\n if (elementWidth > 0 && elementHeight > 0) {\n canvasWidth = elementWidth;\n canvasHeight = elementHeight;\n } else {\n // Default to 300x150 (standard canvas default size)\n canvasWidth = 300;\n canvasHeight = 150;\n }\n }\n\n this.canvasRef.value.width = canvasWidth;\n this.canvasRef.value.height = canvasHeight;\n\n // Ensure the image is fully decoded before drawing\n // This is especially important for SVGs\n try {\n await image.decode();\n } catch (decodeError) {\n // Image decode failed, attempting to draw anyway\n }\n\n // Clear canvas first to ensure we're starting fresh\n ctx.clearRect(0, 0, canvasWidth, canvasHeight);\n\n try {\n ctx.drawImage(image, 0, 0, canvasWidth, canvasHeight);\n } catch (drawError) {\n console.error(`[EFImage] drawImage failed:`, drawError);\n throw drawError;\n }\n\n // DON'T revoke the URL yet - keep it alive in case we need to redraw\n // URL.revokeObjectURL(image.src);\n\n // Store the object URL for cleanup later\n if (this.#currentObjectUrl && this.#currentObjectUrl !== image.src) {\n URL.revokeObjectURL(this.#currentObjectUrl);\n }\n this.#currentObjectUrl = image.src;\n }\n\n // ============================================================================\n // FrameRenderable Implementation\n // ============================================================================\n\n /**\n * Query readiness state for a given time.\n * @implements FrameRenderable\n */\n getFrameState(_timeMs: number): FrameState {\n return {\n needsPreparation: !this.#imageLoaded,\n isReady: this.#imageLoaded,\n priority: PRIORITY_IMAGE,\n };\n }\n\n /**\n * Async preparation - waits for image to load.\n * @implements FrameRenderable\n */\n async prepareFrame(_timeMs: number, signal: AbortSignal): Promise<void> {\n await this.loadImage(signal);\n signal.throwIfAborted();\n }\n\n /**\n * Synchronous render - image is already displayed via img element or canvas.\n * @implements FrameRenderable\n */\n renderFrame(_timeMs: number): void {\n // Image is already displayed - no explicit render action needed\n }\n\n // ============================================================================\n // End FrameRenderable Implementation\n // ============================================================================\n\n protected updated(\n changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>,\n ): void {\n super.updated(changedProperties);\n\n if (changedProperties.has(\"src\") || changedProperties.has(\"fileId\")) {\n this.#imageLoaded = false;\n if (\n changedProperties.get(\"src\") !== undefined ||\n changedProperties.get(\"fileId\") !== undefined\n ) {\n this.emitContentChange(\"source\");\n }\n this.loadImage().catch(() => {});\n this.#renderVersion++;\n }\n }\n\n disconnectedCallback(): void {\n super.disconnectedCallback();\n // Clean up object URL when element is removed\n if (this.#currentObjectUrl) {\n URL.revokeObjectURL(this.#currentObjectUrl);\n this.#currentObjectUrl = null;\n }\n }\n\n /**\n * Get the natural dimensions of the image.\n * Returns null if the image hasn't loaded yet.\n *\n * @public\n */\n getNaturalDimensions(): { width: number; height: number } | null {\n // For direct URLs, check img element\n const img = this.imageRef.value;\n if (img && img.naturalWidth > 0 && img.naturalHeight > 0) {\n return {\n width: img.naturalWidth,\n height: img.naturalHeight,\n };\n }\n\n // For canvas-based images, check canvas dimensions\n const canvas = this.canvasRef.value;\n if (canvas && canvas.width > 0 && canvas.height > 0) {\n return {\n width: canvas.width,\n height: canvas.height,\n };\n }\n\n return null;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"ef-image\": EFImage;\n }\n}\n"],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  import { withSpan } from "../../otel/tracingHelpers.js";
2
+ import { ThumbnailExtractor } from "./shared/ThumbnailExtractor.js";
2
3
  import { BaseMediaEngine, mediaCache } from "./BaseMediaEngine.js";
3
4
  import { convertToScaledTime, roundToMilliseconds } from "./shared/PrecisionUtils.js";
4
- import { ThumbnailExtractor } from "./shared/ThumbnailExtractor.js";
5
5
 
6
6
  //#region src/elements/EFMedia/AssetMediaEngine.ts
7
7
  var AssetMediaEngine = class AssetMediaEngine extends BaseMediaEngine {
@@ -18,7 +18,7 @@ var AssetMediaEngine = class AssetMediaEngine extends BaseMediaEngine {
18
18
  let normalizedSrc = src.startsWith("/") ? src.slice(1) : src;
19
19
  normalizedSrc = normalizedSrc.replace(/^\/+/, "");
20
20
  const apiBaseUrl = urlGenerator.getBaseUrl();
21
- const url = apiBaseUrl ? `${apiBaseUrl}/api/v1/isobmff_files/local/index?src=${encodeURIComponent(normalizedSrc)}` : `/api/v1/isobmff_files/local/index?src=${encodeURIComponent(normalizedSrc)}`;
21
+ const url = apiBaseUrl ? `${apiBaseUrl}/api/v1/files/local/index?src=${encodeURIComponent(normalizedSrc)}` : `/api/v1/files/local/index?src=${encodeURIComponent(normalizedSrc)}`;
22
22
  engine.data = await engine.fetchManifest(url, signal);
23
23
  signal?.throwIfAborted();
24
24
  engine.durationMs = Object.values(engine.data).reduce((max, fragment) => Math.max(max, fragment.duration / fragment.timescale), 0) * 1e3;