@editframe/elements 0.16.8-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 (267) hide show
  1. package/README.md +30 -0
  2. package/dist/DecoderResetFrequency.test.d.ts +1 -0
  3. package/dist/DecoderResetRecovery.test.d.ts +1 -0
  4. package/dist/DelayedLoadingState.d.ts +48 -0
  5. package/dist/DelayedLoadingState.integration.test.d.ts +1 -0
  6. package/dist/DelayedLoadingState.js +113 -0
  7. package/dist/DelayedLoadingState.test.d.ts +1 -0
  8. package/dist/EF_FRAMEGEN.d.ts +10 -1
  9. package/dist/EF_FRAMEGEN.js +199 -179
  10. package/dist/EF_INTERACTIVE.js +2 -6
  11. package/dist/EF_RENDERING.js +1 -3
  12. package/dist/LoadingDebounce.test.d.ts +1 -0
  13. package/dist/LoadingIndicator.browsertest.d.ts +0 -0
  14. package/dist/ManualScrubTest.test.d.ts +1 -0
  15. package/dist/ScrubResolvedFlashing.test.d.ts +1 -0
  16. package/dist/ScrubTrackManager.d.ts +96 -0
  17. package/dist/ScrubTrackManager.test.d.ts +1 -0
  18. package/dist/VideoSeekFlashing.browsertest.d.ts +0 -0
  19. package/dist/VideoStuckDiagnostic.test.d.ts +1 -0
  20. package/dist/elements/CrossUpdateController.js +13 -15
  21. package/dist/elements/EFAudio.browsertest.d.ts +0 -0
  22. package/dist/elements/EFAudio.d.ts +22 -3
  23. package/dist/elements/EFAudio.js +60 -43
  24. package/dist/elements/EFCaptions.js +337 -373
  25. package/dist/elements/EFImage.d.ts +1 -0
  26. package/dist/elements/EFImage.js +73 -91
  27. package/dist/elements/EFMedia/AssetIdMediaEngine.d.ts +18 -0
  28. package/dist/elements/EFMedia/AssetIdMediaEngine.js +41 -0
  29. package/dist/elements/EFMedia/AssetIdMediaEngine.test.d.ts +1 -0
  30. package/dist/elements/EFMedia/AssetMediaEngine.d.ts +47 -0
  31. package/dist/elements/EFMedia/AssetMediaEngine.js +116 -0
  32. package/dist/elements/EFMedia/BaseMediaEngine.d.ts +55 -0
  33. package/dist/elements/EFMedia/BaseMediaEngine.js +96 -0
  34. package/dist/elements/EFMedia/BaseMediaEngine.test.d.ts +1 -0
  35. package/dist/elements/EFMedia/BufferedSeekingInput.browsertest.d.ts +1 -0
  36. package/dist/elements/EFMedia/BufferedSeekingInput.d.ts +43 -0
  37. package/dist/elements/EFMedia/BufferedSeekingInput.js +159 -0
  38. package/dist/elements/EFMedia/JitMediaEngine.browsertest.d.ts +0 -0
  39. package/dist/elements/EFMedia/JitMediaEngine.d.ts +31 -0
  40. package/dist/elements/EFMedia/JitMediaEngine.js +62 -0
  41. package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.browsertest.d.ts +9 -0
  42. package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.d.ts +16 -0
  43. package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.js +48 -0
  44. package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.d.ts +3 -0
  45. package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.js +138 -0
  46. package/dist/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.browsertest.d.ts +9 -0
  47. package/dist/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.d.ts +4 -0
  48. package/dist/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.js +16 -0
  49. package/dist/elements/EFMedia/audioTasks/makeAudioInputTask.browsertest.d.ts +9 -0
  50. package/dist/elements/EFMedia/audioTasks/makeAudioInputTask.d.ts +3 -0
  51. package/dist/elements/EFMedia/audioTasks/makeAudioInputTask.js +22 -0
  52. package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.d.ts +7 -0
  53. package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.js +24 -0
  54. package/dist/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.d.ts +4 -0
  55. package/dist/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.js +18 -0
  56. package/dist/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.d.ts +4 -0
  57. package/dist/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.js +16 -0
  58. package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.d.ts +3 -0
  59. package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.js +104 -0
  60. package/dist/elements/EFMedia/services/AudioElementFactory.browsertest.d.ts +1 -0
  61. package/dist/elements/EFMedia/services/AudioElementFactory.d.ts +22 -0
  62. package/dist/elements/EFMedia/services/AudioElementFactory.js +72 -0
  63. package/dist/elements/EFMedia/services/MediaSourceService.browsertest.d.ts +1 -0
  64. package/dist/elements/EFMedia/services/MediaSourceService.d.ts +47 -0
  65. package/dist/elements/EFMedia/services/MediaSourceService.js +73 -0
  66. package/dist/elements/EFMedia/shared/AudioSpanUtils.d.ts +7 -0
  67. package/dist/elements/EFMedia/shared/AudioSpanUtils.js +54 -0
  68. package/dist/elements/EFMedia/shared/BufferUtils.d.ts +70 -0
  69. package/dist/elements/EFMedia/shared/BufferUtils.js +89 -0
  70. package/dist/elements/EFMedia/shared/MediaTaskUtils.d.ts +23 -0
  71. package/dist/elements/EFMedia/shared/RenditionHelpers.browsertest.d.ts +1 -0
  72. package/dist/elements/EFMedia/shared/RenditionHelpers.d.ts +19 -0
  73. package/dist/elements/EFMedia/tasks/makeMediaEngineTask.browsertest.d.ts +1 -0
  74. package/dist/elements/EFMedia/tasks/makeMediaEngineTask.d.ts +18 -0
  75. package/dist/elements/EFMedia/tasks/makeMediaEngineTask.js +60 -0
  76. package/dist/elements/EFMedia/tasks/makeMediaEngineTask.test.d.ts +1 -0
  77. package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.browsertest.d.ts +9 -0
  78. package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.d.ts +16 -0
  79. package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.js +46 -0
  80. package/dist/elements/EFMedia/videoTasks/makeVideoInitSegmentFetchTask.browsertest.d.ts +9 -0
  81. package/dist/elements/EFMedia/videoTasks/makeVideoInitSegmentFetchTask.d.ts +4 -0
  82. package/dist/elements/EFMedia/videoTasks/makeVideoInitSegmentFetchTask.js +16 -0
  83. package/dist/elements/EFMedia/videoTasks/makeVideoInputTask.browsertest.d.ts +9 -0
  84. package/dist/elements/EFMedia/videoTasks/makeVideoInputTask.d.ts +3 -0
  85. package/dist/elements/EFMedia/videoTasks/makeVideoInputTask.js +27 -0
  86. package/dist/elements/EFMedia/videoTasks/makeVideoSeekTask.d.ts +7 -0
  87. package/dist/elements/EFMedia/videoTasks/makeVideoSeekTask.js +25 -0
  88. package/dist/elements/EFMedia/videoTasks/makeVideoSegmentFetchTask.browsertest.d.ts +9 -0
  89. package/dist/elements/EFMedia/videoTasks/makeVideoSegmentFetchTask.d.ts +4 -0
  90. package/dist/elements/EFMedia/videoTasks/makeVideoSegmentFetchTask.js +18 -0
  91. package/dist/elements/EFMedia/videoTasks/makeVideoSegmentIdTask.browsertest.d.ts +9 -0
  92. package/dist/elements/EFMedia/videoTasks/makeVideoSegmentIdTask.d.ts +4 -0
  93. package/dist/elements/EFMedia/videoTasks/makeVideoSegmentIdTask.js +16 -0
  94. package/dist/elements/EFMedia.browsertest.d.ts +1 -0
  95. package/dist/elements/EFMedia.d.ts +95 -66
  96. package/dist/elements/EFMedia.js +204 -683
  97. package/dist/elements/EFSourceMixin.js +31 -48
  98. package/dist/elements/EFTemporal.d.ts +2 -1
  99. package/dist/elements/EFTemporal.js +266 -360
  100. package/dist/elements/EFTimegroup.d.ts +14 -1
  101. package/dist/elements/EFTimegroup.js +337 -323
  102. package/dist/elements/EFVideo.browsertest.d.ts +0 -0
  103. package/dist/elements/EFVideo.d.ts +123 -4
  104. package/dist/elements/EFVideo.js +308 -111
  105. package/dist/elements/EFWaveform.js +375 -411
  106. package/dist/elements/FetchMixin.js +14 -24
  107. package/dist/elements/MediaController.d.ts +30 -0
  108. package/dist/elements/SampleBuffer.d.ts +14 -0
  109. package/dist/elements/SampleBuffer.js +52 -0
  110. package/dist/elements/TargetController.js +130 -156
  111. package/dist/elements/TimegroupController.js +17 -19
  112. package/dist/elements/durationConverter.js +15 -4
  113. package/dist/elements/parseTimeToMs.js +4 -10
  114. package/dist/elements/printTaskStatus.d.ts +2 -0
  115. package/dist/elements/updateAnimations.js +39 -59
  116. package/dist/getRenderInfo.d.ts +2 -2
  117. package/dist/getRenderInfo.js +59 -67
  118. package/dist/gui/ContextMixin.js +150 -288
  119. package/dist/gui/EFConfiguration.js +27 -43
  120. package/dist/gui/EFFilmstrip.d.ts +3 -3
  121. package/dist/gui/EFFilmstrip.js +440 -620
  122. package/dist/gui/EFFitScale.d.ts +2 -2
  123. package/dist/gui/EFFitScale.js +112 -135
  124. package/dist/gui/EFFocusOverlay.js +45 -61
  125. package/dist/gui/EFPreview.js +30 -49
  126. package/dist/gui/EFScrubber.js +78 -99
  127. package/dist/gui/EFTimeDisplay.js +49 -70
  128. package/dist/gui/EFToggleLoop.js +17 -34
  129. package/dist/gui/EFTogglePlay.js +37 -58
  130. package/dist/gui/EFWorkbench.js +66 -88
  131. package/dist/gui/TWMixin.js +2 -48
  132. package/dist/gui/TWMixin2.js +31 -0
  133. package/dist/gui/efContext.js +2 -6
  134. package/dist/gui/fetchContext.js +1 -3
  135. package/dist/gui/focusContext.js +1 -3
  136. package/dist/gui/focusedElementContext.js +2 -6
  137. package/dist/gui/playingContext.js +1 -4
  138. package/dist/gui/services/ElementConnectionManager.browsertest.d.ts +1 -0
  139. package/dist/gui/services/ElementConnectionManager.d.ts +59 -0
  140. package/dist/gui/services/ElementConnectionManager.js +128 -0
  141. package/dist/gui/services/PlaybackController.browsertest.d.ts +1 -0
  142. package/dist/gui/services/PlaybackController.d.ts +103 -0
  143. package/dist/gui/services/PlaybackController.js +290 -0
  144. package/dist/index.js +5 -30
  145. package/dist/msToTimeCode.js +11 -13
  146. package/dist/services/MediaSourceManager.d.ts +62 -0
  147. package/dist/services/MediaSourceManager.js +211 -0
  148. package/dist/style.css +2 -1
  149. package/dist/transcoding/cache/CacheManager.d.ts +73 -0
  150. package/dist/transcoding/cache/RequestDeduplicator.d.ts +29 -0
  151. package/dist/transcoding/cache/RequestDeduplicator.js +53 -0
  152. package/dist/transcoding/cache/RequestDeduplicator.test.d.ts +1 -0
  153. package/dist/transcoding/types/index.d.ts +242 -0
  154. package/dist/transcoding/utils/MediaUtils.d.ts +9 -0
  155. package/dist/transcoding/utils/UrlGenerator.d.ts +26 -0
  156. package/dist/transcoding/utils/UrlGenerator.js +45 -0
  157. package/dist/transcoding/utils/constants.d.ts +27 -0
  158. package/dist/utils/LRUCache.d.ts +34 -0
  159. package/dist/utils/LRUCache.js +115 -0
  160. package/package.json +4 -3
  161. package/src/elements/EFAudio.browsertest.ts +709 -0
  162. package/src/elements/EFAudio.ts +59 -15
  163. package/src/elements/EFCaptions.browsertest.ts +0 -1
  164. package/src/elements/EFImage.browsertest.ts +42 -1
  165. package/src/elements/EFImage.ts +23 -3
  166. package/src/elements/EFMedia/AssetIdMediaEngine.test.ts +222 -0
  167. package/src/elements/EFMedia/AssetIdMediaEngine.ts +70 -0
  168. package/src/elements/EFMedia/AssetMediaEngine.ts +210 -0
  169. package/src/elements/EFMedia/BaseMediaEngine.test.ts +164 -0
  170. package/src/elements/EFMedia/BaseMediaEngine.ts +170 -0
  171. package/src/elements/EFMedia/BufferedSeekingInput.browsertest.ts +400 -0
  172. package/src/elements/EFMedia/BufferedSeekingInput.ts +267 -0
  173. package/src/elements/EFMedia/JitMediaEngine.browsertest.ts +165 -0
  174. package/src/elements/EFMedia/JitMediaEngine.ts +110 -0
  175. package/src/elements/EFMedia/audioTasks/makeAudioBufferTask.browsertest.ts +554 -0
  176. package/src/elements/EFMedia/audioTasks/makeAudioBufferTask.ts +81 -0
  177. package/src/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.ts +241 -0
  178. package/src/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.browsertest.ts +59 -0
  179. package/src/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.ts +23 -0
  180. package/src/elements/EFMedia/audioTasks/makeAudioInputTask.browsertest.ts +55 -0
  181. package/src/elements/EFMedia/audioTasks/makeAudioInputTask.ts +35 -0
  182. package/src/elements/EFMedia/audioTasks/makeAudioSeekTask.ts +42 -0
  183. package/src/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.ts +34 -0
  184. package/src/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.ts +23 -0
  185. package/src/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.ts +174 -0
  186. package/src/elements/EFMedia/services/AudioElementFactory.browsertest.ts +325 -0
  187. package/src/elements/EFMedia/services/AudioElementFactory.ts +119 -0
  188. package/src/elements/EFMedia/services/MediaSourceService.browsertest.ts +257 -0
  189. package/src/elements/EFMedia/services/MediaSourceService.ts +102 -0
  190. package/src/elements/EFMedia/shared/AudioSpanUtils.ts +128 -0
  191. package/src/elements/EFMedia/shared/BufferUtils.ts +310 -0
  192. package/src/elements/EFMedia/shared/MediaTaskUtils.ts +44 -0
  193. package/src/elements/EFMedia/shared/RenditionHelpers.browsertest.ts +247 -0
  194. package/src/elements/EFMedia/shared/RenditionHelpers.ts +79 -0
  195. package/src/elements/EFMedia/tasks/makeMediaEngineTask.browsertest.ts +128 -0
  196. package/src/elements/EFMedia/tasks/makeMediaEngineTask.test.ts +233 -0
  197. package/src/elements/EFMedia/tasks/makeMediaEngineTask.ts +89 -0
  198. package/src/elements/EFMedia/videoTasks/makeVideoBufferTask.browsertest.ts +555 -0
  199. package/src/elements/EFMedia/videoTasks/makeVideoBufferTask.ts +79 -0
  200. package/src/elements/EFMedia/videoTasks/makeVideoInitSegmentFetchTask.browsertest.ts +59 -0
  201. package/src/elements/EFMedia/videoTasks/makeVideoInitSegmentFetchTask.ts +23 -0
  202. package/src/elements/EFMedia/videoTasks/makeVideoInputTask.browsertest.ts +55 -0
  203. package/src/elements/EFMedia/videoTasks/makeVideoInputTask.ts +45 -0
  204. package/src/elements/EFMedia/videoTasks/makeVideoSeekTask.ts +44 -0
  205. package/src/elements/EFMedia/videoTasks/makeVideoSegmentFetchTask.browsertest.ts +57 -0
  206. package/src/elements/EFMedia/videoTasks/makeVideoSegmentFetchTask.ts +32 -0
  207. package/src/elements/EFMedia/videoTasks/makeVideoSegmentIdTask.browsertest.ts +56 -0
  208. package/src/elements/EFMedia/videoTasks/makeVideoSegmentIdTask.ts +23 -0
  209. package/src/elements/EFMedia.browsertest.ts +696 -271
  210. package/src/elements/EFMedia.ts +218 -776
  211. package/src/elements/EFTemporal.browsertest.ts +0 -1
  212. package/src/elements/EFTemporal.ts +13 -3
  213. package/src/elements/EFTimegroup.browsertest.ts +6 -3
  214. package/src/elements/EFTimegroup.ts +221 -27
  215. package/src/elements/EFVideo.browsertest.ts +758 -0
  216. package/src/elements/EFVideo.ts +418 -68
  217. package/src/elements/EFWaveform.ts +5 -5
  218. package/src/elements/MediaController.ts +98 -0
  219. package/src/elements/SampleBuffer.ts +97 -0
  220. package/src/elements/printTaskStatus.ts +16 -0
  221. package/src/elements/updateAnimations.ts +6 -0
  222. package/src/gui/ContextMixin.ts +23 -104
  223. package/src/gui/TWMixin.ts +10 -3
  224. package/src/gui/services/ElementConnectionManager.browsertest.ts +263 -0
  225. package/src/gui/services/ElementConnectionManager.ts +224 -0
  226. package/src/gui/services/PlaybackController.browsertest.ts +437 -0
  227. package/src/gui/services/PlaybackController.ts +521 -0
  228. package/src/services/MediaSourceManager.ts +333 -0
  229. package/src/transcoding/cache/CacheManager.ts +208 -0
  230. package/src/transcoding/cache/RequestDeduplicator.test.ts +170 -0
  231. package/src/transcoding/cache/RequestDeduplicator.ts +65 -0
  232. package/src/transcoding/types/index.ts +265 -0
  233. package/src/transcoding/utils/MediaUtils.ts +63 -0
  234. package/src/transcoding/utils/UrlGenerator.ts +68 -0
  235. package/src/transcoding/utils/constants.ts +36 -0
  236. package/src/utils/LRUCache.ts +153 -0
  237. package/test/EFVideo.framegen.browsertest.ts +127 -0
  238. 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
  239. 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
  240. 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
  241. 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
  242. 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
  243. 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
  244. 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
  245. 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
  246. 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
  247. 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
  248. 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
  249. 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
  250. 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
  251. 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
  252. 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
  253. 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
  254. 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
  255. 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
  256. 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
  257. 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
  258. package/test/__cache__/GET__api_v1_transcode_manifest_json_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__3be92a0437de726b431ed5af2369158a/data.bin +1 -0
  259. package/test/__cache__/GET__api_v1_transcode_manifest_json_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__3be92a0437de726b431ed5af2369158a/metadata.json +19 -0
  260. package/test/createJitTestClips.ts +425 -0
  261. package/test/recordReplayProxyPlugin.js +302 -0
  262. package/test/useAssetMSW.ts +49 -0
  263. package/test/useMSW.ts +44 -0
  264. package/types.json +1 -1
  265. package/dist/gui/TWMixin.css.js +0 -4
  266. /package/dist/elements/{TargetController.test.d.ts → TargetController.browsertest.d.ts} +0 -0
  267. /package/src/elements/{TargetController.test.ts → TargetController.browsertest.ts} +0 -0
