@editframe/elements 0.17.6-beta.0 → 0.18.3-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/EF_FRAMEGEN.js +1 -1
- package/dist/ScrubTrackManager.d.ts +2 -2
- package/dist/elements/EFAudio.d.ts +21 -2
- package/dist/elements/EFAudio.js +41 -11
- package/dist/elements/EFImage.d.ts +1 -0
- package/dist/elements/EFImage.js +11 -3
- package/dist/elements/EFMedia/AssetIdMediaEngine.d.ts +18 -0
- package/dist/elements/EFMedia/AssetIdMediaEngine.js +41 -0
- package/dist/elements/EFMedia/AssetMediaEngine.d.ts +47 -0
- package/dist/elements/EFMedia/AssetMediaEngine.js +116 -0
- package/dist/elements/EFMedia/BaseMediaEngine.d.ts +55 -0
- package/dist/elements/EFMedia/BaseMediaEngine.js +96 -0
- package/dist/elements/EFMedia/BufferedSeekingInput.d.ts +43 -0
- package/dist/elements/EFMedia/BufferedSeekingInput.js +159 -0
- package/dist/elements/EFMedia/JitMediaEngine.browsertest.d.ts +0 -0
- package/dist/elements/EFMedia/JitMediaEngine.d.ts +31 -0
- package/dist/elements/EFMedia/JitMediaEngine.js +62 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.browsertest.d.ts +9 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.d.ts +16 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.js +48 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.d.ts +3 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.js +138 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.browsertest.d.ts +9 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.d.ts +4 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.js +16 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioInputTask.browsertest.d.ts +9 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioInputTask.d.ts +3 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioInputTask.js +22 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.d.ts +7 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.js +24 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.d.ts +4 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.js +18 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.d.ts +4 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.js +16 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.d.ts +3 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.js +104 -0
- package/dist/elements/EFMedia/services/AudioElementFactory.d.ts +22 -0
- package/dist/elements/EFMedia/services/AudioElementFactory.js +72 -0
- package/dist/elements/EFMedia/services/MediaSourceService.browsertest.d.ts +1 -0
- package/dist/elements/EFMedia/services/MediaSourceService.d.ts +47 -0
- package/dist/elements/EFMedia/services/MediaSourceService.js +73 -0
- package/dist/elements/EFMedia/shared/AudioSpanUtils.d.ts +7 -0
- package/dist/elements/EFMedia/shared/AudioSpanUtils.js +54 -0
- package/dist/elements/EFMedia/shared/BufferUtils.d.ts +70 -0
- package/dist/elements/EFMedia/shared/BufferUtils.js +89 -0
- package/dist/elements/EFMedia/shared/MediaTaskUtils.d.ts +23 -0
- package/dist/elements/EFMedia/shared/RenditionHelpers.browsertest.d.ts +1 -0
- package/dist/elements/EFMedia/shared/RenditionHelpers.d.ts +19 -0
- package/dist/elements/EFMedia/tasks/makeMediaEngineTask.browsertest.d.ts +1 -0
- package/dist/elements/EFMedia/tasks/makeMediaEngineTask.d.ts +18 -0
- package/dist/elements/EFMedia/tasks/makeMediaEngineTask.js +60 -0
- package/dist/elements/EFMedia/tasks/makeMediaEngineTask.test.d.ts +1 -0
- package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.browsertest.d.ts +9 -0
- package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.d.ts +16 -0
- package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.js +46 -0
- package/dist/elements/EFMedia/videoTasks/makeVideoInitSegmentFetchTask.browsertest.d.ts +9 -0
- package/dist/elements/EFMedia/videoTasks/makeVideoInitSegmentFetchTask.d.ts +4 -0
- package/dist/elements/EFMedia/videoTasks/makeVideoInitSegmentFetchTask.js +16 -0
- package/dist/elements/EFMedia/videoTasks/makeVideoInputTask.browsertest.d.ts +9 -0
- package/dist/elements/EFMedia/videoTasks/makeVideoInputTask.d.ts +3 -0
- package/dist/elements/EFMedia/videoTasks/makeVideoInputTask.js +27 -0
- package/dist/elements/EFMedia/videoTasks/makeVideoSeekTask.d.ts +7 -0
- package/dist/elements/EFMedia/videoTasks/makeVideoSeekTask.js +25 -0
- package/dist/elements/EFMedia/videoTasks/makeVideoSegmentFetchTask.browsertest.d.ts +9 -0
- package/dist/elements/EFMedia/videoTasks/makeVideoSegmentFetchTask.d.ts +4 -0
- package/dist/elements/EFMedia/videoTasks/makeVideoSegmentFetchTask.js +18 -0
- package/dist/elements/EFMedia/videoTasks/makeVideoSegmentIdTask.browsertest.d.ts +9 -0
- package/dist/elements/EFMedia/videoTasks/makeVideoSegmentIdTask.d.ts +4 -0
- package/dist/elements/EFMedia/videoTasks/makeVideoSegmentIdTask.js +16 -0
- package/dist/elements/EFMedia.browsertest.d.ts +1 -0
- package/dist/elements/EFMedia.d.ts +75 -111
- package/dist/elements/EFMedia.js +141 -1111
- package/dist/elements/EFTemporal.d.ts +1 -1
- package/dist/elements/EFTemporal.js +1 -1
- package/dist/elements/EFTimegroup.d.ts +11 -0
- package/dist/elements/EFTimegroup.js +88 -13
- package/dist/elements/EFVideo.d.ts +60 -29
- package/dist/elements/EFVideo.js +103 -203
- package/dist/elements/EFWaveform.js +2 -2
- package/dist/elements/SampleBuffer.d.ts +14 -0
- package/dist/elements/SampleBuffer.js +52 -0
- package/dist/getRenderInfo.d.ts +2 -2
- package/dist/getRenderInfo.js +2 -1
- package/dist/gui/ContextMixin.js +17 -70
- package/dist/gui/EFFilmstrip.d.ts +3 -3
- package/dist/gui/EFFilmstrip.js +1 -1
- package/dist/gui/EFFitScale.d.ts +2 -2
- package/dist/gui/TWMixin.js +1 -1
- package/dist/gui/services/ElementConnectionManager.browsertest.d.ts +1 -0
- package/dist/gui/services/ElementConnectionManager.d.ts +59 -0
- package/dist/gui/services/ElementConnectionManager.js +128 -0
- package/dist/gui/services/PlaybackController.browsertest.d.ts +1 -0
- package/dist/gui/services/PlaybackController.d.ts +103 -0
- package/dist/gui/services/PlaybackController.js +290 -0
- package/dist/services/MediaSourceManager.d.ts +62 -0
- package/dist/services/MediaSourceManager.js +211 -0
- package/dist/style.css +1 -1
- package/dist/transcoding/cache/CacheManager.d.ts +73 -0
- package/dist/transcoding/cache/RequestDeduplicator.d.ts +29 -0
- package/dist/transcoding/cache/RequestDeduplicator.js +53 -0
- package/dist/transcoding/cache/RequestDeduplicator.test.d.ts +1 -0
- package/dist/transcoding/types/index.d.ts +242 -0
- package/dist/transcoding/utils/MediaUtils.d.ts +9 -0
- package/dist/transcoding/utils/UrlGenerator.d.ts +26 -0
- package/dist/transcoding/utils/UrlGenerator.js +45 -0
- package/dist/transcoding/utils/constants.d.ts +27 -0
- package/dist/utils/LRUCache.d.ts +34 -0
- package/dist/utils/LRUCache.js +115 -0
- package/package.json +3 -2
- package/src/elements/EFAudio.browsertest.ts +183 -43
- package/src/elements/EFAudio.ts +59 -13
- package/src/elements/EFImage.browsertest.ts +42 -0
- package/src/elements/EFImage.ts +23 -3
- package/src/elements/EFMedia/AssetIdMediaEngine.test.ts +222 -0
- package/src/elements/EFMedia/AssetIdMediaEngine.ts +70 -0
- package/src/elements/EFMedia/AssetMediaEngine.ts +210 -0
- package/src/elements/EFMedia/BaseMediaEngine.test.ts +164 -0
- package/src/elements/EFMedia/BaseMediaEngine.ts +170 -0
- package/src/elements/EFMedia/BufferedSeekingInput.browsertest.ts +400 -0
- package/src/elements/EFMedia/BufferedSeekingInput.ts +267 -0
- package/src/elements/EFMedia/JitMediaEngine.browsertest.ts +165 -0
- package/src/elements/EFMedia/JitMediaEngine.ts +110 -0
- package/src/elements/EFMedia/audioTasks/makeAudioBufferTask.browsertest.ts +554 -0
- package/src/elements/EFMedia/audioTasks/makeAudioBufferTask.ts +81 -0
- package/src/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.ts +241 -0
- package/src/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.browsertest.ts +59 -0
- package/src/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.ts +23 -0
- package/src/elements/EFMedia/audioTasks/makeAudioInputTask.browsertest.ts +55 -0
- package/src/elements/EFMedia/audioTasks/makeAudioInputTask.ts +35 -0
- package/src/elements/EFMedia/audioTasks/makeAudioSeekTask.ts +42 -0
- package/src/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.ts +34 -0
- package/src/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.ts +23 -0
- package/src/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.ts +174 -0
- package/src/elements/EFMedia/services/AudioElementFactory.browsertest.ts +325 -0
- package/src/elements/EFMedia/services/AudioElementFactory.ts +119 -0
- package/src/elements/EFMedia/services/MediaSourceService.browsertest.ts +257 -0
- package/src/elements/EFMedia/services/MediaSourceService.ts +102 -0
- package/src/elements/EFMedia/shared/AudioSpanUtils.ts +128 -0
- package/src/elements/EFMedia/shared/BufferUtils.ts +310 -0
- package/src/elements/EFMedia/shared/MediaTaskUtils.ts +44 -0
- package/src/elements/EFMedia/shared/RenditionHelpers.browsertest.ts +247 -0
- package/src/elements/EFMedia/shared/RenditionHelpers.ts +79 -0
- package/src/elements/EFMedia/tasks/makeMediaEngineTask.browsertest.ts +128 -0
- package/src/elements/EFMedia/tasks/makeMediaEngineTask.test.ts +233 -0
- package/src/elements/EFMedia/tasks/makeMediaEngineTask.ts +89 -0
- package/src/elements/EFMedia/videoTasks/makeVideoBufferTask.browsertest.ts +555 -0
- package/src/elements/EFMedia/videoTasks/makeVideoBufferTask.ts +79 -0
- package/src/elements/EFMedia/videoTasks/makeVideoInitSegmentFetchTask.browsertest.ts +59 -0
- package/src/elements/EFMedia/videoTasks/makeVideoInitSegmentFetchTask.ts +23 -0
- package/src/elements/EFMedia/videoTasks/makeVideoInputTask.browsertest.ts +55 -0
- package/src/elements/EFMedia/videoTasks/makeVideoInputTask.ts +45 -0
- package/src/elements/EFMedia/videoTasks/makeVideoSeekTask.ts +44 -0
- package/src/elements/EFMedia/videoTasks/makeVideoSegmentFetchTask.browsertest.ts +57 -0
- package/src/elements/EFMedia/videoTasks/makeVideoSegmentFetchTask.ts +32 -0
- package/src/elements/EFMedia/videoTasks/makeVideoSegmentIdTask.browsertest.ts +56 -0
- package/src/elements/EFMedia/videoTasks/makeVideoSegmentIdTask.ts +23 -0
- package/src/elements/EFMedia.browsertest.ts +658 -265
- package/src/elements/EFMedia.ts +173 -1763
- package/src/elements/EFTemporal.ts +3 -4
- package/src/elements/EFTimegroup.browsertest.ts +6 -3
- package/src/elements/EFTimegroup.ts +152 -21
- package/src/elements/EFVideo.browsertest.ts +115 -37
- package/src/elements/EFVideo.ts +123 -452
- package/src/elements/EFWaveform.ts +1 -1
- package/src/elements/MediaController.ts +2 -12
- package/src/elements/SampleBuffer.ts +97 -0
- package/src/gui/ContextMixin.ts +23 -104
- package/src/gui/services/ElementConnectionManager.browsertest.ts +263 -0
- package/src/gui/services/ElementConnectionManager.ts +224 -0
- package/src/gui/services/PlaybackController.browsertest.ts +437 -0
- package/src/gui/services/PlaybackController.ts +521 -0
- package/src/services/MediaSourceManager.ts +333 -0
- package/src/transcoding/cache/CacheManager.ts +208 -0
- package/src/transcoding/cache/RequestDeduplicator.test.ts +170 -0
- package/src/transcoding/cache/RequestDeduplicator.ts +65 -0
- package/src/transcoding/types/index.ts +265 -0
- package/src/transcoding/utils/MediaUtils.ts +63 -0
- package/src/transcoding/utils/UrlGenerator.ts +68 -0
- package/src/transcoding/utils/constants.ts +36 -0
- package/src/utils/LRUCache.ts +153 -0
- package/test/EFVideo.framegen.browsertest.ts +38 -29
- package/test/__cache__/GET__api_v1_transcode_audio_1_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__32da3954ba60c96ad732020c65a08ebc/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_audio_1_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__32da3954ba60c96ad732020c65a08ebc/metadata.json +21 -0
- package/test/__cache__/GET__api_v1_transcode_audio_2_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__b0b2b07efcf607de8ee0f650328c32f7/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_audio_2_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__b0b2b07efcf607de8ee0f650328c32f7/metadata.json +21 -0
- package/test/__cache__/GET__api_v1_transcode_audio_3_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__a75c2252b542e0c152c780e9a8d7b154/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_audio_3_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__a75c2252b542e0c152c780e9a8d7b154/metadata.json +21 -0
- package/test/__cache__/GET__api_v1_transcode_audio_4_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__a64ff1cfb1b52cae14df4b5dfa1e222b/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_audio_4_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__a64ff1cfb1b52cae14df4b5dfa1e222b/metadata.json +21 -0
- package/test/__cache__/GET__api_v1_transcode_audio_5_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__91e8a522f950809b9f09f4173113b4b0/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_audio_5_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__91e8a522f950809b9f09f4173113b4b0/metadata.json +21 -0
- package/test/__cache__/GET__api_v1_transcode_audio_init_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__e66d2c831d951e74ad0aeaa6489795d0/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_audio_init_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__e66d2c831d951e74ad0aeaa6489795d0/metadata.json +21 -0
- package/test/__cache__/GET__api_v1_transcode_high_1_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__26197f6f7c46cacb0a71134131c3f775/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_high_1_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__26197f6f7c46cacb0a71134131c3f775/metadata.json +21 -0
- package/test/__cache__/GET__api_v1_transcode_high_2_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__4cb6774cd3650ccf59c8f8dc6678c0b9/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_high_2_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__4cb6774cd3650ccf59c8f8dc6678c0b9/metadata.json +21 -0
- package/test/__cache__/GET__api_v1_transcode_high_3_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__0b3b2b1c8933f7fcf8a9ecaa88d58b41/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_high_3_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__0b3b2b1c8933f7fcf8a9ecaa88d58b41/metadata.json +21 -0
- package/test/__cache__/GET__api_v1_transcode_high_init_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__0798c479b44aaeef850609a430f6e613/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_high_init_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__0798c479b44aaeef850609a430f6e613/metadata.json +21 -0
- package/test/__cache__/GET__api_v1_transcode_manifest_json_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__3be92a0437de726b431ed5af2369158a/data.bin +1 -0
- package/test/__cache__/GET__api_v1_transcode_manifest_json_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__3be92a0437de726b431ed5af2369158a/metadata.json +19 -0
- package/test/createJitTestClips.ts +320 -188
- package/test/recordReplayProxyPlugin.js +302 -0
- package/test/useAssetMSW.ts +1 -1
- package/test/useMSW.ts +35 -22
- package/types.json +1 -1
- package/dist/JitTranscodingClient.d.ts +0 -167
- package/dist/JitTranscodingClient.js +0 -373
- package/dist/ScrubTrackManager.js +0 -216
- package/dist/elements/printTaskStatus.js +0 -11
- package/src/elements/__screenshots__/EFMedia.browsertest.ts/EFMedia-JIT-audio-playback-audioBufferTask-should-work-in-JIT-mode-without-URL-errors-1.png +0 -0
- package/test/EFVideo.frame-tasks.browsertest.ts +0 -524
- /package/dist/{JitTranscodingClient.browsertest.d.ts → elements/EFMedia/AssetIdMediaEngine.test.d.ts} +0 -0
- /package/dist/{JitTranscodingClient.test.d.ts → elements/EFMedia/BaseMediaEngine.test.d.ts} +0 -0
- /package/dist/{ScrubTrackIntegration.test.d.ts → elements/EFMedia/BufferedSeekingInput.browsertest.d.ts} +0 -0
- /package/dist/{SegmentSwitchLoading.test.d.ts → elements/EFMedia/services/AudioElementFactory.browsertest.d.ts} +0 -0
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
export interface PlaybackControllerOptions {
|
|
2
|
+
fps?: number;
|
|
3
|
+
onTimeUpdate?: (timeMs: number) => void;
|
|
4
|
+
onPlayStateChange?: (playing: boolean) => void;
|
|
5
|
+
onError?: (error: Error) => void;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Manages playback timing, AudioContext lifecycle, and timeline synchronization
|
|
10
|
+
* Extracted from ContextMixin to improve separation of concerns and testability
|
|
11
|
+
*/
|
|
12
|
+
export class PlaybackController {
|
|
13
|
+
private playbackAudioContext: AudioContext | null = null;
|
|
14
|
+
private animationFrameRequest: number | null = null;
|
|
15
|
+
private options: Required<PlaybackControllerOptions>;
|
|
16
|
+
private playing = false;
|
|
17
|
+
private currentTimeMs = 0;
|
|
18
|
+
private msPerFrame: number;
|
|
19
|
+
private audioStartTime = 0;
|
|
20
|
+
private playbackStartTimeMs = 0; // Track the actual timeline position where playback started
|
|
21
|
+
|
|
22
|
+
// Progressive chunking properties
|
|
23
|
+
private activeChunks = new Map<number, AudioBufferSourceNode>();
|
|
24
|
+
private chunkDurationMs = 4000; // 4-second chunks
|
|
25
|
+
private lookaheadChunks = 2; // Always have 2 chunks ready
|
|
26
|
+
private currentChunkIndex = 0;
|
|
27
|
+
private renderingChunks = new Set<number>(); // Track chunks being rendered
|
|
28
|
+
|
|
29
|
+
constructor(options: PlaybackControllerOptions = {}) {
|
|
30
|
+
this.options = {
|
|
31
|
+
fps: 30,
|
|
32
|
+
onTimeUpdate: () => {},
|
|
33
|
+
onPlayStateChange: () => {},
|
|
34
|
+
onError: () => {},
|
|
35
|
+
...options,
|
|
36
|
+
};
|
|
37
|
+
this.msPerFrame = 1000 / this.options.fps;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Start playback for the given timegroup
|
|
42
|
+
*/
|
|
43
|
+
async startPlayback(timegroup: any, fromMs?: number): Promise<void> {
|
|
44
|
+
await this.stopPlayback();
|
|
45
|
+
|
|
46
|
+
if (!timegroup) {
|
|
47
|
+
this.setPlaying(false);
|
|
48
|
+
this.options.onPlayStateChange(false); // Always notify on failed start attempt
|
|
49
|
+
this.options.onError(new Error("No timegroup provided"));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
await timegroup.waitForMediaDurations?.();
|
|
54
|
+
|
|
55
|
+
const currentMs = fromMs ?? timegroup.currentTimeMs ?? 0;
|
|
56
|
+
const toMs = timegroup.endTimeMs;
|
|
57
|
+
|
|
58
|
+
if (currentMs >= toMs) {
|
|
59
|
+
this.setPlaying(false);
|
|
60
|
+
this.options.onPlayStateChange(false); // Always notify on failed start attempt
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
this.playbackAudioContext = new AudioContext({
|
|
66
|
+
latencyHint: "playback",
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
if (this.playbackAudioContext.state === "suspended") {
|
|
70
|
+
console.warn("AudioContext is suspended, attempting to resume...");
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
// Add timeout for resume operation to prevent hanging in browser tests
|
|
74
|
+
await Promise.race([
|
|
75
|
+
this.playbackAudioContext.resume(),
|
|
76
|
+
new Promise((_, reject) =>
|
|
77
|
+
setTimeout(
|
|
78
|
+
() => reject(new Error("AudioContext resume timeout")),
|
|
79
|
+
2000,
|
|
80
|
+
),
|
|
81
|
+
),
|
|
82
|
+
]);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
console.warn("AudioContext resume failed:", error);
|
|
85
|
+
// Continue anyway - in test environments this is often expected
|
|
86
|
+
}
|
|
87
|
+
} else {
|
|
88
|
+
await this.playbackAudioContext.resume();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Initialize progressive playback
|
|
92
|
+
this.audioStartTime = this.playbackAudioContext.currentTime;
|
|
93
|
+
this.playbackStartTimeMs = currentMs; // Store where we started in the timeline
|
|
94
|
+
this.currentChunkIndex = Math.floor(currentMs / this.chunkDurationMs);
|
|
95
|
+
|
|
96
|
+
// Start progressive chunk rendering and playback
|
|
97
|
+
await this.startProgressivePlayback(timegroup, currentMs, toMs);
|
|
98
|
+
|
|
99
|
+
// Set playing state based on successful setup
|
|
100
|
+
// In browser test environments, AudioContext may remain suspended but playback should still be considered active
|
|
101
|
+
if (this.isAudioContextReady()) {
|
|
102
|
+
this.setPlaying(true);
|
|
103
|
+
// Ensure onTimeUpdate is called at least once during startup
|
|
104
|
+
this.currentTimeMs = currentMs;
|
|
105
|
+
this.options.onTimeUpdate(currentMs);
|
|
106
|
+
this.syncPlayheadToAudioBuffer(timegroup, currentMs);
|
|
107
|
+
} else {
|
|
108
|
+
this.setPlaying(false);
|
|
109
|
+
this.options.onPlayStateChange(false); // Always notify on failed start attempt
|
|
110
|
+
console.warn(
|
|
111
|
+
"AudioContext not ready for playback, state:",
|
|
112
|
+
this.playbackAudioContext?.state,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
} catch (error) {
|
|
116
|
+
console.error(
|
|
117
|
+
"🎵 [PLAYBACK_ERROR] Failed to setup progressive audio playback:",
|
|
118
|
+
error,
|
|
119
|
+
);
|
|
120
|
+
this.setPlaying(false);
|
|
121
|
+
this.options.onPlayStateChange(false); // Always notify on failed start attempt
|
|
122
|
+
this.options.onError(error as Error);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Stop playback and clean up resources
|
|
128
|
+
*/
|
|
129
|
+
async stopPlayback(): Promise<void> {
|
|
130
|
+
// Stop all active chunks
|
|
131
|
+
for (const [_chunkIndex, bufferSource] of this.activeChunks.entries()) {
|
|
132
|
+
try {
|
|
133
|
+
bufferSource.stop();
|
|
134
|
+
} catch (_error) {
|
|
135
|
+
// Ignore errors when stopping already stopped sources
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
this.activeChunks.clear();
|
|
139
|
+
this.renderingChunks.clear();
|
|
140
|
+
|
|
141
|
+
if (this.playbackAudioContext) {
|
|
142
|
+
if (this.playbackAudioContext.state !== "closed") {
|
|
143
|
+
await this.playbackAudioContext.close();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (this.animationFrameRequest) {
|
|
148
|
+
cancelAnimationFrame(this.animationFrameRequest);
|
|
149
|
+
this.animationFrameRequest = null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
this.playbackAudioContext = null;
|
|
153
|
+
this.setPlaying(false);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Pause playback (can be resumed)
|
|
158
|
+
*/
|
|
159
|
+
async pausePlayback(): Promise<void> {
|
|
160
|
+
if (
|
|
161
|
+
this.playbackAudioContext &&
|
|
162
|
+
this.playbackAudioContext.state === "running"
|
|
163
|
+
) {
|
|
164
|
+
try {
|
|
165
|
+
// Add timeout for suspend operation to prevent hanging in browser tests
|
|
166
|
+
await Promise.race([
|
|
167
|
+
this.playbackAudioContext.suspend(),
|
|
168
|
+
new Promise((_, reject) =>
|
|
169
|
+
setTimeout(
|
|
170
|
+
() => reject(new Error("AudioContext suspend timeout")),
|
|
171
|
+
2000,
|
|
172
|
+
),
|
|
173
|
+
),
|
|
174
|
+
]);
|
|
175
|
+
} catch (error) {
|
|
176
|
+
console.warn("AudioContext suspend failed:", error);
|
|
177
|
+
// Continue anyway - in test environments this is often expected
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (this.animationFrameRequest) {
|
|
182
|
+
cancelAnimationFrame(this.animationFrameRequest);
|
|
183
|
+
this.animationFrameRequest = null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
this.setPlaying(false);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Resume paused playback
|
|
191
|
+
*/
|
|
192
|
+
async resumePlayback(): Promise<void> {
|
|
193
|
+
if (
|
|
194
|
+
this.playbackAudioContext &&
|
|
195
|
+
this.playbackAudioContext.state === "suspended"
|
|
196
|
+
) {
|
|
197
|
+
try {
|
|
198
|
+
// Add timeout for resume operation to prevent hanging in browser tests
|
|
199
|
+
await Promise.race([
|
|
200
|
+
this.playbackAudioContext.resume(),
|
|
201
|
+
new Promise((_, reject) =>
|
|
202
|
+
setTimeout(
|
|
203
|
+
() => reject(new Error("AudioContext resume timeout")),
|
|
204
|
+
2000,
|
|
205
|
+
),
|
|
206
|
+
),
|
|
207
|
+
]);
|
|
208
|
+
} catch (error) {
|
|
209
|
+
console.warn("AudioContext resume failed:", error);
|
|
210
|
+
// Continue anyway - in test environments this is often expected
|
|
211
|
+
}
|
|
212
|
+
this.setPlaying(true);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Seek to a specific time (restarts progressive playback from new position)
|
|
218
|
+
*/
|
|
219
|
+
async seekTo(timeMs: number, timegroup?: any): Promise<void> {
|
|
220
|
+
this.currentTimeMs = timeMs;
|
|
221
|
+
this.options.onTimeUpdate(timeMs);
|
|
222
|
+
|
|
223
|
+
// For progressive chunks, we need to restart playback from the new position
|
|
224
|
+
if (this.playing && timegroup) {
|
|
225
|
+
// Stop current chunks
|
|
226
|
+
for (const bufferSource of this.activeChunks.values()) {
|
|
227
|
+
try {
|
|
228
|
+
bufferSource.stop();
|
|
229
|
+
} catch (_error) {
|
|
230
|
+
// Ignore stop errors
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
this.activeChunks.clear();
|
|
234
|
+
this.renderingChunks.clear();
|
|
235
|
+
|
|
236
|
+
// Restart progressive playback from new position with proper timing
|
|
237
|
+
this.audioStartTime = this.playbackAudioContext?.currentTime ?? 0;
|
|
238
|
+
this.playbackStartTimeMs = timeMs; // Set the new playback start position
|
|
239
|
+
this.currentChunkIndex = Math.floor(timeMs / this.chunkDurationMs);
|
|
240
|
+
|
|
241
|
+
await this.startProgressivePlayback(
|
|
242
|
+
timegroup,
|
|
243
|
+
timeMs,
|
|
244
|
+
timegroup.endTimeMs,
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Internal method to sync playhead with unified audio buffer timing
|
|
251
|
+
*/
|
|
252
|
+
private syncPlayheadToAudioBuffer(timegroup: any, startMs: number): void {
|
|
253
|
+
if (!this.playbackAudioContext || !this.playing) {
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const elapsedAudioTime =
|
|
258
|
+
this.playbackAudioContext.currentTime - this.audioStartTime;
|
|
259
|
+
const rawTimeMs = startMs + elapsedAudioTime * 1000;
|
|
260
|
+
const nextTimeMs =
|
|
261
|
+
Math.round(rawTimeMs / this.msPerFrame) * this.msPerFrame;
|
|
262
|
+
|
|
263
|
+
if (nextTimeMs !== this.currentTimeMs) {
|
|
264
|
+
this.currentTimeMs = nextTimeMs;
|
|
265
|
+
this.options.onTimeUpdate(nextTimeMs);
|
|
266
|
+
|
|
267
|
+
// Update the timegroup's currentTimeMs, which will automatically
|
|
268
|
+
// sync all child elements (video, animations, etc.)
|
|
269
|
+
if (timegroup && timegroup.currentTimeMs !== nextTimeMs) {
|
|
270
|
+
timegroup.currentTimeMs = nextTimeMs;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Update progressive chunks as playhead advances
|
|
274
|
+
this.updateProgressiveChunks(timegroup, nextTimeMs, timegroup.endTimeMs);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
this.animationFrameRequest = requestAnimationFrame(() => {
|
|
278
|
+
this.syncPlayheadToAudioBuffer(timegroup, startMs);
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Update playing state and notify observers
|
|
284
|
+
*/
|
|
285
|
+
private setPlaying(playing: boolean): void {
|
|
286
|
+
if (this.playing !== playing) {
|
|
287
|
+
this.playing = playing;
|
|
288
|
+
this.options.onPlayStateChange(playing);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Get current playback state
|
|
294
|
+
*/
|
|
295
|
+
isPlaying(): boolean {
|
|
296
|
+
return this.playing;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Get current time
|
|
301
|
+
*/
|
|
302
|
+
getCurrentTime(): number {
|
|
303
|
+
return this.currentTimeMs;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Get current AudioContext
|
|
308
|
+
*/
|
|
309
|
+
getAudioContext(): AudioContext | null {
|
|
310
|
+
return this.playbackAudioContext;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Check if AudioContext is ready
|
|
315
|
+
*/
|
|
316
|
+
isAudioContextReady(): boolean {
|
|
317
|
+
// In test environments, AudioContext might remain suspended, so be more lenient
|
|
318
|
+
return (
|
|
319
|
+
this.playbackAudioContext != null &&
|
|
320
|
+
this.playbackAudioContext.state !== "closed"
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Get playback statistics for debugging
|
|
326
|
+
*/
|
|
327
|
+
getPlaybackInfo(): {
|
|
328
|
+
playing: boolean;
|
|
329
|
+
currentTimeMs: number;
|
|
330
|
+
audioContextState: string | null;
|
|
331
|
+
hasElementManager: boolean;
|
|
332
|
+
} {
|
|
333
|
+
return {
|
|
334
|
+
playing: this.playing,
|
|
335
|
+
currentTimeMs: this.currentTimeMs,
|
|
336
|
+
audioContextState: this.playbackAudioContext?.state || null,
|
|
337
|
+
hasElementManager: false, // No longer tracking ElementConnectionManager
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Update playback options
|
|
343
|
+
*/
|
|
344
|
+
updateOptions(options: Partial<PlaybackControllerOptions>): void {
|
|
345
|
+
Object.assign(this.options, options);
|
|
346
|
+
if (options.fps) {
|
|
347
|
+
this.msPerFrame = 1000 / options.fps;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Start progressive chunk rendering and playback
|
|
353
|
+
*/
|
|
354
|
+
private async startProgressivePlayback(
|
|
355
|
+
timegroup: any,
|
|
356
|
+
fromMs: number,
|
|
357
|
+
_toMs: number,
|
|
358
|
+
): Promise<void> {
|
|
359
|
+
// Calculate the starting chunk and offset
|
|
360
|
+
const firstChunkIndex = Math.floor(fromMs / this.chunkDurationMs);
|
|
361
|
+
const firstChunkStart = firstChunkIndex * this.chunkDurationMs;
|
|
362
|
+
const offsetInChunk = fromMs - firstChunkStart;
|
|
363
|
+
|
|
364
|
+
// Render the first chunk immediately to ensure it starts playing right away
|
|
365
|
+
await this.renderAndScheduleChunk(
|
|
366
|
+
timegroup,
|
|
367
|
+
firstChunkStart,
|
|
368
|
+
firstChunkIndex,
|
|
369
|
+
offsetInChunk,
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Render and schedule a single audio chunk
|
|
375
|
+
*/
|
|
376
|
+
private async renderAndScheduleChunk(
|
|
377
|
+
timegroup: any,
|
|
378
|
+
chunkStartMs: number,
|
|
379
|
+
chunkIndex: number,
|
|
380
|
+
offsetInChunk = 0,
|
|
381
|
+
): Promise<void> {
|
|
382
|
+
if (
|
|
383
|
+
this.renderingChunks.has(chunkIndex) ||
|
|
384
|
+
this.activeChunks.has(chunkIndex)
|
|
385
|
+
) {
|
|
386
|
+
return; // Already rendering or scheduled
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
this.renderingChunks.add(chunkIndex);
|
|
390
|
+
|
|
391
|
+
try {
|
|
392
|
+
const chunkEndMs = chunkStartMs + this.chunkDurationMs;
|
|
393
|
+
|
|
394
|
+
// Render the chunk using the existing audio composition logic
|
|
395
|
+
const chunkBuffer = await timegroup.renderAudio(chunkStartMs, chunkEndMs);
|
|
396
|
+
|
|
397
|
+
// Create and schedule the buffer source
|
|
398
|
+
const bufferSource = this.playbackAudioContext?.createBufferSource();
|
|
399
|
+
if (!bufferSource || !this.playbackAudioContext?.destination) {
|
|
400
|
+
throw new Error("Audio context or buffer source not available");
|
|
401
|
+
}
|
|
402
|
+
bufferSource.buffer = chunkBuffer;
|
|
403
|
+
bufferSource.connect(this.playbackAudioContext.destination);
|
|
404
|
+
|
|
405
|
+
// Calculate precise timing: chunks should be scheduled relative to playback start
|
|
406
|
+
const chunkTimelineStartMs = chunkIndex * this.chunkDurationMs;
|
|
407
|
+
const relativeDelayMs = Math.max(
|
|
408
|
+
0,
|
|
409
|
+
chunkTimelineStartMs - this.playbackStartTimeMs,
|
|
410
|
+
);
|
|
411
|
+
const chunkStartTime = this.audioStartTime + relativeDelayMs / 1000;
|
|
412
|
+
const startOffset = offsetInChunk / 1000;
|
|
413
|
+
|
|
414
|
+
// Schedule the chunk with precise timing
|
|
415
|
+
const now = this.playbackAudioContext?.currentTime ?? 0;
|
|
416
|
+
if (chunkStartTime <= now) {
|
|
417
|
+
console.warn(
|
|
418
|
+
`🎵 [CHUNK_TIMING_WARNING] Chunk ${chunkIndex} scheduled in the past! startTime=${chunkStartTime.toFixed(3)}s, currentTime=${now.toFixed(3)}s`,
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
bufferSource.start(chunkStartTime, startOffset);
|
|
423
|
+
this.activeChunks.set(chunkIndex, bufferSource);
|
|
424
|
+
|
|
425
|
+
// Handle chunk completion and trigger next chunk rendering
|
|
426
|
+
bufferSource.onended = () => {
|
|
427
|
+
this.activeChunks.delete(chunkIndex);
|
|
428
|
+
|
|
429
|
+
// Don't trigger more rendering here to avoid race conditions
|
|
430
|
+
// Let the sync loop handle chunk advancement
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
// Remove automatic async rendering to eliminate race conditions
|
|
434
|
+
// this.ensureAheadChunksReady(timegroup, chunkIndex);
|
|
435
|
+
} catch (error) {
|
|
436
|
+
console.error(
|
|
437
|
+
`🎵 [CHUNK_ERROR] Failed to render chunk ${chunkIndex}:`,
|
|
438
|
+
error,
|
|
439
|
+
);
|
|
440
|
+
} finally {
|
|
441
|
+
this.renderingChunks.delete(chunkIndex);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Update chunk rendering as playhead advances - now handles all chunk management
|
|
447
|
+
*/
|
|
448
|
+
private updateProgressiveChunks(
|
|
449
|
+
timegroup: any,
|
|
450
|
+
currentTimeMs: number,
|
|
451
|
+
maxTimeMs: number,
|
|
452
|
+
): void {
|
|
453
|
+
const newChunkIndex = Math.floor(currentTimeMs / this.chunkDurationMs);
|
|
454
|
+
|
|
455
|
+
if (newChunkIndex !== this.currentChunkIndex) {
|
|
456
|
+
this.currentChunkIndex = newChunkIndex;
|
|
457
|
+
|
|
458
|
+
// Cleanup old chunks
|
|
459
|
+
this.cleanupOldChunks();
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Always ensure we have enough chunks ahead (check every frame)
|
|
463
|
+
this.ensureChunksAhead(timegroup, maxTimeMs);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Systematically ensure chunks are ready ahead of current playback (synchronous)
|
|
468
|
+
*/
|
|
469
|
+
private ensureChunksAhead(timegroup: any, maxTimeMs: number): void {
|
|
470
|
+
// Start from chunk 1 ahead of current (chunk 0 was handled at startup)
|
|
471
|
+
for (let i = 1; i <= this.lookaheadChunks; i++) {
|
|
472
|
+
const targetChunkIndex = this.currentChunkIndex + i;
|
|
473
|
+
const targetChunkStartMs = targetChunkIndex * this.chunkDurationMs;
|
|
474
|
+
|
|
475
|
+
// Don't render beyond timegroup end
|
|
476
|
+
if (targetChunkStartMs >= maxTimeMs) {
|
|
477
|
+
break;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Check if this chunk needs to be rendered
|
|
481
|
+
if (
|
|
482
|
+
!this.renderingChunks.has(targetChunkIndex) &&
|
|
483
|
+
!this.activeChunks.has(targetChunkIndex)
|
|
484
|
+
) {
|
|
485
|
+
// Future chunks don't need offset since they start from the beginning
|
|
486
|
+
const offsetInChunk = 0;
|
|
487
|
+
|
|
488
|
+
// Render chunk asynchronously to avoid blocking the sync loop
|
|
489
|
+
this.renderAndScheduleChunk(
|
|
490
|
+
timegroup,
|
|
491
|
+
targetChunkStartMs,
|
|
492
|
+
targetChunkIndex,
|
|
493
|
+
offsetInChunk,
|
|
494
|
+
).catch((error) => {
|
|
495
|
+
console.error(
|
|
496
|
+
`🎵 [ENSURE_CHUNKS_ERROR] Failed to render chunk ${targetChunkIndex}:`,
|
|
497
|
+
error,
|
|
498
|
+
);
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Clean up chunks that are behind the current playhead
|
|
506
|
+
*/
|
|
507
|
+
private cleanupOldChunks(): void {
|
|
508
|
+
const cutoffChunkIndex = this.currentChunkIndex - 1; // Keep 1 chunk behind for safety
|
|
509
|
+
|
|
510
|
+
for (const [chunkIndex, bufferSource] of this.activeChunks.entries()) {
|
|
511
|
+
if (chunkIndex < cutoffChunkIndex) {
|
|
512
|
+
try {
|
|
513
|
+
bufferSource.stop();
|
|
514
|
+
} catch (_error) {
|
|
515
|
+
// Ignore stop errors for already stopped sources
|
|
516
|
+
}
|
|
517
|
+
this.activeChunks.delete(chunkIndex);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|