@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,290 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Manages playback timing, AudioContext lifecycle, and timeline synchronization
|
|
3
|
-
* Extracted from ContextMixin to improve separation of concerns and testability
|
|
4
|
-
*/
|
|
5
|
-
var PlaybackController = class {
|
|
6
|
-
constructor(options = {}) {
|
|
7
|
-
this.playbackAudioContext = null;
|
|
8
|
-
this.animationFrameRequest = null;
|
|
9
|
-
this.playing = false;
|
|
10
|
-
this.currentTimeMs = 0;
|
|
11
|
-
this.audioStartTime = 0;
|
|
12
|
-
this.playbackStartTimeMs = 0;
|
|
13
|
-
this.activeChunks = /* @__PURE__ */ new Map();
|
|
14
|
-
this.chunkDurationMs = 4e3;
|
|
15
|
-
this.lookaheadChunks = 2;
|
|
16
|
-
this.currentChunkIndex = 0;
|
|
17
|
-
this.renderingChunks = /* @__PURE__ */ new Set();
|
|
18
|
-
this.options = {
|
|
19
|
-
fps: 30,
|
|
20
|
-
onTimeUpdate: () => {},
|
|
21
|
-
onPlayStateChange: () => {},
|
|
22
|
-
onError: () => {},
|
|
23
|
-
...options
|
|
24
|
-
};
|
|
25
|
-
this.msPerFrame = 1e3 / this.options.fps;
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* Start playback for the given timegroup
|
|
29
|
-
*/
|
|
30
|
-
async startPlayback(timegroup, fromMs) {
|
|
31
|
-
await this.stopPlayback();
|
|
32
|
-
if (!timegroup) {
|
|
33
|
-
this.setPlaying(false);
|
|
34
|
-
this.options.onPlayStateChange(false);
|
|
35
|
-
this.options.onError(/* @__PURE__ */ new Error("No timegroup provided"));
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
await timegroup.waitForMediaDurations?.();
|
|
39
|
-
const currentMs = fromMs ?? timegroup.currentTimeMs ?? 0;
|
|
40
|
-
const toMs = timegroup.endTimeMs;
|
|
41
|
-
if (currentMs >= toMs) {
|
|
42
|
-
this.setPlaying(false);
|
|
43
|
-
this.options.onPlayStateChange(false);
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
try {
|
|
47
|
-
this.playbackAudioContext = new AudioContext({ latencyHint: "playback" });
|
|
48
|
-
if (this.playbackAudioContext.state === "suspended") {
|
|
49
|
-
console.warn("AudioContext is suspended, attempting to resume...");
|
|
50
|
-
try {
|
|
51
|
-
await Promise.race([this.playbackAudioContext.resume(), new Promise((_, reject) => setTimeout(() => reject(/* @__PURE__ */ new Error("AudioContext resume timeout")), 2e3))]);
|
|
52
|
-
} catch (error) {
|
|
53
|
-
console.warn("AudioContext resume failed:", error);
|
|
54
|
-
}
|
|
55
|
-
} else await this.playbackAudioContext.resume();
|
|
56
|
-
this.audioStartTime = this.playbackAudioContext.currentTime;
|
|
57
|
-
this.playbackStartTimeMs = currentMs;
|
|
58
|
-
this.currentChunkIndex = Math.floor(currentMs / this.chunkDurationMs);
|
|
59
|
-
await this.startProgressivePlayback(timegroup, currentMs, toMs);
|
|
60
|
-
if (this.isAudioContextReady()) {
|
|
61
|
-
this.setPlaying(true);
|
|
62
|
-
this.currentTimeMs = currentMs;
|
|
63
|
-
this.options.onTimeUpdate(currentMs);
|
|
64
|
-
this.syncPlayheadToAudioBuffer(timegroup, currentMs);
|
|
65
|
-
} else {
|
|
66
|
-
this.setPlaying(false);
|
|
67
|
-
this.options.onPlayStateChange(false);
|
|
68
|
-
console.warn("AudioContext not ready for playback, state:", this.playbackAudioContext?.state);
|
|
69
|
-
}
|
|
70
|
-
} catch (error) {
|
|
71
|
-
console.error("🎵 [PLAYBACK_ERROR] Failed to setup progressive audio playback:", error);
|
|
72
|
-
this.setPlaying(false);
|
|
73
|
-
this.options.onPlayStateChange(false);
|
|
74
|
-
this.options.onError(error);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
/**
|
|
78
|
-
* Stop playback and clean up resources
|
|
79
|
-
*/
|
|
80
|
-
async stopPlayback() {
|
|
81
|
-
for (const [_chunkIndex, bufferSource] of this.activeChunks.entries()) try {
|
|
82
|
-
bufferSource.stop();
|
|
83
|
-
} catch (_error) {}
|
|
84
|
-
this.activeChunks.clear();
|
|
85
|
-
this.renderingChunks.clear();
|
|
86
|
-
if (this.playbackAudioContext) {
|
|
87
|
-
if (this.playbackAudioContext.state !== "closed") await this.playbackAudioContext.close();
|
|
88
|
-
}
|
|
89
|
-
if (this.animationFrameRequest) {
|
|
90
|
-
cancelAnimationFrame(this.animationFrameRequest);
|
|
91
|
-
this.animationFrameRequest = null;
|
|
92
|
-
}
|
|
93
|
-
this.playbackAudioContext = null;
|
|
94
|
-
this.setPlaying(false);
|
|
95
|
-
}
|
|
96
|
-
/**
|
|
97
|
-
* Pause playback (can be resumed)
|
|
98
|
-
*/
|
|
99
|
-
async pausePlayback() {
|
|
100
|
-
if (this.playbackAudioContext && this.playbackAudioContext.state === "running") try {
|
|
101
|
-
await Promise.race([this.playbackAudioContext.suspend(), new Promise((_, reject) => setTimeout(() => reject(/* @__PURE__ */ new Error("AudioContext suspend timeout")), 2e3))]);
|
|
102
|
-
} catch (error) {
|
|
103
|
-
console.warn("AudioContext suspend failed:", error);
|
|
104
|
-
}
|
|
105
|
-
if (this.animationFrameRequest) {
|
|
106
|
-
cancelAnimationFrame(this.animationFrameRequest);
|
|
107
|
-
this.animationFrameRequest = null;
|
|
108
|
-
}
|
|
109
|
-
this.setPlaying(false);
|
|
110
|
-
}
|
|
111
|
-
/**
|
|
112
|
-
* Resume paused playback
|
|
113
|
-
*/
|
|
114
|
-
async resumePlayback() {
|
|
115
|
-
if (this.playbackAudioContext && this.playbackAudioContext.state === "suspended") {
|
|
116
|
-
try {
|
|
117
|
-
await Promise.race([this.playbackAudioContext.resume(), new Promise((_, reject) => setTimeout(() => reject(/* @__PURE__ */ new Error("AudioContext resume timeout")), 2e3))]);
|
|
118
|
-
} catch (error) {
|
|
119
|
-
console.warn("AudioContext resume failed:", error);
|
|
120
|
-
}
|
|
121
|
-
this.setPlaying(true);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
/**
|
|
125
|
-
* Seek to a specific time (restarts progressive playback from new position)
|
|
126
|
-
*/
|
|
127
|
-
async seekTo(timeMs, timegroup) {
|
|
128
|
-
this.currentTimeMs = timeMs;
|
|
129
|
-
this.options.onTimeUpdate(timeMs);
|
|
130
|
-
if (this.playing && timegroup) {
|
|
131
|
-
for (const bufferSource of this.activeChunks.values()) try {
|
|
132
|
-
bufferSource.stop();
|
|
133
|
-
} catch (_error) {}
|
|
134
|
-
this.activeChunks.clear();
|
|
135
|
-
this.renderingChunks.clear();
|
|
136
|
-
this.audioStartTime = this.playbackAudioContext?.currentTime ?? 0;
|
|
137
|
-
this.playbackStartTimeMs = timeMs;
|
|
138
|
-
this.currentChunkIndex = Math.floor(timeMs / this.chunkDurationMs);
|
|
139
|
-
await this.startProgressivePlayback(timegroup, timeMs, timegroup.endTimeMs);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
/**
|
|
143
|
-
* Internal method to sync playhead with unified audio buffer timing
|
|
144
|
-
*/
|
|
145
|
-
syncPlayheadToAudioBuffer(timegroup, startMs) {
|
|
146
|
-
if (!this.playbackAudioContext || !this.playing) return;
|
|
147
|
-
const elapsedAudioTime = this.playbackAudioContext.currentTime - this.audioStartTime;
|
|
148
|
-
const rawTimeMs = startMs + elapsedAudioTime * 1e3;
|
|
149
|
-
const nextTimeMs = Math.round(rawTimeMs / this.msPerFrame) * this.msPerFrame;
|
|
150
|
-
if (nextTimeMs !== this.currentTimeMs) {
|
|
151
|
-
this.currentTimeMs = nextTimeMs;
|
|
152
|
-
this.options.onTimeUpdate(nextTimeMs);
|
|
153
|
-
if (timegroup && timegroup.currentTimeMs !== nextTimeMs) timegroup.currentTimeMs = nextTimeMs;
|
|
154
|
-
this.updateProgressiveChunks(timegroup, nextTimeMs, timegroup.endTimeMs);
|
|
155
|
-
}
|
|
156
|
-
this.animationFrameRequest = requestAnimationFrame(() => {
|
|
157
|
-
this.syncPlayheadToAudioBuffer(timegroup, startMs);
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
/**
|
|
161
|
-
* Update playing state and notify observers
|
|
162
|
-
*/
|
|
163
|
-
setPlaying(playing) {
|
|
164
|
-
if (this.playing !== playing) {
|
|
165
|
-
this.playing = playing;
|
|
166
|
-
this.options.onPlayStateChange(playing);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
/**
|
|
170
|
-
* Get current playback state
|
|
171
|
-
*/
|
|
172
|
-
isPlaying() {
|
|
173
|
-
return this.playing;
|
|
174
|
-
}
|
|
175
|
-
/**
|
|
176
|
-
* Get current time
|
|
177
|
-
*/
|
|
178
|
-
getCurrentTime() {
|
|
179
|
-
return this.currentTimeMs;
|
|
180
|
-
}
|
|
181
|
-
/**
|
|
182
|
-
* Get current AudioContext
|
|
183
|
-
*/
|
|
184
|
-
getAudioContext() {
|
|
185
|
-
return this.playbackAudioContext;
|
|
186
|
-
}
|
|
187
|
-
/**
|
|
188
|
-
* Check if AudioContext is ready
|
|
189
|
-
*/
|
|
190
|
-
isAudioContextReady() {
|
|
191
|
-
return this.playbackAudioContext != null && this.playbackAudioContext.state !== "closed";
|
|
192
|
-
}
|
|
193
|
-
/**
|
|
194
|
-
* Get playback statistics for debugging
|
|
195
|
-
*/
|
|
196
|
-
getPlaybackInfo() {
|
|
197
|
-
return {
|
|
198
|
-
playing: this.playing,
|
|
199
|
-
currentTimeMs: this.currentTimeMs,
|
|
200
|
-
audioContextState: this.playbackAudioContext?.state || null,
|
|
201
|
-
hasElementManager: false
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
|
-
/**
|
|
205
|
-
* Update playback options
|
|
206
|
-
*/
|
|
207
|
-
updateOptions(options) {
|
|
208
|
-
Object.assign(this.options, options);
|
|
209
|
-
if (options.fps) this.msPerFrame = 1e3 / options.fps;
|
|
210
|
-
}
|
|
211
|
-
/**
|
|
212
|
-
* Start progressive chunk rendering and playback
|
|
213
|
-
*/
|
|
214
|
-
async startProgressivePlayback(timegroup, fromMs, _toMs) {
|
|
215
|
-
const firstChunkIndex = Math.floor(fromMs / this.chunkDurationMs);
|
|
216
|
-
const firstChunkStart = firstChunkIndex * this.chunkDurationMs;
|
|
217
|
-
const offsetInChunk = fromMs - firstChunkStart;
|
|
218
|
-
await this.renderAndScheduleChunk(timegroup, firstChunkStart, firstChunkIndex, offsetInChunk);
|
|
219
|
-
}
|
|
220
|
-
/**
|
|
221
|
-
* Render and schedule a single audio chunk
|
|
222
|
-
*/
|
|
223
|
-
async renderAndScheduleChunk(timegroup, chunkStartMs, chunkIndex, offsetInChunk = 0) {
|
|
224
|
-
if (this.renderingChunks.has(chunkIndex) || this.activeChunks.has(chunkIndex)) return;
|
|
225
|
-
this.renderingChunks.add(chunkIndex);
|
|
226
|
-
try {
|
|
227
|
-
const chunkEndMs = chunkStartMs + this.chunkDurationMs;
|
|
228
|
-
const chunkBuffer = await timegroup.renderAudio(chunkStartMs, chunkEndMs);
|
|
229
|
-
const bufferSource = this.playbackAudioContext?.createBufferSource();
|
|
230
|
-
if (!bufferSource || !this.playbackAudioContext?.destination) throw new Error("Audio context or buffer source not available");
|
|
231
|
-
bufferSource.buffer = chunkBuffer;
|
|
232
|
-
bufferSource.connect(this.playbackAudioContext.destination);
|
|
233
|
-
const chunkTimelineStartMs = chunkIndex * this.chunkDurationMs;
|
|
234
|
-
const relativeDelayMs = Math.max(0, chunkTimelineStartMs - this.playbackStartTimeMs);
|
|
235
|
-
const chunkStartTime = this.audioStartTime + relativeDelayMs / 1e3;
|
|
236
|
-
const startOffset = offsetInChunk / 1e3;
|
|
237
|
-
const now = this.playbackAudioContext?.currentTime ?? 0;
|
|
238
|
-
if (chunkStartTime <= now) console.warn(`🎵 [CHUNK_TIMING_WARNING] Chunk ${chunkIndex} scheduled in the past! startTime=${chunkStartTime.toFixed(3)}s, currentTime=${now.toFixed(3)}s`);
|
|
239
|
-
bufferSource.start(chunkStartTime, startOffset);
|
|
240
|
-
this.activeChunks.set(chunkIndex, bufferSource);
|
|
241
|
-
bufferSource.onended = () => {
|
|
242
|
-
this.activeChunks.delete(chunkIndex);
|
|
243
|
-
};
|
|
244
|
-
} catch (error) {
|
|
245
|
-
console.error(`🎵 [CHUNK_ERROR] Failed to render chunk ${chunkIndex}:`, error);
|
|
246
|
-
} finally {
|
|
247
|
-
this.renderingChunks.delete(chunkIndex);
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
/**
|
|
251
|
-
* Update chunk rendering as playhead advances - now handles all chunk management
|
|
252
|
-
*/
|
|
253
|
-
updateProgressiveChunks(timegroup, currentTimeMs, maxTimeMs) {
|
|
254
|
-
const newChunkIndex = Math.floor(currentTimeMs / this.chunkDurationMs);
|
|
255
|
-
if (newChunkIndex !== this.currentChunkIndex) {
|
|
256
|
-
this.currentChunkIndex = newChunkIndex;
|
|
257
|
-
this.cleanupOldChunks();
|
|
258
|
-
}
|
|
259
|
-
this.ensureChunksAhead(timegroup, maxTimeMs);
|
|
260
|
-
}
|
|
261
|
-
/**
|
|
262
|
-
* Systematically ensure chunks are ready ahead of current playback (synchronous)
|
|
263
|
-
*/
|
|
264
|
-
ensureChunksAhead(timegroup, maxTimeMs) {
|
|
265
|
-
for (let i = 1; i <= this.lookaheadChunks; i++) {
|
|
266
|
-
const targetChunkIndex = this.currentChunkIndex + i;
|
|
267
|
-
const targetChunkStartMs = targetChunkIndex * this.chunkDurationMs;
|
|
268
|
-
if (targetChunkStartMs >= maxTimeMs) break;
|
|
269
|
-
if (!this.renderingChunks.has(targetChunkIndex) && !this.activeChunks.has(targetChunkIndex)) {
|
|
270
|
-
const offsetInChunk = 0;
|
|
271
|
-
this.renderAndScheduleChunk(timegroup, targetChunkStartMs, targetChunkIndex, offsetInChunk).catch((error) => {
|
|
272
|
-
console.error(`🎵 [ENSURE_CHUNKS_ERROR] Failed to render chunk ${targetChunkIndex}:`, error);
|
|
273
|
-
});
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
/**
|
|
278
|
-
* Clean up chunks that are behind the current playhead
|
|
279
|
-
*/
|
|
280
|
-
cleanupOldChunks() {
|
|
281
|
-
const cutoffChunkIndex = this.currentChunkIndex - 1;
|
|
282
|
-
for (const [chunkIndex, bufferSource] of this.activeChunks.entries()) if (chunkIndex < cutoffChunkIndex) {
|
|
283
|
-
try {
|
|
284
|
-
bufferSource.stop();
|
|
285
|
-
} catch (_error) {}
|
|
286
|
-
this.activeChunks.delete(chunkIndex);
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
};
|
|
290
|
-
export { PlaybackController };
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
export interface MediaSourceManagerOptions {
|
|
2
|
-
onError?: (error: Error) => void;
|
|
3
|
-
onReady?: () => void;
|
|
4
|
-
onUpdateEnd?: () => void;
|
|
5
|
-
timeout?: number;
|
|
6
|
-
}
|
|
7
|
-
/**
|
|
8
|
-
* Manages MediaSource for audio streaming
|
|
9
|
-
*/
|
|
10
|
-
export declare class MediaSourceManager {
|
|
11
|
-
private mediaSource;
|
|
12
|
-
private audioElement;
|
|
13
|
-
private sourceBuffer;
|
|
14
|
-
private mediaSourceReady;
|
|
15
|
-
private pendingSegments;
|
|
16
|
-
private options;
|
|
17
|
-
constructor(options?: MediaSourceManagerOptions);
|
|
18
|
-
/**
|
|
19
|
-
* Initialize MediaSource for audio streaming
|
|
20
|
-
*/
|
|
21
|
-
initialize(): Promise<void>;
|
|
22
|
-
/**
|
|
23
|
-
* Create SourceBuffer with codec fallback
|
|
24
|
-
*/
|
|
25
|
-
private createSourceBuffer;
|
|
26
|
-
/**
|
|
27
|
-
* Setup SourceBuffer event listeners
|
|
28
|
-
*/
|
|
29
|
-
private setupSourceBufferListeners;
|
|
30
|
-
/**
|
|
31
|
-
* Feed audio segments directly to MediaSource SourceBuffer
|
|
32
|
-
*/
|
|
33
|
-
feedSegment(segmentBuffer: ArrayBuffer): Promise<void>;
|
|
34
|
-
/**
|
|
35
|
-
* Process any queued segments when SourceBuffer becomes available
|
|
36
|
-
*/
|
|
37
|
-
private processPendingSegments;
|
|
38
|
-
/**
|
|
39
|
-
* Log debug information for troubleshooting
|
|
40
|
-
*/
|
|
41
|
-
private logDebugInfo;
|
|
42
|
-
/**
|
|
43
|
-
* Set audio element current time
|
|
44
|
-
*/
|
|
45
|
-
setCurrentTime(timeMs: number): void;
|
|
46
|
-
/**
|
|
47
|
-
* Get the audio element for MediaElementSource
|
|
48
|
-
*/
|
|
49
|
-
getAudioElement(): HTMLAudioElement | null;
|
|
50
|
-
/**
|
|
51
|
-
* Check if MediaSource is ready
|
|
52
|
-
*/
|
|
53
|
-
isReady(): boolean;
|
|
54
|
-
/**
|
|
55
|
-
* Get buffered time ranges
|
|
56
|
-
*/
|
|
57
|
-
getBuffered(): TimeRanges | null;
|
|
58
|
-
/**
|
|
59
|
-
* Clean up MediaSource resources
|
|
60
|
-
*/
|
|
61
|
-
cleanup(_preserveCache?: boolean): void;
|
|
62
|
-
}
|
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Manages MediaSource for audio streaming
|
|
3
|
-
*/
|
|
4
|
-
var MediaSourceManager = class {
|
|
5
|
-
constructor(options = {}) {
|
|
6
|
-
this.mediaSource = null;
|
|
7
|
-
this.audioElement = null;
|
|
8
|
-
this.sourceBuffer = null;
|
|
9
|
-
this.mediaSourceReady = false;
|
|
10
|
-
this.pendingSegments = [];
|
|
11
|
-
this.options = {
|
|
12
|
-
timeout: 1e4,
|
|
13
|
-
...options
|
|
14
|
-
};
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* Initialize MediaSource for audio streaming
|
|
18
|
-
*/
|
|
19
|
-
async initialize() {
|
|
20
|
-
this.cleanup(true);
|
|
21
|
-
this.mediaSource = new MediaSource();
|
|
22
|
-
this.audioElement = document.createElement("audio");
|
|
23
|
-
this.audioElement.addEventListener("error", (event) => {
|
|
24
|
-
const error = this.audioElement?.error;
|
|
25
|
-
console.error("🎵 [AUDIO_ELEMENT_ERROR] Audio element error:", {
|
|
26
|
-
code: error?.code,
|
|
27
|
-
message: error?.message,
|
|
28
|
-
event
|
|
29
|
-
});
|
|
30
|
-
if (this.options.onError) this.options.onError(/* @__PURE__ */ new Error(`Audio element error: ${error?.message}`));
|
|
31
|
-
});
|
|
32
|
-
this.audioElement.src = URL.createObjectURL(this.mediaSource);
|
|
33
|
-
return new Promise((resolve, reject) => {
|
|
34
|
-
this.mediaSource?.addEventListener("sourceopen", () => {
|
|
35
|
-
try {
|
|
36
|
-
const sourceBuffer = this.createSourceBuffer();
|
|
37
|
-
if (!sourceBuffer) throw new Error("Failed to create SourceBuffer with any supported codec");
|
|
38
|
-
this.sourceBuffer = sourceBuffer;
|
|
39
|
-
this.setupSourceBufferListeners();
|
|
40
|
-
this.mediaSourceReady = true;
|
|
41
|
-
if (this.options.onReady) this.options.onReady();
|
|
42
|
-
resolve();
|
|
43
|
-
} catch (error) {
|
|
44
|
-
console.error("🎵 [MEDIA_SOURCE_ERROR] Failed to create SourceBuffer:", error);
|
|
45
|
-
reject(error);
|
|
46
|
-
}
|
|
47
|
-
});
|
|
48
|
-
this.mediaSource?.addEventListener("error", (error) => {
|
|
49
|
-
console.error("🎵 [MEDIA_SOURCE_ERROR] MediaSource error:", error);
|
|
50
|
-
reject(error);
|
|
51
|
-
});
|
|
52
|
-
setTimeout(() => {
|
|
53
|
-
if (!this.mediaSourceReady) {
|
|
54
|
-
const timeoutError = /* @__PURE__ */ new Error("MediaSource failed to open within timeout");
|
|
55
|
-
console.error("🎵 [MEDIA_SOURCE_TIMEOUT] MediaSource initialization timeout");
|
|
56
|
-
reject(timeoutError);
|
|
57
|
-
}
|
|
58
|
-
}, 4e3);
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
/**
|
|
62
|
-
* Create SourceBuffer with codec fallback
|
|
63
|
-
*/
|
|
64
|
-
createSourceBuffer() {
|
|
65
|
-
const codecOptions = [
|
|
66
|
-
"audio/mp4; codecs=\"mp4a.40.2\"",
|
|
67
|
-
"audio/mp4; codecs=\"mp4a.40.5\"",
|
|
68
|
-
"audio/mp4"
|
|
69
|
-
];
|
|
70
|
-
let sourceBuffer;
|
|
71
|
-
let lastError;
|
|
72
|
-
for (const codec of codecOptions) try {
|
|
73
|
-
if (MediaSource.isTypeSupported(codec)) {
|
|
74
|
-
sourceBuffer = this.mediaSource?.addSourceBuffer(codec);
|
|
75
|
-
break;
|
|
76
|
-
}
|
|
77
|
-
} catch (error) {
|
|
78
|
-
console.error(`🎵 [CODEC_ERROR] Failed to create SourceBuffer with ${codec}:`, error);
|
|
79
|
-
lastError = error;
|
|
80
|
-
}
|
|
81
|
-
if (!sourceBuffer && lastError) throw new Error(`Failed to create SourceBuffer with any supported codec. Last error: ${lastError.message}`);
|
|
82
|
-
return sourceBuffer;
|
|
83
|
-
}
|
|
84
|
-
/**
|
|
85
|
-
* Setup SourceBuffer event listeners
|
|
86
|
-
*/
|
|
87
|
-
setupSourceBufferListeners() {
|
|
88
|
-
if (!this.sourceBuffer) return;
|
|
89
|
-
this.sourceBuffer.addEventListener("updateend", () => {
|
|
90
|
-
this.processPendingSegments();
|
|
91
|
-
if (this.options.onUpdateEnd) this.options.onUpdateEnd();
|
|
92
|
-
});
|
|
93
|
-
this.sourceBuffer.addEventListener("error", (event) => {
|
|
94
|
-
console.error("🎵 [SOURCE_BUFFER_EVENT_ERROR] SourceBuffer error event:", event);
|
|
95
|
-
if (this.options.onError) this.options.onError(/* @__PURE__ */ new Error("SourceBuffer error"));
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
/**
|
|
99
|
-
* Feed audio segments directly to MediaSource SourceBuffer
|
|
100
|
-
*/
|
|
101
|
-
async feedSegment(segmentBuffer) {
|
|
102
|
-
if (!this.mediaSourceReady || !this.sourceBuffer) {
|
|
103
|
-
this.pendingSegments.push(segmentBuffer);
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
if (this.sourceBuffer.updating) {
|
|
107
|
-
this.pendingSegments.push(segmentBuffer);
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
if (this.audioElement?.error) {
|
|
111
|
-
const error = this.audioElement.error;
|
|
112
|
-
console.error("🎵 [MEDIA_ELEMENT_ERROR] HTMLMediaElement error detected:", {
|
|
113
|
-
code: error.code,
|
|
114
|
-
message: error.message,
|
|
115
|
-
MEDIA_ERR_ABORTED: error.code === MediaError.MEDIA_ERR_ABORTED,
|
|
116
|
-
MEDIA_ERR_NETWORK: error.code === MediaError.MEDIA_ERR_NETWORK,
|
|
117
|
-
MEDIA_ERR_DECODE: error.code === MediaError.MEDIA_ERR_DECODE,
|
|
118
|
-
MEDIA_ERR_SRC_NOT_SUPPORTED: error.code === MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED
|
|
119
|
-
});
|
|
120
|
-
this.audioElement.load();
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
try {
|
|
124
|
-
this.sourceBuffer.appendBuffer(segmentBuffer);
|
|
125
|
-
} catch (error) {
|
|
126
|
-
console.error("🎵 [SOURCE_BUFFER_ERROR] Failed to append segment:", error);
|
|
127
|
-
this.logDebugInfo();
|
|
128
|
-
this.pendingSegments.push(segmentBuffer);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
/**
|
|
132
|
-
* Process any queued segments when SourceBuffer becomes available
|
|
133
|
-
*/
|
|
134
|
-
processPendingSegments() {
|
|
135
|
-
if (!this.sourceBuffer || this.sourceBuffer.updating || this.pendingSegments.length === 0) return;
|
|
136
|
-
const nextSegment = this.pendingSegments.shift();
|
|
137
|
-
if (nextSegment) this.feedSegment(nextSegment);
|
|
138
|
-
}
|
|
139
|
-
/**
|
|
140
|
-
* Log debug information for troubleshooting
|
|
141
|
-
*/
|
|
142
|
-
logDebugInfo() {
|
|
143
|
-
console.error("🎵 [SOURCE_BUFFER_DEBUG] SourceBuffer state:", {
|
|
144
|
-
updating: this.sourceBuffer?.updating,
|
|
145
|
-
buffered: this.sourceBuffer?.buffered ? Array.from({ length: this.sourceBuffer.buffered.length }, (_, i) => `${this.sourceBuffer?.buffered.start(i)}-${this.sourceBuffer?.buffered.end(i)}`) : [],
|
|
146
|
-
mode: this.sourceBuffer?.mode,
|
|
147
|
-
timestampOffset: this.sourceBuffer?.timestampOffset
|
|
148
|
-
});
|
|
149
|
-
console.error("🎵 [MEDIA_SOURCE_DEBUG] MediaSource state:", {
|
|
150
|
-
readyState: this.mediaSource?.readyState,
|
|
151
|
-
sourceBuffers: this.mediaSource?.sourceBuffers.length,
|
|
152
|
-
duration: this.mediaSource?.duration
|
|
153
|
-
});
|
|
154
|
-
console.error("🎵 [AUDIO_ELEMENT_DEBUG] Audio element state:", {
|
|
155
|
-
readyState: this.audioElement?.readyState,
|
|
156
|
-
networkState: this.audioElement?.networkState,
|
|
157
|
-
error: this.audioElement?.error?.code,
|
|
158
|
-
src: `${this.audioElement?.src.substring(0, 50)}...`
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
/**
|
|
162
|
-
* Set audio element current time
|
|
163
|
-
*/
|
|
164
|
-
setCurrentTime(timeMs) {
|
|
165
|
-
if (this.audioElement) this.audioElement.currentTime = timeMs / 1e3;
|
|
166
|
-
}
|
|
167
|
-
/**
|
|
168
|
-
* Get the audio element for MediaElementSource
|
|
169
|
-
*/
|
|
170
|
-
getAudioElement() {
|
|
171
|
-
return this.audioElement;
|
|
172
|
-
}
|
|
173
|
-
/**
|
|
174
|
-
* Check if MediaSource is ready
|
|
175
|
-
*/
|
|
176
|
-
isReady() {
|
|
177
|
-
return this.mediaSourceReady;
|
|
178
|
-
}
|
|
179
|
-
/**
|
|
180
|
-
* Get buffered time ranges
|
|
181
|
-
*/
|
|
182
|
-
getBuffered() {
|
|
183
|
-
return this.sourceBuffer?.buffered || null;
|
|
184
|
-
}
|
|
185
|
-
/**
|
|
186
|
-
* Clean up MediaSource resources
|
|
187
|
-
*/
|
|
188
|
-
cleanup(_preserveCache = false) {
|
|
189
|
-
if (this.sourceBuffer && this.mediaSource && this.mediaSource.readyState === "open") try {
|
|
190
|
-
this.mediaSource.removeSourceBuffer(this.sourceBuffer);
|
|
191
|
-
} catch (error) {
|
|
192
|
-
console.warn("🎵 [CLEANUP_ERROR] Error removing SourceBuffer:", error);
|
|
193
|
-
}
|
|
194
|
-
if (this.mediaSource) try {
|
|
195
|
-
if (this.mediaSource.readyState === "open") this.mediaSource.endOfStream();
|
|
196
|
-
} catch (error) {
|
|
197
|
-
console.warn("🎵 [CLEANUP_ERROR] Error ending MediaSource:", error);
|
|
198
|
-
}
|
|
199
|
-
if (this.audioElement) try {
|
|
200
|
-
URL.revokeObjectURL(this.audioElement.src);
|
|
201
|
-
} catch (error) {
|
|
202
|
-
console.warn("🎵 [CLEANUP_ERROR] Error revoking URL:", error);
|
|
203
|
-
}
|
|
204
|
-
this.mediaSource = null;
|
|
205
|
-
this.audioElement = null;
|
|
206
|
-
this.sourceBuffer = null;
|
|
207
|
-
this.mediaSourceReady = false;
|
|
208
|
-
this.pendingSegments = [];
|
|
209
|
-
}
|
|
210
|
-
};
|
|
211
|
-
export { MediaSourceManager };
|