@editframe/elements 0.17.6-beta.0 → 0.18.3-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 (218) hide show
  1. package/dist/EF_FRAMEGEN.js +1 -1
  2. package/dist/ScrubTrackManager.d.ts +2 -2
  3. package/dist/elements/EFAudio.d.ts +21 -2
  4. package/dist/elements/EFAudio.js +41 -11
  5. package/dist/elements/EFImage.d.ts +1 -0
  6. package/dist/elements/EFImage.js +11 -3
  7. package/dist/elements/EFMedia/AssetIdMediaEngine.d.ts +18 -0
  8. package/dist/elements/EFMedia/AssetIdMediaEngine.js +41 -0
  9. package/dist/elements/EFMedia/AssetMediaEngine.d.ts +47 -0
  10. package/dist/elements/EFMedia/AssetMediaEngine.js +116 -0
  11. package/dist/elements/EFMedia/BaseMediaEngine.d.ts +55 -0
  12. package/dist/elements/EFMedia/BaseMediaEngine.js +96 -0
  13. package/dist/elements/EFMedia/BufferedSeekingInput.d.ts +43 -0
  14. package/dist/elements/EFMedia/BufferedSeekingInput.js +159 -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 +62 -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 +138 -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 +22 -0
  29. package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.d.ts +7 -0
  30. package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.js +24 -0
  31. package/dist/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.d.ts +4 -0
  32. package/dist/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.js +18 -0
  33. package/dist/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.d.ts +4 -0
  34. package/dist/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.js +16 -0
  35. package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.d.ts +3 -0
  36. package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.js +104 -0
  37. package/dist/elements/EFMedia/services/AudioElementFactory.d.ts +22 -0
  38. package/dist/elements/EFMedia/services/AudioElementFactory.js +72 -0
  39. package/dist/elements/EFMedia/services/MediaSourceService.browsertest.d.ts +1 -0
  40. package/dist/elements/EFMedia/services/MediaSourceService.d.ts +47 -0
  41. package/dist/elements/EFMedia/services/MediaSourceService.js +73 -0
  42. package/dist/elements/EFMedia/shared/AudioSpanUtils.d.ts +7 -0
  43. package/dist/elements/EFMedia/shared/AudioSpanUtils.js +54 -0
  44. package/dist/elements/EFMedia/shared/BufferUtils.d.ts +70 -0
  45. package/dist/elements/EFMedia/shared/BufferUtils.js +89 -0
  46. package/dist/elements/EFMedia/shared/MediaTaskUtils.d.ts +23 -0
  47. package/dist/elements/EFMedia/shared/RenditionHelpers.browsertest.d.ts +1 -0
  48. package/dist/elements/EFMedia/shared/RenditionHelpers.d.ts +19 -0
  49. package/dist/elements/EFMedia/tasks/makeMediaEngineTask.browsertest.d.ts +1 -0
  50. package/dist/elements/EFMedia/tasks/makeMediaEngineTask.d.ts +18 -0
  51. package/dist/elements/EFMedia/tasks/makeMediaEngineTask.js +60 -0
  52. package/dist/elements/EFMedia/tasks/makeMediaEngineTask.test.d.ts +1 -0
  53. package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.browsertest.d.ts +9 -0
  54. package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.d.ts +16 -0
  55. package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.js +46 -0
  56. package/dist/elements/EFMedia/videoTasks/makeVideoInitSegmentFetchTask.browsertest.d.ts +9 -0
  57. package/dist/elements/EFMedia/videoTasks/makeVideoInitSegmentFetchTask.d.ts +4 -0
  58. package/dist/elements/EFMedia/videoTasks/makeVideoInitSegmentFetchTask.js +16 -0
  59. package/dist/elements/EFMedia/videoTasks/makeVideoInputTask.browsertest.d.ts +9 -0
  60. package/dist/elements/EFMedia/videoTasks/makeVideoInputTask.d.ts +3 -0
  61. package/dist/elements/EFMedia/videoTasks/makeVideoInputTask.js +27 -0
  62. package/dist/elements/EFMedia/videoTasks/makeVideoSeekTask.d.ts +7 -0
  63. package/dist/elements/EFMedia/videoTasks/makeVideoSeekTask.js +25 -0
  64. package/dist/elements/EFMedia/videoTasks/makeVideoSegmentFetchTask.browsertest.d.ts +9 -0
  65. package/dist/elements/EFMedia/videoTasks/makeVideoSegmentFetchTask.d.ts +4 -0
  66. package/dist/elements/EFMedia/videoTasks/makeVideoSegmentFetchTask.js +18 -0
  67. package/dist/elements/EFMedia/videoTasks/makeVideoSegmentIdTask.browsertest.d.ts +9 -0
  68. package/dist/elements/EFMedia/videoTasks/makeVideoSegmentIdTask.d.ts +4 -0
  69. package/dist/elements/EFMedia/videoTasks/makeVideoSegmentIdTask.js +16 -0
  70. package/dist/elements/EFMedia.browsertest.d.ts +1 -0
  71. package/dist/elements/EFMedia.d.ts +75 -111
  72. package/dist/elements/EFMedia.js +141 -1111
  73. package/dist/elements/EFTemporal.d.ts +1 -1
  74. package/dist/elements/EFTemporal.js +1 -1
  75. package/dist/elements/EFTimegroup.d.ts +11 -0
  76. package/dist/elements/EFTimegroup.js +88 -13
  77. package/dist/elements/EFVideo.d.ts +60 -29
  78. package/dist/elements/EFVideo.js +103 -203
  79. package/dist/elements/EFWaveform.js +2 -2
  80. package/dist/elements/SampleBuffer.d.ts +14 -0
  81. package/dist/elements/SampleBuffer.js +52 -0
  82. package/dist/getRenderInfo.d.ts +2 -2
  83. package/dist/getRenderInfo.js +2 -1
  84. package/dist/gui/ContextMixin.js +17 -70
  85. package/dist/gui/EFFilmstrip.d.ts +3 -3
  86. package/dist/gui/EFFilmstrip.js +1 -1
  87. package/dist/gui/EFFitScale.d.ts +2 -2
  88. package/dist/gui/TWMixin.js +1 -1
  89. package/dist/gui/services/ElementConnectionManager.browsertest.d.ts +1 -0
  90. package/dist/gui/services/ElementConnectionManager.d.ts +59 -0
  91. package/dist/gui/services/ElementConnectionManager.js +128 -0
  92. package/dist/gui/services/PlaybackController.browsertest.d.ts +1 -0
  93. package/dist/gui/services/PlaybackController.d.ts +103 -0
  94. package/dist/gui/services/PlaybackController.js +290 -0
  95. package/dist/services/MediaSourceManager.d.ts +62 -0
  96. package/dist/services/MediaSourceManager.js +211 -0
  97. package/dist/style.css +1 -1
  98. package/dist/transcoding/cache/CacheManager.d.ts +73 -0
  99. package/dist/transcoding/cache/RequestDeduplicator.d.ts +29 -0
  100. package/dist/transcoding/cache/RequestDeduplicator.js +53 -0
  101. package/dist/transcoding/cache/RequestDeduplicator.test.d.ts +1 -0
  102. package/dist/transcoding/types/index.d.ts +242 -0
  103. package/dist/transcoding/utils/MediaUtils.d.ts +9 -0
  104. package/dist/transcoding/utils/UrlGenerator.d.ts +26 -0
  105. package/dist/transcoding/utils/UrlGenerator.js +45 -0
  106. package/dist/transcoding/utils/constants.d.ts +27 -0
  107. package/dist/utils/LRUCache.d.ts +34 -0
  108. package/dist/utils/LRUCache.js +115 -0
  109. package/package.json +3 -2
  110. package/src/elements/EFAudio.browsertest.ts +183 -43
  111. package/src/elements/EFAudio.ts +59 -13
  112. package/src/elements/EFImage.browsertest.ts +42 -0
  113. package/src/elements/EFImage.ts +23 -3
  114. package/src/elements/EFMedia/AssetIdMediaEngine.test.ts +222 -0
  115. package/src/elements/EFMedia/AssetIdMediaEngine.ts +70 -0
  116. package/src/elements/EFMedia/AssetMediaEngine.ts +210 -0
  117. package/src/elements/EFMedia/BaseMediaEngine.test.ts +164 -0
  118. package/src/elements/EFMedia/BaseMediaEngine.ts +170 -0
  119. package/src/elements/EFMedia/BufferedSeekingInput.browsertest.ts +400 -0
  120. package/src/elements/EFMedia/BufferedSeekingInput.ts +267 -0
  121. package/src/elements/EFMedia/JitMediaEngine.browsertest.ts +165 -0
  122. package/src/elements/EFMedia/JitMediaEngine.ts +110 -0
  123. package/src/elements/EFMedia/audioTasks/makeAudioBufferTask.browsertest.ts +554 -0
  124. package/src/elements/EFMedia/audioTasks/makeAudioBufferTask.ts +81 -0
  125. package/src/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.ts +241 -0
  126. package/src/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.browsertest.ts +59 -0
  127. package/src/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.ts +23 -0
  128. package/src/elements/EFMedia/audioTasks/makeAudioInputTask.browsertest.ts +55 -0
  129. package/src/elements/EFMedia/audioTasks/makeAudioInputTask.ts +35 -0
  130. package/src/elements/EFMedia/audioTasks/makeAudioSeekTask.ts +42 -0
  131. package/src/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.ts +34 -0
  132. package/src/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.ts +23 -0
  133. package/src/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.ts +174 -0
  134. package/src/elements/EFMedia/services/AudioElementFactory.browsertest.ts +325 -0
  135. package/src/elements/EFMedia/services/AudioElementFactory.ts +119 -0
  136. package/src/elements/EFMedia/services/MediaSourceService.browsertest.ts +257 -0
  137. package/src/elements/EFMedia/services/MediaSourceService.ts +102 -0
  138. package/src/elements/EFMedia/shared/AudioSpanUtils.ts +128 -0
  139. package/src/elements/EFMedia/shared/BufferUtils.ts +310 -0
  140. package/src/elements/EFMedia/shared/MediaTaskUtils.ts +44 -0
  141. package/src/elements/EFMedia/shared/RenditionHelpers.browsertest.ts +247 -0
  142. package/src/elements/EFMedia/shared/RenditionHelpers.ts +79 -0
  143. package/src/elements/EFMedia/tasks/makeMediaEngineTask.browsertest.ts +128 -0
  144. package/src/elements/EFMedia/tasks/makeMediaEngineTask.test.ts +233 -0
  145. package/src/elements/EFMedia/tasks/makeMediaEngineTask.ts +89 -0
  146. package/src/elements/EFMedia/videoTasks/makeVideoBufferTask.browsertest.ts +555 -0
  147. package/src/elements/EFMedia/videoTasks/makeVideoBufferTask.ts +79 -0
  148. package/src/elements/EFMedia/videoTasks/makeVideoInitSegmentFetchTask.browsertest.ts +59 -0
  149. package/src/elements/EFMedia/videoTasks/makeVideoInitSegmentFetchTask.ts +23 -0
  150. package/src/elements/EFMedia/videoTasks/makeVideoInputTask.browsertest.ts +55 -0
  151. package/src/elements/EFMedia/videoTasks/makeVideoInputTask.ts +45 -0
  152. package/src/elements/EFMedia/videoTasks/makeVideoSeekTask.ts +44 -0
  153. package/src/elements/EFMedia/videoTasks/makeVideoSegmentFetchTask.browsertest.ts +57 -0
  154. package/src/elements/EFMedia/videoTasks/makeVideoSegmentFetchTask.ts +32 -0
  155. package/src/elements/EFMedia/videoTasks/makeVideoSegmentIdTask.browsertest.ts +56 -0
  156. package/src/elements/EFMedia/videoTasks/makeVideoSegmentIdTask.ts +23 -0
  157. package/src/elements/EFMedia.browsertest.ts +658 -265
  158. package/src/elements/EFMedia.ts +173 -1763
  159. package/src/elements/EFTemporal.ts +3 -4
  160. package/src/elements/EFTimegroup.browsertest.ts +6 -3
  161. package/src/elements/EFTimegroup.ts +152 -21
  162. package/src/elements/EFVideo.browsertest.ts +115 -37
  163. package/src/elements/EFVideo.ts +123 -452
  164. package/src/elements/EFWaveform.ts +1 -1
  165. package/src/elements/MediaController.ts +2 -12
  166. package/src/elements/SampleBuffer.ts +97 -0
  167. package/src/gui/ContextMixin.ts +23 -104
  168. package/src/gui/services/ElementConnectionManager.browsertest.ts +263 -0
  169. package/src/gui/services/ElementConnectionManager.ts +224 -0
  170. package/src/gui/services/PlaybackController.browsertest.ts +437 -0
  171. package/src/gui/services/PlaybackController.ts +521 -0
  172. package/src/services/MediaSourceManager.ts +333 -0
  173. package/src/transcoding/cache/CacheManager.ts +208 -0
  174. package/src/transcoding/cache/RequestDeduplicator.test.ts +170 -0
  175. package/src/transcoding/cache/RequestDeduplicator.ts +65 -0
  176. package/src/transcoding/types/index.ts +265 -0
  177. package/src/transcoding/utils/MediaUtils.ts +63 -0
  178. package/src/transcoding/utils/UrlGenerator.ts +68 -0
  179. package/src/transcoding/utils/constants.ts +36 -0
  180. package/src/utils/LRUCache.ts +153 -0
  181. package/test/EFVideo.framegen.browsertest.ts +38 -29
  182. 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
  183. 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
  184. 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
  185. 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
  186. 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
  187. 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
  188. 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
  189. 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
  190. 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
  191. 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
  192. 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
  193. 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
  194. 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
  195. 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
  196. 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
  197. 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
  198. 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
  199. 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
  200. 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
  201. 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
  202. package/test/__cache__/GET__api_v1_transcode_manifest_json_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__3be92a0437de726b431ed5af2369158a/data.bin +1 -0
  203. package/test/__cache__/GET__api_v1_transcode_manifest_json_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__3be92a0437de726b431ed5af2369158a/metadata.json +19 -0
  204. package/test/createJitTestClips.ts +320 -188
  205. package/test/recordReplayProxyPlugin.js +302 -0
  206. package/test/useAssetMSW.ts +1 -1
  207. package/test/useMSW.ts +35 -22
  208. package/types.json +1 -1
  209. package/dist/JitTranscodingClient.d.ts +0 -167
  210. package/dist/JitTranscodingClient.js +0 -373
  211. package/dist/ScrubTrackManager.js +0 -216
  212. package/dist/elements/printTaskStatus.js +0 -11
  213. package/src/elements/__screenshots__/EFMedia.browsertest.ts/EFMedia-JIT-audio-playback-audioBufferTask-should-work-in-JIT-mode-without-URL-errors-1.png +0 -0
  214. package/test/EFVideo.frame-tasks.browsertest.ts +0 -524
  215. /package/dist/{JitTranscodingClient.browsertest.d.ts → elements/EFMedia/AssetIdMediaEngine.test.d.ts} +0 -0
  216. /package/dist/{JitTranscodingClient.test.d.ts → elements/EFMedia/BaseMediaEngine.test.d.ts} +0 -0
  217. /package/dist/{ScrubTrackIntegration.test.d.ts → elements/EFMedia/BufferedSeekingInput.browsertest.d.ts} +0 -0
  218. /package/dist/{SegmentSwitchLoading.test.d.ts → elements/EFMedia/services/AudioElementFactory.browsertest.d.ts} +0 -0
