@editframe/elements 0.18.3-beta.0 → 0.18.8-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 (110) hide show
  1. package/dist/elements/EFAudio.d.ts +1 -2
  2. package/dist/elements/EFAudio.js +6 -9
  3. package/dist/elements/EFMedia/AssetMediaEngine.browsertest.d.ts +0 -0
  4. package/dist/elements/EFMedia/AssetMediaEngine.d.ts +2 -4
  5. package/dist/elements/EFMedia/AssetMediaEngine.js +34 -5
  6. package/dist/elements/EFMedia/BaseMediaEngine.js +20 -1
  7. package/dist/elements/EFMedia/BufferedSeekingInput.d.ts +5 -5
  8. package/dist/elements/EFMedia/BufferedSeekingInput.js +27 -7
  9. package/dist/elements/EFMedia/JitMediaEngine.d.ts +1 -1
  10. package/dist/elements/EFMedia/JitMediaEngine.js +22 -3
  11. package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.js +4 -1
  12. package/dist/elements/EFMedia/audioTasks/makeAudioInputTask.js +11 -3
  13. package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.chunkboundary.regression.browsertest.d.ts +0 -0
  14. package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.js +17 -4
  15. package/dist/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.js +11 -1
  16. package/dist/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.js +3 -2
  17. package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.js +4 -1
  18. package/dist/elements/EFMedia/shared/PrecisionUtils.d.ts +28 -0
  19. package/dist/elements/EFMedia/shared/PrecisionUtils.js +29 -0
  20. package/dist/elements/EFMedia/videoTasks/makeVideoSeekTask.js +11 -2
  21. package/dist/elements/EFMedia/videoTasks/makeVideoSegmentFetchTask.js +11 -1
  22. package/dist/elements/EFMedia/videoTasks/makeVideoSegmentIdTask.js +3 -2
  23. package/dist/elements/EFMedia.d.ts +0 -12
  24. package/dist/elements/EFMedia.js +4 -30
  25. package/dist/elements/EFTimegroup.js +12 -17
  26. package/dist/elements/EFVideo.d.ts +0 -9
  27. package/dist/elements/EFVideo.js +0 -7
  28. package/dist/elements/SampleBuffer.js +6 -6
  29. package/dist/getRenderInfo.d.ts +2 -2
  30. package/dist/gui/ContextMixin.js +71 -17
  31. package/dist/gui/TWMixin.js +1 -1
  32. package/dist/style.css +1 -1
  33. package/dist/transcoding/types/index.d.ts +9 -9
  34. package/package.json +2 -3
  35. package/src/elements/EFAudio.browsertest.ts +7 -7
  36. package/src/elements/EFAudio.ts +7 -20
  37. package/src/elements/EFMedia/AssetMediaEngine.browsertest.ts +100 -0
  38. package/src/elements/EFMedia/AssetMediaEngine.ts +72 -7
  39. package/src/elements/EFMedia/BaseMediaEngine.ts +50 -1
  40. package/src/elements/EFMedia/BufferedSeekingInput.browsertest.ts +135 -54
  41. package/src/elements/EFMedia/BufferedSeekingInput.ts +74 -17
  42. package/src/elements/EFMedia/JitMediaEngine.ts +58 -2
  43. package/src/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.ts +10 -1
  44. package/src/elements/EFMedia/audioTasks/makeAudioInputTask.ts +16 -8
  45. package/src/elements/EFMedia/audioTasks/makeAudioSeekTask.chunkboundary.regression.browsertest.ts +199 -0
  46. package/src/elements/EFMedia/audioTasks/makeAudioSeekTask.ts +35 -4
  47. package/src/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.ts +12 -1
  48. package/src/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.ts +3 -2
  49. package/src/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.ts +10 -1
  50. package/src/elements/EFMedia/shared/PrecisionUtils.ts +46 -0
  51. package/src/elements/EFMedia/videoTasks/makeVideoSeekTask.ts +27 -3
  52. package/src/elements/EFMedia/videoTasks/makeVideoSegmentFetchTask.ts +12 -1
  53. package/src/elements/EFMedia/videoTasks/makeVideoSegmentIdTask.ts +3 -2
  54. package/src/elements/EFMedia.browsertest.ts +73 -33
  55. package/src/elements/EFMedia.ts +11 -54
  56. package/src/elements/EFTimegroup.ts +21 -26
  57. package/src/elements/EFVideo.browsertest.ts +895 -162
  58. package/src/elements/EFVideo.ts +0 -16
  59. package/src/elements/SampleBuffer.ts +8 -10
  60. package/src/gui/ContextMixin.ts +104 -26
  61. package/src/transcoding/types/index.ts +10 -6
  62. package/test/EFVideo.framegen.browsertest.ts +1 -1
  63. package/test/__cache__/GET__api_v1_transcode_audio_1_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__32da3954ba60c96ad732020c65a08ebc/metadata.json +3 -3
  64. package/test/__cache__/GET__api_v1_transcode_audio_1_mp4_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4_bytes_0__9ed2d25c675aa6bb6ff5b3ae23887c71/data.bin +0 -0
  65. package/test/__cache__/GET__api_v1_transcode_audio_1_mp4_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4_bytes_0__9ed2d25c675aa6bb6ff5b3ae23887c71/metadata.json +22 -0
  66. package/test/__cache__/GET__api_v1_transcode_audio_2_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__b0b2b07efcf607de8ee0f650328c32f7/metadata.json +3 -3
  67. package/test/__cache__/GET__api_v1_transcode_audio_2_mp4_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4_bytes_0__d5a3309a2bf756dd6e304807eb402f56/data.bin +0 -0
  68. package/test/__cache__/GET__api_v1_transcode_audio_2_mp4_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4_bytes_0__d5a3309a2bf756dd6e304807eb402f56/metadata.json +22 -0
  69. package/test/__cache__/GET__api_v1_transcode_audio_3_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__a75c2252b542e0c152c780e9a8d7b154/metadata.json +3 -3
  70. package/test/__cache__/GET__api_v1_transcode_audio_3_mp4_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4_bytes_0__773254bb671e3466fca8677139fb239e/data.bin +0 -0
  71. package/test/__cache__/GET__api_v1_transcode_audio_3_mp4_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4_bytes_0__773254bb671e3466fca8677139fb239e/metadata.json +22 -0
  72. package/test/__cache__/GET__api_v1_transcode_audio_4_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__a64ff1cfb1b52cae14df4b5dfa1e222b/metadata.json +3 -3
  73. package/test/__cache__/GET__api_v1_transcode_audio_init_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__e66d2c831d951e74ad0aeaa6489795d0/metadata.json +3 -3
  74. package/test/__cache__/GET__api_v1_transcode_high_1_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__26197f6f7c46cacb0a71134131c3f775/metadata.json +3 -3
  75. package/test/__cache__/GET__api_v1_transcode_high_2_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__4cb6774cd3650ccf59c8f8dc6678c0b9/metadata.json +3 -3
  76. package/test/__cache__/GET__api_v1_transcode_high_4_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__a6fb05a22b18d850f7f2950bbcdbdeed/data.bin +0 -0
  77. package/test/__cache__/GET__api_v1_transcode_high_4_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__a6fb05a22b18d850f7f2950bbcdbdeed/metadata.json +21 -0
  78. package/test/__cache__/GET__api_v1_transcode_high_5_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__a50058c7c3602e90879fe3428ed891f4/data.bin +0 -0
  79. package/test/__cache__/GET__api_v1_transcode_high_5_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__a50058c7c3602e90879fe3428ed891f4/metadata.json +21 -0
  80. package/test/__cache__/GET__api_v1_transcode_high_init_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__0798c479b44aaeef850609a430f6e613/metadata.json +3 -3
  81. package/test/__cache__/GET__api_v1_transcode_manifest_json_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__3be92a0437de726b431ed5af2369158a/data.bin +1 -1
  82. package/test/__cache__/GET__api_v1_transcode_manifest_json_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__3be92a0437de726b431ed5af2369158a/metadata.json +4 -4
  83. package/test/recordReplayProxyPlugin.js +50 -0
  84. package/types.json +1 -1
  85. package/dist/DecoderResetFrequency.test.d.ts +0 -1
  86. package/dist/DecoderResetRecovery.test.d.ts +0 -1
  87. package/dist/ScrubTrackManager.d.ts +0 -96
  88. package/dist/elements/EFMedia/services/AudioElementFactory.browsertest.d.ts +0 -1
  89. package/dist/elements/EFMedia/services/AudioElementFactory.d.ts +0 -22
  90. package/dist/elements/EFMedia/services/AudioElementFactory.js +0 -72
  91. package/dist/elements/EFMedia/services/MediaSourceService.browsertest.d.ts +0 -1
  92. package/dist/elements/EFMedia/services/MediaSourceService.d.ts +0 -47
  93. package/dist/elements/EFMedia/services/MediaSourceService.js +0 -73
  94. package/dist/gui/services/ElementConnectionManager.browsertest.d.ts +0 -1
  95. package/dist/gui/services/ElementConnectionManager.d.ts +0 -59
  96. package/dist/gui/services/ElementConnectionManager.js +0 -128
  97. package/dist/gui/services/PlaybackController.browsertest.d.ts +0 -1
  98. package/dist/gui/services/PlaybackController.d.ts +0 -103
  99. package/dist/gui/services/PlaybackController.js +0 -290
  100. package/dist/services/MediaSourceManager.d.ts +0 -62
  101. package/dist/services/MediaSourceManager.js +0 -211
  102. package/src/elements/EFMedia/services/AudioElementFactory.browsertest.ts +0 -325
  103. package/src/elements/EFMedia/services/AudioElementFactory.ts +0 -119
  104. package/src/elements/EFMedia/services/MediaSourceService.browsertest.ts +0 -257
  105. package/src/elements/EFMedia/services/MediaSourceService.ts +0 -102
  106. package/src/gui/services/ElementConnectionManager.browsertest.ts +0 -263
  107. package/src/gui/services/ElementConnectionManager.ts +0 -224
  108. package/src/gui/services/PlaybackController.browsertest.ts +0 -437
  109. package/src/gui/services/PlaybackController.ts +0 -521
  110. package/src/services/MediaSourceManager.ts +0 -333
