@editframe/elements 0.33.0-beta → 0.34.5-beta

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 (251) hide show
  1. package/dist/EF_FRAMEGEN.js +5 -3
  2. package/dist/EF_FRAMEGEN.js.map +1 -1
  3. package/dist/_virtual/{_@oxc-project_runtime@0.94.0 → _@oxc-project_runtime@0.95.0}/helpers/decorate.js +1 -1
  4. package/dist/canvas/EFCanvas.d.ts +7 -4
  5. package/dist/canvas/EFCanvas.js +1 -1
  6. package/dist/canvas/EFCanvasItem.d.ts +4 -4
  7. package/dist/canvas/EFCanvasItem.js +1 -1
  8. package/dist/canvas/overlays/SelectionOverlay.d.ts +95 -0
  9. package/dist/canvas/overlays/SelectionOverlay.js +1 -1
  10. package/dist/canvas/selection/SelectionController.js +7 -11
  11. package/dist/canvas/selection/SelectionController.js.map +1 -1
  12. package/dist/elements/EFAudio.d.ts +25 -7
  13. package/dist/elements/EFAudio.js +31 -61
  14. package/dist/elements/EFAudio.js.map +1 -1
  15. package/dist/elements/EFCaptions.d.ts +65 -52
  16. package/dist/elements/EFCaptions.js +186 -400
  17. package/dist/elements/EFCaptions.js.map +1 -1
  18. package/dist/elements/EFImage.d.ts +34 -6
  19. package/dist/elements/EFImage.js +114 -79
  20. package/dist/elements/EFImage.js.map +1 -1
  21. package/dist/elements/EFMedia/AssetIdMediaEngine.js +17 -17
  22. package/dist/elements/EFMedia/AssetIdMediaEngine.js.map +1 -1
  23. package/dist/elements/EFMedia/AssetMediaEngine.js +41 -25
  24. package/dist/elements/EFMedia/AssetMediaEngine.js.map +1 -1
  25. package/dist/elements/EFMedia/BaseMediaEngine.js +4 -4
  26. package/dist/elements/EFMedia/BaseMediaEngine.js.map +1 -1
  27. package/dist/elements/EFMedia/BufferedSeekingInput.js +1 -1
  28. package/dist/elements/EFMedia/BufferedSeekingInput.js.map +1 -1
  29. package/dist/elements/EFMedia/JitMediaEngine.js +31 -17
  30. package/dist/elements/EFMedia/JitMediaEngine.js.map +1 -1
  31. package/dist/elements/EFMedia/shared/AudioSpanUtils.js +3 -3
  32. package/dist/elements/EFMedia/shared/AudioSpanUtils.js.map +1 -1
  33. package/dist/elements/EFMedia/videoTasks/ScrubInputCache.js +17 -9
  34. package/dist/elements/EFMedia/videoTasks/ScrubInputCache.js.map +1 -1
  35. package/dist/elements/EFMedia.d.ts +66 -20
  36. package/dist/elements/EFMedia.js +412 -30
  37. package/dist/elements/EFMedia.js.map +1 -1
  38. package/dist/elements/EFPanZoom.d.ts +4 -4
  39. package/dist/elements/EFPanZoom.js +1 -1
  40. package/dist/elements/EFSourceMixin.js +43 -15
  41. package/dist/elements/EFSourceMixin.js.map +1 -1
  42. package/dist/elements/EFSurface.d.ts +23 -10
  43. package/dist/elements/EFSurface.js +64 -22
  44. package/dist/elements/EFSurface.js.map +1 -1
  45. package/dist/elements/EFTemporal.d.ts +8 -2
  46. package/dist/elements/EFTemporal.js +42 -31
  47. package/dist/elements/EFTemporal.js.map +1 -1
  48. package/dist/elements/EFText.d.ts +5 -4
  49. package/dist/elements/EFText.js +11 -2
  50. package/dist/elements/EFText.js.map +1 -1
  51. package/dist/elements/EFTextSegment.d.ts +4 -4
  52. package/dist/elements/EFTextSegment.js +1 -1
  53. package/dist/elements/EFThumbnailStrip.d.ts +4 -4
  54. package/dist/elements/EFThumbnailStrip.js +1 -1
  55. package/dist/elements/EFTimegroup.d.ts +22 -8
  56. package/dist/elements/EFTimegroup.js +203 -115
  57. package/dist/elements/EFTimegroup.js.map +1 -1
  58. package/dist/elements/EFVideo.d.ts +57 -20
  59. package/dist/elements/EFVideo.js +324 -72
  60. package/dist/elements/EFVideo.js.map +1 -1
  61. package/dist/elements/EFWaveform.d.ts +33 -7
  62. package/dist/elements/EFWaveform.js +103 -59
  63. package/dist/elements/EFWaveform.js.map +1 -1
  64. package/dist/elements/renderTemporalAudio.js +14 -3
  65. package/dist/elements/renderTemporalAudio.js.map +1 -1
  66. package/dist/getRenderInfo.d.ts +2 -2
  67. package/dist/gui/ContextMixin.js +1 -1
  68. package/dist/gui/Controllable.d.ts +2 -0
  69. package/dist/gui/EFActiveRootTemporal.d.ts +4 -4
  70. package/dist/gui/EFActiveRootTemporal.js +1 -1
  71. package/dist/gui/EFConfiguration.d.ts +4 -4
  72. package/dist/gui/EFConfiguration.js +1 -1
  73. package/dist/gui/EFControls.d.ts +2 -2
  74. package/dist/gui/EFControls.js +1 -1
  75. package/dist/gui/EFDial.d.ts +4 -4
  76. package/dist/gui/EFDial.js +1 -1
  77. package/dist/gui/EFFilmstrip.d.ts +3 -2
  78. package/dist/gui/EFFilmstrip.js +1 -1
  79. package/dist/gui/EFFitScale.js +1 -1
  80. package/dist/gui/EFFocusOverlay.d.ts +4 -4
  81. package/dist/gui/EFFocusOverlay.js +1 -1
  82. package/dist/gui/EFOverlayItem.d.ts +4 -4
  83. package/dist/gui/EFOverlayItem.js +1 -1
  84. package/dist/gui/EFOverlayLayer.d.ts +4 -4
  85. package/dist/gui/EFOverlayLayer.js +1 -1
  86. package/dist/gui/EFPause.d.ts +4 -4
  87. package/dist/gui/EFPause.js +1 -1
  88. package/dist/gui/EFPlay.d.ts +4 -4
  89. package/dist/gui/EFPlay.js +1 -1
  90. package/dist/gui/EFPreview.d.ts +4 -4
  91. package/dist/gui/EFPreview.js +1 -1
  92. package/dist/gui/EFResizableBox.d.ts +4 -4
  93. package/dist/gui/EFResizableBox.js +1 -1
  94. package/dist/gui/EFScrubber.d.ts +4 -4
  95. package/dist/gui/EFScrubber.js +1 -1
  96. package/dist/gui/EFTimeDisplay.d.ts +4 -4
  97. package/dist/gui/EFTimeDisplay.js +1 -1
  98. package/dist/gui/EFTimelineRuler.d.ts +4 -4
  99. package/dist/gui/EFTimelineRuler.js +1 -1
  100. package/dist/gui/EFToggleLoop.d.ts +4 -4
  101. package/dist/gui/EFToggleLoop.js +1 -1
  102. package/dist/gui/EFTogglePlay.d.ts +4 -4
  103. package/dist/gui/EFTogglePlay.js +1 -1
  104. package/dist/gui/EFTransformHandles.d.ts +4 -4
  105. package/dist/gui/EFTransformHandles.js +1 -1
  106. package/dist/gui/EFWorkbench.d.ts +5 -4
  107. package/dist/gui/EFWorkbench.js +1 -1
  108. package/dist/gui/PlaybackController.d.ts +10 -2
  109. package/dist/gui/PlaybackController.js +52 -30
  110. package/dist/gui/PlaybackController.js.map +1 -1
  111. package/dist/gui/TWMixin.js +1 -1
  112. package/dist/gui/TWMixin.js.map +1 -1
  113. package/dist/gui/TargetOrContextMixin.js +1 -1
  114. package/dist/gui/hierarchy/EFHierarchy.d.ts +4 -4
  115. package/dist/gui/hierarchy/EFHierarchy.js +1 -1
  116. package/dist/gui/hierarchy/EFHierarchyItem.d.ts +3 -3
  117. package/dist/gui/hierarchy/EFHierarchyItem.js +1 -1
  118. package/dist/gui/timeline/EFTimeline.d.ts +6 -2
  119. package/dist/gui/timeline/EFTimeline.js +1 -1
  120. package/dist/gui/timeline/EFTimelineRow.d.ts +57 -0
  121. package/dist/gui/timeline/EFTimelineRow.js +1 -1
  122. package/dist/gui/timeline/TrimHandles.d.ts +4 -4
  123. package/dist/gui/timeline/TrimHandles.js +1 -1
  124. package/dist/gui/timeline/tracks/AudioTrack.d.ts +2 -0
  125. package/dist/gui/timeline/tracks/AudioTrack.js +1 -1
  126. package/dist/gui/timeline/tracks/CaptionsTrack.d.ts +58 -0
  127. package/dist/gui/timeline/tracks/CaptionsTrack.js +1 -1
  128. package/dist/gui/timeline/tracks/HTMLTrack.d.ts +13 -0
  129. package/dist/gui/timeline/tracks/HTMLTrack.js +1 -1
  130. package/dist/gui/timeline/tracks/ImageTrack.d.ts +14 -0
  131. package/dist/gui/timeline/tracks/ImageTrack.js +1 -1
  132. package/dist/gui/timeline/tracks/TextTrack.d.ts +26 -0
  133. package/dist/gui/timeline/tracks/TextTrack.js +1 -1
  134. package/dist/gui/timeline/tracks/TimegroupTrack.d.ts +47 -0
  135. package/dist/gui/timeline/tracks/TimegroupTrack.js +4 -12
  136. package/dist/gui/timeline/tracks/TimegroupTrack.js.map +1 -1
  137. package/dist/gui/timeline/tracks/TrackItem.d.ts +81 -0
  138. package/dist/gui/timeline/tracks/TrackItem.js +1 -1
  139. package/dist/gui/timeline/tracks/VideoTrack.d.ts +25 -0
  140. package/dist/gui/timeline/tracks/VideoTrack.js +1 -1
  141. package/dist/gui/timeline/tracks/WaveformTrack.d.ts +14 -0
  142. package/dist/gui/timeline/tracks/WaveformTrack.js +1 -1
  143. package/dist/gui/timeline/tracks/ensureTrackItemInit.d.ts +1 -0
  144. package/dist/gui/timeline/tracks/preloadTracks.d.ts +9 -0
  145. package/dist/gui/tree/EFTree.d.ts +5 -4
  146. package/dist/gui/tree/EFTree.js +1 -1
  147. package/dist/gui/tree/EFTreeItem.d.ts +4 -4
  148. package/dist/gui/tree/EFTreeItem.js +1 -1
  149. package/dist/index.d.ts +4 -1
  150. package/dist/preview/AdaptiveResolutionTracker.js +6 -14
  151. package/dist/preview/AdaptiveResolutionTracker.js.map +1 -1
  152. package/dist/preview/FrameController.d.ts +123 -0
  153. package/dist/preview/FrameController.js +216 -0
  154. package/dist/preview/FrameController.js.map +1 -0
  155. package/dist/preview/RenderContext.d.ts +1 -0
  156. package/dist/preview/RenderContext.js +193 -0
  157. package/dist/preview/RenderContext.js.map +1 -0
  158. package/dist/preview/encoding/canvasEncoder.js +166 -0
  159. package/dist/preview/encoding/canvasEncoder.js.map +1 -0
  160. package/dist/preview/encoding/mainThreadEncoder.js +39 -0
  161. package/dist/preview/encoding/mainThreadEncoder.js.map +1 -0
  162. package/dist/preview/encoding/types.d.ts +1 -0
  163. package/dist/preview/encoding/workerEncoder.js +58 -0
  164. package/dist/preview/encoding/workerEncoder.js.map +1 -0
  165. package/dist/preview/logger.js +41 -0
  166. package/dist/preview/logger.js.map +1 -0
  167. package/dist/preview/previewTypes.js +11 -10
  168. package/dist/preview/previewTypes.js.map +1 -1
  169. package/dist/preview/renderTimegroupPreview.js +259 -236
  170. package/dist/preview/renderTimegroupPreview.js.map +1 -1
  171. package/dist/preview/renderTimegroupToCanvas.d.ts +5 -0
  172. package/dist/preview/renderTimegroupToCanvas.js +99 -489
  173. package/dist/preview/renderTimegroupToCanvas.js.map +1 -1
  174. package/dist/preview/renderTimegroupToVideo.d.ts +1 -0
  175. package/dist/preview/renderTimegroupToVideo.js +80 -22
  176. package/dist/preview/renderTimegroupToVideo.js.map +1 -1
  177. package/dist/preview/renderers.js.map +1 -1
  178. package/dist/preview/rendering/inlineImages.js +56 -0
  179. package/dist/preview/rendering/inlineImages.js.map +1 -0
  180. package/dist/preview/rendering/renderToImage.d.ts +1 -0
  181. package/dist/preview/rendering/renderToImage.js +120 -0
  182. package/dist/preview/rendering/renderToImage.js.map +1 -0
  183. package/dist/preview/rendering/renderToImageForeignObject.js +135 -0
  184. package/dist/preview/rendering/renderToImageForeignObject.js.map +1 -0
  185. package/dist/preview/rendering/renderToImageNative.d.ts +1 -0
  186. package/dist/preview/rendering/renderToImageNative.js +129 -0
  187. package/dist/preview/rendering/renderToImageNative.js.map +1 -0
  188. package/dist/preview/rendering/svgSerializer.js +43 -0
  189. package/dist/preview/rendering/svgSerializer.js.map +1 -0
  190. package/dist/preview/rendering/types.d.ts +2 -0
  191. package/dist/preview/statsTrackingStrategy.js +3 -1
  192. package/dist/preview/statsTrackingStrategy.js.map +1 -1
  193. package/dist/preview/workers/WorkerPool.js +8 -57
  194. package/dist/preview/workers/WorkerPool.js.map +1 -1
  195. package/dist/render/EFRenderAPI.d.ts +35 -0
  196. package/dist/render/EFRenderAPI.js +1 -0
  197. package/dist/render/EFRenderAPI.js.map +1 -1
  198. package/dist/sandbox/PlaybackControls.d.ts +1 -0
  199. package/dist/sandbox/ScenarioRunner.d.ts +1 -0
  200. package/dist/sandbox/defineSandbox.d.ts +1 -0
  201. package/dist/sandbox/index.d.ts +3 -0
  202. package/dist/style.css +3 -0
  203. package/dist/transcoding/types/index.d.ts +6 -3
  204. package/package.json +2 -3
  205. package/test/EFVideo.framegen.browsertest.ts +8 -1
  206. package/test/profilingPlugin.ts +1 -3
  207. package/test/setup.ts +23 -1
  208. package/dist/EF_INTERACTIVE.js +0 -7
  209. package/dist/EF_INTERACTIVE.js.map +0 -1
  210. package/dist/elements/EFMedia/BufferedSeekingInput.d.ts +0 -50
  211. package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.d.ts +0 -12
  212. package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.js +0 -104
  213. package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.js.map +0 -1
  214. package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.js +0 -168
  215. package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.js.map +0 -1
  216. package/dist/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.js +0 -46
  217. package/dist/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.js.map +0 -1
  218. package/dist/elements/EFMedia/audioTasks/makeAudioInputTask.js +0 -49
  219. package/dist/elements/EFMedia/audioTasks/makeAudioInputTask.js.map +0 -1
  220. package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.js +0 -30
  221. package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.js.map +0 -1
  222. package/dist/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.js +0 -49
  223. package/dist/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.js.map +0 -1
  224. package/dist/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.js +0 -47
  225. package/dist/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.js.map +0 -1
  226. package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.js +0 -140
  227. package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.js.map +0 -1
  228. package/dist/elements/EFMedia/shared/BufferUtils.d.ts +0 -13
  229. package/dist/elements/EFMedia/shared/BufferUtils.js +0 -86
  230. package/dist/elements/EFMedia/shared/BufferUtils.js.map +0 -1
  231. package/dist/elements/EFMedia/shared/MediaTaskUtils.d.ts +0 -17
  232. package/dist/elements/EFMedia/tasks/makeMediaEngineTask.js +0 -90
  233. package/dist/elements/EFMedia/tasks/makeMediaEngineTask.js.map +0 -1
  234. package/dist/elements/EFMedia/videoTasks/makeScrubVideoBufferTask.js +0 -80
  235. package/dist/elements/EFMedia/videoTasks/makeScrubVideoBufferTask.js.map +0 -1
  236. package/dist/elements/EFMedia/videoTasks/makeScrubVideoInitSegmentFetchTask.js +0 -49
  237. package/dist/elements/EFMedia/videoTasks/makeScrubVideoInitSegmentFetchTask.js.map +0 -1
  238. package/dist/elements/EFMedia/videoTasks/makeScrubVideoInputTask.js +0 -58
  239. package/dist/elements/EFMedia/videoTasks/makeScrubVideoInputTask.js.map +0 -1
  240. package/dist/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.js +0 -71
  241. package/dist/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.js.map +0 -1
  242. package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentFetchTask.js +0 -52
  243. package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentFetchTask.js.map +0 -1
  244. package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentIdTask.js +0 -50
  245. package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentIdTask.js.map +0 -1
  246. package/dist/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.js +0 -109
  247. package/dist/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.js.map +0 -1
  248. package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.d.ts +0 -12
  249. package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.js +0 -97
  250. package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.js.map +0 -1
  251. package/dist/elements/SampleBuffer.d.ts +0 -19
