@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.
- package/dist/EF_FRAMEGEN.js +1 -1
- package/dist/ScrubTrackManager.d.ts +2 -2
- package/dist/elements/EFAudio.d.ts +21 -2
- package/dist/elements/EFAudio.js +41 -11
- package/dist/elements/EFImage.d.ts +1 -0
- package/dist/elements/EFImage.js +11 -3
- package/dist/elements/EFMedia/AssetIdMediaEngine.d.ts +18 -0
- package/dist/elements/EFMedia/AssetIdMediaEngine.js +41 -0
- package/dist/elements/EFMedia/AssetMediaEngine.d.ts +47 -0
- package/dist/elements/EFMedia/AssetMediaEngine.js +116 -0
- package/dist/elements/EFMedia/BaseMediaEngine.d.ts +55 -0
- package/dist/elements/EFMedia/BaseMediaEngine.js +96 -0
- package/dist/elements/EFMedia/BufferedSeekingInput.d.ts +43 -0
- package/dist/elements/EFMedia/BufferedSeekingInput.js +159 -0
- package/dist/elements/EFMedia/JitMediaEngine.browsertest.d.ts +0 -0
- package/dist/elements/EFMedia/JitMediaEngine.d.ts +31 -0
- package/dist/elements/EFMedia/JitMediaEngine.js +62 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.browsertest.d.ts +9 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.d.ts +16 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.js +48 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.d.ts +3 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.js +138 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.browsertest.d.ts +9 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.d.ts +4 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.js +16 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioInputTask.browsertest.d.ts +9 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioInputTask.d.ts +3 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioInputTask.js +22 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.d.ts +7 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.js +24 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.d.ts +4 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.js +18 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.d.ts +4 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.js +16 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.d.ts +3 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.js +104 -0
- package/dist/elements/EFMedia/services/AudioElementFactory.d.ts +22 -0
- package/dist/elements/EFMedia/services/AudioElementFactory.js +72 -0
- package/dist/elements/EFMedia/services/MediaSourceService.browsertest.d.ts +1 -0
- package/dist/elements/EFMedia/services/MediaSourceService.d.ts +47 -0
- package/dist/elements/EFMedia/services/MediaSourceService.js +73 -0
- package/dist/elements/EFMedia/shared/AudioSpanUtils.d.ts +7 -0
- package/dist/elements/EFMedia/shared/AudioSpanUtils.js +54 -0
- package/dist/elements/EFMedia/shared/BufferUtils.d.ts +70 -0
- package/dist/elements/EFMedia/shared/BufferUtils.js +89 -0
- package/dist/elements/EFMedia/shared/MediaTaskUtils.d.ts +23 -0
- package/dist/elements/EFMedia/shared/RenditionHelpers.browsertest.d.ts +1 -0
- package/dist/elements/EFMedia/shared/RenditionHelpers.d.ts +19 -0
- package/dist/elements/EFMedia/tasks/makeMediaEngineTask.browsertest.d.ts +1 -0
- package/dist/elements/EFMedia/tasks/makeMediaEngineTask.d.ts +18 -0
- package/dist/elements/EFMedia/tasks/makeMediaEngineTask.js +60 -0
- package/dist/elements/EFMedia/tasks/makeMediaEngineTask.test.d.ts +1 -0
- package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.browsertest.d.ts +9 -0
- package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.d.ts +16 -0
- package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.js +46 -0
- package/dist/elements/EFMedia/videoTasks/makeVideoInitSegmentFetchTask.browsertest.d.ts +9 -0
- package/dist/elements/EFMedia/videoTasks/makeVideoInitSegmentFetchTask.d.ts +4 -0
- package/dist/elements/EFMedia/videoTasks/makeVideoInitSegmentFetchTask.js +16 -0
- package/dist/elements/EFMedia/videoTasks/makeVideoInputTask.browsertest.d.ts +9 -0
- package/dist/elements/EFMedia/videoTasks/makeVideoInputTask.d.ts +3 -0
- package/dist/elements/EFMedia/videoTasks/makeVideoInputTask.js +27 -0
- package/dist/elements/EFMedia/videoTasks/makeVideoSeekTask.d.ts +7 -0
- package/dist/elements/EFMedia/videoTasks/makeVideoSeekTask.js +25 -0
- package/dist/elements/EFMedia/videoTasks/makeVideoSegmentFetchTask.browsertest.d.ts +9 -0
- package/dist/elements/EFMedia/videoTasks/makeVideoSegmentFetchTask.d.ts +4 -0
- package/dist/elements/EFMedia/videoTasks/makeVideoSegmentFetchTask.js +18 -0
- package/dist/elements/EFMedia/videoTasks/makeVideoSegmentIdTask.browsertest.d.ts +9 -0
- package/dist/elements/EFMedia/videoTasks/makeVideoSegmentIdTask.d.ts +4 -0
- package/dist/elements/EFMedia/videoTasks/makeVideoSegmentIdTask.js +16 -0
- package/dist/elements/EFMedia.browsertest.d.ts +1 -0
- package/dist/elements/EFMedia.d.ts +75 -111
- package/dist/elements/EFMedia.js +141 -1111
- package/dist/elements/EFTemporal.d.ts +1 -1
- package/dist/elements/EFTemporal.js +1 -1
- package/dist/elements/EFTimegroup.d.ts +11 -0
- package/dist/elements/EFTimegroup.js +88 -13
- package/dist/elements/EFVideo.d.ts +60 -29
- package/dist/elements/EFVideo.js +103 -203
- package/dist/elements/EFWaveform.js +2 -2
- package/dist/elements/SampleBuffer.d.ts +14 -0
- package/dist/elements/SampleBuffer.js +52 -0
- package/dist/getRenderInfo.d.ts +2 -2
- package/dist/getRenderInfo.js +2 -1
- package/dist/gui/ContextMixin.js +17 -70
- package/dist/gui/EFFilmstrip.d.ts +3 -3
- package/dist/gui/EFFilmstrip.js +1 -1
- package/dist/gui/EFFitScale.d.ts +2 -2
- package/dist/gui/TWMixin.js +1 -1
- package/dist/gui/services/ElementConnectionManager.browsertest.d.ts +1 -0
- package/dist/gui/services/ElementConnectionManager.d.ts +59 -0
- package/dist/gui/services/ElementConnectionManager.js +128 -0
- package/dist/gui/services/PlaybackController.browsertest.d.ts +1 -0
- package/dist/gui/services/PlaybackController.d.ts +103 -0
- package/dist/gui/services/PlaybackController.js +290 -0
- package/dist/services/MediaSourceManager.d.ts +62 -0
- package/dist/services/MediaSourceManager.js +211 -0
- package/dist/style.css +1 -1
- package/dist/transcoding/cache/CacheManager.d.ts +73 -0
- package/dist/transcoding/cache/RequestDeduplicator.d.ts +29 -0
- package/dist/transcoding/cache/RequestDeduplicator.js +53 -0
- package/dist/transcoding/cache/RequestDeduplicator.test.d.ts +1 -0
- package/dist/transcoding/types/index.d.ts +242 -0
- package/dist/transcoding/utils/MediaUtils.d.ts +9 -0
- package/dist/transcoding/utils/UrlGenerator.d.ts +26 -0
- package/dist/transcoding/utils/UrlGenerator.js +45 -0
- package/dist/transcoding/utils/constants.d.ts +27 -0
- package/dist/utils/LRUCache.d.ts +34 -0
- package/dist/utils/LRUCache.js +115 -0
- package/package.json +3 -2
- package/src/elements/EFAudio.browsertest.ts +183 -43
- package/src/elements/EFAudio.ts +59 -13
- package/src/elements/EFImage.browsertest.ts +42 -0
- package/src/elements/EFImage.ts +23 -3
- package/src/elements/EFMedia/AssetIdMediaEngine.test.ts +222 -0
- package/src/elements/EFMedia/AssetIdMediaEngine.ts +70 -0
- package/src/elements/EFMedia/AssetMediaEngine.ts +210 -0
- package/src/elements/EFMedia/BaseMediaEngine.test.ts +164 -0
- package/src/elements/EFMedia/BaseMediaEngine.ts +170 -0
- package/src/elements/EFMedia/BufferedSeekingInput.browsertest.ts +400 -0
- package/src/elements/EFMedia/BufferedSeekingInput.ts +267 -0
- package/src/elements/EFMedia/JitMediaEngine.browsertest.ts +165 -0
- package/src/elements/EFMedia/JitMediaEngine.ts +110 -0
- package/src/elements/EFMedia/audioTasks/makeAudioBufferTask.browsertest.ts +554 -0
- package/src/elements/EFMedia/audioTasks/makeAudioBufferTask.ts +81 -0
- package/src/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.ts +241 -0
- package/src/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.browsertest.ts +59 -0
- package/src/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.ts +23 -0
- package/src/elements/EFMedia/audioTasks/makeAudioInputTask.browsertest.ts +55 -0
- package/src/elements/EFMedia/audioTasks/makeAudioInputTask.ts +35 -0
- package/src/elements/EFMedia/audioTasks/makeAudioSeekTask.ts +42 -0
- package/src/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.ts +34 -0
- package/src/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.ts +23 -0
- package/src/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.ts +174 -0
- package/src/elements/EFMedia/services/AudioElementFactory.browsertest.ts +325 -0
- package/src/elements/EFMedia/services/AudioElementFactory.ts +119 -0
- package/src/elements/EFMedia/services/MediaSourceService.browsertest.ts +257 -0
- package/src/elements/EFMedia/services/MediaSourceService.ts +102 -0
- package/src/elements/EFMedia/shared/AudioSpanUtils.ts +128 -0
- package/src/elements/EFMedia/shared/BufferUtils.ts +310 -0
- package/src/elements/EFMedia/shared/MediaTaskUtils.ts +44 -0
- package/src/elements/EFMedia/shared/RenditionHelpers.browsertest.ts +247 -0
- package/src/elements/EFMedia/shared/RenditionHelpers.ts +79 -0
- package/src/elements/EFMedia/tasks/makeMediaEngineTask.browsertest.ts +128 -0
- package/src/elements/EFMedia/tasks/makeMediaEngineTask.test.ts +233 -0
- package/src/elements/EFMedia/tasks/makeMediaEngineTask.ts +89 -0
- package/src/elements/EFMedia/videoTasks/makeVideoBufferTask.browsertest.ts +555 -0
- package/src/elements/EFMedia/videoTasks/makeVideoBufferTask.ts +79 -0
- package/src/elements/EFMedia/videoTasks/makeVideoInitSegmentFetchTask.browsertest.ts +59 -0
- package/src/elements/EFMedia/videoTasks/makeVideoInitSegmentFetchTask.ts +23 -0
- package/src/elements/EFMedia/videoTasks/makeVideoInputTask.browsertest.ts +55 -0
- package/src/elements/EFMedia/videoTasks/makeVideoInputTask.ts +45 -0
- package/src/elements/EFMedia/videoTasks/makeVideoSeekTask.ts +44 -0
- package/src/elements/EFMedia/videoTasks/makeVideoSegmentFetchTask.browsertest.ts +57 -0
- package/src/elements/EFMedia/videoTasks/makeVideoSegmentFetchTask.ts +32 -0
- package/src/elements/EFMedia/videoTasks/makeVideoSegmentIdTask.browsertest.ts +56 -0
- package/src/elements/EFMedia/videoTasks/makeVideoSegmentIdTask.ts +23 -0
- package/src/elements/EFMedia.browsertest.ts +658 -265
- package/src/elements/EFMedia.ts +173 -1763
- package/src/elements/EFTemporal.ts +3 -4
- package/src/elements/EFTimegroup.browsertest.ts +6 -3
- package/src/elements/EFTimegroup.ts +152 -21
- package/src/elements/EFVideo.browsertest.ts +115 -37
- package/src/elements/EFVideo.ts +123 -452
- package/src/elements/EFWaveform.ts +1 -1
- package/src/elements/MediaController.ts +2 -12
- package/src/elements/SampleBuffer.ts +97 -0
- package/src/gui/ContextMixin.ts +23 -104
- package/src/gui/services/ElementConnectionManager.browsertest.ts +263 -0
- package/src/gui/services/ElementConnectionManager.ts +224 -0
- package/src/gui/services/PlaybackController.browsertest.ts +437 -0
- package/src/gui/services/PlaybackController.ts +521 -0
- package/src/services/MediaSourceManager.ts +333 -0
- package/src/transcoding/cache/CacheManager.ts +208 -0
- package/src/transcoding/cache/RequestDeduplicator.test.ts +170 -0
- package/src/transcoding/cache/RequestDeduplicator.ts +65 -0
- package/src/transcoding/types/index.ts +265 -0
- package/src/transcoding/utils/MediaUtils.ts +63 -0
- package/src/transcoding/utils/UrlGenerator.ts +68 -0
- package/src/transcoding/utils/constants.ts +36 -0
- package/src/utils/LRUCache.ts +153 -0
- package/test/EFVideo.framegen.browsertest.ts +38 -29
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- package/test/__cache__/GET__api_v1_transcode_manifest_json_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__3be92a0437de726b431ed5af2369158a/data.bin +1 -0
- package/test/__cache__/GET__api_v1_transcode_manifest_json_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__3be92a0437de726b431ed5af2369158a/metadata.json +19 -0
- package/test/createJitTestClips.ts +320 -188
- package/test/recordReplayProxyPlugin.js +302 -0
- package/test/useAssetMSW.ts +1 -1
- package/test/useMSW.ts +35 -22
- package/types.json +1 -1
- package/dist/JitTranscodingClient.d.ts +0 -167
- package/dist/JitTranscodingClient.js +0 -373
- package/dist/ScrubTrackManager.js +0 -216
- package/dist/elements/printTaskStatus.js +0 -11
- package/src/elements/__screenshots__/EFMedia.browsertest.ts/EFMedia-JIT-audio-playback-audioBufferTask-should-work-in-JIT-mode-without-URL-errors-1.png +0 -0
- package/test/EFVideo.frame-tasks.browsertest.ts +0 -524
- /package/dist/{JitTranscodingClient.browsertest.d.ts → elements/EFMedia/AssetIdMediaEngine.test.d.ts} +0 -0
- /package/dist/{JitTranscodingClient.test.d.ts → elements/EFMedia/BaseMediaEngine.test.d.ts} +0 -0
- /package/dist/{ScrubTrackIntegration.test.d.ts → elements/EFMedia/BufferedSeekingInput.browsertest.d.ts} +0 -0
- /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
|
+
});
|