@@ -1,56 +1,36 @@
1
- import { Task } from "@lit/task";
2
- import { deepArrayEquals } from "@lit/task/deep-equals.js";
3
- import debug from "debug";
4
- import { LitElement, type PropertyValueMap, css } from "lit";
1
+ import { css, LitElement, type PropertyValueMap } from "lit";
5
2
  import { property, state } from "lit/decorators.js";
6
- import type * as MP4Box from "mp4box";
7
3
 
8
- import type { TrackFragmentIndex, TrackSegment } from "@editframe/assets";
9
-
10
- import { VideoAsset } from "@editframe/assets/EncodedAsset.js";
11
- import { MP4File } from "@editframe/assets/MP4File.js";
12
- import { EF_INTERACTIVE } from "../EF_INTERACTIVE.js";
4
+ import type { AudioSpan } from "../transcoding/types/index.ts";
5
+ import { UrlGenerator } from "../transcoding/utils/UrlGenerator.ts";
6
+ // Audio task imports
7
+ import { makeAudioBufferTask } from "./EFMedia/audioTasks/makeAudioBufferTask.ts";
8
+ import { makeAudioFrequencyAnalysisTask } from "./EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.ts";
9
+ import { makeAudioInitSegmentFetchTask } from "./EFMedia/audioTasks/makeAudioInitSegmentFetchTask.ts";
10
+ import { makeAudioInputTask } from "./EFMedia/audioTasks/makeAudioInputTask.ts";
11
+ import { makeAudioSeekTask } from "./EFMedia/audioTasks/makeAudioSeekTask.ts";
12
+ import { makeAudioSegmentFetchTask } from "./EFMedia/audioTasks/makeAudioSegmentFetchTask.ts";
13
+ import { makeAudioSegmentIdTask } from "./EFMedia/audioTasks/makeAudioSegmentIdTask.ts";
14
+ import { makeAudioTimeDomainAnalysisTask } from "./EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.ts";
15
+ import { AudioElementFactory } from "./EFMedia/services/AudioElementFactory.js";
16
+ // Import extracted services and utilities
17
+ import { MediaSourceService } from "./EFMedia/services/MediaSourceService.js";
18
+ // Common task imports
19
+ import { makeMediaEngineTask } from "./EFMedia/tasks/makeMediaEngineTask.ts";
13
20
  import { EFSourceMixin } from "./EFSourceMixin.js";
