@editframe/elements 0.37.3-beta → 0.38.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (321) 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 +4 -4
  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 +7 -10
  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 +4 -4
  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 +4 -4
  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 +4 -4
  107. package/dist/gui/EFFocusOverlay.js +3 -3
  108. package/dist/gui/EFFocusOverlay.js.map +1 -1
  109. package/dist/gui/EFOverlayItem.d.ts +4 -4
  110. package/dist/gui/EFOverlayLayer.d.ts +4 -4
  111. package/dist/gui/EFPause.d.ts +4 -4
  112. package/dist/gui/EFPause.js +1 -1
  113. package/dist/gui/EFPlay.d.ts +4 -4
  114. package/dist/gui/EFPlay.js +1 -1
  115. package/dist/gui/EFPreview.js +1 -1
  116. package/dist/gui/EFResizableBox.d.ts +4 -4
  117. package/dist/gui/EFResizableBox.js +5 -5
  118. package/dist/gui/EFResizableBox.js.map +1 -1
  119. package/dist/gui/EFScrubber.d.ts +4 -4
  120. package/dist/gui/EFScrubber.js +8 -13
  121. package/dist/gui/EFScrubber.js.map +1 -1
  122. package/dist/gui/EFTimeDisplay.d.ts +8 -4
  123. package/dist/gui/EFTimeDisplay.js +25 -7
  124. package/dist/gui/EFTimeDisplay.js.map +1 -1
  125. package/dist/gui/EFTimelineRuler.d.ts +4 -4
  126. package/dist/gui/EFTimelineRuler.js +3 -3
  127. package/dist/gui/EFTimelineRuler.js.map +1 -1
  128. package/dist/gui/EFToggleLoop.d.ts +4 -4
  129. package/dist/gui/EFToggleLoop.js +1 -1
  130. package/dist/gui/EFTogglePlay.d.ts +4 -4
  131. package/dist/gui/EFTogglePlay.js +1 -1
  132. package/dist/gui/EFTransformHandles.d.ts +4 -4
  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 +3 -1
  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 +4 -4
  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 +4 -4
  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.js +267 -145
  238. package/dist/preview/renderTimegroupToCanvas.js.map +1 -1
  239. package/dist/preview/renderTimegroupToCanvas.types.d.ts +30 -0
  240. package/dist/preview/renderTimegroupToVideo.js +85 -105
  241. package/dist/preview/renderTimegroupToVideo.js.map +1 -1
  242. package/dist/preview/{renderTimegroupToVideo.d.ts → renderTimegroupToVideo.types.d.ts} +9 -9
  243. package/dist/preview/renderVideoToVideo.js +286 -0
  244. package/dist/preview/renderVideoToVideo.js.map +1 -0
  245. package/dist/preview/renderers.js.map +1 -1
  246. package/dist/preview/rendering/ScaleConfig.js +74 -0
  247. package/dist/preview/rendering/ScaleConfig.js.map +1 -0
  248. package/dist/preview/rendering/inlineImages.js +1 -44
  249. package/dist/preview/rendering/inlineImages.js.map +1 -1
  250. package/dist/preview/rendering/loadImage.js +22 -0
  251. package/dist/preview/rendering/loadImage.js.map +1 -0
  252. package/dist/preview/rendering/renderToImageNative.js +3 -3
  253. package/dist/preview/rendering/renderToImageNative.js.map +1 -1
  254. package/dist/preview/rendering/serializeTimelineDirect.js +224 -68
  255. package/dist/preview/rendering/serializeTimelineDirect.js.map +1 -1
  256. package/dist/preview/statsTrackingStrategy.js +1 -101
  257. package/dist/preview/statsTrackingStrategy.js.map +1 -1
  258. package/dist/preview/workers/WorkerPool.js +0 -1
  259. package/dist/preview/workers/WorkerPool.js.map +1 -1
  260. package/dist/preview/workers/encoderWorkerInline.js +21 -54
  261. package/dist/preview/workers/encoderWorkerInline.js.map +1 -1
  262. package/dist/render/EFRenderAPI.d.ts +2 -1
  263. package/dist/render/EFRenderAPI.js +12 -36
  264. package/dist/render/EFRenderAPI.js.map +1 -1
  265. package/dist/render/getRenderData.js +4 -4
  266. package/dist/render/getRenderData.js.map +1 -1
  267. package/dist/style.css +114 -163
  268. package/dist/transcoding/cache/RequestDeduplicator.js +1 -0
  269. package/dist/transcoding/cache/RequestDeduplicator.js.map +1 -1
  270. package/dist/transcoding/types/index.d.ts +1 -1
  271. package/dist/transcoding/utils/UrlGenerator.js +10 -3
  272. package/dist/transcoding/utils/UrlGenerator.js.map +1 -1
  273. package/dist/utils/LRUCache.js +1 -0
  274. package/dist/utils/LRUCache.js.map +1 -1
  275. package/dist/utils/frameTime.js +23 -1
  276. package/dist/utils/frameTime.js.map +1 -1
  277. package/package.json +21 -8
  278. package/scripts/build-css.js +8 -1
  279. package/test/setup.ts +0 -1
  280. package/test/useAssetMSW.ts +50 -0
  281. package/test/visualRegressionUtils.ts +23 -9
  282. package/dist/_virtual/rolldown_runtime.js +0 -27
  283. package/dist/elements/EFMedia/AssetIdMediaEngine.js.map +0 -1
  284. package/dist/elements/EFThumbnailStrip.d.ts +0 -167
  285. package/dist/elements/EFThumbnailStrip.js +0 -731
  286. package/dist/elements/EFThumbnailStrip.js.map +0 -1
  287. package/dist/elements/SessionThumbnailCache.js +0 -154
  288. package/dist/elements/SessionThumbnailCache.js.map +0 -1
  289. package/dist/node_modules/react/cjs/react-jsx-runtime.development.js +0 -688
  290. package/dist/node_modules/react/cjs/react-jsx-runtime.development.js.map +0 -1
  291. package/dist/node_modules/react/cjs/react.development.js +0 -1521
  292. package/dist/node_modules/react/cjs/react.development.js.map +0 -1
  293. package/dist/node_modules/react/index.js +0 -13
  294. package/dist/node_modules/react/index.js.map +0 -1
  295. package/dist/node_modules/react/jsx-runtime.js +0 -13
  296. package/dist/node_modules/react/jsx-runtime.js.map +0 -1
  297. package/dist/preview/encoding/types.d.ts +0 -1
  298. package/dist/preview/renderTimegroupPreview.js +0 -686
  299. package/dist/preview/renderTimegroupPreview.js.map +0 -1
  300. package/dist/preview/renderTimegroupToCanvas.d.ts +0 -42
  301. package/dist/preview/rendering/renderToImage.d.ts +0 -2
  302. package/dist/preview/rendering/renderToImage.js +0 -95
  303. package/dist/preview/rendering/renderToImage.js.map +0 -1
  304. package/dist/preview/rendering/renderToImageForeignObject.js +0 -163
  305. package/dist/preview/rendering/renderToImageForeignObject.js.map +0 -1
  306. package/dist/preview/rendering/renderToImageNative.d.ts +0 -1
  307. package/dist/preview/rendering/svgSerializer.js +0 -43
  308. package/dist/preview/rendering/svgSerializer.js.map +0 -1
  309. package/dist/preview/rendering/types.d.ts +0 -2
  310. package/dist/preview/thumbnailCacheSettings.js +0 -52
  311. package/dist/preview/thumbnailCacheSettings.js.map +0 -1
  312. package/dist/sandbox/PlaybackControls.d.ts +0 -1
  313. package/dist/sandbox/PlaybackControls.js +0 -10
  314. package/dist/sandbox/PlaybackControls.js.map +0 -1
  315. package/dist/sandbox/ScenarioRunner.d.ts +0 -1
  316. package/dist/sandbox/ScenarioRunner.js +0 -1
  317. package/dist/sandbox/defineSandbox.d.ts +0 -1
  318. package/dist/sandbox/index.d.ts +0 -3
  319. package/dist/sandbox/index.js +0 -2
  320. package/test/EFVideo.framegen.browsertest.ts +0 -80
  321. package/test/thumbnail-performance-test.html +0 -116
