@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
@@ -1,5 +1,4 @@
1
1
  import type { ReactiveController, ReactiveControllerHost } from "lit";
2
- import { JitTranscodingClient } from "../JitTranscodingClient.ts";
3
2
 
4
3
  export class MediaController implements ReactiveController {
5
4
  #src: string | null = null;
@@ -29,10 +28,6 @@ export class MediaController implements ReactiveController {
29
28
  return "asset";
30
29
  }
31
30
 
32
- if (JitTranscodingClient.isJitTranscodeEligible(this.host.src)) {
33
- return "jit-transcode";
34
- }
35
-
36
31
  return "asset";
37
32
  }
38
33
 
@@ -84,7 +79,6 @@ export class MediaController implements ReactiveController {
84
79
 
85
80
  handleUpdate(changes: Map<string, { oldValue: any; newValue: any }>) {
86
81
  if (changes.has("src") || changes.has("assetId") || changes.has("mode")) {
87
- console.log("SRC/ASSETID/MODE changed");
88
82
  }
89
83
  }
90
84
 
@@ -98,11 +92,7 @@ export class MediaController implements ReactiveController {
98
92
  this.assetId = this.host.assetId;
99
93
  }
100
94
 
101
- hostDisconnected() {
102
- console.log("hostDisconnected");
103
- }
95
+ hostDisconnected() {}
104
96
 
105
- updated() {
106
- console.log("updated");
107
- }
97
+ updated() {}
108
98
  }
@@ -0,0 +1,97 @@
1
+ import type { AudioSample, VideoSample } from "mediabunny";
2
+ export type MediaSample = VideoSample | AudioSample;
3
+
4
+ // Generic sample buffer that works with both VideoSample and AudioSample
5
+ export class SampleBuffer {
6
+ private buffer: MediaSample[] = [];
7
+ private bufferSize: number;
8
+
9
+ constructor(bufferSize = 10) {
10
+ this.bufferSize = bufferSize;
11
+ }
12
+
13
+ push(sample: MediaSample) {
14
+ // Defensive copy to avoid concurrent modification during iteration
15
+ const currentBuffer = [...this.buffer];
16
+ currentBuffer.push(sample);
17
+
18
+ if (currentBuffer.length > this.bufferSize) {
19
+ const shifted = currentBuffer.shift();
20
+ if (shifted) {
21
+ try {
22
+ shifted.close();
23
+ } catch (_error) {
24
+ // Sample already closed, continue
25
+ }
26
+ }
27
+ }
28
+
29
+ // Update buffer atomically
30
+ this.buffer = currentBuffer;
31
+ }
32
+
33
+ clear() {
34
+ // Get current buffer and clear atomically
35
+ const toClose = this.buffer;
36
+ this.buffer = [];
37
+
38
+ // Close samples after clearing to avoid holding references
39
+ for (const sample of toClose) {
40
+ try {
41
+ sample.close();
42
+ } catch (_error) {
43
+ // Sample already closed, continue
44
+ }
45
+ }
46
+ }
47
+
48
+ peek(): MediaSample | undefined {
49
+ // Defensive read - get current buffer state
50
+ const currentBuffer = this.buffer;
51
+ return currentBuffer[0];
52
+ }
53
+
54
+ find(desiredSeekTimeMs: number): MediaSample | undefined {
55
+ // Take snapshot to avoid concurrent modification during iteration
56
+ const currentBuffer = [...this.buffer];
57
+
58
+ if (currentBuffer.length === 0) return undefined;
59
+
60
+ // Round to microsecond precision to handle floating point issues
61
+ // without introducing timing aliasing problems
62
+ const roundToMicroseconds = (timeMs: number) =>
63
+ Math.round(timeMs * 1000) / 1000;
64
+ const targetTimeMs = roundToMicroseconds(desiredSeekTimeMs);
65
+
66
+ // Find the sample that contains the target time
67
+ for (const sample of currentBuffer) {
68
+ const sampleStartMs = roundToMicroseconds((sample.timestamp || 0) * 1000);
69
+ const sampleDurationMs = roundToMicroseconds(
70
+ (sample.duration || 0) * 1000,
71
+ );
72
+ const sampleEndMs = sampleStartMs + sampleDurationMs;
73
+
74
+ // Check if the desired time falls within this sample's time span [start, end)
75
+ if (targetTimeMs >= sampleStartMs && targetTimeMs < sampleEndMs) {
76
+ return sample;
77
+ }
78
+ }
79
+
80
+ return undefined; // No sample contains the target time
81
+ }
82
+
83
+ get length() {
84
+ return this.buffer.length;
85
+ }
86
+
87
+ get firstTimestamp() {
88
+ // Defensive read - get current buffer state
89
+ const currentBuffer = this.buffer;
90
+ return currentBuffer[0]?.timestamp || 0;
91
+ }
92
+
93
+ getContents(): MediaSample[] {
94
+ // Defensive copy to avoid concurrent modification during iteration
95
+ return [...this.buffer];
96
+ }
97
+ }
@@ -1,7 +1,6 @@
1
1
  import { consume, createContext, provide } from "@lit/context";