14
21
  import { EFTemporal } from "./EFTemporal.js";
15
22
  import { FetchMixin } from "./FetchMixin.js";
16
23
  import { EFTargetable } from "./TargetController.ts";
17
24
  import { updateAnimations } from "./updateAnimations.ts";
18
25
 
19
- const log = debug("ef:elements:EFMedia");
26
+ // EF_FRAMEGEN is a global instance created in EF_FRAMEGEN.ts
27
+ declare global {
28
+ var EF_FRAMEGEN: import("../EF_FRAMEGEN.js").EFFramegen;
29
+ }
20
30
 
21
31
  const freqWeightsCache = new Map<number, Float32Array>();
22
32
 
23
- class LRUCache<K, V> {
24
- private cache = new Map<K, V>();
25
- private readonly maxSize: number;
26
-
27
- constructor(maxSize: number) {
28
- this.maxSize = maxSize;
29
- }
30
-
31
- get(key: K): V | undefined {
32
- const value = this.cache.get(key);
33
- if (value) {
34
- // Refresh position by removing and re-adding
35
- this.cache.delete(key);
36
- this.cache.set(key, value);
37
- }
38
- return value;
39
- }
40
-
41
- set(key: K, value: V): void {
42
- if (this.cache.has(key)) {
43
- this.cache.delete(key);
44
- } else if (this.cache.size >= this.maxSize) {
45
- // Remove oldest entry (first item in map)
46
- const firstKey = this.cache.keys().next().value;
47
- if (firstKey) {
48
- this.cache.delete(firstKey);
49
- }
50
- }
51
- this.cache.set(key, value);
52
- }
53
- }
33
+ export class IgnorableError extends Error {}
54
34
 