@@ -1,15 +1,15 @@
1
- import { updateAnimations } from "./updateAnimations.js";
2
- import { __decorate } from "../_virtual/_@oxc-project_runtime@0.95.0/helpers/decorate.js";
3
1
  import { TWMixin } from "../gui/TWMixin2.js";
4
2
  import { withSpanSync } from "../otel/tracingHelpers.js";
5
- import { PRIORITY_VIDEO, createFrameTaskWrapper } from "../preview/FrameController.js";
3
+ import { PRIORITY_VIDEO } from "../preview/FrameController.js";
4
+ import { updateAnimations } from "./updateAnimations.js";
5
+ import { __decorate } from "../_virtual/_@oxc-project_runtime@0.95.0/helpers/decorate.js";
6
6
  import { EFMedia } from "./EFMedia.js";
7
7
  import { DelayedLoadingState } from "../DelayedLoadingState.js";
8
8
  import { MainVideoInputCache } from "./EFMedia/videoTasks/MainVideoInputCache.js";
9
9
  import { ScrubInputCache } from "./EFMedia/videoTasks/ScrubInputCache.js";
10
10
  import debug from "debug";
11
11
  import { css, html } from "lit";
12
- import { customElement, property, state } from "lit/decorators.js";
12
+ import { customElement, state } from "lit/decorators.js";
13
13
  import { context, trace } from "@opentelemetry/api";
14
14
  import { createRef, ref } from "lit/directives/ref.js";
15
15
 
@@ -17,21 +17,44 @@ import { createRef, ref } from "lit/directives/ref.js";
17
17
  const mainVideoInputCache = new MainVideoInputCache();
18
18
  const scrubInputCache = new ScrubInputCache();
19
19
  const log = debug("ef:elements:EFVideo");
