@editframe/elements 0.17.6-beta.0 → 0.18.7-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (211) hide show
  1. package/dist/EF_FRAMEGEN.js +1 -1
  2. package/dist/elements/EFAudio.d.ts +21 -2
  3. package/dist/elements/EFAudio.js +41 -11
  4. package/dist/elements/EFImage.d.ts +1 -0
  5. package/dist/elements/EFImage.js +11 -3
  6. package/dist/elements/EFMedia/AssetIdMediaEngine.d.ts +18 -0
  7. package/dist/elements/EFMedia/AssetIdMediaEngine.js +41 -0
  8. package/dist/elements/EFMedia/AssetMediaEngine.browsertest.d.ts +0 -0
  9. package/dist/elements/EFMedia/AssetMediaEngine.d.ts +45 -0
  10. package/dist/elements/EFMedia/AssetMediaEngine.js +135 -0
  11. package/dist/elements/EFMedia/BaseMediaEngine.d.ts +55 -0
  12. package/dist/elements/EFMedia/BaseMediaEngine.js +115 -0
  13. package/dist/elements/EFMedia/BufferedSeekingInput.d.ts +43 -0
  14. package/dist/elements/EFMedia/BufferedSeekingInput.js +179 -0
  15. package/dist/elements/EFMedia/JitMediaEngine.browsertest.d.ts +0 -0
  16. package/dist/elements/EFMedia/JitMediaEngine.d.ts +31 -0
  17. package/dist/elements/EFMedia/JitMediaEngine.js +81 -0
  18. package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.browsertest.d.ts +9 -0
  19. package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.d.ts +16 -0
  20. package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.js +48 -0
  21. package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.d.ts +3 -0
  22. package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.js +141 -0
  23. package/dist/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.browsertest.d.ts +9 -0
  24. package/dist/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.d.ts +4 -0
  25. package/dist/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.js +16 -0
  26. package/dist/elements/EFMedia/audioTasks/makeAudioInputTask.browsertest.d.ts +9 -0
  27. package/dist/elements/EFMedia/audioTasks/makeAudioInputTask.d.ts +3 -0
  28. package/dist/elements/EFMedia/audioTasks/makeAudioInputTask.js +30 -0
  29. package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.chunkboundary.regression.browsertest.d.ts +0 -0
  30. package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.d.ts +7 -0
  31. package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.js +32 -0
  32. package/dist/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.d.ts +4 -0
  33. package/dist/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.js +28 -0
  34. package/dist/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.d.ts +4 -0
  35. package/dist/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.js +17 -0
  36. package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.d.ts +3 -0
  37. package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.js +107 -0
  38. package/dist/elements/EFMedia/shared/AudioSpanUtils.d.ts +7 -0
  39. package/dist/elements/EFMedia/shared/AudioSpanUtils.js +54 -0
  40. package/dist/elements/EFMedia/shared/BufferUtils.d.ts +70 -0
  41. package/dist/elements/EFMedia/shared/BufferUtils.js +89 -0
  42. package/dist/elements/EFMedia/shared/MediaTaskUtils.d.ts +23 -0
  43. package/dist/elements/EFMedia/shared/PrecisionUtils.d.ts +28 -0
  44. package/dist/elements/EFMedia/shared/PrecisionUtils.js +29 -0
  45. package/dist/elements/EFMedia/shared/RenditionHelpers.d.ts +19 -0
  46. package/dist/elements/EFMedia/tasks/makeMediaEngineTask.d.ts +18 -0
  47. package/dist/elements/EFMedia/tasks/makeMediaEngineTask.js +60 -0
  48. package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.browsertest.d.ts +9 -0
  49. package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.d.ts +16 -0
  50. package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.js +46 -0
  51. package/dist/elements/EFMedia/videoTasks/makeVideoInitSegmentFetchTask.browsertest.d.ts +9 -0
  52. package/dist/elements/EFMedia/videoTasks/makeVideoInitSegmentFetchTask.d.ts +4 -0
  53. package/dist/elements/EFMedia/videoTasks/makeVideoInitSegmentFetchTask.js +16 -0
  54. package/dist/elements/EFMedia/videoTasks/makeVideoInputTask.browsertest.d.ts +9 -0
  55. package/dist/elements/EFMedia/videoTasks/makeVideoInputTask.d.ts +3 -0
  56. package/dist/elements/EFMedia/videoTasks/makeVideoInputTask.js +27 -0
  57. package/dist/elements/EFMedia/videoTasks/makeVideoSeekTask.d.ts +7 -0
  58. package/dist/elements/EFMedia/videoTasks/makeVideoSeekTask.js +34 -0
  59. package/dist/elements/EFMedia/videoTasks/makeVideoSegmentFetchTask.browsertest.d.ts +9 -0
  60. package/dist/elements/EFMedia/videoTasks/makeVideoSegmentFetchTask.d.ts +4 -0
  61. package/dist/elements/EFMedia/videoTasks/makeVideoSegmentFetchTask.js +28 -0
  62. package/dist/elements/EFMedia/videoTasks/makeVideoSegmentIdTask.browsertest.d.ts +9 -0
  63. package/dist/elements/EFMedia/videoTasks/makeVideoSegmentIdTask.d.ts +4 -0
  64. package/dist/elements/EFMedia/videoTasks/makeVideoSegmentIdTask.js +17 -0
  65. package/dist/elements/EFMedia.browsertest.d.ts +1 -0
  66. package/dist/elements/EFMedia.d.ts +63 -111
  67. package/dist/elements/EFMedia.js +117 -1113
  68. package/dist/elements/EFTemporal.d.ts +1 -1
  69. package/dist/elements/EFTemporal.js +1 -1
  70. package/dist/elements/EFTimegroup.d.ts +11 -0
  71. package/dist/elements/EFTimegroup.js +83 -13
  72. package/dist/elements/EFVideo.d.ts +54 -32
  73. package/dist/elements/EFVideo.js +100 -207
  74. package/dist/elements/EFWaveform.js +2 -2
  75. package/dist/elements/SampleBuffer.d.ts +14 -0
  76. package/dist/elements/SampleBuffer.js +52 -0
  77. package/dist/getRenderInfo.js +2 -1
  78. package/dist/gui/ContextMixin.js +3 -2
  79. package/dist/gui/EFFilmstrip.d.ts +3 -3
  80. package/dist/gui/EFFilmstrip.js +1 -1
  81. package/dist/gui/EFFitScale.d.ts +2 -2
  82. package/dist/gui/TWMixin.js +1 -1
  83. package/dist/style.css +1 -1
  84. package/dist/transcoding/cache/CacheManager.d.ts +73 -0
  85. package/dist/transcoding/cache/RequestDeduplicator.d.ts +29 -0
  86. package/dist/transcoding/cache/RequestDeduplicator.js +53 -0
  87. package/dist/transcoding/cache/RequestDeduplicator.test.d.ts +1 -0
  88. package/dist/transcoding/types/index.d.ts +242 -0
  89. package/dist/transcoding/utils/MediaUtils.d.ts +9 -0
  90. package/dist/transcoding/utils/UrlGenerator.d.ts +26 -0
  91. package/dist/transcoding/utils/UrlGenerator.js +45 -0
  92. package/dist/transcoding/utils/constants.d.ts +27 -0
  93. package/dist/utils/LRUCache.d.ts +34 -0
  94. package/dist/utils/LRUCache.js +115 -0
  95. package/package.json +3 -3
  96. package/src/elements/EFAudio.browsertest.ts +189 -49
  97. package/src/elements/EFAudio.ts +59 -13
  98. package/src/elements/EFImage.browsertest.ts +42 -0
  99. package/src/elements/EFImage.ts +23 -3
  100. package/src/elements/EFMedia/AssetIdMediaEngine.test.ts +222 -0
  101. package/src/elements/EFMedia/AssetIdMediaEngine.ts +70 -0
  102. package/src/elements/EFMedia/AssetMediaEngine.browsertest.ts +100 -0
  103. package/src/elements/EFMedia/AssetMediaEngine.ts +255 -0
  104. package/src/elements/EFMedia/BaseMediaEngine.test.ts +164 -0
  105. package/src/elements/EFMedia/BaseMediaEngine.ts +219 -0
  106. package/src/elements/EFMedia/BufferedSeekingInput.browsertest.ts +481 -0
  107. package/src/elements/EFMedia/BufferedSeekingInput.ts +324 -0
  108. package/src/elements/EFMedia/JitMediaEngine.browsertest.ts +165 -0
  109. package/src/elements/EFMedia/JitMediaEngine.ts +166 -0
  110. package/src/elements/EFMedia/audioTasks/makeAudioBufferTask.browsertest.ts +554 -0
  111. package/src/elements/EFMedia/audioTasks/makeAudioBufferTask.ts +81 -0
  112. package/src/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.ts +250 -0
  113. package/src/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.browsertest.ts +59 -0
  114. package/src/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.ts +23 -0
  115. package/src/elements/EFMedia/audioTasks/makeAudioInputTask.browsertest.ts +55 -0
  116. package/src/elements/EFMedia/audioTasks/makeAudioInputTask.ts +43 -0
  117. package/src/elements/EFMedia/audioTasks/makeAudioSeekTask.chunkboundary.regression.browsertest.ts +199 -0
  118. package/src/elements/EFMedia/audioTasks/makeAudioSeekTask.ts +64 -0
  119. package/src/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.ts +45 -0
  120. package/src/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.ts +24 -0
  121. package/src/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.ts +183 -0
  122. package/src/elements/EFMedia/shared/AudioSpanUtils.ts +128 -0
  123. package/src/elements/EFMedia/shared/BufferUtils.ts +310 -0
  124. package/src/elements/EFMedia/shared/MediaTaskUtils.ts +44 -0
  125. package/src/elements/EFMedia/shared/PrecisionUtils.ts +46 -0
  126. package/src/elements/EFMedia/shared/RenditionHelpers.browsertest.ts +247 -0
  127. package/src/elements/EFMedia/shared/RenditionHelpers.ts +79 -0
  128. package/src/elements/EFMedia/tasks/makeMediaEngineTask.browsertest.ts +128 -0
  129. package/src/elements/EFMedia/tasks/makeMediaEngineTask.test.ts +233 -0
  130. package/src/elements/EFMedia/tasks/makeMediaEngineTask.ts +89 -0
  131. package/src/elements/EFMedia/videoTasks/makeVideoBufferTask.browsertest.ts +555 -0
  132. package/src/elements/EFMedia/videoTasks/makeVideoBufferTask.ts +79 -0
  133. package/src/elements/EFMedia/videoTasks/makeVideoInitSegmentFetchTask.browsertest.ts +59 -0
  134. package/src/elements/EFMedia/videoTasks/makeVideoInitSegmentFetchTask.ts +23 -0
  135. package/src/elements/EFMedia/videoTasks/makeVideoInputTask.browsertest.ts +55 -0
  136. package/src/elements/EFMedia/videoTasks/makeVideoInputTask.ts +45 -0
  137. package/src/elements/EFMedia/videoTasks/makeVideoSeekTask.ts +68 -0
  138. package/src/elements/EFMedia/videoTasks/makeVideoSegmentFetchTask.browsertest.ts +57 -0
  139. package/src/elements/EFMedia/videoTasks/makeVideoSegmentFetchTask.ts +43 -0
  140. package/src/elements/EFMedia/videoTasks/makeVideoSegmentIdTask.browsertest.ts +56 -0
  141. package/src/elements/EFMedia/videoTasks/makeVideoSegmentIdTask.ts +24 -0
  142. package/src/elements/EFMedia.browsertest.ts +706 -273
  143. package/src/elements/EFMedia.ts +136 -1769
  144. package/src/elements/EFTemporal.ts +3 -4
  145. package/src/elements/EFTimegroup.browsertest.ts +6 -3
  146. package/src/elements/EFTimegroup.ts +147 -21
  147. package/src/elements/EFVideo.browsertest.ts +980 -169
  148. package/src/elements/EFVideo.ts +113 -458
  149. package/src/elements/EFWaveform.ts +1 -1
  150. package/src/elements/MediaController.ts +2 -12
  151. package/src/elements/SampleBuffer.ts +95 -0
  152. package/src/gui/ContextMixin.ts +3 -6
  153. package/src/transcoding/cache/CacheManager.ts +208 -0
  154. package/src/transcoding/cache/RequestDeduplicator.test.ts +170 -0
  155. package/src/transcoding/cache/RequestDeduplicator.ts +65 -0
  156. package/src/transcoding/types/index.ts +269 -0
  157. package/src/transcoding/utils/MediaUtils.ts +63 -0
  158. package/src/transcoding/utils/UrlGenerator.ts +68 -0
  159. package/src/transcoding/utils/constants.ts +36 -0
  160. package/src/utils/LRUCache.ts +153 -0
  161. package/test/EFVideo.framegen.browsertest.ts +39 -30
  162. package/test/__cache__/GET__api_v1_transcode_audio_1_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__32da3954ba60c96ad732020c65a08ebc/data.bin +0 -0
  163. package/test/__cache__/GET__api_v1_transcode_audio_1_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__32da3954ba60c96ad732020c65a08ebc/metadata.json +21 -0
  164. package/test/__cache__/GET__api_v1_transcode_audio_1_mp4_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4_bytes_0__9ed2d25c675aa6bb6ff5b3ae23887c71/data.bin +0 -0
  165. package/test/__cache__/GET__api_v1_transcode_audio_1_mp4_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4_bytes_0__9ed2d25c675aa6bb6ff5b3ae23887c71/metadata.json +22 -0
  166. package/test/__cache__/GET__api_v1_transcode_audio_2_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__b0b2b07efcf607de8ee0f650328c32f7/data.bin +0 -0
  167. package/test/__cache__/GET__api_v1_transcode_audio_2_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__b0b2b07efcf607de8ee0f650328c32f7/metadata.json +21 -0
  168. package/test/__cache__/GET__api_v1_transcode_audio_2_mp4_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4_bytes_0__d5a3309a2bf756dd6e304807eb402f56/data.bin +0 -0
  169. package/test/__cache__/GET__api_v1_transcode_audio_2_mp4_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4_bytes_0__d5a3309a2bf756dd6e304807eb402f56/metadata.json +22 -0
  170. package/test/__cache__/GET__api_v1_transcode_audio_3_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__a75c2252b542e0c152c780e9a8d7b154/data.bin +0 -0
  171. package/test/__cache__/GET__api_v1_transcode_audio_3_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__a75c2252b542e0c152c780e9a8d7b154/metadata.json +21 -0
  172. package/test/__cache__/GET__api_v1_transcode_audio_3_mp4_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4_bytes_0__773254bb671e3466fca8677139fb239e/data.bin +0 -0
  173. package/test/__cache__/GET__api_v1_transcode_audio_3_mp4_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4_bytes_0__773254bb671e3466fca8677139fb239e/metadata.json +22 -0
  174. package/test/__cache__/GET__api_v1_transcode_audio_4_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__a64ff1cfb1b52cae14df4b5dfa1e222b/data.bin +0 -0
  175. package/test/__cache__/GET__api_v1_transcode_audio_4_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__a64ff1cfb1b52cae14df4b5dfa1e222b/metadata.json +21 -0
  176. package/test/__cache__/GET__api_v1_transcode_audio_5_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__91e8a522f950809b9f09f4173113b4b0/data.bin +0 -0
  177. package/test/__cache__/GET__api_v1_transcode_audio_5_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__91e8a522f950809b9f09f4173113b4b0/metadata.json +21 -0
  178. package/test/__cache__/GET__api_v1_transcode_audio_init_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__e66d2c831d951e74ad0aeaa6489795d0/data.bin +0 -0
  179. package/test/__cache__/GET__api_v1_transcode_audio_init_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__e66d2c831d951e74ad0aeaa6489795d0/metadata.json +21 -0
  180. package/test/__cache__/GET__api_v1_transcode_high_1_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__26197f6f7c46cacb0a71134131c3f775/data.bin +0 -0
  181. package/test/__cache__/GET__api_v1_transcode_high_1_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__26197f6f7c46cacb0a71134131c3f775/metadata.json +21 -0
  182. package/test/__cache__/GET__api_v1_transcode_high_2_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__4cb6774cd3650ccf59c8f8dc6678c0b9/data.bin +0 -0
  183. package/test/__cache__/GET__api_v1_transcode_high_2_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__4cb6774cd3650ccf59c8f8dc6678c0b9/metadata.json +21 -0
  184. package/test/__cache__/GET__api_v1_transcode_high_3_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__0b3b2b1c8933f7fcf8a9ecaa88d58b41/data.bin +0 -0
  185. package/test/__cache__/GET__api_v1_transcode_high_3_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__0b3b2b1c8933f7fcf8a9ecaa88d58b41/metadata.json +21 -0
  186. package/test/__cache__/GET__api_v1_transcode_high_4_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__a6fb05a22b18d850f7f2950bbcdbdeed/data.bin +0 -0
  187. package/test/__cache__/GET__api_v1_transcode_high_4_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__a6fb05a22b18d850f7f2950bbcdbdeed/metadata.json +21 -0
  188. package/test/__cache__/GET__api_v1_transcode_high_5_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__a50058c7c3602e90879fe3428ed891f4/data.bin +0 -0
  189. package/test/__cache__/GET__api_v1_transcode_high_5_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__a50058c7c3602e90879fe3428ed891f4/metadata.json +21 -0
  190. package/test/__cache__/GET__api_v1_transcode_high_init_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__0798c479b44aaeef850609a430f6e613/data.bin +0 -0
  191. package/test/__cache__/GET__api_v1_transcode_high_init_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__0798c479b44aaeef850609a430f6e613/metadata.json +21 -0
  192. package/test/__cache__/GET__api_v1_transcode_manifest_json_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__3be92a0437de726b431ed5af2369158a/data.bin +1 -0
  193. package/test/__cache__/GET__api_v1_transcode_manifest_json_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__3be92a0437de726b431ed5af2369158a/metadata.json +19 -0
  194. package/test/createJitTestClips.ts +320 -188
  195. package/test/recordReplayProxyPlugin.js +352 -0
  196. package/test/useAssetMSW.ts +1 -1
  197. package/test/useMSW.ts +35 -22
  198. package/types.json +1 -1
  199. package/dist/JitTranscodingClient.d.ts +0 -167
  200. package/dist/JitTranscodingClient.js +0 -373
  201. package/dist/ScrubTrackManager.d.ts +0 -96
  202. package/dist/ScrubTrackManager.js +0 -216
  203. package/dist/elements/printTaskStatus.js +0 -11
  204. package/src/elements/__screenshots__/EFMedia.browsertest.ts/EFMedia-JIT-audio-playback-audioBufferTask-should-work-in-JIT-mode-without-URL-errors-1.png +0 -0
  205. package/test/EFVideo.frame-tasks.browsertest.ts +0 -524
  206. /package/dist/{DecoderResetFrequency.test.d.ts → elements/EFMedia/AssetIdMediaEngine.test.d.ts} +0 -0
  207. /package/dist/{DecoderResetRecovery.test.d.ts → elements/EFMedia/BaseMediaEngine.test.d.ts} +0 -0
  208. /package/dist/{JitTranscodingClient.browsertest.d.ts → elements/EFMedia/BufferedSeekingInput.browsertest.d.ts} +0 -0
  209. /package/dist/{JitTranscodingClient.test.d.ts → elements/EFMedia/shared/RenditionHelpers.browsertest.d.ts} +0 -0
  210. /package/dist/{ScrubTrackIntegration.test.d.ts → elements/EFMedia/tasks/makeMediaEngineTask.browsertest.d.ts} +0 -0
  211. /package/dist/{SegmentSwitchLoading.test.d.ts → elements/EFMedia/tasks/makeMediaEngineTask.test.d.ts} +0 -0
