@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,325 @@
1
+ import { afterEach, beforeEach, describe, vi } from "vitest";
2
+ import { assetMSWHandlers } from "../../../../test/useAssetMSW.js";
3
+ import { test as baseTest } from "../../../../test/useMSW.js";
4
+ import { AudioElementFactory } from "./AudioElementFactory.js";
5
+ import { MediaSourceService } from "./MediaSourceService.js";
6
+
7
+ const test = baseTest.extend({
8
+ setupAssetHandlers: [
9
+ async ({ worker }, use) => {
10
+ worker.use(...assetMSWHandlers);
11
+ await use(undefined);
12
+ },
13
+ { auto: true },
14
+ ],
15
+ });
16
+
17
+ describe("AudioElementFactory", () => {
18
+ let factory: AudioElementFactory;
19
+ let mediaSourceService: MediaSourceService;
20
+ let audioContext1: AudioContext;
21
+ let audioContext2: AudioContext;
22
+
23
+ beforeEach(() => {
24
+ // Clean up any existing elements
25
+ while (document.body.children.length) {
26
+ document.body.children[0]?.remove();
27
+ }
28
+
29
+ factory = new AudioElementFactory();
30
+ mediaSourceService = new MediaSourceService();
31
+ audioContext1 = new AudioContext();
32
+ audioContext2 = new AudioContext();
33
+ });
34
+
35
+ afterEach(async () => {
36
+ factory.clearCache();
37
+ mediaSourceService.cleanup();
38
+ // Close AudioContexts safely
39
+ if (audioContext1.state !== "closed") await audioContext1.close();
40
+ if (audioContext2.state !== "closed") await audioContext2.close();
41
+ vi.clearAllMocks();
42
+ });
43
+
44
+ describe("MediaElementSource Creation", () => {
45
+ test("should create MediaElementAudioSourceNode when MediaSource is ready", async ({
46
+ expect,
47
+ }) => {
48
+ // Mock a successful MediaSource
49
+ vi.spyOn(mediaSourceService, "ensureInitialized").mockResolvedValue();
50
+ vi.spyOn(mediaSourceService, "getAudioElement").mockReturnValue(
51
+ document.createElement("audio"),
52
+ );
53
+
54
+ const source = await factory.createMediaElementSource(
55
+ audioContext1,
56
+ mediaSourceService,
57
+ );
58
+
59
+ expect(source).toBeDefined();
60
+ expect(source).toBeInstanceOf(MediaElementAudioSourceNode);
61
+ expect(mediaSourceService.ensureInitialized).toHaveBeenCalledOnce();
62
+ });
63
+
64
+ test("should throw error when audio element not available", async ({
65
+ expect,
66
+ }) => {
67
+ vi.spyOn(mediaSourceService, "ensureInitialized").mockResolvedValue();
68
+ vi.spyOn(mediaSourceService, "getAudioElement").mockReturnValue(null);
69
+
70
+ await expect(
71
+ factory.createMediaElementSource(audioContext1, mediaSourceService),
72
+ ).rejects.toThrow("Audio element not available from MediaSourceService");
73
+ });
74
+
75
+ test("should propagate MediaSourceService initialization errors", async ({
76
+ expect,
77
+ }) => {
78
+ const initError = new Error("MediaSource initialization failed");
79
+ vi.spyOn(mediaSourceService, "ensureInitialized").mockRejectedValue(
80
+ initError,
81
+ );
82
+
83
+ await expect(
84
+ factory.createMediaElementSource(audioContext1, mediaSourceService),
85
+ ).rejects.toThrow("MediaSource initialization failed");
86
+ });
87
+ });
88
+
89
+ describe("Caching Behavior", () => {
90
+ test("should cache MediaElementSource per AudioContext", async ({
91
+ expect,
92
+ }) => {
93
+ vi.spyOn(mediaSourceService, "ensureInitialized").mockResolvedValue();
94
+ vi.spyOn(mediaSourceService, "getAudioElement").mockReturnValue(
95
+ document.createElement("audio"),
96
+ );
97
+
98
+ const source1 = await factory.createMediaElementSource(
99
+ audioContext1,
100
+ mediaSourceService,
101
+ );
102
+ const source2 = await factory.createMediaElementSource(
103
+ audioContext1,
104
+ mediaSourceService,
105
+ );
106
+
107
+ expect(source1).toBe(source2);
108
+ expect(mediaSourceService.ensureInitialized).toHaveBeenCalledOnce();
109
+ });
110
+
111
+ test("should create separate sources for different AudioContexts", async ({
112
+ expect,
113
+ }) => {
114
+ vi.spyOn(mediaSourceService, "ensureInitialized").mockResolvedValue();
115
+
116
+ // Mock different audio elements for each call
117
+ const audio1 = document.createElement("audio");
118
+ const audio2 = document.createElement("audio");
119
+ vi.spyOn(mediaSourceService, "getAudioElement")
120
+ .mockReturnValueOnce(audio1)
121
+ .mockReturnValueOnce(audio2);
122
+
123
+ const source1 = await factory.createMediaElementSource(
124
+ audioContext1,
125
+ mediaSourceService,
126
+ );
127
+ const source2 = await factory.createMediaElementSource(
128
+ audioContext2,
129
+ mediaSourceService,
130
+ );
131
+
132
+ expect(source1).not.toBe(source2);
133
+ expect(mediaSourceService.ensureInitialized).toHaveBeenCalledTimes(2);
134
+ });
135
+
136
+ test("should respect hasCachedSource method", async ({ expect }) => {
137
+ expect(factory.hasCachedSource(audioContext1)).toBe(false);
138
+
139
+ vi.spyOn(mediaSourceService, "ensureInitialized").mockResolvedValue();
140
+ vi.spyOn(mediaSourceService, "getAudioElement").mockReturnValue(
141
+ document.createElement("audio"),
142
+ );
143
+
144
+ await factory.createMediaElementSource(audioContext1, mediaSourceService);
145
+
146
+ expect(factory.hasCachedSource(audioContext1)).toBe(true);
147
+ expect(factory.hasCachedSource(audioContext2)).toBe(false);
148
+ });
149
+
150
+ test("should not use cache when AudioContext is closed", async ({
151
+ expect,
152
+ }) => {
153
+ vi.spyOn(mediaSourceService, "ensureInitialized").mockResolvedValue();
154
+ // Mock different audio elements to avoid "already connected" error
155
+ const audio1 = document.createElement("audio");
156
+ const audio2 = document.createElement("audio");
157
+ vi.spyOn(mediaSourceService, "getAudioElement")
158
+ .mockReturnValueOnce(audio1)
159
+ .mockReturnValueOnce(audio2);
160
+
161
+ const source1 = await factory.createMediaElementSource(
162
+ audioContext1,
163
+ mediaSourceService,
164
+ );
165
+
166
+ // Close the AudioContext
167
+ await audioContext1.close();
168
+
169
+ // Should create new source since context is closed
170
+ const source2 = await factory.createMediaElementSource(
171
+ audioContext1,
172
+ mediaSourceService,
173
+ );
174
+
175
+ expect(source1).not.toBe(source2);
176
+ expect(mediaSourceService.ensureInitialized).toHaveBeenCalledTimes(2);
177
+ });
178
+ });
179
+
180
+ describe("Cleanup and Lifecycle", () => {
181
+ test("should clear all cached sources", async ({ expect }) => {
182
+ vi.spyOn(mediaSourceService, "ensureInitialized").mockResolvedValue();
183
+ // Mock different audio elements for each AudioContext
184
+ const audio1 = document.createElement("audio");
185
+ const audio2 = document.createElement("audio");
186
+ vi.spyOn(mediaSourceService, "getAudioElement")
187
+ .mockReturnValueOnce(audio1)
188
+ .mockReturnValueOnce(audio2);
189
+
190
+ await factory.createMediaElementSource(audioContext1, mediaSourceService);
191
+ await factory.createMediaElementSource(audioContext2, mediaSourceService);
192
+
193
+ // Due to browser limitation, only the most recent AudioContext should remain cached
194
+ // (audioContext1 gets disconnected when audioContext2 is created)
195
+ expect(factory.hasCachedSource(audioContext1)).toBe(false);
196
+ expect(factory.hasCachedSource(audioContext2)).toBe(true);
197
+
198
+ factory.clearCache();
199
+
200
+ expect(factory.hasCachedSource(audioContext1)).toBe(false);
201
+ expect(factory.hasCachedSource(audioContext2)).toBe(false);
202
+ });
203
+
204
+ test("should automatically clean up cache when AudioContext state changes", async ({
205
+ expect,
206
+ }) => {
207
+ vi.spyOn(mediaSourceService, "ensureInitialized").mockResolvedValue();
208
+ vi.spyOn(mediaSourceService, "getAudioElement").mockReturnValue(
209
+ document.createElement("audio"),
210
+ );
211
+
212
+ await factory.createMediaElementSource(audioContext1, mediaSourceService);
213
+ expect(factory.hasCachedSource(audioContext1)).toBe(true);
214
+
215
+ // Simulate AudioContext state change to closed
216
+ await audioContext1.close();
217
+
218
+ // Trigger state change event manually since we're in test environment
219
+ audioContext1.dispatchEvent(new Event("statechange"));
220
+
221
+ // Wait a tick for event processing
222
+ await new Promise((resolve) => setTimeout(resolve, 0));
223
+
224
+ expect(factory.hasCachedSource(audioContext1)).toBe(false);
225
+ });
226
+
227
+ test("should handle multiple cleanup calls gracefully", ({ expect }) => {
228
+ expect(() => {
229
+ factory.clearCache();
230
+ factory.clearCache();
231
+ factory.clearCache();
232
+ }).not.toThrow();
233
+ });
234
+ });
235
+
236
+ describe("Integration with MediaSourceService", () => {
237
+ test("should call ensureInitialized before creating source", async ({
238
+ expect,
239
+ }) => {
240
+ const ensureInitSpy = vi
241
+ .spyOn(mediaSourceService, "ensureInitialized")
242
+ .mockResolvedValue();
243
+ const getAudioElementSpy = vi
244
+ .spyOn(mediaSourceService, "getAudioElement")
245
+ .mockReturnValue(document.createElement("audio"));
246
+
247
+ await factory.createMediaElementSource(audioContext1, mediaSourceService);
248
+
249
+ expect(ensureInitSpy).toHaveBeenCalled();
250
+ expect(getAudioElementSpy).toHaveBeenCalled();
251
+ // Both should be called, order verified by implementation logic
252
+ });
253
+
254
+ test("should handle MediaSourceService reinitialization", async ({
255
+ expect,
256
+ }) => {
257
+ vi.spyOn(mediaSourceService, "ensureInitialized").mockResolvedValue();
258
+
259
+ const audio1 = document.createElement("audio");
260
+ const audio2 = document.createElement("audio");
261
+ vi.spyOn(mediaSourceService, "getAudioElement")
262
+ .mockReturnValueOnce(audio1)
263
+ .mockReturnValueOnce(audio2);
264
+
265
+ const source1 = await factory.createMediaElementSource(
266
+ audioContext1,
267
+ mediaSourceService,
268
+ );
269
+
270
+ // Clear cache to simulate reinitialization
271
+ factory.clearCache();
272
+
273
+ const source2 = await factory.createMediaElementSource(
274
+ audioContext1,
275
+ mediaSourceService,
276
+ );
277
+
278
+ expect(source1).not.toBe(source2);
279
+ expect(mediaSourceService.ensureInitialized).toHaveBeenCalledTimes(2);
280
+ });
281
+ });
282
+
283
+ describe("Error Handling", () => {
284
+ test("should handle MediaSourceService timeout gracefully", async ({
285
+ expect,
286
+ }) => {
287
+ const timeoutError = new Error(
288
+ "MediaSource failed to open within timeout",
289
+ );
290
+ vi.spyOn(mediaSourceService, "ensureInitialized").mockRejectedValue(
291
+ timeoutError,
292
+ );
293
+
294
+ await expect(
295
+ factory.createMediaElementSource(audioContext1, mediaSourceService),
296
+ ).rejects.toThrow("MediaSource failed to open within timeout");
297
+
298
+ // Should not have cached anything on error
299
+ expect(factory.hasCachedSource(audioContext1)).toBe(false);
300
+ });
301
+
302
+ test("should handle createMediaElementSource browser errors", async ({
303
+ expect,
304
+ }) => {
305
+ vi.spyOn(mediaSourceService, "ensureInitialized").mockResolvedValue();
306
+ vi.spyOn(mediaSourceService, "getAudioElement").mockReturnValue(
307
+ document.createElement("audio"),
308
+ );
309
+
310
+ // Mock createMediaElementSource to throw
311
+ const createSourceError = new Error("HTMLMediaElement already connected");
312
+ vi.spyOn(audioContext1, "createMediaElementSource").mockImplementation(
313
+ () => {
314
+ throw createSourceError;
315
+ },
316
+ );
317
+
318
+ await expect(
319
+ factory.createMediaElementSource(audioContext1, mediaSourceService),
320
+ ).rejects.toThrow("HTMLMediaElement already connected");
321
+
322
+ expect(factory.hasCachedSource(audioContext1)).toBe(false);
323
+ });
324
+ });
325
+ });
@@ -0,0 +1,119 @@
1
+ import type { MediaSourceService } from "./MediaSourceService.js";
2
+
3
+ /**
4
+ * Factory for creating and caching MediaElementAudioSourceNode instances
5
+ * Handles the complex lifecycle and caching logic previously embedded in EFMedia
6
+ */
7
+ export class AudioElementFactory {
8
+ private cache = new WeakMap<AudioContext, MediaElementAudioSourceNode>();
9
+ private currentSource: MediaElementAudioSourceNode | null = null;
10
+ private currentAudioContext: AudioContext | null = null;
11
+
12
+ /**
13
+ * Create or retrieve cached MediaElementAudioSourceNode for the given AudioContext
14
+ */
15
+ async createMediaElementSource(
16
+ audioContext: AudioContext,
17
+ mediaSourceService: MediaSourceService,
18
+ ): Promise<MediaElementAudioSourceNode> {
19
+ // Check if we already have a cached source for this AudioContext
20
+ const cached = this.cache.get(audioContext);
21
+ if (
22
+ cached &&
23
+ audioContext.state !== "closed" &&
24
+ this.currentAudioContext === audioContext
25
+ ) {
26
+ return cached;
27
+ }
28
+
29
+ // Disconnect previous MediaElementSource BEFORE any operations if switching AudioContexts
30
+ // HTML audio elements can only be connected to one MediaElementSourceNode at a time
31
+ if (this.currentSource && this.currentAudioContext !== audioContext) {
32
+ this.currentSource.disconnect();
33
+ if (this.currentAudioContext) {
34
+ this.cache.delete(this.currentAudioContext);
35
+ }
36
+ // Clear current tracking immediately
37
+ this.currentSource = null;
38
+ this.currentAudioContext = null;
39
+ }
40
+
41
+ // Ensure MediaSource is initialized
42
+ await mediaSourceService.ensureInitialized();
43
+
44
+ const audioElement = mediaSourceService.getAudioElement();
45
+ if (!audioElement) {
46
+ throw new Error("Audio element not available from MediaSourceService");
47
+ }
48
+
49
+ // Create MediaElementAudioSourceNode
50
+ let mediaElementSource: MediaElementAudioSourceNode;
51
+ try {
52
+ mediaElementSource = audioContext.createMediaElementSource(audioElement);
53
+ } catch (error) {
54
+ // If still connected to another source, force clear and retry once
55
+ if (
56
+ error instanceof Error &&
57
+ error.message.includes("already connected")
58
+ ) {
59
+ // Clear all caches and try to disconnect any lingering connections
60
+ this.clearCache();
61
+ try {
62
+ mediaElementSource =
63
+ audioContext.createMediaElementSource(audioElement);
64
+ } catch (retryError) {
65
+ console.warn(
66
+ "AudioElementFactory: Failed to create MediaElementSource even after clearing cache:",
67
+ retryError,
68
+ );
69
+ throw retryError;
70
+ }
71
+ } else {
72
+ throw error;
73
+ }
74
+ }
75
+
76
+ // Update current tracking
77
+ this.currentSource = mediaElementSource;
78
+ this.currentAudioContext = audioContext;
79
+
80
+ // Cache the source for this AudioContext
81
+ this.cache.set(audioContext, mediaElementSource);
82
+
83
+ // Clean up cache and tracking when AudioContext is closed
84
+ const cleanup = () => {
85
+ if (audioContext.state === "closed") {
86
+ this.cache.delete(audioContext);
87
+ if (this.currentAudioContext === audioContext) {
88
+ this.currentSource = null;
89
+ this.currentAudioContext = null;
90
+ }
91
+ audioContext.removeEventListener("statechange", cleanup);
92
+ }
93
+ };
94
+ audioContext.addEventListener("statechange", cleanup);
95
+
96
+ return mediaElementSource;
97
+ }
98
+
99
+ /**
100
+ * Clear all cached sources (useful for testing or cleanup)
101
+ */
102
+ clearCache(): void {
103
+ // Disconnect current source if it exists
104
+ if (this.currentSource) {
105
+ this.currentSource.disconnect();
106
+ }
107
+
108
+ this.cache = new WeakMap();
109
+ this.currentSource = null;
110
+ this.currentAudioContext = null;
111
+ }
112
+
113
+ /**
114
+ * Check if we have a cached source for the given AudioContext
115
+ */
116
+ hasCachedSource(audioContext: AudioContext): boolean {
117
+ return this.cache.has(audioContext) && audioContext.state !== "closed";
118
+ }
119
+ }
@@ -0,0 +1,257 @@
1
+ import { afterEach, beforeEach, describe, vi } from "vitest";
2
+ import { assetMSWHandlers } from "../../../../test/useAssetMSW.js";
3
+ import { test as baseTest } from "../../../../test/useMSW.js";
4
+ import { MediaSourceService } from "./MediaSourceService.js";
5
+
6
+ const test = baseTest.extend({
7
+ setupAssetHandlers: [
8
+ async ({ worker }, use) => {
9
+ worker.use(...assetMSWHandlers);
10
+ await use(undefined);
11
+ },
12
+ { auto: true },
13
+ ],
14
+ });
15
+
16
+ describe("MediaSourceService", () => {
17
+ let service: MediaSourceService;
18
+
19
+ beforeEach(() => {
20
+ // Clean up any existing elements
21
+ while (document.body.children.length) {
22
+ document.body.children[0]?.remove();
23
+ }
24
+ });
25
+
26
+ afterEach(() => {
27
+ service?.cleanup();
28
+ vi.clearAllMocks();
29
+ });
30
+
31
+ describe("Initialization", () => {
32
+ test("should create service without initializing MediaSource", ({
33
+ expect,
34
+ }) => {
35
+ service = new MediaSourceService();
36
+
37
+ expect(service).toBeDefined();
38
+ expect(service.isReady()).toBe(false);
39
+ expect(service.getAudioElement()).toBeNull();
40
+ });
41
+
42
+ test("should initialize MediaSource when ensureInitialized is called", async ({
43
+ expect,
44
+ }) => {
45
+ service = new MediaSourceService();
46
+
47
+ try {
48
+ await service.ensureInitialized();
49
+
50
+ // If initialization succeeds (real browser), verify behavior
51
+ expect(service.isReady()).toBe(true);
52
+ expect(service.getAudioElement()).toBeDefined();
53
+ expect(service.getAudioElement()).toBeInstanceOf(HTMLAudioElement);
54
+ } catch (error) {
55
+ // If MediaSource doesn't work in test environment, verify error handling
56
+ expect(error).toBeDefined();
57
+ expect(service.isReady()).toBe(false);
58
+ }
59
+ });
60
+
61
+ test("should not reinitialize if already ready", async ({ expect }) => {
62
+ service = new MediaSourceService();
63
+
64
+ await service.ensureInitialized();
65
+ const firstElement = service.getAudioElement();
66
+
67
+ await service.ensureInitialized();
68
+ const secondElement = service.getAudioElement();
69
+
70
+ expect(firstElement).toBe(secondElement);
71
+ });
72
+
73
+ test("should call onReady callback when initialized", async ({
74
+ expect,
75
+ }) => {
76
+ const onReady = vi.fn();
77
+ service = new MediaSourceService({ onReady });
78
+
79
+ await service.ensureInitialized();
80
+
81
+ expect(onReady).toHaveBeenCalledOnce();
82
+ });
83
+
84
+ test("should call onError callback on initialization failure", async ({
85
+ expect,
86
+ }) => {
87
+ const onError = vi.fn();
88
+ // Use default timeout but expect failures in test environment
89
+ service = new MediaSourceService({ onError });
90
+
91
+ try {
92
+ await service.ensureInitialized();
93
+ } catch (error) {
94
+ // MediaSource may not work in test environment - that's expected
95
+ expect(error).toBeDefined();
96
+ }
97
+
98
+ // onError might have been called during MediaSource creation
99
+ if (onError.mock.calls.length > 0) {
100
+ expect(onError).toHaveBeenCalled();
101
+ }
102
+ });
103
+ });
104
+
105
+ describe("Audio Element Management", () => {
106
+ test("should return null audio element when not initialized", ({
107
+ expect,
108
+ }) => {
109
+ service = new MediaSourceService();
110
+
111
+ expect(service.getAudioElement()).toBeNull();
112
+ });
113
+
114
+ test("should return audio element after initialization (when possible)", async ({
115
+ expect,
116
+ }) => {
117
+ service = new MediaSourceService();
118
+
119
+ try {
120
+ await service.ensureInitialized();
121
+ const audioElement = service.getAudioElement();
122
+
123
+ if (service.isReady()) {
124
+ expect(audioElement).toBeDefined();
125
+ expect(audioElement).toBeInstanceOf(HTMLAudioElement);
126
+ }
127
+ } catch (_error) {
128
+ // MediaSource might not work in test environment
129
+ expect(service.getAudioElement()).toBeNull();
130
+ }
131
+ });
132
+
133
+ test("should handle cleanup and reinitialization cycle", async ({
134
+ expect,
135
+ }) => {
136
+ service = new MediaSourceService();
137
+
138
+ try {
139
+ await service.ensureInitialized();
140
+ service.cleanup();
141
+
142
+ expect(service.isReady()).toBe(false);
143
+ expect(service.getAudioElement()).toBeNull();
144
+
145
+ // Try to reinitialize
146
+ await service.ensureInitialized();
147
+ // Should either work or fail consistently
148
+ expect(typeof service.isReady()).toBe("boolean");
149
+ } catch (_error) {
150
+ // Expected in test environment - verify cleanup still works
151
+ expect(service.isReady()).toBe(false);
152
+ }
153
+ });
154
+ });
155
+
156
+ describe("Segment Feeding", () => {
157
+ test("should auto-initialize when feeding segments", async ({ expect }) => {
158
+ service = new MediaSourceService();
159
+ const segmentData = new ArrayBuffer(1024);
160
+
161
+ expect(service.isReady()).toBe(false);
162
+
163
+ await service.feedSegment(segmentData);
164
+
165
+ expect(service.isReady()).toBe(true);
166
+ });
167
+
168
+ test("should handle segment feeding when already initialized", async ({
169
+ expect,
170
+ }) => {
171
+ service = new MediaSourceService();
172
+ await service.ensureInitialized();
173
+
174
+ const segmentData = new ArrayBuffer(1024);
175
+
176
+ // Should not throw
177
+ await expect(service.feedSegment(segmentData)).resolves.not.toThrow();
178
+ });
179
+ });
180
+
181
+ describe("Time Management", () => {
182
+ test("should handle time setting when not initialized", ({ expect }) => {
183
+ service = new MediaSourceService();
184
+
185
+ // Should not throw
186
+ expect(() => service.setCurrentTime(1000)).not.toThrow();
187
+ });
188
+
189
+ test("should set time on audio element when initialized", async ({
190
+ expect,
191
+ }) => {
192
+ service = new MediaSourceService();
193
+ await service.ensureInitialized();
194
+
195
+ const audioElement = service.getAudioElement();
196
+ const setCurrentTimeSpy = vi.spyOn(audioElement!, "currentTime", "set");
197
+
198
+ service.setCurrentTime(2000);
199
+
200
+ expect(setCurrentTimeSpy).toHaveBeenCalledWith(2);
201
+ });
202
+ });
203
+
204
+ describe("Cleanup", () => {
205
+ test("should handle cleanup when not initialized", ({ expect }) => {
206
+ service = new MediaSourceService();
207
+
208
+ expect(() => service.cleanup()).not.toThrow();
209
+ expect(service.isReady()).toBe(false);
210
+ });
211
+
212
+ test("should cleanup initialized MediaSource", async ({ expect }) => {
213
+ service = new MediaSourceService();
214
+ await service.ensureInitialized();
215
+
216
+ expect(service.isReady()).toBe(true);
217
+
218
+ service.cleanup();
219
+
220
+ expect(service.isReady()).toBe(false);
221
+ expect(service.getAudioElement()).toBeNull();
222
+ });
223
+
224
+ test("should allow reinitialization after cleanup", async ({ expect }) => {
225
+ service = new MediaSourceService();
226
+
227
+ await service.ensureInitialized();
228
+ service.cleanup();
229
+ await service.ensureInitialized();
230
+
231
+ expect(service.isReady()).toBe(true);
232
+ expect(service.getAudioElement()).toBeDefined();
233
+ });
234
+ });
235
+
236
+ describe("Buffered Ranges", () => {
237
+ test("should return null buffered ranges when not initialized", ({
238
+ expect,
239
+ }) => {
240
+ service = new MediaSourceService();
241
+
242
+ expect(service.getBuffered()).toBeNull();
243
+ });
244
+
245
+ test("should return buffered ranges when initialized", async ({
246
+ expect,
247
+ }) => {
248
+ service = new MediaSourceService();
249
+ await service.ensureInitialized();
250
+
251
+ const buffered = service.getBuffered();
252
+
253
+ // May be null or TimeRanges depending on state
254
+ expect(buffered === null || buffered instanceof TimeRanges).toBe(true);
255
+ });
256
+ });
257
+ });