20
+ var VideoSeekTask = class {
21
+ constructor() {
22
+ this.value = void 0;
23
+ this.task = void 0;
24
+ this.taskComplete = Promise.resolve(void 0);
25
+ }
26
+ #resolve;
27
+ begin() {
28
+ this.taskComplete = new Promise((resolve) => {
29
+ this.#resolve = resolve;
30
+ });
31
+ }
32
+ complete(sample) {
33
+ this.value = sample;
34
+ this.#resolve?.(sample);
35
+ }
36
+ abort() {
37
+ this.#resolve?.(void 0);
38
+ }
39
+ };
20
40
  let EFVideo = class EFVideo$1 extends TWMixin(EFMedia) {
21
41
  static {
22
42
  this.styles = [css`
23
43
  :host {
24
44
  display: block;
25
45
  position: relative;
46
+ object-fit: contain;
47
+ object-position: center;
26
48
  }
27
49
  canvas {
28
50
  overflow: hidden;
29
51
  position: static;
30
52
  width: 100%;
31
53
  height: 100%;
54
+ object-fit: inherit;
55
+ object-position: inherit;
32
56
  margin: 0;
33
57
  padding: 0;
34
- overflow: hidden;
35
58
  border: none;
36
59
  outline: none;
37
60
  box-shadow: none;
@@ -85,9 +108,29 @@ let EFVideo = class EFVideo$1 extends TWMixin(EFMedia) {
85
108
  #cachedVideoSample = void 0;
86
109
  #cachedVideoSampleTimeMs = void 0;
87
110
  /**
111
+ * Quality upgrade intent tracking.
112
+ * Tracks what upgrade tasks were last submitted to avoid redundant scheduler calls.
113
+ */
114
+ #upgradeState = null;
115
+ /**
116
+ * Standalone upgrade controller for elements without a timegroup.
117
+ */
118
+ #standaloneUpgradeController = null;
119
+ /**
120
+ * Current rendition being displayed (for observability).
121
+ */
122
+ #currentRenditionId = void 0;
123
+ /**
124
+ * Get the current rendition being displayed.
125
+ * @public
126
+ */
127
+ get currentRenditionId() {
128
+ return this.#currentRenditionId;
129
+ }
130
+ /**
88
131
  * Query readiness state for a given time.
89
132
  * @implements FrameRenderable
90
- *
133
+ *
91
134
  * Note: The timeMs parameter is the root timegroup's time. We check against
92
135
  * this.currentSourceTimeMs since that's what we cache in prepareFrame.
93
136
  */
@@ -103,7 +146,7 @@ let EFVideo = class EFVideo$1 extends TWMixin(EFMedia) {
103
146
  /**
104
147
  * Async preparation - seeks video and caches the sample.
105
148
  * @implements FrameRenderable
106
- *
149
+ *
107
150
  * Note: The timeMs parameter is the root timegroup's time. We ignore it and
108
151
  * use this.currentSourceTimeMs instead, which accounts for:
109
152
  * - Our position within the parent timegroup (ownCurrentTimeMs)
@@ -111,11 +154,13 @@ let EFVideo = class EFVideo$1 extends TWMixin(EFMedia) {
111
154
  */
112
155
  async prepareFrame(_timeMs, signal) {
113
156
  signal.throwIfAborted();
157
+ this.unifiedVideoSeekTask.begin();
114
158
  const sourceTimeMs = this.currentSourceTimeMs;
115
159
  const mediaEngine = await this.getMediaEngine(signal);
116
160
  if (!mediaEngine) {
117
161
  this.#cachedVideoSample = void 0;
118
162
  this.#cachedVideoSampleTimeMs = sourceTimeMs;
163
+ this.unifiedVideoSeekTask.complete(void 0);
119
164
  return;
120
165
  }
121
166
  signal.throwIfAborted();
@@ -124,18 +169,23 @@ let EFVideo = class EFVideo$1 extends TWMixin(EFMedia) {
124
169
  signal.throwIfAborted();
125
170
  this.#cachedVideoSample = videoSample;
126
171
  this.#cachedVideoSampleTimeMs = sourceTimeMs;
172
+ this.unifiedVideoSeekTask.complete(videoSample);
127
173
  } catch (error) {
128
- if (error instanceof DOMException && error.name === "AbortError") throw error;
174
+ if (error instanceof DOMException && error.name === "AbortError") {
175
+ this.unifiedVideoSeekTask.abort();
176
+ throw error;
177
+ }
129
178
  console.warn(`Video seek error at ${sourceTimeMs}ms:`, error);
130
179
  this.#cachedVideoSample = void 0;
131
180
  this.#cachedVideoSampleTimeMs = sourceTimeMs;
181
+ this.unifiedVideoSeekTask.complete(void 0);
132
182
  }
133
183
  }
134
184
  /**
135
185
  * Synchronous render - paints cached video sample to canvas.
136
186
  * @implements FrameRenderable
137
- *
138
- * Note: The timeMs parameter is the root timegroup's time. We use
187
+ *
188
+ * Note: The timeMs parameter is the root timegroup's time. We use
139
189
  * this.currentSourceTimeMs to match what prepareFrame cached.
140
190
  */
141
191
  renderFrame(_timeMs) {
@@ -152,7 +202,7 @@ let EFVideo = class EFVideo$1 extends TWMixin(EFMedia) {
152
202
  }
153
203
  /**
154
204
  * Fetch video sample for a given time.
155
- *
205
+ *
156
206
  * Uses a quality routing strategy:
157
207
  * - In production rendering: always use main (full quality) track
158
208
  * - In preview mode: try scrub track first for faster scrubbing, fall back to main
@@ -162,13 +212,24 @@ let EFVideo = class EFVideo$1 extends TWMixin(EFMedia) {
162
212
  const mainRendition = mediaEngine.videoRendition;
163
213
  if (mainRendition) {
164
214
  const mainSegmentId = mediaEngine.computeSegmentId(desiredSeekTimeMs, mainRendition);
165
- if (mainSegmentId !== void 0 && mediaEngine.isSegmentCached(mainSegmentId, mainRendition)) return this.#getMainVideoSampleForFrame(mediaEngine, desiredSeekTimeMs, signal);
215
+ if (mainSegmentId !== void 0 && mediaEngine.isSegmentCached(mainSegmentId, mainRendition)) {
216
+ this.#currentRenditionId = "main";
217
+ return this.#getMainVideoSampleForFrame(mediaEngine, desiredSeekTimeMs, signal);
218
+ }
219
+ }
220
+ if (this.isInProductionRenderingMode()) {
221
+ this.#currentRenditionId = "main";
222
+ return this.#getMainVideoSampleForFrame(mediaEngine, desiredSeekTimeMs, signal);
166
223
  }
167
- if (this.isInProductionRenderingMode()) return this.#getMainVideoSampleForFrame(mediaEngine, desiredSeekTimeMs, signal);
168
224
  if (mediaEngine.getScrubVideoRendition?.()) {
169
225
  const scrubSample = await this.#getScrubVideoSampleForFrame(mediaEngine, desiredSeekTimeMs, signal);
170
- if (scrubSample) return scrubSample;
226
+ if (scrubSample) {
227
+ this.#currentRenditionId = "scrub";
228
+ this.#maybeScheduleQualityUpgrade(mediaEngine, desiredSeekTimeMs);
229
+ return scrubSample;
230
+ }
171
231
  }
232
+ this.#currentRenditionId = "main";
172
233
  return this.#getMainVideoSampleForFrame(mediaEngine, desiredSeekTimeMs, signal);
173
234
  }
174
235
  /**
@@ -188,7 +249,11 @@ let EFVideo = class EFVideo$1 extends TWMixin(EFMedia) {
188
249
  let initSegment;
189
250
  let mediaSegment;
190
251
  try {
191
- [initSegment, mediaSegment] = await Promise.all([mediaEngine.fetchInitSegment(scrubRenditionWithSrc, signal), mediaEngine.fetchMediaSegment(segmentId, scrubRenditionWithSrc, signal)]);
252
+ const initP = mediaEngine.fetchInitSegment(scrubRenditionWithSrc, signal);
253
+ const mediaP = mediaEngine.fetchMediaSegment(segmentId, scrubRenditionWithSrc, signal);
254
+ initP.catch(() => {});
255
+ mediaP.catch(() => {});
256
+ [initSegment, mediaSegment] = await Promise.all([initP, mediaP]);
192
257
  } catch (error) {
193
258
  if (error instanceof DOMException && error.name === "AbortError") throw error;
194
259
  return;
@@ -225,7 +290,11 @@ let EFVideo = class EFVideo$1 extends TWMixin(EFMedia) {
225
290
  let initSegment;
226
291
  let mediaSegment;
227
292
  try {
228
- [initSegment, mediaSegment] = await Promise.all([mediaEngine.fetchInitSegment(videoRendition, signal), mediaEngine.fetchMediaSegment(segmentId, videoRendition, signal)]);
293
+ const initP = mediaEngine.fetchInitSegment(videoRendition, signal);
294
+ const mediaP = mediaEngine.fetchMediaSegment(segmentId, videoRendition, signal);
295
+ initP.catch(() => {});
296
+ mediaP.catch(() => {});
297
+ [initSegment, mediaSegment] = await Promise.all([initP, mediaP]);
229
298
  } catch (error) {
230
299
  if (error instanceof DOMException && error.name === "AbortError") throw error;
231
300
  if (error instanceof Error && (error.message.includes("401") || error.message.includes("UNAUTHORIZED") || error.message.includes("Failed to fetch") || error.message.includes("File not found") || error.message.includes("Media segment not found") || error.message.includes("Init segment not found") || error.message.includes("Track not found"))) return;
@@ -249,7 +318,7 @@ let EFVideo = class EFVideo$1 extends TWMixin(EFMedia) {
249
318
  const videoTrack = await mainInput.getFirstVideoTrack();
250
319
  if (!videoTrack) return;
251
320
  signal.throwIfAborted();
252
- return mainInput.seek(videoTrack.id, desiredSeekTimeMs);
321
+ return await mainInput.seek(videoTrack.id, desiredSeekTimeMs);
253
322
  }
254
323
  /**
255
324
  * Delayed loading state manager for user feedback
@@ -258,21 +327,25 @@ let EFVideo = class EFVideo$1 extends TWMixin(EFMedia) {
258
327
  constructor() {
259
328
  super();
260
329
  this.canvasRef = createRef();
261
- this.videoBufferDurationMs = 1e4;
262
- this.maxVideoBufferFetches = 2;
263
- this.enableVideoBuffering = true;
330
+ this.unifiedVideoSeekTask = new VideoSeekTask();
264
331
  this.loadingState = {
265
332
  isLoading: false,
266
333
  operation: null,
267
334
  message: ""
268
335
  };
269
- this.frameTask = createFrameTaskWrapper(this, { getTimeMs: () => this.desiredSeekTimeMs });
270
336
  this.#delayedLoadingState = new DelayedLoadingState(250, (isLoading, message) => {
271
337
  this.setLoadingState(isLoading, null, message);
272
338
  });
273
339
  }
274
340
  updated(changedProperties) {
275
341
  super.updated(changedProperties);
342
+ if (changedProperties.has("src") || changedProperties.has("fileId")) this.#invalidateUpgradeState("src-change");
343
+ if ([
344
+ "_trimStartMs",
345
+ "_trimEndMs",
346
+ "_sourceInMs",
347
+ "_sourceOutMs"
348
+ ].some((prop) => changedProperties.has(prop))) this.#invalidateUpgradeState("bounds-change");
276
349
  }
277
350
  render() {
278
351
  return html`
@@ -320,7 +393,6 @@ let EFVideo = class EFVideo$1 extends TWMixin(EFMedia) {
320
393
  }
321
394
  /**
322
395
  * Paint the current video frame to canvas
323
- * Called by frameTask after seek is complete
324
396
  */
325
397
  paint(seekToMs, parentSpan) {
326
398
  const parentContext = parentSpan ? trace.setSpan(context.active(), parentSpan) : void 0;
@@ -356,14 +428,7 @@ let EFVideo = class EFVideo$1 extends TWMixin(EFMedia) {
356
428
  } catch (error) {
357
429
  console.warn("Video pipeline error:", error);
358
430
  }
359
- if (!isProductionRendering) {
360
- const isInRenderClone = !!this.closest(".ef-render-clone-container");
361
- if (isInRenderClone) span.setAttribute("renderClone", true);
362
- if (!isInRenderClone && (!this.rootTimegroup || this.rootTimegroup.currentTimeMs === 0 && this.desiredSeekTimeMs === 0)) {
363
- span.setAttribute("skipped", "preview-initialization");
364
- return;
365
- }
366
- } else {
431
+ if (!isProductionRendering) {} else {
367
432
  if (!this.rootTimegroup) {
368
433
  span.setAttribute("skipped", "no-root-timegroup");
369
434
  return;
@@ -382,7 +447,7 @@ let EFVideo = class EFVideo$1 extends TWMixin(EFMedia) {
382
447
  */
383
448
  clearCanvas() {
384
449
  if (!this.canvasElement) return;
385
- const ctx = this.canvasElement.getContext("2d");
450
+ const ctx = this.canvasElement.getContext("2d", { willReadFrequently: true });
386
451
  if (ctx) ctx.clearRect(0, 0, this.canvasElement.width, this.canvasElement.height);
387
452
  }
388
453
  /**
@@ -408,22 +473,26 @@ let EFVideo = class EFVideo$1 extends TWMixin(EFMedia) {
408
473
  }
409
474
  const t1 = performance.now();
410
475
  span.setAttribute("getCanvasMs", Math.round((t1 - t0) * 100) / 100);
411
- const ctx = this.canvasElement.getContext("2d");
476
+ const ctx = this.canvasElement.getContext("2d", { willReadFrequently: true });
412
477
  const t2 = performance.now();
413
478
  span.setAttribute("getCtxMs", Math.round((t2 - t1) * 100) / 100);
414
479
  if (!ctx) {
415
480
  log("trace: displayFrame aborted - no canvas context");
416
481
  throw new Error(`Frame display failed: Unable to get 2D canvas context at time ${seekToMs}ms. This may indicate a browser compatibility issue or canvas corruption.`);
417
482
  }
483
+ const frameWidth = frame.displayWidth;
484
+ const frameHeight = frame.displayHeight;
418
485
  let resized = false;
419
- if (frame?.codedWidth && frame?.codedHeight) {
420
- if (this.canvasElement.width !== frame.codedWidth || this.canvasElement.height !== frame.codedHeight) {
486
+ if (frameWidth && frameHeight) {
487
+ if (frameWidth > this.canvasElement.width || frameHeight > this.canvasElement.height) {
488
+ const newWidth = Math.max(this.canvasElement.width, frameWidth);
489
+ const newHeight = Math.max(this.canvasElement.height, frameHeight);
421
490
  log("trace: updating canvas dimensions", {
422
- width: frame.codedWidth,
423
- height: frame.codedHeight
491
+ width: newWidth,
492
+ height: newHeight
424
493
  });
425
- this.canvasElement.width = frame.codedWidth;
426
- this.canvasElement.height = frame.codedHeight;
494
+ this.canvasElement.width = newWidth;
495
+ this.canvasElement.height = newHeight;
427
496
  resized = true;
428
497
  const t3 = performance.now();
429
498
  span.setAttribute("resizeMs", Math.round((t3 - t2) * 100) / 100);
@@ -462,78 +531,41 @@ let EFVideo = class EFVideo$1 extends TWMixin(EFMedia) {
462
531
  return (this.rootTimegroup?.currentTimeMs || 0) >= renderStartTime;
463
532
  }
464
533
  /**
465
- * Legacy getter for fragment index task
466
- * Still used by EFCaptions - maps to frameTask
467
- */
468
- get fragmentIndexTask() {
469
- return this.frameTask;
470
- }
471
- #pendingFrameReadyTime = null;
472
- #pendingFrameReadyPromise = null;
473
- /**
474
- * Helper method for tests: wait for the current frame to be ready
475
- * This encapsulates the complexity of ensuring the video has updated
476
- * and its frameTask has completed.
534
+ * Get a decoded VideoFrame at a specific source media timestamp.
535
+ * Returns a standard WebCodecs VideoFrame caller MUST call .close() when done.
477
536
  *
478
- * @returns Promise that resolves when the frame is ready
479
- */
480
- async waitForFrameReady() {
481
- const currentTime = this.currentSourceTimeMs;
482
- if (this.desiredSeekTimeMs !== currentTime) this.desiredSeekTimeMs = currentTime;
483
- if (this.#pendingFrameReadyTime === currentTime && this.#pendingFrameReadyPromise) return this.#pendingFrameReadyPromise;
484
- this.#pendingFrameReadyTime = currentTime;
485
- this.#pendingFrameReadyPromise = this.#doWaitForFrameReady(currentTime);
486
- try {
487
- await this.#pendingFrameReadyPromise;
488
- } finally {
489
- if (this.#pendingFrameReadyTime === currentTime) {
490
- this.#pendingFrameReadyTime = null;
491
- this.#pendingFrameReadyPromise = null;
492
- }
493
- }
494
- }
495
- async #doWaitForFrameReady(_targetTimeMs) {
496
- await this.updateComplete;
497
- try {
498
- await this.frameTask.run();
499
- } catch (error) {
500
- if (error instanceof DOMException && error.name === "AbortError" || error instanceof Error && (error.name === "AbortError" || error.message?.includes("signal is aborted") || error.message?.includes("The user aborted a request"))) return;
501
- throw error;
502
- }
503
- }
504
- /**
505
- * Capture a video frame directly at a source media timestamp.
506
- * Bypasses the frameTask system - designed for export/rendering.
507
- * Does NOT paint to the element's internal canvas.
508
- *
509
- * Uses the same routing logic as unified video system:
537
+ * Uses the same routing logic as the unified video system:
510
538
  * - "auto": main track for production rendering, follows normal routing otherwise
511
539
  * - "scrub": force low-res scrub track (for thumbnails)
512
540
  * - "main": force full-quality main track
513
- *
541
+ *
514
542
  * @param sourceTimeMs - Timestamp in source media coordinates (not timeline)
515
- * @param options - Capture options including quality and abort signal
516
- * @returns Frame data for serialization
543
+ * @param options - Quality and abort signal
544
+ * @returns VideoFrame that the caller must close()
517
545
  * @public
518
546
  */
519
- async captureFrameAtSourceTime(sourceTimeMs, options = {}) {
520
- const { quality = "auto", signal } = options;
521
- signal?.throwIfAborted();
547
+ async getVideoFrameAtSourceTime(sourceTimeMs, options = {}) {
548
+ const { quality = "auto", signal: providedSignal } = options;
549
+ const signal = providedSignal ?? new AbortController().signal;
550
+ signal.throwIfAborted();
522
551
  const mediaEngine = await this.getMediaEngine(signal);
523
- signal?.throwIfAborted();
552
+ signal.throwIfAborted();
524
553
  if (!mediaEngine) throw new Error("No media engine available for frame capture");
525
554
  const useMainTrack = quality === "main" || quality === "auto" && this.isInProductionRenderingMode();
526
555
  let videoSample;
527
556
  const { BufferedSeekingInput } = await import("./EFMedia/BufferedSeekingInput.js");
528
- signal?.throwIfAborted();
557
+ signal.throwIfAborted();
529
558
  if (useMainTrack) {
530
559
  const videoRendition = mediaEngine.getVideoRendition?.() || mediaEngine.videoRendition;
531
560
  if (!videoRendition) throw new Error("No video rendition available");
532
561
  const segmentId = mediaEngine.computeSegmentId(sourceTimeMs, videoRendition);
533
562
  if (segmentId === void 0) throw new Error(`Cannot compute segment ID for time ${sourceTimeMs}ms`);
534
563
  const seekingInput = await mainVideoInputCache.getOrCreateInput(mediaEngine.src, segmentId, videoRendition.id, async () => {
535
- const fetchSignal = signal ?? new AbortController().signal;
536
- const [initSegment, mediaSegment] = await Promise.all([mediaEngine.fetchInitSegment(videoRendition, fetchSignal), mediaEngine.fetchMediaSegment(segmentId, videoRendition, fetchSignal)]);
564
+ const initP = mediaEngine.fetchInitSegment(videoRendition, signal);
565
+ const mediaP = mediaEngine.fetchMediaSegment(segmentId, videoRendition, signal);
566
+ initP.catch(() => {});
567
+ mediaP.catch(() => {});
568
+ const [initSegment, mediaSegment] = await Promise.all([initP, mediaP]);
537
569
  if (!initSegment || !mediaSegment) return;
538
570
  return new BufferedSeekingInput(await new Blob([initSegment, mediaSegment]).arrayBuffer(), {
539
571
  videoBufferSize: EFMedia.VIDEO_SAMPLE_BUFFER_SIZE,
@@ -541,16 +573,16 @@ let EFVideo = class EFVideo$1 extends TWMixin(EFMedia) {
541
573
  startTimeOffsetMs: videoRendition.startTimeOffsetMs
542
574
  });
543
575
  });
544
- signal?.throwIfAborted();
576
+ signal.throwIfAborted();
545
577
  if (!seekingInput) throw new Error(`Failed to fetch video segments for time ${sourceTimeMs}ms`);
546
578
  const videoTrack = await seekingInput.getFirstVideoTrack();
547
- signal?.throwIfAborted();
579
+ signal.throwIfAborted();
548
580
  if (!videoTrack) throw new Error("No video track found in segment");
549
581
  videoSample = await seekingInput.seek(videoTrack.id, sourceTimeMs);
550
- signal?.throwIfAborted();
582
+ signal.throwIfAborted();
551
583
  } else {
552
584
  const scrubRendition = mediaEngine.getScrubVideoRendition?.();
553
- if (!scrubRendition) return this.captureFrameAtSourceTime(sourceTimeMs, {
585
+ if (!scrubRendition) return this.getVideoFrameAtSourceTime(sourceTimeMs, {
554
586
  quality: "main",
555
587
  signal
556
588
  });
@@ -561,8 +593,11 @@ let EFVideo = class EFVideo$1 extends TWMixin(EFMedia) {
561
593
  const segmentId = mediaEngine.computeSegmentId(sourceTimeMs, scrubRenditionWithSrc);
562
594
  if (segmentId === void 0) throw new Error(`Cannot compute scrub segment ID for time ${sourceTimeMs}ms`);
563
595
  const seekingInput = await scrubInputCache.getOrCreateInput(mediaEngine.src, segmentId, async () => {
564
- const scrubFetchSignal = signal ?? new AbortController().signal;
565
- const [initSegment, mediaSegment] = await Promise.all([mediaEngine.fetchInitSegment(scrubRenditionWithSrc, scrubFetchSignal), mediaEngine.fetchMediaSegment(segmentId, scrubRenditionWithSrc, scrubFetchSignal)]);
596
+ const initP = mediaEngine.fetchInitSegment(scrubRenditionWithSrc, signal);
597
+ const mediaP = mediaEngine.fetchMediaSegment(segmentId, scrubRenditionWithSrc, signal);
598
+ initP.catch(() => {});
599
+ mediaP.catch(() => {});
600
+ const [initSegment, mediaSegment] = await Promise.all([initP, mediaP]);
566
601
  if (!initSegment || !mediaSegment) return;
567
602
  return new BufferedSeekingInput(await new Blob([initSegment, mediaSegment]).arrayBuffer(), {
568
603
  videoBufferSize: EFMedia.VIDEO_SAMPLE_BUFFER_SIZE,
@@ -570,41 +605,59 @@ let EFVideo = class EFVideo$1 extends TWMixin(EFMedia) {
570
605
  startTimeOffsetMs: scrubRendition.startTimeOffsetMs
571
606
  });
572
607
  });
573
- signal?.throwIfAborted();
574
- if (!seekingInput) return this.captureFrameAtSourceTime(sourceTimeMs, {
608
+ signal.throwIfAborted();
609
+ if (!seekingInput) return this.getVideoFrameAtSourceTime(sourceTimeMs, {
575
610
  quality: "main",
576
611
  signal
577
612
  });
578
613
  const videoTrack = await seekingInput.getFirstVideoTrack();
579
- signal?.throwIfAborted();
580
- if (!videoTrack) return this.captureFrameAtSourceTime(sourceTimeMs, {
614
+ signal.throwIfAborted();
615
+ if (!videoTrack) return this.getVideoFrameAtSourceTime(sourceTimeMs, {
581
616
  quality: "main",
582
617
  signal
583
618
  });
584
619
  videoSample = await seekingInput.seek(videoTrack.id, sourceTimeMs);
585
- signal?.throwIfAborted();
620
+ signal.throwIfAborted();
586
621
  }
587
622
  if (!videoSample) throw new Error(`No video sample found at ${sourceTimeMs}ms`);
588
- const videoFrame = videoSample.toVideoFrame();
623
+ return videoSample.toVideoFrame();
624
+ }
625
+ /**
626
+ * Capture a video frame directly at a source media timestamp.
627
+ * Designed for export/rendering.
628
+ * Does NOT paint to the element's internal canvas.
629
+ *
630
+ * Uses the same routing logic as unified video system:
631
+ * - "auto": main track for production rendering, follows normal routing otherwise
632
+ * - "scrub": force low-res scrub track (for thumbnails)
633
+ * - "main": force full-quality main track
634
+ *
635
+ * @param sourceTimeMs - Timestamp in source media coordinates (not timeline)
636
+ * @param options - Capture options including quality and abort signal
637
+ * @returns Frame data for serialization
638
+ * @public
639
+ */
640
+ async captureFrameAtSourceTime(sourceTimeMs, options = {}) {
641
+ const videoFrame = await this.getVideoFrameAtSourceTime(sourceTimeMs, options);
589
642
  try {
590
- signal?.throwIfAborted();
643
+ options.signal?.throwIfAborted();
591
644
  const canvas = new OffscreenCanvas(videoFrame.codedWidth, videoFrame.codedHeight);
592
645
  const ctx = canvas.getContext("2d");
593
646
  if (!ctx) throw new Error("Failed to get 2d context from OffscreenCanvas");
594
647
  ctx.drawImage(videoFrame, 0, 0);
595
- signal?.throwIfAborted();
648
+ options.signal?.throwIfAborted();
596
649
  const blob = await canvas.convertToBlob({
597
650
  type: "image/jpeg",
598
651
  quality: .92
599
652
  });
600
- signal?.throwIfAborted();
653
+ options.signal?.throwIfAborted();
601
654
  const dataUrl = await new Promise((resolve, reject) => {
602
655
  const reader = new FileReader();
603
656
  reader.onload = () => resolve(reader.result);
604
657
  reader.onerror = reject;
605
658
  reader.readAsDataURL(blob);
606
659
  });
607
- signal?.throwIfAborted();
660
+ options.signal?.throwIfAborted();
608
661
  return {
609
662
  dataUrl,
610
663
  width: videoFrame.codedWidth,
@@ -621,11 +674,12 @@ let EFVideo = class EFVideo$1 extends TWMixin(EFMedia) {
621
674
  *
622
675
  * @param timestamps - Array of timestamps (in ms) that will be captured
623
676
  * @param onProgress - Optional callback for loading progress
677
+ * @param signal - Optional AbortSignal for cancellation
624
678
  * @returns Promise that resolves when all segments are cached
625
679
  * @public
626
680
  */
627
- async prefetchScrubSegments(timestamps, onProgress) {
628
- const mediaEngine = await this.getMediaEngine();
681
+ async prefetchScrubSegments(timestamps, onProgress, signal) {
682
+ const mediaEngine = await this.getMediaEngine(signal);
629
683
  if (!mediaEngine) {
630
684
  log("prefetchScrubSegments: no media engine available");
631
685
  return;
@@ -666,8 +720,9 @@ let EFVideo = class EFVideo$1 extends TWMixin(EFMedia) {
666
720
  bubbles: true,
667
721
  composed: true
668
722
  }));
723
+ const fetchSignal = signal ?? new AbortController().signal;
669
724
  try {
670
- await mediaEngine.fetchMediaSegment(firstSegmentId, scrubRenditionWithSrc);
725
+ await mediaEngine.fetchMediaSegment(firstSegmentId, scrubRenditionWithSrc, fetchSignal);
671
726
  log(`prefetchScrubSegments: scrub track loaded`);
672
727
  } catch (error) {
673
728
  log(`prefetchScrubSegments: failed to load scrub track`, error);
@@ -687,57 +742,85 @@ let EFVideo = class EFVideo$1 extends TWMixin(EFMedia) {
687
742
  log(`prefetchScrubSegments: complete`);
688
743
  }
689
744
  /**
690
- * Pre-fetch main video segments for given timestamps.
691
- * This ensures segments are cached for fast seeking during video export.
692
- *
693
- * @param timestamps - Array of timestamps (in ms) that will be captured
694
- * @param onProgress - Optional callback for loading progress
695
- * @returns Promise that resolves when all segments are cached
696
- * @public
745
+ * Maybe schedule quality upgrade tasks for this element.
746
+ * Called when returning a scrub sample - checks if state has changed and submits tasks.
697
747
  */
698
- async prefetchMainVideoSegments(timestamps, onProgress) {
699
- const mediaEngine = await this.getMediaEngine();
700
- if (!mediaEngine) {
701
- log("prefetchMainVideoSegments: no media engine available");
702
- return;
703
- }
704
- const videoRendition = mediaEngine.getVideoRendition?.() || mediaEngine.videoRendition;
705
- if (!videoRendition) {
706
- log("prefetchMainVideoSegments: no video rendition available");
707
- return;
708
- }
709
- const segmentIds = /* @__PURE__ */ new Set();
710
- for (const ts of timestamps) {
711
- const segmentId = mediaEngine.computeSegmentId(ts, videoRendition);
712
- if (segmentId !== void 0) segmentIds.add(segmentId);
713
- }
714
- if (segmentIds.size === 0) {
715
- log("prefetchMainVideoSegments: no segments to prefetch");
716
- return;
717
- }
718
- const uncachedSegmentIds = [];
719
- for (const segmentId of segmentIds) if (!mediaEngine.isSegmentCached(segmentId, videoRendition)) uncachedSegmentIds.push(segmentId);
720
- if (uncachedSegmentIds.length === 0) {
721
- log("prefetchMainVideoSegments: all segments already cached");
722
- onProgress?.(segmentIds.size, segmentIds.size);
723
- return;
724
- }
725
- log(`prefetchMainVideoSegments: fetching ${uncachedSegmentIds.length} segments...`);
726
- try {
727
- await mediaEngine.fetchInitSegment(videoRendition);
728
- } catch (error) {
729
- log("prefetchMainVideoSegments: failed to fetch init segment", error);
730
- return;
731
- }
732
- let loaded = segmentIds.size - uncachedSegmentIds.length;
733
- for (const segmentId of uncachedSegmentIds) try {
734
- await mediaEngine.fetchMediaSegment(segmentId, videoRendition);
735
- loaded++;
736
- onProgress?.(loaded, segmentIds.size);
737
- } catch (error) {
738
- log(`prefetchMainVideoSegments: failed to fetch segment ${segmentId}`, error);
748
+ #maybeScheduleQualityUpgrade(mediaEngine, sourceTimeMs) {
749
+ const mainRendition = mediaEngine.videoRendition;
750
+ if (!mainRendition) return;
751
+ const segmentId = mediaEngine.computeSegmentId(sourceTimeMs, mainRendition);
752
+ if (segmentId === void 0) return;
753
+ const startTimeMs = this.startTimeMs;
754
+ if (!(this.#upgradeState === null || this.#upgradeState.segmentId !== segmentId || this.#upgradeState.startTimeMs !== startTimeMs)) return;
755
+ const segments = this.#computeLookaheadSegments(mediaEngine, sourceTimeMs, mainRendition);
756
+ if (segments.length === 0) return;
757
+ const tasks = segments.map((seg) => ({
758
+ key: `${this.id}:${seg.segmentId}:${mainRendition.id}`,
759
+ fetch: async (signal) => {
760
+ await mediaEngine.fetchInitSegment(mainRendition, signal);
761
+ await mediaEngine.fetchMediaSegment(seg.segmentId, mainRendition, signal);
762
+ },
763
+ deadlineMs: seg.deadlineMs,
764
+ owner: this.id
765
+ }));
766
+ const scheduler = this.rootTimegroup?.qualityUpgradeScheduler;
767
+ if (scheduler) scheduler.replaceForOwner(this.id, tasks);
768
+ else this.#fetchStandalone(tasks);
769
+ this.#upgradeState = {
770
+ sourceTimeMs,
771
+ segmentId,
772
+ startTimeMs,
773
+ submittedKeys: new Set(tasks.map((t) => t.key))
774
+ };
775
+ }
776
+ /**
777
+ * Compute lookahead segments with deadlines in timeline space.
778
+ */
779
+ #computeLookaheadSegments(mediaEngine, currentSourceTimeMs, rendition, maxLookahead = 5) {
780
+ const results = [];
781
+ const playheadMs = this.rootTimegroup?.currentTimeMs ?? 0;
782
+ const seen = /* @__PURE__ */ new Set();
783
+ let probeTimeMs = currentSourceTimeMs;
784
+ while (seen.size < maxLookahead) {
785
+ const segmentId = mediaEngine.computeSegmentId(probeTimeMs, rendition);
786
+ if (segmentId === void 0) break;
787
+ if (seen.has(segmentId)) break;
788
+ seen.add(segmentId);
789
+ if (!mediaEngine.isSegmentCached(segmentId, rendition)) {
790
+ const deadlineMs = playheadMs + (probeTimeMs - currentSourceTimeMs);
791
+ results.push({
792
+ segmentId,
793
+ deadlineMs
794
+ });
795
+ }
796
+ const thisDuration = rendition.segmentDurationsMs?.[segmentId - 1] ?? rendition.segmentDurationMs ?? 2e3;
797
+ probeTimeMs += thisDuration;
739
798
  }
740
- log(`prefetchMainVideoSegments: complete (${loaded}/${segmentIds.size} segments)`);
799
+ return results;
800
+ }
801
+ /**
802
+ * Standalone mode: fetch tasks sequentially without scheduler.
803
+ */
804
+ #fetchStandalone(tasks) {
805
+ this.#standaloneUpgradeController?.abort();
806
+ this.#standaloneUpgradeController = new AbortController();
807
+ const signal = this.#standaloneUpgradeController.signal;
808
+ (async () => {
809
+ for (const task of tasks) {
810
+ if (signal.aborted) break;
811
+ try {
812
+ await task.fetch(signal);
813
+ } catch {}
814
+ }
815
+ if (!signal.aborted) this.playbackController?.runThrottledFrameTask();
816
+ })().catch(() => {});
817
+ }
818
+ /**
819
+ * Invalidate upgrade state and optionally cancel queued tasks.
820
+ */
821
+ #invalidateUpgradeState(reason) {
822
+ if (reason === "src-change" || reason === "disconnect") this.rootTimegroup?.qualityUpgradeScheduler?.cancelForOwner(this.id);
823
+ this.#upgradeState = null;
741
824
  }
742
825
  /**
743
826
  * Clean up resources when component is disconnected
@@ -745,6 +828,9 @@ let EFVideo = class EFVideo$1 extends TWMixin(EFMedia) {
745
828
  disconnectedCallback() {
746
829
  super.disconnectedCallback();
747
830
  this.#delayedLoadingState.clearAllLoading();
831
+ this.#invalidateUpgradeState("disconnect");
832
+ this.#standaloneUpgradeController?.abort();
833
+ this.#standaloneUpgradeController = null;
748
834
  }
749
835
  didBecomeRoot() {
750
836
  super.didBecomeRoot();
@@ -766,19 +852,20 @@ let EFVideo = class EFVideo$1 extends TWMixin(EFMedia) {
766
852
  height: canvas.height
767
853
  };
768
854
  }
855
+ /**
856
+ * Render this video element to an MP4 using the direct video-to-video fast path.
857
+ * Bypasses DOM serialization — decodes frames directly and re-encodes to MP4.
858
+ * Respects trim, CSS filter, and opacity.
859
+ *
860
+ * @param options - Rendering options (fps, codec, bitrate, etc.)
861
+ * @returns Promise resolving to video buffer (if returnBuffer), or undefined
862
+ * @public
863
+ */
864
+ async renderToVideo(options) {
865
+ const { renderVideoToVideo } = await import("../preview/renderVideoToVideo.js");
866
+ return renderVideoToVideo(this, options);
867
+ }
769
868
  };
770
- __decorate([property({
771
- type: Number,
772
- attribute: "video-buffer-duration"
773
- })], EFVideo.prototype, "videoBufferDurationMs", void 0);
774
- __decorate([property({
775
- type: Number,
776
- attribute: "max-video-buffer-fetches"
777
- })], EFVideo.prototype, "maxVideoBufferFetches", void 0);
778
- __decorate([property({
779
- type: Boolean,
780
- attribute: "enable-video-buffering"
781
- })], EFVideo.prototype, "enableVideoBuffering", void 0);
782
869
  __decorate([state()], EFVideo.prototype, "loadingState", void 0);
783
870
  EFVideo = __decorate([customElement("ef-video")], EFVideo);
784
871