55
35
  export const deepGetMediaElements = (
56
36
  element: Element,
@@ -71,6 +51,37 @@ export class EFMedia extends EFTargetable(
71
51
  assetType: "isobmff_files",
72
52
  }),
73
53
  ) {
54
+ // Sample buffer size configuration
55
+ static readonly VIDEO_SAMPLE_BUFFER_SIZE = 30;
56
+ static readonly AUDIO_SAMPLE_BUFFER_SIZE = 120;
57
+
58
+ static get observedAttributes() {
59
+ // biome-ignore lint/complexity/noThisInStatic: We need to access super
60
+ const parentAttributes = super.observedAttributes || [];
61
+ return [
62
+ ...parentAttributes,
63
+ "mute",
64
+ "fft-size",
65
+ "fft-decay",
66
+ "fft-gain",
67
+ "interpolate-frequencies",
68
+ "asset-id",
69
+ "audio-buffer-duration",
70
+ "max-audio-buffer-fetches",
71
+ "enable-audio-buffering",
72
+ ];
73
+ }
74
+
75
+ // Services for media source and audio element management
76
+ private mediaSourceService = new MediaSourceService({
77
+ onError: (error) => {
78
+ console.error("🎵 [EFMedia] MediaSourceService error:", error);
79
+ },
80
+ onReady: () => {},
81
+ });
82
+
83
+ private audioElementFactory = new AudioElementFactory();
84
+
74
85
  static styles = [
75
86
  css`
76
87
  :host {
@@ -84,261 +95,134 @@ export class EFMedia extends EFTargetable(
84
95
  @property({ type: Number })
85
96
  currentTimeMs = 0;
86
97
 
87
- #assetId: string | null = null;
98
+ /**
99
+ * Duration in milliseconds for audio buffering ahead of current time
100
+ * @domAttribute "audio-buffer-duration"
101
+ */
102
+ @property({ type: Number, attribute: "audio-buffer-duration" })
103
+ audioBufferDurationMs = 30000; // 30 seconds
88
104
 
89
105
  /**
90
- * The unique identifier for the media asset.
91
- * This property can be set programmatically or via the "asset-id" attribute.
92
- * @domAttribute "asset-id"
106
+ * Maximum number of concurrent audio segment fetches for buffering
107
+ * @domAttribute "max-audio-buffer-fetches"
93
108
  */
94
- @property({ type: String, attribute: "asset-id", reflect: true })
95
- set assetId(value: string | null) {
96
- this.#assetId = value;
97
- }
109
+ @property({ type: Number, attribute: "max-audio-buffer-fetches" })
110
+ maxAudioBufferFetches = 2;
98
111
 
99
- get assetId() {
100
- return this.#assetId || this.getAttribute("asset-id");
101
- }
112
+ /**
113
+ * Enable/disable audio buffering system
114
+ * @domAttribute "enable-audio-buffering"
115
+ */
116
+ @property({ type: Boolean, attribute: "enable-audio-buffering" })
117
+ enableAudioBuffering = true;
102
118
 
103
- fragmentIndexPath() {
104
- if (this.assetId) {
105
- return `${this.apiHost}/api/v1/isobmff_files/${this.assetId}/index`;
106
- }
107
- return `/@ef-track-fragment-index/${this.src ?? ""}`;
108
- }
119
+ /**
120
+ * Mute/unmute the media element
121
+ * @domAttribute "mute"
122
+ */
123
+ @property({
124
+ type: Boolean,
125
+ attribute: "mute",
126
+ reflect: true,
127
+ })
128
+ mute = false;
109
129
 
110
- fragmentTrackPath(trackId: string) {
111
- if (this.assetId) {
112
- return `${this.apiHost}/api/v1/isobmff_tracks/${this.assetId}/${trackId}`;
113
- }
114
- // trackId is only specified as a query in the @ef-track url shape
115
- // this is because that system doesn't have a full url matching system.
116
- // This is an annoying incosistency that should be fixed.
117
- return `/@ef-track/${this.src ?? ""}?trackId=${trackId}`;
118
- }
130
+ /**
131
+ * FFT size for frequency analysis
132
+ * @domAttribute "fft-size"
133
+ */
134
+ @property({ type: Number, attribute: "fft-size", reflect: true })
135
+ fftSize = 128;
119
136
 
120
- public trackFragmentIndexLoader = new Task(this, {
121
- args: () => [this.fragmentIndexPath(), this.fetch] as const,
122
- task: async ([fragmentIndexPath, fetch], { signal }) => {
123
- try {
124
- const response = await fetch(fragmentIndexPath, { signal });
137
+ /**
138
+ * FFT decay rate for frequency analysis
139
+ * @domAttribute "fft-decay"
140
+ */
141
+ @property({ type: Number, attribute: "fft-decay", reflect: true })
142
+ fftDecay = 8;
125
143
 
126
- return (await response.json()) as Record<number, TrackFragmentIndex>;
127
- } catch (error) {
128
- log("Failed to load track fragment index", error);
129
- return undefined;
130
- }
131
- },
132
- onComplete: () => {
133
- this.requestUpdate("intrinsicDurationMs");
134
- this.requestUpdate("ownCurrentTimeMs");
135
- this.rootTimegroup?.requestUpdate("ownCurrentTimeMs");
136
- this.rootTimegroup?.requestUpdate("durationMs");
137
- },
138
- });
144
+ /**
145
+ * FFT gain for frequency analysis
146
+ * @domAttribute "fft-gain"
147
+ */
148
+ @property({ type: Number, attribute: "fft-gain", reflect: true })
149
+ fftGain = 3.0;
139
150
 
140
- public initSegmentsLoader = new Task(this, {
141
- autoRun: EF_INTERACTIVE,
142
- args: () =>
143
- [this.trackFragmentIndexLoader.value, this.src, this.fetch] as const,
144
- task: async ([fragmentIndex, _src, fetch], { signal }) => {
145
- if (!fragmentIndex) {
146
- return;
147
- }
148
- return await Promise.all(
149
- Object.entries(fragmentIndex).map(async ([trackId, track]) => {
150
- const start = track.initSegment.offset;
151
- const end = track.initSegment.offset + track.initSegment.size;
152
- const response = await fetch(this.fragmentTrackPath(trackId), {
153
- signal,
154
- headers: { Range: `bytes=${start}-${end - 1}` },
155
- });
156
- const buffer =
157
- (await response.arrayBuffer()) as MP4Box.MP4ArrayBuffer;
158
- buffer.fileStart = 0;
159
- const mp4File = new MP4File();
160
- mp4File.appendBuffer(buffer, true);
161
- mp4File.flush();
162
- await mp4File.readyPromise;
163
-
164
- return { trackId, buffer, mp4File };
165
- }),
166
- );
167
- },
168
- });
151
+ /**
152
+ * Enable/disable frequency interpolation
153
+ * @domAttribute "interpolate-frequencies"
154
+ */
155
+ @property({
156
+ type: Boolean,
157
+ attribute: "interpolate-frequencies",
158
+ reflect: true,
159
+ })
160
+ interpolateFrequencies = false;
169
161
 
170
- get defaultVideoTrackId() {
171
- return Object.values(this.trackFragmentIndexLoader.value ?? {}).find(
172
- (track) => track.type === "video",
173
- )?.track;
174
- }
162
+ // Update FREQ_WEIGHTS to use the instance fftSize instead of a static value
163
+ get FREQ_WEIGHTS() {
164
+ if (freqWeightsCache.has(this.fftSize)) {
165
+ // biome-ignore lint/style/noNonNullAssertion: We know the value is set due to the guard above
166
+ return freqWeightsCache.get(this.fftSize)!;
167
+ }
175
168
 
176
- get defaultAudioTrackId() {
177
- return Object.values(this.trackFragmentIndexLoader.value ?? {}).find(
178
- (track) => track.type === "audio",
179
- )?.track;
180
- }
169
+ const weights = new Float32Array(this.fftSize / 2).map((_, i) => {
170
+ const frequency = (i * 48000) / this.fftSize;
171
+ if (frequency < 60) return 0.3;
172
+ if (frequency < 250) return 0.4;
173
+ if (frequency < 500) return 0.6;
174
+ if (frequency < 2000) return 0.8;
175
+ if (frequency < 4000) return 1.2;
176
+ if (frequency < 8000) return 1.6;
177
+ return 2.0;
178
+ });
181
179
 
182
- seekTask = new Task(this, {
183
- autoRun: EF_INTERACTIVE,
184
- args: () =>
185
- [
186
- this.desiredSeekTimeMs,
187
- this.trackFragmentIndexLoader.value,
188
- this.initSegmentsLoader.value,
189
- ] as const,
190
- task: async (
191
- [seekToMs, fragmentIndex, initSegments],
192
- { signal: _signal },
193
- ) => {
194
- if (fragmentIndex === undefined) {
195
- return;
196
- }
197
- if (initSegments === undefined) {
198
- return;
199
- }
180
+ freqWeightsCache.set(this.fftSize, weights);
181
+ return weights;
182
+ }
200
183
 
201
- const result: Record<
202
- string,
203
- {
204
- segment: TrackSegment;
205
- track: MP4Box.TrackInfo;
206
- nextSegment?: TrackSegment;
207
- }
208
- > = {};
209
-
210
- for (const index of Object.values(fragmentIndex)) {
211
- const track = initSegments
212
- .find((segment) => segment.trackId === String(index.track))
213
- ?.mp4File.getInfo().tracks[0];
214
-
215
- if (!track) {
216
- throw new Error("Could not finding matching track");
217
- }
218
-
219
- const segment = index.segments.toReversed().find((segment) => {
220
- return (segment.dts / track.timescale) * 1000 <= seekToMs;
221
- });
222
-
223
- const nextSegment = index.segments.find((segment) => {
224
- return (segment.dts / track.timescale) * 1000 > seekToMs;
225
- });
226
-
227
- if (!segment) {
228
- return;
229
- }
230
-
231
- result[index.track] = { segment, track, nextSegment };
232
- }
184
+ // Helper getter for backwards compatibility
185
+ get shouldInterpolateFrequencies() {
186
+ return this.interpolateFrequencies;
187
+ }
233
188
 
234
- return result;
235
- },
236
- });
189
+ get urlGenerator() {
190
+ return new UrlGenerator(() => this.apiHost ?? "");
191
+ }
237
192
 
238
- fetchSeekTask = new Task(this, {
239
- autoRun: EF_INTERACTIVE,
240
- argsEqual: deepArrayEquals,
241
- args: () =>
242
- [this.initSegmentsLoader.value, this.seekTask.value, this.fetch] as const,
243
- task: async ([initSegments, seekResult, fetch], { signal }) => {
244
- if (!initSegments) {
245
- return;
246
- }
247
- if (!seekResult) {
248
- return;
249
- }
193
+ mediaEngineTask = makeMediaEngineTask(this);
250
194
 
251
- const files: Record<string, File> = {};
252
-
253
- for (const [trackId, { segment, track, nextSegment }] of Object.entries(
254
- seekResult,
255
- )) {
256
- const start = segment.offset;
257
- const end = segment.offset + segment.size;
258
-
259
- const response = await fetch(this.fragmentTrackPath(trackId), {
260
- signal,
261
- headers: { Range: `bytes=${start}-${end - 1}` },
262
- });
263
-
264
- if (nextSegment) {
265
- const nextStart = nextSegment.offset;
266
- const nextEnd = nextSegment.offset + nextSegment.size;
267
- fetch(this.fragmentTrackPath(trackId), {
268
- signal,
269
- headers: { Range: `bytes=${nextStart}-${nextEnd - 1}` },
270
- })
271
- .then(() => {
272
- log("Prefetched next segment");
273
- })
274
- .catch((error) => {
275
- log("Failed to prefetch next segment", error);
276
- });
277
- }
278
-
279
- const initSegment = Object.values(initSegments).find(
280
- (initSegment) => initSegment.trackId === String(track.id),
281
- );
282
- if (!initSegment) {
283
- throw new Error("Could not find matching init segment");
284
- }
285
- const initBuffer = initSegment.buffer;
286
-
287
- const mediaBuffer =
288
- (await response.arrayBuffer()) as unknown as MP4Box.MP4ArrayBuffer;
289
-
290
- files[trackId] = new File([initBuffer, mediaBuffer], "video.mp4", {
291
- type: "video/mp4",
292
- });
293
- }
195
+ audioSegmentIdTask = makeAudioSegmentIdTask(this);
196
+ audioInitSegmentFetchTask = makeAudioInitSegmentFetchTask(this);
197
+ audioSegmentFetchTask = makeAudioSegmentFetchTask(this);
198
+ audioInputTask = makeAudioInputTask(this);
199
+ audioSeekTask = makeAudioSeekTask(this);
294
200
 
295
- return files;
296
- },
297
- });
201
+ audioBufferTask = makeAudioBufferTask(this);
298
202
 
299
- videoAssetTask = new Task(this, {
300
- autoRun: EF_INTERACTIVE,
301
- args: () => [this.fetchSeekTask.value] as const,
302
- task: async ([files], { signal: _signal }) => {
303
- if (!files) {
304
- return;
305
- }
306
- if (!this.defaultVideoTrackId) {
307
- return;
308
- }
309
- const videoFile = files[this.defaultVideoTrackId];
310
- if (!videoFile) {
311
- return;
312
- }
313
- // TODO: Extract to general cleanup function
314
- for (const frame of this.videoAssetTask.value?.decodedFrames || []) {
315
- frame.close();
316
- }
317
- this.videoAssetTask.value?.videoDecoder?.close();
318
- return await VideoAsset.createFromReadableStream(
319
- "video.mp4",
320
- videoFile.stream(),
321
- videoFile,
322
- );
323
- },
324
- });
203
+ // Audio analysis tasks for frequency and time domain analysis
204
+ byteTimeDomainTask = makeAudioTimeDomainAnalysisTask(this);
205
+ frequencyDataTask = makeAudioFrequencyAnalysisTask(this);
325
206
 
326
- @state()
327
- desiredSeekTimeMs = 0;
207
+ /**
208
+ * The unique identifier for the media asset.
209
+ * This property can be set programmatically or via the "asset-id" attribute.
210
+ * @domAttribute "asset-id"
211
+ */
212
+ @property({ type: String, attribute: "asset-id", reflect: true })
213
+ assetId: string | null = null;
328
214
 
329
- protected async executeSeek(seekToMs: number) {
330
- this.desiredSeekTimeMs = seekToMs;
215
+ get intrinsicDurationMs() {
216
+ return this.mediaEngineTask.value?.durationMs ?? 0;
331
217
  }
332
218
 
333
219
  protected updated(
334
220
  changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>,
335
221
  ): void {
222
+ super.updated(changedProperties);
336
223
  if (changedProperties.has("ownCurrentTimeMs")) {
337
224
  this.executeSeek(this.currentSourceTimeMs);
338
225
  }
339
- // TODO: this is copied straight from EFTimegroup.ts
340
- // and should be refactored to be shared/reduce bad duplication of
341
- // critical logic.
342
226
  if (
343
227
  changedProperties.has("currentTime") ||
344
228
  changedProperties.has("ownCurrentTimeMs")
@@ -351,534 +235,92 @@ export class EFMedia extends EFTargetable(
351
235
  return true;
352
236
  }
353
237
 
354
- get intrinsicDurationMs() {
355
- if (!this.trackFragmentIndexLoader.value) {
356
- return 0;
357
- }
238
+ @state()
239
+ private _desiredSeekTimeMs = 0; // Initialize to 0 for proper segment loading
358
240
 
359
- const durations = Object.values(this.trackFragmentIndexLoader.value).map(
360
- (track) => {
361
- return (track.duration / track.timescale) * 1000;
362
- },
363
- );
364
- if (durations.length === 0) {
365
- return 0;
366
- }
367
- return Math.max(...durations);
241
+ get desiredSeekTimeMs() {
242
+ return this._desiredSeekTimeMs;
368
243
  }
369
244
 
370
- #audioContext = new OfflineAudioContext(2, 48000 / 30, 48000);
371
-
372
- audioBufferTask = new Task(this, {
373
- autoRun: EF_INTERACTIVE,
374
- args: () => [this.fetchSeekTask.value, this.seekTask.value] as const,
375
- task: async ([files, segments], { signal: _signal }) => {
376
- if (!files) {
377
- return;
378
- }
379
- if (!segments) {
380
- return;
381
- }
382
- if (!this.defaultAudioTrackId) {
383
- return;
384
- }
385
- const segment = segments[this.defaultAudioTrackId];
386
- if (!segment) {
387
- return;
388
- }
389
- const audioFile = files[this.defaultAudioTrackId];
390
- if (!audioFile) {
391
- return;
392
- }
393
- return {
394
- buffer: await this.#audioContext.decodeAudioData(
395
- await audioFile.arrayBuffer(),
396
- ),
397
- startOffsetMs: (segment.segment.cts / segment.track.timescale) * 1000,
398
- };
399
- },
400
- });
401
-
402
- async fetchAudioSpanningTime(fromMs: number, toMs: number) {
403
- // Adjust range for track's own time
404
- if (this.sourceInMs) {
405
- fromMs -=
406
- this.startTimeMs - (this.trimStartMs ?? 0) - (this.sourceInMs ?? 0);
407
- }
408
- if (this.sourceOutMs) {
409
- toMs -=
410
- this.startTimeMs - (this.trimStartMs ?? 0) - (this.sourceOutMs ?? 0);
411
- }
412
- fromMs -= this.startTimeMs - (this.trimStartMs ?? 0);
413
- toMs -= this.startTimeMs - (this.trimStartMs ?? 0);
414
-
415
- await this.trackFragmentIndexLoader.taskComplete;
416
- const audioTrackId = this.defaultAudioTrackId;
417
- if (!audioTrackId) {
418
- log("No audio track found");
419
- return;
245
+ set desiredSeekTimeMs(value: number) {
246
+ if (this._desiredSeekTimeMs !== value) {
247
+ this._desiredSeekTimeMs = value;
420
248
  }
421
-
422
- const audioTrackIndex = this.trackFragmentIndexLoader.value?.[audioTrackId];
423
- if (!audioTrackIndex) {
424
- log("No audio track found");
425
- return;
426
- }
427
-
428
- const start = audioTrackIndex.initSegment.offset;
429
- const end =
430
- audioTrackIndex.initSegment.offset + audioTrackIndex.initSegment.size;
431
- const audioInitFragmentRequest = this.fetch(
432
- this.fragmentTrackPath(String(audioTrackId)),
433
- {
434
- headers: { Range: `bytes=${start}-${end - 1}` },
435
- },
436
- );
437
-
438
- const fragments = Object.values(audioTrackIndex.segments).filter(
439
- (segment) => {
440
- const segmentStartsBeforeEnd =
441
- segment.dts <= (toMs * audioTrackIndex.timescale) / 1000;
442
- const segmentEndsAfterStart =
443
- segment.dts + segment.duration >=
444
- (fromMs * audioTrackIndex.timescale) / 1000;
445
- return segmentStartsBeforeEnd && segmentEndsAfterStart;
446
- },
447
- );
448
-
449
- const firstFragment = fragments[0];
450
- if (!firstFragment) {
451
- log("No audio fragments found");
452
- return;
453
- }
454
- const lastFragment = fragments[fragments.length - 1];
455
- if (!lastFragment) {
456
- log("No audio fragments found");
457
- return;
458
- }
459
- const fragmentStart = firstFragment.offset;
460
- const fragmentEnd = lastFragment.offset + lastFragment.size;
461
-
462
- const audioFragmentRequest = this.fetch(
463
- this.fragmentTrackPath(String(audioTrackId)),
464
- {
465
- headers: { Range: `bytes=${fragmentStart}-${fragmentEnd - 1}` },
466
- },
467
- );
468
-
469
- const initResponse = await audioInitFragmentRequest;
470
- const dataResponse = await audioFragmentRequest;
471
-
472
- const initBuffer = await initResponse.arrayBuffer();
473
- const dataBuffer = await dataResponse.arrayBuffer();
474
-
475
- const audioBlob = new Blob([initBuffer, dataBuffer], {
476
- type: "audio/mp4",
477
- });
478
-
479
- return {
480
- blob: audioBlob,
481
- startMs:
482
- (firstFragment.dts / audioTrackIndex.timescale) * 1000 -
483
- (this.trimStartMs ?? 0),
484
- endMs:
485
- (lastFragment.dts / audioTrackIndex.timescale) * 1000 +
486
- (lastFragment.duration / audioTrackIndex.timescale) * 1000 -
487
- (this.trimEndMs ?? 0),
488
- };
489
- }
490
-
491
- set fftSize(value: number) {
492
- const oldValue = this.fftSize;
493
- this.setAttribute("fft-size", String(value));
494
- this.requestUpdate("fft-size", oldValue);
495
- }
496
-
497
- set fftDecay(value: number) {
498
- const oldValue = this.fftDecay;
499
- this.setAttribute("fft-decay", String(value));
500
- this.requestUpdate("fft-decay", oldValue);
501
- }
502
-
503
- get fftSize() {
504
- return Number.parseInt(this.getAttribute("fft-size") ?? "128", 10);
505
- }
506
-
507
- get fftDecay() {
508
- return Number.parseInt(this.getAttribute("fft-decay") ?? "8", 10);
509
- }
510
-
511
- set interpolateFrequencies(value: boolean) {
512
- const oldValue = this.interpolateFrequencies;
513
- this.setAttribute("interpolate-frequencies", String(value));
514
- this.requestUpdate("interpolate-frequencies", oldValue);
515
249
  }
516
250
 
517
- get interpolateFrequencies() {
518
- return this.getAttribute("interpolate-frequencies") !== "false";
519
- }
520
-
521
- get shouldInterpolateFrequencies() {
522
- if (this.hasAttribute("interpolate-frequencies")) {
523
- return this.getAttribute("interpolate-frequencies") !== "false";
524
- }
525
- return false;
251
+ protected async executeSeek(seekToMs: number) {
252
+ this.desiredSeekTimeMs = seekToMs;
526
253
  }
527
254
 
528
- private static readonly DECAY_WEIGHT = 0.7;
529
-
530
- // Update FREQ_WEIGHTS to use the instance fftSize instead of a static value
531
- get FREQ_WEIGHTS() {
532
- if (freqWeightsCache.has(this.fftSize)) {
533
- // biome-ignore lint/style/noNonNullAssertion: Will exist due to prior has check
534
- return freqWeightsCache.get(this.fftSize)!;
535
- }
536
-
537
- const weights = new Float32Array(this.fftSize / 2).map((_, i) => {
538
- const frequency = (i * 48000) / this.fftSize;
539
- if (frequency < 60) return 0.3;
540
- if (frequency < 250) return 0.4;
541
- if (frequency < 500) return 0.6;
542
- if (frequency < 2000) return 0.8;
543
- if (frequency < 4000) return 1.2;
544
- if (frequency < 8000) return 1.6;
545
- return 2.0;
546
- });
255
+ /**
256
+ * Main integration method for EFTimegroup audio playback
257
+ * Now powered by clean, testable utility functions
258
+ */
259
+ async fetchAudioSpanningTime(
260
+ fromMs: number,
261
+ toMs: number,
262
+ signal: AbortSignal = new AbortController().signal,
263
+ ): Promise<AudioSpan> {
264
+ // Reset MediaSourceManager for fresh playback session
265
+ await this.mediaSourceService.initialize();
266
+
267
+ // Use the clean, testable utility function
268
+ const { fetchAudioSpanningTime: fetchAudioSpan } = await import(
269
+ "./EFMedia/shared/AudioSpanUtils.ts"
270
+ );
547
271
 
548
- freqWeightsCache.set(this.fftSize, weights);
549
- return weights;
272
+ return fetchAudioSpan(this, fromMs, toMs, signal);
550
273
  }
551
274
 
552
- #byteTimeDomainCache = new LRUCache<string, Uint8Array>(100);
553
-
554
- byteTimeDomainTask = new Task(this, {
555
- autoRun: EF_INTERACTIVE,
556
- args: () =>
557
- [
558
- this.audioBufferTask.status,
559
- this.currentSourceTimeMs,
560
- this.fftSize,
561
- this.fftDecay,
562
- this.fftGain,
563
- this.shouldInterpolateFrequencies,
564
- ] as const,
565
- task: async () => {
566
- await this.audioBufferTask.taskComplete;
567
- if (!this.audioBufferTask.value) return null;
568
- if (this.currentSourceTimeMs <= 0) return null;
569
-
570
- const currentTimeMs = this.currentSourceTimeMs;
571
- const startOffsetMs = this.audioBufferTask.value.startOffsetMs;
572
- const audioBuffer = this.audioBufferTask.value.buffer;
573
-
574
- const smoothedKey = `${this.shouldInterpolateFrequencies}:${this.fftSize}:${this.fftDecay}:${this.fftGain}:${startOffsetMs}:${currentTimeMs}`;
575
- const cachedData = this.#byteTimeDomainCache.get(smoothedKey);
576
- if (cachedData) return cachedData;
577
-
578
- // Process multiple frames with decay, similar to the reference code
579
- const framesData = await Promise.all(
580
- Array.from({ length: this.fftDecay }, async (_, frameIndex) => {
581
- const frameOffset = frameIndex * (1000 / 30);
582
- const startTime = Math.max(
583
- 0,
584
- (currentTimeMs - frameOffset - startOffsetMs) / 1000,
585
- );
586
-
587
- const cacheKey = `${this.shouldInterpolateFrequencies}:${this.fftSize}:${this.fftGain}:${startOffsetMs}:${startTime}`;
588
- const cachedFrame = this.#byteTimeDomainCache.get(cacheKey);
589
- if (cachedFrame) return cachedFrame;
590
-
591
- const audioContext = new OfflineAudioContext(
592
- 2,
593
- 48000 * (1 / 30),
594
- 48000,
595
- );
596
-
597
- const source = audioContext.createBufferSource();
598
- source.buffer = audioBuffer;
599
-
600
- // Create analyzer for PCM data
601
- const analyser = audioContext.createAnalyser();
602
- analyser.fftSize = this.fftSize; // Ensure enough samples
603
- analyser.minDecibels = -90;
604
- analyser.maxDecibels = -20;
605
-
606
- const gainNode = audioContext.createGain();
607
- gainNode.gain.value = this.fftGain; // Amplify the signal
608
-
609
- source.connect(gainNode);
610
- gainNode.connect(analyser);
611
- analyser.connect(audioContext.destination);
612
-
613
- source.start(0, startTime, 1 / 30);
614
-
615
- const dataLength = analyser.fftSize / 2;
616
- try {
617
- await audioContext.startRendering();
618
- const frameData = new Uint8Array(dataLength);
619
- analyser.getByteTimeDomainData(frameData);
620
-
621
- // const points = frameData;
622
- // Calculate RMS and midpoint values
623
- const points = new Uint8Array(dataLength);
624
- for (let i = 0; i < dataLength; i++) {
625
- const pointSamples = frameData.slice(
626
- i * (frameData.length / dataLength),
627
- (i + 1) * (frameData.length / dataLength),
628
- );
629
-
630
- // Calculate RMS while preserving sign
631
- const rms = Math.sqrt(
632
- pointSamples.reduce((sum, sample) => {
633
- const normalized = (sample - 128) / 128;
634
- return sum + normalized * normalized;
635
- }, 0) / pointSamples.length,
636
- );
637
-
638
- // Get average sign of the samples to determine direction
639
- const avgSign = Math.sign(
640
- pointSamples.reduce((sum, sample) => sum + (sample - 128), 0),
641
- );
642
-
643
- // Convert RMS back to byte range, preserving direction
644
- points[i] = Math.min(255, Math.round(128 + avgSign * rms * 128));
645
- }
646
-
647
- this.#byteTimeDomainCache.set(cacheKey, points);
648
- return points;
649
- } finally {
650
- source.disconnect();
651
- analyser.disconnect();
652
- }
653
- }),
654
- );
655
-
656
- // Combine frames with decay weighting
657
- const frameLength = framesData[0]?.length ?? 0;
658
- const smoothedData = new Uint8Array(frameLength);
659
-
660
- for (let i = 0; i < frameLength; i++) {
661
- let weightedSum = 0;
662
- let weightSum = 0;
663
-
664
- framesData.forEach((frame, frameIndex) => {
665
- const decayWeight = EFMedia.DECAY_WEIGHT ** frameIndex;
666
- weightedSum += (frame[i] ?? 0) * decayWeight;
667
- weightSum += decayWeight;
668
- });
669
-
670
- smoothedData[i] = Math.min(255, Math.round(weightedSum / weightSum));
671
- }
672
-
673
- this.#byteTimeDomainCache.set(smoothedKey, smoothedData);
674
- return smoothedData;
675
- },
676
- });
677
-
678
- #frequencyDataCache = new LRUCache<string, Uint8Array>(100);
679
-
680
- frequencyDataTask = new Task(this, {
681
- autoRun: EF_INTERACTIVE,
682
- args: () =>
683
- [
684
- this.audioBufferTask.status,
685
- this.currentSourceTimeMs,
686
- this.fftSize,
687
- this.fftDecay,
688
- this.fftGain,
689
- this.shouldInterpolateFrequencies,
690
- ] as const,
691
- task: async () => {
692
- await this.audioBufferTask.taskComplete;
693
- if (!this.audioBufferTask.value) return null;
694
- if (this.currentSourceTimeMs <= 0) return null;
695
-
696
- const currentTimeMs = this.currentSourceTimeMs;
697
- const startOffsetMs = this.audioBufferTask.value.startOffsetMs;
698
- const audioBuffer = this.audioBufferTask.value.buffer;
699
- const smoothedKey = `${this.shouldInterpolateFrequencies}:${this.fftSize}:${this.fftDecay}:${this.fftGain}:${startOffsetMs}:${currentTimeMs}`;
700
-
701
- const cachedSmoothedData = this.#frequencyDataCache.get(smoothedKey);
702
- if (cachedSmoothedData) {
703
- return cachedSmoothedData;
704
- }
705
-
706
- const framesData = await Promise.all(
707
- Array.from({ length: this.fftDecay }, async (_, i) => {
708
- const frameOffset = i * (1000 / 30);
709
- const startTime = Math.max(
710
- 0,
711
- (currentTimeMs - frameOffset - startOffsetMs) / 1000,
712
- );
713
-
714
- // Cache key for this specific frame
715
- const cacheKey = `${this.shouldInterpolateFrequencies}:${this.fftSize}:${this.fftGain}:${startOffsetMs}:${startTime}`;
716
-
717
- // Check cache for this specific frame
718
- const cachedFrame = this.#frequencyDataCache.get(cacheKey);
719
- if (cachedFrame) {
720
- return cachedFrame;
721
- }
722
-
723
- const audioContext = new OfflineAudioContext(
724
- 2,
725
- 48000 * (1 / 30),
726
- 48000,
727
- );
728
- const analyser = audioContext.createAnalyser();
729
- analyser.fftSize = this.fftSize;
730
- analyser.minDecibels = -90;
731
- analyser.maxDecibels = -10;
732
-
733
- const gainNode = audioContext.createGain();
734
- gainNode.gain.value = this.fftGain;
735
-
736
- const filter = audioContext.createBiquadFilter();
737
- filter.type = "bandpass";
738
- filter.frequency.value = 15000;
739
- filter.Q.value = 0.05;
740
-
741
- const audioBufferSource = audioContext.createBufferSource();
742
- audioBufferSource.buffer = audioBuffer;
743
-
744
- audioBufferSource.connect(filter);
745
- filter.connect(gainNode);
746
- gainNode.connect(analyser);
747
- analyser.connect(audioContext.destination);
748
-
749
- audioBufferSource.start(0, startTime, 1 / 30);
750
-
751
- try {
752
- await audioContext.startRendering();
753
- const frameData = new Uint8Array(this.fftSize / 2);
754
- analyser.getByteFrequencyData(frameData);
755
-
756
- // Cache this frame's analysis
757
- this.#frequencyDataCache.set(cacheKey, frameData);
758
- return frameData;
759
- } finally {
760
- audioBufferSource.disconnect();
761
- analyser.disconnect();
762
- }
763
- }),
764
- );
765
-
766
- const frameLength = framesData[0]?.length ?? 0;
767
-
768
- // Combine frames with decay
769
- const smoothedData = new Uint8Array(frameLength);
770
- for (let i = 0; i < frameLength; i++) {
771
- let weightedSum = 0;
772
- let weightSum = 0;
773
-
774
- framesData.forEach((frame, frameIndex) => {
775
- const decayWeight = EFMedia.DECAY_WEIGHT ** frameIndex;
776
- // biome-ignore lint/style/noNonNullAssertion: Manual bounds check
777
- weightedSum += frame[i]! * decayWeight;
778
- weightSum += decayWeight;
779
- });
780
-
781
- smoothedData[i] = Math.min(255, Math.round(weightedSum / weightSum));
782
- }
783
-
784
- // Apply frequency weights using instance FREQ_WEIGHTS
785
- smoothedData.forEach((value, i) => {
786
- // biome-ignore lint/style/noNonNullAssertion: Manual bounds check
787
- const freqWeight = this.FREQ_WEIGHTS[i]!;
788
- smoothedData[i] = Math.min(255, Math.round(value * freqWeight));
789
- });
790
-
791
- // Only return the lower half of the frequency data
792
- // The top half is zeroed out, which makes for aesthetically unpleasing waveforms
793
- const slicedData = smoothedData.slice(
794
- 0,
795
- Math.floor(smoothedData.length / 2),
796
- );
797
- const processedData = this.shouldInterpolateFrequencies
798
- ? processFFTData(slicedData)
799
- : slicedData;
800
- this.#frequencyDataCache.set(smoothedKey, processedData);
801
- return processedData;
802
- },
803
- });
804
-
805
- set fftGain(value: number) {
806
- const oldValue = this.fftGain;
807
- this.setAttribute("fft-gain", String(value));
808
- this.requestUpdate("fft-gain", oldValue);
275
+ /**
276
+ * Get the HTML audio element for ContextMixin integration
277
+ */
278
+ get audioElement(): HTMLAudioElement | null {
279
+ return this.mediaSourceService.getAudioElement();
809
280
  }
810
281
 
811
- get fftGain() {
812
- return Number.parseFloat(this.getAttribute("fft-gain") ?? "3.0");
282
+ /**
283
+ * Check if an audio segment is cached in the unified buffer system
284
+ * Now uses the same caching approach as video for consistency
285
+ */
286
+ getCachedAudioSegment(segmentId: number): boolean {
287
+ return this.audioBufferTask.value?.cachedSegments.has(segmentId) ?? false;
813
288
  }
814
- }
815
-
816
- function processFFTData(fftData: Uint8Array, zeroThresholdPercent = 0.1) {
817
- // Step 1: Determine the threshold for zeros
818
- const totalBins = fftData.length;
819
- const zeroThresholdCount = Math.floor(totalBins * zeroThresholdPercent);
820
-
821
- // Step 2: Interrogate the FFT output to find the cutoff point
822
- let zeroCount = 0;
823
- let cutoffIndex = totalBins; // Default to the end of the array
824
289
 
825
- for (let i = totalBins - 1; i >= 0; i--) {
826
- // biome-ignore lint/style/noNonNullAssertion: Manual bounds check
827
- if (fftData[i]! < 10) {
828
- zeroCount++;
829
- } else {
830
- // If we encounter a non-zero value, we can stop
831
- if (zeroCount >= zeroThresholdCount) {
832
- cutoffIndex = i + 1; // Include this index
833
- break;
834
- }
290
+ /**
291
+ * Get cached audio segments from the unified buffer system
292
+ * Now uses the same caching approach as video for consistency
293
+ */
294
+ getCachedAudioSegments(segmentIds: number[]): Set<number> {
295
+ const bufferState = this.audioBufferTask.value;
296
+ if (!bufferState) {
297
+ return new Set();
835
298
  }
299
+ return new Set(
300
+ segmentIds.filter((id) => bufferState.cachedSegments.has(id)),
301
+ );
836
302
  }
837
303
 
838
- if (cutoffIndex < zeroThresholdCount) {
839
- return fftData;
840
- }
841
-
842
- // Step 3: Resample the "good" portion of the data
843
- const goodData = fftData.slice(0, cutoffIndex);
844
- const resampledData = interpolateData(goodData, fftData.length);
845
-
846
- // Step 4: Attenuate the top 10% of interpolated samples
847
- const attenuationStartIndex = Math.floor(totalBins * 0.9);
848
- for (let i = attenuationStartIndex; i < totalBins; i++) {
849
- // Calculate attenuation factor that goes from 1 to 0 over the top 10%
850
- const attenuationProgress =
851
- (i - attenuationStartIndex) / (totalBins - attenuationStartIndex) + 0.2;
852
- const attenuationFactor = Math.max(0, 1 - attenuationProgress);
853
- // biome-ignore lint/style/noNonNullAssertion: Manual bounds check
854
- resampledData[i] = Math.floor(resampledData[i]! * attenuationFactor);
304
+ /**
305
+ * Get MediaElementAudioSourceNode for ContextMixin integration
306
+ * Uses AudioElementFactory for proper caching and lifecycle management
307
+ */
308
+ async getMediaElementSource(
309
+ audioContext: AudioContext,
310
+ ): Promise<MediaElementAudioSourceNode> {
311
+ return this.audioElementFactory.createMediaElementSource(
312
+ audioContext,
313
+ this.mediaSourceService,
314
+ );
855
315
  }
856
316
 
857
- return resampledData;
858
- }
859
-
860
- function interpolateData(data: Uint8Array, targetSize: number) {
861
- const resampled = new Uint8Array(targetSize);
862
- const dataLength = data.length;
317
+ disconnectedCallback(): void {
318
+ super.disconnectedCallback?.();
863
319
 
864
- for (let i = 0; i < targetSize; i++) {
865
- // Calculate the corresponding index in the original data
866
- const ratio = (i / (targetSize - 1)) * (dataLength - 1);
867
- const index = Math.floor(ratio);
868
- const fraction = ratio - index;
320
+ // Clean up MediaSource service
321
+ this.mediaSourceService.cleanup();
869
322
 
870
- // Handle edge cases
871
- if (index >= dataLength - 1) {
872
- // biome-ignore lint/style/noNonNullAssertion: Manual bounds check
873
- resampled[i] = data[dataLength - 1]!; // Last value
874
- } else {
875
- // Linear interpolation
876
- resampled[i] = Math.round(
877
- // biome-ignore lint/style/noNonNullAssertion: Manual bounds check
878
- data[index]! * (1 - fraction) + data[index + 1]! * fraction,
879
- );
880
- }
323
+ // Clear audio element factory cache
324
+ this.audioElementFactory.clearCache();
881
325
  }
882
-
883
- return resampled;
884
326
  }