@@ -0,0 +1,324 @@
1
+ import {
2
+ AudioSampleSink,
3
+ BufferSource,
4
+ Input,
5
+ MP4,
6
+ VideoSampleSink,
7
+ } from "mediabunny";
8
+ import { type MediaSample, SampleBuffer } from "../SampleBuffer";
9
+ import { roundToMilliseconds } from "./shared/PrecisionUtils";
10
+
11
+ interface BufferedSeekingInputOptions {
12
+ videoBufferSize?: number;
13
+ audioBufferSize?: number;
14
+ /**
15
+ * Timeline offset in milliseconds to map user timeline to media timeline.
16
+ * Applied during seeking to handle media that doesn't start at 0ms.
17
+ */
18
+ startTimeOffsetMs?: number;
19
+ }
20
+
21
+ const defaultOptions: BufferedSeekingInputOptions = {
22
+ videoBufferSize: 30,
23
+ audioBufferSize: 100,
24
+ startTimeOffsetMs: 0,
25
+ };
26
+
27
+ export class NoSample extends RangeError {}
28
+
29
+ export class BufferedSeekingInput {
30
+ private input: Input;
31
+ private trackIterators: Map<number, AsyncIterator<MediaSample>> = new Map();
32
+ private trackBuffers: Map<number, SampleBuffer> = new Map();
33
+ private options: BufferedSeekingInputOptions;
34
+ // Separate locks for different operation types to prevent unnecessary blocking
35
+ private trackIteratorCreationPromises: Map<number, Promise<any>> = new Map();
36
+ private trackSeekPromises: Map<number, Promise<any>> = new Map();
37
+
38
+ /**
39
+ * Timeline offset in milliseconds to map user timeline to media timeline.
40
+ * Applied during seeking to handle media that doesn't start at 0ms.
41
+ */
42
+ private readonly startTimeOffsetMs: number;
43
+
44
+ constructor(arrayBuffer: ArrayBuffer, options?: BufferedSeekingInputOptions) {
45
+ const bufferSource = new BufferSource(arrayBuffer);
46
+ const input = new Input({
47
+ source: bufferSource,
48
+ formats: [MP4],
49
+ });
50
+ this.input = input;
51
+ this.options = { ...defaultOptions, ...options };
52
+ this.startTimeOffsetMs = this.options.startTimeOffsetMs ?? 0;
53
+ }
54
+
55
+ // Buffer inspection API for testing
56
+ getBufferSize(trackId: number): number {
57
+ const buffer = this.trackBuffers.get(trackId);
58
+ return buffer ? buffer.length : 0;
59
+ }
60
+
61
+ getBufferContents(trackId: number): readonly MediaSample[] {
62
+ const buffer = this.trackBuffers.get(trackId);
63
+ return buffer ? Object.freeze([...buffer.getContents()]) : [];
64
+ }
65
+
66
+ getBufferTimestamps(trackId: number): number[] {
67
+ const contents = this.getBufferContents(trackId);
68
+ return contents.map((sample) => sample.timestamp || 0);
69
+ }
70
+
71
+ clearBuffer(trackId: number): void {
72
+ const buffer = this.trackBuffers.get(trackId);
73
+ if (buffer) {
74
+ buffer.clear();
75
+ }
76
+ }
77
+
78
+ computeDuration() {
79
+ return this.input.computeDuration();
80
+ }
81
+
82
+ async getTrack(trackId: number) {
83
+ const tracks = await this.input.getTracks();
84
+ const track = tracks.find((track) => track.id === trackId);
85
+ if (!track) {
86
+ throw new Error(`Track ${trackId} not found`);
87
+ }
88
+ return track;
89
+ }
90
+
91
+ async getAudioTrack(trackId: number) {
92
+ const tracks = await this.input.getAudioTracks();
93
+ const track = tracks.find(
94
+ (track) => track.id === trackId && track.type === "audio",
95
+ );
96
+ if (!track) {
97
+ throw new Error(`Track ${trackId} not found`);
98
+ }
99
+ return track;
100
+ }
101
+
102
+ async getVideoTrack(trackId: number) {
103
+ const tracks = await this.input.getVideoTracks();
104
+ const track = tracks.find(
105
+ (track) => track.id === trackId && track.type === "video",
106
+ );
107
+ if (!track) {
108
+ throw new Error(`Track ${trackId} not found`);
109
+ }
110
+ return track;
111
+ }
112
+
113
+ async getFirstVideoTrack() {
114
+ const tracks = await this.input.getVideoTracks();
115
+ return tracks[0];
116
+ }
117
+
118
+ async getFirstAudioTrack() {
119
+ const tracks = await this.input.getAudioTracks();
120
+ return tracks[0];
121
+ }
122
+
123
+ async getTrackIterator(trackId: number) {
124
+ if (this.trackIterators.has(trackId)) {
125
+ // biome-ignore lint/style/noNonNullAssertion: we know the map has the key
126
+ return this.trackIterators.get(trackId)!;
127
+ }
128
+
129
+ // Serialize iterator creation per track (but don't block seeks)
130
+ const existingIteratorCreation =
131
+ this.trackIteratorCreationPromises.get(trackId);
132
+ if (existingIteratorCreation) {
133
+ await existingIteratorCreation;
134
+ // Check again after waiting - another operation might have created it
135
+ if (this.trackIterators.has(trackId)) {
136
+ // biome-ignore lint/style/noNonNullAssertion: we know the map has the key
137
+ return this.trackIterators.get(trackId)!;
138
+ }
139
+ }
140
+
141
+ const creationPromise = this.createIteratorSafe(trackId);
142
+ this.trackIteratorCreationPromises.set(trackId, creationPromise);
143
+
144
+ try {
145
+ const iterator = await creationPromise;
146
+ return iterator;
147
+ } finally {
148
+ this.trackIteratorCreationPromises.delete(trackId);
149
+ }
150
+ }
151
+
152
+ private async createIteratorSafe(trackId: number) {
153
+ const track = await this.getTrack(trackId);
154
+ if (track.type === "audio") {
155
+ const track = await this.getAudioTrack(trackId);
156
+ const sampleSink = new AudioSampleSink(track);
157
+ const iterator = sampleSink.samples();
158
+ this.trackIterators.set(trackId, iterator);
159
+ return iterator;
160
+ }
161
+ {
162
+ const track = await this.getVideoTrack(trackId);
163
+ const sampleSink = new VideoSampleSink(track);
164
+ const iterator = sampleSink.samples();
165
+ this.trackIterators.set(trackId, iterator);
166
+ return iterator;
167
+ }
168
+ }
169
+
170
+ async createTrackBuffer(trackId: number) {
171
+ const track = await this.getTrack(trackId);
172
+ if (track.type === "audio") {
173
+ const bufferSize = this.options.audioBufferSize;
174
+ this.trackBuffers.set(trackId, new SampleBuffer(bufferSize));
175
+ } else {
176
+ const bufferSize = this.options.videoBufferSize;
177
+ this.trackBuffers.set(trackId, new SampleBuffer(bufferSize));
178
+ }
179
+ }
180
+
181
+ async seek(trackId: number, timeMs: number) {
182
+ // Apply timeline offset to map user timeline to media timeline
183
+ const mediaTimeMs = timeMs + this.startTimeOffsetMs;
184
+
185
+ // Round using consistent precision handling
186
+ const roundedMediaTimeMs = roundToMilliseconds(mediaTimeMs);
187
+
188
+ // Serialize seek operations per track (but don't block iterator creation)
189
+ const existingSeek = this.trackSeekPromises.get(trackId);
190
+ if (existingSeek) {
191
+ await existingSeek;
192
+ }
193
+
194
+ const seekPromise = this.seekSafe(trackId, roundedMediaTimeMs);
195
+ this.trackSeekPromises.set(trackId, seekPromise);
196
+
197
+ try {
198
+ return await seekPromise;
199
+ } finally {
200
+ this.trackSeekPromises.delete(trackId);
201
+ }
202
+ }
203
+
204
+ private async resetIterator(trackId: number) {
205
+ const trackBuffer = this.trackBuffers.get(trackId);
206
+ trackBuffer?.clear();
207
+ // Clean up iterator safely - wait for any ongoing iterator creation
208
+ const ongoingIteratorCreation =
209
+ this.trackIteratorCreationPromises.get(trackId);
210
+ if (ongoingIteratorCreation) {
211
+ await ongoingIteratorCreation;
212
+ }
213
+
214
+ const iterator = this.trackIterators.get(trackId);
215
+ if (iterator) {
216
+ try {
217
+ await iterator.return?.();
218
+ } catch (_error) {
219
+ // Iterator cleanup failed, continue anyway
220
+ }
221
+ }
222
+ this.trackIterators.delete(trackId);
223
+ }
224
+
225
+ private async seekSafe(trackId: number, timeMs: number) {
226
+ // Get or create track-specific buffer
227
+ if (!this.trackBuffers.has(trackId)) {
228
+ await this.createTrackBuffer(trackId);
229
+ }
230
+ // biome-ignore lint/style/noNonNullAssertion: we know the map has the key
231
+ const trackBuffer = this.trackBuffers.get(trackId)!;
232
+
233
+ const track = await this.getTrack(trackId);
234
+
235
+ // Early validation: check if seek time is outside track bounds
236
+ // Use consistent precision handling throughout
237
+ const firstTimestampMs = roundToMilliseconds(
238
+ (await track.getFirstTimestamp()) * 1000,
239
+ );
240
+ let roundedTimeMs = roundToMilliseconds(timeMs);
241
+
242
+ // During rapid scrubbing, track.computeDuration() may only return the duration
243
+ // of currently loaded segments. Only validate against the start time, as the
244
+ // end time may not be accurate until all segments are loaded.
245
+ if (roundedTimeMs < firstTimestampMs) {
246
+ // GRACEFUL HANDLING: During rapid seeking, tasks can complete out of order, causing
247
+ // the audio buffer to contain segments for a different time range than the seek target.
248
+ // Only apply graceful adjustment if we have buffer contents that suggest a race condition.
249
+ // For empty buffers, allow normal seeking to proceed which may load the appropriate segments.
250
+
251
+ const bufferContents = trackBuffer.getContents();
252
+
253
+ if (bufferContents.length > 0) {
254
+ // We have loaded segments but they're for a different time range - adjust gracefully
255
+ timeMs = firstTimestampMs;
256
+ roundedTimeMs = roundToMilliseconds(timeMs);
257
+ } else {
258
+ // Empty buffer - let normal seeking proceed to load appropriate segments
259
+ // This maintains normal seeking behavior for tests and initial loads
260
+ }
261
+ }
262
+
263
+ // Note: If seeking beyond currently loaded segments, allow it to proceed
264
+ // The segment loading logic will handle fetching the needed segments
265
+ // No logging needed as this is a normal part of seeking behavior
266
+
267
+ // Check if we need to reset iterator for seeks outside current buffer range
268
+ const bufferContents = trackBuffer.getContents();
269
+ if (bufferContents.length > 0) {
270
+ const bufferStartMs = roundToMilliseconds(
271
+ trackBuffer.firstTimestamp * 1000,
272
+ );
273
+ const lastSample = bufferContents[bufferContents.length - 1];
274
+ const bufferEndMs = lastSample
275
+ ? roundToMilliseconds(
276
+ (lastSample.timestamp + (lastSample.duration || 0)) * 1000,
277
+ )
278
+ : bufferStartMs;
279
+
280
+ // If seeking outside current buffer range, reset iterator to load appropriate data
281
+ if (roundedTimeMs < bufferStartMs || roundedTimeMs > bufferEndMs) {
282
+ await this.resetIterator(trackId);
283
+ }
284
+ }
285
+
286
+ const alreadyInBuffer = trackBuffer.find(timeMs);
287
+ if (alreadyInBuffer) return alreadyInBuffer;
288
+
289
+ const iterator = await this.getTrackIterator(trackId);
290
+ while (true) {
291
+ const { done, value: decodedSample } = await iterator.next();
292
+ if (decodedSample) {
293
+ trackBuffer.push(decodedSample);
294
+ }
295
+ const foundSample = trackBuffer.find(timeMs);
296
+ if (foundSample) {
297
+ return foundSample;
298
+ }
299
+ if (done) {
300
+ break;
301
+ }
302
+ }
303
+
304
+ // If no exact sample found and we've reached the end of the track,
305
+ // check if the seek time is beyond the actual track duration.
306
+ // If so, return the last available sample instead of throwing an error.
307
+ const finalBufferContents = trackBuffer.getContents();
308
+ if (finalBufferContents.length > 0) {
309
+ const lastSample = finalBufferContents[finalBufferContents.length - 1];
310
+ const lastSampleEndMs = roundToMilliseconds(
311
+ ((lastSample?.timestamp || 0) + (lastSample?.duration || 0)) * 1000,
312
+ );
313
+
314
+ // If seeking past the last sample, return the last sample silently
315
+ if (roundToMilliseconds(timeMs) >= lastSampleEndMs) {
316
+ return lastSample;
317
+ }
318
+ }
319
+
320
+ throw new NoSample(
321
+ `Sample not found for time ${timeMs} in ${track.type} track ${trackId}`,
322
+ );
323
+ }
324
+ }
@@ -0,0 +1,165 @@
1
+ import { describe } from "vitest";
2
+ import { test as baseTest } from "../../../test/useMSW.js";
3
+
4
+ import type { ManifestResponse } from "../../transcoding/types/index.js";
5
+ import { UrlGenerator } from "../../transcoding/utils/UrlGenerator";
6
+ import "../EFVideo.js";
7
+ import type { EFVideo } from "../EFVideo.js";
8
+ import { JitMediaEngine } from "./JitMediaEngine";
9
+
10
+ const test = baseTest.extend<{
11
+ emptyManifestResponse: ManifestResponse;
12
+ urlGenerator: UrlGenerator;
13
+ manifestUrl: string;
14
+ mediaEngine: JitMediaEngine;
15
+ abortSignal: AbortSignal;
16
+ testUrl: string;
17
+ host: EFVideo;
18
+ }>({
19
+ mediaEngine: async ({ manifestUrl, urlGenerator, host }, use: any) => {
20
+ const engine = await JitMediaEngine.fetch(host, urlGenerator, manifestUrl);
21
+ await use(engine);
22
+ },
23
+ manifestUrl: async ({ urlGenerator, host }, use: any) => {
24
+ const url = urlGenerator.generateManifestUrl(host.src);
25
+ await use(url);
26
+ },
27
+
28
+ emptyManifestResponse: async ({}, use: any) => {
29
+ const emptyResponse: ManifestResponse = {
30
+ version: "1.0",
31
+ type: "cmaf",
32
+ duration: 60,
33
+ durationMs: 60000,
34
+ segmentDuration: 4000,
35
+ baseUrl: "http://api.example.com/",
36
+ sourceUrl: "http://example.com/video.mp4",
37
+ audioRenditions: [],
38
+ videoRenditions: [],
39
+ endpoints: {
40
+ initSegment: "http://api.example.com/init/{renditionId}",
41
+ mediaSegment:
42
+ "http://api.example.com/segment/{segmentId}/{renditionId}",
43
+ },
44
+ jitInfo: {
45
+ parallelTranscodingSupported: true,
46
+ expectedTranscodeLatency: 1000,
47
+ segmentCount: 15,
48
+ },
49
+ };
50
+ await use(emptyResponse);
51
+ },
52
+ host: async ({}, use: any) => {
53
+ const configuration = document.createElement("ef-configuration");
54
+ // Use integrated proxy server (same host/port as test runner)
55
+ const apiHost = `${window.location.protocol}//${window.location.host}`;
56
+ configuration.setAttribute("api-host", apiHost);
57
+ configuration.apiHost = apiHost;
58
+ const host = document.createElement("ef-video");
59
+ configuration.appendChild(host);
60
+ host.src = "http://web:3000/head-moov-480p.mp4";
61
+ await use(host);
62
+ },
63
+ urlGenerator: async ({}, use: any) => {
64
+ // UrlGenerator points to integrated proxy server (same host/port as test runner)
65
+ const apiHost = `${window.location.protocol}//${window.location.host}`;
66
+ const generator = new UrlGenerator(() => apiHost);
67
+ await use(generator);
68
+ },
69
+
70
+ abortSignal: async ({}, use: any) => {
71
+ const signal = new AbortController().signal;
72
+ await use(signal);
73
+ },
74
+ testUrl: async ({}, use: any) => {
75
+ const url = "http://api.example.com/manifest";
76
+ await use(url);
77
+ },
78
+ });
79
+
80
+ describe("JitMediaEngine", () => {
81
+ test("provides duration from manifest data", async ({
82
+ mediaEngine,
83
+ expect,
84
+ }) => {
85
+ expect(mediaEngine.durationMs).toBe(10000);
86
+ });
87
+
88
+ test("provides source URL from manifest data", async ({
89
+ mediaEngine,
90
+ host,
91
+ expect,
92
+ }) => {
93
+ expect(mediaEngine.src).toBe(host.src);
94
+ });
95
+
96
+ test("returns audio rendition with correct properties", ({
97
+ mediaEngine,
98
+ host,
99
+ expect,
100
+ }) => {
101
+ const audioRendition = mediaEngine.audioRendition;
102
+
103
+ expect(audioRendition).toBeDefined();
104
+ expect(audioRendition!.id).toBe("audio");
105
+ expect(audioRendition!.trackId).toBeUndefined();
106
+ expect(audioRendition!.src).toBe(host.src);
107
+ expect(audioRendition!.segmentDurationMs).toBe(2000);
108
+ });
109
+
110
+ test("returns undefined audio rendition when none available", ({
111
+ urlGenerator,
112
+ emptyManifestResponse,
113
+ host,
114
+ expect,
115
+ }) => {
116
+ const engine = new JitMediaEngine(
117
+ host,
118
+ urlGenerator,
119
+ emptyManifestResponse,
120
+ );
121
+
122
+ expect(engine.audioRendition).toBeUndefined();
123
+ });
124
+
125
+ test("returns video rendition with correct properties", ({
126
+ mediaEngine,
127
+ host,
128
+ expect,
129
+ }) => {
130
+ const videoRendition = mediaEngine.videoRendition;
131
+
132
+ expect(videoRendition).toBeDefined();
133
+ expect(videoRendition!.id).toBe("high");
134
+ expect(videoRendition!.trackId).toBeUndefined();
135
+ expect(videoRendition!.src).toBe(host.src);
136
+ expect(videoRendition!.segmentDurationMs).toBe(2000);
137
+ });
138
+
139
+ test("returns undefined video rendition when none available", ({
140
+ urlGenerator,
141
+ emptyManifestResponse,
142
+ host,
143
+ expect,
144
+ }) => {
145
+ const engine = new JitMediaEngine(
146
+ host,
147
+ urlGenerator,
148
+ emptyManifestResponse,
149
+ );
150
+
151
+ expect(engine.videoRendition).toBeUndefined();
152
+ });
153
+
154
+ test("provides templates from manifest endpoints", ({
155
+ mediaEngine,
156
+ expect,
157
+ }) => {
158
+ expect(mediaEngine.templates).toEqual({
159
+ initSegment:
160
+ "http://localhost:63315/api/v1/transcode/{rendition}/init.m4s?url=http%3A%2F%2Fweb%3A3000%2Fhead-moov-480p.mp4",
161
+ mediaSegment:
162
+ "http://localhost:63315/api/v1/transcode/{rendition}/{segmentId}.m4s?url=http%3A%2F%2Fweb%3A3000%2Fhead-moov-480p.mp4",
163
+ });
164
+ });
165
+ });
@@ -0,0 +1,166 @@
1
+ import type {
2
+ AudioRendition,
3
+ MediaEngine,
4
+ RenditionId,
5
+ VideoRendition,
6
+ } from "../../transcoding/types";
7
+ import type { ManifestResponse } from "../../transcoding/types/index.js";
8
+ import type { UrlGenerator } from "../../transcoding/utils/UrlGenerator";
9
+ import type { EFMedia } from "../EFMedia.js";
10
+ import { BaseMediaEngine } from "./BaseMediaEngine";
11
+
12
+ export class JitMediaEngine extends BaseMediaEngine implements MediaEngine {
13
+ static async fetch(host: EFMedia, urlGenerator: UrlGenerator, url: string) {
14
+ const response = await host.fetch(url);
15
+ const data = (await response.json()) as ManifestResponse;
16
+ return new JitMediaEngine(host, urlGenerator, data);
17
+ }
18
+
19
+ constructor(
20
+ public host: EFMedia,
21
+ private urlGenerator: UrlGenerator,
22
+ private data: ManifestResponse,
23
+ ) {
24
+ super();
25
+ }
26
+
27
+ get durationMs() {
28
+ return this.data.durationMs;
29
+ }
30
+
31
+ get src() {
32
+ return this.data.sourceUrl;
33
+ }
34
+
35
+ get audioRendition(): AudioRendition | undefined {
36
+ const rendition = this.data.audioRenditions[0];
37
+
38
+ if (!rendition) return undefined;
39
+ return {
40
+ id: rendition.id as RenditionId,
41
+ trackId: undefined,
42
+ src: this.data.sourceUrl,
43
+ segmentDurationMs: rendition.segmentDurationMs,
44
+ segmentDurationsMs: rendition.segmentDurationsMs,
45
+ };
46
+ }
47
+
48
+ get videoRendition(): VideoRendition | undefined {
49
+ const rendition = this.data.videoRenditions[0];
50
+
51
+ if (!rendition) return undefined;
52
+ return {
53
+ id: rendition.id as RenditionId,
54
+ trackId: undefined,
55
+ src: this.data.sourceUrl,
56
+ segmentDurationMs: rendition.segmentDurationMs,
57
+ segmentDurationsMs: rendition.segmentDurationsMs,
58
+ };
59
+ }
60
+
61
+ get templates() {
62
+ return this.data.endpoints;
63
+ }
64
+
65
+ async fetchInitSegment(
66
+ rendition: { id?: RenditionId; trackId: number | undefined; src: string },
67
+ signal: AbortSignal,
68
+ ) {
69
+ if (!rendition.id) {
70
+ throw new Error("Rendition ID is required for JIT metadata");
71
+ }
72
+ const url = this.urlGenerator.generateSegmentUrl(
73
+ "init",
74
+ rendition.id,
75
+ this,
76
+ );
77
+ const response = await this.host.fetch(url, { signal });
78
+ const arrayBuffer = await response.arrayBuffer();
79
+ return arrayBuffer;
80
+ }
81
+
82
+ async fetchMediaSegmentImpl(
83
+ segmentId: number,
84
+ rendition: { id?: RenditionId; trackId: number | undefined; src: string },
85
+ ) {
86
+ if (!rendition.id) {
87
+ throw new Error("Rendition ID is required for JIT metadata");
88
+ }
89
+ const url = this.urlGenerator.generateSegmentUrl(
90
+ segmentId,
91
+ rendition.id,
92
+ this,
93
+ );
94
+ return this.fetchMediaCache(url);
95
+ }
96
+
97
+ computeSegmentId(
98
+ desiredSeekTimeMs: number,
99
+ rendition: VideoRendition | AudioRendition,
100
+ ) {
101
+ // Don't request segments beyond the actual file duration
102
+ // Note: seeking to exactly durationMs should be allowed (it's the last moment of the file)
103
+ if (desiredSeekTimeMs > this.durationMs) {
104
+ return undefined;
105
+ }
106
+
107
+ // Use actual segment durations if available (more accurate)
108
+ if (
109
+ rendition.segmentDurationsMs &&
110
+ rendition.segmentDurationsMs.length > 0
111
+ ) {
112
+ let cumulativeTime = 0;
113
+
114
+ for (let i = 0; i < rendition.segmentDurationsMs.length; i++) {
115
+ const segmentDuration = rendition.segmentDurationsMs[i];
116
+ if (segmentDuration === undefined) {
117
+ throw new Error("Segment duration is required for JIT metadata");
118
+ }
119
+ const segmentStartMs = cumulativeTime;
120
+ const segmentEndMs = cumulativeTime + segmentDuration;
121
+
122
+ // Check if the desired seek time falls within this segment
123
+ // Special case: for the last segment, include the exact end time
124
+ const isLastSegment = i === rendition.segmentDurationsMs.length - 1;
125
+ const includesEndTime =
126
+ isLastSegment && desiredSeekTimeMs === this.durationMs;
127
+
128
+ if (
129
+ desiredSeekTimeMs >= segmentStartMs &&
130
+ (desiredSeekTimeMs < segmentEndMs || includesEndTime)
131
+ ) {
132
+ return i + 1; // Convert 0-based to 1-based segment ID
133
+ }
134
+
135
+ cumulativeTime += segmentDuration;
136
+
137
+ // If we've reached or exceeded file duration, stop
138
+ if (cumulativeTime >= this.durationMs) {
139
+ break;
140
+ }
141
+ }
142
+
143
+ // If we didn't find a segment, return undefined
144
+ return undefined;
145
+ }
146
+
147
+ // Fall back to fixed duration calculation for backward compatibility
148
+ if (!rendition.segmentDurationMs) {
149
+ throw new Error("Segment duration is required for JIT metadata");
150
+ }
151
+
152
+ const segmentIndex = Math.floor(
153
+ desiredSeekTimeMs / rendition.segmentDurationMs,
154
+ );
155
+
156
+ // Calculate the actual segment start time
157
+ const segmentStartMs = segmentIndex * rendition.segmentDurationMs;
158
+
159
+ // If this segment would start at or beyond file duration, it doesn't exist
160
+ if (segmentStartMs >= this.durationMs) {
161
+ return undefined;
162
+ }
163
+
164
+ return segmentIndex + 1; // Convert 0-based to 1-based
165
+ }
166
+ }