@@ -1 +1 @@
1
- {"version":3,"file":"AssetIdMediaEngine.js","names":["data: Record<number, TrackFragmentIndex>","assetId: string","apiHost: string","paths: InitSegmentPaths"],"sources":["../../../src/elements/EFMedia/AssetIdMediaEngine.ts"],"sourcesContent":["import type { TrackFragmentIndex } from \"@editframe/assets\";\nimport type {\n InitSegmentPaths,\n MediaEngine,\n VideoRendition,\n} from \"../../transcoding/types\";\nimport type { UrlGenerator } from \"../../transcoding/utils/UrlGenerator\";\nimport type { EFMedia } from \"../EFMedia\";\nimport { AssetMediaEngine } from \"./AssetMediaEngine\";\n\nexport class AssetIdMediaEngine\n extends AssetMediaEngine\n implements MediaEngine\n{\n static async fetchByAssetId(\n host: EFMedia,\n _urlGenerator: UrlGenerator,\n assetId: string,\n apiHost: string,\n requiredTracks: \"audio\" | \"video\" | \"both\" = \"both\",\n signal?: AbortSignal,\n ) {\n const url = `${apiHost}/api/v1/isobmff_files/${assetId}/index`;\n const response = await host.fetch(url, { signal });\n \n // Check for abort after potentially slow network operation\n signal?.throwIfAborted();\n \n // Check if response is ok before parsing JSON\n if (!response.ok) {\n const text = await response.text();\n throw new Error(`Failed to fetch asset index: ${response.status} ${text}`);\n }\n \n // Check content type to avoid parsing non-JSON responses\n const contentType = response.headers.get(\"content-type\");\n if (contentType && !contentType.includes(\"application/json\")) {\n const text = await response.text();\n throw new Error(`Expected JSON but got ${contentType}: ${text.substring(0, 100)}`);\n }\n \n let data: Record<number, TrackFragmentIndex>;\n try {\n data = (await response.json()) as Record<number, TrackFragmentIndex>;\n \n // Check for abort after potentially slow JSON parsing\n signal?.throwIfAborted();\n } catch (error) {\n // If aborted during JSON parsing, re-throw to propagate cancellation\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n // If JSON parse fails, the response might be \"File not found\" or similar text\n const text = await response.text();\n throw new Error(`Failed to parse JSON response: ${text.substring(0, 100)}`);\n }\n \n const engine = new AssetIdMediaEngine(host, assetId, data, apiHost, _urlGenerator);\n \n // Check for abort after engine construction\n signal?.throwIfAborted();\n \n // Validate that segments are accessible by trying to fetch the first init segment\n // This prevents creating a media engine that will fail on all subsequent segment fetches\n // If segments require authentication that's not available, fail early\n // Only validate tracks that are actually required by the consumer (e.g., EFAudio only needs audio)\n // Skip validation if no signal provided (backwards compatibility) - validation is optional\n if (signal) {\n const videoTrack = engine.videoTrackIndex;\n const audioTrack = engine.audioTrackIndex;\n const needsVideo = requiredTracks === \"video\" || requiredTracks === \"both\";\n const needsAudio = requiredTracks === \"audio\" || requiredTracks === \"both\";\n \n // Validate video track if required and available\n if (needsVideo && videoTrack && videoTrack.track !== undefined) {\n try {\n await engine.fetchInitSegment(\n { trackId: videoTrack.track, src: engine.src },\n signal,\n );\n } catch (error) {\n // If aborted, re-throw to propagate cancellation\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n // If fetch fails with 401, segments require authentication that's not available\n // Fail media engine creation early to avoid all subsequent fetch calls\n if (\n error instanceof Error &&\n (error.message.includes(\"401\") ||\n error.message.includes(\"UNAUTHORIZED\") ||\n (error.message.includes(\"Failed to fetch\") && error.message.includes(\"401\")))\n ) {\n throw new Error(`Video segments require authentication: ${error.message}`);\n }\n // For other errors (404, network errors, etc.), allow media engine creation\n // These might be transient or expected in some test scenarios\n }\n }\n \n // Check for abort between validations\n signal?.throwIfAborted();\n \n // Validate audio track if required and available\n if (needsAudio && audioTrack && audioTrack.track !== undefined) {\n try {\n await engine.fetchInitSegment(\n { trackId: audioTrack.track, src: engine.src },\n signal,\n );\n } catch (error) {\n // If aborted, re-throw to propagate cancellation\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n // If fetch fails with 401, segments require authentication that's not available\n // Fail media engine creation early to avoid all subsequent fetch calls\n if (\n error instanceof Error &&\n (error.message.includes(\"401\") ||\n error.message.includes(\"UNAUTHORIZED\") ||\n (error.message.includes(\"Failed to fetch\") && error.message.includes(\"401\")))\n ) {\n throw new Error(`Audio segments require authentication: ${error.message}`);\n }\n // For other errors (404, network errors, etc.), allow media engine creation\n // These might be transient or expected in some test scenarios\n }\n }\n }\n \n return engine;\n }\n\n constructor(\n host: EFMedia,\n public assetId: string,\n data: Record<number, TrackFragmentIndex>,\n private apiHost: string,\n urlGenerator: UrlGenerator,\n ) {\n // Pass assetId as src to parent constructor for compatibility\n super(host, assetId, urlGenerator);\n // Initialize data after parent constructor\n this.data = data;\n\n // Calculate duration from the data\n const longestFragment = Object.values(this.data).reduce(\n (max, fragment) => Math.max(max, fragment.duration / fragment.timescale),\n 0,\n );\n this.durationMs = longestFragment * 1000;\n }\n\n // Override URL-building methods to use API endpoints instead of file paths\n get initSegmentPaths() {\n const paths: InitSegmentPaths = {};\n\n if (this.audioTrackIndex !== undefined) {\n paths.audio = {\n path: `${this.apiHost}/api/v1/isobmff_tracks/${this.assetId}/${this.audioTrackIndex.track}`,\n pos: this.audioTrackIndex.initSegment.offset,\n size: this.audioTrackIndex.initSegment.size,\n };\n }\n\n if (this.videoTrackIndex !== undefined) {\n paths.video = {\n path: `${this.apiHost}/api/v1/isobmff_tracks/${this.assetId}/${this.videoTrackIndex.track}`,\n pos: this.videoTrackIndex.initSegment.offset,\n size: this.videoTrackIndex.initSegment.size,\n };\n }\n\n return paths;\n }\n\n get templates() {\n return {\n initSegment: `${this.apiHost}/api/v1/isobmff_tracks/${this.assetId}/{trackId}`,\n mediaSegment: `${this.apiHost}/api/v1/isobmff_tracks/${this.assetId}/{trackId}`,\n };\n }\n\n buildInitSegmentUrl(trackId: number) {\n return `${this.apiHost}/api/v1/isobmff_tracks/${this.assetId}/${trackId}`;\n }\n\n buildMediaSegmentUrl(trackId: number, _segmentId: number) {\n return `${this.apiHost}/api/v1/isobmff_tracks/${this.assetId}/${trackId}`;\n }\n\n convertToSegmentRelativeTimestamps(\n globalTimestamps: number[],\n segmentId: number,\n rendition: VideoRendition,\n ): number[] {\n if (!rendition.trackId) {\n throw new Error(\n \"[convertToSegmentRelativeTimestamps] Track ID is required for asset metadata\",\n );\n }\n // For AssetMediaEngine, we need to calculate the actual segment start time\n // using the precise segment boundaries from the track fragment index\n const trackData = this.data[rendition.trackId];\n if (!trackData) {\n throw new Error(\"Track not found\");\n }\n const segment = trackData.segments?.[segmentId];\n if (!segment) {\n throw new Error(\"Segment not found\");\n }\n const segmentStartMs = (segment.cts / trackData.timescale) * 1000;\n\n return globalTimestamps.map(\n (globalMs) => (globalMs - segmentStartMs) / 1000,\n );\n }\n}\n"],"mappings":";;;AAUA,IAAa,qBAAb,MAAa,2BACH,iBAEV;CACE,aAAa,eACX,MACA,eACA,SACA,SACA,iBAA6C,QAC7C,QACA;EACA,MAAM,MAAM,GAAG,QAAQ,wBAAwB,QAAQ;EACvD,MAAM,WAAW,MAAM,KAAK,MAAM,KAAK,EAAE,QAAQ,CAAC;AAGlD,UAAQ,gBAAgB;AAGxB,MAAI,CAAC,SAAS,IAAI;GAChB,MAAM,OAAO,MAAM,SAAS,MAAM;AAClC,SAAM,IAAI,MAAM,gCAAgC,SAAS,OAAO,GAAG,OAAO;;EAI5E,MAAM,cAAc,SAAS,QAAQ,IAAI,eAAe;AACxD,MAAI,eAAe,CAAC,YAAY,SAAS,mBAAmB,EAAE;GAC5D,MAAM,OAAO,MAAM,SAAS,MAAM;AAClC,SAAM,IAAI,MAAM,yBAAyB,YAAY,IAAI,KAAK,UAAU,GAAG,IAAI,GAAG;;EAGpF,IAAIA;AACJ,MAAI;AACF,UAAQ,MAAM,SAAS,MAAM;AAG7B,WAAQ,gBAAgB;WACjB,OAAO;AAEd,OAAI,iBAAiB,gBAAgB,MAAM,SAAS,aAClD,OAAM;GAGR,MAAM,OAAO,MAAM,SAAS,MAAM;AAClC,SAAM,IAAI,MAAM,kCAAkC,KAAK,UAAU,GAAG,IAAI,GAAG;;EAG7E,MAAM,SAAS,IAAI,mBAAmB,MAAM,SAAS,MAAM,SAAS,cAAc;AAGlF,UAAQ,gBAAgB;AAOxB,MAAI,QAAQ;GACV,MAAM,aAAa,OAAO;GAC1B,MAAM,aAAa,OAAO;GAC1B,MAAM,aAAa,mBAAmB,WAAW,mBAAmB;GACpE,MAAM,aAAa,mBAAmB,WAAW,mBAAmB;AAGpE,OAAI,cAAc,cAAc,WAAW,UAAU,OACnD,KAAI;AACF,UAAM,OAAO,iBACX;KAAE,SAAS,WAAW;KAAO,KAAK,OAAO;KAAK,EAC9C,OACD;YACM,OAAO;AAEd,QAAI,iBAAiB,gBAAgB,MAAM,SAAS,aAClD,OAAM;AAIR,QACE,iBAAiB,UAChB,MAAM,QAAQ,SAAS,MAAM,IAC5B,MAAM,QAAQ,SAAS,eAAe,IACrC,MAAM,QAAQ,SAAS,kBAAkB,IAAI,MAAM,QAAQ,SAAS,MAAM,EAE7E,OAAM,IAAI,MAAM,0CAA0C,MAAM,UAAU;;AAQhF,WAAQ,gBAAgB;AAGxB,OAAI,cAAc,cAAc,WAAW,UAAU,OACnD,KAAI;AACF,UAAM,OAAO,iBACX;KAAE,SAAS,WAAW;KAAO,KAAK,OAAO;KAAK,EAC9C,OACD;YACM,OAAO;AAEd,QAAI,iBAAiB,gBAAgB,MAAM,SAAS,aAClD,OAAM;AAIR,QACE,iBAAiB,UAChB,MAAM,QAAQ,SAAS,MAAM,IAC5B,MAAM,QAAQ,SAAS,eAAe,IACrC,MAAM,QAAQ,SAAS,kBAAkB,IAAI,MAAM,QAAQ,SAAS,MAAM,EAE7E,OAAM,IAAI,MAAM,0CAA0C,MAAM,UAAU;;;AAQlF,SAAO;;CAGT,YACE,MACA,AAAOC,SACP,MACA,AAAQC,SACR,cACA;AAEA,QAAM,MAAM,SAAS,aAAa;EAN3B;EAEC;AAMR,OAAK,OAAO;AAOZ,OAAK,aAJmB,OAAO,OAAO,KAAK,KAAK,CAAC,QAC9C,KAAK,aAAa,KAAK,IAAI,KAAK,SAAS,WAAW,SAAS,UAAU,EACxE,EACD,GACmC;;CAItC,IAAI,mBAAmB;EACrB,MAAMC,QAA0B,EAAE;AAElC,MAAI,KAAK,oBAAoB,OAC3B,OAAM,QAAQ;GACZ,MAAM,GAAG,KAAK,QAAQ,yBAAyB,KAAK,QAAQ,GAAG,KAAK,gBAAgB;GACpF,KAAK,KAAK,gBAAgB,YAAY;GACtC,MAAM,KAAK,gBAAgB,YAAY;GACxC;AAGH,MAAI,KAAK,oBAAoB,OAC3B,OAAM,QAAQ;GACZ,MAAM,GAAG,KAAK,QAAQ,yBAAyB,KAAK,QAAQ,GAAG,KAAK,gBAAgB;GACpF,KAAK,KAAK,gBAAgB,YAAY;GACtC,MAAM,KAAK,gBAAgB,YAAY;GACxC;AAGH,SAAO;;CAGT,IAAI,YAAY;AACd,SAAO;GACL,aAAa,GAAG,KAAK,QAAQ,yBAAyB,KAAK,QAAQ;GACnE,cAAc,GAAG,KAAK,QAAQ,yBAAyB,KAAK,QAAQ;GACrE;;CAGH,oBAAoB,SAAiB;AACnC,SAAO,GAAG,KAAK,QAAQ,yBAAyB,KAAK,QAAQ,GAAG;;CAGlE,qBAAqB,SAAiB,YAAoB;AACxD,SAAO,GAAG,KAAK,QAAQ,yBAAyB,KAAK,QAAQ,GAAG;;CAGlE,mCACE,kBACA,WACA,WACU;AACV,MAAI,CAAC,UAAU,QACb,OAAM,IAAI,MACR,+EACD;EAIH,MAAM,YAAY,KAAK,KAAK,UAAU;AACtC,MAAI,CAAC,UACH,OAAM,IAAI,MAAM,kBAAkB;EAEpC,MAAM,UAAU,UAAU,WAAW;AACrC,MAAI,CAAC,QACH,OAAM,IAAI,MAAM,oBAAoB;EAEtC,MAAM,iBAAkB,QAAQ,MAAM,UAAU,YAAa;AAE7D,SAAO,iBAAiB,KACrB,cAAc,WAAW,kBAAkB,IAC7C"}
1
+ {"version":3,"file":"AssetIdMediaEngine.js","names":["data: Record<number, TrackFragmentIndex>","assetId: string","apiHost: string","paths: InitSegmentPaths"],"sources":["../../../src/elements/EFMedia/AssetIdMediaEngine.ts"],"sourcesContent":["import type { TrackFragmentIndex } from \"@editframe/assets\";\nimport type {\n InitSegmentPaths,\n MediaEngine,\n VideoRendition,\n} from \"../../transcoding/types\";\nimport type { UrlGenerator } from \"../../transcoding/utils/UrlGenerator\";\nimport type { EFMedia } from \"../EFMedia\";\nimport { AssetMediaEngine } from \"./AssetMediaEngine\";\n\nexport class AssetIdMediaEngine\n extends AssetMediaEngine\n implements MediaEngine\n{\n static async fetchByAssetId(\n host: EFMedia,\n _urlGenerator: UrlGenerator,\n assetId: string,\n apiHost: string,\n requiredTracks: \"audio\" | \"video\" | \"both\" = \"both\",\n signal?: AbortSignal,\n ) {\n const url = `${apiHost}/api/v1/isobmff_files/${assetId}/index`;\n const response = await host.fetch(url, { signal });\n \n // Check for abort after potentially slow network operation\n signal?.throwIfAborted();\n \n // Check if response is ok before parsing JSON\n if (!response.ok) {\n const text = await response.text();\n throw new Error(`Failed to fetch asset index: ${response.status} ${text}`);\n }\n \n // Check content type to avoid parsing non-JSON responses\n const contentType = response.headers.get(\"content-type\");\n if (contentType && !contentType.includes(\"application/json\")) {\n const text = await response.text();\n throw new Error(`Expected JSON but got ${contentType}: ${text.substring(0, 100)}`);\n }\n \n let data: Record<number, TrackFragmentIndex>;\n try {\n data = (await response.json()) as Record<number, TrackFragmentIndex>;\n \n // Check for abort after potentially slow JSON parsing\n signal?.throwIfAborted();\n } catch (error) {\n // If aborted during JSON parsing, re-throw to propagate cancellation\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n // If JSON parse fails, the response might be \"File not found\" or similar text\n const text = await response.text();\n throw new Error(`Failed to parse JSON response: ${text.substring(0, 100)}`);\n }\n \n const engine = new AssetIdMediaEngine(host, assetId, data, apiHost, _urlGenerator);\n \n // Check for abort after engine construction\n signal?.throwIfAborted();\n \n // Validate that segments are accessible by trying to fetch the first init segment\n // This prevents creating a media engine that will fail on all subsequent segment fetches\n // If segments require authentication that's not available, fail early\n // Only validate tracks that are actually required by the consumer (e.g., EFAudio only needs audio)\n // Skip validation if no signal provided (backwards compatibility) - validation is optional\n if (signal) {\n const videoTrack = engine.getVideoTrackIndex();\n const audioTrack = engine.getAudioTrackIndex();\n const needsVideo = requiredTracks === \"video\" || requiredTracks === \"both\";\n const needsAudio = requiredTracks === \"audio\" || requiredTracks === \"both\";\n \n // Validate video track if required and available\n if (needsVideo && videoTrack && videoTrack.track !== undefined) {\n try {\n await engine.fetchInitSegment(\n { trackId: videoTrack.track, src: engine.src },\n signal,\n );\n } catch (error) {\n // If aborted, re-throw to propagate cancellation\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n // If fetch fails with 401, segments require authentication that's not available\n // Fail media engine creation early to avoid all subsequent fetch calls\n if (\n error instanceof Error &&\n (error.message.includes(\"401\") ||\n error.message.includes(\"UNAUTHORIZED\") ||\n (error.message.includes(\"Failed to fetch\") && error.message.includes(\"401\")))\n ) {\n throw new Error(`Video segments require authentication: ${error.message}`);\n }\n // For other errors (404, network errors, etc.), allow media engine creation\n // These might be transient or expected in some test scenarios\n }\n }\n \n // Check for abort between validations\n signal?.throwIfAborted();\n \n // Validate audio track if required and available\n if (needsAudio && audioTrack && audioTrack.track !== undefined) {\n try {\n await engine.fetchInitSegment(\n { trackId: audioTrack.track, src: engine.src },\n signal,\n );\n } catch (error) {\n // If aborted, re-throw to propagate cancellation\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n // If fetch fails with 401, segments require authentication that's not available\n // Fail media engine creation early to avoid all subsequent fetch calls\n if (\n error instanceof Error &&\n (error.message.includes(\"401\") ||\n error.message.includes(\"UNAUTHORIZED\") ||\n (error.message.includes(\"Failed to fetch\") && error.message.includes(\"401\")))\n ) {\n throw new Error(`Audio segments require authentication: ${error.message}`);\n }\n // For other errors (404, network errors, etc.), allow media engine creation\n // These might be transient or expected in some test scenarios\n }\n }\n }\n \n return engine;\n }\n\n constructor(\n host: EFMedia,\n public assetId: string,\n data: Record<number, TrackFragmentIndex>,\n private apiHost: string,\n urlGenerator: UrlGenerator,\n ) {\n // Pass assetId as src to parent constructor for compatibility\n super(host, assetId, urlGenerator);\n // Initialize data after parent constructor\n this.data = data;\n\n // Calculate duration from the data\n const longestFragment = Object.values(this.data).reduce(\n (max, fragment) => Math.max(max, fragment.duration / fragment.timescale),\n 0,\n );\n this.durationMs = longestFragment * 1000;\n \n // Initialize MediaEngine interface properties\n this.templates = {\n initSegment: `${apiHost}/api/v1/isobmff_tracks/${assetId}/{trackId}`,\n mediaSegment: `${apiHost}/api/v1/isobmff_tracks/${assetId}/{trackId}`,\n };\n }\n\n // Override URL-building methods to use API endpoints instead of file paths\n getInitSegmentPaths(): InitSegmentPaths {\n const paths: InitSegmentPaths = {};\n const audioTrack = this.getAudioTrackIndex();\n const videoTrack = this.getVideoTrackIndex();\n\n if (audioTrack !== undefined) {\n paths.audio = {\n path: `${this.apiHost}/api/v1/isobmff_tracks/${this.assetId}/${audioTrack.track}`,\n pos: audioTrack.initSegment.offset,\n size: audioTrack.initSegment.size,\n };\n }\n\n if (videoTrack !== undefined) {\n paths.video = {\n path: `${this.apiHost}/api/v1/isobmff_tracks/${this.assetId}/${videoTrack.track}`,\n pos: videoTrack.initSegment.offset,\n size: videoTrack.initSegment.size,\n };\n }\n\n return paths;\n }\n\n // MediaEngine interface property - initialized in constructor/static fetch\n templates!: { initSegment: string; mediaSegment: string };\n\n buildInitSegmentUrl(trackId: number) {\n return `${this.apiHost}/api/v1/isobmff_tracks/${this.assetId}/${trackId}`;\n }\n\n buildMediaSegmentUrl(trackId: number, _segmentId: number) {\n return `${this.apiHost}/api/v1/isobmff_tracks/${this.assetId}/${trackId}`;\n }\n\n convertToSegmentRelativeTimestamps(\n globalTimestamps: number[],\n segmentId: number,\n rendition: VideoRendition,\n ): number[] {\n if (!rendition.trackId) {\n throw new Error(\n \"[convertToSegmentRelativeTimestamps] Track ID is required for asset metadata\",\n );\n }\n // For AssetMediaEngine, we need to calculate the actual segment start time\n // using the precise segment boundaries from the track fragment index\n const trackData = this.data[rendition.trackId];\n if (!trackData) {\n throw new Error(\"Track not found\");\n }\n const segment = trackData.segments?.[segmentId];\n if (!segment) {\n throw new Error(\"Segment not found\");\n }\n const segmentStartMs = (segment.cts / trackData.timescale) * 1000;\n\n return globalTimestamps.map(\n (globalMs) => (globalMs - segmentStartMs) / 1000,\n );\n }\n}\n"],"mappings":"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"}
@@ -17,15 +17,21 @@ var AssetMediaEngine = class AssetMediaEngine extends BaseMediaEngine {
17
17
  const engine = new AssetMediaEngine(host, src, urlGenerator);
18
18
  let normalizedSrc = src.startsWith("/") ? src.slice(1) : src;
19
19
  normalizedSrc = normalizedSrc.replace(/^\/+/, "");
20
- const baseUrl = urlGenerator.getBaseUrl();
21
- const url = baseUrl ? `${baseUrl}/api/v1/isobmff_files/local/index?src=${encodeURIComponent(normalizedSrc)}` : `/api/v1/isobmff_files/local/index?src=${encodeURIComponent(normalizedSrc)}`;
20
+ const apiBaseUrl = urlGenerator.getBaseUrl();
21
+ const url = apiBaseUrl ? `${apiBaseUrl}/api/v1/isobmff_files/local/index?src=${encodeURIComponent(normalizedSrc)}` : `/api/v1/isobmff_files/local/index?src=${encodeURIComponent(normalizedSrc)}`;
22
22
  engine.data = await engine.fetchManifest(url, signal);
23
23
  signal?.throwIfAborted();
24
24
  engine.durationMs = Object.values(engine.data).reduce((max, fragment) => Math.max(max, fragment.duration / fragment.timescale), 0) * 1e3;
25
25
  if (src.startsWith("/")) engine.src = src.slice(1);
26
+ const sourceUrl = engine.getSourceUrlForJit();
27
+ const jitBaseUrl = engine.getBaseUrlForJit();
28
+ engine.templates = {
29
+ initSegment: `${jitBaseUrl}/api/v1/transcode/{rendition}/init.m4s?url=${encodeURIComponent(sourceUrl)}`,
30
+ mediaSegment: `${jitBaseUrl}/api/v1/transcode/{rendition}/{segmentId}.m4s?url=${encodeURIComponent(sourceUrl)}`
31
+ };
26
32
  if (signal) {
27
- const videoTrack = engine.videoTrackIndex;
28
- const audioTrack = engine.audioTrackIndex;
33
+ const videoTrack = engine.getVideoTrackIndex();
34
+ const audioTrack = engine.getAudioTrackIndex();
29
35
  const needsVideo = requiredTracks === "video" || requiredTracks === "both";
30
36
  const needsAudio = requiredTracks === "audio" || requiredTracks === "both";
31
37
  if (needsVideo && videoTrack && videoTrack.track !== void 0) try {
@@ -50,33 +56,51 @@ var AssetMediaEngine = class AssetMediaEngine extends BaseMediaEngine {
50
56
  }
51
57
  return engine;
52
58
  }
53
- get audioTrackIndex() {
59
+ getAudioTrackIndex() {
54
60
  return Object.values(this.data).find((track) => track.type === "audio");
55
61
  }
56
- get videoTrackIndex() {
62
+ getVideoTrackIndex() {
57
63
  return Object.values(this.data).find((track) => track.type === "video" && track.track !== void 0 && track.track > 0);
58
64
  }
59
- get scrubTrackIndex() {
65
+ getScrubTrackIndex() {
60
66
  return this.data[-1];
61
67
  }
62
- get videoRendition() {
63
- const videoTrack = this.videoTrackIndex;
64
- if (!videoTrack || videoTrack.track === void 0) return;
65
- return {
68
+ #cachedVideoRendition = null;
69
+ #cachedAudioRendition = null;
70
+ getVideoRenditionInternal() {
71
+ if (this.#cachedVideoRendition !== null) return this.#cachedVideoRendition;
72
+ const videoTrack = this.getVideoTrackIndex();
73
+ if (!videoTrack || videoTrack.track === void 0) {
74
+ this.#cachedVideoRendition = void 0;
75
+ return;
76
+ }
77
+ this.#cachedVideoRendition = {
66
78
  id: "high",
67
79
  trackId: videoTrack.track,
68
80
  src: this.src,
69
81
  startTimeOffsetMs: videoTrack.startTimeOffsetMs
70
82
  };
83
+ return this.#cachedVideoRendition;
71
84
  }
72
- get audioRendition() {
73
- const audioTrack = this.audioTrackIndex;
74
- if (!audioTrack || audioTrack.track === void 0) return;
75
- return {
85
+ getAudioRenditionInternal() {
86
+ if (this.#cachedAudioRendition !== null) return this.#cachedAudioRendition;
87
+ const audioTrack = this.getAudioTrackIndex();
88
+ if (!audioTrack || audioTrack.track === void 0) {
89
+ this.#cachedAudioRendition = void 0;
90
+ return;
91
+ }
92
+ this.#cachedAudioRendition = {
76
93
  id: "audio",
77
94
  trackId: audioTrack.track,
78
95
  src: this.src
79
96
  };
97
+ return this.#cachedAudioRendition;
98
+ }
99
+ get videoRendition() {
100
+ return this.getVideoRenditionInternal();
101
+ }
102
+ get audioRendition() {
103
+ return this.getAudioRenditionInternal();
80
104
  }
81
105
  /**
82
106
  * Get the source URL for JIT format (needs to be absolute URL)
@@ -96,14 +120,6 @@ var AssetMediaEngine = class AssetMediaEngine extends BaseMediaEngine {
96
120
  if (!baseUrl) baseUrl = typeof window !== "undefined" ? window.location.origin : "";
97
121
  return baseUrl;
98
122
  }
99
- get templates() {
100
- const sourceUrl = this.getSourceUrlForJit();
101
- const baseUrl = this.getBaseUrlForJit();
102
- return {
103
- initSegment: `${baseUrl}/api/v1/transcode/{rendition}/init.m4s?url=${encodeURIComponent(sourceUrl)}`,
104
- mediaSegment: `${baseUrl}/api/v1/transcode/{rendition}/{segmentId}.m4s?url=${encodeURIComponent(sourceUrl)}`
105
- };
106
- }
107
123
  /**
108
124
  * Map trackId to JIT rendition ID for URL generation
109
125
  * - trackId 1 (video) -> "high" (default video rendition)
@@ -214,7 +230,7 @@ var AssetMediaEngine = class AssetMediaEngine extends BaseMediaEngine {
214
230
  return nearestSegmentIndex;
215
231
  }
216
232
  getScrubVideoRendition() {
217
- const scrubTrack = this.scrubTrackIndex;
233
+ const scrubTrack = this.getScrubTrackIndex();
218
234
  if (!scrubTrack || scrubTrack.track === void 0) return;
219
235
  const scrubSegmentDurationMs = 3e4;
220
236
  const segmentDurationsMs = scrubTrack.segments.length > 0 ? scrubTrack.segments.map((segment) => {
@@ -248,7 +264,7 @@ var AssetMediaEngine = class AssetMediaEngine extends BaseMediaEngine {
248
264
  * may have incomplete segment data that doesn't cover the full video duration.
249
265
  */
250
266
  async extractThumbnails(timestamps, signal) {
251
- const rendition = this.videoRendition;
267
+ const rendition = this.getVideoRenditionInternal();
252
268
  if (!rendition) {
253
269
  console.warn("AssetMediaEngine: No video rendition available for thumbnails");
254
270
  return timestamps.map(() => null);
@@ -1 +1 @@
1
- {"version":3,"file":"AssetMediaEngine.js","names":["segmentRanges: SegmentTimeRange[]","distance: number","segmentDurationsMs: number[] | undefined"],"sources":["../../../src/elements/EFMedia/AssetMediaEngine.ts"],"sourcesContent":["import type { TrackFragmentIndex } from \"@editframe/assets\";\n\nimport { withSpan } from \"../../otel/tracingHelpers.js\";\nimport type {\n AudioRendition,\n MediaEngine,\n RenditionId,\n SegmentTimeRange,\n ThumbnailResult,\n VideoRendition,\n} from \"../../transcoding/types\";\nimport type { UrlGenerator } from \"../../transcoding/utils/UrlGenerator\";\nimport type { EFMedia } from \"../EFMedia\";\nimport { BaseMediaEngine, mediaCache } from \"./BaseMediaEngine\";\nimport type { MediaRendition } from \"./shared/MediaTaskUtils\";\nimport {\n convertToScaledTime,\n roundToMilliseconds,\n} from \"./shared/PrecisionUtils\";\nimport { ThumbnailExtractor } from \"./shared/ThumbnailExtractor.js\";\n\nexport class AssetMediaEngine extends BaseMediaEngine implements MediaEngine {\n public src: string;\n protected data: Record<number, TrackFragmentIndex> = {};\n durationMs = 0;\n private thumbnailExtractor: ThumbnailExtractor;\n protected urlGenerator: UrlGenerator;\n\n constructor(host: EFMedia, src: string, urlGenerator: UrlGenerator) {\n super(host);\n this.src = src;\n this.thumbnailExtractor = new ThumbnailExtractor(this);\n this.urlGenerator = urlGenerator;\n }\n\n static async fetch(\n host: EFMedia, \n urlGenerator: UrlGenerator, \n src: string,\n requiredTracks: \"audio\" | \"video\" | \"both\" = \"both\",\n signal?: AbortSignal,\n ) {\n const engine = new AssetMediaEngine(host, src, urlGenerator);\n \n // Normalize the path: remove leading slash and any double slashes\n let normalizedSrc = src.startsWith(\"/\")\n ? src.slice(1)\n : src;\n normalizedSrc = normalizedSrc.replace(/^\\/+/, \"\");\n \n // Use production API format: /api/v1/isobmff_files/local/index?src={src}\n // This route is handled by the vite plugin for local development\n const baseUrl = urlGenerator.getBaseUrl();\n const url = baseUrl \n ? `${baseUrl}/api/v1/isobmff_files/local/index?src=${encodeURIComponent(normalizedSrc)}`\n : `/api/v1/isobmff_files/local/index?src=${encodeURIComponent(normalizedSrc)}`;\n const data = await engine.fetchManifest(url, signal);\n engine.data = data as Record<number, TrackFragmentIndex>;\n\n // Check for abort after potentially slow network operation\n signal?.throwIfAborted();\n\n // Calculate duration from the data\n const longestFragment = Object.values(engine.data).reduce(\n (max, fragment) => Math.max(max, fragment.duration / fragment.timescale),\n 0,\n );\n engine.durationMs = longestFragment * 1000;\n\n if (src.startsWith(\"/\")) {\n engine.src = src.slice(1);\n }\n\n // Validate that segments are accessible by trying to fetch the first init segment\n // This prevents creating a media engine that will fail on all subsequent segment fetches\n // If segments require authentication that's not available, fail early\n // Only validate tracks that are actually required by the consumer (e.g., EFAudio only needs audio)\n // Skip validation if no signal provided (backwards compatibility) - validation is optional\n if (signal) {\n const videoTrack = engine.videoTrackIndex;\n const audioTrack = engine.audioTrackIndex;\n const needsVideo = requiredTracks === \"video\" || requiredTracks === \"both\";\n const needsAudio = requiredTracks === \"audio\" || requiredTracks === \"both\";\n \n // Validate video track if required and available\n if (needsVideo && videoTrack && videoTrack.track !== undefined) {\n try {\n await engine.fetchInitSegment(\n { trackId: videoTrack.track, src: engine.src },\n signal,\n );\n } catch (error) {\n // If aborted, re-throw to propagate cancellation\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n // If fetch fails with 401, segments require authentication that's not available\n // Fail media engine creation early to avoid all subsequent fetch calls\n if (\n error instanceof Error &&\n (error.message.includes(\"401\") ||\n error.message.includes(\"UNAUTHORIZED\") ||\n (error.message.includes(\"Failed to fetch\") && error.message.includes(\"401\")))\n ) {\n throw new Error(`Video segments require authentication: ${error.message}`);\n }\n // For other errors (404, network errors, etc.), allow media engine creation\n // These might be transient or expected in some test scenarios\n }\n }\n \n // Check for abort between validations\n signal?.throwIfAborted();\n \n // Validate audio track if required and available\n if (needsAudio && audioTrack && audioTrack.track !== undefined) {\n try {\n await engine.fetchInitSegment(\n { trackId: audioTrack.track, src: engine.src },\n signal,\n );\n } catch (error) {\n // If aborted, re-throw to propagate cancellation\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n // If fetch fails with 401, segments require authentication that's not available\n // Fail media engine creation early to avoid all subsequent fetch calls\n if (\n error instanceof Error &&\n (error.message.includes(\"401\") ||\n error.message.includes(\"UNAUTHORIZED\") ||\n (error.message.includes(\"Failed to fetch\") && error.message.includes(\"401\")))\n ) {\n throw new Error(`Audio segments require authentication: ${error.message}`);\n }\n // For other errors (404, network errors, etc.), allow media engine creation\n // These might be transient or expected in some test scenarios\n }\n }\n }\n\n return engine;\n }\n\n get audioTrackIndex() {\n return Object.values(this.data).find((track) => track.type === \"audio\");\n }\n\n get videoTrackIndex() {\n return Object.values(this.data).find(\n (track) => track.type === \"video\" && track.track !== undefined && track.track > 0,\n );\n }\n\n get scrubTrackIndex() {\n // Scrub track uses track ID -1\n return this.data[-1];\n }\n\n get videoRendition() {\n const videoTrack = this.videoTrackIndex;\n\n if (!videoTrack || videoTrack.track === undefined) {\n return undefined;\n }\n\n return {\n id: \"high\" as RenditionId, // Use JIT-style rendition ID\n trackId: videoTrack.track,\n src: this.src,\n startTimeOffsetMs: videoTrack.startTimeOffsetMs,\n };\n }\n\n get audioRendition() {\n const audioTrack = this.audioTrackIndex;\n\n if (!audioTrack || audioTrack.track === undefined) {\n return undefined;\n }\n\n return {\n id: \"audio\" as RenditionId, // Use JIT-style rendition ID\n trackId: audioTrack.track,\n src: this.src,\n };\n }\n\n\n /**\n * Get the source URL for JIT format (needs to be absolute URL)\n */\n private getSourceUrlForJit(): string {\n // If src is already an absolute URL, use it\n if (this.src.startsWith(\"http://\") || this.src.startsWith(\"https://\")) {\n return this.src;\n }\n \n // Otherwise, construct absolute URL from baseUrl or current origin\n let baseUrl = this.urlGenerator.getBaseUrl();\n // If baseUrl is empty (no apiHost set), use current origin\n if (!baseUrl) {\n baseUrl = typeof window !== \"undefined\" ? window.location.origin : \"\";\n }\n // If src starts with /, keep it as-is (absolute path)\n // Otherwise, prepend with /\n const normalizedSrc = this.src.startsWith(\"/\") ? this.src : `/${this.src}`;\n return `${baseUrl}${normalizedSrc}`;\n }\n \n /**\n * Get the base URL for constructing JIT endpoints\n */\n private getBaseUrlForJit(): string {\n let baseUrl = this.urlGenerator.getBaseUrl();\n // If baseUrl is empty (no apiHost set), use current origin\n if (!baseUrl) {\n baseUrl = typeof window !== \"undefined\" ? window.location.origin : \"\";\n }\n return baseUrl;\n }\n\n get templates() {\n const sourceUrl = this.getSourceUrlForJit();\n const baseUrl = this.getBaseUrlForJit();\n return {\n initSegment: `${baseUrl}/api/v1/transcode/{rendition}/init.m4s?url=${encodeURIComponent(sourceUrl)}`,\n mediaSegment: `${baseUrl}/api/v1/transcode/{rendition}/{segmentId}.m4s?url=${encodeURIComponent(sourceUrl)}`,\n };\n }\n\n /**\n * Map trackId to JIT rendition ID for URL generation\n * - trackId 1 (video) -> \"high\" (default video rendition)\n * - trackId 2 (audio) -> \"audio\"\n * - trackId -1 (scrub) -> \"scrub\"\n */\n private getRenditionId(trackId: number): RenditionId {\n if (trackId === -1) return \"scrub\";\n if (trackId === 2) return \"audio\";\n return \"high\"; // Default video rendition (trackId 1)\n }\n\n /**\n * Override isSegmentCached to use URL-based cache checking (like JitMediaEngine)\n */\n override isSegmentCached(\n segmentId: number,\n rendition: AudioRendition | VideoRendition,\n ): boolean {\n // Use URL-based cache checking (same as JitMediaEngine)\n if (!rendition.id) {\n return false;\n }\n \n // JIT uses 1-based segment IDs, but AssetMediaEngine uses 0-based internally\n const jitSegmentId = segmentId + 1;\n const segmentUrl = this.urlGenerator.generateSegmentUrl(jitSegmentId, rendition.id, this);\n return mediaCache.has(segmentUrl);\n }\n\n async fetchInitSegment(\n rendition: { id?: RenditionId; trackId: number | undefined; src: string },\n signal?: AbortSignal,\n ) {\n return withSpan(\n \"assetEngine.fetchInitSegment\",\n {\n trackId: rendition.trackId || -1,\n src: rendition.src,\n },\n undefined,\n async () => {\n if (!rendition.trackId) {\n throw new Error(\n \"[fetchInitSegment] Track ID is required for asset metadata\",\n );\n }\n \n // Use rendition ID if provided, otherwise map from trackId\n const renditionId = rendition.id || this.getRenditionId(rendition.trackId);\n const url = this.urlGenerator.generateSegmentUrl(\"init\", renditionId, this);\n \n // Segments are now served directly (not via byte ranges), so use simple fetch\n return this.fetchMedia(url, signal);\n },\n );\n }\n\n async fetchMediaSegment(\n segmentId: number,\n rendition: { id?: RenditionId; trackId: number | undefined; src: string },\n signal?: AbortSignal,\n ) {\n return withSpan(\n \"assetEngine.fetchMediaSegment\",\n {\n segmentId,\n trackId: rendition.trackId || -1,\n src: rendition.src,\n },\n undefined,\n async () => {\n if (!rendition.trackId) {\n throw new Error(\n \"[fetchMediaSegment] Track ID is required for asset metadata\",\n );\n }\n if (segmentId === undefined) {\n throw new Error(\"Segment ID is not available\");\n }\n \n // Use rendition ID if provided, otherwise map from trackId\n const renditionId = rendition.id || this.getRenditionId(rendition.trackId);\n \n // JIT uses 1-based segment IDs, but AssetMediaEngine uses 0-based internally\n // So we need to add 1 to segmentId for the URL\n const jitSegmentId = segmentId + 1;\n const url = this.urlGenerator.generateSegmentUrl(jitSegmentId, renditionId, this);\n\n // Segments are now served directly (not via byte ranges), so use simple fetch\n return this.fetchMedia(url, signal);\n },\n );\n }\n\n /**\n * Calculate audio segments for variable-duration segments using track fragment index\n */\n calculateAudioSegmentRange(\n fromMs: number,\n toMs: number,\n rendition: AudioRendition,\n _durationMs: number,\n ): SegmentTimeRange[] {\n if (fromMs >= toMs || !rendition.trackId) {\n console.warn(\n `calculateAudioSegmentRange: invalid fromMs ${fromMs} toMs ${toMs} rendition ${JSON.stringify(\n rendition,\n )}`,\n );\n return [];\n }\n\n const track = this.data[rendition.trackId];\n if (!track) {\n console.warn(\n `calculateAudioSegmentRange: track not found for rendition ${JSON.stringify(\n rendition,\n )}`,\n );\n return [];\n }\n\n const { timescale, segments } = track;\n const segmentRanges: SegmentTimeRange[] = [];\n\n for (let i = 0; i < segments.length; i++) {\n // biome-ignore lint/style/noNonNullAssertion: we know the segment is not null\n const segment = segments[i]!;\n const segmentStartTime = segment.cts;\n const segmentEndTime = segment.cts + segment.duration;\n\n // Convert to milliseconds\n const segmentStartMs = (segmentStartTime / timescale) * 1000;\n const segmentEndMs = (segmentEndTime / timescale) * 1000;\n\n // Check if segment overlaps with requested time range\n if (segmentStartMs < toMs && segmentEndMs > fromMs) {\n segmentRanges.push({\n segmentId: i, // AssetMediaEngine uses 0-based segment IDs\n startMs: segmentStartMs,\n endMs: segmentEndMs,\n });\n }\n }\n if (segmentRanges.length === 0) {\n console.warn(\n `calculateAudioSegmentRange: no segments found for fromMs ${fromMs} toMs ${toMs} rendition ${JSON.stringify(\n {\n rendition,\n track,\n },\n )}`,\n );\n }\n\n return segmentRanges;\n }\n\n computeSegmentId(seekTimeMs: number, rendition: MediaRendition) {\n if (!rendition.trackId) {\n console.warn(\n `computeSegmentId: trackId not found for rendition ${JSON.stringify(\n rendition,\n )}`,\n );\n throw new Error(\n \"[computeSegmentId] Track ID is required for asset metadata\",\n );\n }\n const track = this.data[rendition.trackId];\n if (!track) {\n throw new Error(\"Track not found\");\n }\n const { timescale, segments } = track;\n\n // Apply startTimeOffsetMs to map user timeline to media timeline for segment selection\n const startTimeOffsetMs =\n (\"startTimeOffsetMs\" in rendition && rendition.startTimeOffsetMs) || 0;\n\n const offsetSeekTimeMs = roundToMilliseconds(\n seekTimeMs + startTimeOffsetMs,\n );\n // Convert to timescale units using consistent precision\n const scaledSeekTime = convertToScaledTime(offsetSeekTimeMs, timescale);\n\n // Find the segment that contains the actual seek time\n for (let i = segments.length - 1; i >= 0; i--) {\n // biome-ignore lint/style/noNonNullAssertion: we know the segment is not null\n const segment = segments[i]!;\n const segmentEndTime = segment.cts + segment.duration;\n\n // Check if the seek time falls within this segment\n if (segment.cts <= scaledSeekTime && scaledSeekTime < segmentEndTime) {\n return i;\n }\n }\n\n // Handle gaps: if no exact segment contains the time, find the nearest one\n // This handles cases where seek time falls between segments (like 8041.667ms)\n let nearestSegmentIndex = 0;\n let nearestDistance = Number.MAX_SAFE_INTEGER;\n\n for (let i = 0; i < segments.length; i++) {\n // biome-ignore lint/style/noNonNullAssertion: we know the segment is not null\n const segment = segments[i]!;\n const segmentStartTime = segment.cts;\n const segmentEndTime = segment.cts + segment.duration;\n\n let distance: number;\n if (scaledSeekTime < segmentStartTime) {\n // Time is before this segment\n distance = segmentStartTime - scaledSeekTime;\n } else if (scaledSeekTime >= segmentEndTime) {\n // Time is after this segment\n distance = scaledSeekTime - segmentEndTime;\n } else {\n // Time is within this segment (should have been caught above, but just in case)\n return i;\n }\n\n if (distance < nearestDistance) {\n nearestDistance = distance;\n nearestSegmentIndex = i;\n }\n }\n\n return nearestSegmentIndex;\n }\n\n getScrubVideoRendition(): VideoRendition | undefined {\n const scrubTrack = this.scrubTrackIndex;\n\n if (!scrubTrack || scrubTrack.track === undefined) {\n return undefined;\n }\n\n // Calculate segment duration from scrub track segments\n // Scrub tracks use 30-second segments\n const scrubSegmentDurationMs = 30000;\n\n // Calculate segment durations array if segments exist\n const segmentDurationsMs: number[] | undefined =\n scrubTrack.segments.length > 0\n ? scrubTrack.segments.map((segment) => {\n // Convert segment duration from timescale units to milliseconds\n return (segment.duration / scrubTrack.timescale) * 1000;\n })\n : undefined;\n\n return {\n id: \"scrub\" as RenditionId, // Use JIT-style rendition ID\n trackId: scrubTrack.track,\n src: this.src,\n segmentDurationMs: scrubSegmentDurationMs,\n segmentDurationsMs,\n startTimeOffsetMs: scrubTrack.startTimeOffsetMs,\n };\n }\n\n /**\n * Get preferred buffer configuration for this media engine\n * AssetMediaEngine uses lower buffering since segments are already optimized\n */\n getBufferConfig() {\n return {\n // Buffer just 1 segment ahead (~2 seconds) for assets\n videoBufferDurationMs: 2000,\n audioBufferDurationMs: 2000,\n maxVideoBufferFetches: 1,\n maxAudioBufferFetches: 1,\n bufferThresholdMs: 30000, // Timeline-aware buffering threshold\n };\n }\n\n /**\n * Extract thumbnail canvases using main video rendition\n * Note: We prefer main video over scrub track because scrub track in AssetMediaEngine\n * may have incomplete segment data that doesn't cover the full video duration.\n */\n async extractThumbnails(\n timestamps: number[],\n signal?: AbortSignal,\n ): Promise<(ThumbnailResult | null)[]> {\n // Use main video rendition for thumbnails - scrub track may have incomplete segments\n const rendition = this.videoRendition;\n\n if (!rendition) {\n console.warn(\n \"AssetMediaEngine: No video rendition available for thumbnails\",\n );\n return timestamps.map(() => null);\n }\n\n return this.thumbnailExtractor.extractThumbnails(\n timestamps,\n rendition,\n this.durationMs,\n signal,\n );\n }\n\n convertToSegmentRelativeTimestamps(\n globalTimestamps: number[],\n _segmentId: number,\n rendition: VideoRendition,\n ): number[] {\n // For fragmented MP4 (Asset), when we create a mediabunny Input from init+media segment,\n // mediabunny sees the samples with their ABSOLUTE timestamps from the container.\n // This is because the tfdt box contains the baseMediaDecodeTime which is the absolute\n // position of this segment in the container timeline.\n //\n // So we just need to convert user time to container time by adding startTimeOffsetMs,\n // then pass that to mediabunny (in seconds).\n \n const startTimeOffsetMs = rendition.startTimeOffsetMs || 0;\n\n return globalTimestamps.map((globalMs) => {\n // User time -> container time -> seconds for mediabunny\n const containerTimeMs = globalMs + startTimeOffsetMs;\n return containerTimeMs / 1000;\n });\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"AssetMediaEngine.js","names":["#cachedVideoRendition","#cachedAudioRendition","segmentRanges: SegmentTimeRange[]","distance: number","segmentDurationsMs: number[] | undefined"],"sources":["../../../src/elements/EFMedia/AssetMediaEngine.ts"],"sourcesContent":["import type { TrackFragmentIndex } from \"@editframe/assets\";\n\nimport { withSpan } from \"../../otel/tracingHelpers.js\";\nimport type {\n AudioRendition,\n MediaEngine,\n RenditionId,\n SegmentTimeRange,\n ThumbnailResult,\n VideoRendition,\n} from \"../../transcoding/types\";\nimport type { UrlGenerator } from \"../../transcoding/utils/UrlGenerator\";\nimport type { EFMedia } from \"../EFMedia\";\nimport { BaseMediaEngine, mediaCache } from \"./BaseMediaEngine\";\nimport type { MediaRendition } from \"./shared/MediaTaskUtils.js\";\nimport {\n convertToScaledTime,\n roundToMilliseconds,\n} from \"./shared/PrecisionUtils\";\nimport { ThumbnailExtractor } from \"./shared/ThumbnailExtractor.js\";\n\nexport class AssetMediaEngine extends BaseMediaEngine implements MediaEngine {\n public src: string;\n protected data: Record<number, TrackFragmentIndex> = {};\n durationMs = 0;\n private thumbnailExtractor: ThumbnailExtractor;\n protected urlGenerator: UrlGenerator;\n\n // MediaEngine interface properties\n templates!: { initSegment: string; mediaSegment: string };\n\n constructor(host: EFMedia, src: string, urlGenerator: UrlGenerator) {\n super(host);\n this.src = src;\n this.thumbnailExtractor = new ThumbnailExtractor(this);\n this.urlGenerator = urlGenerator;\n }\n\n static async fetch(\n host: EFMedia, \n urlGenerator: UrlGenerator, \n src: string,\n requiredTracks: \"audio\" | \"video\" | \"both\" = \"both\",\n signal?: AbortSignal,\n ) {\n const engine = new AssetMediaEngine(host, src, urlGenerator);\n \n // Normalize the path: remove leading slash and any double slashes\n let normalizedSrc = src.startsWith(\"/\")\n ? src.slice(1)\n : src;\n normalizedSrc = normalizedSrc.replace(/^\\/+/, \"\");\n \n // Use production API format: /api/v1/isobmff_files/local/index?src={src}\n // This route is handled by the vite plugin for local development\n const apiBaseUrl = urlGenerator.getBaseUrl();\n const url = apiBaseUrl \n ? `${apiBaseUrl}/api/v1/isobmff_files/local/index?src=${encodeURIComponent(normalizedSrc)}`\n : `/api/v1/isobmff_files/local/index?src=${encodeURIComponent(normalizedSrc)}`;\n const data = await engine.fetchManifest(url, signal);\n engine.data = data as Record<number, TrackFragmentIndex>;\n\n // Check for abort after potentially slow network operation\n signal?.throwIfAborted();\n\n // Calculate duration from the data\n const longestFragment = Object.values(engine.data).reduce(\n (max, fragment) => Math.max(max, fragment.duration / fragment.timescale),\n 0,\n );\n engine.durationMs = longestFragment * 1000;\n\n if (src.startsWith(\"/\")) {\n engine.src = src.slice(1);\n }\n\n // Initialize MediaEngine interface properties\n const sourceUrl = engine.getSourceUrlForJit();\n const jitBaseUrl = engine.getBaseUrlForJit();\n engine.templates = {\n initSegment: `${jitBaseUrl}/api/v1/transcode/{rendition}/init.m4s?url=${encodeURIComponent(sourceUrl)}`,\n mediaSegment: `${jitBaseUrl}/api/v1/transcode/{rendition}/{segmentId}.m4s?url=${encodeURIComponent(sourceUrl)}`,\n };\n\n // Validate that segments are accessible by trying to fetch the first init segment\n // This prevents creating a media engine that will fail on all subsequent segment fetches\n // If segments require authentication that's not available, fail early\n // Only validate tracks that are actually required by the consumer (e.g., EFAudio only needs audio)\n // Skip validation if no signal provided (backwards compatibility) - validation is optional\n if (signal) {\n const videoTrack = engine.getVideoTrackIndex();\n const audioTrack = engine.getAudioTrackIndex();\n const needsVideo = requiredTracks === \"video\" || requiredTracks === \"both\";\n const needsAudio = requiredTracks === \"audio\" || requiredTracks === \"both\";\n \n // Validate video track if required and available\n if (needsVideo && videoTrack && videoTrack.track !== undefined) {\n try {\n await engine.fetchInitSegment(\n { trackId: videoTrack.track, src: engine.src },\n signal,\n );\n } catch (error) {\n // If aborted, re-throw to propagate cancellation\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n // If fetch fails with 401, segments require authentication that's not available\n // Fail media engine creation early to avoid all subsequent fetch calls\n if (\n error instanceof Error &&\n (error.message.includes(\"401\") ||\n error.message.includes(\"UNAUTHORIZED\") ||\n (error.message.includes(\"Failed to fetch\") && error.message.includes(\"401\")))\n ) {\n throw new Error(`Video segments require authentication: ${error.message}`);\n }\n // For other errors (404, network errors, etc.), allow media engine creation\n // These might be transient or expected in some test scenarios\n }\n }\n \n // Check for abort between validations\n signal?.throwIfAborted();\n \n // Validate audio track if required and available\n if (needsAudio && audioTrack && audioTrack.track !== undefined) {\n try {\n await engine.fetchInitSegment(\n { trackId: audioTrack.track, src: engine.src },\n signal,\n );\n } catch (error) {\n // If aborted, re-throw to propagate cancellation\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n // If fetch fails with 401, segments require authentication that's not available\n // Fail media engine creation early to avoid all subsequent fetch calls\n if (\n error instanceof Error &&\n (error.message.includes(\"401\") ||\n error.message.includes(\"UNAUTHORIZED\") ||\n (error.message.includes(\"Failed to fetch\") && error.message.includes(\"401\")))\n ) {\n throw new Error(`Audio segments require authentication: ${error.message}`);\n }\n // For other errors (404, network errors, etc.), allow media engine creation\n // These might be transient or expected in some test scenarios\n }\n }\n }\n\n return engine;\n }\n\n getAudioTrackIndex() {\n return Object.values(this.data).find((track) => track.type === \"audio\");\n }\n\n getVideoTrackIndex() {\n return Object.values(this.data).find(\n (track) => track.type === \"video\" && track.track !== undefined && track.track > 0,\n );\n }\n\n getScrubTrackIndex() {\n // Scrub track uses track ID -1\n return this.data[-1];\n }\n\n // Cache renditions to avoid recomputing on every access\n #cachedVideoRendition: VideoRendition | undefined | null = null;\n #cachedAudioRendition: AudioRendition | undefined | null = null;\n\n protected getVideoRenditionInternal(): VideoRendition | undefined {\n if (this.#cachedVideoRendition !== null) {\n return this.#cachedVideoRendition;\n }\n const videoTrack = this.getVideoTrackIndex();\n\n if (!videoTrack || videoTrack.track === undefined) {\n this.#cachedVideoRendition = undefined;\n return undefined;\n }\n\n this.#cachedVideoRendition = {\n id: \"high\" as RenditionId, // Use JIT-style rendition ID\n trackId: videoTrack.track,\n src: this.src,\n startTimeOffsetMs: videoTrack.startTimeOffsetMs,\n };\n return this.#cachedVideoRendition;\n }\n\n protected getAudioRenditionInternal(): AudioRendition | undefined {\n if (this.#cachedAudioRendition !== null) {\n return this.#cachedAudioRendition;\n }\n const audioTrack = this.getAudioTrackIndex();\n\n if (!audioTrack || audioTrack.track === undefined) {\n this.#cachedAudioRendition = undefined;\n return undefined;\n }\n\n this.#cachedAudioRendition = {\n id: \"audio\" as RenditionId, // Use JIT-style rendition ID\n trackId: audioTrack.track,\n src: this.src,\n };\n return this.#cachedAudioRendition;\n }\n\n // MediaEngine interface properties\n get videoRendition(): VideoRendition | undefined {\n return this.getVideoRenditionInternal();\n }\n\n get audioRendition(): AudioRendition | undefined {\n return this.getAudioRenditionInternal();\n }\n\n /**\n * Get the source URL for JIT format (needs to be absolute URL)\n */\n private getSourceUrlForJit(): string {\n // If src is already an absolute URL, use it\n if (this.src.startsWith(\"http://\") || this.src.startsWith(\"https://\")) {\n return this.src;\n }\n \n // Otherwise, construct absolute URL from baseUrl or current origin\n let baseUrl = this.urlGenerator.getBaseUrl();\n // If baseUrl is empty (no apiHost set), use current origin\n if (!baseUrl) {\n baseUrl = typeof window !== \"undefined\" ? window.location.origin : \"\";\n }\n // If src starts with /, keep it as-is (absolute path)\n // Otherwise, prepend with /\n const normalizedSrc = this.src.startsWith(\"/\") ? this.src : `/${this.src}`;\n return `${baseUrl}${normalizedSrc}`;\n }\n \n /**\n * Get the base URL for constructing JIT endpoints\n */\n private getBaseUrlForJit(): string {\n let baseUrl = this.urlGenerator.getBaseUrl();\n // If baseUrl is empty (no apiHost set), use current origin\n if (!baseUrl) {\n baseUrl = typeof window !== \"undefined\" ? window.location.origin : \"\";\n }\n return baseUrl;\n }\n\n\n /**\n * Map trackId to JIT rendition ID for URL generation\n * - trackId 1 (video) -> \"high\" (default video rendition)\n * - trackId 2 (audio) -> \"audio\"\n * - trackId -1 (scrub) -> \"scrub\"\n */\n private getRenditionId(trackId: number): RenditionId {\n if (trackId === -1) return \"scrub\";\n if (trackId === 2) return \"audio\";\n return \"high\"; // Default video rendition (trackId 1)\n }\n\n /**\n * Override isSegmentCached to use URL-based cache checking (like JitMediaEngine)\n */\n override isSegmentCached(\n segmentId: number,\n rendition: AudioRendition | VideoRendition,\n ): boolean {\n // Use URL-based cache checking (same as JitMediaEngine)\n if (!rendition.id) {\n return false;\n }\n \n // JIT uses 1-based segment IDs, but AssetMediaEngine uses 0-based internally\n const jitSegmentId = segmentId + 1;\n const segmentUrl = this.urlGenerator.generateSegmentUrl(jitSegmentId, rendition.id, this);\n return mediaCache.has(segmentUrl);\n }\n\n async fetchInitSegment(\n rendition: { id?: RenditionId; trackId: number | undefined; src: string },\n signal?: AbortSignal,\n ) {\n return withSpan(\n \"assetEngine.fetchInitSegment\",\n {\n trackId: rendition.trackId || -1,\n src: rendition.src,\n },\n undefined,\n async () => {\n if (!rendition.trackId) {\n throw new Error(\n \"[fetchInitSegment] Track ID is required for asset metadata\",\n );\n }\n \n // Use rendition ID if provided, otherwise map from trackId\n const renditionId = rendition.id || this.getRenditionId(rendition.trackId);\n const url = this.urlGenerator.generateSegmentUrl(\"init\", renditionId, this);\n \n // Segments are now served directly (not via byte ranges), so use simple fetch\n return this.fetchMedia(url, signal);\n },\n );\n }\n\n async fetchMediaSegment(\n segmentId: number,\n rendition: { id?: RenditionId; trackId: number | undefined; src: string },\n signal?: AbortSignal,\n ) {\n return withSpan(\n \"assetEngine.fetchMediaSegment\",\n {\n segmentId,\n trackId: rendition.trackId || -1,\n src: rendition.src,\n },\n undefined,\n async () => {\n if (!rendition.trackId) {\n throw new Error(\n \"[fetchMediaSegment] Track ID is required for asset metadata\",\n );\n }\n if (segmentId === undefined) {\n throw new Error(\"Segment ID is not available\");\n }\n \n // Use rendition ID if provided, otherwise map from trackId\n const renditionId = rendition.id || this.getRenditionId(rendition.trackId);\n \n // JIT uses 1-based segment IDs, but AssetMediaEngine uses 0-based internally\n // So we need to add 1 to segmentId for the URL\n const jitSegmentId = segmentId + 1;\n const url = this.urlGenerator.generateSegmentUrl(jitSegmentId, renditionId, this);\n\n // Segments are now served directly (not via byte ranges), so use simple fetch\n return this.fetchMedia(url, signal);\n },\n );\n }\n\n /**\n * Calculate audio segments for variable-duration segments using track fragment index\n */\n calculateAudioSegmentRange(\n fromMs: number,\n toMs: number,\n rendition: AudioRendition,\n _durationMs: number,\n ): SegmentTimeRange[] {\n if (fromMs >= toMs || !rendition.trackId) {\n console.warn(\n `calculateAudioSegmentRange: invalid fromMs ${fromMs} toMs ${toMs} rendition ${JSON.stringify(\n rendition,\n )}`,\n );\n return [];\n }\n\n const track = this.data[rendition.trackId];\n if (!track) {\n console.warn(\n `calculateAudioSegmentRange: track not found for rendition ${JSON.stringify(\n rendition,\n )}`,\n );\n return [];\n }\n\n const { timescale, segments } = track;\n const segmentRanges: SegmentTimeRange[] = [];\n\n for (let i = 0; i < segments.length; i++) {\n // biome-ignore lint/style/noNonNullAssertion: we know the segment is not null\n const segment = segments[i]!;\n const segmentStartTime = segment.cts;\n const segmentEndTime = segment.cts + segment.duration;\n\n // Convert to milliseconds\n const segmentStartMs = (segmentStartTime / timescale) * 1000;\n const segmentEndMs = (segmentEndTime / timescale) * 1000;\n\n // Check if segment overlaps with requested time range\n if (segmentStartMs < toMs && segmentEndMs > fromMs) {\n segmentRanges.push({\n segmentId: i, // AssetMediaEngine uses 0-based segment IDs\n startMs: segmentStartMs,\n endMs: segmentEndMs,\n });\n }\n }\n if (segmentRanges.length === 0) {\n console.warn(\n `calculateAudioSegmentRange: no segments found for fromMs ${fromMs} toMs ${toMs} rendition ${JSON.stringify(\n {\n rendition,\n track,\n },\n )}`,\n );\n }\n\n return segmentRanges;\n }\n\n computeSegmentId(seekTimeMs: number, rendition: MediaRendition) {\n if (!rendition.trackId) {\n console.warn(\n `computeSegmentId: trackId not found for rendition ${JSON.stringify(\n rendition,\n )}`,\n );\n throw new Error(\n \"[computeSegmentId] Track ID is required for asset metadata\",\n );\n }\n const track = this.data[rendition.trackId];\n if (!track) {\n throw new Error(\"Track not found\");\n }\n const { timescale, segments } = track;\n\n // Apply startTimeOffsetMs to map user timeline to media timeline for segment selection\n const startTimeOffsetMs =\n (\"startTimeOffsetMs\" in rendition && rendition.startTimeOffsetMs) || 0;\n\n const offsetSeekTimeMs = roundToMilliseconds(\n seekTimeMs + startTimeOffsetMs,\n );\n // Convert to timescale units using consistent precision\n const scaledSeekTime = convertToScaledTime(offsetSeekTimeMs, timescale);\n\n // Find the segment that contains the actual seek time\n for (let i = segments.length - 1; i >= 0; i--) {\n // biome-ignore lint/style/noNonNullAssertion: we know the segment is not null\n const segment = segments[i]!;\n const segmentEndTime = segment.cts + segment.duration;\n\n // Check if the seek time falls within this segment\n if (segment.cts <= scaledSeekTime && scaledSeekTime < segmentEndTime) {\n return i;\n }\n }\n\n // Handle gaps: if no exact segment contains the time, find the nearest one\n // This handles cases where seek time falls between segments (like 8041.667ms)\n let nearestSegmentIndex = 0;\n let nearestDistance = Number.MAX_SAFE_INTEGER;\n\n for (let i = 0; i < segments.length; i++) {\n // biome-ignore lint/style/noNonNullAssertion: we know the segment is not null\n const segment = segments[i]!;\n const segmentStartTime = segment.cts;\n const segmentEndTime = segment.cts + segment.duration;\n\n let distance: number;\n if (scaledSeekTime < segmentStartTime) {\n // Time is before this segment\n distance = segmentStartTime - scaledSeekTime;\n } else if (scaledSeekTime >= segmentEndTime) {\n // Time is after this segment\n distance = scaledSeekTime - segmentEndTime;\n } else {\n // Time is within this segment (should have been caught above, but just in case)\n return i;\n }\n\n if (distance < nearestDistance) {\n nearestDistance = distance;\n nearestSegmentIndex = i;\n }\n }\n\n return nearestSegmentIndex;\n }\n\n getScrubVideoRendition(): VideoRendition | undefined {\n const scrubTrack = this.getScrubTrackIndex();\n\n if (!scrubTrack || scrubTrack.track === undefined) {\n return undefined;\n }\n\n // Calculate segment duration from scrub track segments\n // Scrub tracks use 30-second segments\n const scrubSegmentDurationMs = 30000;\n\n // Calculate segment durations array if segments exist\n const segmentDurationsMs: number[] | undefined =\n scrubTrack.segments.length > 0\n ? scrubTrack.segments.map((segment) => {\n // Convert segment duration from timescale units to milliseconds\n return (segment.duration / scrubTrack.timescale) * 1000;\n })\n : undefined;\n\n return {\n id: \"scrub\" as RenditionId, // Use JIT-style rendition ID\n trackId: scrubTrack.track,\n src: this.src,\n segmentDurationMs: scrubSegmentDurationMs,\n segmentDurationsMs,\n startTimeOffsetMs: scrubTrack.startTimeOffsetMs,\n };\n }\n\n /**\n * Get preferred buffer configuration for this media engine\n * AssetMediaEngine uses lower buffering since segments are already optimized\n */\n getBufferConfig() {\n return {\n // Buffer just 1 segment ahead (~2 seconds) for assets\n videoBufferDurationMs: 2000,\n audioBufferDurationMs: 2000,\n maxVideoBufferFetches: 1,\n maxAudioBufferFetches: 1,\n bufferThresholdMs: 30000, // Timeline-aware buffering threshold\n };\n }\n\n /**\n * Extract thumbnail canvases using main video rendition\n * Note: We prefer main video over scrub track because scrub track in AssetMediaEngine\n * may have incomplete segment data that doesn't cover the full video duration.\n */\n async extractThumbnails(\n timestamps: number[],\n signal?: AbortSignal,\n ): Promise<(ThumbnailResult | null)[]> {\n // Use main video rendition for thumbnails - scrub track may have incomplete segments\n const rendition = this.getVideoRenditionInternal();\n\n if (!rendition) {\n console.warn(\n \"AssetMediaEngine: No video rendition available for thumbnails\",\n );\n return timestamps.map(() => null);\n }\n\n return this.thumbnailExtractor.extractThumbnails(\n timestamps,\n rendition,\n this.durationMs,\n signal,\n );\n }\n\n convertToSegmentRelativeTimestamps(\n globalTimestamps: number[],\n _segmentId: number,\n rendition: VideoRendition,\n ): number[] {\n // For fragmented MP4 (Asset), when we create a mediabunny Input from init+media segment,\n // mediabunny sees the samples with their ABSOLUTE timestamps from the container.\n // This is because the tfdt box contains the baseMediaDecodeTime which is the absolute\n // position of this segment in the container timeline.\n //\n // So we just need to convert user time to container time by adding startTimeOffsetMs,\n // then pass that to mediabunny (in seconds).\n \n const startTimeOffsetMs = rendition.startTimeOffsetMs || 0;\n\n return globalTimestamps.map((globalMs) => {\n // User time -> container time -> seconds for mediabunny\n const containerTimeMs = globalMs + startTimeOffsetMs;\n return containerTimeMs / 1000;\n });\n }\n}\n"],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  import { withSpan } from "../../otel/tracingHelpers.js";
2
- import { RequestDeduplicator } from "../../transcoding/cache/RequestDeduplicator.js";
3
2
  import { SizeAwareLRUCache } from "../../utils/LRUCache.js";
3
+ import { RequestDeduplicator } from "../../transcoding/cache/RequestDeduplicator.js";
4
4
 
5
5
  //#region src/elements/EFMedia/BaseMediaEngine.ts
6
6
  const mediaCache = new SizeAwareLRUCache(100 * 1024 * 1024);
@@ -14,14 +14,14 @@ var BaseMediaEngine = class {
14
14
  * Callers should handle undefined gracefully.
15
15
  */
16
16
  getVideoRendition() {
17
- return this.videoRendition;
17
+ return this.getVideoRenditionInternal();
18
18
  }
19
19
  /**
20
20
  * Get audio rendition if available. Returns undefined for video-only assets.
21
- * Callers should handle undefined gracefully.
21
+ * Callers should handle undefined appropriately.
22
22
  */
23
23
  getAudioRendition() {
24
- return this.audioRendition;
24
+ return this.getAudioRenditionInternal();
25
25
  }
26
26
  /**
27
27
  * Generate cache key for segment requests
@@ -1 +1 @@
1
- {"version":3,"file":"BaseMediaEngine.js","names":["result","tEnd","segments: SegmentTimeRange[]"],"sources":["../../../src/elements/EFMedia/BaseMediaEngine.ts"],"sourcesContent":["import { withSpan } from \"../../otel/tracingHelpers.js\";\nimport { RequestDeduplicator } from \"../../transcoding/cache/RequestDeduplicator.js\";\nimport type {\n AudioRendition,\n SegmentTimeRange,\n ThumbnailResult,\n VideoRendition,\n} from \"../../transcoding/types\";\nimport { SizeAwareLRUCache } from \"../../utils/LRUCache.js\";\nimport type { EFMedia } from \"../EFMedia.js\";\nimport type { MediaRendition } from \"./shared/MediaTaskUtils.js\";\n\n// Global instances shared across all media engines\nexport const mediaCache = new SizeAwareLRUCache<string>(100 * 1024 * 1024); // 100MB cache limit\nexport const globalRequestDeduplicator = new RequestDeduplicator();\n\nexport abstract class BaseMediaEngine {\n protected host: EFMedia;\n\n constructor(host: EFMedia) {\n this.host = host;\n }\n\n abstract get videoRendition(): VideoRendition | undefined;\n abstract get audioRendition(): AudioRendition | undefined;\n\n /**\n * Get video rendition if available. Returns undefined for audio-only assets.\n * Callers should handle undefined gracefully.\n */\n getVideoRendition(): VideoRendition | undefined {\n return this.videoRendition;\n }\n\n /**\n * Get audio rendition if available. Returns undefined for video-only assets.\n * Callers should handle undefined gracefully.\n */\n getAudioRendition(): AudioRendition | undefined {\n return this.audioRendition;\n }\n\n /**\n * Generate cache key for segment requests\n */\n private getSegmentCacheKey(\n segmentId: number,\n rendition: { src: string; trackId: number | undefined; id?: string },\n ): string {\n return `${rendition.src}-${rendition.id}-${segmentId}-${rendition.trackId}`;\n }\n\n /**\n * Unified fetch method with caching and global deduplication\n * All requests (media, manifest, init segments) go through this method\n */\n protected async fetchWithCache(\n url: string,\n options: {\n responseType: \"arrayBuffer\" | \"json\";\n headers?: Record<string, string>;\n signal?: AbortSignal;\n },\n ): Promise<any> {\n return withSpan(\n \"mediaEngine.fetchWithCache\",\n {\n url: url.length > 100 ? `${url.substring(0, 100)}...` : url,\n responseType: options.responseType,\n hasHeaders: !!options.headers,\n },\n undefined,\n async (span) => {\n const t0 = performance.now();\n const { responseType, headers, signal } = options;\n\n // Create cache key that includes URL and headers for proper isolation\n // Note: We don't include signal in cache key as it would prevent proper deduplication\n const cacheKey = headers ? `${url}:${JSON.stringify(headers)}` : url;\n\n // Check cache first\n const t1 = performance.now();\n const cached = mediaCache.get(cacheKey);\n const t2 = performance.now();\n span.setAttribute(\"cacheLookupMs\", Math.round((t2 - t1) * 1000) / 1000);\n\n if (cached) {\n span.setAttribute(\"cacheHit\", true);\n // If we have a cached promise, we need to handle the caller's abort signal\n // without affecting the underlying request that other instances might be using\n if (signal) {\n const t3 = performance.now();\n const result = await this.handleAbortForCachedRequest(\n cached,\n signal,\n );\n const t4 = performance.now();\n span.setAttribute(\n \"handleAbortMs\",\n Math.round((t4 - t3) * 100) / 100,\n );\n span.setAttribute(\n \"totalCacheHitMs\",\n Math.round((t4 - t0) * 100) / 100,\n );\n return result;\n }\n span.setAttribute(\n \"totalCacheHitMs\",\n Math.round((t2 - t0) * 100) / 100,\n );\n return cached;\n }\n\n span.setAttribute(\"cacheHit\", false);\n\n // Use global deduplicator to prevent concurrent requests for the same resource\n // Note: We do NOT pass the signal to the deduplicator - each caller manages their own abort\n const promise = globalRequestDeduplicator.executeRequest(\n cacheKey,\n async () => {\n const fetchStart = performance.now();\n try {\n // Pass the signal to host.fetch() so network requests can be canceled when tasks are aborted\n // If multiple callers are waiting on the same request and one aborts, the request will be canceled\n // Other callers will get an error, but they can retry if needed\n const response = await this.host.fetch(url, { headers, signal });\n const fetchEnd = performance.now();\n span.setAttribute(\"fetchMs\", fetchEnd - fetchStart);\n\n // Check response status before parsing\n if (!response.ok) {\n // Read body once - can't read again after this\n const text = await response.text();\n throw new Error(`Failed to fetch: ${response.status} ${text.substring(0, 100)}`);\n }\n\n if (responseType === \"json\") {\n // Check content type header (doesn't consume body)\n const contentType = response.headers.get(\"content-type\");\n if (contentType && !contentType.includes(\"application/json\") && !contentType.includes(\"text/json\")) {\n // Read body once - can't read again after this\n const text = await response.text();\n throw new Error(`Expected JSON but got ${contentType}: ${text.substring(0, 100)}`);\n }\n try {\n // Read body once as JSON\n return await response.json();\n } catch (error) {\n // JSON parse failed - body is already consumed, can't read again\n // The error should contain enough info, but if we need the text, we'd need to clone the response first\n throw new Error(`Failed to parse JSON response: ${error instanceof Error ? error.message : String(error)}`);\n }\n }\n const buffer = await response.arrayBuffer();\n span.setAttribute(\"sizeBytes\", buffer.byteLength);\n return buffer;\n } catch (error) {\n // If the request was aborted, don't cache the error\n if (\n error instanceof DOMException &&\n error.name === \"AbortError\"\n ) {\n // Remove from cache so other requests can retry\n mediaCache.delete(cacheKey);\n }\n throw error;\n }\n },\n );\n\n // Cache the promise (not the result) to handle concurrent requests\n mediaCache.set(cacheKey, promise);\n\n // Handle the case where the promise might be aborted\n promise.catch((error) => {\n // If the request was aborted, remove it from cache to prevent corrupted data\n if (error instanceof DOMException && error.name === \"AbortError\") {\n mediaCache.delete(cacheKey);\n }\n });\n\n // If the caller has a signal, handle abort logic without affecting the underlying request\n if (signal) {\n const result = await this.handleAbortForCachedRequest(\n promise,\n signal,\n );\n const tEnd = performance.now();\n span.setAttribute(\n \"totalFetchMs\",\n Math.round((tEnd - t0) * 100) / 100,\n );\n return result;\n }\n\n const result = await promise;\n const tEnd = performance.now();\n span.setAttribute(\"totalFetchMs\", Math.round((tEnd - t0) * 100) / 100);\n return result;\n },\n );\n }\n\n /**\n * Handles abort logic for a cached request without affecting the underlying fetch\n * This allows multiple instances to share the same cached request while each\n * manages their own abort behavior\n */\n private handleAbortForCachedRequest<T>(\n promise: Promise<T>,\n signal: AbortSignal,\n ): Promise<T> {\n // If signal is already aborted, reject immediately\n if (signal.aborted) {\n throw new DOMException(\"Aborted\", \"AbortError\");\n }\n\n // Return a promise that respects the caller's abort signal\n // but doesn't affect the underlying cached request\n return Promise.race([\n promise,\n new Promise<never>((_, reject) => {\n signal.addEventListener(\"abort\", () => {\n reject(new DOMException(\"Aborted\", \"AbortError\"));\n });\n }),\n ]);\n }\n\n // Public wrapper methods that delegate to fetchWithCache\n async fetchMedia(url: string, signal?: AbortSignal): Promise<ArrayBuffer> {\n // Check abort signal immediately before any processing\n if (signal?.aborted) {\n throw new DOMException(\"Aborted\", \"AbortError\");\n }\n return this.fetchWithCache(url, { responseType: \"arrayBuffer\", signal });\n }\n\n async fetchManifest(url: string, signal?: AbortSignal): Promise<any> {\n // Check abort signal immediately before any processing\n if (signal?.aborted) {\n throw new DOMException(\"Aborted\", \"AbortError\");\n }\n return this.fetchWithCache(url, { responseType: \"json\", signal });\n }\n\n async fetchMediaWithHeaders(\n url: string,\n headers: Record<string, string>,\n signal?: AbortSignal,\n ): Promise<ArrayBuffer> {\n // Check abort signal immediately before any processing\n if (signal?.aborted) {\n throw new DOMException(\"Aborted\", \"AbortError\");\n }\n return this.fetchWithCache(url, {\n responseType: \"arrayBuffer\",\n headers,\n signal,\n });\n }\n\n // Legacy methods for backward compatibility\n async fetchMediaCache(\n url: string,\n signal?: AbortSignal,\n ): Promise<ArrayBuffer> {\n return this.fetchMedia(url, signal);\n }\n\n async fetchManifestCache(url: string, signal?: AbortSignal): Promise<any> {\n return this.fetchManifest(url, signal);\n }\n\n async fetchMediaCacheWithHeaders(\n url: string,\n headers: Record<string, string>,\n signal?: AbortSignal,\n ): Promise<ArrayBuffer> {\n return this.fetchMediaWithHeaders(url, headers, signal);\n }\n\n /**\n * Abstract method for actual segment fetching - implemented by subclasses\n */\n abstract fetchMediaSegment(\n segmentId: number,\n rendition: { trackId: number | undefined; src: string },\n signal?: AbortSignal,\n ): Promise<ArrayBuffer>;\n\n abstract fetchInitSegment(\n rendition: { trackId: number | undefined; src: string },\n signal?: AbortSignal,\n ): Promise<ArrayBuffer>;\n\n abstract computeSegmentId(\n desiredSeekTimeMs: number,\n rendition: MediaRendition,\n ): number | undefined;\n\n /**\n * Fetch media segment with built-in deduplication\n * Now uses global deduplication for all requests\n */\n async fetchMediaSegmentWithDeduplication(\n segmentId: number,\n rendition: { trackId: number | undefined; src: string },\n signal?: AbortSignal,\n ): Promise<ArrayBuffer> {\n const cacheKey = this.getSegmentCacheKey(segmentId, rendition);\n\n return globalRequestDeduplicator.executeRequest(cacheKey, async () => {\n return this.fetchMediaSegment(segmentId, rendition, signal);\n });\n }\n\n /**\n * Check if a segment is currently being fetched\n */\n isSegmentBeingFetched(\n segmentId: number,\n rendition: { src: string; trackId: number | undefined },\n ): boolean {\n const cacheKey = this.getSegmentCacheKey(segmentId, rendition);\n return globalRequestDeduplicator.isPending(cacheKey);\n }\n\n /**\n * Get count of active segment requests (for debugging/monitoring)\n */\n getActiveSegmentRequestCount(): number {\n return globalRequestDeduplicator.getPendingCount();\n }\n\n /**\n * Cancel all active segment requests (for cleanup)\n */\n cancelAllSegmentRequests(): void {\n globalRequestDeduplicator.clear();\n }\n\n /**\n * Calculate audio segments needed for a time range\n * Each media engine implements this based on their segment structure\n */\n calculateAudioSegmentRange(\n fromMs: number,\n toMs: number,\n rendition: AudioRendition,\n durationMs: number,\n ): SegmentTimeRange[] {\n // Default implementation for uniform segments (used by JitMediaEngine)\n if (fromMs >= toMs) {\n return [];\n }\n\n const segments: SegmentTimeRange[] = [];\n\n // Use actual segment durations if available (more accurate)\n if (\n rendition.segmentDurationsMs &&\n rendition.segmentDurationsMs.length > 0\n ) {\n let cumulativeTime = 0;\n\n for (let i = 0; i < rendition.segmentDurationsMs.length; i++) {\n const segmentDuration = rendition.segmentDurationsMs[i];\n if (segmentDuration === undefined) {\n continue; // Skip undefined segment durations\n }\n const segmentStartMs = cumulativeTime;\n const segmentEndMs = Math.min(\n cumulativeTime + segmentDuration,\n durationMs,\n );\n\n // Don't include segments that start at or beyond the file duration\n if (segmentStartMs >= durationMs) {\n break;\n }\n\n // Only include segments that overlap with requested time range\n if (segmentStartMs < toMs && segmentEndMs > fromMs) {\n segments.push({\n segmentId: i + 1, // Convert to 1-based\n startMs: segmentStartMs,\n endMs: segmentEndMs,\n });\n }\n\n cumulativeTime += segmentDuration;\n\n // If we've reached or exceeded file duration, stop\n if (cumulativeTime >= durationMs) {\n break;\n }\n }\n\n return segments;\n }\n\n // Fall back to fixed duration calculation for backward compatibility\n const segmentDurationMs = rendition.segmentDurationMs || 1000;\n const startSegmentIndex = Math.floor(fromMs / segmentDurationMs);\n const endSegmentIndex = Math.floor(toMs / segmentDurationMs);\n\n for (let i = startSegmentIndex; i <= endSegmentIndex; i++) {\n const segmentId = i + 1; // Convert to 1-based\n const segmentStartMs = i * segmentDurationMs;\n const segmentEndMs = Math.min((i + 1) * segmentDurationMs, durationMs);\n\n // Don't include segments that start at or beyond the file duration\n if (segmentStartMs >= durationMs) {\n break;\n }\n\n // Only include segments that overlap with requested time range\n if (segmentStartMs < toMs && segmentEndMs > fromMs) {\n segments.push({\n segmentId,\n startMs: segmentStartMs,\n endMs: segmentEndMs,\n });\n }\n }\n\n return segments;\n }\n\n /**\n * Check if a segment is cached for a given rendition\n * This needs to check the URL-based cache since that's where segments are actually stored\n */\n isSegmentCached(\n segmentId: number,\n rendition: AudioRendition | VideoRendition,\n ): boolean {\n try {\n // Check if this is a JIT engine by looking for urlGenerator property\n const maybeJitEngine = this as any;\n if (\n maybeJitEngine.urlGenerator &&\n typeof maybeJitEngine.urlGenerator.generateSegmentUrl === \"function\"\n ) {\n // This is a JIT engine - generate the URL and check URL-based cache\n if (!rendition.id) {\n return false;\n }\n\n const segmentUrl = maybeJitEngine.urlGenerator.generateSegmentUrl(\n segmentId,\n rendition.id,\n maybeJitEngine,\n );\n const urlIsCached = mediaCache.has(segmentUrl);\n\n return urlIsCached;\n }\n // For other engine types, fall back to the old segment-based key approach\n const cacheKey = `${rendition.src}-${rendition.id || \"default\"}-${segmentId}-${rendition.trackId}`;\n const isCached = mediaCache.has(cacheKey);\n return isCached;\n } catch (error) {\n console.warn(\n `🎬 BaseMediaEngine: Error checking if segment ${segmentId} is cached:`,\n error,\n );\n return false;\n }\n }\n\n /**\n * Get cached segment IDs from a list for a given rendition\n */\n getCachedSegments(\n segmentIds: number[],\n rendition: AudioRendition | VideoRendition,\n ): Set<number> {\n return new Set(\n segmentIds.filter((id) => this.isSegmentCached(id, rendition)),\n );\n }\n\n /**\n * Extract thumbnail canvases at multiple timestamps efficiently\n * Default implementation provides helpful error information\n */\n async extractThumbnails(\n timestamps: number[],\n signal?: AbortSignal,\n ): Promise<(ThumbnailResult | null)[]> {\n const engineName = this.constructor.name;\n console.warn(\n `${engineName}: extractThumbnails not properly implemented. ` +\n \"This MediaEngine type does not support thumbnail generation. \" +\n \"Supported engines: JitMediaEngine. \" +\n `Requested ${timestamps.length} thumbnail${timestamps.length === 1 ? \"\" : \"s\"}.`,\n );\n return timestamps.map(() => null);\n }\n\n abstract convertToSegmentRelativeTimestamps(\n globalTimestamps: number[],\n segmentId: number,\n rendition: VideoRendition,\n ): number[];\n\n /**\n * Get buffer configuration for this media engine\n * Can be overridden by subclasses to provide custom buffer settings\n */\n getBufferConfig(): {\n videoBufferDurationMs: number;\n audioBufferDurationMs: number;\n maxVideoBufferFetches: number;\n maxAudioBufferFetches: number;\n bufferThresholdMs: number;\n } {\n return {\n videoBufferDurationMs: 10000, // 10 seconds\n audioBufferDurationMs: 10000, // 10 seconds\n maxVideoBufferFetches: 3,\n maxAudioBufferFetches: 3,\n bufferThresholdMs: 30000, // 30 seconds - timeline-aware buffering threshold\n };\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"BaseMediaEngine.js","names":["result","tEnd","segments: SegmentTimeRange[]"],"sources":["../../../src/elements/EFMedia/BaseMediaEngine.ts"],"sourcesContent":["import { withSpan } from \"../../otel/tracingHelpers.js\";\nimport { RequestDeduplicator } from \"../../transcoding/cache/RequestDeduplicator.js\";\nimport type {\n AudioRendition,\n SegmentTimeRange,\n ThumbnailResult,\n VideoRendition,\n} from \"../../transcoding/types\";\nimport { SizeAwareLRUCache } from \"../../utils/LRUCache.js\";\nimport type { EFMedia } from \"../EFMedia.js\";\nimport type { MediaRendition } from \"./shared/MediaTaskUtils.js\";\n\n// Global instances shared across all media engines\nexport const mediaCache = new SizeAwareLRUCache<string>(100 * 1024 * 1024); // 100MB cache limit\nexport const globalRequestDeduplicator = new RequestDeduplicator();\n\nexport abstract class BaseMediaEngine {\n protected host: EFMedia;\n\n constructor(host: EFMedia) {\n this.host = host;\n }\n\n // Use protected abstract methods instead of abstract getters to avoid TypeScript bug\n // See: https://github.com/microsoft/TypeScript/issues/58020\n // Note: Abstract getters ALSO trigger this bug, not just getters in object literals\n protected abstract getVideoRenditionInternal(): VideoRendition | undefined;\n protected abstract getAudioRenditionInternal(): AudioRendition | undefined;\n\n /**\n * Get video rendition if available. Returns undefined for audio-only assets.\n * Callers should handle undefined gracefully.\n */\n getVideoRendition(): VideoRendition | undefined {\n return this.getVideoRenditionInternal();\n }\n\n /**\n * Get audio rendition if available. Returns undefined for video-only assets.\n * Callers should handle undefined appropriately.\n */\n getAudioRendition(): AudioRendition | undefined {\n return this.getAudioRenditionInternal();\n }\n\n /**\n * Generate cache key for segment requests\n */\n private getSegmentCacheKey(\n segmentId: number,\n rendition: { src: string; trackId: number | undefined; id?: string },\n ): string {\n return `${rendition.src}-${rendition.id}-${segmentId}-${rendition.trackId}`;\n }\n\n /**\n * Unified fetch method with caching and global deduplication\n * All requests (media, manifest, init segments) go through this method\n */\n protected async fetchWithCache(\n url: string,\n options: {\n responseType: \"arrayBuffer\" | \"json\";\n headers?: Record<string, string>;\n signal?: AbortSignal;\n },\n ): Promise<any> {\n return withSpan(\n \"mediaEngine.fetchWithCache\",\n {\n url: url.length > 100 ? `${url.substring(0, 100)}...` : url,\n responseType: options.responseType,\n hasHeaders: !!options.headers,\n },\n undefined,\n async (span) => {\n const t0 = performance.now();\n const { responseType, headers, signal } = options;\n\n // Create cache key that includes URL and headers for proper isolation\n // Note: We don't include signal in cache key as it would prevent proper deduplication\n const cacheKey = headers ? `${url}:${JSON.stringify(headers)}` : url;\n\n // Check cache first\n const t1 = performance.now();\n const cached = mediaCache.get(cacheKey);\n const t2 = performance.now();\n span.setAttribute(\"cacheLookupMs\", Math.round((t2 - t1) * 1000) / 1000);\n\n if (cached) {\n span.setAttribute(\"cacheHit\", true);\n // If we have a cached promise, we need to handle the caller's abort signal\n // without affecting the underlying request that other instances might be using\n if (signal) {\n const t3 = performance.now();\n const result = await this.handleAbortForCachedRequest(\n cached,\n signal,\n );\n const t4 = performance.now();\n span.setAttribute(\n \"handleAbortMs\",\n Math.round((t4 - t3) * 100) / 100,\n );\n span.setAttribute(\n \"totalCacheHitMs\",\n Math.round((t4 - t0) * 100) / 100,\n );\n return result;\n }\n span.setAttribute(\n \"totalCacheHitMs\",\n Math.round((t2 - t0) * 100) / 100,\n );\n return cached;\n }\n\n span.setAttribute(\"cacheHit\", false);\n\n // Use global deduplicator to prevent concurrent requests for the same resource\n // Note: We do NOT pass the signal to the deduplicator - each caller manages their own abort\n const promise = globalRequestDeduplicator.executeRequest(\n cacheKey,\n async () => {\n const fetchStart = performance.now();\n try {\n // Pass the signal to host.fetch() so network requests can be canceled when tasks are aborted\n // If multiple callers are waiting on the same request and one aborts, the request will be canceled\n // Other callers will get an error, but they can retry if needed\n const response = await this.host.fetch(url, { headers, signal });\n const fetchEnd = performance.now();\n span.setAttribute(\"fetchMs\", fetchEnd - fetchStart);\n\n // Check response status before parsing\n if (!response.ok) {\n // Read body once - can't read again after this\n const text = await response.text();\n throw new Error(`Failed to fetch: ${response.status} ${text.substring(0, 100)}`);\n }\n\n if (responseType === \"json\") {\n // Check content type header (doesn't consume body)\n const contentType = response.headers.get(\"content-type\");\n if (contentType && !contentType.includes(\"application/json\") && !contentType.includes(\"text/json\")) {\n // Read body once - can't read again after this\n const text = await response.text();\n throw new Error(`Expected JSON but got ${contentType}: ${text.substring(0, 100)}`);\n }\n try {\n // Read body once as JSON\n return await response.json();\n } catch (error) {\n // JSON parse failed - body is already consumed, can't read again\n // The error should contain enough info, but if we need the text, we'd need to clone the response first\n throw new Error(`Failed to parse JSON response: ${error instanceof Error ? error.message : String(error)}`);\n }\n }\n const buffer = await response.arrayBuffer();\n span.setAttribute(\"sizeBytes\", buffer.byteLength);\n return buffer;\n } catch (error) {\n // If the request was aborted, don't cache the error\n if (\n error instanceof DOMException &&\n error.name === \"AbortError\"\n ) {\n // Remove from cache so other requests can retry\n mediaCache.delete(cacheKey);\n }\n throw error;\n }\n },\n );\n\n // Cache the promise (not the result) to handle concurrent requests\n mediaCache.set(cacheKey, promise);\n\n // Handle the case where the promise might be aborted\n promise.catch((error) => {\n // If the request was aborted, remove it from cache to prevent corrupted data\n if (error instanceof DOMException && error.name === \"AbortError\") {\n mediaCache.delete(cacheKey);\n }\n });\n\n // If the caller has a signal, handle abort logic without affecting the underlying request\n if (signal) {\n const result = await this.handleAbortForCachedRequest(\n promise,\n signal,\n );\n const tEnd = performance.now();\n span.setAttribute(\n \"totalFetchMs\",\n Math.round((tEnd - t0) * 100) / 100,\n );\n return result;\n }\n\n const result = await promise;\n const tEnd = performance.now();\n span.setAttribute(\"totalFetchMs\", Math.round((tEnd - t0) * 100) / 100);\n return result;\n },\n );\n }\n\n /**\n * Handles abort logic for a cached request without affecting the underlying fetch\n * This allows multiple instances to share the same cached request while each\n * manages their own abort behavior\n */\n private handleAbortForCachedRequest<T>(\n promise: Promise<T>,\n signal: AbortSignal,\n ): Promise<T> {\n // If signal is already aborted, reject immediately\n if (signal.aborted) {\n throw new DOMException(\"Aborted\", \"AbortError\");\n }\n\n // Return a promise that respects the caller's abort signal\n // but doesn't affect the underlying cached request\n return Promise.race([\n promise,\n new Promise<never>((_, reject) => {\n signal.addEventListener(\"abort\", () => {\n reject(new DOMException(\"Aborted\", \"AbortError\"));\n });\n }),\n ]);\n }\n\n // Public wrapper methods that delegate to fetchWithCache\n async fetchMedia(url: string, signal?: AbortSignal): Promise<ArrayBuffer> {\n // Check abort signal immediately before any processing\n if (signal?.aborted) {\n throw new DOMException(\"Aborted\", \"AbortError\");\n }\n return this.fetchWithCache(url, { responseType: \"arrayBuffer\", signal });\n }\n\n async fetchManifest(url: string, signal?: AbortSignal): Promise<any> {\n // Check abort signal immediately before any processing\n if (signal?.aborted) {\n throw new DOMException(\"Aborted\", \"AbortError\");\n }\n return this.fetchWithCache(url, { responseType: \"json\", signal });\n }\n\n async fetchMediaWithHeaders(\n url: string,\n headers: Record<string, string>,\n signal?: AbortSignal,\n ): Promise<ArrayBuffer> {\n // Check abort signal immediately before any processing\n if (signal?.aborted) {\n throw new DOMException(\"Aborted\", \"AbortError\");\n }\n return this.fetchWithCache(url, {\n responseType: \"arrayBuffer\",\n headers,\n signal,\n });\n }\n\n // Legacy methods for backward compatibility\n async fetchMediaCache(\n url: string,\n signal?: AbortSignal,\n ): Promise<ArrayBuffer> {\n return this.fetchMedia(url, signal);\n }\n\n async fetchManifestCache(url: string, signal?: AbortSignal): Promise<any> {\n return this.fetchManifest(url, signal);\n }\n\n async fetchMediaCacheWithHeaders(\n url: string,\n headers: Record<string, string>,\n signal?: AbortSignal,\n ): Promise<ArrayBuffer> {\n return this.fetchMediaWithHeaders(url, headers, signal);\n }\n\n /**\n * Abstract method for actual segment fetching - implemented by subclasses\n */\n abstract fetchMediaSegment(\n segmentId: number,\n rendition: { trackId: number | undefined; src: string },\n signal?: AbortSignal,\n ): Promise<ArrayBuffer>;\n\n abstract fetchInitSegment(\n rendition: { trackId: number | undefined; src: string },\n signal?: AbortSignal,\n ): Promise<ArrayBuffer>;\n\n abstract computeSegmentId(\n desiredSeekTimeMs: number,\n rendition: MediaRendition,\n ): number | undefined;\n\n /**\n * Fetch media segment with built-in deduplication\n * Now uses global deduplication for all requests\n */\n async fetchMediaSegmentWithDeduplication(\n segmentId: number,\n rendition: { trackId: number | undefined; src: string },\n signal?: AbortSignal,\n ): Promise<ArrayBuffer> {\n const cacheKey = this.getSegmentCacheKey(segmentId, rendition);\n\n return globalRequestDeduplicator.executeRequest(cacheKey, async () => {\n return this.fetchMediaSegment(segmentId, rendition, signal);\n });\n }\n\n /**\n * Check if a segment is currently being fetched\n */\n isSegmentBeingFetched(\n segmentId: number,\n rendition: { src: string; trackId: number | undefined },\n ): boolean {\n const cacheKey = this.getSegmentCacheKey(segmentId, rendition);\n return globalRequestDeduplicator.isPending(cacheKey);\n }\n\n /**\n * Get count of active segment requests (for debugging/monitoring)\n */\n getActiveSegmentRequestCount(): number {\n return globalRequestDeduplicator.getPendingCount();\n }\n\n /**\n * Cancel all active segment requests (for cleanup)\n */\n cancelAllSegmentRequests(): void {\n globalRequestDeduplicator.clear();\n }\n\n /**\n * Calculate audio segments needed for a time range\n * Each media engine implements this based on their segment structure\n */\n calculateAudioSegmentRange(\n fromMs: number,\n toMs: number,\n rendition: AudioRendition,\n durationMs: number,\n ): SegmentTimeRange[] {\n // Default implementation for uniform segments (used by JitMediaEngine)\n if (fromMs >= toMs) {\n return [];\n }\n\n const segments: SegmentTimeRange[] = [];\n\n // Use actual segment durations if available (more accurate)\n if (\n rendition.segmentDurationsMs &&\n rendition.segmentDurationsMs.length > 0\n ) {\n let cumulativeTime = 0;\n\n for (let i = 0; i < rendition.segmentDurationsMs.length; i++) {\n const segmentDuration = rendition.segmentDurationsMs[i];\n if (segmentDuration === undefined) {\n continue; // Skip undefined segment durations\n }\n const segmentStartMs = cumulativeTime;\n const segmentEndMs = Math.min(\n cumulativeTime + segmentDuration,\n durationMs,\n );\n\n // Don't include segments that start at or beyond the file duration\n if (segmentStartMs >= durationMs) {\n break;\n }\n\n // Only include segments that overlap with requested time range\n if (segmentStartMs < toMs && segmentEndMs > fromMs) {\n segments.push({\n segmentId: i + 1, // Convert to 1-based\n startMs: segmentStartMs,\n endMs: segmentEndMs,\n });\n }\n\n cumulativeTime += segmentDuration;\n\n // If we've reached or exceeded file duration, stop\n if (cumulativeTime >= durationMs) {\n break;\n }\n }\n\n return segments;\n }\n\n // Fall back to fixed duration calculation for backward compatibility\n const segmentDurationMs = rendition.segmentDurationMs || 1000;\n const startSegmentIndex = Math.floor(fromMs / segmentDurationMs);\n const endSegmentIndex = Math.floor(toMs / segmentDurationMs);\n\n for (let i = startSegmentIndex; i <= endSegmentIndex; i++) {\n const segmentId = i + 1; // Convert to 1-based\n const segmentStartMs = i * segmentDurationMs;\n const segmentEndMs = Math.min((i + 1) * segmentDurationMs, durationMs);\n\n // Don't include segments that start at or beyond the file duration\n if (segmentStartMs >= durationMs) {\n break;\n }\n\n // Only include segments that overlap with requested time range\n if (segmentStartMs < toMs && segmentEndMs > fromMs) {\n segments.push({\n segmentId,\n startMs: segmentStartMs,\n endMs: segmentEndMs,\n });\n }\n }\n\n return segments;\n }\n\n /**\n * Check if a segment is cached for a given rendition\n * This needs to check the URL-based cache since that's where segments are actually stored\n */\n isSegmentCached(\n segmentId: number,\n rendition: AudioRendition | VideoRendition,\n ): boolean {\n try {\n // Check if this is a JIT engine by looking for urlGenerator property\n const maybeJitEngine = this as any;\n if (\n maybeJitEngine.urlGenerator &&\n typeof maybeJitEngine.urlGenerator.generateSegmentUrl === \"function\"\n ) {\n // This is a JIT engine - generate the URL and check URL-based cache\n if (!rendition.id) {\n return false;\n }\n\n const segmentUrl = maybeJitEngine.urlGenerator.generateSegmentUrl(\n segmentId,\n rendition.id,\n maybeJitEngine,\n );\n const urlIsCached = mediaCache.has(segmentUrl);\n\n return urlIsCached;\n }\n // For other engine types, fall back to the old segment-based key approach\n const cacheKey = `${rendition.src}-${rendition.id || \"default\"}-${segmentId}-${rendition.trackId}`;\n const isCached = mediaCache.has(cacheKey);\n return isCached;\n } catch (error) {\n console.warn(\n `🎬 BaseMediaEngine: Error checking if segment ${segmentId} is cached:`,\n error,\n );\n return false;\n }\n }\n\n /**\n * Get cached segment IDs from a list for a given rendition\n */\n getCachedSegments(\n segmentIds: number[],\n rendition: AudioRendition | VideoRendition,\n ): Set<number> {\n return new Set(\n segmentIds.filter((id) => this.isSegmentCached(id, rendition)),\n );\n }\n\n /**\n * Extract thumbnail canvases at multiple timestamps efficiently\n * Default implementation provides helpful error information\n */\n async extractThumbnails(\n timestamps: number[],\n signal?: AbortSignal,\n ): Promise<(ThumbnailResult | null)[]> {\n const engineName = this.constructor.name;\n console.warn(\n `${engineName}: extractThumbnails not properly implemented. ` +\n \"This MediaEngine type does not support thumbnail generation. \" +\n \"Supported engines: JitMediaEngine. \" +\n `Requested ${timestamps.length} thumbnail${timestamps.length === 1 ? \"\" : \"s\"}.`,\n );\n return timestamps.map(() => null);\n }\n\n abstract convertToSegmentRelativeTimestamps(\n globalTimestamps: number[],\n segmentId: number,\n rendition: VideoRendition,\n ): number[];\n\n /**\n * Get buffer configuration for this media engine\n * Can be overridden by subclasses to provide custom buffer settings\n */\n getBufferConfig(): {\n videoBufferDurationMs: number;\n audioBufferDurationMs: number;\n maxVideoBufferFetches: number;\n maxAudioBufferFetches: number;\n bufferThresholdMs: number;\n } {\n return {\n videoBufferDurationMs: 10000, // 10 seconds\n audioBufferDurationMs: 10000, // 10 seconds\n maxVideoBufferFetches: 3,\n maxAudioBufferFetches: 3,\n bufferThresholdMs: 30000, // 30 seconds - timeline-aware buffering threshold\n };\n }\n}\n"],"mappings":"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"}
@@ -145,7 +145,7 @@ var BufferedSeekingInput = class {
145
145
  const roundedTimeMs = roundToMilliseconds(timeMs);
146
146
  const firstTimestampMs = roundToMilliseconds(await track.getFirstTimestamp() * 1e3);
147
147
  span.setAttribute("firstTimestampMs", firstTimestampMs);
148
- if (roundedTimeMs < firstTimestampMs) {
148
+ if (roundedTimeMs < firstTimestampMs - .01) {
149
149
  console.error("Seeking outside bounds of input", {
150
150
  roundedTimeMs,
151
151
  firstTimestampMs
@@ -1 +1 @@
1
- {"version":3,"file":"BufferedSeekingInput.js","names":["defaultOptions: BufferedSeekingInputOptions","track","bufferSize","#seekLock","contents"],"sources":["../../../src/elements/EFMedia/BufferedSeekingInput.ts"],"sourcesContent":["import {\n AudioSampleSink,\n BufferSource,\n Input,\n InputAudioTrack,\n type InputTrack,\n InputVideoTrack,\n MP4,\n VideoSampleSink,\n} from \"mediabunny\";\nimport { withSpan } from \"../../otel/tracingHelpers.js\";\nimport { type MediaSample, SampleBuffer } from \"../SampleBuffer\";\nimport { roundToMilliseconds } from \"./shared/PrecisionUtils\";\n\ninterface BufferedSeekingInputOptions {\n videoBufferSize?: number;\n audioBufferSize?: number;\n /**\n * Timeline offset in milliseconds to map user timeline to media timeline.\n * Applied during seeking to handle media that doesn't start at 0ms.\n */\n startTimeOffsetMs?: number;\n}\n\nconst defaultOptions: BufferedSeekingInputOptions = {\n videoBufferSize: 30,\n audioBufferSize: 100,\n startTimeOffsetMs: 0,\n};\n\nexport class NoSample extends RangeError {}\n\nexport class ConcurrentSeekError extends RangeError {}\n\nexport class BufferedSeekingInput {\n private input: Input;\n private trackIterators: Map<number, AsyncIterator<MediaSample>> = new Map();\n private trackBuffers: Map<number, SampleBuffer> = new Map();\n private options: BufferedSeekingInputOptions;\n // Separate locks for different operation types to prevent unnecessary blocking\n private trackIteratorCreationPromises: Map<number, Promise<any>> = new Map();\n private trackSeekPromises: Map<number, Promise<any>> = new Map();\n\n /**\n * Timeline offset in milliseconds to map user timeline to media timeline.\n * Applied during seeking to handle media that doesn't start at 0ms.\n */\n private readonly startTimeOffsetMs: number;\n\n constructor(arrayBuffer: ArrayBuffer, options?: BufferedSeekingInputOptions) {\n const bufferSource = new BufferSource(arrayBuffer);\n const input = new Input({\n source: bufferSource,\n formats: [MP4],\n });\n this.input = input;\n this.options = { ...defaultOptions, ...options };\n this.startTimeOffsetMs = this.options.startTimeOffsetMs ?? 0;\n }\n\n // Buffer inspection API for testing\n getBufferSize(trackId: number): number {\n const buffer = this.trackBuffers.get(trackId);\n return buffer ? buffer.length : 0;\n }\n\n getBufferContents(trackId: number): readonly MediaSample[] {\n const buffer = this.trackBuffers.get(trackId);\n return buffer ? Object.freeze([...buffer.getContents()]) : [];\n }\n\n getBufferTimestamps(trackId: number): number[] {\n const contents = this.getBufferContents(trackId);\n return contents.map((sample) => sample.timestamp || 0);\n }\n\n clearBuffer(trackId: number): void {\n const buffer = this.trackBuffers.get(trackId);\n if (buffer) {\n buffer.clear();\n }\n }\n\n computeDuration() {\n return this.input.computeDuration();\n }\n\n async getTrack(trackId: number) {\n const tracks = await this.input.getTracks();\n const track = tracks.find((track) => track.id === trackId);\n if (!track) {\n throw new Error(`Track ${trackId} not found`);\n }\n return track;\n }\n\n async getAudioTrack(trackId: number) {\n const tracks = await this.input.getAudioTracks();\n const track = tracks.find(\n (track) => track.id === trackId && track.type === \"audio\",\n );\n if (!track) {\n throw new Error(`Track ${trackId} not found`);\n }\n return track;\n }\n\n async getVideoTrack(trackId: number) {\n const tracks = await this.input.getVideoTracks();\n const track = tracks.find(\n (track) => track.id === trackId && track.type === \"video\",\n );\n if (!track) {\n throw new Error(`Track ${trackId} not found`);\n }\n return track;\n }\n\n async getFirstVideoTrack() {\n const tracks = await this.input.getVideoTracks();\n return tracks[0];\n }\n\n async getFirstAudioTrack() {\n const tracks = await this.input.getAudioTracks();\n return tracks[0];\n }\n\n getTrackIterator(track: InputTrack) {\n if (this.trackIterators.has(track.id)) {\n // biome-ignore lint/style/noNonNullAssertion: we know the map has the key\n return this.trackIterators.get(track.id)!;\n }\n\n const trackIterator = this.createTrackIterator(track);\n\n this.trackIterators.set(track.id, trackIterator);\n\n return trackIterator;\n }\n\n createTrackSampleSink(track: InputTrack) {\n if (track instanceof InputAudioTrack) {\n return new AudioSampleSink(track);\n }\n if (track instanceof InputVideoTrack) {\n return new VideoSampleSink(track);\n }\n throw new Error(`Unsupported track type ${track.type}`);\n }\n\n createTrackIterator(track: InputTrack) {\n const sampleSink = this.createTrackSampleSink(track);\n return sampleSink.samples();\n }\n\n createTrackBuffer(track: InputTrack) {\n if (track.type === \"audio\") {\n const bufferSize = this.options.audioBufferSize;\n const sampleBuffer = new SampleBuffer(bufferSize);\n return sampleBuffer;\n }\n const bufferSize = this.options.videoBufferSize;\n const sampleBuffer = new SampleBuffer(bufferSize);\n return sampleBuffer;\n }\n\n getTrackBuffer(track: InputTrack) {\n const maybeTrackBuffer = this.trackBuffers.get(track.id);\n\n if (maybeTrackBuffer) {\n return maybeTrackBuffer;\n }\n\n const trackBuffer = this.createTrackBuffer(track);\n this.trackBuffers.set(track.id, trackBuffer);\n return trackBuffer;\n }\n\n async seek(trackId: number, timeMs: number) {\n return withSpan(\n \"bufferedInput.seek\",\n {\n trackId,\n timeMs,\n startTimeOffsetMs: this.startTimeOffsetMs,\n },\n undefined,\n async (span) => {\n // Apply timeline offset to map user timeline to media timeline\n const mediaTimeMs = timeMs + this.startTimeOffsetMs;\n\n // Round using consistent precision handling\n const roundedMediaTimeMs = roundToMilliseconds(mediaTimeMs);\n span.setAttribute(\"roundedMediaTimeMs\", roundedMediaTimeMs);\n\n // Serialize seek operations per track (but don't block iterator creation)\n const existingSeek = this.trackSeekPromises.get(trackId);\n if (existingSeek) {\n span.setAttribute(\"waitedForExistingSeek\", true);\n await existingSeek;\n }\n\n const seekPromise = this.seekSafe(trackId, roundedMediaTimeMs);\n this.trackSeekPromises.set(trackId, seekPromise);\n\n try {\n return await seekPromise;\n } finally {\n this.trackSeekPromises.delete(trackId);\n }\n },\n );\n }\n\n private async resetIterator(track: InputTrack) {\n const trackBuffer = this.trackBuffers.get(track.id);\n trackBuffer?.clear();\n // Clean up iterator safely - wait for any ongoing iterator creation\n const ongoingIteratorCreation = this.trackIteratorCreationPromises.get(\n track.id,\n );\n if (ongoingIteratorCreation) {\n await ongoingIteratorCreation;\n }\n\n const iterator = this.trackIterators.get(track.id);\n if (iterator) {\n try {\n await iterator.return?.();\n } catch (_error) {\n // Iterator cleanup failed, continue anyway\n }\n }\n this.trackIterators.delete(track.id);\n }\n\n #seekLock?: PromiseWithResolvers<void>;\n\n private async seekSafe(trackId: number, timeMs: number) {\n return withSpan(\n \"bufferedInput.seekSafe\",\n {\n trackId,\n timeMs,\n },\n undefined,\n async (span) => {\n if (this.#seekLock) {\n span.setAttribute(\"waitedForSeekLock\", true);\n await this.#seekLock.promise;\n }\n const seekLock = Promise.withResolvers<void>();\n this.#seekLock = seekLock;\n\n try {\n const track = await this.getTrack(trackId);\n span.setAttribute(\"trackType\", track.type);\n\n const trackBuffer = this.getTrackBuffer(track);\n\n const roundedTimeMs = roundToMilliseconds(timeMs);\n const firstTimestampMs = roundToMilliseconds(\n (await track.getFirstTimestamp()) * 1000,\n );\n span.setAttribute(\"firstTimestampMs\", firstTimestampMs);\n\n if (roundedTimeMs < firstTimestampMs) {\n console.error(\"Seeking outside bounds of input\", {\n roundedTimeMs,\n firstTimestampMs,\n });\n throw new NoSample(\n `Seeking outside bounds of input ${roundedTimeMs} < ${firstTimestampMs}`,\n );\n }\n\n // Check if we need to reset iterator for seeks outside current buffer range\n const bufferContents = trackBuffer.getContents();\n span.setAttribute(\"bufferContentsLength\", bufferContents.length);\n\n if (bufferContents.length > 0) {\n const bufferStartMs = roundToMilliseconds(\n trackBuffer.firstTimestamp * 1000,\n );\n span.setAttribute(\"bufferStartMs\", bufferStartMs);\n\n if (roundedTimeMs < bufferStartMs) {\n span.setAttribute(\"resetIterator\", true);\n await this.resetIterator(track);\n }\n }\n\n const alreadyInBuffer = trackBuffer.find(timeMs);\n if (alreadyInBuffer) {\n span.setAttribute(\"foundInBuffer\", true);\n span.setAttribute(\"bufferSize\", trackBuffer.length);\n const contents = trackBuffer.getContents();\n if (contents.length > 0) {\n span.setAttribute(\n \"bufferTimestamps\",\n contents\n .map((s) => Math.round((s.timestamp || 0) * 1000))\n .slice(0, 10)\n .join(\",\"),\n );\n }\n return alreadyInBuffer;\n }\n\n // Buffer miss - record buffer state\n span.setAttribute(\"foundInBuffer\", false);\n span.setAttribute(\"bufferSize\", trackBuffer.length);\n span.setAttribute(\"requestedTimeMs\", Math.round(timeMs));\n\n const contents = trackBuffer.getContents();\n if (contents.length > 0) {\n const firstSample = contents[0];\n const lastSample = contents[contents.length - 1];\n if (firstSample && lastSample) {\n const bufferStartMs = Math.round(\n (firstSample.timestamp || 0) * 1000,\n );\n const bufferEndMs = Math.round(\n ((lastSample.timestamp || 0) + (lastSample.duration || 0)) *\n 1000,\n );\n span.setAttribute(\"bufferStartMs\", bufferStartMs);\n span.setAttribute(\"bufferEndMs\", bufferEndMs);\n span.setAttribute(\n \"bufferRangeMs\",\n `${bufferStartMs}-${bufferEndMs}`,\n );\n }\n }\n\n const iterator = this.getTrackIterator(track);\n let iterationCount = 0;\n const decodeStart = performance.now();\n\n while (true) {\n iterationCount++;\n const iterStart = performance.now();\n const { done, value: decodedSample } = await iterator.next();\n const iterEnd = performance.now();\n\n // Record individual iteration timing for first 5 iterations\n if (iterationCount <= 5) {\n span.setAttribute(\n `iter${iterationCount}Ms`,\n Math.round((iterEnd - iterStart) * 100) / 100,\n );\n }\n\n if (decodedSample) {\n trackBuffer.push(decodedSample);\n if (iterationCount <= 5) {\n span.setAttribute(\n `iter${iterationCount}Timestamp`,\n Math.round((decodedSample.timestamp || 0) * 1000),\n );\n }\n }\n\n const foundSample = trackBuffer.find(roundedTimeMs);\n if (foundSample) {\n const decodeEnd = performance.now();\n span.setAttribute(\"iterationCount\", iterationCount);\n span.setAttribute(\n \"decodeMs\",\n Math.round((decodeEnd - decodeStart) * 100) / 100,\n );\n span.setAttribute(\n \"avgIterMs\",\n Math.round(((decodeEnd - decodeStart) / iterationCount) * 100) /\n 100,\n );\n span.setAttribute(\"foundSample\", true);\n span.setAttribute(\n \"foundTimestamp\",\n Math.round((foundSample.timestamp || 0) * 1000),\n );\n return foundSample;\n }\n if (done) {\n break;\n }\n }\n\n span.setAttribute(\"iterationCount\", iterationCount);\n span.setAttribute(\"reachedEnd\", true);\n\n // Check if we're seeking to the exact end of the track (legitimate use case)\n const finalBufferContents = trackBuffer.getContents();\n if (finalBufferContents.length > 0) {\n const lastSample =\n finalBufferContents[finalBufferContents.length - 1];\n const lastSampleEndMs = roundToMilliseconds(\n ((lastSample?.timestamp || 0) + (lastSample?.duration || 0)) *\n 1000,\n );\n\n // Only return last sample if seeking to exactly the track duration\n // (end of video) AND we have the final segment loaded\n const trackDurationMs = (await track.computeDuration()) * 1000;\n const isSeekingToTrackEnd =\n roundToMilliseconds(timeMs) ===\n roundToMilliseconds(trackDurationMs);\n const isAtEndOfTrack =\n roundToMilliseconds(timeMs) >= lastSampleEndMs;\n\n if (isSeekingToTrackEnd && isAtEndOfTrack) {\n span.setAttribute(\"returnedLastSample\", true);\n return lastSample;\n }\n }\n\n // For all other cases (seeking within track but outside buffer range), throw error\n // The caller should ensure the correct segment is loaded before seeking\n throw new NoSample(\n `Sample not found for time ${timeMs} in ${track.type} track ${trackId}`,\n );\n } finally {\n this.#seekLock = undefined;\n seekLock.resolve();\n }\n },\n );\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"BufferedSeekingInput.js","names":["defaultOptions: BufferedSeekingInputOptions","track","bufferSize","#seekLock","contents"],"sources":["../../../src/elements/EFMedia/BufferedSeekingInput.ts"],"sourcesContent":["import {\n AudioSampleSink,\n BufferSource,\n Input,\n InputAudioTrack,\n type InputTrack,\n InputVideoTrack,\n MP4,\n VideoSampleSink,\n} from \"mediabunny\";\nimport { withSpan } from \"../../otel/tracingHelpers.js\";\nimport { type MediaSample, SampleBuffer } from \"../SampleBuffer\";\nimport { roundToMilliseconds } from \"./shared/PrecisionUtils\";\n\ninterface BufferedSeekingInputOptions {\n videoBufferSize?: number;\n audioBufferSize?: number;\n /**\n * Timeline offset in milliseconds to map user timeline to media timeline.\n * Applied during seeking to handle media that doesn't start at 0ms.\n */\n startTimeOffsetMs?: number;\n}\n\nconst defaultOptions: BufferedSeekingInputOptions = {\n videoBufferSize: 30,\n audioBufferSize: 100,\n startTimeOffsetMs: 0,\n};\n\nexport class NoSample extends RangeError {}\n\nexport class ConcurrentSeekError extends RangeError {}\n\nexport class BufferedSeekingInput {\n private input: Input;\n private trackIterators: Map<number, AsyncIterator<MediaSample>> = new Map();\n private trackBuffers: Map<number, SampleBuffer> = new Map();\n private options: BufferedSeekingInputOptions;\n // Separate locks for different operation types to prevent unnecessary blocking\n private trackIteratorCreationPromises: Map<number, Promise<any>> = new Map();\n private trackSeekPromises: Map<number, Promise<any>> = new Map();\n\n /**\n * Timeline offset in milliseconds to map user timeline to media timeline.\n * Applied during seeking to handle media that doesn't start at 0ms.\n */\n private readonly startTimeOffsetMs: number;\n\n constructor(arrayBuffer: ArrayBuffer, options?: BufferedSeekingInputOptions) {\n const bufferSource = new BufferSource(arrayBuffer);\n const input = new Input({\n source: bufferSource,\n formats: [MP4],\n });\n this.input = input;\n this.options = { ...defaultOptions, ...options };\n this.startTimeOffsetMs = this.options.startTimeOffsetMs ?? 0;\n }\n\n // Buffer inspection API for testing\n getBufferSize(trackId: number): number {\n const buffer = this.trackBuffers.get(trackId);\n return buffer ? buffer.length : 0;\n }\n\n getBufferContents(trackId: number): readonly MediaSample[] {\n const buffer = this.trackBuffers.get(trackId);\n return buffer ? Object.freeze([...buffer.getContents()]) : [];\n }\n\n getBufferTimestamps(trackId: number): number[] {\n const contents = this.getBufferContents(trackId);\n return contents.map((sample) => sample.timestamp || 0);\n }\n\n clearBuffer(trackId: number): void {\n const buffer = this.trackBuffers.get(trackId);\n if (buffer) {\n buffer.clear();\n }\n }\n\n computeDuration() {\n return this.input.computeDuration();\n }\n\n async getTrack(trackId: number) {\n const tracks = await this.input.getTracks();\n const track = tracks.find((track) => track.id === trackId);\n if (!track) {\n throw new Error(`Track ${trackId} not found`);\n }\n return track;\n }\n\n async getAudioTrack(trackId: number) {\n const tracks = await this.input.getAudioTracks();\n const track = tracks.find(\n (track) => track.id === trackId && track.type === \"audio\",\n );\n if (!track) {\n throw new Error(`Track ${trackId} not found`);\n }\n return track;\n }\n\n async getVideoTrack(trackId: number) {\n const tracks = await this.input.getVideoTracks();\n const track = tracks.find(\n (track) => track.id === trackId && track.type === \"video\",\n );\n if (!track) {\n throw new Error(`Track ${trackId} not found`);\n }\n return track;\n }\n\n async getFirstVideoTrack() {\n const tracks = await this.input.getVideoTracks();\n return tracks[0];\n }\n\n async getFirstAudioTrack() {\n const tracks = await this.input.getAudioTracks();\n return tracks[0];\n }\n\n getTrackIterator(track: InputTrack) {\n if (this.trackIterators.has(track.id)) {\n // biome-ignore lint/style/noNonNullAssertion: we know the map has the key\n return this.trackIterators.get(track.id)!;\n }\n\n const trackIterator = this.createTrackIterator(track);\n\n this.trackIterators.set(track.id, trackIterator);\n\n return trackIterator;\n }\n\n createTrackSampleSink(track: InputTrack) {\n if (track instanceof InputAudioTrack) {\n return new AudioSampleSink(track);\n }\n if (track instanceof InputVideoTrack) {\n return new VideoSampleSink(track);\n }\n throw new Error(`Unsupported track type ${track.type}`);\n }\n\n createTrackIterator(track: InputTrack) {\n const sampleSink = this.createTrackSampleSink(track);\n return sampleSink.samples();\n }\n\n createTrackBuffer(track: InputTrack) {\n if (track.type === \"audio\") {\n const bufferSize = this.options.audioBufferSize;\n const sampleBuffer = new SampleBuffer(bufferSize);\n return sampleBuffer;\n }\n const bufferSize = this.options.videoBufferSize;\n const sampleBuffer = new SampleBuffer(bufferSize);\n return sampleBuffer;\n }\n\n getTrackBuffer(track: InputTrack) {\n const maybeTrackBuffer = this.trackBuffers.get(track.id);\n\n if (maybeTrackBuffer) {\n return maybeTrackBuffer;\n }\n\n const trackBuffer = this.createTrackBuffer(track);\n this.trackBuffers.set(track.id, trackBuffer);\n return trackBuffer;\n }\n\n async seek(trackId: number, timeMs: number) {\n return withSpan(\n \"bufferedInput.seek\",\n {\n trackId,\n timeMs,\n startTimeOffsetMs: this.startTimeOffsetMs,\n },\n undefined,\n async (span) => {\n // Apply timeline offset to map user timeline to media timeline\n const mediaTimeMs = timeMs + this.startTimeOffsetMs;\n\n // Round using consistent precision handling\n const roundedMediaTimeMs = roundToMilliseconds(mediaTimeMs);\n span.setAttribute(\"roundedMediaTimeMs\", roundedMediaTimeMs);\n\n // Serialize seek operations per track (but don't block iterator creation)\n const existingSeek = this.trackSeekPromises.get(trackId);\n if (existingSeek) {\n span.setAttribute(\"waitedForExistingSeek\", true);\n await existingSeek;\n }\n\n const seekPromise = this.seekSafe(trackId, roundedMediaTimeMs);\n this.trackSeekPromises.set(trackId, seekPromise);\n\n try {\n return await seekPromise;\n } finally {\n this.trackSeekPromises.delete(trackId);\n }\n },\n );\n }\n\n private async resetIterator(track: InputTrack) {\n const trackBuffer = this.trackBuffers.get(track.id);\n trackBuffer?.clear();\n // Clean up iterator safely - wait for any ongoing iterator creation\n const ongoingIteratorCreation = this.trackIteratorCreationPromises.get(\n track.id,\n );\n if (ongoingIteratorCreation) {\n await ongoingIteratorCreation;\n }\n\n const iterator = this.trackIterators.get(track.id);\n if (iterator) {\n try {\n await iterator.return?.();\n } catch (_error) {\n // Iterator cleanup failed, continue anyway\n }\n }\n this.trackIterators.delete(track.id);\n }\n\n #seekLock?: PromiseWithResolvers<void>;\n\n private async seekSafe(trackId: number, timeMs: number) {\n return withSpan(\n \"bufferedInput.seekSafe\",\n {\n trackId,\n timeMs,\n },\n undefined,\n async (span) => {\n if (this.#seekLock) {\n span.setAttribute(\"waitedForSeekLock\", true);\n await this.#seekLock.promise;\n }\n const seekLock = Promise.withResolvers<void>();\n this.#seekLock = seekLock;\n\n try {\n const track = await this.getTrack(trackId);\n span.setAttribute(\"trackType\", track.type);\n\n const trackBuffer = this.getTrackBuffer(track);\n\n const roundedTimeMs = roundToMilliseconds(timeMs);\n const firstTimestampMs = roundToMilliseconds(\n (await track.getFirstTimestamp()) * 1000,\n );\n span.setAttribute(\"firstTimestampMs\", firstTimestampMs);\n\n // Use tolerance for floating point comparison (0.01ms tolerance)\n // This handles rounding errors like 20916.666 vs 20916.667\n const PRECISION_TOLERANCE_MS = 0.01;\n if (roundedTimeMs < firstTimestampMs - PRECISION_TOLERANCE_MS) {\n console.error(\"Seeking outside bounds of input\", {\n roundedTimeMs,\n firstTimestampMs,\n });\n throw new NoSample(\n `Seeking outside bounds of input ${roundedTimeMs} < ${firstTimestampMs}`,\n );\n }\n\n // Check if we need to reset iterator for seeks outside current buffer range\n const bufferContents = trackBuffer.getContents();\n span.setAttribute(\"bufferContentsLength\", bufferContents.length);\n\n if (bufferContents.length > 0) {\n const bufferStartMs = roundToMilliseconds(\n trackBuffer.firstTimestamp * 1000,\n );\n span.setAttribute(\"bufferStartMs\", bufferStartMs);\n\n if (roundedTimeMs < bufferStartMs) {\n span.setAttribute(\"resetIterator\", true);\n await this.resetIterator(track);\n }\n }\n\n const alreadyInBuffer = trackBuffer.find(timeMs);\n if (alreadyInBuffer) {\n span.setAttribute(\"foundInBuffer\", true);\n span.setAttribute(\"bufferSize\", trackBuffer.length);\n const contents = trackBuffer.getContents();\n if (contents.length > 0) {\n span.setAttribute(\n \"bufferTimestamps\",\n contents\n .map((s) => Math.round((s.timestamp || 0) * 1000))\n .slice(0, 10)\n .join(\",\"),\n );\n }\n return alreadyInBuffer;\n }\n\n // Buffer miss - record buffer state\n span.setAttribute(\"foundInBuffer\", false);\n span.setAttribute(\"bufferSize\", trackBuffer.length);\n span.setAttribute(\"requestedTimeMs\", Math.round(timeMs));\n\n const contents = trackBuffer.getContents();\n if (contents.length > 0) {\n const firstSample = contents[0];\n const lastSample = contents[contents.length - 1];\n if (firstSample && lastSample) {\n const bufferStartMs = Math.round(\n (firstSample.timestamp || 0) * 1000,\n );\n const bufferEndMs = Math.round(\n ((lastSample.timestamp || 0) + (lastSample.duration || 0)) *\n 1000,\n );\n span.setAttribute(\"bufferStartMs\", bufferStartMs);\n span.setAttribute(\"bufferEndMs\", bufferEndMs);\n span.setAttribute(\n \"bufferRangeMs\",\n `${bufferStartMs}-${bufferEndMs}`,\n );\n }\n }\n\n const iterator = this.getTrackIterator(track);\n let iterationCount = 0;\n const decodeStart = performance.now();\n\n while (true) {\n iterationCount++;\n const iterStart = performance.now();\n const { done, value: decodedSample } = await iterator.next();\n const iterEnd = performance.now();\n\n // Record individual iteration timing for first 5 iterations\n if (iterationCount <= 5) {\n span.setAttribute(\n `iter${iterationCount}Ms`,\n Math.round((iterEnd - iterStart) * 100) / 100,\n );\n }\n\n if (decodedSample) {\n trackBuffer.push(decodedSample);\n if (iterationCount <= 5) {\n span.setAttribute(\n `iter${iterationCount}Timestamp`,\n Math.round((decodedSample.timestamp || 0) * 1000),\n );\n }\n }\n\n const foundSample = trackBuffer.find(roundedTimeMs);\n if (foundSample) {\n const decodeEnd = performance.now();\n span.setAttribute(\"iterationCount\", iterationCount);\n span.setAttribute(\n \"decodeMs\",\n Math.round((decodeEnd - decodeStart) * 100) / 100,\n );\n span.setAttribute(\n \"avgIterMs\",\n Math.round(((decodeEnd - decodeStart) / iterationCount) * 100) /\n 100,\n );\n span.setAttribute(\"foundSample\", true);\n span.setAttribute(\n \"foundTimestamp\",\n Math.round((foundSample.timestamp || 0) * 1000),\n );\n return foundSample;\n }\n if (done) {\n break;\n }\n }\n\n span.setAttribute(\"iterationCount\", iterationCount);\n span.setAttribute(\"reachedEnd\", true);\n\n // Check if we're seeking to the exact end of the track (legitimate use case)\n const finalBufferContents = trackBuffer.getContents();\n if (finalBufferContents.length > 0) {\n const lastSample =\n finalBufferContents[finalBufferContents.length - 1];\n const lastSampleEndMs = roundToMilliseconds(\n ((lastSample?.timestamp || 0) + (lastSample?.duration || 0)) *\n 1000,\n );\n\n // Only return last sample if seeking to exactly the track duration\n // (end of video) AND we have the final segment loaded\n const trackDurationMs = (await track.computeDuration()) * 1000;\n const isSeekingToTrackEnd =\n roundToMilliseconds(timeMs) ===\n roundToMilliseconds(trackDurationMs);\n const isAtEndOfTrack =\n roundToMilliseconds(timeMs) >= lastSampleEndMs;\n\n if (isSeekingToTrackEnd && isAtEndOfTrack) {\n span.setAttribute(\"returnedLastSample\", true);\n return lastSample;\n }\n }\n\n // For all other cases (seeking within track but outside buffer range), throw error\n // The caller should ensure the correct segment is loaded before seeking\n throw new NoSample(\n `Sample not found for time ${timeMs} in ${track.type} track ${trackId}`,\n );\n } finally {\n this.#seekLock = undefined;\n seekLock.resolve();\n }\n },\n );\n }\n}\n"],"mappings":"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"}