@@ -1,102 +0,0 @@
1
- import {
2
- MediaSourceManager,
3
- type MediaSourceManagerOptions,
4
- } from "../../../services/MediaSourceManager.js";
5
-
6
- export interface MediaSourceServiceOptions {
7
- onError?: (error: Error) => void;
8
- onReady?: () => void;
9
- onUpdateEnd?: () => void;
10
- timeout?: number;
11
- }
12
-
13
- /**
14
- * Service for managing MediaSource lifecycle and audio element creation
15
- * Extracted from EFMedia to improve separation of concerns and testability
16
- */
17
- export class MediaSourceService {
18
- private mediaSourceManager: MediaSourceManager | null = null;
19
- private options: MediaSourceServiceOptions;
20
-
21
- constructor(options: MediaSourceServiceOptions = {}) {
22
- this.options = options;
23
- }
24
-
25
- /**
26
- * Initialize MediaSource if not already initialized
27
- */
28
- async ensureInitialized(): Promise<void> {
29
- if (this.mediaSourceManager?.isReady()) {
30
- return;
31
- }
32
-
33
- await this.initialize();
34
- }
35
-
36
- /**
37
- * Initialize fresh MediaSource
38
- */
39
- async initialize(): Promise<void> {
40
- // Clean up existing instance
41
- this.cleanup();
42
-
43
- // Create new MediaSourceManager
44
- const managerOptions: MediaSourceManagerOptions = {
45
- onError: this.options.onError,
46
- onReady: this.options.onReady,
47
- onUpdateEnd: this.options.onUpdateEnd,
48
- timeout: this.options.timeout,
49
- };
50
-
51
- this.mediaSourceManager = new MediaSourceManager(managerOptions);
52
- await this.mediaSourceManager.initialize();
53
- }
54
-
55
- /**
56
- * Get audio element for MediaElementSource creation
57
- */
58
- getAudioElement(): HTMLAudioElement | null {
59
- return this.mediaSourceManager?.getAudioElement() || null;
60
- }
61
-
62
- /**
63
- * Feed audio segments to MediaSource
64
- */
65
- async feedSegment(segmentBuffer: ArrayBuffer): Promise<void> {
66
- await this.ensureInitialized();
67
- if (this.mediaSourceManager) {
68
- await this.mediaSourceManager.feedSegment(segmentBuffer);
69
- }
70
- }
71
-
72
- /**
73
- * Check if MediaSource is ready
74
- */
75
- isReady(): boolean {
76
- return this.mediaSourceManager?.isReady() ?? false;
77
- }
78
-
79
- /**
80
- * Get buffered time ranges
81
- */
82
- getBuffered(): TimeRanges | null {
83
- return this.mediaSourceManager?.getBuffered() || null;
84
- }
85
-
86
- /**
87
- * Set audio element current time
88
- */
89
- setCurrentTime(timeMs: number): void {
90
- this.mediaSourceManager?.setCurrentTime(timeMs);
91
- }
92
-
93
- /**
94
- * Clean up MediaSource resources
95
- */
96
- cleanup(): void {
97
- if (this.mediaSourceManager) {
98
- this.mediaSourceManager.cleanup();
99
- this.mediaSourceManager = null;
100
- }
101
- }
102
- }
@@ -1,263 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
2
- import { ElementConnectionManager } from "./ElementConnectionManager.js";
3
-
4
- describe("ElementConnectionManager", () => {
5
- let manager: ElementConnectionManager;
6
- let audioContext: AudioContext;
7
- let mockTimegroup: any;
8
- let mockMediaElements: any[];
9
-
10
- beforeEach(() => {
11
- manager = new ElementConnectionManager(3000);
12
- audioContext = new AudioContext();
13
-
14
- // Create mock media elements
15
- mockMediaElements = [
16
- {
17
- src: "audio1.mp3",
18
- startTimeMs: 1000,
19
- endTimeMs: 3000,
20
- currentSourceTimeMs: 0,
21
- audioElement: { currentTime: 0, play: vi.fn(), pause: vi.fn() },
22
- getMediaElementSource: vi.fn(),
23
- },
24
- {
25
- src: "audio2.mp3",
26
- startTimeMs: 2500,
27
- endTimeMs: 5000,
28
- currentSourceTimeMs: 0,
29
- audioElement: { currentTime: 0, play: vi.fn(), pause: vi.fn() },
30
- getMediaElementSource: vi.fn(),
31
- },
32
- {
33
- src: "audio3.mp3",
34
- startTimeMs: 6000,
35
- endTimeMs: 8000,
36
- currentSourceTimeMs: 0,
37
- audioElement: { currentTime: 0, play: vi.fn(), pause: vi.fn() },
38
- getMediaElementSource: vi.fn(),
39
- },
40
- ];
41
-
42
- // Mock timegroup
43
- mockTimegroup = {
44
- querySelectorAll: vi.fn().mockReturnValue(mockMediaElements),
45
- };
46
-
47
- // Mock MediaElementAudioSourceNode for each element
48
- mockMediaElements.forEach((element, _index) => {
49
- const mockSource = {
50
- connect: vi.fn(),
51
- disconnect: vi.fn(),
52
- context: audioContext,
53
- } as any;
54
- element.getMediaElementSource.mockResolvedValue(mockSource);
55
- });
56
- });
57
-
58
- afterEach(async () => {
59
- manager.clearAll();
60
- if (audioContext.state !== "closed") {
61
- await audioContext.close();
62
- }
63
- vi.clearAllMocks();
64
- });
65
-
66
- describe("Constructor and Configuration", () => {
67
- test("should initialize with default lookahead", () => {
68
- const defaultManager = new ElementConnectionManager();
69
- expect(defaultManager.getLookaheadMs()).toBe(3000);
70
- });
71
-
72
- test("should initialize with custom lookahead", () => {
73
- const customManager = new ElementConnectionManager(5000);
74
- expect(customManager.getLookaheadMs()).toBe(5000);
75
- });
76
-
77
- test("should allow updating lookahead", () => {
78
- manager.setLookaheadMs(4000);
79
- expect(manager.getLookaheadMs()).toBe(4000);
80
- });
81
- });
82
-
83
- describe("Element Connection Logic", () => {
84
- test("should prepare elements that are starting soon", async () => {
85
- // At time 0ms with 3s lookahead, both audio1 (1000ms) and audio2 (2500ms) should be prepared
86
- await manager.updateConnectedElements(audioContext, mockTimegroup, 0);
87
-
88
- const info = manager.getConnectionInfo();
89
- expect(info.total).toBe(2); // Both audio1 and audio2 within 3s lookahead
90
- expect(info.prepared).toBe(2);
91
- expect(info.connected).toBe(0);
92
-
93
- // Verify both elements were prepared
94
- expect(mockMediaElements[0].getMediaElementSource).toHaveBeenCalledWith(
95
- audioContext,
96
- );
97
- expect(mockMediaElements[1].getMediaElementSource).toHaveBeenCalledWith(
98
- audioContext,
99
- );
100
- });
101
-
102
- test("should activate elements when they become current", async () => {
103
- // First prepare the element
104
- await manager.updateConnectedElements(audioContext, mockTimegroup, 500);
105
-
106
- // Then activate when it becomes current
107
- await manager.updateConnectedElements(audioContext, mockTimegroup, 1500);
108
-
109
- const info = manager.getConnectionInfo();
110
- expect(info.connected).toBe(1);
111
- expect(mockMediaElements[0].audioElement.play).toHaveBeenCalled();
112
- });
113
-
114
- test("should handle multiple active elements simultaneously", async () => {
115
- // At time 2750ms, both audio1 (1000-3000) and audio2 (2500-5000) should be active
116
- await manager.updateConnectedElements(audioContext, mockTimegroup, 2750);
117
-
118
- const info = manager.getConnectionInfo();
119
- expect(info.connected).toBe(2);
120
-
121
- expect(mockMediaElements[0].audioElement.play).toHaveBeenCalled();
122
- expect(mockMediaElements[1].audioElement.play).toHaveBeenCalled();
123
- });
124
-
125
- test("should deactivate elements when they end", async () => {
126
- // Activate audio1
127
- await manager.updateConnectedElements(audioContext, mockTimegroup, 2000);
128
- expect(manager.getConnectionInfo().connected).toBe(1);
129
-
130
- // Move past audio1's end time
131
- await manager.updateConnectedElements(audioContext, mockTimegroup, 3500);
132
-
133
- const info = manager.getConnectionInfo();
134
- // audio1 should be deactivated but still prepared, audio2 should be active
135
- expect(info.connected).toBe(1); // Only audio2 active
136
- expect(mockMediaElements[0].audioElement.pause).toHaveBeenCalled();
137
- });
138
-
139
- test("should cleanup old elements outside cleanup threshold", async () => {
140
- // Activate and then move far past
141
- await manager.updateConnectedElements(audioContext, mockTimegroup, 2000);
142
- await manager.updateConnectedElements(audioContext, mockTimegroup, 10000); // 7 seconds later
143
-
144
- const info = manager.getConnectionInfo();
145
- // All old elements should be cleaned up, only audio3 might be prepared if in lookahead
146
- expect(info.total).toBeLessThan(mockMediaElements.length);
147
- });
148
- });
149
-
150
- describe("Error Handling", () => {
151
- test("should propagate getMediaElementSource errors", async () => {
152
- // Mock both elements within lookahead to fail
153
- mockMediaElements[0].getMediaElementSource.mockRejectedValue(
154
- new Error("Connection failed"),
155
- );
156
- mockMediaElements[1].getMediaElementSource.mockRejectedValue(
157
- new Error("Connection failed"),
158
- );
159
-
160
- // Should throw the built-in error
161
- await expect(
162
- manager.updateConnectedElements(audioContext, mockTimegroup, 500),
163
- ).rejects.toThrow("Connection failed");
164
- });
165
-
166
- test("should propagate activation errors", async () => {
167
- mockMediaElements[0].audioElement.play.mockRejectedValue(
168
- new Error("Play failed"),
169
- );
170
-
171
- await manager.updateConnectedElements(audioContext, mockTimegroup, 500);
172
-
173
- // Should throw the built-in error when activating
174
- await expect(
175
- manager.updateConnectedElements(audioContext, mockTimegroup, 1500),
176
- ).rejects.toThrow("Play failed");
177
- });
178
-
179
- test("should handle closed AudioContext gracefully", async () => {
180
- await audioContext.close();
181
-
182
- await expect(
183
- manager.updateConnectedElements(audioContext, mockTimegroup, 1500),
184
- ).resolves.not.toThrow();
185
-
186
- expect(manager.getConnectionInfo().total).toBe(0);
187
- });
188
- });
189
-
190
- describe("Cleanup and Lifecycle", () => {
191
- test("should clear all connections", async () => {
192
- await manager.updateConnectedElements(audioContext, mockTimegroup, 2000);
193
- expect(manager.getConnectionInfo().total).toBeGreaterThan(0);
194
-
195
- manager.clearAll();
196
- expect(manager.getConnectionInfo().total).toBe(0);
197
- });
198
-
199
- test("should disconnect connected elements during clearAll", async () => {
200
- await manager.updateConnectedElements(audioContext, mockTimegroup, 2000);
201
-
202
- const mockSource = await mockMediaElements[0].getMediaElementSource();
203
-
204
- manager.clearAll();
205
-
206
- expect(mockSource.disconnect).toHaveBeenCalled();
207
- });
208
-
209
- test("should handle clearAll with no elements gracefully", () => {
210
- expect(() => manager.clearAll()).not.toThrow();
211
- });
212
- });
213
-
214
- describe("Connection Info and Debugging", () => {
215
- test("should provide accurate connection info", async () => {
216
- const initialInfo = manager.getConnectionInfo();
217
- expect(initialInfo).toEqual({ total: 0, connected: 0, prepared: 0 });
218
-
219
- await manager.updateConnectedElements(audioContext, mockTimegroup, 500);
220
- const preparedInfo = manager.getConnectionInfo();
221
- expect(preparedInfo.prepared).toBeGreaterThan(0);
222
-
223
- await manager.updateConnectedElements(audioContext, mockTimegroup, 1500);
224
- const activeInfo = manager.getConnectionInfo();
225
- expect(activeInfo.connected).toBeGreaterThan(0);
226
- });
227
-
228
- test("should track connected vs prepared states accurately", async () => {
229
- // Prepare multiple elements
230
- await manager.updateConnectedElements(audioContext, mockTimegroup, 0);
231
-
232
- // Activate one
233
- await manager.updateConnectedElements(audioContext, mockTimegroup, 2750);
234
-
235
- const info = manager.getConnectionInfo();
236
- expect(info.total).toBe(info.connected + info.prepared);
237
- expect(info.connected).toBeGreaterThan(0);
238
- });
239
- });
240
-
241
- describe("Lookahead Behavior", () => {
242
- test("should respect lookahead window for preparation", async () => {
243
- manager.setLookaheadMs(1000); // 1 second lookahead
244
-
245
- // At time 0, only elements starting within 1 second should be prepared
246
- await manager.updateConnectedElements(audioContext, mockTimegroup, 0);
247
-
248
- const info = manager.getConnectionInfo();
249
- expect(info.total).toBe(1); // Only audio1 at 1000ms
250
- });
251
-
252
- test("should adjust preparation based on lookahead changes", async () => {
253
- manager.setLookaheadMs(500); // Very short lookahead
254
-
255
- await manager.updateConnectedElements(audioContext, mockTimegroup, 0);
256
- expect(manager.getConnectionInfo().total).toBe(0); // No elements within 500ms
257
-
258
- manager.setLookaheadMs(2000); // Longer lookahead
259
- await manager.updateConnectedElements(audioContext, mockTimegroup, 0);
260
- expect(manager.getConnectionInfo().total).toBe(1); // audio1 now within range
261
- });
262
- });
263
- });
@@ -1,224 +0,0 @@
1
- /**
2
- * Manages dynamic connection/disconnection of media elements to AudioContext
3
- * Extracted from ContextMixin to improve separation of concerns and testability
4
- */
5
- export class ElementConnectionManager {
6
- private connectedMediaSources = new Map<
7
- any,
8
- { mediaElementSource: MediaElementAudioSourceNode; connected: boolean }
9
- >();
10
- private lookaheadMs: number;
11
-
12
- constructor(lookaheadMs = 3000) {
13
- this.lookaheadMs = lookaheadMs;
14
- }
15
-
16
- /**
17
- * Update connected media elements based on current playhead position
18
- * Connects upcoming elements and disconnects past elements
19
- */
20
- async updateConnectedElements(
21
- audioContext: AudioContext,
22
- timegroup: any, // EFTimegroup type
23
- currentMs: number,
24
- ): Promise<void> {
25
- if (!audioContext || audioContext.state === "closed") return;
26
-
27
- const allMediaElements = Array.from(
28
- timegroup.querySelectorAll("ef-audio, ef-video"),
29
- ) as any[];
30
- const lookaheadMs = currentMs + this.lookaheadMs;
31
-
32
- // Find elements that should be connected (active now or active soon)
33
- const elementsToConnect = this.getElementsToConnect(
34
- allMediaElements,
35
- currentMs,
36
- lookaheadMs,
37
- );
38
-
39
- // Connect new elements
40
- await this.connectNewElements(audioContext, elementsToConnect);
41
-
42
- // Update connection states for active elements
43
- await this.updateElementStates(currentMs);
44
-
45
- // Clean up old elements
46
- this.cleanupOldElements(currentMs);
47
- }
48
-
49
- /**
50
- * Find elements that should be connected based on timeline position
51
- */
52
- private getElementsToConnect(
53
- allElements: any[],
54
- currentMs: number,
55
- lookaheadMs: number,
56
- ): any[] {
57
- return allElements.filter((mediaElement) => {
58
- const startTime = mediaElement.startTimeMs;
59
- const endTime = mediaElement.endTimeMs;
60
-
61
- // Connect if:
62
- // 1. Currently active: currentMs is within [startTime, endTime]
63
- // 2. Starting soon: startTime is within lookahead window
64
- const isCurrentlyActive = currentMs >= startTime && currentMs < endTime;
65
- const isStartingSoon = startTime > currentMs && startTime <= lookaheadMs;
66
-
67
- return isCurrentlyActive || isStartingSoon;
68
- });
69
- }
70
-
71
- /**
72
- * Connect new elements that aren't already connected
73
- */
74
- private async connectNewElements(
75
- audioContext: AudioContext,
76
- elementsToConnect: any[],
77
- ): Promise<void> {
78
- for (const mediaElement of elementsToConnect) {
79
- if (!this.connectedMediaSources.has(mediaElement)) {
80
- const mediaElementSource =
81
- await mediaElement.getMediaElementSource(audioContext);
82
-
83
- this.connectedMediaSources.set(mediaElement, {
84
- mediaElementSource,
85
- connected: false, // Will be activated when element becomes active
86
- });
87
- }
88
- }
89
- }
90
-
91
- /**
92
- * Update connection states for all managed elements
93
- */
94
- private async updateElementStates(currentMs: number): Promise<void> {
95
- for (const [
96
- mediaElement,
97
- sourceInfo,
98
- ] of this.connectedMediaSources.entries()) {
99
- const startTime = mediaElement.startTimeMs;
100
- const endTime = mediaElement.endTimeMs;
101
- const isCurrentlyActive = currentMs >= startTime && currentMs < endTime;
102
-
103
- if (isCurrentlyActive && !sourceInfo.connected) {
104
- await this.activateElement(mediaElement, sourceInfo);
105
- } else if (!isCurrentlyActive && sourceInfo.connected) {
106
- await this.deactivateElement(mediaElement, sourceInfo);
107
- }
108
- }
109
- }
110
-
111
- /**
112
- * Activate an element (connect to destination and start playback)
113
- */
114
- private async activateElement(
115
- mediaElement: any,
116
- sourceInfo: {
117
- mediaElementSource: MediaElementAudioSourceNode;
118
- connected: boolean;
119
- },
120
- ): Promise<void> {
121
- sourceInfo.mediaElementSource.connect(
122
- sourceInfo.mediaElementSource.context.destination,
123
- );
124
- sourceInfo.connected = true;
125
-
126
- // Set correct timing
127
- if (mediaElement.audioElement) {
128
- const mediaTimeMs = mediaElement.currentSourceTimeMs;
129
- mediaElement.audioElement.currentTime = mediaTimeMs / 1000;
130
- await mediaElement.audioElement.play();
131
- }
132
- }
133
-
134
- /**
135
- * Deactivate an element (disconnect but keep prepared)
136
- */
137
- private async deactivateElement(
138
- mediaElement: any,
139
- sourceInfo: {
140
- mediaElementSource: MediaElementAudioSourceNode;
141
- connected: boolean;
142
- },
143
- ): Promise<void> {
144
- sourceInfo.mediaElementSource.disconnect();
145
- sourceInfo.connected = false;
146
-
147
- if (mediaElement.audioElement) {
148
- mediaElement.audioElement.pause();
149
- }
150
- }
151
-
152
- /**
153
- * Clean up elements that are far in the past
154
- */
155
- private cleanupOldElements(currentMs: number): void {
156
- const cleanupThresholdMs = currentMs - this.lookaheadMs;
157
-
158
- for (const [
159
- mediaElement,
160
- sourceInfo,
161
- ] of this.connectedMediaSources.entries()) {
162
- const endTime = mediaElement.endTimeMs;
163
-
164
- if (endTime < cleanupThresholdMs) {
165
- if (sourceInfo.connected) {
166
- sourceInfo.mediaElementSource.disconnect();
167
- }
168
- this.connectedMediaSources.delete(mediaElement);
169
- }
170
- }
171
- }
172
-
173
- /**
174
- * Clear all connected media sources (for cleanup)
175
- */
176
- clearAll(): void {
177
- for (const [, sourceInfo] of this.connectedMediaSources.entries()) {
178
- try {
179
- if (sourceInfo.connected) {
180
- sourceInfo.mediaElementSource.disconnect();
181
- }
182
- } catch (_error) {
183
- // Ignore cleanup errors
184
- }
185
- }
186
- this.connectedMediaSources.clear();
187
- }
188
-
189
- /**
190
- * Get connection status for testing/debugging
191
- */
192
- getConnectionInfo(): { total: number; connected: number; prepared: number } {
193
- let connected = 0;
194
- let prepared = 0;
195
-
196
- for (const [, sourceInfo] of this.connectedMediaSources.entries()) {
197
- if (sourceInfo.connected) {
198
- connected++;
199
- } else {
200
- prepared++;
201
- }
202
- }
203
-
204
- return {
205
- total: this.connectedMediaSources.size,
206
- connected,
207
- prepared,
208
- };
209
- }
210
-
211
- /**
212
- * Set lookahead time
213
- */
214
- setLookaheadMs(lookaheadMs: number): void {
215
- this.lookaheadMs = lookaheadMs;
216
- }
217
-
218
- /**
219
- * Get current lookahead time
220
- */
221
- getLookaheadMs(): number {
222
- return this.lookaheadMs;
223
- }
224
- }