@@ -0,0 +1,170 @@
1
+ import { RequestDeduplicator } from "../../transcoding/cache/RequestDeduplicator.js";
2
+ import type {
3
+ AudioRendition,
4
+ SegmentTimeRange,
5
+ VideoRendition,
6
+ } from "../../transcoding/types";
7
+ import { SizeAwareLRUCache } from "../../utils/LRUCache.js";
8
+ import type { EFMedia } from "../EFMedia.js";
9
+
10
+ // 100MB cache limit
11
+ const mediaCache = new SizeAwareLRUCache<string>(100 * 1024 * 1024);
12
+
13
+ export abstract class BaseMediaEngine {
14
+ // Request deduplicator for in-flight segment fetches
15
+ private requestDeduplicator = new RequestDeduplicator();
16
+
17
+ abstract get videoRendition(): VideoRendition | undefined;
18
+ abstract get audioRendition(): AudioRendition | undefined;
19
+ abstract get host(): EFMedia;
20
+
21
+ getVideoRendition(): VideoRendition {
22
+ if (!this.videoRendition) {
23
+ throw new Error("No video rendition available");
24
+ }
25
+ return this.videoRendition;
26
+ }
27
+
28
+ getAudioRendition(): AudioRendition {
29
+ if (!this.audioRendition) {
30
+ throw new Error("No audio rendition available");
31
+ }
32
+ return this.audioRendition;
33
+ }
34
+
35
+ /**
36
+ * Generate cache key for segment requests
37
+ */
38
+ private getSegmentCacheKey(
39
+ segmentId: number,
40
+ rendition: { src: string; trackId: number | undefined; id?: string },
41
+ ): string {
42
+ return `${rendition.src}-${rendition.id}-${segmentId}-${rendition.trackId}`;
43
+ }
44
+
45
+ /**
46
+ * Abstract method for actual segment fetching - implemented by subclasses
47
+ */
48
+ abstract fetchMediaSegmentImpl(
49
+ segmentId: number,
50
+ rendition: { trackId: number | undefined; src: string },
51
+ ): Promise<ArrayBuffer>;
52
+
53
+ /**
54
+ * Fetch media segment with built-in deduplication
55
+ * Eliminates the need for separate coordinators - cleaner architecture
56
+ */
57
+ async fetchMediaSegment(
58
+ segmentId: number,
59
+ rendition: { trackId: number | undefined; src: string },
60
+ _signal?: AbortSignal,
61
+ ): Promise<ArrayBuffer> {
62
+ const cacheKey = this.getSegmentCacheKey(segmentId, rendition);
63
+
64
+ return this.requestDeduplicator.executeRequest(cacheKey, async () => {
65
+ return this.fetchMediaSegmentImpl(segmentId, rendition);
66
+ });
67
+ }
68
+
69
+ /**
70
+ * Check if a segment is currently being fetched
71
+ */
72
+ isSegmentBeingFetched(
73
+ segmentId: number,
74
+ rendition: { src: string; trackId: number | undefined },
75
+ ): boolean {
76
+ const cacheKey = this.getSegmentCacheKey(segmentId, rendition);
77
+ return this.requestDeduplicator.isPending(cacheKey);
78
+ }
79
+
80
+ /**
81
+ * Get count of active segment requests (for debugging/monitoring)
82
+ */
83
+ getActiveSegmentRequestCount(): number {
84
+ return this.requestDeduplicator.getPendingCount();
85
+ }
86
+
87
+ /**
88
+ * Cancel all active segment requests (for cleanup)
89
+ */
90
+ cancelAllSegmentRequests(): void {
91
+ this.requestDeduplicator.clear();
92
+ }
93
+
94
+ async fetchMediaCache(mediaUrl: string) {
95
+ const cached = mediaCache.get(mediaUrl);
96
+ if (cached) {
97
+ return cached;
98
+ }
99
+ const promise = this.host
100
+ .fetch(mediaUrl)
101
+ .then((response) => response.arrayBuffer());
102
+ mediaCache.set(mediaUrl, promise);
103
+ return promise;
104
+ }
105
+
106
+ /**
107
+ * Enhanced caching method that supports custom headers (e.g., Range requests)
108
+ * Cache key includes both URL and headers for proper cache isolation
109
+ */
110
+ async fetchMediaCacheWithHeaders(
111
+ mediaUrl: string,
112
+ headers?: Record<string, string>,
113
+ signal?: AbortSignal,
114
+ ) {
115
+ // Create a cache key that includes both URL and headers
116
+ const cacheKey = headers
117
+ ? `${mediaUrl}:${JSON.stringify(headers)}`
118
+ : mediaUrl;
119
+
120
+ const cached = mediaCache.get(cacheKey);
121
+ if (cached) {
122
+ return cached;
123
+ }
124
+
125
+ const promise = this.host
126
+ .fetch(mediaUrl, { headers, signal })
127
+ .then((response) => response.arrayBuffer());
128
+ mediaCache.set(cacheKey, promise);
129
+ return promise;
130
+ }
131
+
132
+ /**
133
+ * Calculate audio segments needed for a time range
134
+ * Each media engine implements this based on their segment structure
135
+ */
136
+ calculateAudioSegmentRange(
137
+ fromMs: number,
138
+ toMs: number,
139
+ rendition: AudioRendition,
140
+ durationMs: number,
141
+ ): SegmentTimeRange[] {
142
+ // Default implementation for uniform segments (used by JitMediaEngine)
143
+ if (fromMs >= toMs) {
144
+ return [];
145
+ }
146
+
147
+ const segmentDurationMs = rendition.segmentDurationMs || 1000;
148
+ const segments: SegmentTimeRange[] = [];
149
+
150
+ const startSegmentIndex = Math.floor(fromMs / segmentDurationMs);
151
+ const endSegmentIndex = Math.floor(toMs / segmentDurationMs);
152
+
153
+ for (let i = startSegmentIndex; i <= endSegmentIndex; i++) {
154
+ const segmentId = i + 1; // Convert to 1-based
155
+ const segmentStartMs = i * segmentDurationMs;
156
+ const segmentEndMs = Math.min((i + 1) * segmentDurationMs, durationMs);
157
+
158
+ // Only include segments that overlap with requested time range
159
+ if (segmentStartMs < toMs && segmentEndMs > fromMs) {
160
+ segments.push({
161
+ segmentId,
162
+ startMs: segmentStartMs,
163
+ endMs: segmentEndMs,
164
+ });
165
+ }
166
+ }
167
+
168
+ return segments;
169
+ }
170
+ }
@@ -0,0 +1,400 @@
1
+ import { test as baseTest, describe } from "vitest";
2
+ import { BufferedSeekingInput, NoSample } from "./BufferedSeekingInput";
3
+
4
+ const test = baseTest.extend<{
5
+ fiveSampleBuffer: BufferedSeekingInput;
6
+ inputAtStart: BufferedSeekingInput;
7
+ inputAtMiddle: BufferedSeekingInput;
8
+ segment2: BufferedSeekingInput;
9
+ }>({
10
+ fiveSampleBuffer: async ({}, use) => {
11
+ const response = await fetch("/jit-segments/segment-0ms-2s-low.mp4");
12
+ const arrayBuffer = await response.arrayBuffer();
13
+ const input = new BufferedSeekingInput(arrayBuffer, {
14
+ videoBufferSize: 5,
15
+ audioBufferSize: 5,
16
+ });
17
+ await use(input);
18
+ },
19
+ inputAtStart: async ({}, use) => {
20
+ const response = await fetch("/jit-segments/segment-0ms-2s-low.mp4");
21
+ const arrayBuffer = await response.arrayBuffer();
22
+ const input = new BufferedSeekingInput(arrayBuffer);
23
+ await use(input);
24
+ },
25
+ inputAtMiddle: async ({}, use) => {
26
+ const response = await fetch("/jit-segments/segment-6000ms-1s-low.mp4");
27
+ const arrayBuffer = await response.arrayBuffer();
28
+ const input = new BufferedSeekingInput(arrayBuffer);
29
+ await use(input);
30
+ },
31
+ segment2: async ({}, use) => {
32
+ const response = await fetch("/jit-segments/segment-2.mp4");
33
+ const arrayBuffer = await response.arrayBuffer();
34
+ const input = new BufferedSeekingInput(arrayBuffer);
35
+ await use(input);
36
+ },
37
+ });
38
+
39
+ describe("BufferedSeekingInput", () => {
40
+ describe("computeDuration", () => {
41
+ test("computes duration", async ({
42
+ expect,
43
+ inputAtStart,
44
+ inputAtMiddle,
45
+ }) => {
46
+ await expect(inputAtStart.computeDuration()).resolves.toBe(2);
47
+ await expect(inputAtMiddle.computeDuration()).resolves.toBeCloseTo(0.96);
48
+ });
49
+ });
50
+
51
+ describe("basic seeking", () => {
52
+ test("seeks to frame at 0 seconds", async ({ expect, inputAtStart }) => {
53
+ const sample = await inputAtStart.seek(1, 0);
54
+ expect(sample.timestamp).toBe(0);
55
+ });
56
+
57
+ test("seeks to frame at 0.02 seconds", async ({ expect, inputAtStart }) => {
58
+ const sample = await inputAtStart.seek(1, 20);
59
+ expect(sample.timestamp).toBe(0);
60
+ });
61
+
62
+ test("seeks to frame at 0.04 seconds", async ({ expect, inputAtStart }) => {
63
+ const sample = await inputAtStart.seek(1, 40);
64
+ expect(sample.timestamp).toBe(0.04);
65
+ });
66
+ });
67
+
68
+ describe("deterministic seeking behavior", () => {
69
+ test("seeks to exact sample timestamps", async ({
70
+ expect,
71
+ inputAtStart,
72
+ }) => {
73
+ expect((await inputAtStart.seek(1, 0)).timestamp).toBe(0);
74
+ expect((await inputAtStart.seek(1, 40)).timestamp).toBe(0.04);
75
+ expect((await inputAtStart.seek(1, 80)).timestamp).toBe(0.08);
76
+ expect((await inputAtStart.seek(1, 120)).timestamp).toBe(0.12);
77
+ expect((await inputAtStart.seek(1, 160)).timestamp).toBe(0.16);
78
+ });
79
+
80
+ test("seeks between samples returns previous sample", async ({
81
+ expect,
82
+ inputAtStart,
83
+ }) => {
84
+ expect((await inputAtStart.seek(1, 30)).timestamp).toBe(0);
85
+ expect((await inputAtStart.seek(1, 60)).timestamp).toBe(0.04);
86
+ expect((await inputAtStart.seek(1, 100)).timestamp).toBe(0.08);
87
+ expect((await inputAtStart.seek(1, 140)).timestamp).toBe(0.12);
88
+ });
89
+
90
+ test("seeks before first sample", async ({ expect, inputAtStart }) => {
91
+ inputAtStart.clearBuffer(1);
92
+ expect((await inputAtStart.seek(1, 0)).timestamp).toBe(0);
93
+ });
94
+
95
+ test("seeks to later samples in media", async ({
96
+ expect,
97
+ inputAtStart,
98
+ }) => {
99
+ const result200 = await inputAtStart.seek(1, 200);
100
+ const result1000 = await inputAtStart.seek(1, 1000);
101
+
102
+ expect(result200.timestamp! * 1000).toBeLessThanOrEqual(200);
103
+ expect(result1000.timestamp! * 1000).toBeLessThanOrEqual(1000);
104
+ expect(result200.timestamp).toBeGreaterThanOrEqual(0);
105
+ expect(result1000.timestamp).toBeGreaterThanOrEqual(result200.timestamp!);
106
+ });
107
+
108
+ test("never returns future sample", async ({ expect, inputAtStart }) => {
109
+ const testCases = [
110
+ { seekTimeMs: 0, expectedTimestamp: 0 },
111
+ { seekTimeMs: 10, expectedTimestamp: 0 },
112
+ { seekTimeMs: 30, expectedTimestamp: 0 },
113
+ { seekTimeMs: 40, expectedTimestamp: 0.04 },
114
+ { seekTimeMs: 50, expectedTimestamp: 0.04 },
115
+ { seekTimeMs: 70, expectedTimestamp: 0.04 },
116
+ { seekTimeMs: 80, expectedTimestamp: 0.08 },
117
+ { seekTimeMs: 90, expectedTimestamp: 0.08 },
118
+ ];
119
+
120
+ for (const { seekTimeMs, expectedTimestamp } of testCases) {
121
+ const result = await inputAtStart.seek(1, seekTimeMs);
122
+ expect(result.timestamp).toBe(expectedTimestamp);
123
+
124
+ const resultTimeMs = result.timestamp! * 1000;
125
+ expect(resultTimeMs).toBeLessThanOrEqual(seekTimeMs);
126
+ }
127
+ });
128
+ });
129
+
130
+ describe("buffer state management", () => {
131
+ test("starts with empty buffer", async ({ expect, inputAtStart }) => {
132
+ expect(inputAtStart.getBufferSize(1)).toBe(0);
133
+ expect(inputAtStart.getBufferTimestamps(1)).toEqual([]);
134
+ expect(inputAtStart.getBufferContents(1)).toEqual([]);
135
+ });
136
+
137
+ test("maintains separate buffers per track", async ({
138
+ expect,
139
+ inputAtStart,
140
+ }) => {
141
+ await inputAtStart.seek(1, 0);
142
+ const track1BufferSize = inputAtStart.getBufferSize(1);
143
+ expect(track1BufferSize).toBeGreaterThan(0);
144
+
145
+ expect(inputAtStart.getBufferSize(2)).toBe(0);
146
+
147
+ await inputAtStart.seek(2, 0);
148
+ expect(inputAtStart.getBufferSize(2)).toBeGreaterThan(0);
149
+ expect(inputAtStart.getBufferSize(1)).toBe(track1BufferSize);
150
+ });
151
+
152
+ test("buffer accumulates samples in order", async ({
153
+ expect,
154
+ inputAtStart,
155
+ }) => {
156
+ inputAtStart.clearBuffer(1);
157
+
158
+ await inputAtStart.seek(1, 0);
159
+ await inputAtStart.seek(1, 40);
160
+ await inputAtStart.seek(1, 80);
161
+
162
+ const timestamps = inputAtStart.getBufferTimestamps(1);
163
+ expect(timestamps).toContain(0);
164
+ expect(timestamps).toContain(0.04);
165
+ expect(timestamps).toContain(0.08);
166
+ });
167
+
168
+ test("buffer extends one sample ahead", async ({
169
+ expect,
170
+ fiveSampleBuffer,
171
+ }) => {
172
+ await fiveSampleBuffer.seek(1, 960);
173
+ expect(fiveSampleBuffer.getBufferTimestamps(1)).toEqual([
174
+ 0.8, 0.84, 0.88, 0.92, 0.96,
175
+ ]);
176
+ });
177
+
178
+ test("buffer resets when seeking back before the buffer", async ({
179
+ expect,
180
+ fiveSampleBuffer,
181
+ }) => {
182
+ await fiveSampleBuffer.seek(1, 960);
183
+ await fiveSampleBuffer.seek(1, 0);
184
+ expect(fiveSampleBuffer.getBufferTimestamps(1)).toEqual([0]);
185
+ });
186
+
187
+ test("buffer is maintained when seeking forwards within the buffer", async ({
188
+ expect,
189
+ fiveSampleBuffer,
190
+ }) => {
191
+ await fiveSampleBuffer.seek(1, 960);
192
+ expect(fiveSampleBuffer.getBufferTimestamps(1)).toEqual([
193
+ 0.8, 0.84, 0.88, 0.92, 0.96,
194
+ ]);
195
+ await fiveSampleBuffer.seek(1, 900);
196
+ expect(fiveSampleBuffer.getBufferTimestamps(1)).toEqual([
197
+ 0.8, 0.84, 0.88, 0.92, 0.96,
198
+ ]);
199
+ await fiveSampleBuffer.seek(1, 960);
200
+ expect(fiveSampleBuffer.getBufferTimestamps(1)).toEqual([
201
+ 0.8, 0.84, 0.88, 0.92, 0.96,
202
+ ]);
203
+ });
204
+
205
+ test("buffer is maintained when seeking backwards within the buffer", async ({
206
+ expect,
207
+ fiveSampleBuffer,
208
+ }) => {
209
+ await fiveSampleBuffer.seek(1, 960);
210
+ expect(fiveSampleBuffer.getBufferTimestamps(1)).toEqual([
211
+ 0.8, 0.84, 0.88, 0.92, 0.96,
212
+ ]);
213
+ await fiveSampleBuffer.seek(1, 900);
214
+ expect(fiveSampleBuffer.getBufferTimestamps(1)).toEqual([
215
+ 0.8, 0.84, 0.88, 0.92, 0.96,
216
+ ]);
217
+ });
218
+
219
+ test("buffer is maintained when seeking backwards to start of buffer", async ({
220
+ expect,
221
+ fiveSampleBuffer,
222
+ }) => {
223
+ await fiveSampleBuffer.seek(1, 960);
224
+ expect(fiveSampleBuffer.getBufferTimestamps(1)).toEqual([
225
+ 0.8, 0.84, 0.88, 0.92, 0.96,
226
+ ]);
227
+ await fiveSampleBuffer.seek(1, 800);
228
+ expect(fiveSampleBuffer.getBufferTimestamps(1)).toEqual([
229
+ 0.8, 0.84, 0.88, 0.92, 0.96,
230
+ ]);
231
+ });
232
+
233
+ test("buffer is reset when seeking backwards to arbitrary time before buffer", async ({
234
+ expect,
235
+ fiveSampleBuffer,
236
+ }) => {
237
+ await fiveSampleBuffer.seek(1, 960);
238
+ expect(fiveSampleBuffer.getBufferTimestamps(1)).toEqual([
239
+ 0.8, 0.84, 0.88, 0.92, 0.96,
240
+ ]);
241
+ await fiveSampleBuffer.seek(1, 720);
242
+ expect(fiveSampleBuffer.getBufferTimestamps(1)).toEqual([
243
+ 0.56, 0.6, 0.64, 0.68, 0.72,
244
+ ]);
245
+ });
246
+
247
+ test("buffer is maintained when seeking forwards to end of buffer", async ({
248
+ expect,
249
+ fiveSampleBuffer,
250
+ }) => {
251
+ await fiveSampleBuffer.seek(1, 960);
252
+ expect(fiveSampleBuffer.getBufferTimestamps(1)).toEqual([
253
+ 0.8, 0.84, 0.88, 0.92, 0.96,
254
+ ]);
255
+ await fiveSampleBuffer.seek(1, 900);
256
+ expect(fiveSampleBuffer.getBufferTimestamps(1)).toEqual([
257
+ 0.8, 0.84, 0.88, 0.92, 0.96,
258
+ ]);
259
+ await fiveSampleBuffer.seek(1, 960);
260
+ expect(fiveSampleBuffer.getBufferTimestamps(1)).toEqual([
261
+ 0.8, 0.84, 0.88, 0.92, 0.96,
262
+ ]);
263
+ });
264
+
265
+ test("buffer is maintained when seeking forwards past the buffer", async ({
266
+ expect,
267
+ fiveSampleBuffer,
268
+ }) => {
269
+ await fiveSampleBuffer.seek(1, 960);
270
+ expect(fiveSampleBuffer.getBufferTimestamps(1)).toEqual([
271
+ 0.8, 0.84, 0.88, 0.92, 0.96,
272
+ ]);
273
+ await fiveSampleBuffer.seek(1, 1000);
274
+ expect(fiveSampleBuffer.getBufferTimestamps(1)).toEqual([
275
+ 0.84, 0.88, 0.92, 0.96, 1,
276
+ ]);
277
+ });
278
+ });
279
+
280
+ describe("seeing to time not in buffer (time is before buffer)", () => {
281
+ test("throws error", async ({ expect, segment2 }) => {
282
+ await expect(segment2.seek(1, 0)).rejects.toThrow(NoSample);
283
+ });
284
+ });
285
+
286
+ describe("seeking forward at 1ms intervals", () => {
287
+ test("returns all samples in the media", async ({
288
+ expect,
289
+ inputAtStart,
290
+ }) => {
291
+ const timestamps = new Set<number>();
292
+ for (let i = 0; i < 1999; i++) {
293
+ const sample = await inputAtStart.seek(1, i);
294
+ timestamps.add(sample.timestamp!);
295
+ }
296
+ expect(Array.from(timestamps)).toEqual([
297
+ 0, 0.04, 0.08, 0.12, 0.16, 0.2, 0.24, 0.28, 0.32, 0.36, 0.4, 0.44, 0.48,
298
+ 0.52, 0.56, 0.6, 0.64, 0.68, 0.72, 0.76, 0.8, 0.84, 0.88, 0.92, 0.96, 1,
299
+ 1.04, 1.08, 1.12, 1.16, 1.2, 1.24, 1.28, 1.32, 1.36, 1.4, 1.44, 1.48,
300
+ 1.52, 1.56, 1.6, 1.64, 1.68, 1.72, 1.76, 1.8, 1.84, 1.88, 1.92, 1.96,
301
+ ]);
302
+ });
303
+ });
304
+
305
+ describe("error handling", () => {
306
+ test("throws error for non-existent track", async ({
307
+ expect,
308
+ inputAtStart,
309
+ }) => {
310
+ await expect(inputAtStart.seek(999, 0)).rejects.toThrow(
311
+ "Track 999 not found",
312
+ );
313
+ });
314
+ });
315
+
316
+ describe("concurrency handling", () => {
317
+ test("handles concurrent seeking operations safely", async ({
318
+ expect,
319
+ inputAtStart,
320
+ }) => {
321
+ const seek1 = inputAtStart.seek(1, 0);
322
+ const seek2 = inputAtStart.seek(1, 40);
323
+ const seek3 = inputAtStart.seek(1, 80);
324
+ const seek4 = inputAtStart.seek(1, 120);
325
+
326
+ const [sample1, sample2, sample3, sample4] = await Promise.all([
327
+ seek1,
328
+ seek2,
329
+ seek3,
330
+ seek4,
331
+ ]);
332
+
333
+ expect(sample1.timestamp).toBe(0);
334
+ expect(sample2.timestamp).toBe(0.04);
335
+ expect(sample3.timestamp).toBe(0.08);
336
+ expect(sample4.timestamp).toBe(0.12);
337
+
338
+ const bufferTimestamps = inputAtStart.getBufferTimestamps(1);
339
+ expect(bufferTimestamps.length).toBeGreaterThan(0);
340
+ for (let i = 1; i < bufferTimestamps.length; i++) {
341
+ expect(bufferTimestamps[i]).toBeGreaterThanOrEqual(
342
+ bufferTimestamps[i - 1]!,
343
+ );
344
+ }
345
+ });
346
+
347
+ test("handles concurrent seeks with backward jumps", async ({
348
+ expect,
349
+ inputAtStart,
350
+ }) => {
351
+ await inputAtStart.seek(1, 200);
352
+
353
+ const seek1 = inputAtStart.seek(1, 40);
354
+ const seek2 = inputAtStart.seek(1, 160);
355
+ const seek3 = inputAtStart.seek(1, 0);
356
+
357
+ const [sample1, sample2, sample3] = await Promise.all([
358
+ seek1,
359
+ seek2,
360
+ seek3,
361
+ ]);
362
+
363
+ expect(sample1.timestamp).toBe(0.04);
364
+ expect(sample2.timestamp).toBe(0.16);
365
+ expect(sample3.timestamp).toBe(0);
366
+ expect(inputAtStart.getBufferSize(1)).toBeGreaterThan(0);
367
+ });
368
+
369
+ test("handles concurrent seeks to same time", async ({
370
+ expect,
371
+ inputAtStart,
372
+ }) => {
373
+ const seeks = Array(5)
374
+ .fill(null)
375
+ .map(() => inputAtStart.seek(1, 80));
376
+ const results = await Promise.all(seeks);
377
+
378
+ for (const result of results) {
379
+ expect(result.timestamp).toBe(0.08);
380
+ }
381
+ expect(inputAtStart.getBufferSize(1)).toBeGreaterThan(0);
382
+ });
383
+
384
+ test("handles mixed concurrent operations across different tracks", async ({
385
+ expect,
386
+ inputAtStart,
387
+ }) => {
388
+ const track1Seek1 = inputAtStart.seek(1, 40);
389
+ const track1Seek2 = inputAtStart.seek(1, 80);
390
+
391
+ const [result1, result2] = await Promise.all([track1Seek1, track1Seek2]);
392
+
393
+ expect(result1.timestamp).toBe(0.04);
394
+ expect(result2.timestamp).toBe(0.08);
395
+
396
+ const track1Buffer = inputAtStart.getBufferTimestamps(1);
397
+ expect(track1Buffer.length).toBeGreaterThan(0);
398
+ });
399
+ });
400
+ });