@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
@@ -0,0 +1,76 @@
1
+ //#region src/preview/QualityUpgradeScheduler.d.ts
2
+ /**
3
+ * QualityUpgradeScheduler: Centralized deadline-ordered work queue
4
+ *
5
+ * Coordinates main-quality segment fetching across multiple video elements.
6
+ * Generic scheduler that doesn't understand media concepts (segments, renditions, etc.)
7
+ * - only processes { key, deadlineMs, fetch, owner } tuples.
8
+ *
9
+ * Design principles:
10
+ * - Deadline-based ordering: always process nearest deadline first
11
+ * - Ground-truth cache validation: check cache before starting any fetch
12
+ * - In-flight fetches never cancelled: they populate shared cache
13
+ * - Event-driven: elements submit tasks only on state changes, not every frame
14
+ */
15
+ interface UpgradeTask {
16
+ /** Opaque dedup key (e.g. "${owner}:${segmentId}:${renditionId}") */
17
+ key: string;
18
+ /** Fetch function that populates the cache */
19
+ fetch: (signal: AbortSignal) => Promise<void>;
20
+ /** Timeline time when this segment will be needed */
21
+ deadlineMs: number;
22
+ /** Element ID, for bulk operations */
23
+ owner: string;
24
+ }
25
+ interface UpgradeTaskStatus {
26
+ key: string;
27
+ owner: string;
28
+ deadlineMs: number;
29
+ status: "queued" | "active" | "completed" | "failed";
30
+ error?: string;
31
+ }
32
+ interface OwnerProgress {
33
+ queued: number;
34
+ active: number;
35
+ completed: number;
36
+ failed: number;
37
+ }
38
+ declare class QualityUpgradeScheduler {
39
+ #private;
40
+ constructor(options: {
41
+ requestFrameRender: () => void;
42
+ maxConcurrent?: number;
43
+ isCached?: (key: string) => boolean;
44
+ });
45
+ /**
46
+ * Add tasks without affecting existing ones (additive).
47
+ * Used for lookahead extension during playback.
48
+ */
49
+ enqueue(tasks: UpgradeTask[]): void;
50
+ /**
51
+ * Replace all queued tasks for an owner.
52
+ * Used on seeks, trim changes, timeline position changes where old deadlines are stale.
53
+ * Does NOT cancel in-flight tasks (they populate shared cache).
54
+ */
55
+ replaceForOwner(owner: string, tasks: UpgradeTask[]): void;
56
+ /**
57
+ * Cancel all tasks for an owner.
58
+ * Removes queued tasks. Does NOT abort in-flight fetches.
59
+ */
60
+ cancelForOwner(owner: string): void;
61
+ /**
62
+ * Get snapshot of current queue state for debugging.
63
+ */
64
+ getQueueSnapshot(): UpgradeTaskStatus[];
65
+ /**
66
+ * Get progress for a specific owner.
67
+ */
68
+ getOwnerProgress(owner: string): OwnerProgress;
69
+ /**
70
+ * Dispose the scheduler - abort all in-flight work.
71
+ */
72
+ dispose(): void;
73
+ }
74
+ //#endregion
75
+ export { QualityUpgradeScheduler };
76
+ //# sourceMappingURL=QualityUpgradeScheduler.d.ts.map
@@ -0,0 +1,158 @@
1
+ //#region src/preview/QualityUpgradeScheduler.ts
2
+ var QualityUpgradeScheduler = class {
3
+ #maxConcurrent;
4
+ #queue = [];
5
+ #activeTasks = /* @__PURE__ */ new Map();
6
+ #completedTasks = /* @__PURE__ */ new Map();
7
+ #abortController;
8
+ #requestFrameRender;
9
+ #isCached;
10
+ constructor(options) {
11
+ this.#requestFrameRender = options.requestFrameRender;
12
+ this.#maxConcurrent = options.maxConcurrent ?? 4;
13
+ this.#isCached = options.isCached;
14
+ this.#abortController = new AbortController();
15
+ }
16
+ /**
17
+ * Add tasks without affecting existing ones (additive).
18
+ * Used for lookahead extension during playback.
19
+ */
20
+ enqueue(tasks) {
21
+ if (this.#abortController.signal.aborted) return;
22
+ for (const task of tasks) {
23
+ if (this.#queue.some((t) => t.key === task.key) || this.#activeTasks.has(task.key) || this.#completedTasks.has(task.key)) continue;
24
+ this.#queue.push(task);
25
+ }
26
+ this.#queue.sort((a, b) => a.deadlineMs - b.deadlineMs);
27
+ this.#processQueue();
28
+ }
29
+ /**
30
+ * Replace all queued tasks for an owner.
31
+ * Used on seeks, trim changes, timeline position changes where old deadlines are stale.
32
+ * Does NOT cancel in-flight tasks (they populate shared cache).
33
+ */
34
+ replaceForOwner(owner, tasks) {
35
+ if (this.#abortController.signal.aborted) return;
36
+ this.#queue = this.#queue.filter((t) => t.owner !== owner);
37
+ for (const task of tasks) {
38
+ if (this.#activeTasks.has(task.key) || this.#completedTasks.has(task.key)) continue;
39
+ this.#queue.push(task);
40
+ }
41
+ this.#queue.sort((a, b) => a.deadlineMs - b.deadlineMs);
42
+ this.#processQueue();
43
+ }
44
+ /**
45
+ * Cancel all tasks for an owner.
46
+ * Removes queued tasks. Does NOT abort in-flight fetches.
47
+ */
48
+ cancelForOwner(owner) {
49
+ this.#queue = this.#queue.filter((t) => t.owner !== owner);
50
+ for (const [key, task] of this.#completedTasks.entries()) if (task.owner === owner) this.#completedTasks.delete(key);
51
+ }
52
+ /**
53
+ * Process the queue - start tasks up to maxConcurrent limit.
54
+ */
55
+ #processQueue() {
56
+ if (this.#abortController.signal.aborted) return;
57
+ while (this.#activeTasks.size < this.#maxConcurrent && this.#queue.length > 0) {
58
+ const task = this.#queue.shift();
59
+ if (!task) break;
60
+ if (this.#isCached?.(task.key)) {
61
+ this.#completedTasks.set(task.key, {
62
+ key: task.key,
63
+ owner: task.owner,
64
+ status: "completed"
65
+ });
66
+ continue;
67
+ }
68
+ this.#startTask(task);
69
+ }
70
+ }
71
+ /**
72
+ * Start a single task.
73
+ */
74
+ #startTask(task) {
75
+ const promise = task.fetch(this.#abortController.signal).then(() => {
76
+ this.#activeTasks.delete(task.key);
77
+ this.#completedTasks.set(task.key, {
78
+ key: task.key,
79
+ owner: task.owner,
80
+ status: "completed"
81
+ });
82
+ this.#requestFrameRender();
83
+ this.#processQueue();
84
+ }).catch((error) => {
85
+ this.#activeTasks.delete(task.key);
86
+ if (!(error instanceof DOMException && error.name === "AbortError")) this.#completedTasks.set(task.key, {
87
+ key: task.key,
88
+ owner: task.owner,
89
+ status: "failed",
90
+ error: error instanceof Error ? error.message : String(error)
91
+ });
92
+ this.#processQueue();
93
+ });
94
+ this.#activeTasks.set(task.key, {
95
+ task,
96
+ startedAt: performance.now(),
97
+ promise
98
+ });
99
+ }
100
+ /**
101
+ * Get snapshot of current queue state for debugging.
102
+ */
103
+ getQueueSnapshot() {
104
+ const results = [];
105
+ for (const task of this.#queue) results.push({
106
+ key: task.key,
107
+ owner: task.owner,
108
+ deadlineMs: task.deadlineMs,
109
+ status: "queued"
110
+ });
111
+ for (const [key, activeTask] of this.#activeTasks.entries()) results.push({
112
+ key,
113
+ owner: activeTask.task.owner,
114
+ deadlineMs: activeTask.task.deadlineMs,
115
+ status: "active"
116
+ });
117
+ for (const [key, completed] of this.#completedTasks.entries()) results.push({
118
+ key,
119
+ owner: completed.owner,
120
+ deadlineMs: 0,
121
+ status: completed.status,
122
+ error: completed.error
123
+ });
124
+ return results;
125
+ }
126
+ /**
127
+ * Get progress for a specific owner.
128
+ */
129
+ getOwnerProgress(owner) {
130
+ const queued = this.#queue.filter((t) => t.owner === owner).length;
131
+ let active = 0;
132
+ for (const activeTask of this.#activeTasks.values()) if (activeTask.task.owner === owner) active++;
133
+ let completed = 0;
134
+ let failed = 0;
135
+ for (const task of this.#completedTasks.values()) if (task.owner === owner) if (task.status === "completed") completed++;
136
+ else failed++;
137
+ return {
138
+ queued,
139
+ active,
140
+ completed,
141
+ failed
142
+ };
143
+ }
144
+ /**
145
+ * Dispose the scheduler - abort all in-flight work.
146
+ */
147
+ dispose() {
148
+ for (const activeTask of this.#activeTasks.values()) activeTask.promise.catch(() => {});
149
+ this.#abortController.abort();
150
+ this.#queue = [];
151
+ this.#activeTasks.clear();
152
+ this.#completedTasks.clear();
153
+ }
154
+ };
155
+
156
+ //#endregion
157
+ export { QualityUpgradeScheduler };
158
+ //# sourceMappingURL=QualityUpgradeScheduler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"QualityUpgradeScheduler.js","names":["#requestFrameRender","#maxConcurrent","#isCached","#abortController","#queue","#activeTasks","#completedTasks","#processQueue","#startTask","results: UpgradeTaskStatus[]"],"sources":["../../src/preview/QualityUpgradeScheduler.ts"],"sourcesContent":["/**\n * QualityUpgradeScheduler: Centralized deadline-ordered work queue\n *\n * Coordinates main-quality segment fetching across multiple video elements.\n * Generic scheduler that doesn't understand media concepts (segments, renditions, etc.)\n * - only processes { key, deadlineMs, fetch, owner } tuples.\n *\n * Design principles:\n * - Deadline-based ordering: always process nearest deadline first\n * - Ground-truth cache validation: check cache before starting any fetch\n * - In-flight fetches never cancelled: they populate shared cache\n * - Event-driven: elements submit tasks only on state changes, not every frame\n */\n\nexport interface UpgradeTask {\n /** Opaque dedup key (e.g. \"${owner}:${segmentId}:${renditionId}\") */\n key: string;\n /** Fetch function that populates the cache */\n fetch: (signal: AbortSignal) => Promise<void>;\n /** Timeline time when this segment will be needed */\n deadlineMs: number;\n /** Element ID, for bulk operations */\n owner: string;\n}\n\nexport interface UpgradeTaskStatus {\n key: string;\n owner: string;\n deadlineMs: number;\n status: \"queued\" | \"active\" | \"completed\" | \"failed\";\n error?: string;\n}\n\nexport interface OwnerProgress {\n queued: number;\n active: number;\n completed: number;\n failed: number;\n}\n\ninterface ActiveTask {\n task: UpgradeTask;\n startedAt: number;\n promise: Promise<void>;\n}\n\ninterface CompletedTask {\n key: string;\n owner: string;\n status: \"completed\" | \"failed\";\n error?: string;\n}\n\nexport class QualityUpgradeScheduler {\n #maxConcurrent: number;\n #queue: UpgradeTask[] = [];\n #activeTasks = new Map<string, ActiveTask>();\n #completedTasks = new Map<string, CompletedTask>();\n #abortController: AbortController;\n #requestFrameRender: () => void;\n #isCached?: (key: string) => boolean;\n\n constructor(options: {\n requestFrameRender: () => void;\n maxConcurrent?: number;\n isCached?: (key: string) => boolean;\n }) {\n this.#requestFrameRender = options.requestFrameRender;\n this.#maxConcurrent = options.maxConcurrent ?? 4;\n this.#isCached = options.isCached;\n this.#abortController = new AbortController();\n }\n\n /**\n * Add tasks without affecting existing ones (additive).\n * Used for lookahead extension during playback.\n */\n enqueue(tasks: UpgradeTask[]): void {\n if (this.#abortController.signal.aborted) return;\n\n for (const task of tasks) {\n // Skip if already queued, active, or completed\n if (\n this.#queue.some((t) => t.key === task.key) ||\n this.#activeTasks.has(task.key) ||\n this.#completedTasks.has(task.key)\n ) {\n continue;\n }\n\n this.#queue.push(task);\n }\n\n // Sort queue by deadline (ascending)\n this.#queue.sort((a, b) => a.deadlineMs - b.deadlineMs);\n\n // Start processing if we have capacity\n this.#processQueue();\n }\n\n /**\n * Replace all queued tasks for an owner.\n * Used on seeks, trim changes, timeline position changes where old deadlines are stale.\n * Does NOT cancel in-flight tasks (they populate shared cache).\n */\n replaceForOwner(owner: string, tasks: UpgradeTask[]): void {\n if (this.#abortController.signal.aborted) return;\n\n // Remove queued (not active) tasks for this owner\n this.#queue = this.#queue.filter((t) => t.owner !== owner);\n\n // Add new tasks\n for (const task of tasks) {\n // Skip if already active or completed\n if (\n this.#activeTasks.has(task.key) ||\n this.#completedTasks.has(task.key)\n ) {\n continue;\n }\n\n this.#queue.push(task);\n }\n\n // Sort queue by deadline (ascending)\n this.#queue.sort((a, b) => a.deadlineMs - b.deadlineMs);\n\n // Start processing if we have capacity\n this.#processQueue();\n }\n\n /**\n * Cancel all tasks for an owner.\n * Removes queued tasks. Does NOT abort in-flight fetches.\n */\n cancelForOwner(owner: string): void {\n // Remove from queue\n this.#queue = this.#queue.filter((t) => t.owner !== owner);\n\n // Remove from completed tracking (allows resubmission)\n for (const [key, task] of this.#completedTasks.entries()) {\n if (task.owner === owner) {\n this.#completedTasks.delete(key);\n }\n }\n\n // Note: we do NOT cancel active tasks - they populate the shared cache\n }\n\n /**\n * Process the queue - start tasks up to maxConcurrent limit.\n */\n #processQueue(): void {\n if (this.#abortController.signal.aborted) return;\n\n while (\n this.#activeTasks.size < this.#maxConcurrent &&\n this.#queue.length > 0\n ) {\n const task = this.#queue.shift();\n if (!task) break;\n\n // Ground-truth cache check before starting\n if (this.#isCached?.(task.key)) {\n // Already cached from another path, mark as completed and continue\n this.#completedTasks.set(task.key, {\n key: task.key,\n owner: task.owner,\n status: \"completed\",\n });\n continue;\n }\n\n // Start the task\n this.#startTask(task);\n }\n }\n\n /**\n * Start a single task.\n */\n #startTask(task: UpgradeTask): void {\n const promise = task\n .fetch(this.#abortController.signal)\n .then(() => {\n // Success\n this.#activeTasks.delete(task.key);\n this.#completedTasks.set(task.key, {\n key: task.key,\n owner: task.owner,\n status: \"completed\",\n });\n\n // Trigger re-render so upgraded quality gets displayed\n this.#requestFrameRender();\n\n // Start next task if available\n this.#processQueue();\n })\n .catch((error) => {\n // Failure\n this.#activeTasks.delete(task.key);\n\n // Don't track AbortError as failure (intentional cancellation)\n const isAbortError =\n error instanceof DOMException && error.name === \"AbortError\";\n\n if (!isAbortError) {\n this.#completedTasks.set(task.key, {\n key: task.key,\n owner: task.owner,\n status: \"failed\",\n error: error instanceof Error ? error.message : String(error),\n });\n }\n\n // Continue processing queue even after failure\n this.#processQueue();\n });\n\n this.#activeTasks.set(task.key, {\n task,\n startedAt: performance.now(),\n promise,\n });\n }\n\n /**\n * Get snapshot of current queue state for debugging.\n */\n getQueueSnapshot(): UpgradeTaskStatus[] {\n const results: UpgradeTaskStatus[] = [];\n\n // Queued tasks\n for (const task of this.#queue) {\n results.push({\n key: task.key,\n owner: task.owner,\n deadlineMs: task.deadlineMs,\n status: \"queued\",\n });\n }\n\n // Active tasks\n for (const [key, activeTask] of this.#activeTasks.entries()) {\n results.push({\n key,\n owner: activeTask.task.owner,\n deadlineMs: activeTask.task.deadlineMs,\n status: \"active\",\n });\n }\n\n // Completed tasks\n for (const [key, completed] of this.#completedTasks.entries()) {\n results.push({\n key,\n owner: completed.owner,\n deadlineMs: 0, // No longer relevant\n status: completed.status as \"completed\" | \"failed\",\n error: completed.error,\n });\n }\n\n return results;\n }\n\n /**\n * Get progress for a specific owner.\n */\n getOwnerProgress(owner: string): OwnerProgress {\n const queued = this.#queue.filter((t) => t.owner === owner).length;\n\n let active = 0;\n for (const activeTask of this.#activeTasks.values()) {\n if (activeTask.task.owner === owner) {\n active++;\n }\n }\n\n let completed = 0;\n let failed = 0;\n for (const task of this.#completedTasks.values()) {\n if (task.owner === owner) {\n if (task.status === \"completed\") {\n completed++;\n } else {\n failed++;\n }\n }\n }\n\n return { queued, active, completed, failed };\n }\n\n /**\n * Dispose the scheduler - abort all in-flight work.\n */\n dispose(): void {\n // Suppress in-flight task rejections before aborting to avoid unhandled\n // rejection events from the synchronous abort signal firing.\n for (const activeTask of this.#activeTasks.values()) {\n activeTask.promise.catch(() => {});\n }\n this.#abortController.abort();\n this.#queue = [];\n this.#activeTasks.clear();\n this.#completedTasks.clear();\n }\n}\n"],"mappings":"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"}
@@ -1 +1,119 @@
1
- import "../elements/EFVideo.js";
1
+ import { EFVideo } from "../elements/EFVideo.js";
2
+
3
+ //#region src/preview/RenderContext.d.ts
4
+
5
+ /**
6
+ * Result of capturing a video frame.
7
+ */
8
+ interface CapturedFrame {
9
+ dataUrl: string;
10
+ width: number;
11
+ height: number;
12
+ }
13
+ /**
14
+ * Options for creating a RenderContext.
15
+ */
16
+ interface RenderContextOptions {
17
+ /** Maximum number of canvas dataURLs to cache (default: 50) */
18
+ maxCanvasCacheSize?: number;
19
+ /** Maximum number of video frame dataURLs to cache (default: 100) */
20
+ maxVideoFrameCacheSize?: number;
21
+ }
22
+ /**
23
+ * RenderContext provides scoped caching for render operations.
24
+ *
25
+ * Create at the start of a render, dispose when complete:
26
+ * ```typescript
27
+ * const context = new RenderContext();
28
+ * try {
29
+ * // ... render operations
30
+ * } finally {
31
+ * context.dispose();
32
+ * }
33
+ * ```
34
+ */
35
+ declare class RenderContext {
36
+ #private;
37
+ constructor(options?: RenderContextOptions);
38
+ /**
39
+ * Check if the context has been disposed.
40
+ */
41
+ get disposed(): boolean;
42
+ /**
43
+ * Get cache metrics for monitoring.
44
+ */
45
+ get metrics(): {
46
+ canvasCacheHits: number;
47
+ canvasCacheMisses: number;
48
+ videoFrameCacheHits: number;
49
+ videoFrameCacheMisses: number;
50
+ };
51
+ /**
52
+ * Get a cached dataURL for a static element.
53
+ * Returns undefined if not cached or element doesn't support caching.
54
+ */
55
+ getCachedCanvasDataUrl(element: Element): string | undefined;
56
+ /**
57
+ * Cache a dataURL for a static element.
58
+ * Does nothing if the element doesn't support caching.
59
+ */
60
+ setCachedCanvasDataUrl(element: Element, dataUrl: string): void;
61
+ /**
62
+ * Get a cached video frame.
63
+ * Returns undefined if not cached.
64
+ */
65
+ getCachedVideoFrame(videoElement: Element, sourceTimeMs: number): CapturedFrame | undefined;
66
+ /**
67
+ * Cache a video frame.
68
+ */
69
+ setCachedVideoFrame(videoElement: Element, sourceTimeMs: number, frame: CapturedFrame): void;
70
+ /**
71
+ * Convenience method to get or capture a video frame.
72
+ * Checks cache first, then captures if not cached.
73
+ *
74
+ * @param video - The ef-video element
75
+ * @param sourceTimeMs - Source media timestamp
76
+ * @param options - Capture options including quality and signal
77
+ * @returns The captured frame data
78
+ */
79
+ getOrCaptureVideoFrame(video: EFVideo, sourceTimeMs: number, options?: {
80
+ quality?: "auto" | "scrub" | "main";
81
+ signal?: AbortSignal;
82
+ }): Promise<CapturedFrame>;
83
+ /**
84
+ * Get cached document styles.
85
+ * Returns undefined if not cached.
86
+ */
87
+ getCachedDocumentStyles(): string | undefined;
88
+ /**
89
+ * Cache document styles.
90
+ */
91
+ setCachedDocumentStyles(styles: string): void;
92
+ /**
93
+ * Dispose the context and clear all caches.
94
+ * Should be called when rendering is complete.
95
+ */
96
+ dispose(): void;
97
+ /**
98
+ * Symbol.dispose implementation for use with the `using` keyword.
99
+ *
100
+ * @example
101
+ * ```typescript
102
+ * using context = new RenderContext();
103
+ * // ... render operations
104
+ * // context is automatically disposed when scope exits
105
+ * ```
106
+ */
107
+ [Symbol.dispose](): void;
108
+ /**
109
+ * Get the current size of the canvas cache.
110
+ */
111
+ get canvasCacheSize(): number;
112
+ /**
113
+ * Get the current size of the video frame cache.
114
+ */
115
+ get videoFrameCacheSize(): number;
116
+ }
117
+ //#endregion
118
+ export { RenderContext };
119
+ //# sourceMappingURL=RenderContext.d.ts.map
@@ -33,7 +33,7 @@ function getElementUniqueId(element) {
33
33
  }
34
34
  /**
35
35
  * RenderContext provides scoped caching for render operations.
36
- *
36
+ *
37
37
  * Create at the start of a render, dispose when complete:
38
38
  * ```typescript
39
39
  * const context = new RenderContext();
@@ -49,6 +49,8 @@ var RenderContext = class {
49
49
  #canvasCache;
50
50
  /** Cache for video frames by source timestamp */
51
51
  #videoFrameCache;
52
+ /** Cache for document styles (computed once per render session) */
53
+ #documentStylesCache = null;
52
54
  /** Whether this context has been disposed */
53
55
  #disposed = false;
54
56
  /** Metrics for monitoring cache effectiveness */
@@ -138,7 +140,7 @@ var RenderContext = class {
138
140
  /**
139
141
  * Convenience method to get or capture a video frame.
140
142
  * Checks cache first, then captures if not cached.
141
- *
143
+ *
142
144
  * @param video - The ef-video element
143
145
  * @param sourceTimeMs - Source media timestamp
144
146
  * @param options - Capture options including quality and signal
@@ -152,6 +154,21 @@ var RenderContext = class {
152
154
  return frame;
153
155
  }
154
156
  /**
157
+ * Get cached document styles.
158
+ * Returns undefined if not cached.
159
+ */
160
+ getCachedDocumentStyles() {
161
+ if (this.#disposed) return void 0;
162
+ return this.#documentStylesCache ?? void 0;
163
+ }
164
+ /**
165
+ * Cache document styles.
166
+ */
167
+ setCachedDocumentStyles(styles) {
168
+ if (this.#disposed) return;
169
+ this.#documentStylesCache = styles;
170
+ }
171
+ /**
155
172
  * Dispose the context and clear all caches.
156
173
  * Should be called when rendering is complete.
157
174
  */
@@ -159,11 +176,12 @@ var RenderContext = class {
159
176
  if (this.#disposed) return;
160
177
  this.#canvasCache.clear();
161
178
  this.#videoFrameCache.clear();
179
+ this.#documentStylesCache = null;
162
180
  this.#disposed = true;
163
181
  }
164
182
  /**
165
183
  * Symbol.dispose implementation for use with the `using` keyword.
166
- *
184
+ *
167
185
  * @example
168
186
  * ```typescript
169
187
  * using context = new RenderContext();
@@ -1 +1 @@
1
- {"version":3,"file":"RenderContext.js","names":["#canvasCache","#videoFrameCache","#disposed","#metrics","#getCanvasCacheKey","#getVideoFrameCacheKey"],"sources":["../../src/preview/RenderContext.ts"],"sourcesContent":["/**\n * RenderContext manages scoped caches for the rendering pipeline.\n * \n * Used during foreignObject serialization to cache:\n * - Video frames by source timestamp (useful for freeze frames, slow-mo)\n * - Static element canvases by element identity + renderVersion\n * \n * The context should be created at the start of a render operation\n * and disposed when the render completes (success or failure).\n */\n\nimport { LRUCache } from \"../utils/LRUCache.js\";\nimport type { EFVideo } from \"../elements/EFVideo.js\";\n\n/**\n * Check if an element has a renderVersion property.\n */\nfunction hasRenderVersion(element: Element): element is Element & { renderVersion: number } {\n return \"renderVersion\" in element && typeof (element as any).renderVersion === \"number\";\n}\n\n/**\n * Module-level counter for generating unique element IDs.\n * This ensures uniqueness across all RenderContext instances.\n */\nlet nextElementId = 1;\n\n/**\n * WeakMap to store unique IDs for elements.\n * Using WeakMap ensures we don't prevent garbage collection of elements.\n * The ID is stable for the lifetime of the element.\n */\nconst elementUniqueIds = new WeakMap<Element, number>();\n\n/**\n * Get or create a unique ID for an element.\n * This guarantees uniqueness even for elements with the same id attribute\n * or no id attribute at all.\n */\nfunction getElementUniqueId(element: Element): number {\n let id = elementUniqueIds.get(element);\n if (id === undefined) {\n id = nextElementId++;\n elementUniqueIds.set(element, id);\n }\n return id;\n}\n\n/**\n * Result of capturing a video frame.\n */\nexport interface CapturedFrame {\n dataUrl: string;\n width: number;\n height: number;\n}\n\n/**\n * Options for creating a RenderContext.\n */\nexport interface RenderContextOptions {\n /** Maximum number of canvas dataURLs to cache (default: 50) */\n maxCanvasCacheSize?: number;\n /** Maximum number of video frame dataURLs to cache (default: 100) */\n maxVideoFrameCacheSize?: number;\n}\n\n/**\n * RenderContext provides scoped caching for render operations.\n * \n * Create at the start of a render, dispose when complete:\n * ```typescript\n * const context = new RenderContext();\n * try {\n * // ... render operations\n * } finally {\n * context.dispose();\n * }\n * ```\n */\nexport class RenderContext {\n /** Cache for static element canvases (ef-image, ef-waveform) */\n #canvasCache: LRUCache<string, string>;\n \n /** Cache for video frames by source timestamp */\n #videoFrameCache: LRUCache<string, CapturedFrame>;\n \n /** Whether this context has been disposed */\n #disposed = false;\n \n /** Metrics for monitoring cache effectiveness */\n #metrics = {\n canvasCacheHits: 0,\n canvasCacheMisses: 0,\n videoFrameCacheHits: 0,\n videoFrameCacheMisses: 0,\n };\n\n constructor(options: RenderContextOptions = {}) {\n const { maxCanvasCacheSize = 50, maxVideoFrameCacheSize = 100 } = options;\n this.#canvasCache = new LRUCache(maxCanvasCacheSize);\n this.#videoFrameCache = new LRUCache(maxVideoFrameCacheSize);\n }\n\n /**\n * Check if the context has been disposed.\n */\n get disposed(): boolean {\n return this.#disposed;\n }\n\n /**\n * Get cache metrics for monitoring.\n */\n get metrics() {\n return { ...this.#metrics };\n }\n\n // ============================================================================\n // Static Element Cache (ef-image, ef-waveform)\n // ============================================================================\n\n /**\n * Generate a cache key for a static element.\n * Uses a unique element ID (via WeakMap) to ensure uniqueness even if\n * multiple elements have the same id attribute.\n * Returns null if the element doesn't support caching (no renderVersion).\n */\n #getCanvasCacheKey(element: Element): string | null {\n if (!hasRenderVersion(element)) {\n return null;\n }\n // Use unique element ID + render version for guaranteed uniqueness\n const uniqueId = getElementUniqueId(element);\n return `canvas:${uniqueId}:${element.renderVersion}`;\n }\n\n /**\n * Get a cached dataURL for a static element.\n * Returns undefined if not cached or element doesn't support caching.\n */\n getCachedCanvasDataUrl(element: Element): string | undefined {\n if (this.#disposed) return undefined;\n \n const key = this.#getCanvasCacheKey(element);\n if (!key) return undefined;\n \n const cached = this.#canvasCache.get(key);\n if (cached) {\n this.#metrics.canvasCacheHits++;\n } else {\n this.#metrics.canvasCacheMisses++;\n }\n return cached;\n }\n\n /**\n * Cache a dataURL for a static element.\n * Does nothing if the element doesn't support caching.\n */\n setCachedCanvasDataUrl(element: Element, dataUrl: string): void {\n if (this.#disposed) return;\n \n const key = this.#getCanvasCacheKey(element);\n if (key) {\n this.#canvasCache.set(key, dataUrl);\n }\n }\n\n // ============================================================================\n // Video Frame Cache\n // ============================================================================\n\n /**\n * Generate a cache key for a video frame.\n * Uses a unique element ID (via WeakMap) to ensure uniqueness even if\n * multiple videos have the same id attribute.\n */\n #getVideoFrameCacheKey(videoElement: Element, sourceTimeMs: number): string {\n const uniqueId = getElementUniqueId(videoElement);\n // Round to nearest ms to avoid floating point issues\n const roundedTime = Math.round(sourceTimeMs);\n return `video:${uniqueId}:${roundedTime}`;\n }\n\n /**\n * Get a cached video frame.\n * Returns undefined if not cached.\n */\n getCachedVideoFrame(videoElement: Element, sourceTimeMs: number): CapturedFrame | undefined {\n if (this.#disposed) return undefined;\n \n const key = this.#getVideoFrameCacheKey(videoElement, sourceTimeMs);\n const cached = this.#videoFrameCache.get(key);\n if (cached) {\n this.#metrics.videoFrameCacheHits++;\n } else {\n this.#metrics.videoFrameCacheMisses++;\n }\n return cached;\n }\n\n /**\n * Cache a video frame.\n */\n setCachedVideoFrame(videoElement: Element, sourceTimeMs: number, frame: CapturedFrame): void {\n if (this.#disposed) return;\n \n const key = this.#getVideoFrameCacheKey(videoElement, sourceTimeMs);\n this.#videoFrameCache.set(key, frame);\n }\n\n /**\n * Convenience method to get or capture a video frame.\n * Checks cache first, then captures if not cached.\n * \n * @param video - The ef-video element\n * @param sourceTimeMs - Source media timestamp\n * @param options - Capture options including quality and signal\n * @returns The captured frame data\n */\n async getOrCaptureVideoFrame(\n video: EFVideo,\n sourceTimeMs: number,\n options: {\n quality?: \"auto\" | \"scrub\" | \"main\";\n signal?: AbortSignal;\n } = {}\n ): Promise<CapturedFrame> {\n // Check cache first\n const cached = this.getCachedVideoFrame(video, sourceTimeMs);\n if (cached) {\n return cached;\n }\n\n // Capture frame using direct API\n const frame = await video.captureFrameAtSourceTime(sourceTimeMs, options);\n \n // Cache for future use\n this.setCachedVideoFrame(video, sourceTimeMs, frame);\n \n return frame;\n }\n\n // ============================================================================\n // Cleanup\n // ============================================================================\n\n /**\n * Dispose the context and clear all caches.\n * Should be called when rendering is complete.\n */\n dispose(): void {\n if (this.#disposed) return;\n \n this.#canvasCache.clear();\n this.#videoFrameCache.clear();\n this.#disposed = true;\n }\n\n /**\n * Symbol.dispose implementation for use with the `using` keyword.\n * \n * @example\n * ```typescript\n * using context = new RenderContext();\n * // ... render operations\n * // context is automatically disposed when scope exits\n * ```\n */\n [Symbol.dispose](): void {\n this.dispose();\n }\n\n /**\n * Get the current size of the canvas cache.\n */\n get canvasCacheSize(): number {\n return this.#canvasCache.size;\n }\n\n /**\n * Get the current size of the video frame cache.\n */\n get videoFrameCacheSize(): number {\n return this.#videoFrameCache.size;\n }\n}\n"],"mappings":";;;;;;AAiBA,SAAS,iBAAiB,SAAkE;AAC1F,QAAO,mBAAmB,WAAW,OAAQ,QAAgB,kBAAkB;;;;;;AAOjF,IAAI,gBAAgB;;;;;;AAOpB,MAAM,mCAAmB,IAAI,SAA0B;;;;;;AAOvD,SAAS,mBAAmB,SAA0B;CACpD,IAAI,KAAK,iBAAiB,IAAI,QAAQ;AACtC,KAAI,OAAO,QAAW;AACpB,OAAK;AACL,mBAAiB,IAAI,SAAS,GAAG;;AAEnC,QAAO;;;;;;;;;;;;;;;AAmCT,IAAa,gBAAb,MAA2B;;CAEzB;;CAGA;;CAGA,YAAY;;CAGZ,WAAW;EACT,iBAAiB;EACjB,mBAAmB;EACnB,qBAAqB;EACrB,uBAAuB;EACxB;CAED,YAAY,UAAgC,EAAE,EAAE;EAC9C,MAAM,EAAE,qBAAqB,IAAI,yBAAyB,QAAQ;AAClE,QAAKA,cAAe,IAAI,SAAS,mBAAmB;AACpD,QAAKC,kBAAmB,IAAI,SAAS,uBAAuB;;;;;CAM9D,IAAI,WAAoB;AACtB,SAAO,MAAKC;;;;;CAMd,IAAI,UAAU;AACZ,SAAO,EAAE,GAAG,MAAKC,SAAU;;;;;;;;CAa7B,mBAAmB,SAAiC;AAClD,MAAI,CAAC,iBAAiB,QAAQ,CAC5B,QAAO;AAIT,SAAO,UADU,mBAAmB,QAAQ,CAClB,GAAG,QAAQ;;;;;;CAOvC,uBAAuB,SAAsC;AAC3D,MAAI,MAAKD,SAAW,QAAO;EAE3B,MAAM,MAAM,MAAKE,kBAAmB,QAAQ;AAC5C,MAAI,CAAC,IAAK,QAAO;EAEjB,MAAM,SAAS,MAAKJ,YAAa,IAAI,IAAI;AACzC,MAAI,OACF,OAAKG,QAAS;MAEd,OAAKA,QAAS;AAEhB,SAAO;;;;;;CAOT,uBAAuB,SAAkB,SAAuB;AAC9D,MAAI,MAAKD,SAAW;EAEpB,MAAM,MAAM,MAAKE,kBAAmB,QAAQ;AAC5C,MAAI,IACF,OAAKJ,YAAa,IAAI,KAAK,QAAQ;;;;;;;CAavC,uBAAuB,cAAuB,cAA8B;AAI1E,SAAO,SAHU,mBAAmB,aAAa,CAGxB,GADL,KAAK,MAAM,aAAa;;;;;;CAQ9C,oBAAoB,cAAuB,cAAiD;AAC1F,MAAI,MAAKE,SAAW,QAAO;EAE3B,MAAM,MAAM,MAAKG,sBAAuB,cAAc,aAAa;EACnE,MAAM,SAAS,MAAKJ,gBAAiB,IAAI,IAAI;AAC7C,MAAI,OACF,OAAKE,QAAS;MAEd,OAAKA,QAAS;AAEhB,SAAO;;;;;CAMT,oBAAoB,cAAuB,cAAsB,OAA4B;AAC3F,MAAI,MAAKD,SAAW;EAEpB,MAAM,MAAM,MAAKG,sBAAuB,cAAc,aAAa;AACnE,QAAKJ,gBAAiB,IAAI,KAAK,MAAM;;;;;;;;;;;CAYvC,MAAM,uBACJ,OACA,cACA,UAGI,EAAE,EACkB;EAExB,MAAM,SAAS,KAAK,oBAAoB,OAAO,aAAa;AAC5D,MAAI,OACF,QAAO;EAIT,MAAM,QAAQ,MAAM,MAAM,yBAAyB,cAAc,QAAQ;AAGzE,OAAK,oBAAoB,OAAO,cAAc,MAAM;AAEpD,SAAO;;;;;;CAWT,UAAgB;AACd,MAAI,MAAKC,SAAW;AAEpB,QAAKF,YAAa,OAAO;AACzB,QAAKC,gBAAiB,OAAO;AAC7B,QAAKC,WAAY;;;;;;;;;;;;CAanB,CAAC,OAAO,WAAiB;AACvB,OAAK,SAAS;;;;;CAMhB,IAAI,kBAA0B;AAC5B,SAAO,MAAKF,YAAa;;;;;CAM3B,IAAI,sBAA8B;AAChC,SAAO,MAAKC,gBAAiB"}
1
+ {"version":3,"file":"RenderContext.js","names":["#canvasCache","#videoFrameCache","#disposed","#metrics","#getCanvasCacheKey","#getVideoFrameCacheKey","#documentStylesCache"],"sources":["../../src/preview/RenderContext.ts"],"sourcesContent":["/**\n * RenderContext manages scoped caches for the rendering pipeline.\n *\n * Used during foreignObject serialization to cache:\n * - Video frames by source timestamp (useful for freeze frames, slow-mo)\n * - Static element canvases by element identity + renderVersion\n *\n * The context should be created at the start of a render operation\n * and disposed when the render completes (success or failure).\n */\n\nimport { LRUCache } from \"../utils/LRUCache.js\";\nimport type { EFVideo } from \"../elements/EFVideo.js\";\n\n/**\n * Check if an element has a renderVersion property.\n */\nfunction hasRenderVersion(\n element: Element,\n): element is Element & { renderVersion: number } {\n return (\n \"renderVersion\" in element &&\n typeof (element as any).renderVersion === \"number\"\n );\n}\n\n/**\n * Module-level counter for generating unique element IDs.\n * This ensures uniqueness across all RenderContext instances.\n */\nlet nextElementId = 1;\n\n/**\n * WeakMap to store unique IDs for elements.\n * Using WeakMap ensures we don't prevent garbage collection of elements.\n * The ID is stable for the lifetime of the element.\n */\nconst elementUniqueIds = new WeakMap<Element, number>();\n\n/**\n * Get or create a unique ID for an element.\n * This guarantees uniqueness even for elements with the same id attribute\n * or no id attribute at all.\n */\nfunction getElementUniqueId(element: Element): number {\n let id = elementUniqueIds.get(element);\n if (id === undefined) {\n id = nextElementId++;\n elementUniqueIds.set(element, id);\n }\n return id;\n}\n\n/**\n * Result of capturing a video frame.\n */\nexport interface CapturedFrame {\n dataUrl: string;\n width: number;\n height: number;\n}\n\n/**\n * Options for creating a RenderContext.\n */\nexport interface RenderContextOptions {\n /** Maximum number of canvas dataURLs to cache (default: 50) */\n maxCanvasCacheSize?: number;\n /** Maximum number of video frame dataURLs to cache (default: 100) */\n maxVideoFrameCacheSize?: number;\n}\n\n/**\n * RenderContext provides scoped caching for render operations.\n *\n * Create at the start of a render, dispose when complete:\n * ```typescript\n * const context = new RenderContext();\n * try {\n * // ... render operations\n * } finally {\n * context.dispose();\n * }\n * ```\n */\nexport class RenderContext {\n /** Cache for static element canvases (ef-image, ef-waveform) */\n #canvasCache: LRUCache<string, string>;\n\n /** Cache for video frames by source timestamp */\n #videoFrameCache: LRUCache<string, CapturedFrame>;\n\n /** Cache for document styles (computed once per render session) */\n #documentStylesCache: string | null = null;\n\n /** Whether this context has been disposed */\n #disposed = false;\n\n /** Metrics for monitoring cache effectiveness */\n #metrics = {\n canvasCacheHits: 0,\n canvasCacheMisses: 0,\n videoFrameCacheHits: 0,\n videoFrameCacheMisses: 0,\n };\n\n constructor(options: RenderContextOptions = {}) {\n const { maxCanvasCacheSize = 50, maxVideoFrameCacheSize = 100 } = options;\n this.#canvasCache = new LRUCache(maxCanvasCacheSize);\n this.#videoFrameCache = new LRUCache(maxVideoFrameCacheSize);\n }\n\n /**\n * Check if the context has been disposed.\n */\n get disposed(): boolean {\n return this.#disposed;\n }\n\n /**\n * Get cache metrics for monitoring.\n */\n get metrics() {\n return { ...this.#metrics };\n }\n\n // ============================================================================\n // Static Element Cache (ef-image, ef-waveform)\n // ============================================================================\n\n /**\n * Generate a cache key for a static element.\n * Uses a unique element ID (via WeakMap) to ensure uniqueness even if\n * multiple elements have the same id attribute.\n * Returns null if the element doesn't support caching (no renderVersion).\n */\n #getCanvasCacheKey(element: Element): string | null {\n if (!hasRenderVersion(element)) {\n return null;\n }\n // Use unique element ID + render version for guaranteed uniqueness\n const uniqueId = getElementUniqueId(element);\n return `canvas:${uniqueId}:${element.renderVersion}`;\n }\n\n /**\n * Get a cached dataURL for a static element.\n * Returns undefined if not cached or element doesn't support caching.\n */\n getCachedCanvasDataUrl(element: Element): string | undefined {\n if (this.#disposed) return undefined;\n\n const key = this.#getCanvasCacheKey(element);\n if (!key) return undefined;\n\n const cached = this.#canvasCache.get(key);\n if (cached) {\n this.#metrics.canvasCacheHits++;\n } else {\n this.#metrics.canvasCacheMisses++;\n }\n return cached;\n }\n\n /**\n * Cache a dataURL for a static element.\n * Does nothing if the element doesn't support caching.\n */\n setCachedCanvasDataUrl(element: Element, dataUrl: string): void {\n if (this.#disposed) return;\n\n const key = this.#getCanvasCacheKey(element);\n if (key) {\n this.#canvasCache.set(key, dataUrl);\n }\n }\n\n // ============================================================================\n // Video Frame Cache\n // ============================================================================\n\n /**\n * Generate a cache key for a video frame.\n * Uses a unique element ID (via WeakMap) to ensure uniqueness even if\n * multiple videos have the same id attribute.\n */\n #getVideoFrameCacheKey(videoElement: Element, sourceTimeMs: number): string {\n const uniqueId = getElementUniqueId(videoElement);\n // Round to nearest ms to avoid floating point issues\n const roundedTime = Math.round(sourceTimeMs);\n return `video:${uniqueId}:${roundedTime}`;\n }\n\n /**\n * Get a cached video frame.\n * Returns undefined if not cached.\n */\n getCachedVideoFrame(\n videoElement: Element,\n sourceTimeMs: number,\n ): CapturedFrame | undefined {\n if (this.#disposed) return undefined;\n\n const key = this.#getVideoFrameCacheKey(videoElement, sourceTimeMs);\n const cached = this.#videoFrameCache.get(key);\n if (cached) {\n this.#metrics.videoFrameCacheHits++;\n } else {\n this.#metrics.videoFrameCacheMisses++;\n }\n return cached;\n }\n\n /**\n * Cache a video frame.\n */\n setCachedVideoFrame(\n videoElement: Element,\n sourceTimeMs: number,\n frame: CapturedFrame,\n ): void {\n if (this.#disposed) return;\n\n const key = this.#getVideoFrameCacheKey(videoElement, sourceTimeMs);\n this.#videoFrameCache.set(key, frame);\n }\n\n /**\n * Convenience method to get or capture a video frame.\n * Checks cache first, then captures if not cached.\n *\n * @param video - The ef-video element\n * @param sourceTimeMs - Source media timestamp\n * @param options - Capture options including quality and signal\n * @returns The captured frame data\n */\n async getOrCaptureVideoFrame(\n video: EFVideo,\n sourceTimeMs: number,\n options: {\n quality?: \"auto\" | \"scrub\" | \"main\";\n signal?: AbortSignal;\n } = {},\n ): Promise<CapturedFrame> {\n // Check cache first\n const cached = this.getCachedVideoFrame(video, sourceTimeMs);\n if (cached) {\n return cached;\n }\n\n // Capture frame using direct API\n const frame = await video.captureFrameAtSourceTime(sourceTimeMs, options);\n\n // Cache for future use\n this.setCachedVideoFrame(video, sourceTimeMs, frame);\n\n return frame;\n }\n\n // ============================================================================\n // Document Styles Cache\n // ============================================================================\n\n /**\n * Get cached document styles.\n * Returns undefined if not cached.\n */\n getCachedDocumentStyles(): string | undefined {\n if (this.#disposed) return undefined;\n return this.#documentStylesCache ?? undefined;\n }\n\n /**\n * Cache document styles.\n */\n setCachedDocumentStyles(styles: string): void {\n if (this.#disposed) return;\n this.#documentStylesCache = styles;\n }\n\n // ============================================================================\n // Cleanup\n // ============================================================================\n\n /**\n * Dispose the context and clear all caches.\n * Should be called when rendering is complete.\n */\n dispose(): void {\n if (this.#disposed) return;\n\n this.#canvasCache.clear();\n this.#videoFrameCache.clear();\n this.#documentStylesCache = null;\n this.#disposed = true;\n }\n\n /**\n * Symbol.dispose implementation for use with the `using` keyword.\n *\n * @example\n * ```typescript\n * using context = new RenderContext();\n * // ... render operations\n * // context is automatically disposed when scope exits\n * ```\n */\n [Symbol.dispose](): void {\n this.dispose();\n }\n\n /**\n * Get the current size of the canvas cache.\n */\n get canvasCacheSize(): number {\n return this.#canvasCache.size;\n }\n\n /**\n * Get the current size of the video frame cache.\n */\n get videoFrameCacheSize(): number {\n return this.#videoFrameCache.size;\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"RenderProfiler.js","names":["parts: string[]"],"sources":["../../src/preview/RenderProfiler.ts"],"sourcesContent":["/**\n * Profiling utility for render operations.\n * Centralizes timing accumulation and logging to keep business logic clean.\n */\n\n/** Interval between profiling log outputs (ms) */\nconst DEFAULT_LOG_INTERVAL_MS = 2000;\n\n/** Interval for periodic frame logging (every N frames) */\nconst DEFAULT_FRAME_LOG_INTERVAL = 60;\n\n/**\n * Phases tracked during rendering.\n */\nexport interface RenderTimings {\n setup: number;\n draw: number;\n downsample: number;\n canvasEncode: number;\n inline: number;\n serialize: number;\n base64: number;\n imageLoad: number;\n restore: number;\n}\n\n/**\n * Profiler for render operations.\n * Accumulates timing data and provides structured logging.\n */\nexport class RenderProfiler {\n private _renderCount = 0;\n private _lastLogTime = 0;\n private _timingLoggedAt = 0;\n \n private _timings: RenderTimings = {\n setup: 0,\n draw: 0,\n downsample: 0,\n canvasEncode: 0,\n inline: 0,\n serialize: 0,\n base64: 0,\n imageLoad: 0,\n restore: 0,\n };\n\n /**\n * Reset all timing data.\n */\n reset(): void {\n this._renderCount = 0;\n this._lastLogTime = 0;\n this._timingLoggedAt = 0;\n \n for (const key of Object.keys(this._timings) as (keyof RenderTimings)[]) {\n this._timings[key] = 0;\n }\n }\n\n /**\n * Get current render count.\n */\n get renderCount(): number {\n return this._renderCount;\n }\n\n /**\n * Increment render count.\n */\n incrementRenderCount(): void {\n this._renderCount++;\n }\n\n /**\n * Add time to a specific phase.\n */\n addTime(phase: keyof RenderTimings, ms: number): void {\n this._timings[phase] += ms;\n }\n\n /**\n * Time a synchronous operation and add to the specified phase.\n */\n time<T>(phase: keyof RenderTimings, fn: () => T): T {\n const start = performance.now();\n const result = fn();\n this._timings[phase] += performance.now() - start;\n return result;\n }\n\n /**\n * Time an async operation and add to the specified phase.\n */\n async timeAsync<T>(phase: keyof RenderTimings, fn: () => Promise<T>): Promise<T> {\n const start = performance.now();\n const result = await fn();\n this._timings[phase] += performance.now() - start;\n return result;\n }\n\n /**\n * Check if enough time has passed since last log (for time-based logging).\n */\n shouldLogByTime(intervalMs: number = DEFAULT_LOG_INTERVAL_MS): boolean {\n const now = performance.now();\n if (now - this._lastLogTime > intervalMs) {\n this._lastLogTime = now;\n return true;\n }\n return false;\n }\n\n /**\n * Check if enough frames have passed since last log (for frame-based logging).\n */\n shouldLogByFrameCount(interval: number = DEFAULT_FRAME_LOG_INTERVAL): boolean {\n if (this._renderCount - this._timingLoggedAt >= interval) {\n this._timingLoggedAt = this._renderCount;\n return true;\n }\n return false;\n }\n\n /**\n * Check if this is an early render (for initial debug logging).\n */\n isEarlyRender(threshold: number = 2): boolean {\n return this._renderCount < threshold;\n }\n\n /**\n * Get timing summary string.\n */\n summary(): string {\n const t = this._timings;\n const parts: string[] = [];\n \n if (t.setup > 0) parts.push(`setup=${t.setup.toFixed(0)}ms`);\n if (t.draw > 0) parts.push(`draw=${t.draw.toFixed(0)}ms`);\n if (t.downsample > 0) parts.push(`downsample=${t.downsample.toFixed(0)}ms`);\n if (t.canvasEncode > 0) parts.push(`canvasEncode=${t.canvasEncode.toFixed(0)}ms`);\n if (t.inline > 0) parts.push(`inline=${t.inline.toFixed(0)}ms`);\n if (t.serialize > 0) parts.push(`serialize=${t.serialize.toFixed(0)}ms`);\n if (t.base64 > 0) parts.push(`base64=${t.base64.toFixed(0)}ms`);\n if (t.imageLoad > 0) parts.push(`imageLoad=${t.imageLoad.toFixed(0)}ms`);\n if (t.restore > 0) parts.push(`restore=${t.restore.toFixed(0)}ms`);\n \n return parts.join(', ');\n }\n\n /**\n * Get raw timings object.\n */\n getTimings(): Readonly<RenderTimings> {\n return { ...this._timings };\n }\n}\n\n/**\n * Default shared profiler instance.\n * Can be replaced with a custom instance for testing.\n */\nexport const defaultProfiler = new RenderProfiler();\n"],"mappings":";;;;;;AAMA,MAAM,0BAA0B;;AAGhC,MAAM,6BAA6B;;;;;AAqBnC,IAAa,iBAAb,MAA4B;;sBACH;sBACA;yBACG;kBAEQ;GAChC,OAAO;GACP,MAAM;GACN,YAAY;GACZ,cAAc;GACd,QAAQ;GACR,WAAW;GACX,QAAQ;GACR,WAAW;GACX,SAAS;GACV;;;;;CAKD,QAAc;AACZ,OAAK,eAAe;AACpB,OAAK,eAAe;AACpB,OAAK,kBAAkB;AAEvB,OAAK,MAAM,OAAO,OAAO,KAAK,KAAK,SAAS,CAC1C,MAAK,SAAS,OAAO;;;;;CAOzB,IAAI,cAAsB;AACxB,SAAO,KAAK;;;;;CAMd,uBAA6B;AAC3B,OAAK;;;;;CAMP,QAAQ,OAA4B,IAAkB;AACpD,OAAK,SAAS,UAAU;;;;;CAM1B,KAAQ,OAA4B,IAAgB;EAClD,MAAM,QAAQ,YAAY,KAAK;EAC/B,MAAM,SAAS,IAAI;AACnB,OAAK,SAAS,UAAU,YAAY,KAAK,GAAG;AAC5C,SAAO;;;;;CAMT,MAAM,UAAa,OAA4B,IAAkC;EAC/E,MAAM,QAAQ,YAAY,KAAK;EAC/B,MAAM,SAAS,MAAM,IAAI;AACzB,OAAK,SAAS,UAAU,YAAY,KAAK,GAAG;AAC5C,SAAO;;;;;CAMT,gBAAgB,aAAqB,yBAAkC;EACrE,MAAM,MAAM,YAAY,KAAK;AAC7B,MAAI,MAAM,KAAK,eAAe,YAAY;AACxC,QAAK,eAAe;AACpB,UAAO;;AAET,SAAO;;;;;CAMT,sBAAsB,WAAmB,4BAAqC;AAC5E,MAAI,KAAK,eAAe,KAAK,mBAAmB,UAAU;AACxD,QAAK,kBAAkB,KAAK;AAC5B,UAAO;;AAET,SAAO;;;;;CAMT,cAAc,YAAoB,GAAY;AAC5C,SAAO,KAAK,eAAe;;;;;CAM7B,UAAkB;EAChB,MAAM,IAAI,KAAK;EACf,MAAMA,QAAkB,EAAE;AAE1B,MAAI,EAAE,QAAQ,EAAG,OAAM,KAAK,SAAS,EAAE,MAAM,QAAQ,EAAE,CAAC,IAAI;AAC5D,MAAI,EAAE,OAAO,EAAG,OAAM,KAAK,QAAQ,EAAE,KAAK,QAAQ,EAAE,CAAC,IAAI;AACzD,MAAI,EAAE,aAAa,EAAG,OAAM,KAAK,cAAc,EAAE,WAAW,QAAQ,EAAE,CAAC,IAAI;AAC3E,MAAI,EAAE,eAAe,EAAG,OAAM,KAAK,gBAAgB,EAAE,aAAa,QAAQ,EAAE,CAAC,IAAI;AACjF,MAAI,EAAE,SAAS,EAAG,OAAM,KAAK,UAAU,EAAE,OAAO,QAAQ,EAAE,CAAC,IAAI;AAC/D,MAAI,EAAE,YAAY,EAAG,OAAM,KAAK,aAAa,EAAE,UAAU,QAAQ,EAAE,CAAC,IAAI;AACxE,MAAI,EAAE,SAAS,EAAG,OAAM,KAAK,UAAU,EAAE,OAAO,QAAQ,EAAE,CAAC,IAAI;AAC/D,MAAI,EAAE,YAAY,EAAG,OAAM,KAAK,aAAa,EAAE,UAAU,QAAQ,EAAE,CAAC,IAAI;AACxE,MAAI,EAAE,UAAU,EAAG,OAAM,KAAK,WAAW,EAAE,QAAQ,QAAQ,EAAE,CAAC,IAAI;AAElE,SAAO,MAAM,KAAK,KAAK;;;;;CAMzB,aAAsC;AACpC,SAAO,EAAE,GAAG,KAAK,UAAU;;;;;;;AAQ/B,MAAa,kBAAkB,IAAI,gBAAgB"}
1
+ {"version":3,"file":"RenderProfiler.js","names":["parts: string[]"],"sources":["../../src/preview/RenderProfiler.ts"],"sourcesContent":["/**\n * Profiling utility for render operations.\n * Centralizes timing accumulation and logging to keep business logic clean.\n */\n\n/** Interval between profiling log outputs (ms) */\nconst DEFAULT_LOG_INTERVAL_MS = 2000;\n\n/** Interval for periodic frame logging (every N frames) */\nconst DEFAULT_FRAME_LOG_INTERVAL = 60;\n\n/**\n * Phases tracked during rendering.\n */\nexport interface RenderTimings {\n setup: number;\n draw: number;\n downsample: number;\n canvasEncode: number;\n inline: number;\n serialize: number;\n base64: number;\n imageLoad: number;\n restore: number;\n}\n\n/**\n * Profiler for render operations.\n * Accumulates timing data and provides structured logging.\n */\nexport class RenderProfiler {\n private _renderCount = 0;\n private _lastLogTime = 0;\n private _timingLoggedAt = 0;\n\n private _timings: RenderTimings = {\n setup: 0,\n draw: 0,\n downsample: 0,\n canvasEncode: 0,\n inline: 0,\n serialize: 0,\n base64: 0,\n imageLoad: 0,\n restore: 0,\n };\n\n /**\n * Reset all timing data.\n */\n reset(): void {\n this._renderCount = 0;\n this._lastLogTime = 0;\n this._timingLoggedAt = 0;\n\n for (const key of Object.keys(this._timings) as (keyof RenderTimings)[]) {\n this._timings[key] = 0;\n }\n }\n\n /**\n * Get current render count.\n */\n get renderCount(): number {\n return this._renderCount;\n }\n\n /**\n * Increment render count.\n */\n incrementRenderCount(): void {\n this._renderCount++;\n }\n\n /**\n * Add time to a specific phase.\n */\n addTime(phase: keyof RenderTimings, ms: number): void {\n this._timings[phase] += ms;\n }\n\n /**\n * Time a synchronous operation and add to the specified phase.\n */\n time<T>(phase: keyof RenderTimings, fn: () => T): T {\n const start = performance.now();\n const result = fn();\n this._timings[phase] += performance.now() - start;\n return result;\n }\n\n /**\n * Time an async operation and add to the specified phase.\n */\n async timeAsync<T>(\n phase: keyof RenderTimings,\n fn: () => Promise<T>,\n ): Promise<T> {\n const start = performance.now();\n const result = await fn();\n this._timings[phase] += performance.now() - start;\n return result;\n }\n\n /**\n * Check if enough time has passed since last log (for time-based logging).\n */\n shouldLogByTime(intervalMs: number = DEFAULT_LOG_INTERVAL_MS): boolean {\n const now = performance.now();\n if (now - this._lastLogTime > intervalMs) {\n this._lastLogTime = now;\n return true;\n }\n return false;\n }\n\n /**\n * Check if enough frames have passed since last log (for frame-based logging).\n */\n shouldLogByFrameCount(\n interval: number = DEFAULT_FRAME_LOG_INTERVAL,\n ): boolean {\n if (this._renderCount - this._timingLoggedAt >= interval) {\n this._timingLoggedAt = this._renderCount;\n return true;\n }\n return false;\n }\n\n /**\n * Check if this is an early render (for initial debug logging).\n */\n isEarlyRender(threshold: number = 2): boolean {\n return this._renderCount < threshold;\n }\n\n /**\n * Get timing summary string.\n */\n summary(): string {\n const t = this._timings;\n const parts: string[] = [];\n\n if (t.setup > 0) parts.push(`setup=${t.setup.toFixed(0)}ms`);\n if (t.draw > 0) parts.push(`draw=${t.draw.toFixed(0)}ms`);\n if (t.downsample > 0) parts.push(`downsample=${t.downsample.toFixed(0)}ms`);\n if (t.canvasEncode > 0)\n parts.push(`canvasEncode=${t.canvasEncode.toFixed(0)}ms`);\n if (t.inline > 0) parts.push(`inline=${t.inline.toFixed(0)}ms`);\n if (t.serialize > 0) parts.push(`serialize=${t.serialize.toFixed(0)}ms`);\n if (t.base64 > 0) parts.push(`base64=${t.base64.toFixed(0)}ms`);\n if (t.imageLoad > 0) parts.push(`imageLoad=${t.imageLoad.toFixed(0)}ms`);\n if (t.restore > 0) parts.push(`restore=${t.restore.toFixed(0)}ms`);\n\n return parts.join(\", \");\n }\n\n /**\n * Get raw timings object.\n */\n getTimings(): Readonly<RenderTimings> {\n return { ...this._timings };\n }\n}\n\n/**\n * Default shared profiler instance.\n * Can be replaced with a custom instance for testing.\n */\nexport const defaultProfiler = new RenderProfiler();\n"],"mappings":";;;;;;AAMA,MAAM,0BAA0B;;AAGhC,MAAM,6BAA6B;;;;;AAqBnC,IAAa,iBAAb,MAA4B;;sBACH;sBACA;yBACG;kBAEQ;GAChC,OAAO;GACP,MAAM;GACN,YAAY;GACZ,cAAc;GACd,QAAQ;GACR,WAAW;GACX,QAAQ;GACR,WAAW;GACX,SAAS;GACV;;;;;CAKD,QAAc;AACZ,OAAK,eAAe;AACpB,OAAK,eAAe;AACpB,OAAK,kBAAkB;AAEvB,OAAK,MAAM,OAAO,OAAO,KAAK,KAAK,SAAS,CAC1C,MAAK,SAAS,OAAO;;;;;CAOzB,IAAI,cAAsB;AACxB,SAAO,KAAK;;;;;CAMd,uBAA6B;AAC3B,OAAK;;;;;CAMP,QAAQ,OAA4B,IAAkB;AACpD,OAAK,SAAS,UAAU;;;;;CAM1B,KAAQ,OAA4B,IAAgB;EAClD,MAAM,QAAQ,YAAY,KAAK;EAC/B,MAAM,SAAS,IAAI;AACnB,OAAK,SAAS,UAAU,YAAY,KAAK,GAAG;AAC5C,SAAO;;;;;CAMT,MAAM,UACJ,OACA,IACY;EACZ,MAAM,QAAQ,YAAY,KAAK;EAC/B,MAAM,SAAS,MAAM,IAAI;AACzB,OAAK,SAAS,UAAU,YAAY,KAAK,GAAG;AAC5C,SAAO;;;;;CAMT,gBAAgB,aAAqB,yBAAkC;EACrE,MAAM,MAAM,YAAY,KAAK;AAC7B,MAAI,MAAM,KAAK,eAAe,YAAY;AACxC,QAAK,eAAe;AACpB,UAAO;;AAET,SAAO;;;;;CAMT,sBACE,WAAmB,4BACV;AACT,MAAI,KAAK,eAAe,KAAK,mBAAmB,UAAU;AACxD,QAAK,kBAAkB,KAAK;AAC5B,UAAO;;AAET,SAAO;;;;;CAMT,cAAc,YAAoB,GAAY;AAC5C,SAAO,KAAK,eAAe;;;;;CAM7B,UAAkB;EAChB,MAAM,IAAI,KAAK;EACf,MAAMA,QAAkB,EAAE;AAE1B,MAAI,EAAE,QAAQ,EAAG,OAAM,KAAK,SAAS,EAAE,MAAM,QAAQ,EAAE,CAAC,IAAI;AAC5D,MAAI,EAAE,OAAO,EAAG,OAAM,KAAK,QAAQ,EAAE,KAAK,QAAQ,EAAE,CAAC,IAAI;AACzD,MAAI,EAAE,aAAa,EAAG,OAAM,KAAK,cAAc,EAAE,WAAW,QAAQ,EAAE,CAAC,IAAI;AAC3E,MAAI,EAAE,eAAe,EACnB,OAAM,KAAK,gBAAgB,EAAE,aAAa,QAAQ,EAAE,CAAC,IAAI;AAC3D,MAAI,EAAE,SAAS,EAAG,OAAM,KAAK,UAAU,EAAE,OAAO,QAAQ,EAAE,CAAC,IAAI;AAC/D,MAAI,EAAE,YAAY,EAAG,OAAM,KAAK,aAAa,EAAE,UAAU,QAAQ,EAAE,CAAC,IAAI;AACxE,MAAI,EAAE,SAAS,EAAG,OAAM,KAAK,UAAU,EAAE,OAAO,QAAQ,EAAE,CAAC,IAAI;AAC/D,MAAI,EAAE,YAAY,EAAG,OAAM,KAAK,aAAa,EAAE,UAAU,QAAQ,EAAE,CAAC,IAAI;AACxE,MAAI,EAAE,UAAU,EAAG,OAAM,KAAK,WAAW,EAAE,QAAQ,QAAQ,EAAE,CAAC,IAAI;AAElE,SAAO,MAAM,KAAK,KAAK;;;;;CAMzB,aAAsC;AACpC,SAAO,EAAE,GAAG,KAAK,UAAU;;;;;;;AAQ/B,MAAa,kBAAkB,IAAI,gBAAgB"}
@@ -0,0 +1,85 @@
1
+ //#region src/preview/RenderStats.ts
2
+ /**
3
+ * RenderStats collects performance data from the rendering system.
4
+ *
5
+ * Usage:
6
+ * ```typescript
7
+ * const stats = new RenderStats(adaptiveTracker);
8
+ *
9
+ * // In render loop (always call this)
10
+ * stats.recordFrame(renderTime, timestamp, isAtRest);
11
+ *
12
+ * // In display (only when visible)
13
+ * const data = stats.getStats(renderWidth, renderHeight, resolutionScale);
14
+ * ```
15
+ */
16
+ var RenderStats = class {
17
+ constructor(adaptiveTracker) {
18
+ this.renderTimes = [];
19
+ this.frameIntervals = [];
20
+ this.lastFrameTime = 0;
21
+ this.ROLLING_WINDOW_SIZE = 30;
22
+ this.TARGET_FRAME_TIME_MS = 33.33;
23
+ this.adaptiveTracker = adaptiveTracker;
24
+ }
25
+ /**
26
+ * Record a completed frame render.
27
+ * Call this from the render loop after each frame completes.
28
+ *
29
+ * @param renderTime - Time spent rendering this frame (ms)
30
+ * @param timestamp - Current timestamp from performance.now() or rAF
31
+ * @param isAtRest - Whether the system is at rest (not playing/scrubbing)
32
+ */
33
+ recordFrame(renderTime, timestamp, isAtRest) {
34
+ this.renderTimes.push(renderTime);
35
+ if (this.renderTimes.length > this.ROLLING_WINDOW_SIZE) this.renderTimes.shift();
36
+ if (this.lastFrameTime > 0) {
37
+ const interval = timestamp - this.lastFrameTime;
38
+ this.frameIntervals.push(interval);
39
+ if (this.frameIntervals.length > this.ROLLING_WINDOW_SIZE) this.frameIntervals.shift();
40
+ }
41
+ this.lastFrameTime = timestamp;
42
+ if (!isAtRest) this.adaptiveTracker.recordFrame(renderTime, timestamp);
43
+ }
44
+ /**
45
+ * Get current statistics for display.
46
+ *
47
+ * @param renderWidth - Current render width in pixels
48
+ * @param renderHeight - Current render height in pixels
49
+ * @param resolutionScale - Current resolution scale (0-1), or null if not applicable
50
+ * @returns Current playback statistics
51
+ */
52
+ getStats(renderWidth, renderHeight, resolutionScale = null) {
53
+ const avgRenderTime = this.renderTimes.length > 0 ? this.renderTimes.reduce((a, b) => a + b, 0) / this.renderTimes.length : null;
54
+ const avgFrameInterval = this.frameIntervals.length > 0 ? this.frameIntervals.reduce((a, b) => a + b, 0) / this.frameIntervals.length : 16.67;
55
+ const fps = avgFrameInterval > 0 ? 1e3 / avgFrameInterval : 0;
56
+ const headroom = avgRenderTime !== null ? this.TARGET_FRAME_TIME_MS - avgRenderTime : null;
57
+ const trackerStats = this.adaptiveTracker.getStats();
58
+ return {
59
+ fps,
60
+ avgRenderTime,
61
+ headroom,
62
+ pressureState: trackerStats.pressureState,
63
+ pressureHistory: trackerStats.pressureHistory,
64
+ renderWidth,
65
+ renderHeight,
66
+ resolutionScale,
67
+ samplesAtCurrentScale: trackerStats.samplesAtCurrentScale,
68
+ canScaleUp: trackerStats.canScaleUp,
69
+ canScaleDown: trackerStats.canScaleDown
70
+ };
71
+ }
72
+ /**
73
+ * Reset all collected statistics.
74
+ * Useful when switching modes or starting a new session.
75
+ */
76
+ reset() {
77
+ this.renderTimes = [];
78
+ this.frameIntervals = [];
79
+ this.lastFrameTime = 0;
80
+ }
81
+ };
82
+
83
+ //#endregion
84
+ export { RenderStats };
85
+ //# sourceMappingURL=RenderStats.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RenderStats.js","names":[],"sources":["../../src/preview/RenderStats.ts"],"sourcesContent":["/**\n * RenderStats: Always-on performance statistics collection\n *\n * This class continuously collects rendering performance data regardless of\n * whether stats are being displayed. It acts as a persistent data store that\n * the UI can read from at any time.\n *\n * Key principles:\n * - Collection is always active (not tied to display visibility)\n * - Data persists across mode changes and zoom operations\n * - Display is orthogonal to collection\n */\n\nimport type { AdaptiveResolutionTracker } from \"./AdaptiveResolutionTracker.js\";\n\n/**\n * Playback statistics for display.\n */\nexport interface PlaybackStats {\n fps: number;\n avgRenderTime: number | null;\n headroom: number | null;\n pressureState: string;\n pressureHistory: string[];\n renderWidth: number;\n renderHeight: number;\n resolutionScale: number | null;\n samplesAtCurrentScale?: number;\n canScaleUp?: boolean;\n canScaleDown?: boolean;\n}\n\n/**\n * RenderStats collects performance data from the rendering system.\n *\n * Usage:\n * ```typescript\n * const stats = new RenderStats(adaptiveTracker);\n *\n * // In render loop (always call this)\n * stats.recordFrame(renderTime, timestamp, isAtRest);\n *\n * // In display (only when visible)\n * const data = stats.getStats(renderWidth, renderHeight, resolutionScale);\n * ```\n */\nexport class RenderStats {\n private adaptiveTracker: AdaptiveResolutionTracker;\n\n // Frame timing data\n private renderTimes: number[] = [];\n private frameIntervals: number[] = [];\n private lastFrameTime = 0;\n\n private readonly ROLLING_WINDOW_SIZE = 30; // ~1 second at 30fps\n private readonly TARGET_FRAME_TIME_MS = 33.33; // 30fps target\n\n constructor(adaptiveTracker: AdaptiveResolutionTracker) {\n this.adaptiveTracker = adaptiveTracker;\n }\n\n /**\n * Record a completed frame render.\n * Call this from the render loop after each frame completes.\n *\n * @param renderTime - Time spent rendering this frame (ms)\n * @param timestamp - Current timestamp from performance.now() or rAF\n * @param isAtRest - Whether the system is at rest (not playing/scrubbing)\n */\n recordFrame(renderTime: number, timestamp: number, isAtRest: boolean): void {\n // Track render times for averaging\n this.renderTimes.push(renderTime);\n if (this.renderTimes.length > this.ROLLING_WINDOW_SIZE) {\n this.renderTimes.shift();\n }\n\n // Track frame intervals for FPS calculation\n if (this.lastFrameTime > 0) {\n const interval = timestamp - this.lastFrameTime;\n this.frameIntervals.push(interval);\n if (this.frameIntervals.length > this.ROLLING_WINDOW_SIZE) {\n this.frameIntervals.shift();\n }\n }\n this.lastFrameTime = timestamp;\n\n // Update adaptive tracker (only when in motion)\n if (!isAtRest) {\n this.adaptiveTracker.recordFrame(renderTime, timestamp);\n }\n }\n\n /**\n * Get current statistics for display.\n *\n * @param renderWidth - Current render width in pixels\n * @param renderHeight - Current render height in pixels\n * @param resolutionScale - Current resolution scale (0-1), or null if not applicable\n * @returns Current playback statistics\n */\n getStats(\n renderWidth: number,\n renderHeight: number,\n resolutionScale: number | null = null,\n ): PlaybackStats {\n // Calculate average render time\n const avgRenderTime =\n this.renderTimes.length > 0\n ? this.renderTimes.reduce((a, b) => a + b, 0) / this.renderTimes.length\n : null;\n\n // Calculate FPS from frame intervals\n const avgFrameInterval =\n this.frameIntervals.length > 0\n ? this.frameIntervals.reduce((a, b) => a + b, 0) /\n this.frameIntervals.length\n : 16.67;\n const fps = avgFrameInterval > 0 ? 1000 / avgFrameInterval : 0;\n\n // Calculate headroom (positive = faster than target, negative = slower)\n const headroom =\n avgRenderTime !== null ? this.TARGET_FRAME_TIME_MS - avgRenderTime : null;\n\n // Get adaptive tracker stats\n const trackerStats = this.adaptiveTracker.getStats();\n\n return {\n fps,\n avgRenderTime,\n headroom,\n pressureState: trackerStats.pressureState,\n pressureHistory: trackerStats.pressureHistory,\n renderWidth,\n renderHeight,\n resolutionScale,\n samplesAtCurrentScale: trackerStats.samplesAtCurrentScale,\n canScaleUp: trackerStats.canScaleUp,\n canScaleDown: trackerStats.canScaleDown,\n };\n }\n\n /**\n * Reset all collected statistics.\n * Useful when switching modes or starting a new session.\n */\n reset(): void {\n this.renderTimes = [];\n this.frameIntervals = [];\n this.lastFrameTime = 0;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;AA8CA,IAAa,cAAb,MAAyB;CAWvB,YAAY,iBAA4C;qBAPxB,EAAE;wBACC,EAAE;uBACb;6BAEe;8BACC;AAGtC,OAAK,kBAAkB;;;;;;;;;;CAWzB,YAAY,YAAoB,WAAmB,UAAyB;AAE1E,OAAK,YAAY,KAAK,WAAW;AACjC,MAAI,KAAK,YAAY,SAAS,KAAK,oBACjC,MAAK,YAAY,OAAO;AAI1B,MAAI,KAAK,gBAAgB,GAAG;GAC1B,MAAM,WAAW,YAAY,KAAK;AAClC,QAAK,eAAe,KAAK,SAAS;AAClC,OAAI,KAAK,eAAe,SAAS,KAAK,oBACpC,MAAK,eAAe,OAAO;;AAG/B,OAAK,gBAAgB;AAGrB,MAAI,CAAC,SACH,MAAK,gBAAgB,YAAY,YAAY,UAAU;;;;;;;;;;CAY3D,SACE,aACA,cACA,kBAAiC,MAClB;EAEf,MAAM,gBACJ,KAAK,YAAY,SAAS,IACtB,KAAK,YAAY,QAAQ,GAAG,MAAM,IAAI,GAAG,EAAE,GAAG,KAAK,YAAY,SAC/D;EAGN,MAAM,mBACJ,KAAK,eAAe,SAAS,IACzB,KAAK,eAAe,QAAQ,GAAG,MAAM,IAAI,GAAG,EAAE,GAC9C,KAAK,eAAe,SACpB;EACN,MAAM,MAAM,mBAAmB,IAAI,MAAO,mBAAmB;EAG7D,MAAM,WACJ,kBAAkB,OAAO,KAAK,uBAAuB,gBAAgB;EAGvE,MAAM,eAAe,KAAK,gBAAgB,UAAU;AAEpD,SAAO;GACL;GACA;GACA;GACA,eAAe,aAAa;GAC5B,iBAAiB,aAAa;GAC9B;GACA;GACA;GACA,uBAAuB,aAAa;GACpC,YAAY,aAAa;GACzB,cAAc,aAAa;GAC5B;;;;;;CAOH,QAAc;AACZ,OAAK,cAAc,EAAE;AACrB,OAAK,iBAAiB,EAAE;AACxB,OAAK,gBAAgB"}