@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.
- package/dist/elements/EFAudio.d.ts +1 -2
- package/dist/elements/EFAudio.js +6 -9
- package/dist/elements/EFMedia/AssetMediaEngine.browsertest.d.ts +0 -0
- package/dist/elements/EFMedia/AssetMediaEngine.d.ts +2 -4
- package/dist/elements/EFMedia/AssetMediaEngine.js +34 -5
- package/dist/elements/EFMedia/BaseMediaEngine.js +20 -1
- package/dist/elements/EFMedia/BufferedSeekingInput.d.ts +5 -5
- package/dist/elements/EFMedia/BufferedSeekingInput.js +27 -7
- package/dist/elements/EFMedia/JitMediaEngine.d.ts +1 -1
- package/dist/elements/EFMedia/JitMediaEngine.js +22 -3
- package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.js +4 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioInputTask.js +11 -3
- package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.chunkboundary.regression.browsertest.d.ts +0 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.js +17 -4
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.js +11 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.js +3 -2
- package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.js +4 -1
- package/dist/elements/EFMedia/shared/PrecisionUtils.d.ts +28 -0
- package/dist/elements/EFMedia/shared/PrecisionUtils.js +29 -0
- package/dist/elements/EFMedia/videoTasks/makeVideoSeekTask.js +11 -2
- package/dist/elements/EFMedia/videoTasks/makeVideoSegmentFetchTask.js +11 -1
- package/dist/elements/EFMedia/videoTasks/makeVideoSegmentIdTask.js +3 -2
- package/dist/elements/EFMedia.d.ts +0 -12
- package/dist/elements/EFMedia.js +4 -30
- package/dist/elements/EFTimegroup.js +12 -17
- package/dist/elements/EFVideo.d.ts +0 -9
- package/dist/elements/EFVideo.js +0 -7
- package/dist/elements/SampleBuffer.js +6 -6
- package/dist/getRenderInfo.d.ts +2 -2
- package/dist/gui/ContextMixin.js +71 -17
- package/dist/gui/TWMixin.js +1 -1
- package/dist/style.css +1 -1
- package/dist/transcoding/types/index.d.ts +9 -9
- package/package.json +2 -3
- package/src/elements/EFAudio.browsertest.ts +7 -7
- package/src/elements/EFAudio.ts +7 -20
- package/src/elements/EFMedia/AssetMediaEngine.browsertest.ts +100 -0
- package/src/elements/EFMedia/AssetMediaEngine.ts +72 -7
- package/src/elements/EFMedia/BaseMediaEngine.ts +50 -1
- package/src/elements/EFMedia/BufferedSeekingInput.browsertest.ts +135 -54
- package/src/elements/EFMedia/BufferedSeekingInput.ts +74 -17
- package/src/elements/EFMedia/JitMediaEngine.ts +58 -2
- package/src/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.ts +10 -1
- package/src/elements/EFMedia/audioTasks/makeAudioInputTask.ts +16 -8
- package/src/elements/EFMedia/audioTasks/makeAudioSeekTask.chunkboundary.regression.browsertest.ts +199 -0
- package/src/elements/EFMedia/audioTasks/makeAudioSeekTask.ts +35 -4
- package/src/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.ts +12 -1
- package/src/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.ts +3 -2
- package/src/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.ts +10 -1
- package/src/elements/EFMedia/shared/PrecisionUtils.ts +46 -0
- package/src/elements/EFMedia/videoTasks/makeVideoSeekTask.ts +27 -3
- package/src/elements/EFMedia/videoTasks/makeVideoSegmentFetchTask.ts +12 -1
- package/src/elements/EFMedia/videoTasks/makeVideoSegmentIdTask.ts +3 -2
- package/src/elements/EFMedia.browsertest.ts +73 -33
- package/src/elements/EFMedia.ts +11 -54
- package/src/elements/EFTimegroup.ts +21 -26
- package/src/elements/EFVideo.browsertest.ts +895 -162
- package/src/elements/EFVideo.ts +0 -16
- package/src/elements/SampleBuffer.ts +8 -10
- package/src/gui/ContextMixin.ts +104 -26
- package/src/transcoding/types/index.ts +10 -6
- package/test/EFVideo.framegen.browsertest.ts +1 -1
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- package/test/__cache__/GET__api_v1_transcode_manifest_json_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__3be92a0437de726b431ed5af2369158a/data.bin +1 -1
- package/test/__cache__/GET__api_v1_transcode_manifest_json_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__3be92a0437de726b431ed5af2369158a/metadata.json +4 -4
- package/test/recordReplayProxyPlugin.js +50 -0
- package/types.json +1 -1
- package/dist/DecoderResetFrequency.test.d.ts +0 -1
- package/dist/DecoderResetRecovery.test.d.ts +0 -1
- package/dist/ScrubTrackManager.d.ts +0 -96
- package/dist/elements/EFMedia/services/AudioElementFactory.browsertest.d.ts +0 -1
- package/dist/elements/EFMedia/services/AudioElementFactory.d.ts +0 -22
- package/dist/elements/EFMedia/services/AudioElementFactory.js +0 -72
- package/dist/elements/EFMedia/services/MediaSourceService.browsertest.d.ts +0 -1
- package/dist/elements/EFMedia/services/MediaSourceService.d.ts +0 -47
- package/dist/elements/EFMedia/services/MediaSourceService.js +0 -73
- package/dist/gui/services/ElementConnectionManager.browsertest.d.ts +0 -1
- package/dist/gui/services/ElementConnectionManager.d.ts +0 -59
- package/dist/gui/services/ElementConnectionManager.js +0 -128
- package/dist/gui/services/PlaybackController.browsertest.d.ts +0 -1
- package/dist/gui/services/PlaybackController.d.ts +0 -103
- package/dist/gui/services/PlaybackController.js +0 -290
- package/dist/services/MediaSourceManager.d.ts +0 -62
- package/dist/services/MediaSourceManager.js +0 -211
- package/src/elements/EFMedia/services/AudioElementFactory.browsertest.ts +0 -325
- package/src/elements/EFMedia/services/AudioElementFactory.ts +0 -119
- package/src/elements/EFMedia/services/MediaSourceService.browsertest.ts +0 -257
- package/src/elements/EFMedia/services/MediaSourceService.ts +0 -102
- package/src/gui/services/ElementConnectionManager.browsertest.ts +0 -263
- package/src/gui/services/ElementConnectionManager.ts +0 -224
- package/src/gui/services/PlaybackController.browsertest.ts +0 -437
- package/src/gui/services/PlaybackController.ts +0 -521
- 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
|
-
}
|