@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,267 @@
1
+ import {
2
+ AudioSampleSink,
3
+ BufferSource,
4
+ Input,
5
+ MP4,
6
+ VideoSampleSink,
7
+ } from "mediabunny";
8
+ import { type MediaSample, SampleBuffer } from "../SampleBuffer";
9
+
10
+ interface BufferedSeekingInputOptions {
11
+ videoBufferSize?: number;
12
+ audioBufferSize?: number;
13
+ /**
14
+ * FFmpeg start_time offset in milliseconds from the processed video.
15
+ * Applied during seeking to correct for timing shifts introduced by FFmpeg processing.
16
+ */
17
+ startTimeOffsetMs?: number;
18
+ }
19
+
20
+ const defaultOptions: BufferedSeekingInputOptions = {
21
+ videoBufferSize: 30,
22
+ audioBufferSize: 100,
23
+ startTimeOffsetMs: 0,
24
+ };
25
+
26
+ export class NoSample extends RangeError {}
27
+
28
+ export class BufferedSeekingInput {
29
+ private input: Input;
30
+ private trackIterators: Map<number, AsyncIterator<MediaSample>> = new Map();
31
+ private trackBuffers: Map<number, SampleBuffer> = new Map();
32
+ private options: BufferedSeekingInputOptions;
33
+ // Separate locks for different operation types to prevent unnecessary blocking
34
+ private trackIteratorCreationPromises: Map<number, Promise<any>> = new Map();
35
+ private trackSeekPromises: Map<number, Promise<any>> = new Map();
36
+
37
+ /**
38
+ * FFmpeg start_time offset in milliseconds from the processed video.
39
+ * Applied during seeking to correct for timing shifts introduced by FFmpeg processing.
40
+ */
41
+ private readonly startTimeOffsetMs: number;
42
+
43
+ constructor(arrayBuffer: ArrayBuffer, options?: BufferedSeekingInputOptions) {
44
+ const bufferSource = new BufferSource(arrayBuffer);
45
+ const input = new Input({
46
+ source: bufferSource,
47
+ formats: [MP4],
48
+ });
49
+ this.input = input;
50
+ this.options = { ...defaultOptions, ...options };
51
+ this.startTimeOffsetMs = this.options.startTimeOffsetMs ?? 0;
52
+ }
53
+
54
+ // Buffer inspection API for testing
55
+ getBufferSize(trackId: number): number {
56
+ const buffer = this.trackBuffers.get(trackId);
57
+ return buffer ? buffer.length : 0;
58
+ }
59
+
60
+ getBufferContents(trackId: number): readonly MediaSample[] {
61
+ const buffer = this.trackBuffers.get(trackId);
62
+ return buffer ? Object.freeze([...buffer.getContents()]) : [];
63
+ }
64
+
65
+ getBufferTimestamps(trackId: number): number[] {
66
+ const contents = this.getBufferContents(trackId);
67
+ return contents.map((sample) => sample.timestamp || 0);
68
+ }
69
+
70
+ clearBuffer(trackId: number): void {
71
+ const buffer = this.trackBuffers.get(trackId);
72
+ if (buffer) {
73
+ buffer.clear();
74
+ }
75
+ }
76
+
77
+ computeDuration() {
78
+ return this.input.computeDuration();
79
+ }
80
+
81
+ async getTrack(trackId: number) {
82
+ const tracks = await this.input.getTracks();
83
+ const track = tracks.find((track) => track.id === trackId);
84
+ if (!track) {
85
+ throw new Error(`Track ${trackId} not found`);
86
+ }
87
+ return track;
88
+ }
89
+
90
+ async getAudioTrack(trackId: number) {
91
+ const tracks = await this.input.getAudioTracks();
92
+ const track = tracks.find(
93
+ (track) => track.id === trackId && track.type === "audio",
94
+ );
95
+ if (!track) {
96
+ throw new Error(`Track ${trackId} not found`);
97
+ }
98
+ return track;
99
+ }
100
+
101
+ async getVideoTrack(trackId: number) {
102
+ const tracks = await this.input.getVideoTracks();
103
+ const track = tracks.find(
104
+ (track) => track.id === trackId && track.type === "video",
105
+ );
106
+ if (!track) {
107
+ throw new Error(`Track ${trackId} not found`);
108
+ }
109
+ return track;
110
+ }
111
+
112
+ async getFirstVideoTrack() {
113
+ const tracks = await this.input.getVideoTracks();
114
+ return tracks[0];
115
+ }
116
+
117
+ async getFirstAudioTrack() {
118
+ const tracks = await this.input.getAudioTracks();
119
+ return tracks[0];
120
+ }
121
+
122
+ async getTrackIterator(trackId: number) {
123
+ if (this.trackIterators.has(trackId)) {
124
+ // biome-ignore lint/style/noNonNullAssertion: we know the map has the key
125
+ return this.trackIterators.get(trackId)!;
126
+ }
127
+
128
+ // Serialize iterator creation per track (but don't block seeks)
129
+ const existingIteratorCreation =
130
+ this.trackIteratorCreationPromises.get(trackId);
131
+ if (existingIteratorCreation) {
132
+ await existingIteratorCreation;
133
+ // Check again after waiting - another operation might have created it
134
+ if (this.trackIterators.has(trackId)) {
135
+ // biome-ignore lint/style/noNonNullAssertion: we know the map has the key
136
+ return this.trackIterators.get(trackId)!;
137
+ }
138
+ }
139
+
140
+ const creationPromise = this.createIteratorSafe(trackId);
141
+ this.trackIteratorCreationPromises.set(trackId, creationPromise);
142
+
143
+ try {
144
+ const iterator = await creationPromise;
145
+ return iterator;
146
+ } finally {
147
+ this.trackIteratorCreationPromises.delete(trackId);
148
+ }
149
+ }
150
+
151
+ private async createIteratorSafe(trackId: number) {
152
+ const track = await this.getTrack(trackId);
153
+ if (track.type === "audio") {
154
+ const track = await this.getAudioTrack(trackId);
155
+ const sampleSink = new AudioSampleSink(track);
156
+ const iterator = sampleSink.samples();
157
+ this.trackIterators.set(trackId, iterator);
158
+ return iterator;
159
+ }
160
+ {
161
+ const track = await this.getVideoTrack(trackId);
162
+ const sampleSink = new VideoSampleSink(track);
163
+ const iterator = sampleSink.samples();
164
+ this.trackIterators.set(trackId, iterator);
165
+ return iterator;
166
+ }
167
+ }
168
+
169
+ async createTrackBuffer(trackId: number) {
170
+ const track = await this.getTrack(trackId);
171
+ if (track.type === "audio") {
172
+ const bufferSize = this.options.audioBufferSize;
173
+ this.trackBuffers.set(trackId, new SampleBuffer(bufferSize));
174
+ } else {
175
+ const bufferSize = this.options.videoBufferSize;
176
+ this.trackBuffers.set(trackId, new SampleBuffer(bufferSize));
177
+ }
178
+ }
179
+
180
+ async seek(trackId: number, timeMs: number) {
181
+ // Apply the start time offset to deal with files that don't start on the zero
182
+ const correctedTimeMs = timeMs + this.startTimeOffsetMs;
183
+
184
+ // Serialize seek operations per track (but don't block iterator creation)
185
+ const existingSeek = this.trackSeekPromises.get(trackId);
186
+ if (existingSeek) {
187
+ await existingSeek;
188
+ }
189
+
190
+ const seekPromise = this.seekSafe(trackId, correctedTimeMs);
191
+ this.trackSeekPromises.set(trackId, seekPromise);
192
+
193
+ try {
194
+ return await seekPromise;
195
+ } finally {
196
+ this.trackSeekPromises.delete(trackId);
197
+ }
198
+ }
199
+
200
+ private async resetIterator(trackId: number) {
201
+ const trackBuffer = this.trackBuffers.get(trackId);
202
+ trackBuffer?.clear();
203
+ // Clean up iterator safely - wait for any ongoing iterator creation
204
+ const ongoingIteratorCreation =
205
+ this.trackIteratorCreationPromises.get(trackId);
206
+ if (ongoingIteratorCreation) {
207
+ await ongoingIteratorCreation;
208
+ }
209
+
210
+ const iterator = this.trackIterators.get(trackId);
211
+ if (iterator) {
212
+ try {
213
+ await iterator.return?.();
214
+ } catch (_error) {
215
+ // Iterator cleanup failed, continue anyway
216
+ }
217
+ }
218
+ this.trackIterators.delete(trackId);
219
+ }
220
+
221
+ private async seekSafe(trackId: number, timeMs: number) {
222
+ // Get or create track-specific buffer
223
+ if (!this.trackBuffers.has(trackId)) {
224
+ await this.createTrackBuffer(trackId);
225
+ }
226
+ // biome-ignore lint/style/noNonNullAssertion: we know the map has the key
227
+ const trackBuffer = this.trackBuffers.get(trackId)!;
228
+
229
+ if (timeMs < trackBuffer.firstTimestamp * 1000) {
230
+ await this.resetIterator(trackId);
231
+ }
232
+
233
+ const alreadyInBuffer = trackBuffer.find(timeMs);
234
+ const track = await this.getTrack(trackId);
235
+
236
+ // Early validation: check if seek time is outside track bounds
237
+ const firstTimestampMs = (await track.getFirstTimestamp()) * 1000;
238
+ const lastSampleEndMs = (await track.computeDuration()) * 1000; // computeDuration returns end time of last sample
239
+
240
+ if (timeMs < firstTimestampMs || timeMs >= lastSampleEndMs) {
241
+ throw new NoSample(
242
+ `Seek time ${timeMs}ms is outside track range [${firstTimestampMs}ms, ${lastSampleEndMs}ms]`,
243
+ );
244
+ }
245
+
246
+ if (alreadyInBuffer) return alreadyInBuffer;
247
+
248
+ const iterator = await this.getTrackIterator(trackId);
249
+ while (true) {
250
+ const { done, value: decodedSample } = await iterator.next();
251
+ if (decodedSample) {
252
+ trackBuffer.push(decodedSample);
253
+ }
254
+ const foundSample = trackBuffer.find(timeMs);
255
+ if (foundSample) {
256
+ return foundSample;
257
+ }
258
+ if (done) {
259
+ break;
260
+ }
261
+ }
262
+
263
+ throw new NoSample(
264
+ `Sample not found for time ${timeMs} in ${track.type} track ${trackId}`,
265
+ );
266
+ }
267
+ }
@@ -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,110 @@
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
+ };
45
+ }
46
+
47
+ get videoRendition(): VideoRendition | undefined {
48
+ const rendition = this.data.videoRenditions[0];
49
+
50
+ if (!rendition) return undefined;
51
+ return {
52
+ id: rendition.id as RenditionId,
53
+ trackId: undefined,
54
+ src: this.data.sourceUrl,
55
+ segmentDurationMs: rendition.segmentDurationMs,
56
+ };
57
+ }
58
+
59
+ get templates() {
60
+ return this.data.endpoints;
61
+ }
62
+
63
+ async fetchInitSegment(
64
+ rendition: { id?: RenditionId; trackId: number | undefined; src: string },
65
+ signal: AbortSignal,
66
+ ) {
67
+ if (!rendition.id) {
68
+ throw new Error("Rendition ID is required for JIT metadata");
69
+ }
70
+ const url = this.urlGenerator.generateSegmentUrl(
71
+ "init",
72
+ rendition.id,
73
+ this,
74
+ );
75
+ const response = await this.host.fetch(url, { signal });
76
+ const arrayBuffer = await response.arrayBuffer();
77
+ return arrayBuffer;
78
+ }
79
+
80
+ async fetchMediaSegmentImpl(
81
+ segmentId: number,
82
+ rendition: { id?: RenditionId; trackId: number | undefined; src: string },
83
+ ) {
84
+ if (!rendition.id) {
85
+ throw new Error("Rendition ID is required for JIT metadata");
86
+ }
87
+ const url = this.urlGenerator.generateSegmentUrl(
88
+ segmentId,
89
+ rendition.id,
90
+ this,
91
+ );
92
+ return this.fetchMediaCache(url);
93
+ }
94
+
95
+ computeSegmentId(
96
+ desiredSeekTimeMs: number,
97
+ rendition: VideoRendition | AudioRendition,
98
+ ) {
99
+ if (!rendition.segmentDurationMs) {
100
+ throw new Error("Segment duration is required for JIT metadata");
101
+ }
102
+ const segmentIndex = Math.floor(
103
+ desiredSeekTimeMs / rendition.segmentDurationMs,
104
+ );
105
+ if (segmentIndex * rendition.segmentDurationMs >= this.durationMs) {
106
+ return segmentIndex; // Return 1-based index (segmentIndex is already 0-based, so segmentIndex for the last segment is what we want)
107
+ }
108
+ return segmentIndex + 1; // Convert 0-based to 1-based
109
+ }
110
+ }