2
2
  import type { LitElement } from "lit";
3
3
  import { property, state } from "lit/decorators.js";
4
-
5
4
  import type { EFTimegroup } from "../elements/EFTimegroup.js";
6
5
  import {
7
6
  type EFConfiguration,
@@ -12,6 +11,8 @@ import { fetchContext } from "./fetchContext.js";
12
11
  import { type FocusContext, focusContext } from "./focusContext.js";
13
12
  import { focusedElementContext } from "./focusedElementContext.js";
14
13
  import { loopContext, playingContext } from "./playingContext.js";
14
+ import { ElementConnectionManager } from "./services/ElementConnectionManager.js";
15
+ import { PlaybackController } from "./services/PlaybackController.js";
15
16
 
16
17
  export const targetTimegroupContext = createContext<EFTimegroup | null>(
17
18
  "target-timegroup",
@@ -135,9 +136,6 @@ export function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {
135
136
  @state()
136
137
  currentTimeMs = 0;
137
138
 
138
- #FPS = 30;
139
- #MS_PER_FRAME = 1000 / this.#FPS;
140
-
141
139
  #timegroupObserver = new MutationObserver((mutations) => {
142
140
  for (const mutation of mutations) {
143
141
  if (mutation.type === "childList") {
@@ -210,116 +208,37 @@ export function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {
210
208
  this.playing = false;
211
209
  }
212
210
 
213
- #playbackAudioContext: AudioContext | null = null;
214
- #playbackAnimationFrameRequest: number | null = null;
215
- #AUDIO_PLAYBACK_SLICE_MS = 1000;
216
-
217
- #syncPlayheadToAudioContext(target: EFTimegroup, startMs: number) {
218
- const rawTimeMs =
219
- startMs + (this.#playbackAudioContext?.currentTime ?? 0) * 1000;
220
- const nextTimeMs =
221
- Math.round(rawTimeMs / this.#MS_PER_FRAME) * this.#MS_PER_FRAME;
222
- if (nextTimeMs !== this.currentTimeMs) {
223
- this.currentTimeMs = nextTimeMs;
224
- }
225
- this.#playbackAnimationFrameRequest = requestAnimationFrame(() => {
226
- this.#syncPlayheadToAudioContext(target, startMs);
227
- });
228
- }
211
+ // Services for clean playback management
212
+ // Keep connection manager for potential future use
213
+ // @ts-ignore - Keeping for future use
214
+ private _elementConnectionManager = new ElementConnectionManager(3000);
215
+ private playbackController = new PlaybackController({
216
+ fps: 30,
217
+ onTimeUpdate: (timeMs: number) => {
218
+ this.currentTimeMs = timeMs;
219
+ },
220
+ onPlayStateChange: (playing: boolean) => {
221
+ this.playing = playing;
222
+ },
223
+ onError: (error: Error) => {
224
+ console.error("🎵 [PLAYBACK_ERROR]:", error);
225
+ },
226
+ });
229
227
 
230
228
  private async stopPlayback() {
231
- if (this.#playbackAudioContext) {
232
- if (this.#playbackAudioContext.state !== "closed") {
233
- await this.#playbackAudioContext.close();
234
- }
235
- }
236
- if (this.#playbackAnimationFrameRequest) {
237
- cancelAnimationFrame(this.#playbackAnimationFrameRequest);
238
- }
239
- this.#playbackAudioContext = null;
229
+ await this.playbackController.stopPlayback();
240
230
  }
241
231
 
242
232
  private async startPlayback() {
243
- await this.stopPlayback();
244
233
  const timegroup = this.targetTimegroup;
245
234
  if (!timegroup) {
246
235
  return;
247
236
  }
248
237
 
249
- await timegroup.waitForMediaDurations();
250
-
251
- let currentMs = timegroup.currentTimeMs;
252
- const fromMs = currentMs;
253
- const toMs = timegroup.endTimeMs;
254
-
255
- if (fromMs >= toMs) {
256
- this.pause();
257
- return;
258
- }
259
-
260
- let bufferCount = 0;
261
- this.#playbackAudioContext = new AudioContext({
262
- latencyHint: "playback",
263
- });
264
-
265
- if (this.#playbackAnimationFrameRequest) {
266
- cancelAnimationFrame(this.#playbackAnimationFrameRequest);
267
- }
268
- this.#syncPlayheadToAudioContext(timegroup, currentMs);
269
- const playbackContext = this.#playbackAudioContext;
270
- if (playbackContext.state === "suspended") {
271
- console.warn(
272
- "AudioContext is suspended, media playback will not work until user has interacted with page.",
273
- );
274
- this.playing = false;
275
- return;
276
- }
277
- await playbackContext.suspend();
278
-
279
- const fillBuffer = async () => {
280
- if (bufferCount > 1) {
281
- return;
282
- }
283
- const canFillBuffer = await queueBufferSource();
284
- if (canFillBuffer) {
285
- fillBuffer();
286
- }
287
- };
288
-
289
- const queueBufferSource = async () => {
290
- if (currentMs >= toMs) {
291
- return false;
292
- }
293
- const startMs = currentMs;
294
- const endMs = currentMs + this.#AUDIO_PLAYBACK_SLICE_MS;
295
- currentMs += this.#AUDIO_PLAYBACK_SLICE_MS;
296
- const audioBuffer = await timegroup.renderAudio(startMs, endMs);
297
- bufferCount++;
298
- const source = playbackContext.createBufferSource();
299
- source.buffer = audioBuffer;
300
- source.connect(playbackContext.destination);
301
- source.start((startMs - fromMs) / 1000);
302
- source.onended = () => {
303
- bufferCount--;
304
- if (endMs >= toMs) {
305
- this.pause();
306
- if (this.loop) {
307
- this.updateComplete.then(() => {
308
- this.currentTimeMs = 0;
309
- this.updateComplete.then(() => {
310
- this.play();
311
- });
312
- });
313
- }
314
- } else {
315
- fillBuffer();
316
- }
317
- };
318
- return true;
319
- };
320
-
321
- await fillBuffer();
322
- await playbackContext.resume();
238
+ await this.playbackController.startPlayback(
239
+ timegroup,
240
+ timegroup.currentTimeMs,
241
+ );
323
242
  }
324
243
  }
325
244
 
@@ -0,0 +1,263 @@
1
+ import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
2
+ import { ElementConnectionManager } from "./ElementConnectionManager.js";
3
+
4
+ describe("ElementConnectionManager", () => {
5
+ let manager: ElementConnectionManager;
6
+ let audioContext: AudioContext;
7
+ let mockTimegroup: any;
8
+ let mockMediaElements: any[];
9
+
10
+ beforeEach(() => {
11
+ manager = new ElementConnectionManager(3000);
12
+ audioContext = new AudioContext();
13
+
14
+ // Create mock media elements
15
+ mockMediaElements = [
16
+ {
17
+ src: "audio1.mp3",
18
+ startTimeMs: 1000,
19
+ endTimeMs: 3000,
20
+ currentSourceTimeMs: 0,
21
+ audioElement: { currentTime: 0, play: vi.fn(), pause: vi.fn() },
22
+ getMediaElementSource: vi.fn(),
23
+ },
24
+ {
25
+ src: "audio2.mp3",
26
+ startTimeMs: 2500,
27
+ endTimeMs: 5000,
28
+ currentSourceTimeMs: 0,
29
+ audioElement: { currentTime: 0, play: vi.fn(), pause: vi.fn() },
30
+ getMediaElementSource: vi.fn(),
31
+ },
32
+ {
33
+ src: "audio3.mp3",
34
+ startTimeMs: 6000,
35
+ endTimeMs: 8000,
36
+ currentSourceTimeMs: 0,
37
+ audioElement: { currentTime: 0, play: vi.fn(), pause: vi.fn() },
38
+ getMediaElementSource: vi.fn(),
39
+ },
40
+ ];
41
+
42
+ // Mock timegroup
43
+ mockTimegroup = {
44
+ querySelectorAll: vi.fn().mockReturnValue(mockMediaElements),
45
+ };
46
+
47
+ // Mock MediaElementAudioSourceNode for each element
48
+ mockMediaElements.forEach((element, _index) => {
49
+ const mockSource = {
50
+ connect: vi.fn(),
51
+ disconnect: vi.fn(),
52
+ context: audioContext,
53
+ } as any;
54
+ element.getMediaElementSource.mockResolvedValue(mockSource);
55
+ });
56
+ });
57
+
58
+ afterEach(async () => {
59
+ manager.clearAll();
60
+ if (audioContext.state !== "closed") {
61
+ await audioContext.close();
62
+ }
63
+ vi.clearAllMocks();
64
+ });
65
+
66
+ describe("Constructor and Configuration", () => {
67
+ test("should initialize with default lookahead", () => {
68
+ const defaultManager = new ElementConnectionManager();
69
+ expect(defaultManager.getLookaheadMs()).toBe(3000);
70
+ });
71
+
72
+ test("should initialize with custom lookahead", () => {
73
+ const customManager = new ElementConnectionManager(5000);
74
+ expect(customManager.getLookaheadMs()).toBe(5000);
75
+ });
76
+
77
+ test("should allow updating lookahead", () => {
78
+ manager.setLookaheadMs(4000);
79
+ expect(manager.getLookaheadMs()).toBe(4000);
80
+ });
81
+ });
82
+
83
+ describe("Element Connection Logic", () => {
84
+ test("should prepare elements that are starting soon", async () => {
85
+ // At time 0ms with 3s lookahead, both audio1 (1000ms) and audio2 (2500ms) should be prepared
86
+ await manager.updateConnectedElements(audioContext, mockTimegroup, 0);
87
+
88
+ const info = manager.getConnectionInfo();
89
+ expect(info.total).toBe(2); // Both audio1 and audio2 within 3s lookahead
90
+ expect(info.prepared).toBe(2);
91
+ expect(info.connected).toBe(0);
92
+
93
+ // Verify both elements were prepared
94
+ expect(mockMediaElements[0].getMediaElementSource).toHaveBeenCalledWith(
95
+ audioContext,
96
+ );
97
+ expect(mockMediaElements[1].getMediaElementSource).toHaveBeenCalledWith(
98
+ audioContext,
99
+ );
100
+ });
101
+
102
+ test("should activate elements when they become current", async () => {
103
+ // First prepare the element
104
+ await manager.updateConnectedElements(audioContext, mockTimegroup, 500);
105
+
106
+ // Then activate when it becomes current
107
+ await manager.updateConnectedElements(audioContext, mockTimegroup, 1500);
108
+
109
+ const info = manager.getConnectionInfo();
110
+ expect(info.connected).toBe(1);
111
+ expect(mockMediaElements[0].audioElement.play).toHaveBeenCalled();
112
+ });
113
+
114
+ test("should handle multiple active elements simultaneously", async () => {
115
+ // At time 2750ms, both audio1 (1000-3000) and audio2 (2500-5000) should be active
116
+ await manager.updateConnectedElements(audioContext, mockTimegroup, 2750);
117
+
118
+ const info = manager.getConnectionInfo();
119
+ expect(info.connected).toBe(2);
120
+
121
+ expect(mockMediaElements[0].audioElement.play).toHaveBeenCalled();
122
+ expect(mockMediaElements[1].audioElement.play).toHaveBeenCalled();
123
+ });
124
+
125
+ test("should deactivate elements when they end", async () => {
126
+ // Activate audio1
127
+ await manager.updateConnectedElements(audioContext, mockTimegroup, 2000);
128
+ expect(manager.getConnectionInfo().connected).toBe(1);
129
+
130
+ // Move past audio1's end time
131
+ await manager.updateConnectedElements(audioContext, mockTimegroup, 3500);
132
+
133
+ const info = manager.getConnectionInfo();
134
+ // audio1 should be deactivated but still prepared, audio2 should be active
135
+ expect(info.connected).toBe(1); // Only audio2 active
136
+ expect(mockMediaElements[0].audioElement.pause).toHaveBeenCalled();
137
+ });
138
+
139
+ test("should cleanup old elements outside cleanup threshold", async () => {
140
+ // Activate and then move far past
141
+ await manager.updateConnectedElements(audioContext, mockTimegroup, 2000);
142
+ await manager.updateConnectedElements(audioContext, mockTimegroup, 10000); // 7 seconds later
143
+
144
+ const info = manager.getConnectionInfo();
145
+ // All old elements should be cleaned up, only audio3 might be prepared if in lookahead
146
+ expect(info.total).toBeLessThan(mockMediaElements.length);
147
+ });
148
+ });
149
+
150
+ describe("Error Handling", () => {
151
+ test("should propagate getMediaElementSource errors", async () => {
152
+ // Mock both elements within lookahead to fail
153
+ mockMediaElements[0].getMediaElementSource.mockRejectedValue(
154
+ new Error("Connection failed"),
155
+ );
156
+ mockMediaElements[1].getMediaElementSource.mockRejectedValue(
157
+ new Error("Connection failed"),
158
+ );
159
+
160
+ // Should throw the built-in error
161
+ await expect(
162
+ manager.updateConnectedElements(audioContext, mockTimegroup, 500),
163
+ ).rejects.toThrow("Connection failed");
164
+ });
165
+
166
+ test("should propagate activation errors", async () => {
167
+ mockMediaElements[0].audioElement.play.mockRejectedValue(
168
+ new Error("Play failed"),
169
+ );
170
+
171
+ await manager.updateConnectedElements(audioContext, mockTimegroup, 500);
172
+
173
+ // Should throw the built-in error when activating
174
+ await expect(
175
+ manager.updateConnectedElements(audioContext, mockTimegroup, 1500),
176
+ ).rejects.toThrow("Play failed");
177
+ });
178
+
179
+ test("should handle closed AudioContext gracefully", async () => {
180
+ await audioContext.close();
181
+
182
+ await expect(
183
+ manager.updateConnectedElements(audioContext, mockTimegroup, 1500),
184
+ ).resolves.not.toThrow();
185
+
186
+ expect(manager.getConnectionInfo().total).toBe(0);
187
+ });
188
+ });
189
+
190
+ describe("Cleanup and Lifecycle", () => {
191
+ test("should clear all connections", async () => {
192
+ await manager.updateConnectedElements(audioContext, mockTimegroup, 2000);
193
+ expect(manager.getConnectionInfo().total).toBeGreaterThan(0);
194
+
195
+ manager.clearAll();
196
+ expect(manager.getConnectionInfo().total).toBe(0);
197
+ });
198
+
199
+ test("should disconnect connected elements during clearAll", async () => {
200
+ await manager.updateConnectedElements(audioContext, mockTimegroup, 2000);
201
+
202
+ const mockSource = await mockMediaElements[0].getMediaElementSource();
203
+
204
+ manager.clearAll();
205
+
206
+ expect(mockSource.disconnect).toHaveBeenCalled();
207
+ });
208
+
209
+ test("should handle clearAll with no elements gracefully", () => {
210
+ expect(() => manager.clearAll()).not.toThrow();
211
+ });
212
+ });
213
+
214
+ describe("Connection Info and Debugging", () => {
215
+ test("should provide accurate connection info", async () => {
216
+ const initialInfo = manager.getConnectionInfo();
217
+ expect(initialInfo).toEqual({ total: 0, connected: 0, prepared: 0 });
218
+
219
+ await manager.updateConnectedElements(audioContext, mockTimegroup, 500);
220
+ const preparedInfo = manager.getConnectionInfo();
221
+ expect(preparedInfo.prepared).toBeGreaterThan(0);
222
+
223
+ await manager.updateConnectedElements(audioContext, mockTimegroup, 1500);
224
+ const activeInfo = manager.getConnectionInfo();
225
+ expect(activeInfo.connected).toBeGreaterThan(0);
226
+ });
227
+
228
+ test("should track connected vs prepared states accurately", async () => {
229
+ // Prepare multiple elements
230
+ await manager.updateConnectedElements(audioContext, mockTimegroup, 0);
231
+
232
+ // Activate one
233
+ await manager.updateConnectedElements(audioContext, mockTimegroup, 2750);
234
+
235
+ const info = manager.getConnectionInfo();
236
+ expect(info.total).toBe(info.connected + info.prepared);
237
+ expect(info.connected).toBeGreaterThan(0);
238
+ });
239
+ });
240
+
241
+ describe("Lookahead Behavior", () => {
242
+ test("should respect lookahead window for preparation", async () => {
243
+ manager.setLookaheadMs(1000); // 1 second lookahead
244
+
245
+ // At time 0, only elements starting within 1 second should be prepared
246
+ await manager.updateConnectedElements(audioContext, mockTimegroup, 0);
247
+
248
+ const info = manager.getConnectionInfo();
249
+ expect(info.total).toBe(1); // Only audio1 at 1000ms
250
+ });
251
+
252
+ test("should adjust preparation based on lookahead changes", async () => {
253
+ manager.setLookaheadMs(500); // Very short lookahead
254
+
255
+ await manager.updateConnectedElements(audioContext, mockTimegroup, 0);
256
+ expect(manager.getConnectionInfo().total).toBe(0); // No elements within 500ms
257
+
258
+ manager.setLookaheadMs(2000); // Longer lookahead
259
+ await manager.updateConnectedElements(audioContext, mockTimegroup, 0);
260
+ expect(manager.getConnectionInfo().total).toBe(1); // audio1 now within range
261
+ });
262
+ });
263
+ });