@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,333 @@
1
+ export interface MediaSourceManagerOptions {
2
+ onError?: (error: Error) => void;
3
+ onReady?: () => void;
4
+ onUpdateEnd?: () => void;
5
+ timeout?: number;
6
+ }
7
+
8
+ /**
9
+ * Manages MediaSource for audio streaming
10
+ */
11
+ export class MediaSourceManager {
12
+ private mediaSource: MediaSource | null = null;
13
+ private audioElement: HTMLAudioElement | null = null;
14
+ private sourceBuffer: SourceBuffer | null = null;
15
+ private mediaSourceReady = false;
16
+ private pendingSegments: ArrayBuffer[] = [];
17
+ private options: MediaSourceManagerOptions;
18
+
19
+ constructor(options: MediaSourceManagerOptions = {}) {
20
+ this.options = {
21
+ timeout: 10000,
22
+ ...options,
23
+ };
24
+ }
25
+
26
+ /**
27
+ * Initialize MediaSource for audio streaming
28
+ */
29
+ async initialize(): Promise<void> {
30
+ this.cleanup(true);
31
+
32
+ this.mediaSource = new MediaSource();
33
+ this.audioElement = document.createElement("audio");
34
+
35
+ // Add error event listeners to the audio element
36
+ this.audioElement.addEventListener("error", (event) => {
37
+ const error = this.audioElement?.error;
38
+ console.error("🎵 [AUDIO_ELEMENT_ERROR] Audio element error:", {
39
+ code: error?.code,
40
+ message: error?.message,
41
+ event,
42
+ });
43
+
44
+ if (this.options.onError) {
45
+ this.options.onError(
46
+ new Error(`Audio element error: ${error?.message}`),
47
+ );
48
+ }
49
+ });
50
+
51
+ this.audioElement.src = URL.createObjectURL(this.mediaSource);
52
+
53
+ return new Promise((resolve, reject) => {
54
+ this.mediaSource?.addEventListener("sourceopen", () => {
55
+ try {
56
+ const sourceBuffer = this.createSourceBuffer();
57
+ if (!sourceBuffer) {
58
+ throw new Error(
59
+ "Failed to create SourceBuffer with any supported codec",
60
+ );
61
+ }
62
+
63
+ this.sourceBuffer = sourceBuffer;
64
+ this.setupSourceBufferListeners();
65
+
66
+ this.mediaSourceReady = true;
67
+
68
+ if (this.options.onReady) {
69
+ this.options.onReady();
70
+ }
71
+
72
+ resolve();
73
+ } catch (error) {
74
+ console.error(
75
+ "🎵 [MEDIA_SOURCE_ERROR] Failed to create SourceBuffer:",
76
+ error,
77
+ );
78
+ reject(error);
79
+ }
80
+ });
81
+
82
+ this.mediaSource?.addEventListener("error", (error) => {
83
+ console.error("🎵 [MEDIA_SOURCE_ERROR] MediaSource error:", error);
84
+ reject(error);
85
+ });
86
+
87
+ // Add timeout for MediaSource opening
88
+ setTimeout(() => {
89
+ if (!this.mediaSourceReady) {
90
+ const timeoutError = new Error(
91
+ "MediaSource failed to open within timeout",
92
+ );
93
+ console.error(
94
+ "🎵 [MEDIA_SOURCE_TIMEOUT] MediaSource initialization timeout",
95
+ );
96
+ reject(timeoutError);
97
+ }
98
+ }, 4000);
99
+ });
100
+ }
101
+
102
+ /**
103
+ * Create SourceBuffer with codec fallback
104
+ */
105
+ private createSourceBuffer(): SourceBuffer | undefined {
106
+ const codecOptions = [
107
+ 'audio/mp4; codecs="mp4a.40.2"', // AAC-LC (most common)
108
+ 'audio/mp4; codecs="mp4a.40.5"', // AAC-HE
109
+ "audio/mp4", // Generic MP4 audio
110
+ ];
111
+
112
+ let sourceBuffer: SourceBuffer | undefined;
113
+ let lastError: Error | undefined;
114
+
115
+ for (const codec of codecOptions) {
116
+ try {
117
+ if (MediaSource.isTypeSupported(codec)) {
118
+ sourceBuffer = this.mediaSource?.addSourceBuffer(codec);
119
+ break;
120
+ }
121
+ } catch (error) {
122
+ console.error(
123
+ `🎵 [CODEC_ERROR] Failed to create SourceBuffer with ${codec}:`,
124
+ error,
125
+ );
126
+ lastError = error as Error;
127
+ }
128
+ }
129
+
130
+ if (!sourceBuffer && lastError) {
131
+ throw new Error(
132
+ `Failed to create SourceBuffer with any supported codec. Last error: ${lastError.message}`,
133
+ );
134
+ }
135
+
136
+ return sourceBuffer;
137
+ }
138
+
139
+ /**
140
+ * Setup SourceBuffer event listeners
141
+ */
142
+ private setupSourceBufferListeners(): void {
143
+ if (!this.sourceBuffer) return;
144
+
145
+ this.sourceBuffer.addEventListener("updateend", () => {
146
+ this.processPendingSegments();
147
+
148
+ if (this.options.onUpdateEnd) {
149
+ this.options.onUpdateEnd();
150
+ }
151
+ });
152
+
153
+ this.sourceBuffer.addEventListener("error", (event) => {
154
+ console.error(
155
+ "🎵 [SOURCE_BUFFER_EVENT_ERROR] SourceBuffer error event:",
156
+ event,
157
+ );
158
+
159
+ if (this.options.onError) {
160
+ this.options.onError(new Error("SourceBuffer error"));
161
+ }
162
+ });
163
+ }
164
+
165
+ /**
166
+ * Feed audio segments directly to MediaSource SourceBuffer
167
+ */
168
+ async feedSegment(segmentBuffer: ArrayBuffer): Promise<void> {
169
+ if (!this.mediaSourceReady || !this.sourceBuffer) {
170
+ this.pendingSegments.push(segmentBuffer);
171
+ return;
172
+ }
173
+
174
+ if (this.sourceBuffer.updating) {
175
+ this.pendingSegments.push(segmentBuffer);
176
+ return;
177
+ }
178
+
179
+ // Check for HTMLMediaElement errors before appending
180
+ if (this.audioElement?.error) {
181
+ const error = this.audioElement.error;
182
+ console.error(
183
+ "🎵 [MEDIA_ELEMENT_ERROR] HTMLMediaElement error detected:",
184
+ {
185
+ code: error.code,
186
+ message: error.message,
187
+ MEDIA_ERR_ABORTED: error.code === MediaError.MEDIA_ERR_ABORTED,
188
+ MEDIA_ERR_NETWORK: error.code === MediaError.MEDIA_ERR_NETWORK,
189
+ MEDIA_ERR_DECODE: error.code === MediaError.MEDIA_ERR_DECODE,
190
+ MEDIA_ERR_SRC_NOT_SUPPORTED:
191
+ error.code === MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED,
192
+ },
193
+ );
194
+
195
+ // Reset the audio element to try to recover
196
+ this.audioElement.load();
197
+ return;
198
+ }
199
+
200
+ try {
201
+ this.sourceBuffer.appendBuffer(segmentBuffer);
202
+ } catch (error) {
203
+ console.error(
204
+ "🎵 [SOURCE_BUFFER_ERROR] Failed to append segment:",
205
+ error,
206
+ );
207
+ this.logDebugInfo();
208
+ this.pendingSegments.push(segmentBuffer);
209
+ }
210
+ }
211
+
212
+ /**
213
+ * Process any queued segments when SourceBuffer becomes available
214
+ */
215
+ private processPendingSegments(): void {
216
+ if (
217
+ !this.sourceBuffer ||
218
+ this.sourceBuffer.updating ||
219
+ this.pendingSegments.length === 0
220
+ ) {
221
+ return;
222
+ }
223
+
224
+ const nextSegment = this.pendingSegments.shift();
225
+ if (nextSegment) {
226
+ this.feedSegment(nextSegment);
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Log debug information for troubleshooting
232
+ */
233
+ private logDebugInfo(): void {
234
+ console.error("🎵 [SOURCE_BUFFER_DEBUG] SourceBuffer state:", {
235
+ updating: this.sourceBuffer?.updating,
236
+ buffered: this.sourceBuffer?.buffered
237
+ ? Array.from(
238
+ { length: this.sourceBuffer.buffered.length },
239
+ (_, i) =>
240
+ `${this.sourceBuffer?.buffered.start(i)}-${this.sourceBuffer?.buffered.end(i)}`,
241
+ )
242
+ : [],
243
+ mode: this.sourceBuffer?.mode,
244
+ timestampOffset: this.sourceBuffer?.timestampOffset,
245
+ });
246
+
247
+ console.error("🎵 [MEDIA_SOURCE_DEBUG] MediaSource state:", {
248
+ readyState: this.mediaSource?.readyState,
249
+ sourceBuffers: this.mediaSource?.sourceBuffers.length,
250
+ duration: this.mediaSource?.duration,
251
+ });
252
+
253
+ console.error("🎵 [AUDIO_ELEMENT_DEBUG] Audio element state:", {
254
+ readyState: this.audioElement?.readyState,
255
+ networkState: this.audioElement?.networkState,
256
+ error: this.audioElement?.error?.code,
257
+ src: `${this.audioElement?.src.substring(0, 50)}...`,
258
+ });
259
+ }
260
+
261
+ /**
262
+ * Set audio element current time
263
+ */
264
+ setCurrentTime(timeMs: number): void {
265
+ if (this.audioElement) {
266
+ this.audioElement.currentTime = timeMs / 1000;
267
+ }
268
+ }
269
+
270
+ /**
271
+ * Get the audio element for MediaElementSource
272
+ */
273
+ getAudioElement(): HTMLAudioElement | null {
274
+ return this.audioElement;
275
+ }
276
+
277
+ /**
278
+ * Check if MediaSource is ready
279
+ */
280
+ isReady(): boolean {
281
+ return this.mediaSourceReady;
282
+ }
283
+
284
+ /**
285
+ * Get buffered time ranges
286
+ */
287
+ getBuffered(): TimeRanges | null {
288
+ return this.sourceBuffer?.buffered || null;
289
+ }
290
+
291
+ /**
292
+ * Clean up MediaSource resources
293
+ */
294
+ cleanup(_preserveCache = false): void {
295
+ // Clean up existing MediaSource
296
+ if (
297
+ this.sourceBuffer &&
298
+ this.mediaSource &&
299
+ this.mediaSource.readyState === "open"
300
+ ) {
301
+ try {
302
+ this.mediaSource.removeSourceBuffer(this.sourceBuffer);
303
+ } catch (error) {
304
+ console.warn("🎵 [CLEANUP_ERROR] Error removing SourceBuffer:", error);
305
+ }
306
+ }
307
+
308
+ if (this.mediaSource) {
309
+ try {
310
+ if (this.mediaSource.readyState === "open") {
311
+ this.mediaSource.endOfStream();
312
+ }
313
+ } catch (error) {
314
+ console.warn("🎵 [CLEANUP_ERROR] Error ending MediaSource:", error);
315
+ }
316
+ }
317
+
318
+ if (this.audioElement) {
319
+ try {
320
+ URL.revokeObjectURL(this.audioElement.src);
321
+ } catch (error) {
322
+ console.warn("🎵 [CLEANUP_ERROR] Error revoking URL:", error);
323
+ }
324
+ }
325
+
326
+ // Reset state
327
+ this.mediaSource = null;
328
+ this.audioElement = null;
329
+ this.sourceBuffer = null;
330
+ this.mediaSourceReady = false;
331
+ this.pendingSegments = [];
332
+ }
333
+ }
@@ -0,0 +1,208 @@
1
+ /**
2
+ * Cache manager for handling multiple cache types with LRU eviction and statistics
3
+ */
4
+
5
+ import type {
6
+ CacheStats,
7
+ ManifestResponse,
8
+ VideoMetadata,
9
+ } from "../types/index.js";
10
+
11
+ export class CacheManager {
12
+ private segmentCache = new Map<string, ArrayBuffer>();
13
+ private metadataCache = new Map<string, VideoMetadata>();
14
+ private manifestCache = new Map<string, ManifestResponse>();
15
+ private initSegmentCache = new Map<string, ArrayBuffer>();
16
+ private cacheAccessOrder: string[] = [];
17
+
18
+ // Cache performance tracking
19
+ private cacheHits = 0;
20
+ private cacheMisses = 0;
21
+ private totalRequests = 0;
22
+
23
+ constructor(private maxSize: number) {}
24
+
25
+ /**
26
+ * Cache a segment with LRU eviction
27
+ */
28
+ cacheSegment(cacheKey: string, buffer: ArrayBuffer): void {
29
+ // Implement LRU eviction
30
+ if (this.segmentCache.size >= this.maxSize) {
31
+ const firstKey = this.segmentCache.keys().next().value;
32
+ if (firstKey) {
33
+ this.segmentCache.delete(firstKey);
34
+ // Remove from access order tracking
35
+ const index = this.cacheAccessOrder.indexOf(firstKey);
36
+ if (index > -1) {
37
+ this.cacheAccessOrder.splice(index, 1);
38
+ }
39
+ }
40
+ }
41
+
42
+ this.segmentCache.set(cacheKey, buffer);
43
+
44
+ // Track access order for LRU analytics
45
+ const existingIndex = this.cacheAccessOrder.indexOf(cacheKey);
46
+ if (existingIndex > -1) {
47
+ this.cacheAccessOrder.splice(existingIndex, 1);
48
+ }
49
+ this.cacheAccessOrder.push(cacheKey);
50
+ }
51
+
52
+ /**
53
+ * Get a segment from cache
54
+ */
55
+ getSegment(cacheKey: string): ArrayBuffer | undefined {
56
+ const cached = this.segmentCache.get(cacheKey);
57
+ if (cached) {
58
+ this.cacheHits++;
59
+ // Update access order for LRU
60
+ const index = this.cacheAccessOrder.indexOf(cacheKey);
61
+ if (index > -1) {
62
+ this.cacheAccessOrder.splice(index, 1);
63
+ this.cacheAccessOrder.push(cacheKey);
64
+ }
65
+ return cached;
66
+ }
67
+ this.cacheMisses++;
68
+ return undefined;
69
+ }
70
+
71
+ /**
72
+ * Check if a segment exists in cache
73
+ */
74
+ hasSegment(cacheKey: string): boolean {
75
+ return this.segmentCache.has(cacheKey);
76
+ }
77
+
78
+ /**
79
+ * Cache metadata
80
+ */
81
+ cacheMetadata(url: string, metadata: VideoMetadata): void {
82
+ this.metadataCache.set(url, metadata);
83
+ }
84
+
85
+ /**
86
+ * Get metadata from cache
87
+ */
88
+ getMetadata(url: string): VideoMetadata | undefined {
89
+ const cached = this.metadataCache.get(url);
90
+ if (cached) {
91
+ this.cacheHits++;
92
+ return cached;
93
+ }
94
+ this.cacheMisses++;
95
+ return undefined;
96
+ }
97
+
98
+ /**
99
+ * Cache manifest
100
+ */
101
+ cacheManifest(url: string, manifest: ManifestResponse): void {
102
+ this.manifestCache.set(url, manifest);
103
+ }
104
+
105
+ /**
106
+ * Get manifest from cache
107
+ */
108
+ getManifest(url: string): ManifestResponse | undefined {
109
+ const cached = this.manifestCache.get(url);
110
+ if (cached) {
111
+ this.cacheHits++;
112
+ return cached;
113
+ }
114
+ this.cacheMisses++;
115
+ return undefined;
116
+ }
117
+
118
+ /**
119
+ * Cache init segment
120
+ */
121
+ cacheInitSegment(cacheKey: string, buffer: ArrayBuffer): void {
122
+ this.initSegmentCache.set(cacheKey, buffer);
123
+ }
124
+
125
+ /**
126
+ * Get init segment from cache
127
+ */
128
+ getInitSegment(cacheKey: string): ArrayBuffer | undefined {
129
+ const cached = this.initSegmentCache.get(cacheKey);
130
+ if (cached) {
131
+ this.cacheHits++;
132
+ return cached;
133
+ }
134
+ this.cacheMisses++;
135
+ return undefined;
136
+ }
137
+
138
+ /**
139
+ * Get comprehensive cache statistics
140
+ */
141
+ getCacheStats(): CacheStats {
142
+ this.totalRequests = this.cacheHits + this.cacheMisses;
143
+ const hitRate =
144
+ this.totalRequests > 0 ? this.cacheHits / this.totalRequests : 0;
145
+ const efficiency =
146
+ this.segmentCache.size > 0 ? this.cacheHits / this.segmentCache.size : 0;
147
+
148
+ return {
149
+ size: this.segmentCache.size,
150
+ maxSize: this.maxSize,
151
+ hitRate: hitRate,
152
+ efficiency: efficiency,
153
+ totalRequests: this.totalRequests,
154
+ recentKeys: this.cacheAccessOrder.slice(-5), // Last 5 accessed keys
155
+ };
156
+ }
157
+
158
+ /**
159
+ * Clear all caches
160
+ */
161
+ clearAll(): void {
162
+ this.segmentCache.clear();
163
+ this.metadataCache.clear();
164
+ this.manifestCache.clear();
165
+ this.initSegmentCache.clear();
166
+ this.cacheAccessOrder = [];
167
+ this.cacheHits = 0;
168
+ this.cacheMisses = 0;
169
+ this.totalRequests = 0;
170
+ }
171
+
172
+ /**
173
+ * Get cache sizes for each cache type
174
+ */
175
+ getCacheSizes(): {
176
+ segments: number;
177
+ metadata: number;
178
+ manifests: number;
179
+ initSegments: number;
180
+ } {
181
+ return {
182
+ segments: this.segmentCache.size,
183
+ metadata: this.metadataCache.size,
184
+ manifests: this.manifestCache.size,
185
+ initSegments: this.initSegmentCache.size,
186
+ };
187
+ }
188
+
189
+ /**
190
+ * Clear specific cache type
191
+ */
192
+ clearSegmentCache(): void {
193
+ this.segmentCache.clear();
194
+ this.cacheAccessOrder = [];
195
+ }
196
+
197
+ clearMetadataCache(): void {
198
+ this.metadataCache.clear();
199
+ }
200
+
201
+ clearManifestCache(): void {
202
+ this.manifestCache.clear();
203
+ }
204
+
205
+ clearInitSegmentCache(): void {
206
+ this.initSegmentCache.clear();
207
+ }
208
+ }
@@ -0,0 +1,170 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { RequestDeduplicator } from "./RequestDeduplicator.js";
3
+
4
+ describe("RequestDeduplicator", () => {
5
+ let deduplicator: RequestDeduplicator;
6
+
7
+ beforeEach(() => {
8
+ deduplicator = new RequestDeduplicator();
9
+ });
10
+
11
+ describe("executeRequest", () => {
12
+ it("should execute request and return result for new key", async () => {
13
+ const mockFactory = vi.fn().mockResolvedValue("result");
14
+
15
+ const result = await deduplicator.executeRequest("key1", mockFactory);
16
+
17
+ expect(result).toBe("result");
18
+ expect(mockFactory).toHaveBeenCalledTimes(1);
19
+ });
20
+
21
+ it("should return same promise for concurrent requests with same key", async () => {
22
+ const mockFactory = vi.fn().mockResolvedValue("result");
23
+
24
+ const [result1, result2] = await Promise.all([
25
+ deduplicator.executeRequest("key1", mockFactory),
26
+ deduplicator.executeRequest("key1", mockFactory),
27
+ ]);
28
+
29
+ expect(result1).toBe("result");
30
+ expect(result2).toBe("result");
31
+ expect(mockFactory).toHaveBeenCalledTimes(1); // Should only be called once
32
+ });
33
+
34
+ it("should allow separate requests for different keys", async () => {
35
+ const mockFactory1 = vi.fn().mockResolvedValue("result1");
36
+ const mockFactory2 = vi.fn().mockResolvedValue("result2");
37
+
38
+ const [result1, result2] = await Promise.all([
39
+ deduplicator.executeRequest("key1", mockFactory1),
40
+ deduplicator.executeRequest("key2", mockFactory2),
41
+ ]);
42
+
43
+ expect(result1).toBe("result1");
44
+ expect(result2).toBe("result2");
45
+ expect(mockFactory1).toHaveBeenCalledTimes(1);
46
+ expect(mockFactory2).toHaveBeenCalledTimes(1);
47
+ });
48
+
49
+ it("should handle request failures and clean up", async () => {
50
+ const error = new Error("Request failed");
51
+ const mockFactory = vi.fn().mockRejectedValue(error);
52
+
53
+ await expect(
54
+ deduplicator.executeRequest("key1", mockFactory),
55
+ ).rejects.toThrow("Request failed");
56
+
57
+ // Should allow new request with same key after failure
58
+ const mockFactory2 = vi.fn().mockResolvedValue("success");
59
+ const result = await deduplicator.executeRequest("key1", mockFactory2);
60
+
61
+ expect(result).toBe("success");
62
+ expect(mockFactory).toHaveBeenCalledTimes(1);
63
+ expect(mockFactory2).toHaveBeenCalledTimes(1);
64
+ });
65
+
66
+ it("should clean up pending requests after success", async () => {
67
+ const mockFactory = vi.fn().mockResolvedValue("result");
68
+
69
+ await deduplicator.executeRequest("key1", mockFactory);
70
+
71
+ expect(deduplicator.isPending("key1")).toBe(false);
72
+ expect(deduplicator.getPendingCount()).toBe(0);
73
+ });
74
+ });
75
+
76
+ describe("isPending", () => {
77
+ it("should return true for pending requests", async () => {
78
+ const mockFactory = vi.fn().mockImplementation(
79
+ () =>
80
+ new Promise((resolve) => {
81
+ setTimeout(() => resolve("result"), 100);
82
+ }),
83
+ );
84
+
85
+ const promise = deduplicator.executeRequest("key1", mockFactory);
86
+
87
+ expect(deduplicator.isPending("key1")).toBe(true);
88
+
89
+ await promise;
90
+
91
+ expect(deduplicator.isPending("key1")).toBe(false);
92
+ });
93
+
94
+ it("should return false for non-existent keys", () => {
95
+ expect(deduplicator.isPending("nonexistent")).toBe(false);
96
+ });
97
+ });
98
+
99
+ describe("getPendingCount", () => {
100
+ it("should return 0 initially", () => {
101
+ expect(deduplicator.getPendingCount()).toBe(0);
102
+ });
103
+
104
+ it("should track pending request count", async () => {
105
+ const mockFactory = vi.fn().mockImplementation(
106
+ () =>
107
+ new Promise((resolve) => {
108
+ setTimeout(() => resolve("result"), 100);
109
+ }),
110
+ );
111
+
112
+ const promise1 = deduplicator.executeRequest("key1", mockFactory);
113
+ const promise2 = deduplicator.executeRequest("key2", mockFactory);
114
+
115
+ expect(deduplicator.getPendingCount()).toBe(2);
116
+
117
+ await Promise.all([promise1, promise2]);
118
+
119
+ expect(deduplicator.getPendingCount()).toBe(0);
120
+ });
121
+ });
122
+
123
+ describe("getPendingKeys", () => {
124
+ it("should return empty array initially", () => {
125
+ expect(deduplicator.getPendingKeys()).toEqual([]);
126
+ });
127
+
128
+ it("should return pending keys", async () => {
129
+ const mockFactory = vi.fn().mockImplementation(
130
+ () =>
131
+ new Promise((resolve) => {
132
+ setTimeout(() => resolve("result"), 100);
133
+ }),
134
+ );
135
+
136
+ const promise1 = deduplicator.executeRequest("key1", mockFactory);
137
+ const promise2 = deduplicator.executeRequest("key2", mockFactory);
138
+
139
+ const pendingKeys = deduplicator.getPendingKeys();
140
+ expect(pendingKeys).toHaveLength(2);
141
+ expect(pendingKeys).toContain("key1");
142
+ expect(pendingKeys).toContain("key2");
143
+
144
+ await Promise.all([promise1, promise2]);
145
+
146
+ expect(deduplicator.getPendingKeys()).toEqual([]);
147
+ });
148
+ });
149
+
150
+ describe("clear", () => {
151
+ it("should clear all pending requests", async () => {
152
+ const mockFactory = vi.fn().mockImplementation(
153
+ () =>
154
+ new Promise((resolve) => {
155
+ setTimeout(() => resolve("result"), 100);
156
+ }),
157
+ );
158
+
159
+ deduplicator.executeRequest("key1", mockFactory);
160
+ deduplicator.executeRequest("key2", mockFactory);
161
+
162
+ expect(deduplicator.getPendingCount()).toBe(2);
163
+
164
+ deduplicator.clear();
165
+
166
+ expect(deduplicator.getPendingCount()).toBe(0);
167
+ expect(deduplicator.getPendingKeys()).toEqual([]);
168
+ });
169
+ });
170
+ });