@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.
Files changed (218) hide show
  1. package/dist/EF_FRAMEGEN.js +1 -1
  2. package/dist/ScrubTrackManager.d.ts +2 -2
  3. package/dist/elements/EFAudio.d.ts +21 -2
  4. package/dist/elements/EFAudio.js +41 -11
  5. package/dist/elements/EFImage.d.ts +1 -0
  6. package/dist/elements/EFImage.js +11 -3
  7. package/dist/elements/EFMedia/AssetIdMediaEngine.d.ts +18 -0
  8. package/dist/elements/EFMedia/AssetIdMediaEngine.js +41 -0
  9. package/dist/elements/EFMedia/AssetMediaEngine.d.ts +47 -0
  10. package/dist/elements/EFMedia/AssetMediaEngine.js +116 -0
  11. package/dist/elements/EFMedia/BaseMediaEngine.d.ts +55 -0
  12. package/dist/elements/EFMedia/BaseMediaEngine.js +96 -0
  13. package/dist/elements/EFMedia/BufferedSeekingInput.d.ts +43 -0
  14. package/dist/elements/EFMedia/BufferedSeekingInput.js +159 -0
  15. package/dist/elements/EFMedia/JitMediaEngine.browsertest.d.ts +0 -0
  16. package/dist/elements/EFMedia/JitMediaEngine.d.ts +31 -0
  17. package/dist/elements/EFMedia/JitMediaEngine.js +62 -0
  18. package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.browsertest.d.ts +9 -0
  19. package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.d.ts +16 -0
  20. package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.js +48 -0
  21. package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.d.ts +3 -0
  22. package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.js +138 -0
  23. package/dist/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.browsertest.d.ts +9 -0
  24. package/dist/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.d.ts +4 -0
  25. package/dist/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.js +16 -0
  26. package/dist/elements/EFMedia/audioTasks/makeAudioInputTask.browsertest.d.ts +9 -0
  27. package/dist/elements/EFMedia/audioTasks/makeAudioInputTask.d.ts +3 -0
  28. package/dist/elements/EFMedia/audioTasks/makeAudioInputTask.js +22 -0
  29. package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.d.ts +7 -0
  30. package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.js +24 -0
  31. package/dist/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.d.ts +4 -0
  32. package/dist/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.js +18 -0
  33. package/dist/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.d.ts +4 -0
  34. package/dist/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.js +16 -0
  35. package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.d.ts +3 -0
  36. package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.js +104 -0
  37. package/dist/elements/EFMedia/services/AudioElementFactory.d.ts +22 -0
  38. package/dist/elements/EFMedia/services/AudioElementFactory.js +72 -0
  39. package/dist/elements/EFMedia/services/MediaSourceService.browsertest.d.ts +1 -0
  40. package/dist/elements/EFMedia/services/MediaSourceService.d.ts +47 -0
  41. package/dist/elements/EFMedia/services/MediaSourceService.js +73 -0
  42. package/dist/elements/EFMedia/shared/AudioSpanUtils.d.ts +7 -0
  43. package/dist/elements/EFMedia/shared/AudioSpanUtils.js +54 -0
  44. package/dist/elements/EFMedia/shared/BufferUtils.d.ts +70 -0
  45. package/dist/elements/EFMedia/shared/BufferUtils.js +89 -0
  46. package/dist/elements/EFMedia/shared/MediaTaskUtils.d.ts +23 -0
  47. package/dist/elements/EFMedia/shared/RenditionHelpers.browsertest.d.ts +1 -0
  48. package/dist/elements/EFMedia/shared/RenditionHelpers.d.ts +19 -0
  49. package/dist/elements/EFMedia/tasks/makeMediaEngineTask.browsertest.d.ts +1 -0
  50. package/dist/elements/EFMedia/tasks/makeMediaEngineTask.d.ts +18 -0
  51. package/dist/elements/EFMedia/tasks/makeMediaEngineTask.js +60 -0
  52. package/dist/elements/EFMedia/tasks/makeMediaEngineTask.test.d.ts +1 -0
  53. package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.browsertest.d.ts +9 -0
  54. package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.d.ts +16 -0
  55. package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.js +46 -0
  56. package/dist/elements/EFMedia/videoTasks/makeVideoInitSegmentFetchTask.browsertest.d.ts +9 -0
  57. package/dist/elements/EFMedia/videoTasks/makeVideoInitSegmentFetchTask.d.ts +4 -0
  58. package/dist/elements/EFMedia/videoTasks/makeVideoInitSegmentFetchTask.js +16 -0
  59. package/dist/elements/EFMedia/videoTasks/makeVideoInputTask.browsertest.d.ts +9 -0
  60. package/dist/elements/EFMedia/videoTasks/makeVideoInputTask.d.ts +3 -0
  61. package/dist/elements/EFMedia/videoTasks/makeVideoInputTask.js +27 -0
  62. package/dist/elements/EFMedia/videoTasks/makeVideoSeekTask.d.ts +7 -0
  63. package/dist/elements/EFMedia/videoTasks/makeVideoSeekTask.js +25 -0
  64. package/dist/elements/EFMedia/videoTasks/makeVideoSegmentFetchTask.browsertest.d.ts +9 -0
  65. package/dist/elements/EFMedia/videoTasks/makeVideoSegmentFetchTask.d.ts +4 -0
  66. package/dist/elements/EFMedia/videoTasks/makeVideoSegmentFetchTask.js +18 -0
  67. package/dist/elements/EFMedia/videoTasks/makeVideoSegmentIdTask.browsertest.d.ts +9 -0
  68. package/dist/elements/EFMedia/videoTasks/makeVideoSegmentIdTask.d.ts +4 -0
  69. package/dist/elements/EFMedia/videoTasks/makeVideoSegmentIdTask.js +16 -0
  70. package/dist/elements/EFMedia.browsertest.d.ts +1 -0
  71. package/dist/elements/EFMedia.d.ts +75 -111
  72. package/dist/elements/EFMedia.js +141 -1111
  73. package/dist/elements/EFTemporal.d.ts +1 -1
  74. package/dist/elements/EFTemporal.js +1 -1
  75. package/dist/elements/EFTimegroup.d.ts +11 -0
  76. package/dist/elements/EFTimegroup.js +88 -13
  77. package/dist/elements/EFVideo.d.ts +60 -29
  78. package/dist/elements/EFVideo.js +103 -203
  79. package/dist/elements/EFWaveform.js +2 -2
  80. package/dist/elements/SampleBuffer.d.ts +14 -0
  81. package/dist/elements/SampleBuffer.js +52 -0
  82. package/dist/getRenderInfo.d.ts +2 -2
  83. package/dist/getRenderInfo.js +2 -1
  84. package/dist/gui/ContextMixin.js +17 -70
  85. package/dist/gui/EFFilmstrip.d.ts +3 -3
  86. package/dist/gui/EFFilmstrip.js +1 -1
  87. package/dist/gui/EFFitScale.d.ts +2 -2
  88. package/dist/gui/TWMixin.js +1 -1
  89. package/dist/gui/services/ElementConnectionManager.browsertest.d.ts +1 -0
  90. package/dist/gui/services/ElementConnectionManager.d.ts +59 -0
  91. package/dist/gui/services/ElementConnectionManager.js +128 -0
  92. package/dist/gui/services/PlaybackController.browsertest.d.ts +1 -0
  93. package/dist/gui/services/PlaybackController.d.ts +103 -0
  94. package/dist/gui/services/PlaybackController.js +290 -0
  95. package/dist/services/MediaSourceManager.d.ts +62 -0
  96. package/dist/services/MediaSourceManager.js +211 -0
  97. package/dist/style.css +1 -1
  98. package/dist/transcoding/cache/CacheManager.d.ts +73 -0
  99. package/dist/transcoding/cache/RequestDeduplicator.d.ts +29 -0
  100. package/dist/transcoding/cache/RequestDeduplicator.js +53 -0
  101. package/dist/transcoding/cache/RequestDeduplicator.test.d.ts +1 -0
  102. package/dist/transcoding/types/index.d.ts +242 -0
  103. package/dist/transcoding/utils/MediaUtils.d.ts +9 -0
  104. package/dist/transcoding/utils/UrlGenerator.d.ts +26 -0
  105. package/dist/transcoding/utils/UrlGenerator.js +45 -0
  106. package/dist/transcoding/utils/constants.d.ts +27 -0
  107. package/dist/utils/LRUCache.d.ts +34 -0
  108. package/dist/utils/LRUCache.js +115 -0
  109. package/package.json +3 -2
  110. package/src/elements/EFAudio.browsertest.ts +183 -43
  111. package/src/elements/EFAudio.ts +59 -13
  112. package/src/elements/EFImage.browsertest.ts +42 -0
  113. package/src/elements/EFImage.ts +23 -3
  114. package/src/elements/EFMedia/AssetIdMediaEngine.test.ts +222 -0
  115. package/src/elements/EFMedia/AssetIdMediaEngine.ts +70 -0
  116. package/src/elements/EFMedia/AssetMediaEngine.ts +210 -0
  117. package/src/elements/EFMedia/BaseMediaEngine.test.ts +164 -0
  118. package/src/elements/EFMedia/BaseMediaEngine.ts +170 -0
  119. package/src/elements/EFMedia/BufferedSeekingInput.browsertest.ts +400 -0
  120. package/src/elements/EFMedia/BufferedSeekingInput.ts +267 -0
  121. package/src/elements/EFMedia/JitMediaEngine.browsertest.ts +165 -0
  122. package/src/elements/EFMedia/JitMediaEngine.ts +110 -0
  123. package/src/elements/EFMedia/audioTasks/makeAudioBufferTask.browsertest.ts +554 -0
  124. package/src/elements/EFMedia/audioTasks/makeAudioBufferTask.ts +81 -0
  125. package/src/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.ts +241 -0
  126. package/src/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.browsertest.ts +59 -0
  127. package/src/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.ts +23 -0
  128. package/src/elements/EFMedia/audioTasks/makeAudioInputTask.browsertest.ts +55 -0
  129. package/src/elements/EFMedia/audioTasks/makeAudioInputTask.ts +35 -0
  130. package/src/elements/EFMedia/audioTasks/makeAudioSeekTask.ts +42 -0
  131. package/src/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.ts +34 -0
  132. package/src/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.ts +23 -0
  133. package/src/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.ts +174 -0
  134. package/src/elements/EFMedia/services/AudioElementFactory.browsertest.ts +325 -0
  135. package/src/elements/EFMedia/services/AudioElementFactory.ts +119 -0
  136. package/src/elements/EFMedia/services/MediaSourceService.browsertest.ts +257 -0
  137. package/src/elements/EFMedia/services/MediaSourceService.ts +102 -0
  138. package/src/elements/EFMedia/shared/AudioSpanUtils.ts +128 -0
  139. package/src/elements/EFMedia/shared/BufferUtils.ts +310 -0
  140. package/src/elements/EFMedia/shared/MediaTaskUtils.ts +44 -0
  141. package/src/elements/EFMedia/shared/RenditionHelpers.browsertest.ts +247 -0
  142. package/src/elements/EFMedia/shared/RenditionHelpers.ts +79 -0
  143. package/src/elements/EFMedia/tasks/makeMediaEngineTask.browsertest.ts +128 -0
  144. package/src/elements/EFMedia/tasks/makeMediaEngineTask.test.ts +233 -0
  145. package/src/elements/EFMedia/tasks/makeMediaEngineTask.ts +89 -0
  146. package/src/elements/EFMedia/videoTasks/makeVideoBufferTask.browsertest.ts +555 -0
  147. package/src/elements/EFMedia/videoTasks/makeVideoBufferTask.ts +79 -0
  148. package/src/elements/EFMedia/videoTasks/makeVideoInitSegmentFetchTask.browsertest.ts +59 -0
  149. package/src/elements/EFMedia/videoTasks/makeVideoInitSegmentFetchTask.ts +23 -0
  150. package/src/elements/EFMedia/videoTasks/makeVideoInputTask.browsertest.ts +55 -0
  151. package/src/elements/EFMedia/videoTasks/makeVideoInputTask.ts +45 -0
  152. package/src/elements/EFMedia/videoTasks/makeVideoSeekTask.ts +44 -0
  153. package/src/elements/EFMedia/videoTasks/makeVideoSegmentFetchTask.browsertest.ts +57 -0
  154. package/src/elements/EFMedia/videoTasks/makeVideoSegmentFetchTask.ts +32 -0
  155. package/src/elements/EFMedia/videoTasks/makeVideoSegmentIdTask.browsertest.ts +56 -0
  156. package/src/elements/EFMedia/videoTasks/makeVideoSegmentIdTask.ts +23 -0
  157. package/src/elements/EFMedia.browsertest.ts +658 -265
  158. package/src/elements/EFMedia.ts +173 -1763
  159. package/src/elements/EFTemporal.ts +3 -4
  160. package/src/elements/EFTimegroup.browsertest.ts +6 -3
  161. package/src/elements/EFTimegroup.ts +152 -21
  162. package/src/elements/EFVideo.browsertest.ts +115 -37
  163. package/src/elements/EFVideo.ts +123 -452
  164. package/src/elements/EFWaveform.ts +1 -1
  165. package/src/elements/MediaController.ts +2 -12
  166. package/src/elements/SampleBuffer.ts +97 -0
  167. package/src/gui/ContextMixin.ts +23 -104
  168. package/src/gui/services/ElementConnectionManager.browsertest.ts +263 -0
  169. package/src/gui/services/ElementConnectionManager.ts +224 -0
  170. package/src/gui/services/PlaybackController.browsertest.ts +437 -0
  171. package/src/gui/services/PlaybackController.ts +521 -0
  172. package/src/services/MediaSourceManager.ts +333 -0
  173. package/src/transcoding/cache/CacheManager.ts +208 -0
  174. package/src/transcoding/cache/RequestDeduplicator.test.ts +170 -0
  175. package/src/transcoding/cache/RequestDeduplicator.ts +65 -0
  176. package/src/transcoding/types/index.ts +265 -0
  177. package/src/transcoding/utils/MediaUtils.ts +63 -0
  178. package/src/transcoding/utils/UrlGenerator.ts +68 -0
  179. package/src/transcoding/utils/constants.ts +36 -0
  180. package/src/utils/LRUCache.ts +153 -0
  181. package/test/EFVideo.framegen.browsertest.ts +38 -29
  182. 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
  183. 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
  184. 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
  185. 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
  186. 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
  187. 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
  188. 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
  189. 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
  190. 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
  191. 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
  192. 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
  193. 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
  194. 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
  195. 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
  196. 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
  197. 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
  198. 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
  199. 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
  200. 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
  201. 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
  202. package/test/__cache__/GET__api_v1_transcode_manifest_json_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__3be92a0437de726b431ed5af2369158a/data.bin +1 -0
  203. package/test/__cache__/GET__api_v1_transcode_manifest_json_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__3be92a0437de726b431ed5af2369158a/metadata.json +19 -0
  204. package/test/createJitTestClips.ts +320 -188
  205. package/test/recordReplayProxyPlugin.js +302 -0
  206. package/test/useAssetMSW.ts +1 -1
  207. package/test/useMSW.ts +35 -22
  208. package/types.json +1 -1
  209. package/dist/JitTranscodingClient.d.ts +0 -167
  210. package/dist/JitTranscodingClient.js +0 -373
  211. package/dist/ScrubTrackManager.js +0 -216
  212. package/dist/elements/printTaskStatus.js +0 -11
  213. package/src/elements/__screenshots__/EFMedia.browsertest.ts/EFMedia-JIT-audio-playback-audioBufferTask-should-work-in-JIT-mode-without-URL-errors-1.png +0 -0
  214. package/test/EFVideo.frame-tasks.browsertest.ts +0 -524
  215. /package/dist/{JitTranscodingClient.browsertest.d.ts → elements/EFMedia/AssetIdMediaEngine.test.d.ts} +0 -0
  216. /package/dist/{JitTranscodingClient.test.d.ts → elements/EFMedia/BaseMediaEngine.test.d.ts} +0 -0
  217. /package/dist/{ScrubTrackIntegration.test.d.ts → elements/EFMedia/BufferedSeekingInput.browsertest.d.ts} +0 -0
  218. /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
+ }