@videojs/html 10.0.0-beta.11 → 10.0.0-beta.12

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 (788) hide show
  1. package/cdn/abort-C7q_G_dT.js +2 -0
  2. package/cdn/abort-C7q_G_dT.js.map +1 -0
  3. package/cdn/abort-JT-ewLFq.js +22 -0
  4. package/cdn/abort-JT-ewLFq.js.map +1 -0
  5. package/cdn/audio-minimal.dev.js +23 -12
  6. package/cdn/audio-minimal.dev.js.map +1 -1
  7. package/cdn/audio-minimal.js +1 -1
  8. package/cdn/audio-minimal.js.map +1 -1
  9. package/cdn/audio.dev.js +72 -57
  10. package/cdn/audio.dev.js.map +1 -1
  11. package/cdn/audio.js +1 -1
  12. package/cdn/audio.js.map +1 -1
  13. package/cdn/background.dev.js +3 -4
  14. package/cdn/background.dev.js.map +1 -1
  15. package/cdn/background.js +1 -1
  16. package/cdn/background.js.map +1 -1
  17. package/cdn/{create-player-AcfnN3li.js → create-player-BoPlCSNw.js} +103 -19
  18. package/cdn/create-player-BoPlCSNw.js.map +1 -0
  19. package/cdn/create-player-CA3KLZMe.js +7 -0
  20. package/cdn/create-player-CA3KLZMe.js.map +1 -0
  21. package/cdn/{default-cLso8BHO.js → default-CKnlVEjQ.js} +1 -1
  22. package/cdn/{default-cLso8BHO.js.map → default-CKnlVEjQ.js.map} +1 -1
  23. package/cdn/{default-GgKND7a8.js → default-CnBlD9BM.js} +1 -1
  24. package/cdn/{default-GgKND7a8.js.map → default-CnBlD9BM.js.map} +1 -1
  25. package/cdn/{delegate-CzAcT1xm.js → delegate-CSc5c0ZR.js} +16 -3
  26. package/cdn/delegate-CSc5c0ZR.js.map +1 -0
  27. package/cdn/delegate-jczJeizF.js +2 -0
  28. package/cdn/delegate-jczJeizF.js.map +1 -0
  29. package/cdn/hls-C6htsSW4.js +28661 -0
  30. package/cdn/hls-C6htsSW4.js.map +1 -0
  31. package/cdn/hls-DQ4glyHe.js +41 -0
  32. package/cdn/hls-DQ4glyHe.js.map +1 -0
  33. package/cdn/{listen-YSH3Jfyk.js → listen-BkAEGXCe.js} +1 -1
  34. package/cdn/{listen-YSH3Jfyk.js.map → listen-BkAEGXCe.js.map} +1 -1
  35. package/cdn/{listen-4jqsRSKo.js → listen-UqQNdlqV.js} +1 -1
  36. package/cdn/{listen-4jqsRSKo.js.map → listen-UqQNdlqV.js.map} +1 -1
  37. package/cdn/media/dash-video.dev.js +10 -17
  38. package/cdn/media/dash-video.dev.js.map +1 -1
  39. package/cdn/media/dash-video.js +3 -3
  40. package/cdn/media/dash-video.js.map +1 -1
  41. package/cdn/media/hls-video.dev.js +5 -28336
  42. package/cdn/media/hls-video.dev.js.map +1 -1
  43. package/cdn/media/hls-video.js +1 -40
  44. package/cdn/media/hls-video.js.map +1 -1
  45. package/cdn/media/mux-video.dev.d.ts +1 -0
  46. package/cdn/media/mux-video.dev.js +3122 -0
  47. package/cdn/media/mux-video.dev.js.map +1 -0
  48. package/cdn/media/mux-video.js +25 -0
  49. package/cdn/media/mux-video.js.map +1 -0
  50. package/cdn/media/simple-hls-video.dev.js +2286 -2094
  51. package/cdn/media/simple-hls-video.dev.js.map +1 -1
  52. package/cdn/media/simple-hls-video.js +58 -1
  53. package/cdn/media/simple-hls-video.js.map +1 -1
  54. package/cdn/media-attach-mixin-BIrlT_tz.js +2 -0
  55. package/cdn/{media-attach-mixin-D5_nfJpa.js.map → media-attach-mixin-BIrlT_tz.js.map} +1 -1
  56. package/cdn/{media-attach-mixin-U_KQB_9O.js → media-attach-mixin-Dsn4gxJA.js} +2 -2
  57. package/cdn/{media-attach-mixin-U_KQB_9O.js.map → media-attach-mixin-Dsn4gxJA.js.map} +1 -1
  58. package/cdn/{custom-media-element-DqevSVgS.js → media-props-mixin-BuVUebRp.js} +2 -2
  59. package/cdn/media-props-mixin-BuVUebRp.js.map +1 -0
  60. package/cdn/{custom-media-element-moFa3UZp.js → media-props-mixin-DxsM38Bx.js} +44 -2
  61. package/cdn/media-props-mixin-DxsM38Bx.js.map +1 -0
  62. package/cdn/{minimal-BJfleQcQ.js → minimal-CKMdOXWm.js} +1 -1
  63. package/cdn/{minimal-BJfleQcQ.js.map → minimal-CKMdOXWm.js.map} +1 -1
  64. package/cdn/{minimal-DBMdC_0I.js → minimal-fA2p2Jrn.js} +1 -1
  65. package/cdn/{minimal-DBMdC_0I.js.map → minimal-fA2p2Jrn.js.map} +1 -1
  66. package/cdn/player-Dzvu8Tzs.js +2 -0
  67. package/cdn/{player-C46h14iP.js.map → player-Dzvu8Tzs.js.map} +1 -1
  68. package/cdn/{player-CvrOeLpy.js → player-rkxd0mpV.js} +3 -3
  69. package/cdn/{player-CvrOeLpy.js.map → player-rkxd0mpV.js.map} +1 -1
  70. package/cdn/{poster-Olv5zDI_.js → poster-BPMPXyn3.js} +4 -5
  71. package/cdn/{poster-Olv5zDI_.js.map → poster-BPMPXyn3.js.map} +1 -1
  72. package/cdn/poster-DqjXzMK_.js +2 -0
  73. package/cdn/{poster-odJ4iwIv.js.map → poster-DqjXzMK_.js.map} +1 -1
  74. package/cdn/{context-Be8C5kVd.js → safe-define-D26LrTu4.js} +14 -5
  75. package/cdn/safe-define-D26LrTu4.js.map +1 -0
  76. package/cdn/safe-define-EEn8NTOG.js +14 -0
  77. package/cdn/safe-define-EEn8NTOG.js.map +1 -0
  78. package/cdn/shallow-equal-CaIo44Co.js +15 -0
  79. package/cdn/shallow-equal-CaIo44Co.js.map +1 -0
  80. package/cdn/shallow-equal-zo2IZwso.js +2 -0
  81. package/cdn/shallow-equal-zo2IZwso.js.map +1 -0
  82. package/cdn/video-minimal.dev.js +24 -13
  83. package/cdn/video-minimal.dev.js.map +1 -1
  84. package/cdn/video-minimal.js +1 -1
  85. package/cdn/video-minimal.js.map +1 -1
  86. package/cdn/video.dev.js +107 -92
  87. package/cdn/video.dev.js.map +1 -1
  88. package/cdn/video.js +1 -1
  89. package/cdn/video.js.map +1 -1
  90. package/cdn/{volume-slider-D7BOdSDF.js → volume-slider-BEXiB6_j.js} +245 -14
  91. package/cdn/volume-slider-BEXiB6_j.js.map +1 -0
  92. package/cdn/volume-slider-CQ0Yq947.js +9 -0
  93. package/cdn/volume-slider-CQ0Yq947.js.map +1 -0
  94. package/dist/default/_virtual/inline-css_src/define/audio/minimal-skin.js +2 -2
  95. package/dist/default/_virtual/inline-css_src/define/audio/minimal-skin.js.map +1 -1
  96. package/dist/default/_virtual/inline-css_src/define/audio/skin.js +2 -2
  97. package/dist/default/_virtual/inline-css_src/define/audio/skin.js.map +1 -1
  98. package/dist/default/_virtual/inline-css_src/define/background/skin.js +1 -1
  99. package/dist/default/_virtual/inline-css_src/define/background/skin.js.map +1 -1
  100. package/dist/default/_virtual/inline-css_src/define/base.js +1 -1
  101. package/dist/default/_virtual/inline-css_src/define/base.js.map +1 -1
  102. package/dist/default/_virtual/inline-css_src/define/shared.js +1 -1
  103. package/dist/default/_virtual/inline-css_src/define/shared.js.map +1 -1
  104. package/dist/default/_virtual/inline-css_src/define/video/minimal-skin.js +2 -2
  105. package/dist/default/_virtual/inline-css_src/define/video/minimal-skin.js.map +1 -1
  106. package/dist/default/_virtual/inline-css_src/define/video/skin.js +2 -2
  107. package/dist/default/_virtual/inline-css_src/define/video/skin.js.map +1 -1
  108. package/dist/default/define/audio/minimal-skin.css +81 -59
  109. package/dist/default/define/audio/minimal-skin.js +5 -4
  110. package/dist/default/define/audio/minimal-skin.js.map +1 -1
  111. package/dist/default/define/audio/minimal-skin.tailwind.js +6 -5
  112. package/dist/default/define/audio/minimal-skin.tailwind.js.map +1 -1
  113. package/dist/default/define/audio/player.js +1 -2
  114. package/dist/default/define/audio/player.js.map +1 -1
  115. package/dist/default/define/audio/skin.css +54 -42
  116. package/dist/default/define/audio/skin.js +5 -4
  117. package/dist/default/define/audio/skin.js.map +1 -1
  118. package/dist/default/define/audio/skin.tailwind.js +7 -5
  119. package/dist/default/define/audio/skin.tailwind.js.map +1 -1
  120. package/dist/default/define/background/player.js +1 -2
  121. package/dist/default/define/background/player.js.map +1 -1
  122. package/dist/default/define/background/skin.js +1 -2
  123. package/dist/default/define/background/skin.js.map +1 -1
  124. package/dist/default/define/background/video.js +1 -2
  125. package/dist/default/define/media/background-video.js +1 -2
  126. package/dist/default/define/media/background-video.js.map +1 -1
  127. package/dist/default/define/media/container.js +1 -2
  128. package/dist/default/define/media/container.js.map +1 -1
  129. package/dist/default/define/media/dash-video.js +1 -2
  130. package/dist/default/define/media/dash-video.js.map +1 -1
  131. package/dist/default/define/media/hls-video.js +1 -2
  132. package/dist/default/define/media/hls-video.js.map +1 -1
  133. package/dist/default/define/media/mux-video.js +13 -0
  134. package/dist/default/define/media/mux-video.js.map +1 -0
  135. package/dist/default/define/media/native-hls-video.js +13 -0
  136. package/dist/default/define/media/native-hls-video.js.map +1 -0
  137. package/dist/default/define/media/simple-hls-video.js +3 -3
  138. package/dist/default/define/media/simple-hls-video.js.map +1 -1
  139. package/dist/default/define/safe-define.js +4 -2
  140. package/dist/default/define/safe-define.js.map +1 -1
  141. package/dist/default/define/skin-mixin.js +30 -11
  142. package/dist/default/define/skin-mixin.js.map +1 -1
  143. package/dist/default/define/ui/alert-dialog-close.js +1 -2
  144. package/dist/default/define/ui/alert-dialog-close.js.map +1 -1
  145. package/dist/default/define/ui/alert-dialog-description.js +1 -2
  146. package/dist/default/define/ui/alert-dialog-description.js.map +1 -1
  147. package/dist/default/define/ui/alert-dialog-title.js +1 -2
  148. package/dist/default/define/ui/alert-dialog-title.js.map +1 -1
  149. package/dist/default/define/ui/alert-dialog.js +1 -2
  150. package/dist/default/define/ui/alert-dialog.js.map +1 -1
  151. package/dist/default/define/ui/buffering-indicator.js +1 -2
  152. package/dist/default/define/ui/buffering-indicator.js.map +1 -1
  153. package/dist/default/define/ui/captions-button.js +3 -3
  154. package/dist/default/define/ui/captions-button.js.map +1 -1
  155. package/dist/default/define/ui/controls-group.js +1 -2
  156. package/dist/default/define/ui/controls-group.js.map +1 -1
  157. package/dist/default/define/ui/controls.js +1 -2
  158. package/dist/default/define/ui/controls.js.map +1 -1
  159. package/dist/default/define/ui/error-dialog.js +13 -0
  160. package/dist/default/define/ui/error-dialog.js.map +1 -0
  161. package/dist/default/define/ui/fullscreen-button.js +1 -2
  162. package/dist/default/define/ui/fullscreen-button.js.map +1 -1
  163. package/dist/default/define/ui/mute-button.js +1 -2
  164. package/dist/default/define/ui/mute-button.js.map +1 -1
  165. package/dist/default/define/ui/pip-button.js +1 -2
  166. package/dist/default/define/ui/pip-button.js.map +1 -1
  167. package/dist/default/define/ui/play-button.js +1 -2
  168. package/dist/default/define/ui/play-button.js.map +1 -1
  169. package/dist/default/define/ui/playback-rate-button.js +1 -2
  170. package/dist/default/define/ui/playback-rate-button.js.map +1 -1
  171. package/dist/default/define/ui/popover.js +1 -2
  172. package/dist/default/define/ui/popover.js.map +1 -1
  173. package/dist/default/define/ui/poster.js +1 -2
  174. package/dist/default/define/ui/poster.js.map +1 -1
  175. package/dist/default/define/ui/seek-button.js +1 -2
  176. package/dist/default/define/ui/seek-button.js.map +1 -1
  177. package/dist/default/define/ui/slider-buffer.js +1 -2
  178. package/dist/default/define/ui/slider-buffer.js.map +1 -1
  179. package/dist/default/define/ui/slider-fill.js +1 -2
  180. package/dist/default/define/ui/slider-fill.js.map +1 -1
  181. package/dist/default/define/ui/slider-thumb.js +1 -2
  182. package/dist/default/define/ui/slider-thumb.js.map +1 -1
  183. package/dist/default/define/ui/slider-thumbnail.js +1 -2
  184. package/dist/default/define/ui/slider-thumbnail.js.map +1 -1
  185. package/dist/default/define/ui/slider-track.js +1 -2
  186. package/dist/default/define/ui/slider-track.js.map +1 -1
  187. package/dist/default/define/ui/slider-value.js +1 -2
  188. package/dist/default/define/ui/slider-value.js.map +1 -1
  189. package/dist/default/define/ui/slider.js +1 -2
  190. package/dist/default/define/ui/slider.js.map +1 -1
  191. package/dist/default/define/ui/thumbnail.js +1 -2
  192. package/dist/default/define/ui/thumbnail.js.map +1 -1
  193. package/dist/default/define/ui/time-group.js +1 -2
  194. package/dist/default/define/ui/time-group.js.map +1 -1
  195. package/dist/default/define/ui/time-separator.js +1 -2
  196. package/dist/default/define/ui/time-separator.js.map +1 -1
  197. package/dist/default/define/ui/time-slider.js +1 -2
  198. package/dist/default/define/ui/time-slider.js.map +1 -1
  199. package/dist/default/define/ui/time.js +1 -2
  200. package/dist/default/define/ui/time.js.map +1 -1
  201. package/dist/default/define/ui/tooltip-group.js +1 -2
  202. package/dist/default/define/ui/tooltip-group.js.map +1 -1
  203. package/dist/default/define/ui/tooltip.js +1 -2
  204. package/dist/default/define/ui/tooltip.js.map +1 -1
  205. package/dist/default/define/ui/volume-slider.js +1 -2
  206. package/dist/default/define/ui/volume-slider.js.map +1 -1
  207. package/dist/default/define/video/minimal-skin.css +156 -75
  208. package/dist/default/define/video/minimal-skin.js +5 -4
  209. package/dist/default/define/video/minimal-skin.js.map +1 -1
  210. package/dist/default/define/video/minimal-skin.tailwind.js +6 -7
  211. package/dist/default/define/video/minimal-skin.tailwind.js.map +1 -1
  212. package/dist/default/define/video/player.js +1 -2
  213. package/dist/default/define/video/player.js.map +1 -1
  214. package/dist/default/define/video/skin.css +114 -58
  215. package/dist/default/define/video/skin.js +5 -4
  216. package/dist/default/define/video/skin.js.map +1 -1
  217. package/dist/default/define/video/skin.tailwind.js +6 -6
  218. package/dist/default/define/video/skin.tailwind.js.map +1 -1
  219. package/dist/default/icons/dist/render/default/index.js +1 -1
  220. package/dist/default/icons/dist/render/minimal/index.js +1 -1
  221. package/dist/default/index.js +3 -4
  222. package/dist/default/media/background-video/index.js +1 -2
  223. package/dist/default/media/background-video/index.js.map +1 -1
  224. package/dist/default/media/container-element.js +1 -2
  225. package/dist/default/media/container-element.js.map +1 -1
  226. package/dist/default/media/dash-video/index.js +4 -12
  227. package/dist/default/media/dash-video/index.js.map +1 -1
  228. package/dist/default/media/hls-video/index.js +4 -12
  229. package/dist/default/media/hls-video/index.js.map +1 -1
  230. package/dist/default/media/mux-video/index.js +18 -0
  231. package/dist/default/media/mux-video/index.js.map +1 -0
  232. package/dist/default/media/native-hls-video/index.js +18 -0
  233. package/dist/default/media/native-hls-video/index.js.map +1 -0
  234. package/dist/default/media/simple-hls-video/index.js +4 -12
  235. package/dist/default/media/simple-hls-video/index.js.map +1 -1
  236. package/dist/default/player/context.js +1 -2
  237. package/dist/default/player/context.js.map +1 -1
  238. package/dist/default/player/create-player.js +1 -2
  239. package/dist/default/player/create-player.js.map +1 -1
  240. package/dist/default/player/player-controller.js +1 -2
  241. package/dist/default/player/player-controller.js.map +1 -1
  242. package/dist/default/presets/audio.js +1 -2
  243. package/dist/default/presets/background.js +1 -2
  244. package/dist/default/presets/video.js +1 -2
  245. package/dist/default/skins/dist/default/default/tailwind/audio.tailwind.js +14 -18
  246. package/dist/default/skins/dist/default/default/tailwind/audio.tailwind.js.map +1 -1
  247. package/dist/default/skins/dist/default/default/tailwind/components/buffering.js +1 -1
  248. package/dist/default/skins/dist/default/default/tailwind/components/buffering.js.map +1 -1
  249. package/dist/default/skins/dist/default/default/tailwind/components/button-group.js +7 -0
  250. package/dist/default/skins/dist/default/default/tailwind/components/button-group.js.map +1 -0
  251. package/dist/default/skins/dist/default/default/tailwind/components/button.js +3 -4
  252. package/dist/default/skins/dist/default/default/tailwind/components/button.js.map +1 -1
  253. package/dist/default/skins/dist/default/default/tailwind/components/controls.js +2 -3
  254. package/dist/default/skins/dist/default/default/tailwind/components/controls.js.map +1 -1
  255. package/dist/default/skins/dist/default/default/tailwind/components/error.js +2 -3
  256. package/dist/default/skins/dist/default/default/tailwind/components/error.js.map +1 -1
  257. package/dist/default/skins/dist/default/default/tailwind/components/icon.js +1 -2
  258. package/dist/default/skins/dist/default/default/tailwind/components/icon.js.map +1 -1
  259. package/dist/default/skins/dist/default/default/tailwind/components/overlay.js +2 -3
  260. package/dist/default/skins/dist/default/default/tailwind/components/overlay.js.map +1 -1
  261. package/dist/default/skins/dist/default/default/tailwind/components/playback-rate.js +1 -1
  262. package/dist/default/skins/dist/default/default/tailwind/components/playback-rate.js.map +1 -1
  263. package/dist/default/skins/dist/default/default/tailwind/components/popup.js +4 -5
  264. package/dist/default/skins/dist/default/default/tailwind/components/popup.js.map +1 -1
  265. package/dist/default/skins/dist/default/default/tailwind/components/poster.js +1 -2
  266. package/dist/default/skins/dist/default/default/tailwind/components/poster.js.map +1 -1
  267. package/dist/default/skins/dist/default/default/tailwind/components/preview.js +2 -3
  268. package/dist/default/skins/dist/default/default/tailwind/components/preview.js.map +1 -1
  269. package/dist/default/skins/dist/default/default/tailwind/components/root.js +2 -3
  270. package/dist/default/skins/dist/default/default/tailwind/components/root.js.map +1 -1
  271. package/dist/default/skins/dist/default/default/tailwind/components/seek.js +1 -2
  272. package/dist/default/skins/dist/default/default/tailwind/components/seek.js.map +1 -1
  273. package/dist/default/skins/dist/default/default/tailwind/components/slider.js +2 -3
  274. package/dist/default/skins/dist/default/default/tailwind/components/slider.js.map +1 -1
  275. package/dist/default/skins/dist/default/default/tailwind/components/surface.js +2 -3
  276. package/dist/default/skins/dist/default/default/tailwind/components/surface.js.map +1 -1
  277. package/dist/default/skins/dist/default/default/tailwind/components/time.js +1 -1
  278. package/dist/default/skins/dist/default/default/tailwind/components/time.js.map +1 -1
  279. package/dist/default/skins/dist/default/default/tailwind/video.tailwind.js +25 -20
  280. package/dist/default/skins/dist/default/default/tailwind/video.tailwind.js.map +1 -1
  281. package/dist/default/skins/dist/default/minimal/tailwind/audio.tailwind.js +15 -16
  282. package/dist/default/skins/dist/default/minimal/tailwind/audio.tailwind.js.map +1 -1
  283. package/dist/default/skins/dist/default/minimal/tailwind/components/buffering.js +1 -1
  284. package/dist/default/skins/dist/default/minimal/tailwind/components/buffering.js.map +1 -1
  285. package/dist/default/skins/dist/default/minimal/tailwind/components/button-group.js +1 -2
  286. package/dist/default/skins/dist/default/minimal/tailwind/components/button-group.js.map +1 -1
  287. package/dist/default/skins/dist/default/minimal/tailwind/components/button.js +2 -3
  288. package/dist/default/skins/dist/default/minimal/tailwind/components/button.js.map +1 -1
  289. package/dist/default/skins/dist/default/minimal/tailwind/components/controls.js +2 -3
  290. package/dist/default/skins/dist/default/minimal/tailwind/components/controls.js.map +1 -1
  291. package/dist/default/skins/dist/default/minimal/tailwind/components/error.js +2 -3
  292. package/dist/default/skins/dist/default/minimal/tailwind/components/error.js.map +1 -1
  293. package/dist/default/skins/dist/default/minimal/tailwind/components/icon.js +1 -2
  294. package/dist/default/skins/dist/default/minimal/tailwind/components/icon.js.map +1 -1
  295. package/dist/default/skins/dist/default/minimal/tailwind/components/overlay.js +2 -3
  296. package/dist/default/skins/dist/default/minimal/tailwind/components/overlay.js.map +1 -1
  297. package/dist/default/skins/dist/default/minimal/tailwind/components/playback-rate.js +1 -1
  298. package/dist/default/skins/dist/default/minimal/tailwind/components/playback-rate.js.map +1 -1
  299. package/dist/default/skins/dist/default/minimal/tailwind/components/popup.js +2 -3
  300. package/dist/default/skins/dist/default/minimal/tailwind/components/popup.js.map +1 -1
  301. package/dist/default/skins/dist/default/minimal/tailwind/components/poster.js +1 -2
  302. package/dist/default/skins/dist/default/minimal/tailwind/components/poster.js.map +1 -1
  303. package/dist/default/skins/dist/default/minimal/tailwind/components/preview.js +2 -3
  304. package/dist/default/skins/dist/default/minimal/tailwind/components/preview.js.map +1 -1
  305. package/dist/default/skins/dist/default/minimal/tailwind/components/root.js +2 -3
  306. package/dist/default/skins/dist/default/minimal/tailwind/components/root.js.map +1 -1
  307. package/dist/default/skins/dist/default/minimal/tailwind/components/seek.js +1 -2
  308. package/dist/default/skins/dist/default/minimal/tailwind/components/seek.js.map +1 -1
  309. package/dist/default/skins/dist/default/minimal/tailwind/components/slider.js +2 -3
  310. package/dist/default/skins/dist/default/minimal/tailwind/components/slider.js.map +1 -1
  311. package/dist/default/skins/dist/default/minimal/tailwind/components/time.js +5 -6
  312. package/dist/default/skins/dist/default/minimal/tailwind/components/time.js.map +1 -1
  313. package/dist/default/skins/dist/default/minimal/tailwind/video.tailwind.js +23 -18
  314. package/dist/default/skins/dist/default/minimal/tailwind/video.tailwind.js.map +1 -1
  315. package/dist/default/skins/dist/default/shared/tailwind/icon-state.js +1 -1
  316. package/dist/default/skins/dist/default/shared/tailwind/icon-state.js.map +1 -1
  317. package/dist/default/skins/dist/default/shared/tailwind/tooltip-state.js +1 -1
  318. package/dist/default/skins/dist/default/shared/tailwind/tooltip-state.js.map +1 -1
  319. package/dist/default/store/container-mixin.js +1 -2
  320. package/dist/default/store/container-mixin.js.map +1 -1
  321. package/dist/default/store/media-attach-mixin.js +1 -2
  322. package/dist/default/store/media-attach-mixin.js.map +1 -1
  323. package/dist/default/store/provider-mixin.js +1 -2
  324. package/dist/default/store/provider-mixin.js.map +1 -1
  325. package/dist/default/ui/alert-dialog/alert-dialog-close-element.js +1 -2
  326. package/dist/default/ui/alert-dialog/alert-dialog-close-element.js.map +1 -1
  327. package/dist/default/ui/alert-dialog/alert-dialog-description-element.js +1 -2
  328. package/dist/default/ui/alert-dialog/alert-dialog-description-element.js.map +1 -1
  329. package/dist/default/ui/alert-dialog/alert-dialog-element.js +1 -2
  330. package/dist/default/ui/alert-dialog/alert-dialog-element.js.map +1 -1
  331. package/dist/default/ui/alert-dialog/alert-dialog-title-element.js +1 -2
  332. package/dist/default/ui/alert-dialog/alert-dialog-title-element.js.map +1 -1
  333. package/dist/default/ui/alert-dialog/context.js +2 -5
  334. package/dist/default/ui/alert-dialog/context.js.map +1 -1
  335. package/dist/default/ui/buffering-indicator/buffering-indicator-element.js +1 -2
  336. package/dist/default/ui/buffering-indicator/buffering-indicator-element.js.map +1 -1
  337. package/dist/default/ui/captions-button/captions-button-element.js +1 -2
  338. package/dist/default/ui/captions-button/captions-button-element.js.map +1 -1
  339. package/dist/default/ui/context-part-element.js +1 -2
  340. package/dist/default/ui/context-part-element.js.map +1 -1
  341. package/dist/default/ui/controls/context.js +2 -5
  342. package/dist/default/ui/controls/context.js.map +1 -1
  343. package/dist/default/ui/controls/controls-element.js +1 -2
  344. package/dist/default/ui/controls/controls-element.js.map +1 -1
  345. package/dist/default/ui/controls/controls-group-element.js +1 -2
  346. package/dist/default/ui/controls/controls-group-element.js.map +1 -1
  347. package/dist/default/ui/error-dialog/error-dialog-element.js +76 -0
  348. package/dist/default/ui/error-dialog/error-dialog-element.js.map +1 -0
  349. package/dist/default/ui/fullscreen-button/fullscreen-button-element.js +1 -2
  350. package/dist/default/ui/fullscreen-button/fullscreen-button-element.js.map +1 -1
  351. package/dist/default/ui/media-button-element.js +1 -2
  352. package/dist/default/ui/media-button-element.js.map +1 -1
  353. package/dist/default/ui/media-element.js +1 -2
  354. package/dist/default/ui/media-element.js.map +1 -1
  355. package/dist/default/ui/media-ui-element.js +1 -2
  356. package/dist/default/ui/media-ui-element.js.map +1 -1
  357. package/dist/default/ui/mute-button/mute-button-element.js +1 -2
  358. package/dist/default/ui/mute-button/mute-button-element.js.map +1 -1
  359. package/dist/default/ui/pip-button/pip-button-element.js +1 -2
  360. package/dist/default/ui/pip-button/pip-button-element.js.map +1 -1
  361. package/dist/default/ui/play-button/play-button-element.js +1 -2
  362. package/dist/default/ui/play-button/play-button-element.js.map +1 -1
  363. package/dist/default/ui/playback-rate-button/playback-rate-button-element.js +1 -2
  364. package/dist/default/ui/playback-rate-button/playback-rate-button-element.js.map +1 -1
  365. package/dist/default/ui/popover/popover-element.js +1 -2
  366. package/dist/default/ui/popover/popover-element.js.map +1 -1
  367. package/dist/default/ui/poster/poster-element.js +1 -2
  368. package/dist/default/ui/poster/poster-element.js.map +1 -1
  369. package/dist/default/ui/seek-button/seek-button-element.js +1 -2
  370. package/dist/default/ui/seek-button/seek-button-element.js.map +1 -1
  371. package/dist/default/ui/slider/context.js +2 -5
  372. package/dist/default/ui/slider/context.js.map +1 -1
  373. package/dist/default/ui/slider/slider-buffer-element.js +1 -2
  374. package/dist/default/ui/slider/slider-buffer-element.js.map +1 -1
  375. package/dist/default/ui/slider/slider-element.js +1 -2
  376. package/dist/default/ui/slider/slider-element.js.map +1 -1
  377. package/dist/default/ui/slider/slider-fill-element.js +1 -2
  378. package/dist/default/ui/slider/slider-fill-element.js.map +1 -1
  379. package/dist/default/ui/slider/slider-preview-element.js +1 -2
  380. package/dist/default/ui/slider/slider-preview-element.js.map +1 -1
  381. package/dist/default/ui/slider/slider-thumb-element.js +1 -2
  382. package/dist/default/ui/slider/slider-thumb-element.js.map +1 -1
  383. package/dist/default/ui/slider/slider-thumbnail-element.js +1 -2
  384. package/dist/default/ui/slider/slider-thumbnail-element.js.map +1 -1
  385. package/dist/default/ui/slider/slider-track-element.js +1 -2
  386. package/dist/default/ui/slider/slider-track-element.js.map +1 -1
  387. package/dist/default/ui/slider/slider-value-element.js +1 -2
  388. package/dist/default/ui/slider/slider-value-element.js.map +1 -1
  389. package/dist/default/ui/thumbnail/thumbnail-element.js +1 -2
  390. package/dist/default/ui/thumbnail/thumbnail-element.js.map +1 -1
  391. package/dist/default/ui/time/time-element.js +1 -2
  392. package/dist/default/ui/time/time-element.js.map +1 -1
  393. package/dist/default/ui/time/time-group-element.js +1 -2
  394. package/dist/default/ui/time/time-group-element.js.map +1 -1
  395. package/dist/default/ui/time/time-separator-element.js +1 -2
  396. package/dist/default/ui/time/time-separator-element.js.map +1 -1
  397. package/dist/default/ui/time-slider/time-slider-element.js +1 -2
  398. package/dist/default/ui/time-slider/time-slider-element.js.map +1 -1
  399. package/dist/default/ui/tooltip/context.js +2 -5
  400. package/dist/default/ui/tooltip/context.js.map +1 -1
  401. package/dist/default/ui/tooltip/tooltip-element.js +1 -2
  402. package/dist/default/ui/tooltip/tooltip-element.js.map +1 -1
  403. package/dist/default/ui/tooltip/tooltip-group-element.js +1 -2
  404. package/dist/default/ui/tooltip/tooltip-group-element.js.map +1 -1
  405. package/dist/default/ui/volume-slider/volume-slider-element.js +1 -2
  406. package/dist/default/ui/volume-slider/volume-slider-element.js.map +1 -1
  407. package/dist/default/utils/media-props-mixin.js +44 -0
  408. package/dist/default/utils/media-props-mixin.js.map +1 -0
  409. package/dist/dev/_virtual/inline-css_src/define/audio/minimal-skin.js +2 -2
  410. package/dist/dev/_virtual/inline-css_src/define/audio/minimal-skin.js.map +1 -1
  411. package/dist/dev/_virtual/inline-css_src/define/audio/skin.js +2 -2
  412. package/dist/dev/_virtual/inline-css_src/define/audio/skin.js.map +1 -1
  413. package/dist/dev/_virtual/inline-css_src/define/background/skin.js +1 -1
  414. package/dist/dev/_virtual/inline-css_src/define/background/skin.js.map +1 -1
  415. package/dist/dev/_virtual/inline-css_src/define/base.js +1 -1
  416. package/dist/dev/_virtual/inline-css_src/define/base.js.map +1 -1
  417. package/dist/dev/_virtual/inline-css_src/define/shared.js +1 -1
  418. package/dist/dev/_virtual/inline-css_src/define/shared.js.map +1 -1
  419. package/dist/dev/_virtual/inline-css_src/define/video/minimal-skin.js +2 -2
  420. package/dist/dev/_virtual/inline-css_src/define/video/minimal-skin.js.map +1 -1
  421. package/dist/dev/_virtual/inline-css_src/define/video/skin.js +2 -2
  422. package/dist/dev/_virtual/inline-css_src/define/video/skin.js.map +1 -1
  423. package/dist/dev/define/audio/minimal-skin.css +81 -59
  424. package/dist/dev/define/audio/minimal-skin.d.ts +2 -2
  425. package/dist/dev/define/audio/minimal-skin.d.ts.map +1 -1
  426. package/dist/dev/define/audio/minimal-skin.js +20 -7
  427. package/dist/dev/define/audio/minimal-skin.js.map +1 -1
  428. package/dist/dev/define/audio/minimal-skin.tailwind.d.ts +1 -1
  429. package/dist/dev/define/audio/minimal-skin.tailwind.d.ts.map +1 -1
  430. package/dist/dev/define/audio/minimal-skin.tailwind.js +19 -6
  431. package/dist/dev/define/audio/minimal-skin.tailwind.js.map +1 -1
  432. package/dist/dev/define/audio/player.js +1 -2
  433. package/dist/dev/define/audio/player.js.map +1 -1
  434. package/dist/dev/define/audio/skin.css +54 -42
  435. package/dist/dev/define/audio/skin.d.ts +2 -2
  436. package/dist/dev/define/audio/skin.d.ts.map +1 -1
  437. package/dist/dev/define/audio/skin.js +69 -52
  438. package/dist/dev/define/audio/skin.js.map +1 -1
  439. package/dist/dev/define/audio/skin.tailwind.d.ts +1 -1
  440. package/dist/dev/define/audio/skin.tailwind.d.ts.map +1 -1
  441. package/dist/dev/define/audio/skin.tailwind.js +71 -53
  442. package/dist/dev/define/audio/skin.tailwind.js.map +1 -1
  443. package/dist/dev/define/background/player.js +1 -2
  444. package/dist/dev/define/background/player.js.map +1 -1
  445. package/dist/dev/define/background/skin.js +1 -2
  446. package/dist/dev/define/background/skin.js.map +1 -1
  447. package/dist/dev/define/background/video.js +1 -2
  448. package/dist/dev/define/media/background-video.js +1 -2
  449. package/dist/dev/define/media/background-video.js.map +1 -1
  450. package/dist/dev/define/media/container.js +1 -2
  451. package/dist/dev/define/media/container.js.map +1 -1
  452. package/dist/dev/define/media/dash-video.js +1 -2
  453. package/dist/dev/define/media/dash-video.js.map +1 -1
  454. package/dist/dev/define/media/hls-video.js +1 -2
  455. package/dist/dev/define/media/hls-video.js.map +1 -1
  456. package/dist/dev/define/media/mux-video.d.ts +14 -0
  457. package/dist/dev/define/media/mux-video.d.ts.map +1 -0
  458. package/dist/dev/define/media/mux-video.js +13 -0
  459. package/dist/dev/define/media/mux-video.js.map +1 -0
  460. package/dist/dev/define/media/native-hls-video.d.ts +14 -0
  461. package/dist/dev/define/media/native-hls-video.d.ts.map +1 -0
  462. package/dist/dev/define/media/native-hls-video.js +13 -0
  463. package/dist/dev/define/media/native-hls-video.js.map +1 -0
  464. package/dist/dev/define/media/simple-hls-video.d.ts.map +1 -1
  465. package/dist/dev/define/media/simple-hls-video.js +3 -3
  466. package/dist/dev/define/media/simple-hls-video.js.map +1 -1
  467. package/dist/dev/define/safe-define.js +4 -2
  468. package/dist/dev/define/safe-define.js.map +1 -1
  469. package/dist/dev/define/skin-mixin.d.ts +4 -3
  470. package/dist/dev/define/skin-mixin.d.ts.map +1 -1
  471. package/dist/dev/define/skin-mixin.js +30 -11
  472. package/dist/dev/define/skin-mixin.js.map +1 -1
  473. package/dist/dev/define/ui/alert-dialog-close.js +1 -2
  474. package/dist/dev/define/ui/alert-dialog-close.js.map +1 -1
  475. package/dist/dev/define/ui/alert-dialog-description.js +1 -2
  476. package/dist/dev/define/ui/alert-dialog-description.js.map +1 -1
  477. package/dist/dev/define/ui/alert-dialog-title.js +1 -2
  478. package/dist/dev/define/ui/alert-dialog-title.js.map +1 -1
  479. package/dist/dev/define/ui/alert-dialog.js +1 -2
  480. package/dist/dev/define/ui/alert-dialog.js.map +1 -1
  481. package/dist/dev/define/ui/buffering-indicator.js +1 -2
  482. package/dist/dev/define/ui/buffering-indicator.js.map +1 -1
  483. package/dist/dev/define/ui/captions-button.d.ts.map +1 -1
  484. package/dist/dev/define/ui/captions-button.js +3 -3
  485. package/dist/dev/define/ui/captions-button.js.map +1 -1
  486. package/dist/dev/define/ui/controls-group.js +1 -2
  487. package/dist/dev/define/ui/controls-group.js.map +1 -1
  488. package/dist/dev/define/ui/controls.js +1 -2
  489. package/dist/dev/define/ui/controls.js.map +1 -1
  490. package/dist/dev/define/ui/error-dialog.d.ts +9 -0
  491. package/dist/dev/define/ui/error-dialog.d.ts.map +1 -0
  492. package/dist/dev/define/ui/error-dialog.js +13 -0
  493. package/dist/dev/define/ui/error-dialog.js.map +1 -0
  494. package/dist/dev/define/ui/fullscreen-button.js +1 -2
  495. package/dist/dev/define/ui/fullscreen-button.js.map +1 -1
  496. package/dist/dev/define/ui/mute-button.js +1 -2
  497. package/dist/dev/define/ui/mute-button.js.map +1 -1
  498. package/dist/dev/define/ui/pip-button.js +1 -2
  499. package/dist/dev/define/ui/pip-button.js.map +1 -1
  500. package/dist/dev/define/ui/play-button.js +1 -2
  501. package/dist/dev/define/ui/play-button.js.map +1 -1
  502. package/dist/dev/define/ui/playback-rate-button.js +1 -2
  503. package/dist/dev/define/ui/playback-rate-button.js.map +1 -1
  504. package/dist/dev/define/ui/popover.js +1 -2
  505. package/dist/dev/define/ui/popover.js.map +1 -1
  506. package/dist/dev/define/ui/poster.js +1 -2
  507. package/dist/dev/define/ui/poster.js.map +1 -1
  508. package/dist/dev/define/ui/seek-button.js +1 -2
  509. package/dist/dev/define/ui/seek-button.js.map +1 -1
  510. package/dist/dev/define/ui/slider-buffer.js +1 -2
  511. package/dist/dev/define/ui/slider-buffer.js.map +1 -1
  512. package/dist/dev/define/ui/slider-fill.js +1 -2
  513. package/dist/dev/define/ui/slider-fill.js.map +1 -1
  514. package/dist/dev/define/ui/slider-thumb.js +1 -2
  515. package/dist/dev/define/ui/slider-thumb.js.map +1 -1
  516. package/dist/dev/define/ui/slider-thumbnail.js +1 -2
  517. package/dist/dev/define/ui/slider-thumbnail.js.map +1 -1
  518. package/dist/dev/define/ui/slider-track.js +1 -2
  519. package/dist/dev/define/ui/slider-track.js.map +1 -1
  520. package/dist/dev/define/ui/slider-value.js +1 -2
  521. package/dist/dev/define/ui/slider-value.js.map +1 -1
  522. package/dist/dev/define/ui/slider.js +1 -2
  523. package/dist/dev/define/ui/slider.js.map +1 -1
  524. package/dist/dev/define/ui/thumbnail.js +1 -2
  525. package/dist/dev/define/ui/thumbnail.js.map +1 -1
  526. package/dist/dev/define/ui/time-group.js +1 -2
  527. package/dist/dev/define/ui/time-group.js.map +1 -1
  528. package/dist/dev/define/ui/time-separator.js +1 -2
  529. package/dist/dev/define/ui/time-separator.js.map +1 -1
  530. package/dist/dev/define/ui/time-slider.js +1 -2
  531. package/dist/dev/define/ui/time-slider.js.map +1 -1
  532. package/dist/dev/define/ui/time.js +1 -2
  533. package/dist/dev/define/ui/time.js.map +1 -1
  534. package/dist/dev/define/ui/tooltip-group.js +1 -2
  535. package/dist/dev/define/ui/tooltip-group.js.map +1 -1
  536. package/dist/dev/define/ui/tooltip.js +1 -2
  537. package/dist/dev/define/ui/tooltip.js.map +1 -1
  538. package/dist/dev/define/ui/volume-slider.js +1 -2
  539. package/dist/dev/define/ui/volume-slider.js.map +1 -1
  540. package/dist/dev/define/video/minimal-skin.css +156 -75
  541. package/dist/dev/define/video/minimal-skin.d.ts +2 -2
  542. package/dist/dev/define/video/minimal-skin.d.ts.map +1 -1
  543. package/dist/dev/define/video/minimal-skin.js +21 -8
  544. package/dist/dev/define/video/minimal-skin.js.map +1 -1
  545. package/dist/dev/define/video/minimal-skin.tailwind.d.ts +1 -1
  546. package/dist/dev/define/video/minimal-skin.tailwind.d.ts.map +1 -1
  547. package/dist/dev/define/video/minimal-skin.tailwind.js +22 -11
  548. package/dist/dev/define/video/minimal-skin.tailwind.js.map +1 -1
  549. package/dist/dev/define/video/player.js +1 -2
  550. package/dist/dev/define/video/player.js.map +1 -1
  551. package/dist/dev/define/video/skin.css +114 -58
  552. package/dist/dev/define/video/skin.d.ts +2 -2
  553. package/dist/dev/define/video/skin.d.ts.map +1 -1
  554. package/dist/dev/define/video/skin.js +103 -86
  555. package/dist/dev/define/video/skin.js.map +1 -1
  556. package/dist/dev/define/video/skin.tailwind.d.ts +1 -1
  557. package/dist/dev/define/video/skin.tailwind.d.ts.map +1 -1
  558. package/dist/dev/define/video/skin.tailwind.js +104 -88
  559. package/dist/dev/define/video/skin.tailwind.js.map +1 -1
  560. package/dist/dev/icons/dist/render/default/index.js +1 -1
  561. package/dist/dev/icons/dist/render/minimal/index.js +1 -1
  562. package/dist/dev/index.d.ts +3 -2
  563. package/dist/dev/index.js +3 -4
  564. package/dist/dev/media/background-video/index.js +1 -2
  565. package/dist/dev/media/background-video/index.js.map +1 -1
  566. package/dist/dev/media/container-element.js +1 -2
  567. package/dist/dev/media/container-element.js.map +1 -1
  568. package/dist/dev/media/dash-video/index.d.ts +0 -2
  569. package/dist/dev/media/dash-video/index.d.ts.map +1 -1
  570. package/dist/dev/media/dash-video/index.js +4 -12
  571. package/dist/dev/media/dash-video/index.js.map +1 -1
  572. package/dist/dev/media/hls-video/index.d.ts +0 -2
  573. package/dist/dev/media/hls-video/index.d.ts.map +1 -1
  574. package/dist/dev/media/hls-video/index.js +4 -12
  575. package/dist/dev/media/hls-video/index.js.map +1 -1
  576. package/dist/dev/media/mux-video/index.d.ts +11 -0
  577. package/dist/dev/media/mux-video/index.d.ts.map +1 -0
  578. package/dist/dev/media/mux-video/index.js +18 -0
  579. package/dist/dev/media/mux-video/index.js.map +1 -0
  580. package/dist/dev/media/native-hls-video/index.d.ts +11 -0
  581. package/dist/dev/media/native-hls-video/index.d.ts.map +1 -0
  582. package/dist/dev/media/native-hls-video/index.js +18 -0
  583. package/dist/dev/media/native-hls-video/index.js.map +1 -0
  584. package/dist/dev/media/simple-hls-video/index.d.ts +0 -2
  585. package/dist/dev/media/simple-hls-video/index.d.ts.map +1 -1
  586. package/dist/dev/media/simple-hls-video/index.js +4 -12
  587. package/dist/dev/media/simple-hls-video/index.js.map +1 -1
  588. package/dist/dev/player/context.js +1 -2
  589. package/dist/dev/player/context.js.map +1 -1
  590. package/dist/dev/player/create-player.js +1 -2
  591. package/dist/dev/player/create-player.js.map +1 -1
  592. package/dist/dev/player/player-controller.js +1 -2
  593. package/dist/dev/player/player-controller.js.map +1 -1
  594. package/dist/dev/presets/audio.js +1 -2
  595. package/dist/dev/presets/background.js +1 -2
  596. package/dist/dev/presets/video.js +1 -2
  597. package/dist/dev/skins/dist/default/default/tailwind/audio.tailwind.js +14 -18
  598. package/dist/dev/skins/dist/default/default/tailwind/audio.tailwind.js.map +1 -1
  599. package/dist/dev/skins/dist/default/default/tailwind/components/buffering.js +1 -1
  600. package/dist/dev/skins/dist/default/default/tailwind/components/buffering.js.map +1 -1
  601. package/dist/dev/skins/dist/default/default/tailwind/components/button-group.js +7 -0
  602. package/dist/dev/skins/dist/default/default/tailwind/components/button-group.js.map +1 -0
  603. package/dist/dev/skins/dist/default/default/tailwind/components/button.js +3 -4
  604. package/dist/dev/skins/dist/default/default/tailwind/components/button.js.map +1 -1
  605. package/dist/dev/skins/dist/default/default/tailwind/components/controls.js +2 -3
  606. package/dist/dev/skins/dist/default/default/tailwind/components/controls.js.map +1 -1
  607. package/dist/dev/skins/dist/default/default/tailwind/components/error.js +2 -3
  608. package/dist/dev/skins/dist/default/default/tailwind/components/error.js.map +1 -1
  609. package/dist/dev/skins/dist/default/default/tailwind/components/icon.js +1 -2
  610. package/dist/dev/skins/dist/default/default/tailwind/components/icon.js.map +1 -1
  611. package/dist/dev/skins/dist/default/default/tailwind/components/overlay.js +2 -3
  612. package/dist/dev/skins/dist/default/default/tailwind/components/overlay.js.map +1 -1
  613. package/dist/dev/skins/dist/default/default/tailwind/components/playback-rate.js +1 -1
  614. package/dist/dev/skins/dist/default/default/tailwind/components/playback-rate.js.map +1 -1
  615. package/dist/dev/skins/dist/default/default/tailwind/components/popup.js +4 -5
  616. package/dist/dev/skins/dist/default/default/tailwind/components/popup.js.map +1 -1
  617. package/dist/dev/skins/dist/default/default/tailwind/components/poster.js +1 -2
  618. package/dist/dev/skins/dist/default/default/tailwind/components/poster.js.map +1 -1
  619. package/dist/dev/skins/dist/default/default/tailwind/components/preview.js +2 -3
  620. package/dist/dev/skins/dist/default/default/tailwind/components/preview.js.map +1 -1
  621. package/dist/dev/skins/dist/default/default/tailwind/components/root.js +2 -3
  622. package/dist/dev/skins/dist/default/default/tailwind/components/root.js.map +1 -1
  623. package/dist/dev/skins/dist/default/default/tailwind/components/seek.js +1 -2
  624. package/dist/dev/skins/dist/default/default/tailwind/components/seek.js.map +1 -1
  625. package/dist/dev/skins/dist/default/default/tailwind/components/slider.js +2 -3
  626. package/dist/dev/skins/dist/default/default/tailwind/components/slider.js.map +1 -1
  627. package/dist/dev/skins/dist/default/default/tailwind/components/surface.js +2 -3
  628. package/dist/dev/skins/dist/default/default/tailwind/components/surface.js.map +1 -1
  629. package/dist/dev/skins/dist/default/default/tailwind/components/time.js +1 -1
  630. package/dist/dev/skins/dist/default/default/tailwind/components/time.js.map +1 -1
  631. package/dist/dev/skins/dist/default/default/tailwind/video.tailwind.js +25 -20
  632. package/dist/dev/skins/dist/default/default/tailwind/video.tailwind.js.map +1 -1
  633. package/dist/dev/skins/dist/default/minimal/tailwind/audio.tailwind.js +15 -16
  634. package/dist/dev/skins/dist/default/minimal/tailwind/audio.tailwind.js.map +1 -1
  635. package/dist/dev/skins/dist/default/minimal/tailwind/components/buffering.js +1 -1
  636. package/dist/dev/skins/dist/default/minimal/tailwind/components/buffering.js.map +1 -1
  637. package/dist/dev/skins/dist/default/minimal/tailwind/components/button-group.js +1 -2
  638. package/dist/dev/skins/dist/default/minimal/tailwind/components/button-group.js.map +1 -1
  639. package/dist/dev/skins/dist/default/minimal/tailwind/components/button.js +2 -3
  640. package/dist/dev/skins/dist/default/minimal/tailwind/components/button.js.map +1 -1
  641. package/dist/dev/skins/dist/default/minimal/tailwind/components/controls.js +2 -3
  642. package/dist/dev/skins/dist/default/minimal/tailwind/components/controls.js.map +1 -1
  643. package/dist/dev/skins/dist/default/minimal/tailwind/components/error.js +2 -3
  644. package/dist/dev/skins/dist/default/minimal/tailwind/components/error.js.map +1 -1
  645. package/dist/dev/skins/dist/default/minimal/tailwind/components/icon.js +1 -2
  646. package/dist/dev/skins/dist/default/minimal/tailwind/components/icon.js.map +1 -1
  647. package/dist/dev/skins/dist/default/minimal/tailwind/components/overlay.js +2 -3
  648. package/dist/dev/skins/dist/default/minimal/tailwind/components/overlay.js.map +1 -1
  649. package/dist/dev/skins/dist/default/minimal/tailwind/components/playback-rate.js +1 -1
  650. package/dist/dev/skins/dist/default/minimal/tailwind/components/playback-rate.js.map +1 -1
  651. package/dist/dev/skins/dist/default/minimal/tailwind/components/popup.js +2 -3
  652. package/dist/dev/skins/dist/default/minimal/tailwind/components/popup.js.map +1 -1
  653. package/dist/dev/skins/dist/default/minimal/tailwind/components/poster.js +1 -2
  654. package/dist/dev/skins/dist/default/minimal/tailwind/components/poster.js.map +1 -1
  655. package/dist/dev/skins/dist/default/minimal/tailwind/components/preview.js +2 -3
  656. package/dist/dev/skins/dist/default/minimal/tailwind/components/preview.js.map +1 -1
  657. package/dist/dev/skins/dist/default/minimal/tailwind/components/root.js +2 -3
  658. package/dist/dev/skins/dist/default/minimal/tailwind/components/root.js.map +1 -1
  659. package/dist/dev/skins/dist/default/minimal/tailwind/components/seek.js +1 -2
  660. package/dist/dev/skins/dist/default/minimal/tailwind/components/seek.js.map +1 -1
  661. package/dist/dev/skins/dist/default/minimal/tailwind/components/slider.js +2 -3
  662. package/dist/dev/skins/dist/default/minimal/tailwind/components/slider.js.map +1 -1
  663. package/dist/dev/skins/dist/default/minimal/tailwind/components/time.js +5 -6
  664. package/dist/dev/skins/dist/default/minimal/tailwind/components/time.js.map +1 -1
  665. package/dist/dev/skins/dist/default/minimal/tailwind/video.tailwind.js +23 -18
  666. package/dist/dev/skins/dist/default/minimal/tailwind/video.tailwind.js.map +1 -1
  667. package/dist/dev/skins/dist/default/shared/tailwind/icon-state.js +1 -1
  668. package/dist/dev/skins/dist/default/shared/tailwind/icon-state.js.map +1 -1
  669. package/dist/dev/skins/dist/default/shared/tailwind/tooltip-state.js +1 -1
  670. package/dist/dev/skins/dist/default/shared/tailwind/tooltip-state.js.map +1 -1
  671. package/dist/dev/store/container-mixin.js +1 -2
  672. package/dist/dev/store/container-mixin.js.map +1 -1
  673. package/dist/dev/store/media-attach-mixin.js +1 -2
  674. package/dist/dev/store/media-attach-mixin.js.map +1 -1
  675. package/dist/dev/store/provider-mixin.js +1 -2
  676. package/dist/dev/store/provider-mixin.js.map +1 -1
  677. package/dist/dev/ui/alert-dialog/alert-dialog-close-element.js +1 -2
  678. package/dist/dev/ui/alert-dialog/alert-dialog-close-element.js.map +1 -1
  679. package/dist/dev/ui/alert-dialog/alert-dialog-description-element.js +1 -2
  680. package/dist/dev/ui/alert-dialog/alert-dialog-description-element.js.map +1 -1
  681. package/dist/dev/ui/alert-dialog/alert-dialog-element.js +1 -2
  682. package/dist/dev/ui/alert-dialog/alert-dialog-element.js.map +1 -1
  683. package/dist/dev/ui/alert-dialog/alert-dialog-title-element.js +1 -2
  684. package/dist/dev/ui/alert-dialog/alert-dialog-title-element.js.map +1 -1
  685. package/dist/dev/ui/alert-dialog/context.js +2 -5
  686. package/dist/dev/ui/alert-dialog/context.js.map +1 -1
  687. package/dist/dev/ui/buffering-indicator/buffering-indicator-element.js +1 -2
  688. package/dist/dev/ui/buffering-indicator/buffering-indicator-element.js.map +1 -1
  689. package/dist/dev/ui/captions-button/captions-button-element.js +1 -2
  690. package/dist/dev/ui/captions-button/captions-button-element.js.map +1 -1
  691. package/dist/dev/ui/context-part-element.js +1 -2
  692. package/dist/dev/ui/context-part-element.js.map +1 -1
  693. package/dist/dev/ui/controls/context.js +2 -5
  694. package/dist/dev/ui/controls/context.js.map +1 -1
  695. package/dist/dev/ui/controls/controls-element.js +1 -2
  696. package/dist/dev/ui/controls/controls-element.js.map +1 -1
  697. package/dist/dev/ui/controls/controls-group-element.js +1 -2
  698. package/dist/dev/ui/controls/controls-group-element.js.map +1 -1
  699. package/dist/dev/ui/error-dialog/error-dialog-element.d.ts +16 -0
  700. package/dist/dev/ui/error-dialog/error-dialog-element.d.ts.map +1 -0
  701. package/dist/dev/ui/error-dialog/error-dialog-element.js +76 -0
  702. package/dist/dev/ui/error-dialog/error-dialog-element.js.map +1 -0
  703. package/dist/dev/ui/fullscreen-button/fullscreen-button-element.js +1 -2
  704. package/dist/dev/ui/fullscreen-button/fullscreen-button-element.js.map +1 -1
  705. package/dist/dev/ui/media-button-element.js +1 -2
  706. package/dist/dev/ui/media-button-element.js.map +1 -1
  707. package/dist/dev/ui/media-element.js +1 -2
  708. package/dist/dev/ui/media-element.js.map +1 -1
  709. package/dist/dev/ui/media-ui-element.js +1 -2
  710. package/dist/dev/ui/media-ui-element.js.map +1 -1
  711. package/dist/dev/ui/mute-button/mute-button-element.js +1 -2
  712. package/dist/dev/ui/mute-button/mute-button-element.js.map +1 -1
  713. package/dist/dev/ui/pip-button/pip-button-element.js +1 -2
  714. package/dist/dev/ui/pip-button/pip-button-element.js.map +1 -1
  715. package/dist/dev/ui/play-button/play-button-element.js +1 -2
  716. package/dist/dev/ui/play-button/play-button-element.js.map +1 -1
  717. package/dist/dev/ui/playback-rate-button/playback-rate-button-element.js +1 -2
  718. package/dist/dev/ui/playback-rate-button/playback-rate-button-element.js.map +1 -1
  719. package/dist/dev/ui/popover/popover-element.js +1 -2
  720. package/dist/dev/ui/popover/popover-element.js.map +1 -1
  721. package/dist/dev/ui/poster/poster-element.js +1 -2
  722. package/dist/dev/ui/poster/poster-element.js.map +1 -1
  723. package/dist/dev/ui/seek-button/seek-button-element.js +1 -2
  724. package/dist/dev/ui/seek-button/seek-button-element.js.map +1 -1
  725. package/dist/dev/ui/slider/context.js +2 -5
  726. package/dist/dev/ui/slider/context.js.map +1 -1
  727. package/dist/dev/ui/slider/slider-buffer-element.js +1 -2
  728. package/dist/dev/ui/slider/slider-buffer-element.js.map +1 -1
  729. package/dist/dev/ui/slider/slider-element.js +1 -2
  730. package/dist/dev/ui/slider/slider-element.js.map +1 -1
  731. package/dist/dev/ui/slider/slider-fill-element.js +1 -2
  732. package/dist/dev/ui/slider/slider-fill-element.js.map +1 -1
  733. package/dist/dev/ui/slider/slider-preview-element.js +1 -2
  734. package/dist/dev/ui/slider/slider-preview-element.js.map +1 -1
  735. package/dist/dev/ui/slider/slider-thumb-element.js +1 -2
  736. package/dist/dev/ui/slider/slider-thumb-element.js.map +1 -1
  737. package/dist/dev/ui/slider/slider-thumbnail-element.js +1 -2
  738. package/dist/dev/ui/slider/slider-thumbnail-element.js.map +1 -1
  739. package/dist/dev/ui/slider/slider-track-element.js +1 -2
  740. package/dist/dev/ui/slider/slider-track-element.js.map +1 -1
  741. package/dist/dev/ui/slider/slider-value-element.js +1 -2
  742. package/dist/dev/ui/slider/slider-value-element.js.map +1 -1
  743. package/dist/dev/ui/thumbnail/thumbnail-element.js +1 -2
  744. package/dist/dev/ui/thumbnail/thumbnail-element.js.map +1 -1
  745. package/dist/dev/ui/time/time-element.js +1 -2
  746. package/dist/dev/ui/time/time-element.js.map +1 -1
  747. package/dist/dev/ui/time/time-group-element.js +1 -2
  748. package/dist/dev/ui/time/time-group-element.js.map +1 -1
  749. package/dist/dev/ui/time/time-separator-element.js +1 -2
  750. package/dist/dev/ui/time/time-separator-element.js.map +1 -1
  751. package/dist/dev/ui/time-slider/time-slider-element.js +1 -2
  752. package/dist/dev/ui/time-slider/time-slider-element.js.map +1 -1
  753. package/dist/dev/ui/tooltip/context.js +2 -5
  754. package/dist/dev/ui/tooltip/context.js.map +1 -1
  755. package/dist/dev/ui/tooltip/tooltip-element.js +1 -2
  756. package/dist/dev/ui/tooltip/tooltip-element.js.map +1 -1
  757. package/dist/dev/ui/tooltip/tooltip-group-element.js +1 -2
  758. package/dist/dev/ui/tooltip/tooltip-group-element.js.map +1 -1
  759. package/dist/dev/ui/volume-slider/volume-slider-element.js +1 -2
  760. package/dist/dev/ui/volume-slider/volume-slider-element.js.map +1 -1
  761. package/dist/dev/utils/media-props-mixin.js +44 -0
  762. package/dist/dev/utils/media-props-mixin.js.map +1 -0
  763. package/package.json +10 -10
  764. package/cdn/context-Be8C5kVd.js.map +0 -1
  765. package/cdn/context-CUBywtsB.js +0 -14
  766. package/cdn/context-CUBywtsB.js.map +0 -1
  767. package/cdn/create-player-AcfnN3li.js.map +0 -1
  768. package/cdn/create-player-s_qISCpw.js +0 -7
  769. package/cdn/create-player-s_qISCpw.js.map +0 -1
  770. package/cdn/custom-media-element-DqevSVgS.js.map +0 -1
  771. package/cdn/custom-media-element-moFa3UZp.js.map +0 -1
  772. package/cdn/delegate-CzAcT1xm.js.map +0 -1
  773. package/cdn/delegate-Uc-6tQDR.js +0 -2
  774. package/cdn/delegate-Uc-6tQDR.js.map +0 -1
  775. package/cdn/media-attach-mixin-D5_nfJpa.js +0 -2
  776. package/cdn/player-C46h14iP.js +0 -2
  777. package/cdn/poster-odJ4iwIv.js +0 -2
  778. package/cdn/predicate-BG-dj_kF.js +0 -26
  779. package/cdn/predicate-BG-dj_kF.js.map +0 -1
  780. package/cdn/predicate-Y9jDHLpX.js +0 -2
  781. package/cdn/predicate-Y9jDHLpX.js.map +0 -1
  782. package/cdn/safe-define-B8lHgj_K.js +0 -9
  783. package/cdn/safe-define-B8lHgj_K.js.map +0 -1
  784. package/cdn/safe-define-GrHW3P9e.js +0 -2
  785. package/cdn/safe-define-GrHW3P9e.js.map +0 -1
  786. package/cdn/volume-slider-D7BOdSDF.js.map +0 -1
  787. package/cdn/volume-slider-DPeFF5tt.js +0 -8
  788. package/cdn/volume-slider-DPeFF5tt.js.map +0 -1
@@ -1,151 +1,10 @@
1
- import { n as isNil } from "../predicate-BG-dj_kF.js";
2
- import "../context-Be8C5kVd.js";
3
- import { t as listen } from "../listen-YSH3Jfyk.js";
4
- import { t as DelegateMixin } from "../delegate-CzAcT1xm.js";
5
- import { t as MediaAttachMixin } from "../media-attach-mixin-U_KQB_9O.js";
6
- import { t as CustomMediaMixin } from "../custom-media-element-moFa3UZp.js";
1
+ import { t as anyAbortSignal } from "../abort-JT-ewLFq.js";
2
+ import { t as safeDefine } from "../safe-define-D26LrTu4.js";
3
+ import { t as listen } from "../listen-BkAEGXCe.js";
4
+ import { t as DelegateMixin } from "../delegate-CSc5c0ZR.js";
5
+ import { t as MediaAttachMixin } from "../media-attach-mixin-Dsn4gxJA.js";
6
+ import { n as CustomVideoElement, t as MediaPropsMixin } from "../media-props-mixin-DxsM38Bx.js";
7
7
 
8
- //#region ../spf/dist/dev/core/state/create-state.js
9
- /**
10
- * Reactive state container with selectors, custom equality, and batched updates.
11
- *
12
- * Manages both immutable state values and mutable object references (e.g., HTMLMediaElement).
13
- */
14
- const STATE_SYMBOL = Symbol("@videojs/spf/state");
15
- /**
16
- * Default equality function using Object.is.
17
- */
18
- function defaultEquality(a, b) {
19
- return Object.is(a, b);
20
- }
21
- /**
22
- * State container implementation.
23
- */
24
- var StateContainer = class {
25
- [STATE_SYMBOL] = true;
26
- #current;
27
- #pending = null;
28
- #pendingFlush = false;
29
- #equalityFn;
30
- #listeners = /* @__PURE__ */ new Set();
31
- #selectorListeners = /* @__PURE__ */ new Set();
32
- constructor(initial, config) {
33
- this.#current = typeof initial === "object" && initial !== null ? { ...initial } : initial;
34
- this.#equalityFn = config?.equalityFn ?? defaultEquality;
35
- }
36
- get current() {
37
- return this.#pending ?? this.#current;
38
- }
39
- patch(partial) {
40
- const base = this.#pending ?? this.#current;
41
- if (typeof base !== "object" || base === null) {
42
- const value = partial;
43
- if (!Object.is(base, value)) {
44
- this.#pending = value;
45
- this.#scheduleFlush();
46
- }
47
- return;
48
- }
49
- const next = { ...base };
50
- let changed = false;
51
- for (const key in partial) {
52
- if (!Object.hasOwn(partial, key)) continue;
53
- const value = partial[key];
54
- if (!Object.is(base[key], value)) {
55
- next[key] = value;
56
- changed = true;
57
- }
58
- }
59
- if (changed) {
60
- this.#pending = next;
61
- this.#scheduleFlush();
62
- }
63
- }
64
- subscribe(selectorOrListener, maybeListener, options) {
65
- if (maybeListener === void 0) {
66
- const listener = selectorOrListener;
67
- this.#listeners.add(listener);
68
- listener(this.current);
69
- return () => {
70
- this.#listeners.delete(listener);
71
- };
72
- }
73
- const selector = selectorOrListener;
74
- const listener = maybeListener;
75
- const entry = {
76
- selector,
77
- listener,
78
- options: options ?? {}
79
- };
80
- this.#selectorListeners.add(entry);
81
- listener(selector(this.current));
82
- return () => {
83
- this.#selectorListeners.delete(entry);
84
- };
85
- }
86
- flush() {
87
- if (this.#pending === null) return;
88
- const prev = this.#current;
89
- const next = this.#pending;
90
- this.#pending = null;
91
- this.#pendingFlush = false;
92
- if (this.#equalityFn(prev, next)) return;
93
- this.#current = next;
94
- for (const listener of this.#listeners) listener(this.#current);
95
- for (const entry of this.#selectorListeners) {
96
- const prevSelected = entry.selector(prev);
97
- const nextSelected = entry.selector(this.#current);
98
- if (!(entry.options.equalityFn ?? Object.is)(prevSelected, nextSelected)) entry.listener(nextSelected);
99
- }
100
- }
101
- #scheduleFlush() {
102
- if (this.#pendingFlush) return;
103
- this.#pendingFlush = true;
104
- queueMicrotask(() => this.flush());
105
- }
106
- };
107
- /**
108
- * Create a reactive state container.
109
- *
110
- * @example
111
- * ```typescript
112
- * const state = createState({ count: 0 });
113
- *
114
- * // Subscribe to changes
115
- * state.subscribe((current, prev) => {
116
- * console.log('Changed:', prev, '->', current);
117
- * });
118
- *
119
- * // Updates are batched
120
- * state.patch({ count: 1 });
121
- * state.patch({ count: 2 });
122
- * // Only one notification fires (with count: 2)
123
- * ```
124
- *
125
- * @example Selector subscriptions
126
- * ```typescript
127
- * const state = createState({ count: 0, name: 'test' });
128
- *
129
- * // Only notified when count changes
130
- * state.subscribe(
131
- * s => s.count,
132
- * (current, prev) => console.log(current, prev)
133
- * );
134
- * ```
135
- *
136
- * @example Custom equality
137
- * ```typescript
138
- * const state = createState(
139
- * { count: 0, name: 'test' },
140
- * { equalityFn: (a, b) => a.count === b.count }
141
- * );
142
- * ```
143
- */
144
- function createState(initial, config) {
145
- return new StateContainer(initial, config);
146
- }
147
-
148
- //#endregion
149
8
  //#region ../spf/dist/dev/core/abr/ewma.js
150
9
  /**
151
10
  * Exponentially Weighted Moving Average (EWMA)
@@ -377,6 +236,558 @@ function getSegmentsToLoad(segments, bufferedSegments, currentTime, config = DEF
377
236
  });
378
237
  }
379
238
 
239
+ //#endregion
240
+ //#region ../../node_modules/.pnpm/signal-polyfill@0.2.2/node_modules/signal-polyfill/dist/index.js
241
+ var __defProp = Object.defineProperty;
242
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, {
243
+ enumerable: true,
244
+ configurable: true,
245
+ writable: true,
246
+ value
247
+ }) : obj[key] = value;
248
+ var __publicField = (obj, key, value) => {
249
+ __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
250
+ return value;
251
+ };
252
+ var __accessCheck = (obj, member, msg) => {
253
+ if (!member.has(obj)) throw TypeError("Cannot " + msg);
254
+ };
255
+ var __privateIn = (member, obj) => {
256
+ if (Object(obj) !== obj) throw TypeError("Cannot use the \"in\" operator on this value");
257
+ return member.has(obj);
258
+ };
259
+ var __privateAdd = (obj, member, value) => {
260
+ if (member.has(obj)) throw TypeError("Cannot add the same private member more than once");
261
+ member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
262
+ };
263
+ var __privateMethod = (obj, member, method) => {
264
+ __accessCheck(obj, member, "access private method");
265
+ return method;
266
+ };
267
+ /**
268
+ * @license
269
+ * Copyright Google LLC All Rights Reserved.
270
+ *
271
+ * Use of this source code is governed by an MIT-style license that can be
272
+ * found in the LICENSE file at https://angular.io/license
273
+ */
274
+ function defaultEquals(a, b) {
275
+ return Object.is(a, b);
276
+ }
277
+ /**
278
+ * @license
279
+ * Copyright Google LLC All Rights Reserved.
280
+ *
281
+ * Use of this source code is governed by an MIT-style license that can be
282
+ * found in the LICENSE file at https://angular.io/license
283
+ */
284
+ let activeConsumer = null;
285
+ let inNotificationPhase = false;
286
+ let epoch = 1;
287
+ const SIGNAL = /* @__PURE__ */ Symbol("SIGNAL");
288
+ function setActiveConsumer(consumer) {
289
+ const prev = activeConsumer;
290
+ activeConsumer = consumer;
291
+ return prev;
292
+ }
293
+ function getActiveConsumer() {
294
+ return activeConsumer;
295
+ }
296
+ function isInNotificationPhase() {
297
+ return inNotificationPhase;
298
+ }
299
+ const REACTIVE_NODE = {
300
+ version: 0,
301
+ lastCleanEpoch: 0,
302
+ dirty: false,
303
+ producerNode: void 0,
304
+ producerLastReadVersion: void 0,
305
+ producerIndexOfThis: void 0,
306
+ nextProducerIndex: 0,
307
+ liveConsumerNode: void 0,
308
+ liveConsumerIndexOfThis: void 0,
309
+ consumerAllowSignalWrites: false,
310
+ consumerIsAlwaysLive: false,
311
+ producerMustRecompute: () => false,
312
+ producerRecomputeValue: () => {},
313
+ consumerMarkedDirty: () => {},
314
+ consumerOnSignalRead: () => {}
315
+ };
316
+ function producerAccessed(node) {
317
+ if (inNotificationPhase) throw new Error(typeof ngDevMode !== "undefined" && ngDevMode ? `Assertion error: signal read during notification phase` : "");
318
+ if (activeConsumer === null) return;
319
+ activeConsumer.consumerOnSignalRead(node);
320
+ const idx = activeConsumer.nextProducerIndex++;
321
+ assertConsumerNode(activeConsumer);
322
+ if (idx < activeConsumer.producerNode.length && activeConsumer.producerNode[idx] !== node) {
323
+ if (consumerIsLive(activeConsumer)) {
324
+ const staleProducer = activeConsumer.producerNode[idx];
325
+ producerRemoveLiveConsumerAtIndex(staleProducer, activeConsumer.producerIndexOfThis[idx]);
326
+ }
327
+ }
328
+ if (activeConsumer.producerNode[idx] !== node) {
329
+ activeConsumer.producerNode[idx] = node;
330
+ activeConsumer.producerIndexOfThis[idx] = consumerIsLive(activeConsumer) ? producerAddLiveConsumer(node, activeConsumer, idx) : 0;
331
+ }
332
+ activeConsumer.producerLastReadVersion[idx] = node.version;
333
+ }
334
+ function producerIncrementEpoch() {
335
+ epoch++;
336
+ }
337
+ function producerUpdateValueVersion(node) {
338
+ if (!node.dirty && node.lastCleanEpoch === epoch) return;
339
+ if (!node.producerMustRecompute(node) && !consumerPollProducersForChange(node)) {
340
+ node.dirty = false;
341
+ node.lastCleanEpoch = epoch;
342
+ return;
343
+ }
344
+ node.producerRecomputeValue(node);
345
+ node.dirty = false;
346
+ node.lastCleanEpoch = epoch;
347
+ }
348
+ function producerNotifyConsumers(node) {
349
+ if (node.liveConsumerNode === void 0) return;
350
+ const prev = inNotificationPhase;
351
+ inNotificationPhase = true;
352
+ try {
353
+ for (const consumer of node.liveConsumerNode) if (!consumer.dirty) consumerMarkDirty(consumer);
354
+ } finally {
355
+ inNotificationPhase = prev;
356
+ }
357
+ }
358
+ function producerUpdatesAllowed() {
359
+ return (activeConsumer == null ? void 0 : activeConsumer.consumerAllowSignalWrites) !== false;
360
+ }
361
+ function consumerMarkDirty(node) {
362
+ var _a;
363
+ node.dirty = true;
364
+ producerNotifyConsumers(node);
365
+ (_a = node.consumerMarkedDirty) == null || _a.call(node.wrapper ?? node);
366
+ }
367
+ function consumerBeforeComputation(node) {
368
+ node && (node.nextProducerIndex = 0);
369
+ return setActiveConsumer(node);
370
+ }
371
+ function consumerAfterComputation(node, prevConsumer) {
372
+ setActiveConsumer(prevConsumer);
373
+ if (!node || node.producerNode === void 0 || node.producerIndexOfThis === void 0 || node.producerLastReadVersion === void 0) return;
374
+ if (consumerIsLive(node)) for (let i = node.nextProducerIndex; i < node.producerNode.length; i++) producerRemoveLiveConsumerAtIndex(node.producerNode[i], node.producerIndexOfThis[i]);
375
+ while (node.producerNode.length > node.nextProducerIndex) {
376
+ node.producerNode.pop();
377
+ node.producerLastReadVersion.pop();
378
+ node.producerIndexOfThis.pop();
379
+ }
380
+ }
381
+ function consumerPollProducersForChange(node) {
382
+ assertConsumerNode(node);
383
+ for (let i = 0; i < node.producerNode.length; i++) {
384
+ const producer = node.producerNode[i];
385
+ const seenVersion = node.producerLastReadVersion[i];
386
+ if (seenVersion !== producer.version) return true;
387
+ producerUpdateValueVersion(producer);
388
+ if (seenVersion !== producer.version) return true;
389
+ }
390
+ return false;
391
+ }
392
+ function producerAddLiveConsumer(node, consumer, indexOfThis) {
393
+ var _a;
394
+ assertProducerNode(node);
395
+ assertConsumerNode(node);
396
+ if (node.liveConsumerNode.length === 0) {
397
+ (_a = node.watched) == null || _a.call(node.wrapper);
398
+ for (let i = 0; i < node.producerNode.length; i++) node.producerIndexOfThis[i] = producerAddLiveConsumer(node.producerNode[i], node, i);
399
+ }
400
+ node.liveConsumerIndexOfThis.push(indexOfThis);
401
+ return node.liveConsumerNode.push(consumer) - 1;
402
+ }
403
+ function producerRemoveLiveConsumerAtIndex(node, idx) {
404
+ var _a;
405
+ assertProducerNode(node);
406
+ assertConsumerNode(node);
407
+ if (typeof ngDevMode !== "undefined" && ngDevMode && idx >= node.liveConsumerNode.length) throw new Error(`Assertion error: active consumer index ${idx} is out of bounds of ${node.liveConsumerNode.length} consumers)`);
408
+ if (node.liveConsumerNode.length === 1) {
409
+ (_a = node.unwatched) == null || _a.call(node.wrapper);
410
+ for (let i = 0; i < node.producerNode.length; i++) producerRemoveLiveConsumerAtIndex(node.producerNode[i], node.producerIndexOfThis[i]);
411
+ }
412
+ const lastIdx = node.liveConsumerNode.length - 1;
413
+ node.liveConsumerNode[idx] = node.liveConsumerNode[lastIdx];
414
+ node.liveConsumerIndexOfThis[idx] = node.liveConsumerIndexOfThis[lastIdx];
415
+ node.liveConsumerNode.length--;
416
+ node.liveConsumerIndexOfThis.length--;
417
+ if (idx < node.liveConsumerNode.length) {
418
+ const idxProducer = node.liveConsumerIndexOfThis[idx];
419
+ const consumer = node.liveConsumerNode[idx];
420
+ assertConsumerNode(consumer);
421
+ consumer.producerIndexOfThis[idxProducer] = idx;
422
+ }
423
+ }
424
+ function consumerIsLive(node) {
425
+ var _a;
426
+ return node.consumerIsAlwaysLive || (((_a = node == null ? void 0 : node.liveConsumerNode) == null ? void 0 : _a.length) ?? 0) > 0;
427
+ }
428
+ function assertConsumerNode(node) {
429
+ node.producerNode ?? (node.producerNode = []);
430
+ node.producerIndexOfThis ?? (node.producerIndexOfThis = []);
431
+ node.producerLastReadVersion ?? (node.producerLastReadVersion = []);
432
+ }
433
+ function assertProducerNode(node) {
434
+ node.liveConsumerNode ?? (node.liveConsumerNode = []);
435
+ node.liveConsumerIndexOfThis ?? (node.liveConsumerIndexOfThis = []);
436
+ }
437
+ /**
438
+ * @license
439
+ * Copyright Google LLC All Rights Reserved.
440
+ *
441
+ * Use of this source code is governed by an MIT-style license that can be
442
+ * found in the LICENSE file at https://angular.io/license
443
+ */
444
+ function computedGet(node) {
445
+ producerUpdateValueVersion(node);
446
+ producerAccessed(node);
447
+ if (node.value === ERRORED) throw node.error;
448
+ return node.value;
449
+ }
450
+ function createComputed(computation) {
451
+ const node = Object.create(COMPUTED_NODE);
452
+ node.computation = computation;
453
+ const computed = () => computedGet(node);
454
+ computed[SIGNAL] = node;
455
+ return computed;
456
+ }
457
+ const UNSET = /* @__PURE__ */ Symbol("UNSET");
458
+ const COMPUTING = /* @__PURE__ */ Symbol("COMPUTING");
459
+ const ERRORED = /* @__PURE__ */ Symbol("ERRORED");
460
+ const COMPUTED_NODE = {
461
+ ...REACTIVE_NODE,
462
+ value: UNSET,
463
+ dirty: true,
464
+ error: null,
465
+ equal: defaultEquals,
466
+ producerMustRecompute(node) {
467
+ return node.value === UNSET || node.value === COMPUTING;
468
+ },
469
+ producerRecomputeValue(node) {
470
+ if (node.value === COMPUTING) throw new Error("Detected cycle in computations.");
471
+ const oldValue = node.value;
472
+ node.value = COMPUTING;
473
+ const prevConsumer = consumerBeforeComputation(node);
474
+ let newValue;
475
+ let wasEqual = false;
476
+ try {
477
+ newValue = node.computation.call(node.wrapper);
478
+ wasEqual = oldValue !== UNSET && oldValue !== ERRORED && node.equal.call(node.wrapper, oldValue, newValue);
479
+ } catch (err) {
480
+ newValue = ERRORED;
481
+ node.error = err;
482
+ } finally {
483
+ consumerAfterComputation(node, prevConsumer);
484
+ }
485
+ if (wasEqual) {
486
+ node.value = oldValue;
487
+ return;
488
+ }
489
+ node.value = newValue;
490
+ node.version++;
491
+ }
492
+ };
493
+ /**
494
+ * @license
495
+ * Copyright Google LLC All Rights Reserved.
496
+ *
497
+ * Use of this source code is governed by an MIT-style license that can be
498
+ * found in the LICENSE file at https://angular.io/license
499
+ */
500
+ function defaultThrowError() {
501
+ throw new Error();
502
+ }
503
+ let throwInvalidWriteToSignalErrorFn = defaultThrowError;
504
+ function throwInvalidWriteToSignalError() {
505
+ throwInvalidWriteToSignalErrorFn();
506
+ }
507
+ /**
508
+ * @license
509
+ * Copyright Google LLC All Rights Reserved.
510
+ *
511
+ * Use of this source code is governed by an MIT-style license that can be
512
+ * found in the LICENSE file at https://angular.io/license
513
+ */
514
+ function createSignal(initialValue) {
515
+ const node = Object.create(SIGNAL_NODE);
516
+ node.value = initialValue;
517
+ const getter = () => {
518
+ producerAccessed(node);
519
+ return node.value;
520
+ };
521
+ getter[SIGNAL] = node;
522
+ return getter;
523
+ }
524
+ function signalGetFn() {
525
+ producerAccessed(this);
526
+ return this.value;
527
+ }
528
+ function signalSetFn(node, newValue) {
529
+ if (!producerUpdatesAllowed()) throwInvalidWriteToSignalError();
530
+ if (!node.equal.call(node.wrapper, node.value, newValue)) {
531
+ node.value = newValue;
532
+ signalValueChanged(node);
533
+ }
534
+ }
535
+ const SIGNAL_NODE = {
536
+ ...REACTIVE_NODE,
537
+ equal: defaultEquals,
538
+ value: void 0
539
+ };
540
+ function signalValueChanged(node) {
541
+ node.version++;
542
+ producerIncrementEpoch();
543
+ producerNotifyConsumers(node);
544
+ }
545
+ /**
546
+ * @license
547
+ * Copyright 2024 Bloomberg Finance L.P.
548
+ *
549
+ * Licensed under the Apache License, Version 2.0 (the "License");
550
+ * you may not use this file except in compliance with the License.
551
+ * You may obtain a copy of the License at
552
+ *
553
+ * http://www.apache.org/licenses/LICENSE-2.0
554
+ *
555
+ * Unless required by applicable law or agreed to in writing, software
556
+ * distributed under the License is distributed on an "AS IS" BASIS,
557
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
558
+ * See the License for the specific language governing permissions and
559
+ * limitations under the License.
560
+ */
561
+ const NODE = Symbol("node");
562
+ var Signal;
563
+ ((Signal2) => {
564
+ var _a, _brand, _b, _brand2;
565
+ class State {
566
+ constructor(initialValue, options = {}) {
567
+ __privateAdd(this, _brand);
568
+ __publicField(this, _a);
569
+ const node = createSignal(initialValue)[SIGNAL];
570
+ this[NODE] = node;
571
+ node.wrapper = this;
572
+ if (options) {
573
+ const equals = options.equals;
574
+ if (equals) node.equal = equals;
575
+ node.watched = options[Signal2.subtle.watched];
576
+ node.unwatched = options[Signal2.subtle.unwatched];
577
+ }
578
+ }
579
+ get() {
580
+ if (!(0, Signal2.isState)(this)) throw new TypeError("Wrong receiver type for Signal.State.prototype.get");
581
+ return signalGetFn.call(this[NODE]);
582
+ }
583
+ set(newValue) {
584
+ if (!(0, Signal2.isState)(this)) throw new TypeError("Wrong receiver type for Signal.State.prototype.set");
585
+ if (isInNotificationPhase()) throw new Error("Writes to signals not permitted during Watcher callback");
586
+ const ref = this[NODE];
587
+ signalSetFn(ref, newValue);
588
+ }
589
+ }
590
+ _a = NODE;
591
+ _brand = /* @__PURE__ */ new WeakSet();
592
+ Signal2.isState = (s) => typeof s === "object" && __privateIn(_brand, s);
593
+ Signal2.State = State;
594
+ class Computed {
595
+ constructor(computation, options) {
596
+ __privateAdd(this, _brand2);
597
+ __publicField(this, _b);
598
+ const node = createComputed(computation)[SIGNAL];
599
+ node.consumerAllowSignalWrites = true;
600
+ this[NODE] = node;
601
+ node.wrapper = this;
602
+ if (options) {
603
+ const equals = options.equals;
604
+ if (equals) node.equal = equals;
605
+ node.watched = options[Signal2.subtle.watched];
606
+ node.unwatched = options[Signal2.subtle.unwatched];
607
+ }
608
+ }
609
+ get() {
610
+ if (!(0, Signal2.isComputed)(this)) throw new TypeError("Wrong receiver type for Signal.Computed.prototype.get");
611
+ return computedGet(this[NODE]);
612
+ }
613
+ }
614
+ _b = NODE;
615
+ _brand2 = /* @__PURE__ */ new WeakSet();
616
+ Signal2.isComputed = (c) => typeof c === "object" && __privateIn(_brand2, c);
617
+ Signal2.Computed = Computed;
618
+ ((subtle2) => {
619
+ var _a2, _brand3, _assertSignals, assertSignals_fn;
620
+ function untrack(cb) {
621
+ let output;
622
+ let prevActiveConsumer = null;
623
+ try {
624
+ prevActiveConsumer = setActiveConsumer(null);
625
+ output = cb();
626
+ } finally {
627
+ setActiveConsumer(prevActiveConsumer);
628
+ }
629
+ return output;
630
+ }
631
+ subtle2.untrack = untrack;
632
+ function introspectSources(sink) {
633
+ var _a3;
634
+ if (!(0, Signal2.isComputed)(sink) && !(0, Signal2.isWatcher)(sink)) throw new TypeError("Called introspectSources without a Computed or Watcher argument");
635
+ return ((_a3 = sink[NODE].producerNode) == null ? void 0 : _a3.map((n) => n.wrapper)) ?? [];
636
+ }
637
+ subtle2.introspectSources = introspectSources;
638
+ function introspectSinks(signal) {
639
+ var _a3;
640
+ if (!(0, Signal2.isComputed)(signal) && !(0, Signal2.isState)(signal)) throw new TypeError("Called introspectSinks without a Signal argument");
641
+ return ((_a3 = signal[NODE].liveConsumerNode) == null ? void 0 : _a3.map((n) => n.wrapper)) ?? [];
642
+ }
643
+ subtle2.introspectSinks = introspectSinks;
644
+ function hasSinks(signal) {
645
+ if (!(0, Signal2.isComputed)(signal) && !(0, Signal2.isState)(signal)) throw new TypeError("Called hasSinks without a Signal argument");
646
+ const liveConsumerNode = signal[NODE].liveConsumerNode;
647
+ if (!liveConsumerNode) return false;
648
+ return liveConsumerNode.length > 0;
649
+ }
650
+ subtle2.hasSinks = hasSinks;
651
+ function hasSources(signal) {
652
+ if (!(0, Signal2.isComputed)(signal) && !(0, Signal2.isWatcher)(signal)) throw new TypeError("Called hasSources without a Computed or Watcher argument");
653
+ const producerNode = signal[NODE].producerNode;
654
+ if (!producerNode) return false;
655
+ return producerNode.length > 0;
656
+ }
657
+ subtle2.hasSources = hasSources;
658
+ class Watcher {
659
+ constructor(notify) {
660
+ __privateAdd(this, _brand3);
661
+ __privateAdd(this, _assertSignals);
662
+ __publicField(this, _a2);
663
+ let node = Object.create(REACTIVE_NODE);
664
+ node.wrapper = this;
665
+ node.consumerMarkedDirty = notify;
666
+ node.consumerIsAlwaysLive = true;
667
+ node.consumerAllowSignalWrites = false;
668
+ node.producerNode = [];
669
+ this[NODE] = node;
670
+ }
671
+ watch(...signals) {
672
+ if (!(0, Signal2.isWatcher)(this)) throw new TypeError("Called unwatch without Watcher receiver");
673
+ __privateMethod(this, _assertSignals, assertSignals_fn).call(this, signals);
674
+ const node = this[NODE];
675
+ node.dirty = false;
676
+ const prev = setActiveConsumer(node);
677
+ for (const signal of signals) producerAccessed(signal[NODE]);
678
+ setActiveConsumer(prev);
679
+ }
680
+ unwatch(...signals) {
681
+ if (!(0, Signal2.isWatcher)(this)) throw new TypeError("Called unwatch without Watcher receiver");
682
+ __privateMethod(this, _assertSignals, assertSignals_fn).call(this, signals);
683
+ const node = this[NODE];
684
+ assertConsumerNode(node);
685
+ for (let i = node.producerNode.length - 1; i >= 0; i--) if (signals.includes(node.producerNode[i].wrapper)) {
686
+ producerRemoveLiveConsumerAtIndex(node.producerNode[i], node.producerIndexOfThis[i]);
687
+ const lastIdx = node.producerNode.length - 1;
688
+ node.producerNode[i] = node.producerNode[lastIdx];
689
+ node.producerIndexOfThis[i] = node.producerIndexOfThis[lastIdx];
690
+ node.producerNode.length--;
691
+ node.producerIndexOfThis.length--;
692
+ node.nextProducerIndex--;
693
+ if (i < node.producerNode.length) {
694
+ const idxConsumer = node.producerIndexOfThis[i];
695
+ const producer = node.producerNode[i];
696
+ assertProducerNode(producer);
697
+ producer.liveConsumerIndexOfThis[idxConsumer] = i;
698
+ }
699
+ }
700
+ }
701
+ getPending() {
702
+ if (!(0, Signal2.isWatcher)(this)) throw new TypeError("Called getPending without Watcher receiver");
703
+ return this[NODE].producerNode.filter((n) => n.dirty).map((n) => n.wrapper);
704
+ }
705
+ }
706
+ _a2 = NODE;
707
+ _brand3 = /* @__PURE__ */ new WeakSet();
708
+ _assertSignals = /* @__PURE__ */ new WeakSet();
709
+ assertSignals_fn = function(signals) {
710
+ for (const signal of signals) if (!(0, Signal2.isComputed)(signal) && !(0, Signal2.isState)(signal)) throw new TypeError("Called watch/unwatch without a Computed or State argument");
711
+ };
712
+ Signal2.isWatcher = (w) => __privateIn(_brand3, w);
713
+ subtle2.Watcher = Watcher;
714
+ function currentComputed() {
715
+ var _a3;
716
+ return (_a3 = getActiveConsumer()) == null ? void 0 : _a3.wrapper;
717
+ }
718
+ subtle2.currentComputed = currentComputed;
719
+ subtle2.watched = Symbol("watched");
720
+ subtle2.unwatched = Symbol("unwatched");
721
+ })(Signal2.subtle || (Signal2.subtle = {}));
722
+ })(Signal || (Signal = {}));
723
+
724
+ //#endregion
725
+ //#region ../spf/dist/dev/core/signals/effect.js
726
+ const pending = /* @__PURE__ */ new Set();
727
+ const watcher = new Signal.subtle.Watcher(() => {
728
+ queueMicrotask(runPending);
729
+ });
730
+ function runPending() {
731
+ for (const c of watcher.getPending()) pending.add(c);
732
+ watcher.watch();
733
+ for (const c of pending) {
734
+ pending.delete(c);
735
+ c.get();
736
+ }
737
+ }
738
+ /**
739
+ * Run a side effect whenever its signal dependencies change.
740
+ *
741
+ * Executes immediately (synchronous initial run), then re-runs on the next
742
+ * microtask after any dependency changes. If the callback returns a function,
743
+ * it is called before each re-run and when the effect is stopped — the same
744
+ * cleanup contract as Preact Signals, Maverick Signals, and Svelte 5 $effect.
745
+ *
746
+ * Returns a cleanup function that stops the effect.
747
+ */
748
+ function effect(fn) {
749
+ let cleanup;
750
+ const c = new Signal.Computed(() => {
751
+ if (typeof cleanup === "function") cleanup();
752
+ cleanup = fn();
753
+ });
754
+ watcher.watch(c);
755
+ c.get();
756
+ return () => {
757
+ watcher.unwatch(c);
758
+ if (typeof cleanup === "function") cleanup();
759
+ };
760
+ }
761
+
762
+ //#endregion
763
+ //#region ../spf/dist/dev/core/signals/primitives.js
764
+ /** Read a signal value without tracking it as a dependency. */
765
+ const untrack = Signal.subtle.untrack;
766
+ /** Create a writable reactive value. */
767
+ function signal(initialValue, options) {
768
+ return new Signal.State(initialValue, options);
769
+ }
770
+ /** Create a computed reactive value. */
771
+ function computed(fn, options) {
772
+ return new Signal.Computed(fn, options);
773
+ }
774
+ /**
775
+ * Update a writable signal. Accepts either a partial object to merge into the
776
+ * current state, or an updater function that receives the current state and
777
+ * returns the next state.
778
+ *
779
+ * @example
780
+ * update(state, { playbackRate: 2 });
781
+ * update(state, (s) => ({ ...s, count: s.count + 1 }));
782
+ */
783
+ function update(signal, updater) {
784
+ const current = untrack(() => signal.get());
785
+ signal.set(typeof updater === "function" ? updater(current) : {
786
+ ...current,
787
+ ...updater
788
+ });
789
+ }
790
+
380
791
  //#endregion
381
792
  //#region ../spf/dist/dev/core/types/index.js
382
793
  function isResolvedTrack(track) {
@@ -390,6 +801,43 @@ function hasPresentationDuration(presentation) {
390
801
  return presentation.duration !== void 0;
391
802
  }
392
803
 
804
+ //#endregion
805
+ //#region ../spf/dist/dev/core/utils/track-selection.js
806
+ /**
807
+ * Map track type to selected track ID property key in state.
808
+ */
809
+ const SelectedTrackIdKeyByType = {
810
+ video: "selectedVideoTrackId",
811
+ audio: "selectedAudioTrackId",
812
+ text: "selectedTextTrackId"
813
+ };
814
+ /**
815
+ * Map track type to buffer owner property key.
816
+ * Used for SourceBuffer references in owners.
817
+ */
818
+ const BufferKeyByType = {
819
+ video: "videoBuffer",
820
+ audio: "audioBuffer"
821
+ };
822
+ /**
823
+ * Get selected track from state by type.
824
+ * Returns properly typed track (partially or fully resolved) or undefined.
825
+ * Type parameter T is inferred from the type argument.
826
+ *
827
+ * @example
828
+ * const videoTrack = getSelectedTrack(state, 'video');
829
+ * if (videoTrack && isResolvedTrack(videoTrack)) {
830
+ * // videoTrack is VideoTrack
831
+ * }
832
+ */
833
+ function getSelectedTrack(state, type) {
834
+ const { presentation } = state;
835
+ /** @TODO Consider moving and reusing isUnresolved(presentation) (CJP) */
836
+ if (!presentation || !("id" in presentation)) return void 0;
837
+ const trackId = state[SelectedTrackIdKeyByType[type]];
838
+ return presentation.selectionSets.find(({ type: selectionSetType }) => selectionSetType === type)?.switchingSets[0]?.tracks.find(({ id }) => id === trackId);
839
+ }
840
+
393
841
  //#endregion
394
842
  //#region ../spf/dist/dev/dom/network/chunked-stream-iterable.js
395
843
  const DEFAULT_MIN_CHUNK_SIZE = 2 ** 17;
@@ -490,1812 +938,1511 @@ function getResponseText(response) {
490
938
  }
491
939
 
492
940
  //#endregion
493
- //#region ../spf/dist/dev/core/reactive/combine-latest.js
941
+ //#region ../spf/dist/dev/core/buffer/back-buffer.js
942
+ /**
943
+ * Default back buffer configuration.
944
+ */
945
+ const DEFAULT_BACK_BUFFER_CONFIG = { keepSegments: 2 };
494
946
  /**
495
- * Combines multiple Observable sources into a single Observable.
947
+ * Calculate back buffer flush point.
496
948
  *
497
- * Emits an array of latest values whenever any source emits.
498
- * Only emits after all sources have emitted at least once.
949
+ * Determines where to flush old segments from the back buffer.
950
+ * Keeps a fixed number of segments behind the current playback position.
499
951
  *
500
- * Supports selector-based subscriptions (fires only when the selected value
501
- * changes, per the optional equalityFn) mirroring the createState API.
952
+ * Algorithm:
953
+ * 1. Find segments before currentTime
954
+ * 2. Count back N segments (keepSegments)
955
+ * 3. Return startTime of segment N+1 back (flush everything before this)
502
956
  *
503
- * @param sources - Array of Observable sources
504
- * @returns Combined Observable
957
+ * @param segments - Available segments (should be sorted by startTime)
958
+ * @param currentTime - Current playback position in seconds
959
+ * @param config - Optional back buffer configuration
960
+ * @returns Time in seconds to flush up to (flush range: [0, flushEnd))
505
961
  *
506
962
  * @example
507
- * ```ts
508
- * const state = createState({ count: 0 });
509
- * const events = createEventStream<Action>();
510
- *
511
- * combineLatest([state, events]).subscribe(([state, event]) => {
512
- * if (event.type === 'PLAY' && state.count > 0) {
513
- * // React to event + state condition
514
- * }
515
- * });
516
- * ```
963
+ * const segments = [
964
+ * { startTime: 0, duration: 6, ... },
965
+ * { startTime: 6, duration: 6, ... },
966
+ * { startTime: 12, duration: 6, ... },
967
+ * { startTime: 18, duration: 6, ... },
968
+ * ];
517
969
  *
518
- * @example Selector subscription
519
- * ```ts
520
- * combineLatest([state, owners]).subscribe(
521
- * ([s, o]) => deriveKey(s, o),
522
- * (key) => { ... },
523
- * { equalityFn: keyEq }
524
- * );
525
- * ```
526
- */
527
- function combineLatest(sources) {
528
- const subscribeToSources = (listener) => {
529
- const latest = new Array(sources.length);
530
- const hasValue = new Array(sources.length).fill(false);
531
- const unsubscribers = [];
532
- for (let i = 0; i < sources.length; i++) {
533
- const unsubscribe = sources[i].subscribe((value) => {
534
- latest[i] = value;
535
- hasValue[i] = true;
536
- if (hasValue.every((has) => has)) listener([...latest]);
537
- });
538
- unsubscribers.push(unsubscribe);
539
- }
540
- return () => {
541
- for (const unsubscribe of unsubscribers) unsubscribe();
542
- };
543
- };
544
- return { subscribe(listenerOrSelector, maybeListener, options) {
545
- if (maybeListener === void 0) return subscribeToSources(listenerOrSelector);
546
- const selector = listenerOrSelector;
547
- const listener = maybeListener;
548
- const equalityFn = options?.equalityFn ?? Object.is;
549
- let prevSelected;
550
- let initialized = false;
551
- return subscribeToSources((values) => {
552
- const nextSelected = selector(values);
553
- if (!initialized || !equalityFn(prevSelected, nextSelected)) {
554
- prevSelected = nextSelected;
555
- initialized = true;
556
- listener(nextSelected);
557
- }
558
- });
559
- } };
560
- }
561
-
562
- //#endregion
563
- //#region ../spf/dist/dev/core/hls/resolve-url.js
564
- /**
565
- * Resolve a potentially relative URL against a base URL using native URL API.
970
+ * // Playing at 18s, keep 2 segments
971
+ * const flushEnd = calculateBackBufferFlushPoint(segments, 18);
972
+ * // Returns 6 (flush [0, 6), keep [6-18))
566
973
  */
567
- function resolveUrl(url, baseUrl) {
568
- return new URL(url, baseUrl).href;
974
+ function calculateBackBufferFlushPoint(segments, currentTime, config = DEFAULT_BACK_BUFFER_CONFIG) {
975
+ if (segments.length === 0) return 0;
976
+ const segmentsBefore = segments.filter((seg) => seg.startTime < currentTime);
977
+ if (segmentsBefore.length === 0) return 0;
978
+ const segmentsToFlush = segmentsBefore.length - config.keepSegments;
979
+ if (segmentsToFlush <= 0) return 0;
980
+ if (segmentsToFlush >= segmentsBefore.length) return currentTime;
981
+ return segmentsBefore[segmentsToFlush].startTime;
569
982
  }
570
983
 
571
984
  //#endregion
572
- //#region ../spf/dist/dev/core/hls/parse-attributes.js
985
+ //#region ../spf/dist/dev/dom/features/segment-loader-actor.js
573
986
  /**
574
- * Parse HLS attribute list from a tag line.
575
- * Handles both quoted and unquoted values.
987
+ * Creates a SegmentLoaderActor for one track type (video or audio).
988
+ *
989
+ * Receives load assignments via `send()` and owns all execution: planning,
990
+ * removes, fetches, and appends. Coordinates with the SourceBufferActor for
991
+ * all physical SourceBuffer operations.
992
+ *
993
+ * Planning (Cases 1–3) happens in `send()` on every incoming message, producing
994
+ * an ordered LoadTask list. The runner drains that list sequentially. When a new
995
+ * message arrives mid-run, send() replans and either continues the in-flight
996
+ * operation (if still needed) or preempts it.
997
+ *
998
+ * @param sourceBufferActor - Shared SourceBufferActor reference (not owned)
999
+ * @param fetchBytes - Tracked fetch closure (owns throughput sampling for segments).
1000
+ * Accepts an optional `minChunkSize` in options; init segments pass `Infinity`
1001
+ * so the entire body accumulates as one chunk before appending.
576
1002
  */
577
- function parseAttributeList(line) {
578
- const attributes = /* @__PURE__ */ new Map();
579
- for (const match of line.matchAll(/([A-Z0-9-]+)=(?:"([^"]*)"|([^,]*))/g)) {
580
- const key = match[1];
581
- const value = match[2] ?? match[3] ?? "";
582
- if (key) attributes.set(key, value);
583
- }
584
- return attributes;
585
- }
586
- /**
587
- * Parse RESOLUTION attribute value (WIDTHxHEIGHT).
588
- */
589
- function parseResolution(value) {
590
- const match = /^(\d+)x(\d+)$/.exec(value);
591
- if (!match) return null;
592
- return {
593
- width: Number.parseInt(match[1], 10),
594
- height: Number.parseInt(match[2], 10)
595
- };
596
- }
597
- /**
598
- * Parse FRAME-RATE attribute to rational frame rate.
599
- */
600
- function parseFrameRate(value) {
601
- const fps = Number.parseFloat(value);
602
- if (Number.isNaN(fps) || fps <= 0) return void 0;
603
- if (Math.abs(fps - 23.976) < .01) return {
604
- frameRateNumerator: 24e3,
605
- frameRateDenominator: 1001
606
- };
607
- if (Math.abs(fps - 29.97) < .01) return {
608
- frameRateNumerator: 3e4,
609
- frameRateDenominator: 1001
1003
+ function createSegmentLoaderActor(sourceBufferActor, fetchBytes) {
1004
+ let pendingTasks = null;
1005
+ let inFlightInitTrackId = null;
1006
+ let inFlightSegmentId = null;
1007
+ let abortController = null;
1008
+ let running = false;
1009
+ let destroyed = false;
1010
+ const getBufferedSegments = (allSegments) => {
1011
+ const bufferedIds = new Set(sourceBufferActor.snapshot.get().context.segments.filter((s) => !s.partial).map((s) => s.id));
1012
+ return allSegments.filter((s) => bufferedIds.has(s.id));
610
1013
  };
611
- if (Math.abs(fps - 59.94) < .01) return {
612
- frameRateNumerator: 6e4,
613
- frameRateDenominator: 1001
1014
+ /**
1015
+ * Translate a load message into an ordered LoadTask list based on committed
1016
+ * actor state. In-flight awareness is handled separately in send().
1017
+ *
1018
+ * @todo Rename alongside LoadTask (e.g. planOps).
1019
+ *
1020
+ * Case 1 — Removes: forward and back buffer flush points, segment-aligned.
1021
+ * No flush on track switch: appending new content overwrites existing buffer
1022
+ * ranges, and the actor's time-aligned deduplication keeps the segment model
1023
+ * accurate as new segments arrive.
1024
+ *
1025
+ * Case 2 — Init: schedule if not yet committed for this track.
1026
+ *
1027
+ * Case 3 — Segments: all segments in the load window not yet committed.
1028
+ */
1029
+ const planTasks = (message) => {
1030
+ const { track, range } = message;
1031
+ const actorCtx = sourceBufferActor.snapshot.get().context;
1032
+ const bufferedSegments = getBufferedSegments(track.segments);
1033
+ const currentTime = range?.start ?? 0;
1034
+ const tasks = [];
1035
+ if (range) {
1036
+ const forwardFlushStart = calculateForwardFlushPoint(bufferedSegments, currentTime);
1037
+ if (forwardFlushStart < Infinity) tasks.push({
1038
+ type: "remove",
1039
+ start: forwardFlushStart,
1040
+ end: Infinity
1041
+ });
1042
+ const backFlushEnd = calculateBackBufferFlushPoint(bufferedSegments, currentTime);
1043
+ if (backFlushEnd > 0) tasks.push({
1044
+ type: "remove",
1045
+ start: 0,
1046
+ end: backFlushEnd
1047
+ });
1048
+ }
1049
+ if (actorCtx.initTrackId !== track.id) tasks.push({
1050
+ type: "append-init",
1051
+ meta: { trackId: track.id },
1052
+ url: track.initialization.url,
1053
+ ...track.initialization.byteRange !== void 0 && { byteRange: track.initialization.byteRange }
1054
+ });
1055
+ if (range) {
1056
+ const EPSILON = 1e-4;
1057
+ const segmentsToLoad = getSegmentsToLoad(track.segments, bufferedSegments, currentTime).filter((seg) => {
1058
+ const existing = actorCtx.segments.find((s) => Math.abs(s.startTime - seg.startTime) < EPSILON);
1059
+ if (existing?.partial) return true;
1060
+ if (!existing?.trackBandwidth || !track.bandwidth) return true;
1061
+ return track.bandwidth > existing.trackBandwidth;
1062
+ });
1063
+ for (const segment of segmentsToLoad) tasks.push({
1064
+ type: "append-segment",
1065
+ meta: {
1066
+ id: segment.id,
1067
+ startTime: segment.startTime,
1068
+ duration: segment.duration,
1069
+ trackId: track.id,
1070
+ trackBandwidth: track.bandwidth
1071
+ },
1072
+ url: segment.url,
1073
+ ...segment.byteRange !== void 0 && { byteRange: segment.byteRange }
1074
+ });
1075
+ }
1076
+ return tasks;
614
1077
  };
615
- if (fps % 1 === 0) return { frameRateNumerator: Math.round(fps) };
616
- return { frameRateNumerator: Math.round(fps) };
617
- }
618
- /**
619
- * Parse CODECS attribute into separate video and audio codecs.
620
- */
621
- function parseCodecs(codecs) {
622
- const parts = codecs.split(",").map((s) => s.trim());
623
- const result = {};
624
- for (const codec of parts) if (codec.startsWith("avc1.") || codec.startsWith("hvc1.") || codec.startsWith("hev1.")) result.video = codec;
625
- else if (codec.startsWith("mp4a.")) result.audio = codec;
626
- return result;
627
- }
628
- /**
629
- * Parse #EXTINF duration value.
630
- */
631
- function parseExtInfDuration(value) {
632
- const durationPart = value.split(",")[0] ?? value;
633
- const duration = Number.parseFloat(durationPart);
634
- return Number.isNaN(duration) ? 0 : duration;
635
- }
636
- /**
637
- * Parse BYTERANGE attribute value.
638
- * Format: "length[@offset]"
639
- * If offset is omitted, it continues from the previous byte range end.
640
- */
641
- function parseByteRange(value, previousEnd) {
642
- const match = /^(\d+)(?:@(\d+))?$/.exec(value);
643
- if (!match) return null;
644
- const length = Number.parseInt(match[1], 10);
645
- if (Number.isNaN(length)) return null;
646
- let start;
647
- if (match[2] !== void 0) {
648
- start = Number.parseInt(match[2], 10);
649
- if (Number.isNaN(start)) return null;
650
- } else if (previousEnd !== void 0) start = previousEnd;
651
- else return null;
652
- return {
653
- start,
654
- end: start + length - 1
1078
+ /**
1079
+ * Execute a single LoadTask: fetch (if needed) then forward to SourceBufferActor.
1080
+ * Sets/clears in-flight tracking around async operations so send() can make
1081
+ * accurate continue/preempt decisions at any point during execution.
1082
+ *
1083
+ * @todo Rename alongside LoadTask (e.g. executeOp).
1084
+ */
1085
+ const executeLoadTask = async (task) => {
1086
+ const signal = abortController.signal;
1087
+ try {
1088
+ if (task.type === "remove") {
1089
+ await sourceBufferActor.send(task, signal);
1090
+ return;
1091
+ }
1092
+ if (task.type === "append-init") {
1093
+ inFlightInitTrackId = task.meta.trackId;
1094
+ if (!signal.aborted) {
1095
+ const data = await fetchBytes(task, {
1096
+ signal,
1097
+ minChunkSize: Infinity
1098
+ });
1099
+ const isTrackSwitch = pendingTasks?.some((t) => t.type === "append-init" && t.meta.trackId !== task.meta.trackId);
1100
+ if (!signal.aborted || !isTrackSwitch) {
1101
+ const appendSignal = signal.aborted ? new AbortController().signal : signal;
1102
+ await sourceBufferActor.send({
1103
+ type: "append-init",
1104
+ data,
1105
+ meta: task.meta
1106
+ }, appendSignal);
1107
+ }
1108
+ }
1109
+ return;
1110
+ }
1111
+ inFlightSegmentId = task.meta.id;
1112
+ if (!signal.aborted) {
1113
+ const stream = await fetchBytes(task, { signal });
1114
+ if (!signal.aborted) await sourceBufferActor.send({
1115
+ type: "append-segment",
1116
+ data: stream,
1117
+ meta: task.meta
1118
+ }, signal);
1119
+ }
1120
+ } finally {
1121
+ inFlightInitTrackId = null;
1122
+ inFlightSegmentId = null;
1123
+ }
655
1124
  };
656
- }
657
- /**
658
- * Create AttributeList from raw attribute string.
659
- */
660
- function createAttributeList(line) {
661
- const map = parseAttributeList(line);
662
- return {
663
- get(key) {
664
- return map.get(key);
665
- },
666
- getInt(key, defaultValue) {
667
- const value = map.get(key);
668
- if (value === void 0) return defaultValue;
669
- const parsed = Number.parseInt(value, 10);
670
- return Number.isNaN(parsed) ? defaultValue : parsed;
671
- },
672
- getFloat(key, defaultValue) {
673
- const value = map.get(key);
674
- if (value === void 0) return defaultValue;
675
- const parsed = Number.parseFloat(value);
676
- return Number.isNaN(parsed) ? defaultValue : parsed;
677
- },
678
- getBool(key) {
679
- return map.get(key) === "YES";
680
- },
681
- getResolution(key) {
682
- const value = map.get(key);
683
- if (!value) return void 0;
684
- return parseResolution(value) ?? void 0;
685
- },
686
- getFrameRate(key) {
687
- const value = map.get(key);
688
- if (!value) return void 0;
689
- return parseFrameRate(value);
1125
+ /**
1126
+ * Drain the scheduled task list sequentially.
1127
+ * After each task completes, checks for a pending replacement plan from send().
1128
+ * If the signal was aborted and no new plan arrived, stops immediately.
1129
+ */
1130
+ const runScheduled = async (initialTasks) => {
1131
+ running = true;
1132
+ abortController = new AbortController();
1133
+ let scheduled = initialTasks;
1134
+ while (scheduled.length > 0 && !destroyed) {
1135
+ const task = scheduled[0];
1136
+ scheduled = scheduled.slice(1);
1137
+ try {
1138
+ await executeLoadTask(task);
1139
+ } catch (error) {
1140
+ if (error instanceof Error && error.name === "AbortError") {} else {
1141
+ console.error("Unexpected error in segment loader:", error);
1142
+ scheduled = [];
1143
+ }
1144
+ }
1145
+ if (pendingTasks !== null) {
1146
+ scheduled = pendingTasks;
1147
+ pendingTasks = null;
1148
+ abortController = new AbortController();
1149
+ } else if (abortController.signal.aborted) break;
690
1150
  }
1151
+ abortController = null;
1152
+ running = false;
691
1153
  };
692
- }
693
- /**
694
- * Match a tag and extract its attributes.
695
- * Returns null if the line doesn't match the tag.
696
- */
697
- function matchTag(line, tag) {
698
- const prefix = `#${tag}:`;
699
- if (!line.startsWith(prefix)) return null;
700
- return createAttributeList(line.slice(prefix.length));
701
- }
702
-
703
- //#endregion
704
- //#region ../spf/dist/dev/core/hls/parse-media-playlist.js
705
- /**
706
- * Parse HLS media playlist and resolve track with segments.
707
- *
708
- * Takes an unresolved track (from multivariant playlist) and media playlist text,
709
- * returns a HAM-compliant resolved track with segments.
710
- *
711
- * @param text - Media playlist text content
712
- * @param unresolved - Unresolved track from parseMultivariantPlaylist
713
- * @returns Resolved track with segments (type inferred from input)
714
- */
715
- function parseMediaPlaylist(text, unresolved) {
716
- const lines = text.split(/\r?\n/);
717
- const baseUrl = unresolved.url;
718
- const segments = [];
719
- let initSegmentUrl;
720
- let initSegmentByteRange;
721
- let currentDuration = 0;
722
- let currentByteRange;
723
- let currentTime = 0;
724
- let segmentIndex = 0;
725
- let previousByteRangeEnd;
726
- for (const line of lines) {
727
- const trimmed = line.trim();
728
- if (!trimmed || trimmed.startsWith("#") && !trimmed.startsWith("#EXT")) continue;
729
- if (trimmed === "#EXTM3U" || trimmed.startsWith("#EXT-X-VERSION:") || trimmed.startsWith("#EXT-X-TARGETDURATION:") || trimmed.startsWith("#EXT-X-PLAYLIST-TYPE:") || trimmed.startsWith("#EXT-X-INDEPENDENT-SEGMENTS")) continue;
730
- const mapAttrs = matchTag(trimmed, "EXT-X-MAP");
731
- if (mapAttrs) {
732
- const uri = mapAttrs.get("URI");
733
- if (uri) {
734
- initSegmentUrl = resolveUrl(uri, baseUrl);
735
- const byteRangeStr = mapAttrs.get("BYTERANGE");
736
- if (byteRangeStr) initSegmentByteRange = parseByteRange(byteRangeStr, 0) ?? void 0;
737
- }
738
- continue;
739
- }
740
- if (trimmed.startsWith("#EXTINF:")) {
741
- currentDuration = parseExtInfDuration(trimmed.slice(8));
742
- continue;
743
- }
744
- if (trimmed.startsWith("#EXT-X-BYTERANGE:")) {
745
- currentByteRange = parseByteRange(trimmed.slice(17), previousByteRangeEnd) ?? void 0;
746
- continue;
747
- }
748
- if (trimmed === "#EXT-X-ENDLIST") continue;
749
- if (!trimmed.startsWith("#") && currentDuration > 0) {
750
- const segment = {
751
- id: `segment-${segmentIndex}`,
752
- url: resolveUrl(trimmed, baseUrl),
753
- duration: currentDuration,
754
- startTime: currentTime
755
- };
756
- if (currentByteRange) {
757
- segment.byteRange = currentByteRange;
758
- previousByteRangeEnd = currentByteRange.end + 1;
759
- } else previousByteRangeEnd = void 0;
760
- segments.push(segment);
761
- currentTime += currentDuration;
762
- segmentIndex++;
763
- currentDuration = 0;
764
- currentByteRange = void 0;
765
- }
766
- }
767
- const totalDuration = currentTime;
768
- const initialization = unresolved.type === "text" && !initSegmentUrl ? void 0 : initSegmentUrl ? {
769
- url: initSegmentUrl,
770
- ...initSegmentByteRange ? { byteRange: initSegmentByteRange } : {}
771
- } : { url: "" };
772
1154
  return {
773
- ...unresolved,
774
- startTime: 0,
775
- duration: totalDuration,
776
- segments,
777
- initialization
778
- };
779
- }
780
-
781
- //#endregion
782
- //#region ../spf/dist/dev/core/utils/generate-id.js
783
- /**
784
- * Generate unique ID for HAM objects.
785
- *
786
- * Uses timestamp + random number for sufficient uniqueness.
787
- * IDs are strings without decimals.
788
- *
789
- * @returns Unique string ID in format: timestamp-random
790
- *
791
- * @example
792
- * ```ts
793
- * const id = generateId(); // "1738423156789-542891"
794
- * ```
795
- */
796
- function generateId() {
797
- return `${Date.now()}-${Math.floor(Math.random() * 1e6)}`;
798
- }
799
-
800
- //#endregion
801
- //#region ../spf/dist/dev/core/hls/parse-multivariant.js
802
- /**
803
- * Parse HLS multivariant playlist into a Presentation.
804
- *
805
- * Returns Presentation with partially resolved tracks (no segment information).
806
- * Tracks contain metadata from multivariant playlist (bandwidth, resolution, codecs)
807
- * but segment information is added when media playlists are fetched.
808
- *
809
- * @param text - Raw playlist text content
810
- * @param unresolved - Unresolved presentation (contains URL for base URL resolution)
811
- * @returns Presentation with partially resolved tracks (duration is undefined)
812
- */
813
- function parseMultivariantPlaylist(text, unresolved) {
814
- const baseUrl = unresolved.url;
815
- const lines = text.split(/\r?\n/);
816
- const streams = [];
817
- const audioRenditions = [];
818
- const subtitleRenditions = [];
819
- let pendingStreamInfo = null;
820
- for (const line of lines) {
821
- const trimmed = line.trim();
822
- if (!trimmed || trimmed.startsWith("#") && !trimmed.startsWith("#EXT")) continue;
823
- if (trimmed === "#EXTM3U" || trimmed.startsWith("#EXT-X-VERSION:") || trimmed.startsWith("#EXT-X-INDEPENDENT-SEGMENTS")) continue;
824
- const mediaAttrs = matchTag(trimmed, "EXT-X-MEDIA");
825
- if (mediaAttrs) {
826
- const type = mediaAttrs.get("TYPE");
827
- const groupId = mediaAttrs.get("GROUP-ID");
828
- const name = mediaAttrs.get("NAME");
829
- if (type === "AUDIO" && groupId && name) {
830
- const uri = mediaAttrs.get("URI");
831
- audioRenditions.push({
832
- groupId,
833
- name,
834
- language: mediaAttrs.get("LANGUAGE"),
835
- uri: uri ? resolveUrl(uri, baseUrl) : void 0,
836
- default: mediaAttrs.getBool("DEFAULT"),
837
- autoselect: mediaAttrs.getBool("AUTOSELECT")
838
- });
839
- }
840
- if (type === "SUBTITLES" && groupId && name) {
841
- const uri = mediaAttrs.get("URI");
842
- if (uri) subtitleRenditions.push({
843
- groupId,
844
- name,
845
- language: mediaAttrs.get("LANGUAGE"),
846
- uri: resolveUrl(uri, baseUrl),
847
- default: mediaAttrs.getBool("DEFAULT"),
848
- autoselect: mediaAttrs.getBool("AUTOSELECT"),
849
- forced: mediaAttrs.getBool("FORCED")
850
- });
1155
+ send(message) {
1156
+ if (destroyed) return;
1157
+ const allTasks = planTasks(message);
1158
+ if (!running) {
1159
+ if (allTasks.length === 0) return;
1160
+ runScheduled(allTasks);
1161
+ return;
851
1162
  }
852
- continue;
853
- }
854
- const streamInfAttrs = matchTag(trimmed, "EXT-X-STREAM-INF");
855
- if (streamInfAttrs) {
856
- pendingStreamInfo = {
857
- bandwidth: streamInfAttrs.getInt("BANDWIDTH", 0),
858
- resolution: streamInfAttrs.getResolution("RESOLUTION"),
859
- codecs: streamInfAttrs.get("CODECS"),
860
- frameRate: streamInfAttrs.getFrameRate("FRAME-RATE"),
861
- audioGroupId: streamInfAttrs.get("AUDIO")
862
- };
863
- continue;
864
- }
865
- if (!trimmed.startsWith("#") && pendingStreamInfo) {
866
- streams.push({
867
- ...pendingStreamInfo,
868
- uri: resolveUrl(trimmed, baseUrl)
869
- });
870
- pendingStreamInfo = null;
871
- }
872
- }
873
- const videoStreams = [];
874
- const audioOnlyStreams = [];
875
- for (const stream of streams) {
876
- if (!stream.codecs) {
877
- videoStreams.push(stream);
878
- continue;
879
- }
880
- const parsedCodecs = parseCodecs(stream.codecs);
881
- if (stream.codecs.split(",").length === 1) if (parsedCodecs.audio && !parsedCodecs.video) audioOnlyStreams.push(stream);
882
- else videoStreams.push(stream);
883
- else videoStreams.push(stream);
884
- }
885
- const videoTracks = videoStreams.map((stream) => {
886
- const codecs = stream.codecs ? parseCodecs(stream.codecs) : void 0;
887
- const track = {
888
- type: "video",
889
- id: generateId(),
890
- url: stream.uri,
891
- bandwidth: stream.bandwidth,
892
- mimeType: "video/mp4",
893
- codecs: []
894
- };
895
- if (stream.resolution?.width !== void 0) track.width = stream.resolution.width;
896
- if (stream.resolution?.height !== void 0) track.height = stream.resolution.height;
897
- if (codecs?.video) track.codecs = [codecs.video];
898
- if (stream.frameRate) track.frameRate = stream.frameRate;
899
- if (stream.audioGroupId) track.audioGroupId = stream.audioGroupId;
900
- return track;
901
- });
902
- const audioOnlyTracks = audioOnlyStreams.map((stream) => {
903
- const codecs = stream.codecs ? parseCodecs(stream.codecs) : void 0;
904
- return {
905
- type: "audio",
906
- id: generateId(),
907
- url: stream.uri,
908
- bandwidth: stream.bandwidth,
909
- mimeType: "audio/mp4",
910
- codecs: codecs?.audio ? [codecs.audio] : [],
911
- groupId: stream.audioGroupId || "default",
912
- name: "Default",
913
- sampleRate: 48e3,
914
- channels: 2
915
- };
916
- });
917
- const audioTracks = [...audioRenditions.map((rendition) => {
918
- let audioCodecs;
919
- for (const stream of streams) if (stream.audioGroupId === rendition.groupId && stream.codecs) {
920
- const codecs = parseCodecs(stream.codecs);
921
- if (codecs.audio) {
922
- audioCodecs = [codecs.audio];
923
- break;
1163
+ if (inFlightSegmentId !== null && allTasks.some((t) => t.type === "append-segment" && t.meta.id === inFlightSegmentId) || inFlightInitTrackId !== null && allTasks.some((t) => t.type === "append-init" && t.meta.trackId === inFlightInitTrackId)) pendingTasks = allTasks.filter((t) => !(t.type === "append-segment" && t.meta.id === inFlightSegmentId) && !(t.type === "append-init" && t.meta.trackId === inFlightInitTrackId));
1164
+ else {
1165
+ pendingTasks = allTasks;
1166
+ abortController?.abort();
924
1167
  }
925
- }
926
- const track = {
927
- type: "audio",
928
- id: generateId(),
929
- url: rendition.uri ?? "",
930
- groupId: rendition.groupId,
931
- name: rendition.name,
932
- mimeType: "audio/mp4",
933
- bandwidth: 0,
934
- sampleRate: 48e3,
935
- channels: 2,
936
- codecs: []
937
- };
938
- if (rendition.language) track.language = rendition.language;
939
- if (audioCodecs) track.codecs = audioCodecs;
940
- if (rendition.default) track.default = rendition.default;
941
- if (rendition.autoselect) track.autoselect = rendition.autoselect;
942
- return track;
943
- }), ...audioOnlyTracks];
944
- const textTracks = subtitleRenditions.map((rendition) => {
945
- const track = {
946
- type: "text",
947
- id: generateId(),
948
- url: rendition.uri,
949
- groupId: rendition.groupId,
950
- label: rendition.name,
951
- kind: "subtitles",
952
- mimeType: "text/vtt",
953
- bandwidth: 0
954
- };
955
- if (rendition.language) track.language = rendition.language;
956
- if (rendition.default && rendition.autoselect) track.default = true;
957
- if (rendition.autoselect) track.autoselect = rendition.autoselect;
958
- if (rendition.forced) track.forced = rendition.forced;
959
- return track;
960
- });
961
- const selectionSets = [];
962
- if (videoTracks.length > 0) {
963
- const videoSwitchingSet = {
964
- id: generateId(),
965
- type: "video",
966
- tracks: videoTracks
967
- };
968
- const videoSelectionSet = {
969
- id: generateId(),
970
- type: "video",
971
- switchingSets: [videoSwitchingSet]
972
- };
973
- selectionSets.push(videoSelectionSet);
974
- }
975
- if (audioTracks.length > 0) {
976
- const audioSwitchingSet = {
977
- id: generateId(),
978
- type: "audio",
979
- tracks: audioTracks
980
- };
981
- const audioSelectionSet = {
982
- id: generateId(),
983
- type: "audio",
984
- switchingSets: [audioSwitchingSet]
985
- };
986
- selectionSets.push(audioSelectionSet);
987
- }
988
- if (textTracks.length > 0) {
989
- const textSwitchingSet = {
990
- id: generateId(),
991
- type: "text",
992
- tracks: textTracks
993
- };
994
- const textSelectionSet = {
995
- id: generateId(),
996
- type: "text",
997
- switchingSets: [textSwitchingSet]
998
- };
999
- selectionSets.push(textSelectionSet);
1000
- }
1001
- return {
1002
- id: generateId(),
1003
- url: unresolved.url,
1004
- startTime: 0,
1005
- selectionSets
1006
- };
1007
- }
1008
-
1009
- //#endregion
1010
- //#region ../spf/dist/dev/core/abr/quality-selection.js
1011
- /**
1012
- * Default quality selection configuration.
1013
- * Values match Shaka Player upgrade threshold (0.85 = 15% headroom).
1014
- */
1015
- const DEFAULT_QUALITY_CONFIG = { safetyMargin: .85 };
1016
- /**
1017
- * Select the best video track based on current bandwidth estimate.
1018
- *
1019
- * Selects the highest quality track where bandwidth is sufficient with safety margin:
1020
- * - currentBandwidth >= track.bandwidth / safetyMargin
1021
- * - Default safetyMargin 0.85 means track uses ≤85% of bandwidth (15% headroom)
1022
- * - At same bandwidth, prefers higher resolution
1023
- *
1024
- * @param tracks - Available video tracks (can be unsorted)
1025
- * @param currentBandwidth - Current bandwidth estimate in bits per second
1026
- * @param config - Optional quality selection configuration
1027
- * @returns Selected track, or undefined if no tracks available
1028
- *
1029
- * @example
1030
- * const tracks = [
1031
- * { id: '360p', bandwidth: 500_000, ... },
1032
- * { id: '720p', bandwidth: 2_000_000, ... },
1033
- * { id: '1080p', bandwidth: 4_000_000, ... },
1034
- * ];
1035
- *
1036
- * // With 2.5 Mbps, selects 720p (1080p needs 4M/0.85 = 4.7 Mbps)
1037
- * const selected = selectQuality(tracks, 2_500_000);
1038
- */
1039
- function selectQuality(tracks, currentBandwidth, config = DEFAULT_QUALITY_CONFIG) {
1040
- if (tracks.length === 0) return;
1041
- const sortedTracks = tracks.slice().sort((a, b) => a.bandwidth - b.bandwidth);
1042
- let chosen;
1043
- for (const track of sortedTracks) if (currentBandwidth >= track.bandwidth / config.safetyMargin) {
1044
- if (!chosen || track.bandwidth > chosen.bandwidth || track.bandwidth === chosen.bandwidth && hasHigherResolution(track, chosen)) chosen = track;
1045
- }
1046
- return chosen ?? sortedTracks[0];
1047
- }
1048
- /**
1049
- * Check if track A has higher resolution than track B.
1050
- * Compares by total pixel count (width × height).
1051
- *
1052
- * @param trackA - First track to compare
1053
- * @param trackB - Second track to compare
1054
- * @returns True if trackA has more pixels than trackB
1055
- */
1056
- function hasHigherResolution(trackA, trackB) {
1057
- return (trackA.width ?? 0) * (trackA.height ?? 0) > (trackB.width ?? 0) * (trackB.height ?? 0);
1058
- }
1059
-
1060
- //#endregion
1061
- //#region ../spf/dist/dev/core/buffer/back-buffer.js
1062
- /**
1063
- * Default back buffer configuration.
1064
- */
1065
- const DEFAULT_BACK_BUFFER_CONFIG = { keepSegments: 2 };
1066
- /**
1067
- * Calculate back buffer flush point.
1068
- *
1069
- * Determines where to flush old segments from the back buffer.
1070
- * Keeps a fixed number of segments behind the current playback position.
1071
- *
1072
- * Algorithm:
1073
- * 1. Find segments before currentTime
1074
- * 2. Count back N segments (keepSegments)
1075
- * 3. Return startTime of segment N+1 back (flush everything before this)
1076
- *
1077
- * @param segments - Available segments (should be sorted by startTime)
1078
- * @param currentTime - Current playback position in seconds
1079
- * @param config - Optional back buffer configuration
1080
- * @returns Time in seconds to flush up to (flush range: [0, flushEnd))
1081
- *
1082
- * @example
1083
- * const segments = [
1084
- * { startTime: 0, duration: 6, ... },
1085
- * { startTime: 6, duration: 6, ... },
1086
- * { startTime: 12, duration: 6, ... },
1087
- * { startTime: 18, duration: 6, ... },
1088
- * ];
1089
- *
1090
- * // Playing at 18s, keep 2 segments
1091
- * const flushEnd = calculateBackBufferFlushPoint(segments, 18);
1092
- * // Returns 6 (flush [0, 6), keep [6-18))
1093
- */
1094
- function calculateBackBufferFlushPoint(segments, currentTime, config = DEFAULT_BACK_BUFFER_CONFIG) {
1095
- if (segments.length === 0) return 0;
1096
- const segmentsBefore = segments.filter((seg) => seg.startTime < currentTime);
1097
- if (segmentsBefore.length === 0) return 0;
1098
- const segmentsToFlush = segmentsBefore.length - config.keepSegments;
1099
- if (segmentsToFlush <= 0) return 0;
1100
- if (segmentsToFlush >= segmentsBefore.length) return currentTime;
1101
- return segmentsBefore[segmentsToFlush].startTime;
1168
+ },
1169
+ destroy() {
1170
+ destroyed = true;
1171
+ abortController?.abort();
1172
+ }
1173
+ };
1102
1174
  }
1103
1175
 
1104
1176
  //#endregion
1105
- //#region ../spf/dist/dev/dom/media/mediasource-setup.js
1106
- /**
1107
- * MediaSource Setup
1108
- *
1109
- * Utilities for creating and configuring MediaSource/ManagedMediaSource
1110
- * for MSE (Media Source Extensions) playback.
1111
- *
1112
- * Global ManagedMediaSource types are defined in ./mediasource.d.ts
1113
- */
1114
- /**
1115
- * Check if MediaSource API is supported.
1116
- */
1117
- function supportsMediaSource() {
1118
- return typeof MediaSource !== "undefined";
1177
+ //#region ../spf/dist/dev/dom/features/load-segments.js
1178
+ const ActorKeyByType$1 = {
1179
+ video: "videoBufferActor",
1180
+ audio: "audioBufferActor"
1181
+ };
1182
+ function createTrackedFetch(throughput, onSample) {
1183
+ return async (addressable, options) => {
1184
+ const { minChunkSize, ...fetchOptions } = options ?? {};
1185
+ const response = await fetchResolvable(addressable, fetchOptions);
1186
+ if (!response.body) throw new Error("Response has no body");
1187
+ const body = response.body;
1188
+ return { [Symbol.asyncIterator]: async function* () {
1189
+ let chunkStart = performance.now();
1190
+ for await (const chunk of new ChunkedStreamIterable(body, ...minChunkSize !== void 0 ? [{ minChunkSize }] : [])) {
1191
+ const elapsed = performance.now() - chunkStart;
1192
+ const next = sampleBandwidth(throughput.get(), elapsed, chunk.byteLength);
1193
+ throughput.set(next);
1194
+ onSample?.(next);
1195
+ yield chunk;
1196
+ chunkStart = performance.now();
1197
+ }
1198
+ } };
1199
+ };
1119
1200
  }
1120
1201
  /**
1121
- * Check if ManagedMediaSource API is supported.
1122
- * ManagedMediaSource is a newer Safari API with better lifecycle management.
1202
+ * Non-tracking fetch: eagerly starts the request and returns the response body
1203
+ * as a lazy chunk iterable. Used for audio tracks which don't sample bandwidth.
1204
+ * Pass `minChunkSize: Infinity` to accumulate the full body as a single chunk
1205
+ * (equivalent to arrayBuffer() but through the same streaming path).
1123
1206
  */
1124
- function supportsManagedMediaSource() {
1125
- return typeof ManagedMediaSource !== "undefined";
1207
+ async function fetchStream(addressable, options) {
1208
+ const { minChunkSize, ...fetchOptions } = options ?? {};
1209
+ const response = await fetchResolvable(addressable, fetchOptions);
1210
+ if (!response.body) throw new Error("Response has no body");
1211
+ return new ChunkedStreamIterable(response.body, ...minChunkSize !== void 0 ? [{ minChunkSize }] : []);
1212
+ }
1213
+ function selectLoadingInputs([segmentsCanLoad, state], type) {
1214
+ const { playbackInitiated, preload, currentTime } = state;
1215
+ return {
1216
+ playbackInitiated,
1217
+ preload,
1218
+ currentTime,
1219
+ track: getSelectedTrack(state, type),
1220
+ segmentsCanLoad
1221
+ };
1126
1222
  }
1127
1223
  /**
1128
- * Create a MediaSource or ManagedMediaSource instance.
1224
+ * Equality function encoding the condition hierarchy for relevant changes.
1129
1225
  *
1130
- * @param options - Creation options
1131
- * @returns A MediaSource or ManagedMediaSource instance
1132
- * @throws Error if no MediaSource API is available
1226
+ * Pre-play (!playbackInitiated):
1227
+ * Only preload changes matter. currentTime and resolvedTrackId are ignored
1228
+ * (track changes not supported pre-play; currentTime value is used at
1229
+ * trigger time but changes don't re-trigger).
1133
1230
  *
1134
- * @example
1135
- * const mediaSource = createMediaSource();
1136
- * const mediaElement = document.querySelector('video');
1137
- * attachMediaSource(mediaSource, mediaElement);
1231
+ * playbackInitiated transition:
1232
+ * Always fires (handled in the subscriber; preload='auto' suppression
1233
+ * applied there since equality functions have no memory of prior values).
1234
+ *
1235
+ * Post-play (playbackInitiated):
1236
+ * resolvedTrackId changes (track switch or previously-unresolved track
1237
+ * resolving) and currentTime changes both trigger. preload is irrelevant.
1138
1238
  */
1139
- function createMediaSource(options = {}) {
1140
- const { preferManaged = false } = options;
1141
- if (preferManaged && supportsManagedMediaSource()) return new ManagedMediaSource();
1142
- if (supportsMediaSource()) return new MediaSource();
1143
- throw new Error("MediaSource API is not supported");
1144
- }
1239
+ const segmentStartFor = (currentTime, track) => {
1240
+ if (currentTime == null) return void 0;
1241
+ return track?.segments.find(({ startTime, duration }, i, segments) => currentTime >= startTime && (currentTime < startTime + duration || i === segments.length - 1))?.startTime;
1242
+ };
1145
1243
  /**
1146
- * Attach a MediaSource to an HTMLMediaElement.
1147
- *
1148
- * Uses srcObject for ManagedMediaSource (Safari), or createObjectURL for regular MediaSource.
1149
- *
1150
- * @param mediaSource - The MediaSource to attach
1151
- * @param mediaElement - The media element to attach to
1152
- * @returns Object with URL and detach function
1244
+ * Returns true when the inputs are equal (no meaningful change — don't fire).
1245
+ * Returns false when the inputs differ in a way that requires a new message.
1153
1246
  *
1154
- * @example
1155
- * const mediaSource = createMediaSource();
1156
- * const { detach } = attachMediaSource(mediaSource, videoElement);
1157
- * await waitForSourceOpen(mediaSource);
1158
- * // Use mediaSource...
1159
- * // Later, to clean up:
1160
- * detach();
1247
+ * This IS the shouldLoadSegments logic, expressed as an equality function.
1161
1248
  */
1162
- function attachMediaSource(mediaSource, mediaElement) {
1163
- if (supportsManagedMediaSource() && mediaSource instanceof ManagedMediaSource) {
1164
- mediaElement.disableRemotePlayback = true;
1165
- mediaElement.srcObject = mediaSource;
1166
- const detach = () => {
1167
- mediaElement.srcObject = null;
1168
- mediaElement.load();
1169
- };
1170
- return {
1171
- url: "",
1172
- detach
1173
- };
1249
+ function loadingInputsEq(prevState, curState) {
1250
+ if (!curState.segmentsCanLoad) return true;
1251
+ if (!curState.playbackInitiated) {
1252
+ if (curState.preload === "none") return true;
1253
+ return curState.preload === prevState.preload;
1174
1254
  }
1175
- const url = URL.createObjectURL(mediaSource);
1176
- mediaElement.src = url;
1177
- const detach = () => {
1178
- mediaElement.removeAttribute("src");
1179
- mediaElement.load();
1180
- URL.revokeObjectURL(url);
1181
- };
1182
- return {
1183
- url,
1184
- detach
1185
- };
1255
+ if (!prevState.playbackInitiated && curState.playbackInitiated) {
1256
+ if (prevState.preload !== "auto") return false;
1257
+ }
1258
+ if (!curState.track || !isResolvedTrack(curState.track)) return true;
1259
+ if (prevState.track?.id !== curState.track.id && isResolvedTrack(curState.track)) return false;
1260
+ return segmentStartFor(prevState.currentTime, curState.track) === segmentStartFor(curState.currentTime, curState.track);
1186
1261
  }
1187
1262
  /**
1188
- * Wait for a MediaSource to reach the 'open' state.
1189
- * Resolves immediately if already open.
1263
+ * Load segments orchestration Reactor layer.
1264
+ *
1265
+ * Sends typed load messages to a SegmentLoaderActor when relevant conditions
1266
+ * change. Uses targeted subscriptions rather than broad combineLatest so only
1267
+ * meaningful state changes trigger evaluation.
1268
+ *
1269
+ * Condition hierarchy (see SegmentLoadingKey for detail):
1270
+ *
1271
+ * !playbackInitiated
1272
+ * preload==='none' (or unset) → dormant; no trigger
1273
+ * preload==='metadata' → trigger on transition to 'metadata'
1274
+ * preload==='auto' → trigger on transition to 'auto'
1275
+ *
1276
+ * !playbackInitiated → playbackInitiated
1277
+ * preload !== 'auto' → trigger (message shape changes)
1278
+ * preload === 'auto' → suppressed (was already full-range mode;
1279
+ * let segmentStart take over post-play)
1280
+ * KNOWN LIMITATION: seek-before-play with
1281
+ * preload='auto' is not supported — if the
1282
+ * user seeks before pressing play, the
1283
+ * first re-send is delayed until the next
1284
+ * segment boundary crossing post-play.
1190
1285
  *
1191
- * @param mediaSource - The MediaSource to wait for
1192
- * @param signal - Optional AbortSignal for cancellation
1193
- * @returns Promise that resolves when the MediaSource is open
1286
+ * playbackInitiated
1287
+ * resolvedTrackId changes → trigger
1288
+ * segmentStart(currentTime) changes trigger (segment boundary only)
1194
1289
  *
1195
1290
  * @example
1196
- * const mediaSource = createMediaSource();
1197
- * attachMediaSource(mediaSource, videoElement);
1198
- * await waitForSourceOpen(mediaSource);
1199
- * // MediaSource is now ready for SourceBuffer creation
1291
+ * const cleanup = loadSegments({ state, owners }, { type: 'video' });
1200
1292
  */
1201
- function waitForSourceOpen(mediaSource, signal) {
1202
- return new Promise((resolve, reject) => {
1203
- if (mediaSource.readyState === "open") {
1204
- resolve();
1205
- return;
1293
+ function loadSegments({ state, owners }, config) {
1294
+ const { type } = config;
1295
+ const actorKey = ActorKeyByType$1[type];
1296
+ const initialBandwidth = state.get().bandwidthState;
1297
+ const throughput = signal(initialBandwidth ?? {
1298
+ fastEstimate: 0,
1299
+ fastTotalWeight: 0,
1300
+ slowEstimate: 0,
1301
+ slowTotalWeight: 0,
1302
+ bytesSampled: 0
1303
+ });
1304
+ const fetchBytes = type === "video" ? createTrackedFetch(throughput, initialBandwidth !== void 0 ? (next) => {
1305
+ state.set(Object.assign({}, state.get(), { bandwidthState: next }));
1306
+ } : void 0) : fetchStream;
1307
+ const segmentLoader = signal(void 0);
1308
+ const actorSource = computed(() => owners.get()[actorKey]);
1309
+ let currentLoader;
1310
+ const cleanupActorLifecycle = effect(() => {
1311
+ const actor = actorSource.get();
1312
+ if (currentLoader) {
1313
+ currentLoader.destroy();
1314
+ segmentLoader.set(void 0);
1315
+ currentLoader = void 0;
1206
1316
  }
1207
- if (signal?.aborted) {
1208
- reject(new DOMException("Aborted", "AbortError"));
1209
- return;
1317
+ if (actor) {
1318
+ const loader = createSegmentLoaderActor(actor, fetchBytes);
1319
+ currentLoader = loader;
1320
+ segmentLoader.set(loader);
1210
1321
  }
1211
- const controller = new AbortController();
1212
- const options = { signal: controller.signal };
1213
- mediaSource.addEventListener("sourceopen", () => {
1214
- controller.abort();
1215
- resolve();
1216
- }, options);
1217
- signal?.addEventListener("abort", () => {
1218
- controller.abort();
1219
- reject(new DOMException("Aborted", "AbortError"));
1220
- }, options);
1221
1322
  });
1323
+ const segmentsCanLoad = computed(() => {
1324
+ const track = getSelectedTrack(state.get(), type);
1325
+ return !!track && isResolvedTrack(track) && !!segmentLoader.get();
1326
+ });
1327
+ const loadingInputs = computed(() => selectLoadingInputs([segmentsCanLoad.get(), state.get()], type));
1328
+ let prevInputs;
1329
+ const cleanupLoadEffect = effect(() => {
1330
+ const inputs = loadingInputs.get();
1331
+ if (prevInputs !== void 0 && loadingInputsEq(prevInputs, inputs)) return;
1332
+ const { preload, playbackInitiated, currentTime, track, segmentsCanLoad: canLoad } = inputs;
1333
+ if (!canLoad) return;
1334
+ prevInputs = inputs;
1335
+ if (!(preload === "auto" || !!playbackInitiated))
1336
+ /** @ts-expect-error */
1337
+ segmentLoader.get()?.send({
1338
+ type: "load",
1339
+ track
1340
+ });
1341
+ else segmentLoader.get()?.send({
1342
+ type: "load",
1343
+ track,
1344
+ range: {
1345
+ start: currentTime,
1346
+ end: currentTime + DEFAULT_FORWARD_BUFFER_CONFIG.bufferDuration
1347
+ }
1348
+ });
1349
+ });
1350
+ return () => {
1351
+ cleanupActorLifecycle();
1352
+ cleanupLoadEffect();
1353
+ currentLoader?.destroy();
1354
+ };
1222
1355
  }
1356
+
1357
+ //#endregion
1358
+ //#region ../spf/dist/dev/dom/text/parse-vtt-segment.js
1223
1359
  /**
1224
- * Create a SourceBuffer on a MediaSource.
1225
- *
1226
- * @param mediaSource - The MediaSource (must be in 'open' state)
1227
- * @param mimeCodec - MIME type with codecs (e.g., 'video/mp4; codecs="avc1.42E01E"')
1228
- * @returns The created SourceBuffer
1229
- * @throws Error if MediaSource is not open or codec is unsupported
1360
+ * Parse a VTT segment using browser's native parser.
1230
1361
  *
1231
- * @example
1232
- * await waitForSourceOpen(mediaSource);
1233
- * const buffer = createSourceBuffer(mediaSource, 'video/mp4; codecs="avc1.42E01E"');
1362
+ * Creates a dummy video element with a track element to leverage
1363
+ * the browser's optimized VTT parsing. Returns parsed VTTCue objects.
1234
1364
  */
1235
- function createSourceBuffer(mediaSource, mimeCodec) {
1236
- if (mediaSource.readyState !== "open") throw new Error("MediaSource is not open");
1237
- if (!isCodecSupported(mimeCodec)) throw new Error(`Codec not supported: ${mimeCodec}`);
1238
- return mediaSource.addSourceBuffer(mimeCodec);
1365
+ let dummyVideo = null;
1366
+ function ensureDummyVideo() {
1367
+ if (!dummyVideo) {
1368
+ dummyVideo = document.createElement("video");
1369
+ dummyVideo.muted = true;
1370
+ dummyVideo.preload = "none";
1371
+ dummyVideo.style.display = "none";
1372
+ dummyVideo.crossOrigin = "anonymous";
1373
+ }
1374
+ return dummyVideo;
1375
+ }
1376
+ function parseVttSegment(url) {
1377
+ const video = ensureDummyVideo();
1378
+ const track = document.createElement("track");
1379
+ track.kind = "subtitles";
1380
+ track.default = true;
1381
+ return new Promise((resolve, reject) => {
1382
+ const onLoad = () => {
1383
+ const cues = [];
1384
+ const textTrack = track.track;
1385
+ if (textTrack.cues) for (let i = 0; i < textTrack.cues.length; i++) {
1386
+ const cue = textTrack.cues[i];
1387
+ if (cue) cues.push(cue);
1388
+ }
1389
+ cleanup();
1390
+ resolve(cues);
1391
+ };
1392
+ const onError = () => {
1393
+ cleanup();
1394
+ reject(/* @__PURE__ */ new Error(`Failed to load VTT segment: ${url}`));
1395
+ };
1396
+ const cleanup = () => {
1397
+ track.removeEventListener("load", onLoad);
1398
+ track.removeEventListener("error", onError);
1399
+ video.removeChild(track);
1400
+ };
1401
+ track.addEventListener("load", onLoad);
1402
+ track.addEventListener("error", onError);
1403
+ video.appendChild(track);
1404
+ track.src = url;
1405
+ });
1239
1406
  }
1240
- /**
1241
- * Check if a codec is supported.
1242
- *
1243
- * @param mimeCodec - MIME type with codecs string
1244
- * @returns True if the codec is supported
1245
- *
1246
- * @example
1247
- * if (isCodecSupported('video/mp4; codecs="avc1.42E01E"')) {
1248
- * // Create source buffer
1249
- * }
1250
- */
1251
- function isCodecSupported(mimeCodec) {
1252
- if (!supportsMediaSource()) return false;
1253
- return MediaSource.isTypeSupported(mimeCodec);
1407
+ function destroyVttParser() {
1408
+ dummyVideo = null;
1254
1409
  }
1255
1410
 
1256
1411
  //#endregion
1257
- //#region ../spf/dist/dev/core/events/create-event-stream.js
1258
- /**
1259
- * Minimal event stream with Observable-like shape.
1260
- *
1261
- * Simple Subject/Observable-like implementation for dispatching discrete events.
1262
- * Events are dispatched synchronously to all subscribers.
1263
- */
1264
- const EVENT_STREAM_SYMBOL = Symbol("@videojs/event-stream");
1412
+ //#region ../spf/dist/dev/dom/features/load-text-track-cues.js
1413
+ const CueKeys = [
1414
+ "startTime",
1415
+ "endTime",
1416
+ "text"
1417
+ ];
1418
+ function isDuplicateCue(cue, existingCues) {
1419
+ return Array.prototype.some.call(existingCues ?? [], (existingCue) => {
1420
+ return CueKeys.every((k) => existingCue[k] === cue[k]);
1421
+ });
1422
+ }
1423
+ const loadVttSegmentTask = async ({ segment }, { textTrack }) => {
1424
+ (await parseVttSegment(segment.url)).forEach((cue) => {
1425
+ if (isDuplicateCue(cue, textTrack.cues)) return;
1426
+ textTrack.addCue(cue);
1427
+ });
1428
+ };
1265
1429
  /**
1266
- * Creates a minimal event stream for dispatching discrete events.
1267
- *
1268
- * Events are dispatched synchronously to all subscribers.
1269
- * Conforms to Observable-like shape for future compatibility.
1270
- *
1271
- * Events must have a 'type' property for discriminated union type narrowing.
1272
- *
1273
- * @example
1274
- * ```ts
1275
- * type Action = { type: 'PLAY' } | { type: 'PAUSE' };
1276
- * const events = createEventStream<Action>();
1277
- *
1278
- * events.subscribe((action) => {
1279
- * if (action.type === 'PLAY') {
1280
- * // Type narrowed to { type: 'PLAY' }
1281
- * }
1282
- * });
1283
- *
1284
- * events.dispatch({ type: 'PLAY' });
1285
- * ```
1430
+ * Load text track cues task (composite - orchestrates VTT segment subtasks).
1286
1431
  */
1287
- function createEventStream() {
1288
- const subscribers = /* @__PURE__ */ new Set();
1289
- return {
1290
- [EVENT_STREAM_SYMBOL]: true,
1291
- dispatch(event) {
1292
- const current = Array.from(subscribers);
1293
- for (const listener of current) listener(event);
1294
- },
1295
- subscribe(listener) {
1296
- subscribers.add(listener);
1297
- return () => subscribers.delete(listener);
1432
+ const loadTextTrackCuesTask = async ({ currentState }, context) => {
1433
+ const track = findSelectedTextTrack(currentState);
1434
+ if (!track || !isResolvedTrack(track)) return;
1435
+ const { segments } = track;
1436
+ if (segments.length === 0) return;
1437
+ const trackId = track.id;
1438
+ const loadedIds = new Set((currentState.textBufferState?.[trackId]?.segments ?? []).map((s) => s.id));
1439
+ const segmentsToLoad = getSegmentsToLoad(segments, segments.filter((s) => loadedIds.has(s.id)), currentState.currentTime ?? 0).filter((s) => !loadedIds.has(s.id));
1440
+ if (segmentsToLoad.length === 0) return;
1441
+ for (const segment of segmentsToLoad) {
1442
+ if (context.signal.aborted) break;
1443
+ try {
1444
+ await loadVttSegmentTask({ segment }, { textTrack: context.textTrack });
1445
+ const latestState = context.state.get();
1446
+ const latest = latestState.textBufferState ?? {};
1447
+ const trackState = latest[trackId] ?? { segments: [] };
1448
+ context.state.set({
1449
+ ...latestState,
1450
+ textBufferState: {
1451
+ ...latest,
1452
+ [trackId]: { segments: [...trackState.segments, { id: segment.id }] }
1453
+ }
1454
+ });
1455
+ } catch (error) {
1456
+ if (error instanceof Error && error.name === "AbortError") break;
1457
+ console.error("Failed to load VTT segment:", error);
1298
1458
  }
1299
- };
1300
- }
1301
-
1302
- //#endregion
1303
- //#region ../spf/dist/dev/core/features/resolve-presentation.js
1459
+ }
1460
+ if (context.textTrack.mode === "showing" && context.textTrack.cues) Array.from(context.textTrack.cues).forEach((cue) => {
1461
+ context.textTrack.addCue(cue);
1462
+ });
1463
+ await new Promise((resolve) => requestAnimationFrame(resolve));
1464
+ };
1304
1465
  /**
1305
- * Type guard to check if presentation is unresolved.
1466
+ * Find the selected text track in the presentation.
1306
1467
  */
1307
- function isUnresolved(presentation) {
1308
- return presentation !== void 0 && "url" in presentation && !("id" in presentation);
1309
- }
1310
- function canResolve$1(state) {
1311
- return isUnresolved(state.presentation);
1468
+ function findSelectedTextTrack(state) {
1469
+ if (!state.presentation || !state.selectedTextTrackId) return;
1470
+ const textSet = state.presentation.selectionSets.find((set) => set.type === "text");
1471
+ if (!textSet?.switchingSets?.[0]?.tracks) return;
1472
+ return textSet.switchingSets[0].tracks.find((t) => t.id === state.selectedTextTrackId);
1312
1473
  }
1313
1474
  /**
1314
- * Determines if resolution conditions are met based on preload policy and event.
1475
+ * Get the browser's TextTrack object for the selected text track.
1315
1476
  *
1316
- * Resolution conditions:
1317
- * - State-driven: preload is 'auto' or 'metadata'
1318
- * - Event-driven: play event
1477
+ * Retrieves the live TextTrack interface from the track element in owners,
1478
+ * which is used for adding cues, checking mode, and managing track state.
1319
1479
  *
1320
- * @param state - Current presentation state
1321
- * @param event - Current action/event
1322
- * @returns true if resolution conditions are met
1480
+ * Note: Returns the DOM TextTrack interface (HTMLTrackElement.track),
1481
+ * not the presentation Track metadata type.
1482
+ *
1483
+ * @param state - Current playback state (track selection)
1484
+ * @param owners - DOM owners containing track elements map
1485
+ * @returns DOM TextTrack interface or undefined if not found
1323
1486
  */
1324
- function shouldResolve$1(state, event) {
1325
- const { preload } = state;
1326
- return ["auto", "metadata"].includes(preload) || event.type === "play";
1487
+ function getSelectedTextTrackFromOwners(state, owners) {
1488
+ const trackId = state.selectedTextTrackId;
1489
+ if (!trackId || !owners.textTracks) return;
1490
+ return owners.textTracks.get(trackId)?.track;
1327
1491
  }
1328
1492
  /**
1329
- * Syncs preload attribute from mediaElement to state.
1493
+ * Check if we can load text track cues.
1330
1494
  *
1331
- * Watches the owners state for mediaElement changes and copies the
1332
- * preload attribute to the immutable state.
1495
+ * Requires:
1496
+ * - Selected text track ID exists
1497
+ * - Track elements map exists
1498
+ * - Track element exists for selected track
1499
+ */
1500
+ function canLoadTextTrackCues(state, owners) {
1501
+ return !!state.selectedTextTrackId && !!owners.textTracks && owners.textTracks.has(state.selectedTextTrackId);
1502
+ }
1503
+ /**
1504
+ * Check if we should load text track cues.
1333
1505
  *
1334
- * @param state - Immutable state container
1335
- * @param owners - Mutable platform objects container
1336
- * @returns Cleanup function to stop syncing
1506
+ * Only load if:
1507
+ * - Track is resolved (has segments)
1508
+ * - Track has at least one segment
1509
+ * - Track element exists
1337
1510
  */
1338
- function syncPreloadAttribute(state, owners) {
1339
- return owners.subscribe((current) => {
1340
- if (state.current.preload !== void 0) return;
1341
- const preload = current.mediaElement?.preload || void 0;
1342
- state.patch({ preload });
1343
- });
1511
+ function shouldLoadTextTrackCues(state, owners) {
1512
+ if (!canLoadTextTrackCues(state, owners)) return false;
1513
+ const track = findSelectedTextTrack(state);
1514
+ if (!track || !isResolvedTrack(track) || track.segments.length === 0) return false;
1515
+ if (!getSelectedTextTrackFromOwners(state, owners)) return false;
1516
+ return true;
1344
1517
  }
1345
1518
  /**
1346
- * Resolves unresolved presentations using reactive composition.
1519
+ * Load text track cues orchestration.
1347
1520
  *
1348
- * Uses combineLatest to compose state + events, enabling both state-driven
1349
- * and event-driven resolution triggers.
1521
+ * Triggers when:
1522
+ * - Text track is selected
1523
+ * - Track is resolved (has segments)
1524
+ * - Track element exists
1350
1525
  *
1351
- * Triggers resolution when:
1352
- * - State-driven: Unresolved presentation + preload allows (auto/metadata)
1353
- * - Event-driven: PLAY event when preload="none"
1526
+ * Fetches and parses VTT segments within the forward buffer window, then adds
1527
+ * cues to the track incrementally. Continues on segment errors to provide
1528
+ * partial subtitles.
1354
1529
  *
1355
1530
  * @example
1356
- * ```ts
1357
- * const state = createState({ presentation: undefined, preload: 'auto' });
1358
- * const events = createEventStream<PresentationAction>();
1359
- *
1360
- * const cleanup = resolvePresentation({ state, events });
1361
- *
1362
- * // State-driven: resolves immediately when preload allows
1363
- * state.patch({ presentation: { url: 'http://example.com/playlist.m3u8' } });
1364
- *
1365
- * // Event-driven: resolves on PLAY when preload="none"
1366
- * state.patch({ preload: 'none', presentation: { url: '...' } });
1367
- * events.dispatch({ type: 'PLAY' });
1368
- * ```
1531
+ * const cleanup = loadTextTrackCues({ state, owners });
1369
1532
  */
1370
- function resolvePresentation({ state, events }) {
1371
- let resolving = false;
1533
+ function loadTextTrackCues({ state, owners }) {
1534
+ let currentTask = null;
1372
1535
  let abortController = null;
1373
- const cleanup = combineLatest([state, events]).subscribe(async ([currentState, event]) => {
1374
- if (!canResolve$1(currentState) || !shouldResolve$1(currentState, event) || resolving) return;
1375
- try {
1376
- resolving = true;
1377
- abortController = new AbortController();
1378
- const { presentation } = currentState;
1379
- const parsed = parseMultivariantPlaylist(await getResponseText(await fetchResolvable(presentation, { signal: abortController.signal })), presentation);
1380
- state.patch({ presentation: parsed });
1381
- } catch (error) {
1382
- if (error instanceof Error && error.name === "AbortError") return;
1383
- throw error;
1384
- } finally {
1385
- resolving = false;
1386
- abortController = null;
1536
+ let lastTrackId;
1537
+ const selectedTrackId = computed(() => state.get().selectedTextTrackId);
1538
+ const cleanupEffect = effect(() => {
1539
+ const s = state.get();
1540
+ const o = owners.get();
1541
+ if (selectedTrackId.get() !== lastTrackId) {
1542
+ lastTrackId = selectedTrackId.get();
1543
+ abortController?.abort();
1544
+ currentTask = null;
1387
1545
  }
1546
+ if (currentTask) return;
1547
+ if (!shouldLoadTextTrackCues(s, o)) return;
1548
+ const textTrack = getSelectedTextTrackFromOwners(s, o);
1549
+ if (!textTrack) return;
1550
+ abortController = new AbortController();
1551
+ currentTask = loadTextTrackCuesTask({ currentState: s }, {
1552
+ signal: abortController.signal,
1553
+ textTrack,
1554
+ state
1555
+ }).finally(() => {
1556
+ currentTask = null;
1557
+ });
1388
1558
  });
1389
1559
  return () => {
1390
1560
  abortController?.abort();
1391
- cleanup();
1561
+ cleanupEffect();
1392
1562
  };
1393
1563
  }
1394
1564
 
1395
1565
  //#endregion
1396
- //#region ../spf/dist/dev/core/features/quality-switching.js
1397
- /**
1398
- * Default quality switching configuration.
1399
- */
1400
- const DEFAULT_SWITCHING_CONFIG = {
1401
- safetyMargin: .85,
1402
- minUpgradeInterval: 8e3,
1403
- defaultBandwidth: 5e6
1404
- };
1405
- /**
1406
- * Get all video tracks from a presentation's first switching set.
1407
- * Returns [] when the presentation is still unresolved (no selectionSets yet).
1408
- */
1409
- function getVideoTracks(presentation) {
1410
- return (presentation.selectionSets?.find((s) => s.type === "video"))?.switchingSets[0]?.tracks ?? [];
1411
- }
1566
+ //#region ../spf/dist/dev/dom/features/track-current-time.js
1412
1567
  /**
1413
- * Quality switching orchestration (F9).
1414
- *
1415
- * Reacts to bandwidth estimate changes and updates `selectedVideoTrackId`
1416
- * when a different quality is optimal:
1568
+ * Track current playback position from the media element.
1417
1569
  *
1418
- * - **Downgrades** happen immediately to avoid buffering stalls.
1419
- * - **Upgrades** are gated by `minUpgradeInterval` to prevent oscillation.
1420
- * - The first switch (from any track, or no track) is always immediate.
1570
+ * Mirrors `mediaElement.currentTime` into reactive state on:
1571
+ * - `timeupdate` fires during playback (~4 Hz)
1572
+ * - `seeking` fires when a seek begins; per spec, `currentTime` is
1573
+ * already at the new position when this event dispatches, so buffer
1574
+ * management can react immediately rather than waiting for `timeupdate`,
1575
+ * which does not fire while paused.
1421
1576
  *
1422
- * Smooth switching is handled downstream: when `selectedVideoTrackId` changes,
1423
- * `resolveTrack` fetches the new playlist and `loadSegments` reloads the init
1424
- * segment, then appends media segments from the current position in the new
1425
- * quality. The browser's SourceBuffer replaces the overlapping buffered range.
1577
+ * Also syncs immediately when a media element becomes available.
1426
1578
  *
1427
1579
  * @example
1428
- * const cleanup = switchQuality({ state });
1429
- * // Later, when done:
1430
- * cleanup();
1580
+ * const cleanup = trackCurrentTime({ state, owners });
1431
1581
  */
1432
- function switchQuality({ state }, config = {}) {
1433
- const safetyMargin = config.safetyMargin ?? DEFAULT_SWITCHING_CONFIG.safetyMargin;
1434
- const minUpgradeInterval = config.minUpgradeInterval ?? DEFAULT_SWITCHING_CONFIG.minUpgradeInterval;
1435
- const defaultBandwidth = config.defaultBandwidth ?? DEFAULT_SWITCHING_CONFIG.defaultBandwidth;
1436
- let lastUpgradeTime = Date.now();
1437
- let firstMeaningfulFire = true;
1438
- return state.subscribe((currentState) => {
1439
- const { presentation, bandwidthState, selectedVideoTrackId, abrDisabled } = currentState;
1440
- if (abrDisabled === true) return;
1441
- if (!presentation || !bandwidthState) return;
1442
- const videoTracks = getVideoTracks(presentation);
1443
- if (videoTracks.length === 0) return;
1444
- const isFirst = firstMeaningfulFire;
1445
- firstMeaningfulFire = false;
1446
- const optimal = selectQuality(videoTracks, getBandwidthEstimate(bandwidthState, defaultBandwidth), { safetyMargin });
1447
- if (!optimal || optimal.id === selectedVideoTrackId) return;
1448
- const currentTrack = videoTracks.find((t) => t.id === selectedVideoTrackId);
1449
- if (!currentTrack || optimal.bandwidth > currentTrack.bandwidth) {
1450
- const now = Date.now();
1451
- if (!isFirst && now - lastUpgradeTime < minUpgradeInterval) return;
1452
- lastUpgradeTime = now;
1453
- }
1454
- state.patch({ selectedVideoTrackId: optimal.id });
1582
+ function trackCurrentTime({ state, owners }) {
1583
+ let lastMediaElement;
1584
+ let removeListeners = null;
1585
+ const cleanupEffect = effect(() => {
1586
+ const { mediaElement } = owners.get();
1587
+ if (mediaElement === lastMediaElement) return;
1588
+ removeListeners?.();
1589
+ removeListeners = null;
1590
+ lastMediaElement = mediaElement;
1591
+ if (!mediaElement) return;
1592
+ const sync = () => {
1593
+ update(state, { currentTime: mediaElement.currentTime });
1594
+ };
1595
+ sync();
1596
+ const removeTimeupdate = listen(mediaElement, "timeupdate", sync);
1597
+ const removeSeeking = listen(mediaElement, "seeking", sync);
1598
+ removeListeners = () => {
1599
+ removeTimeupdate();
1600
+ removeSeeking();
1601
+ };
1455
1602
  });
1603
+ return () => {
1604
+ removeListeners?.();
1605
+ cleanupEffect();
1606
+ };
1456
1607
  }
1457
1608
 
1458
1609
  //#endregion
1459
- //#region ../spf/dist/dev/core/utils/track-selection.js
1460
- /**
1461
- * Map track type to selected track ID property key in state.
1462
- */
1463
- const SelectedTrackIdKeyByType = {
1464
- video: "selectedVideoTrackId",
1465
- audio: "selectedAudioTrackId",
1466
- text: "selectedTextTrackId"
1467
- };
1610
+ //#region ../spf/dist/dev/dom/features/track-playback-initiated.js
1468
1611
  /**
1469
- * Get selected track from state by type.
1470
- * Returns properly typed track (partially or fully resolved) or undefined.
1471
- * Type parameter T is inferred from the type argument.
1612
+ * Track whether playback has been initiated for the current presentation URL.
1613
+ *
1614
+ * Uses a local intermediate signal written by two effect streams:
1615
+ * - false stream: resets on URL change
1616
+ * - true stream: sets on play event
1617
+ *
1618
+ * A third merge effect reads the local signal and writes to state, reading
1619
+ * `state.get()` at merge time so the spread uses the up-to-date value after
1620
+ * the async forward bridge has run.
1472
1621
  *
1473
1622
  * @example
1474
- * const videoTrack = getSelectedTrack(state, 'video');
1475
- * if (videoTrack && isResolvedTrack(videoTrack)) {
1476
- * // videoTrack is VideoTrack
1477
- * }
1623
+ * const cleanup = trackPlaybackInitiated({ state, owners, events });
1478
1624
  */
1479
- function getSelectedTrack(state, type) {
1480
- const { presentation } = state;
1481
- /** @TODO Consider moving and reusing isUnresolved(presentation) (CJP) */
1482
- if (!presentation || !("id" in presentation)) return void 0;
1483
- const trackId = state[SelectedTrackIdKeyByType[type]];
1484
- return presentation.selectionSets.find(({ type: selectionSetType }) => selectionSetType === type)?.switchingSets[0]?.tracks.find(({ id }) => id === trackId);
1625
+ function trackPlaybackInitiated({ state, owners }) {
1626
+ const presentationUrl = computed(() => state.get().presentation?.url);
1627
+ const mediaElement = computed(() => owners.get().mediaElement);
1628
+ const playbackInitiated = signal(void 0);
1629
+ let lastPresentationUrl;
1630
+ let lastMediaElement;
1631
+ const cleanupResetEffect = effect(() => {
1632
+ const url = presentationUrl.get();
1633
+ const el = mediaElement.get();
1634
+ if (url !== lastPresentationUrl && lastPresentationUrl !== void 0 || el !== lastMediaElement && lastMediaElement !== void 0) playbackInitiated.set(false);
1635
+ lastPresentationUrl = url;
1636
+ lastMediaElement = el;
1637
+ });
1638
+ const cleanupPlayEffect = effect(() => {
1639
+ const el = mediaElement.get();
1640
+ if (!el) return;
1641
+ return listen(el, "play", () => {
1642
+ playbackInitiated.set(true);
1643
+ });
1644
+ });
1645
+ const cleanupMergeEffect = effect(() => {
1646
+ const pi = playbackInitiated.get();
1647
+ if (pi === void 0) return;
1648
+ if (state.get().playbackInitiated !== pi) update(state, { playbackInitiated: pi });
1649
+ });
1650
+ return () => {
1651
+ cleanupResetEffect();
1652
+ cleanupPlayEffect();
1653
+ cleanupMergeEffect();
1654
+ };
1485
1655
  }
1486
1656
 
1487
1657
  //#endregion
1488
- //#region ../spf/dist/dev/dom/features/segment-loader-actor.js
1658
+ //#region ../spf/dist/dev/dom/media/append-segment.js
1489
1659
  /**
1490
- * Creates a SegmentLoaderActor for one track type (video or audio).
1491
- *
1492
- * Receives load assignments via `send()` and owns all execution: planning,
1493
- * removes, fetches, and appends. Coordinates with the SourceBufferActor for
1494
- * all physical SourceBuffer operations.
1660
+ * Append media data to a SourceBuffer.
1495
1661
  *
1496
- * Planning (Cases 1–3) happens in `send()` on every incoming message, producing
1497
- * an ordered LoadTask list. The runner drains that list sequentially. When a new
1498
- * message arrives mid-run, send() replans and either continues the in-flight
1499
- * operation (if still needed) or preempts it.
1662
+ * Accepts either a full ArrayBuffer (single append) or an AsyncIterable of
1663
+ * Uint8Array chunks (one append per chunk, in order). Waits for `updateend`
1664
+ * between each call so appends are serialized correctly.
1500
1665
  *
1501
- * @param sourceBufferActor - Shared SourceBufferActor reference (not owned)
1502
- * @param fetchBytes - Tracked fetch closure (owns throughput sampling for segments).
1503
- * Accepts an optional `minChunkSize` in options; init segments pass `Infinity`
1504
- * so the entire body accumulates as one chunk before appending.
1666
+ * Errors from the SourceBuffer (`error` event) or from the iterable are
1667
+ * propagated as rejections.
1505
1668
  */
1506
- function createSegmentLoaderActor(sourceBufferActor, fetchBytes) {
1507
- let pendingTasks = null;
1508
- let inFlightInitTrackId = null;
1509
- let inFlightSegmentId = null;
1510
- let abortController = null;
1511
- let running = false;
1512
- let destroyed = false;
1513
- const getBufferedSegments = (allSegments) => {
1514
- const bufferedIds = new Set(sourceBufferActor.snapshot.context.segments.filter((s) => !s.partial).map((s) => s.id));
1515
- return allSegments.filter((s) => bufferedIds.has(s.id));
1516
- };
1517
- /**
1518
- * Translate a load message into an ordered LoadTask list based on committed
1519
- * actor state. In-flight awareness is handled separately in send().
1520
- *
1521
- * @todo Rename alongside LoadTask (e.g. planOps).
1522
- *
1523
- * Case 1 — Removes: forward and back buffer flush points, segment-aligned.
1524
- * No flush on track switch: appending new content overwrites existing buffer
1525
- * ranges, and the actor's time-aligned deduplication keeps the segment model
1526
- * accurate as new segments arrive.
1527
- *
1528
- * Case 2 — Init: schedule if not yet committed for this track.
1529
- *
1530
- * Case 3 — Segments: all segments in the load window not yet committed.
1531
- */
1532
- const planTasks = (message) => {
1533
- const { track, range } = message;
1534
- const actorCtx = sourceBufferActor.snapshot.context;
1535
- const bufferedSegments = getBufferedSegments(track.segments);
1536
- const currentTime = range?.start ?? 0;
1537
- const tasks = [];
1538
- if (range) {
1539
- const forwardFlushStart = calculateForwardFlushPoint(bufferedSegments, currentTime);
1540
- if (forwardFlushStart < Infinity) tasks.push({
1541
- type: "remove",
1542
- start: forwardFlushStart,
1543
- end: Infinity
1544
- });
1545
- const backFlushEnd = calculateBackBufferFlushPoint(bufferedSegments, currentTime);
1546
- if (backFlushEnd > 0) tasks.push({
1547
- type: "remove",
1548
- start: 0,
1549
- end: backFlushEnd
1550
- });
1551
- }
1552
- if (actorCtx.initTrackId !== track.id) tasks.push({
1553
- type: "append-init",
1554
- meta: { trackId: track.id },
1555
- url: track.initialization.url,
1556
- ...track.initialization.byteRange !== void 0 && { byteRange: track.initialization.byteRange }
1557
- });
1558
- if (range) {
1559
- const EPSILON = 1e-4;
1560
- const segmentsToLoad = getSegmentsToLoad(track.segments, bufferedSegments, currentTime).filter((seg) => {
1561
- const existing = actorCtx.segments.find((s) => Math.abs(s.startTime - seg.startTime) < EPSILON);
1562
- if (existing?.partial) return true;
1563
- if (!existing?.trackBandwidth || !track.bandwidth) return true;
1564
- return track.bandwidth > existing.trackBandwidth;
1565
- });
1566
- for (const segment of segmentsToLoad) tasks.push({
1567
- type: "append-segment",
1568
- meta: {
1569
- id: segment.id,
1570
- startTime: segment.startTime,
1571
- duration: segment.duration,
1572
- trackId: track.id,
1573
- trackBandwidth: track.bandwidth
1574
- },
1575
- url: segment.url,
1576
- ...segment.byteRange !== void 0 && { byteRange: segment.byteRange }
1577
- });
1669
+ async function appendSegment(sourceBuffer, data, signal) {
1670
+ if (data instanceof ArrayBuffer) await appendChunk(sourceBuffer, data);
1671
+ else try {
1672
+ for await (const chunk of data) {
1673
+ if (signal?.aborted) throw signal.reason ?? new DOMException("Aborted", "AbortError");
1674
+ await appendChunk(sourceBuffer, chunk);
1578
1675
  }
1579
- return tasks;
1580
- };
1581
- /**
1582
- * Execute a single LoadTask: fetch (if needed) then forward to SourceBufferActor.
1583
- * Sets/clears in-flight tracking around async operations so send() can make
1584
- * accurate continue/preempt decisions at any point during execution.
1585
- *
1586
- * @todo Rename alongside LoadTask (e.g. executeOp).
1587
- */
1588
- const executeLoadTask = async (task) => {
1589
- const signal = abortController.signal;
1676
+ } catch (e) {
1677
+ if (e instanceof DOMException && e.name === "AbortError" && !sourceBuffer.updating) try {
1678
+ sourceBuffer.abort();
1679
+ } catch {}
1680
+ throw e;
1681
+ }
1682
+ }
1683
+ async function appendChunk(sourceBuffer, data) {
1684
+ if (sourceBuffer.updating) await new Promise((resolve) => {
1685
+ const onUpdateEnd = () => {
1686
+ sourceBuffer.removeEventListener("updateend", onUpdateEnd);
1687
+ resolve();
1688
+ };
1689
+ sourceBuffer.addEventListener("updateend", onUpdateEnd);
1690
+ });
1691
+ return new Promise((resolve, reject) => {
1692
+ const onUpdateEnd = () => {
1693
+ cleanup();
1694
+ resolve();
1695
+ };
1696
+ const onError = (event) => {
1697
+ cleanup();
1698
+ reject(/* @__PURE__ */ new Error(`SourceBuffer append error: ${event.type}`));
1699
+ };
1700
+ const cleanup = () => {
1701
+ sourceBuffer.removeEventListener("updateend", onUpdateEnd);
1702
+ sourceBuffer.removeEventListener("error", onError);
1703
+ };
1704
+ sourceBuffer.addEventListener("updateend", onUpdateEnd);
1705
+ sourceBuffer.addEventListener("error", onError);
1590
1706
  try {
1591
- if (task.type === "remove") {
1592
- await sourceBufferActor.send(task, signal);
1593
- return;
1594
- }
1595
- if (task.type === "append-init") {
1596
- inFlightInitTrackId = task.meta.trackId;
1597
- if (!signal.aborted) {
1598
- const data = await fetchBytes(task, {
1599
- signal,
1600
- minChunkSize: Infinity
1601
- });
1602
- const isTrackSwitch = pendingTasks?.some((t) => t.type === "append-init" && t.meta.trackId !== task.meta.trackId);
1603
- if (!signal.aborted || !isTrackSwitch) {
1604
- const appendSignal = signal.aborted ? new AbortController().signal : signal;
1605
- await sourceBufferActor.send({
1606
- type: "append-init",
1607
- data,
1608
- meta: task.meta
1609
- }, appendSignal);
1610
- }
1611
- }
1612
- return;
1613
- }
1614
- inFlightSegmentId = task.meta.id;
1615
- if (!signal.aborted) {
1616
- const stream = await fetchBytes(task, { signal });
1617
- if (!signal.aborted) await sourceBufferActor.send({
1618
- type: "append-segment",
1619
- data: stream,
1620
- meta: task.meta
1621
- }, signal);
1622
- }
1623
- } finally {
1624
- inFlightInitTrackId = null;
1625
- inFlightSegmentId = null;
1626
- }
1627
- };
1628
- /**
1629
- * Drain the scheduled task list sequentially.
1630
- * After each task completes, checks for a pending replacement plan from send().
1631
- * If the signal was aborted and no new plan arrived, stops immediately.
1632
- */
1633
- const runScheduled = async (initialTasks) => {
1634
- running = true;
1635
- abortController = new AbortController();
1636
- let scheduled = initialTasks;
1637
- while (scheduled.length > 0 && !destroyed) {
1638
- const task = scheduled[0];
1639
- scheduled = scheduled.slice(1);
1640
- try {
1641
- await executeLoadTask(task);
1642
- } catch (error) {
1643
- if (error instanceof Error && error.name === "AbortError") {} else {
1644
- console.error("Unexpected error in segment loader:", error);
1645
- scheduled = [];
1646
- }
1647
- }
1648
- if (pendingTasks !== null) {
1649
- scheduled = pendingTasks;
1650
- pendingTasks = null;
1651
- abortController = new AbortController();
1652
- } else if (abortController.signal.aborted) break;
1707
+ sourceBuffer.appendBuffer(data);
1708
+ } catch (error) {
1709
+ cleanup();
1710
+ reject(error);
1653
1711
  }
1654
- abortController = null;
1655
- running = false;
1656
- };
1657
- return {
1658
- send(message) {
1659
- if (destroyed) return;
1660
- const allTasks = planTasks(message);
1661
- if (!running) {
1662
- if (allTasks.length === 0) return;
1663
- runScheduled(allTasks);
1664
- return;
1665
- }
1666
- if (inFlightSegmentId !== null && allTasks.some((t) => t.type === "append-segment" && t.meta.id === inFlightSegmentId) || inFlightInitTrackId !== null && allTasks.some((t) => t.type === "append-init" && t.meta.trackId === inFlightInitTrackId)) pendingTasks = allTasks.filter((t) => !(t.type === "append-segment" && t.meta.id === inFlightSegmentId) && !(t.type === "append-init" && t.meta.trackId === inFlightInitTrackId));
1667
- else {
1668
- pendingTasks = allTasks;
1669
- abortController?.abort();
1670
- }
1671
- },
1672
- destroy() {
1673
- destroyed = true;
1674
- abortController?.abort();
1712
+ });
1713
+ }
1714
+
1715
+ //#endregion
1716
+ //#region ../spf/dist/dev/dom/media/buffer-flusher.js
1717
+ /**
1718
+ * Buffer flusher helper (P12)
1719
+ *
1720
+ * Removes a time range from a SourceBuffer to manage memory.
1721
+ */
1722
+ /**
1723
+ * Remove a time range from a SourceBuffer.
1724
+ *
1725
+ * Waits for the SourceBuffer to be ready (not updating), then removes
1726
+ * the specified range. Returns a promise that resolves when removal completes.
1727
+ *
1728
+ * @param sourceBuffer - The SourceBuffer to remove data from
1729
+ * @param start - Start of the time range to remove (seconds)
1730
+ * @param end - End of the time range to remove (seconds)
1731
+ * @returns Promise that resolves when removal completes
1732
+ *
1733
+ * @example
1734
+ * await flushBuffer(videoSourceBuffer, 0, 30);
1735
+ */
1736
+ async function flushBuffer(sourceBuffer, start, end) {
1737
+ if (sourceBuffer.updating) await new Promise((resolve) => {
1738
+ const onUpdateEnd = () => {
1739
+ sourceBuffer.removeEventListener("updateend", onUpdateEnd);
1740
+ resolve();
1741
+ };
1742
+ sourceBuffer.addEventListener("updateend", onUpdateEnd);
1743
+ });
1744
+ return new Promise((resolve, reject) => {
1745
+ const onUpdateEnd = () => {
1746
+ cleanup();
1747
+ resolve();
1748
+ };
1749
+ const onError = (event) => {
1750
+ cleanup();
1751
+ reject(/* @__PURE__ */ new Error(`SourceBuffer remove error: ${event.type}`));
1752
+ };
1753
+ const cleanup = () => {
1754
+ sourceBuffer.removeEventListener("updateend", onUpdateEnd);
1755
+ sourceBuffer.removeEventListener("error", onError);
1756
+ };
1757
+ sourceBuffer.addEventListener("updateend", onUpdateEnd);
1758
+ sourceBuffer.addEventListener("error", onError);
1759
+ try {
1760
+ sourceBuffer.remove(start, end);
1761
+ } catch (error) {
1762
+ cleanup();
1763
+ reject(error);
1675
1764
  }
1676
- };
1765
+ });
1677
1766
  }
1678
1767
 
1679
1768
  //#endregion
1680
- //#region ../spf/dist/dev/dom/features/load-segments.js
1681
- const ActorKeyByType = {
1682
- video: "videoBufferActor",
1683
- audio: "audioBufferActor"
1684
- };
1685
- function createTrackedFetch(throughput, onSample) {
1686
- return async (addressable, options) => {
1687
- const { minChunkSize, ...fetchOptions } = options ?? {};
1688
- const response = await fetchResolvable(addressable, fetchOptions);
1689
- if (!response.body) throw new Error("Response has no body");
1690
- const body = response.body;
1691
- return { [Symbol.asyncIterator]: async function* () {
1692
- let chunkStart = performance.now();
1693
- for await (const chunk of new ChunkedStreamIterable(body, ...minChunkSize !== void 0 ? [{ minChunkSize }] : [])) {
1694
- const elapsed = performance.now() - chunkStart;
1695
- const next = sampleBandwidth(throughput.current, elapsed, chunk.byteLength);
1696
- throughput.patch(next);
1697
- throughput.flush();
1698
- onSample?.(next);
1699
- yield chunk;
1700
- chunkStart = performance.now();
1701
- }
1702
- } };
1703
- };
1769
+ //#region ../spf/dist/dev/core/features/calculate-presentation-duration.js
1770
+ /**
1771
+ * Check if we can calculate presentation duration (have required data).
1772
+ */
1773
+ function canCalculateDuration(state) {
1774
+ if (!state.presentation) return false;
1775
+ return !!(state.selectedVideoTrackId || state.selectedAudioTrackId);
1704
1776
  }
1705
1777
  /**
1706
- * Non-tracking fetch: eagerly starts the request and returns the response body
1707
- * as a lazy chunk iterable. Used for audio tracks which don't sample bandwidth.
1708
- * Pass `minChunkSize: Infinity` to accumulate the full body as a single chunk
1709
- * (equivalent to arrayBuffer() but through the same streaming path).
1778
+ * Check if we should calculate presentation duration (conditions met).
1710
1779
  */
1711
- async function fetchStream(addressable, options) {
1712
- const { minChunkSize, ...fetchOptions } = options ?? {};
1713
- const response = await fetchResolvable(addressable, fetchOptions);
1714
- if (!response.body) throw new Error("Response has no body");
1715
- return new ChunkedStreamIterable(response.body, ...minChunkSize !== void 0 ? [{ minChunkSize }] : []);
1780
+ function shouldCalculateDuration(state) {
1781
+ if (!canCalculateDuration(state)) return false;
1782
+ const { presentation } = state;
1783
+ if (presentation.duration !== void 0) return false;
1784
+ const videoTrack = state.selectedVideoTrackId ? getSelectedTrack(state, "video") : void 0;
1785
+ const audioTrack = state.selectedAudioTrackId ? getSelectedTrack(state, "audio") : void 0;
1786
+ return !!(videoTrack && isResolvedTrack(videoTrack) || audioTrack && isResolvedTrack(audioTrack));
1716
1787
  }
1717
- function selectLoadingInputs([segmentsCanLoad, state], type) {
1718
- const { playbackInitiated, preload, currentTime } = state;
1719
- return {
1720
- playbackInitiated,
1721
- preload,
1722
- currentTime,
1723
- track: getSelectedTrack(state, type),
1724
- segmentsCanLoad
1725
- };
1788
+ /**
1789
+ * Get duration from the first resolved track (prefer video, fallback to audio).
1790
+ */
1791
+ function getDurationFromResolvedTracks(state) {
1792
+ const videoTrack = state.selectedVideoTrackId ? getSelectedTrack(state, "video") : void 0;
1793
+ if (videoTrack && isResolvedTrack(videoTrack)) return videoTrack.duration;
1794
+ const audioTrack = state.selectedAudioTrackId ? getSelectedTrack(state, "audio") : void 0;
1795
+ if (audioTrack && isResolvedTrack(audioTrack)) return audioTrack.duration;
1726
1796
  }
1727
1797
  /**
1728
- * Equality function encoding the condition hierarchy for relevant changes.
1798
+ * Calculate and set presentation duration from resolved tracks.
1799
+ */
1800
+ function calculatePresentationDuration({ state }) {
1801
+ return effect(() => {
1802
+ const currentState = state.get();
1803
+ if (!shouldCalculateDuration(currentState)) return;
1804
+ const duration = getDurationFromResolvedTracks(currentState);
1805
+ if (duration === void 0 || !Number.isFinite(duration)) return;
1806
+ state.set({
1807
+ ...currentState,
1808
+ presentation: {
1809
+ ...currentState.presentation,
1810
+ duration
1811
+ }
1812
+ });
1813
+ });
1814
+ }
1815
+
1816
+ //#endregion
1817
+ //#region ../spf/dist/dev/core/abr/quality-selection.js
1818
+ /**
1819
+ * Default quality selection configuration.
1820
+ * Values match Shaka Player upgrade threshold (0.85 = 15% headroom).
1821
+ */
1822
+ const DEFAULT_QUALITY_CONFIG = { safetyMargin: .85 };
1823
+ /**
1824
+ * Select the best video track based on current bandwidth estimate.
1729
1825
  *
1730
- * Pre-play (!playbackInitiated):
1731
- * Only preload changes matter. currentTime and resolvedTrackId are ignored
1732
- * (track changes not supported pre-play; currentTime value is used at
1733
- * trigger time but changes don't re-trigger).
1826
+ * Selects the highest quality track where bandwidth is sufficient with safety margin:
1827
+ * - currentBandwidth >= track.bandwidth / safetyMargin
1828
+ * - Default safetyMargin 0.85 means track uses ≤85% of bandwidth (15% headroom)
1829
+ * - At same bandwidth, prefers higher resolution
1734
1830
  *
1735
- * playbackInitiated transition:
1736
- * Always fires (handled in the subscriber; preload='auto' suppression
1737
- * applied there since equality functions have no memory of prior values).
1831
+ * @param tracks - Available video tracks (can be unsorted)
1832
+ * @param currentBandwidth - Current bandwidth estimate in bits per second
1833
+ * @param config - Optional quality selection configuration
1834
+ * @returns Selected track, or undefined if no tracks available
1738
1835
  *
1739
- * Post-play (playbackInitiated):
1740
- * resolvedTrackId changes (track switch or previously-unresolved track
1741
- * resolving) and currentTime changes both trigger. preload is irrelevant.
1742
- */
1743
- const segmentStartFor = (currentTime, track) => {
1744
- if (currentTime == null) return void 0;
1745
- return track?.segments.find(({ startTime, duration }, i, segments) => currentTime >= startTime && (currentTime < startTime + duration || i === segments.length - 1))?.startTime;
1746
- };
1747
- /**
1748
- * Returns true when the inputs are equal (no meaningful change — don't fire).
1749
- * Returns false when the inputs differ in a way that requires a new message.
1836
+ * @example
1837
+ * const tracks = [
1838
+ * { id: '360p', bandwidth: 500_000, ... },
1839
+ * { id: '720p', bandwidth: 2_000_000, ... },
1840
+ * { id: '1080p', bandwidth: 4_000_000, ... },
1841
+ * ];
1750
1842
  *
1751
- * This IS the shouldLoadSegments logic, expressed as an equality function.
1843
+ * // With 2.5 Mbps, selects 720p (1080p needs 4M/0.85 = 4.7 Mbps)
1844
+ * const selected = selectQuality(tracks, 2_500_000);
1752
1845
  */
1753
- function loadingInputsEq(prevState, curState) {
1754
- if (!curState.segmentsCanLoad) return true;
1755
- if (!curState.playbackInitiated) {
1756
- if (curState.preload === "none") return true;
1757
- return curState.preload === prevState.preload;
1758
- }
1759
- if (!prevState.playbackInitiated && curState.playbackInitiated) {
1760
- if (prevState.preload !== "auto") return false;
1846
+ function selectQuality(tracks, currentBandwidth, config = DEFAULT_QUALITY_CONFIG) {
1847
+ if (tracks.length === 0) return;
1848
+ const sortedTracks = tracks.slice().sort((a, b) => a.bandwidth - b.bandwidth);
1849
+ let chosen;
1850
+ for (const track of sortedTracks) if (currentBandwidth >= track.bandwidth / config.safetyMargin) {
1851
+ if (!chosen || track.bandwidth > chosen.bandwidth || track.bandwidth === chosen.bandwidth && hasHigherResolution(track, chosen)) chosen = track;
1761
1852
  }
1762
- if (!curState.track || !isResolvedTrack(curState.track)) return true;
1763
- if (prevState.track?.id !== curState.track.id && isResolvedTrack(curState.track)) return false;
1764
- return segmentStartFor(prevState.currentTime, curState.track) === segmentStartFor(curState.currentTime, curState.track);
1853
+ return chosen ?? sortedTracks[0];
1765
1854
  }
1766
1855
  /**
1767
- * Load segments orchestration Reactor layer.
1768
- *
1769
- * Sends typed load messages to a SegmentLoaderActor when relevant conditions
1770
- * change. Uses targeted subscriptions rather than broad combineLatest so only
1771
- * meaningful state changes trigger evaluation.
1856
+ * Check if track A has higher resolution than track B.
1857
+ * Compares by total pixel count (width × height).
1772
1858
  *
1773
- * Condition hierarchy (see SegmentLoadingKey for detail):
1859
+ * @param trackA - First track to compare
1860
+ * @param trackB - Second track to compare
1861
+ * @returns True if trackA has more pixels than trackB
1862
+ */
1863
+ function hasHigherResolution(trackA, trackB) {
1864
+ return (trackA.width ?? 0) * (trackA.height ?? 0) > (trackB.width ?? 0) * (trackB.height ?? 0);
1865
+ }
1866
+
1867
+ //#endregion
1868
+ //#region ../spf/dist/dev/core/features/quality-switching.js
1869
+ /**
1870
+ * Default quality switching configuration.
1871
+ */
1872
+ const DEFAULT_SWITCHING_CONFIG = {
1873
+ safetyMargin: .85,
1874
+ minUpgradeInterval: 8e3,
1875
+ defaultBandwidth: 5e6
1876
+ };
1877
+ /**
1878
+ * Get all video tracks from a presentation's first switching set.
1879
+ * Returns [] when the presentation is still unresolved (no selectionSets yet).
1880
+ */
1881
+ function getVideoTracks(presentation) {
1882
+ return (presentation.selectionSets?.find((s) => s.type === "video"))?.switchingSets[0]?.tracks ?? [];
1883
+ }
1884
+ /**
1885
+ * Quality switching orchestration (F9).
1774
1886
  *
1775
- * !playbackInitiated
1776
- * preload==='none' (or unset) → dormant; no trigger
1777
- * preload==='metadata' → trigger on transition to 'metadata'
1778
- * preload==='auto' → trigger on transition to 'auto'
1887
+ * Reacts to bandwidth estimate changes and updates `selectedVideoTrackId`
1888
+ * when a different quality is optimal:
1779
1889
  *
1780
- * !playbackInitiated playbackInitiated
1781
- * preload !== 'auto' → trigger (message shape changes)
1782
- * preload === 'auto' → suppressed (was already full-range mode;
1783
- * let segmentStart take over post-play)
1784
- * KNOWN LIMITATION: seek-before-play with
1785
- * preload='auto' is not supported — if the
1786
- * user seeks before pressing play, the
1787
- * first re-send is delayed until the next
1788
- * segment boundary crossing post-play.
1890
+ * - **Downgrades** happen immediately to avoid buffering stalls.
1891
+ * - **Upgrades** are gated by `minUpgradeInterval` to prevent oscillation.
1892
+ * - The first switch (from any track, or no track) is always immediate.
1789
1893
  *
1790
- * playbackInitiated
1791
- * resolvedTrackId changes → trigger
1792
- * segmentStart(currentTime) changes trigger (segment boundary only)
1894
+ * Smooth switching is handled downstream: when `selectedVideoTrackId` changes,
1895
+ * `resolveTrack` fetches the new playlist and `loadSegments` reloads the init
1896
+ * segment, then appends media segments from the current position in the new
1897
+ * quality. The browser's SourceBuffer replaces the overlapping buffered range.
1793
1898
  *
1794
1899
  * @example
1795
- * const cleanup = loadSegments({ state, owners }, { type: 'video' });
1900
+ * const cleanup = switchQuality({ state });
1901
+ * // Later, when done:
1902
+ * cleanup();
1796
1903
  */
1797
- function loadSegments({ state, owners }, config) {
1798
- const { type } = config;
1799
- const actorKey = ActorKeyByType[type];
1800
- const initialBandwidth = state.current.bandwidthState;
1801
- const throughput = createState(initialBandwidth ?? {
1802
- fastEstimate: 0,
1803
- fastTotalWeight: 0,
1804
- slowEstimate: 0,
1805
- slowTotalWeight: 0,
1806
- bytesSampled: 0
1807
- });
1808
- const fetchBytes = type === "video" ? createTrackedFetch(throughput, initialBandwidth !== void 0 ? (next) => {
1809
- state.patch({ bandwidthState: next });
1810
- state.flush();
1811
- } : void 0) : fetchStream;
1812
- const segmentLoader = createState(void 0);
1813
- const unsubActorLifecycle = owners.subscribe((o) => o[actorKey], (actor) => {
1814
- if (actor) segmentLoader.patch(createSegmentLoaderActor(actor, fetchBytes));
1815
- else if (!actor && segmentLoader.current) {
1816
- segmentLoader.current.destroy();
1817
- segmentLoader.patch(void 0);
1904
+ function switchQuality({ state }, config = {}) {
1905
+ const safetyMargin = config.safetyMargin ?? DEFAULT_SWITCHING_CONFIG.safetyMargin;
1906
+ const minUpgradeInterval = config.minUpgradeInterval ?? DEFAULT_SWITCHING_CONFIG.minUpgradeInterval;
1907
+ const defaultBandwidth = config.defaultBandwidth ?? DEFAULT_SWITCHING_CONFIG.defaultBandwidth;
1908
+ let lastUpgradeTime = Date.now();
1909
+ let firstMeaningfulFire = true;
1910
+ return effect(() => {
1911
+ const { presentation, bandwidthState, selectedVideoTrackId, abrDisabled } = state.get();
1912
+ if (abrDisabled === true) return;
1913
+ if (!presentation || !bandwidthState) return;
1914
+ const videoTracks = getVideoTracks(presentation);
1915
+ if (videoTracks.length === 0) return;
1916
+ const isFirst = firstMeaningfulFire;
1917
+ firstMeaningfulFire = false;
1918
+ const optimal = selectQuality(videoTracks, getBandwidthEstimate(bandwidthState, defaultBandwidth), { safetyMargin });
1919
+ if (!optimal || optimal.id === selectedVideoTrackId) return;
1920
+ const currentTrack = videoTracks.find((t) => t.id === selectedVideoTrackId);
1921
+ if (!currentTrack || optimal.bandwidth > currentTrack.bandwidth) {
1922
+ const now = Date.now();
1923
+ if (!isFirst && now - lastUpgradeTime < minUpgradeInterval) return;
1924
+ lastUpgradeTime = now;
1818
1925
  }
1819
- return () => {
1820
- segmentLoader.current?.destroy();
1821
- segmentLoader.patch(void 0);
1822
- };
1926
+ update(state, { selectedVideoTrackId: optimal.id });
1823
1927
  });
1824
- const segmentsCanLoad = createState(false);
1825
- const unsubscribeCanLoadSegments = combineLatest([state, segmentLoader]).subscribe(([currentState, currentSegmentLoader]) => {
1826
- const track = getSelectedTrack(currentState, type);
1827
- const trackResolved = !!track && isResolvedTrack(track);
1828
- const segmentLoaderActorExists = !!currentSegmentLoader;
1829
- segmentsCanLoad.patch(trackResolved && segmentLoaderActorExists);
1830
- });
1831
- const unsubscribeShouldLoadSegments = combineLatest([segmentsCanLoad, state]).subscribe(([segmentsCanLoad, state]) => selectLoadingInputs([segmentsCanLoad, state], type), ({ preload, playbackInitiated, currentTime, track }) => {
1832
- if (!(preload === "auto" || !!playbackInitiated))
1833
- /** @ts-expect-error */
1834
- segmentLoader.current?.send({
1835
- type: "load",
1836
- track
1837
- });
1838
- else segmentLoader.current?.send({
1839
- type: "load",
1840
- track,
1841
- range: {
1842
- start: currentTime,
1843
- end: currentTime + DEFAULT_FORWARD_BUFFER_CONFIG.bufferDuration
1844
- }
1845
- });
1846
- }, { equalityFn: loadingInputsEq });
1847
- return () => {
1848
- unsubscribeCanLoadSegments();
1849
- unsubscribeShouldLoadSegments();
1850
- unsubActorLifecycle();
1851
- };
1852
1928
  }
1853
1929
 
1854
1930
  //#endregion
1855
- //#region ../spf/dist/dev/dom/text/parse-vtt-segment.js
1931
+ //#region ../spf/dist/dev/core/utils/generate-id.js
1856
1932
  /**
1857
- * Parse a VTT segment using browser's native parser.
1933
+ * Generate unique ID for HAM objects.
1858
1934
  *
1859
- * Creates a dummy video element with a track element to leverage
1860
- * the browser's optimized VTT parsing. Returns parsed VTTCue objects.
1935
+ * Uses timestamp + random number for sufficient uniqueness.
1936
+ * IDs are strings without decimals.
1937
+ *
1938
+ * @returns Unique string ID in format: timestamp-random
1939
+ *
1940
+ * @example
1941
+ * ```ts
1942
+ * const id = generateId(); // "1738423156789-542891"
1943
+ * ```
1861
1944
  */
1862
- let dummyVideo = null;
1863
- function ensureDummyVideo() {
1864
- if (!dummyVideo) {
1865
- dummyVideo = document.createElement("video");
1866
- dummyVideo.muted = true;
1867
- dummyVideo.preload = "none";
1868
- dummyVideo.style.display = "none";
1869
- dummyVideo.crossOrigin = "anonymous";
1870
- }
1871
- return dummyVideo;
1872
- }
1873
- function parseVttSegment(url) {
1874
- const video = ensureDummyVideo();
1875
- const track = document.createElement("track");
1876
- track.kind = "subtitles";
1877
- track.default = true;
1878
- return new Promise((resolve, reject) => {
1879
- const onLoad = () => {
1880
- const cues = [];
1881
- const textTrack = track.track;
1882
- if (textTrack.cues) for (let i = 0; i < textTrack.cues.length; i++) {
1883
- const cue = textTrack.cues[i];
1884
- if (cue) cues.push(cue);
1885
- }
1886
- cleanup();
1887
- resolve(cues);
1888
- };
1889
- const onError = () => {
1890
- cleanup();
1891
- reject(/* @__PURE__ */ new Error(`Failed to load VTT segment: ${url}`));
1892
- };
1893
- const cleanup = () => {
1894
- track.removeEventListener("load", onLoad);
1895
- track.removeEventListener("error", onError);
1896
- video.removeChild(track);
1897
- };
1898
- track.addEventListener("load", onLoad);
1899
- track.addEventListener("error", onError);
1900
- video.appendChild(track);
1901
- track.src = url;
1902
- });
1903
- }
1904
- function destroyVttParser() {
1905
- dummyVideo = null;
1945
+ function generateId() {
1946
+ return `${Date.now()}-${Math.floor(Math.random() * 1e6)}`;
1906
1947
  }
1907
1948
 
1908
1949
  //#endregion
1909
- //#region ../spf/dist/dev/dom/features/load-text-track-cues.js
1910
- function isDuplicateCue(cue, textTrack) {
1911
- const { cues } = textTrack;
1912
- if (!cues) return false;
1913
- for (let i = 0; i < cues.length; i++) {
1914
- const existing = cues[i];
1915
- if (existing.startTime === cue.startTime && existing.endTime === cue.endTime && existing.text === cue.text) return true;
1950
+ //#region ../spf/dist/dev/core/hls/parse-attributes.js
1951
+ /**
1952
+ * Parse HLS attribute list from a tag line.
1953
+ * Handles both quoted and unquoted values.
1954
+ */
1955
+ function parseAttributeList(line) {
1956
+ const attributes = /* @__PURE__ */ new Map();
1957
+ for (const match of line.matchAll(/([A-Z0-9-]+)=(?:"([^"]*)"|([^,]*))/g)) {
1958
+ const key = match[1];
1959
+ const value = match[2] ?? match[3] ?? "";
1960
+ if (key) attributes.set(key, value);
1916
1961
  }
1917
- return false;
1962
+ return attributes;
1918
1963
  }
1919
- const loadVttSegmentTask = async ({ segment }, context) => {
1920
- const cues = await parseVttSegment(segment.url);
1921
- for (const cue of cues) if (!isDuplicateCue(cue, context.textTrack)) context.textTrack.addCue(cue);
1922
- };
1923
1964
  /**
1924
- * Load text track cues task (composite - orchestrates VTT segment subtasks).
1965
+ * Parse RESOLUTION attribute value (WIDTHxHEIGHT).
1925
1966
  */
1926
- const loadTextTrackCuesTask = async ({ currentState }, context) => {
1927
- const track = findSelectedTextTrack(currentState);
1928
- if (!track || !isResolvedTrack(track)) return;
1929
- const { segments } = track;
1930
- if (segments.length === 0) return;
1931
- const trackId = track.id;
1932
- const loadedIds = new Set((currentState.textBufferState?.[trackId]?.segments ?? []).map((s) => s.id));
1933
- const segmentsToLoad = getSegmentsToLoad(segments, segments.filter((s) => loadedIds.has(s.id)), currentState.currentTime ?? 0).filter((s) => !loadedIds.has(s.id));
1934
- if (segmentsToLoad.length === 0) return;
1935
- for (const segment of segmentsToLoad) {
1936
- if (context.signal.aborted) break;
1937
- try {
1938
- await loadVttSegmentTask({ segment }, { textTrack: context.textTrack });
1939
- const latest = context.state.current.textBufferState ?? {};
1940
- const trackState = latest[trackId] ?? { segments: [] };
1941
- context.state.patch({ textBufferState: {
1942
- ...latest,
1943
- [trackId]: { segments: [...trackState.segments, { id: segment.id }] }
1944
- } });
1945
- } catch (error) {
1946
- if (error instanceof Error && error.name === "AbortError") break;
1947
- console.error("Failed to load VTT segment:", error);
1948
- }
1949
- }
1950
- if (context.textTrack.mode === "showing" && context.textTrack.cues) Array.from(context.textTrack.cues).forEach((cue) => {
1951
- context.textTrack.addCue(cue);
1952
- });
1953
- await new Promise((resolve) => requestAnimationFrame(resolve));
1954
- };
1967
+ function parseResolution(value) {
1968
+ const match = /^(\d+)x(\d+)$/.exec(value);
1969
+ if (!match) return null;
1970
+ return {
1971
+ width: Number.parseInt(match[1], 10),
1972
+ height: Number.parseInt(match[2], 10)
1973
+ };
1974
+ }
1955
1975
  /**
1956
- * Find the selected text track in the presentation.
1976
+ * Parse FRAME-RATE attribute to rational frame rate.
1957
1977
  */
1958
- function findSelectedTextTrack(state) {
1959
- if (!state.presentation || !state.selectedTextTrackId) return;
1960
- const textSet = state.presentation.selectionSets.find((set) => set.type === "text");
1961
- if (!textSet?.switchingSets?.[0]?.tracks) return;
1962
- return textSet.switchingSets[0].tracks.find((t) => t.id === state.selectedTextTrackId);
1978
+ function parseFrameRate(value) {
1979
+ const fps = Number.parseFloat(value);
1980
+ if (Number.isNaN(fps) || fps <= 0) return void 0;
1981
+ if (Math.abs(fps - 23.976) < .01) return {
1982
+ frameRateNumerator: 24e3,
1983
+ frameRateDenominator: 1001
1984
+ };
1985
+ if (Math.abs(fps - 29.97) < .01) return {
1986
+ frameRateNumerator: 3e4,
1987
+ frameRateDenominator: 1001
1988
+ };
1989
+ if (Math.abs(fps - 59.94) < .01) return {
1990
+ frameRateNumerator: 6e4,
1991
+ frameRateDenominator: 1001
1992
+ };
1993
+ if (fps % 1 === 0) return { frameRateNumerator: Math.round(fps) };
1994
+ return { frameRateNumerator: Math.round(fps) };
1963
1995
  }
1964
1996
  /**
1965
- * Get the browser's TextTrack object for the selected text track.
1966
- *
1967
- * Retrieves the live TextTrack interface from the track element in owners,
1968
- * which is used for adding cues, checking mode, and managing track state.
1969
- *
1970
- * Note: Returns the DOM TextTrack interface (HTMLTrackElement.track),
1971
- * not the presentation Track metadata type.
1972
- *
1973
- * @param state - Current playback state (track selection)
1974
- * @param owners - DOM owners containing track elements map
1975
- * @returns DOM TextTrack interface or undefined if not found
1997
+ * Parse CODECS attribute into separate video and audio codecs.
1976
1998
  */
1977
- function getSelectedTextTrackFromOwners(state, owners) {
1978
- const trackId = state.selectedTextTrackId;
1979
- if (!trackId || !owners.textTracks) return;
1980
- return owners.textTracks.get(trackId)?.track;
1999
+ function parseCodecs(codecs) {
2000
+ const parts = codecs.split(",").map((s) => s.trim());
2001
+ const result = {};
2002
+ for (const codec of parts) if (codec.startsWith("avc1.") || codec.startsWith("hvc1.") || codec.startsWith("hev1.")) result.video = codec;
2003
+ else if (codec.startsWith("mp4a.")) result.audio = codec;
2004
+ return result;
1981
2005
  }
1982
2006
  /**
1983
- * Check if we can load text track cues.
1984
- *
1985
- * Requires:
1986
- * - Selected text track ID exists
1987
- * - Track elements map exists
1988
- * - Track element exists for selected track
2007
+ * Parse #EXTINF duration value.
2008
+ */
2009
+ function parseExtInfDuration(value) {
2010
+ const durationPart = value.split(",")[0] ?? value;
2011
+ const duration = Number.parseFloat(durationPart);
2012
+ return Number.isNaN(duration) ? 0 : duration;
2013
+ }
2014
+ /**
2015
+ * Parse BYTERANGE attribute value.
2016
+ * Format: "length[@offset]"
2017
+ * If offset is omitted, it continues from the previous byte range end.
2018
+ */
2019
+ function parseByteRange(value, previousEnd) {
2020
+ const match = /^(\d+)(?:@(\d+))?$/.exec(value);
2021
+ if (!match) return null;
2022
+ const length = Number.parseInt(match[1], 10);
2023
+ if (Number.isNaN(length)) return null;
2024
+ let start;
2025
+ if (match[2] !== void 0) {
2026
+ start = Number.parseInt(match[2], 10);
2027
+ if (Number.isNaN(start)) return null;
2028
+ } else if (previousEnd !== void 0) start = previousEnd;
2029
+ else return null;
2030
+ return {
2031
+ start,
2032
+ end: start + length - 1
2033
+ };
2034
+ }
2035
+ /**
2036
+ * Create AttributeList from raw attribute string.
2037
+ */
2038
+ function createAttributeList(line) {
2039
+ const map = parseAttributeList(line);
2040
+ return {
2041
+ get(key) {
2042
+ return map.get(key);
2043
+ },
2044
+ getInt(key, defaultValue) {
2045
+ const value = map.get(key);
2046
+ if (value === void 0) return defaultValue;
2047
+ const parsed = Number.parseInt(value, 10);
2048
+ return Number.isNaN(parsed) ? defaultValue : parsed;
2049
+ },
2050
+ getFloat(key, defaultValue) {
2051
+ const value = map.get(key);
2052
+ if (value === void 0) return defaultValue;
2053
+ const parsed = Number.parseFloat(value);
2054
+ return Number.isNaN(parsed) ? defaultValue : parsed;
2055
+ },
2056
+ getBool(key) {
2057
+ return map.get(key) === "YES";
2058
+ },
2059
+ getResolution(key) {
2060
+ const value = map.get(key);
2061
+ if (!value) return void 0;
2062
+ return parseResolution(value) ?? void 0;
2063
+ },
2064
+ getFrameRate(key) {
2065
+ const value = map.get(key);
2066
+ if (!value) return void 0;
2067
+ return parseFrameRate(value);
2068
+ }
2069
+ };
2070
+ }
2071
+ /**
2072
+ * Match a tag and extract its attributes.
2073
+ * Returns null if the line doesn't match the tag.
1989
2074
  */
1990
- function canLoadTextTrackCues(state, owners) {
1991
- return !!state.selectedTextTrackId && !!owners.textTracks && owners.textTracks.has(state.selectedTextTrackId);
2075
+ function matchTag(line, tag) {
2076
+ const prefix = `#${tag}:`;
2077
+ if (!line.startsWith(prefix)) return null;
2078
+ return createAttributeList(line.slice(prefix.length));
1992
2079
  }
2080
+
2081
+ //#endregion
2082
+ //#region ../spf/dist/dev/core/hls/resolve-url.js
1993
2083
  /**
1994
- * Check if we should load text track cues.
1995
- *
1996
- * Only load if:
1997
- * - Track is resolved (has segments)
1998
- * - Track has at least one segment
1999
- * - Track element exists
2084
+ * Resolve a potentially relative URL against a base URL using native URL API.
2000
2085
  */
2001
- function shouldLoadTextTrackCues(state, owners) {
2002
- if (!canLoadTextTrackCues(state, owners)) return false;
2003
- const track = findSelectedTextTrack(state);
2004
- if (!track || !isResolvedTrack(track) || track.segments.length === 0) return false;
2005
- if (!getSelectedTextTrackFromOwners(state, owners)) return false;
2006
- return true;
2086
+ function resolveUrl(url, baseUrl) {
2087
+ return new URL(url, baseUrl).href;
2007
2088
  }
2089
+
2090
+ //#endregion
2091
+ //#region ../spf/dist/dev/core/hls/parse-multivariant.js
2008
2092
  /**
2009
- * Load text track cues orchestration.
2010
- *
2011
- * Triggers when:
2012
- * - Text track is selected
2013
- * - Track is resolved (has segments)
2014
- * - Track element exists
2093
+ * Parse HLS multivariant playlist into a Presentation.
2015
2094
  *
2016
- * Fetches and parses VTT segments within the forward buffer window, then adds
2017
- * cues to the track incrementally. Continues on segment errors to provide
2018
- * partial subtitles.
2095
+ * Returns Presentation with partially resolved tracks (no segment information).
2096
+ * Tracks contain metadata from multivariant playlist (bandwidth, resolution, codecs)
2097
+ * but segment information is added when media playlists are fetched.
2019
2098
  *
2020
- * @example
2021
- * const cleanup = loadTextTrackCues({ state, owners });
2099
+ * @param text - Raw playlist text content
2100
+ * @param unresolved - Unresolved presentation (contains URL for base URL resolution)
2101
+ * @returns Presentation with partially resolved tracks (duration is undefined)
2022
2102
  */
2023
- function loadTextTrackCues({ state, owners }) {
2024
- let currentTask = null;
2025
- let abortController = null;
2026
- let lastTrackId;
2027
- const cleanup = combineLatest([state, owners]).subscribe(async ([currentState, currentOwners]) => {
2028
- if (currentState.selectedTextTrackId !== lastTrackId) {
2029
- lastTrackId = currentState.selectedTextTrackId;
2030
- abortController?.abort();
2031
- currentTask = null;
2103
+ function parseMultivariantPlaylist(text, unresolved) {
2104
+ const baseUrl = unresolved.url;
2105
+ const lines = text.split(/\r?\n/);
2106
+ const streams = [];
2107
+ const audioRenditions = [];
2108
+ const subtitleRenditions = [];
2109
+ let pendingStreamInfo = null;
2110
+ for (const line of lines) {
2111
+ const trimmed = line.trim();
2112
+ if (!trimmed || trimmed.startsWith("#") && !trimmed.startsWith("#EXT")) continue;
2113
+ if (trimmed === "#EXTM3U" || trimmed.startsWith("#EXT-X-VERSION:") || trimmed.startsWith("#EXT-X-INDEPENDENT-SEGMENTS")) continue;
2114
+ const mediaAttrs = matchTag(trimmed, "EXT-X-MEDIA");
2115
+ if (mediaAttrs) {
2116
+ const type = mediaAttrs.get("TYPE");
2117
+ const groupId = mediaAttrs.get("GROUP-ID");
2118
+ const name = mediaAttrs.get("NAME");
2119
+ if (type === "AUDIO" && groupId && name) {
2120
+ const uri = mediaAttrs.get("URI");
2121
+ audioRenditions.push({
2122
+ groupId,
2123
+ name,
2124
+ language: mediaAttrs.get("LANGUAGE"),
2125
+ uri: uri ? resolveUrl(uri, baseUrl) : void 0,
2126
+ default: mediaAttrs.getBool("DEFAULT"),
2127
+ autoselect: mediaAttrs.getBool("AUTOSELECT")
2128
+ });
2129
+ }
2130
+ if (type === "SUBTITLES" && groupId && name) {
2131
+ const uri = mediaAttrs.get("URI");
2132
+ if (uri) subtitleRenditions.push({
2133
+ groupId,
2134
+ name,
2135
+ language: mediaAttrs.get("LANGUAGE"),
2136
+ uri: resolveUrl(uri, baseUrl),
2137
+ default: mediaAttrs.getBool("DEFAULT"),
2138
+ autoselect: mediaAttrs.getBool("AUTOSELECT"),
2139
+ forced: mediaAttrs.getBool("FORCED")
2140
+ });
2141
+ }
2142
+ continue;
2032
2143
  }
2033
- if (currentTask) return;
2034
- if (!shouldLoadTextTrackCues(currentState, currentOwners)) return;
2035
- const textTrack = getSelectedTextTrackFromOwners(currentState, currentOwners);
2036
- if (!textTrack) return;
2037
- abortController = new AbortController();
2038
- currentTask = loadTextTrackCuesTask({ currentState }, {
2039
- signal: abortController.signal,
2040
- textTrack,
2041
- state
2042
- }).finally(() => {
2043
- currentTask = null;
2044
- });
2144
+ const streamInfAttrs = matchTag(trimmed, "EXT-X-STREAM-INF");
2145
+ if (streamInfAttrs) {
2146
+ pendingStreamInfo = {
2147
+ bandwidth: streamInfAttrs.getInt("BANDWIDTH", 0),
2148
+ resolution: streamInfAttrs.getResolution("RESOLUTION"),
2149
+ codecs: streamInfAttrs.get("CODECS"),
2150
+ frameRate: streamInfAttrs.getFrameRate("FRAME-RATE"),
2151
+ audioGroupId: streamInfAttrs.get("AUDIO")
2152
+ };
2153
+ continue;
2154
+ }
2155
+ if (!trimmed.startsWith("#") && pendingStreamInfo) {
2156
+ streams.push({
2157
+ ...pendingStreamInfo,
2158
+ uri: resolveUrl(trimmed, baseUrl)
2159
+ });
2160
+ pendingStreamInfo = null;
2161
+ }
2162
+ }
2163
+ const videoStreams = [];
2164
+ const audioOnlyStreams = [];
2165
+ for (const stream of streams) {
2166
+ if (!stream.codecs) {
2167
+ videoStreams.push(stream);
2168
+ continue;
2169
+ }
2170
+ const parsedCodecs = parseCodecs(stream.codecs);
2171
+ if (stream.codecs.split(",").length === 1) if (parsedCodecs.audio && !parsedCodecs.video) audioOnlyStreams.push(stream);
2172
+ else videoStreams.push(stream);
2173
+ else videoStreams.push(stream);
2174
+ }
2175
+ const videoTracks = videoStreams.map((stream) => {
2176
+ const codecs = stream.codecs ? parseCodecs(stream.codecs) : void 0;
2177
+ const track = {
2178
+ type: "video",
2179
+ id: generateId(),
2180
+ url: stream.uri,
2181
+ bandwidth: stream.bandwidth,
2182
+ mimeType: "video/mp4",
2183
+ codecs: []
2184
+ };
2185
+ if (stream.resolution?.width !== void 0) track.width = stream.resolution.width;
2186
+ if (stream.resolution?.height !== void 0) track.height = stream.resolution.height;
2187
+ if (codecs?.video) track.codecs = [codecs.video];
2188
+ if (stream.frameRate) track.frameRate = stream.frameRate;
2189
+ if (stream.audioGroupId) track.audioGroupId = stream.audioGroupId;
2190
+ return track;
2045
2191
  });
2046
- return () => {
2047
- abortController?.abort();
2048
- cleanup();
2192
+ const audioOnlyTracks = audioOnlyStreams.map((stream) => {
2193
+ const codecs = stream.codecs ? parseCodecs(stream.codecs) : void 0;
2194
+ return {
2195
+ type: "audio",
2196
+ id: generateId(),
2197
+ url: stream.uri,
2198
+ bandwidth: stream.bandwidth,
2199
+ mimeType: "audio/mp4",
2200
+ codecs: codecs?.audio ? [codecs.audio] : [],
2201
+ groupId: stream.audioGroupId || "default",
2202
+ name: "Default",
2203
+ sampleRate: 48e3,
2204
+ channels: 2
2205
+ };
2206
+ });
2207
+ const audioTracks = [...audioRenditions.map((rendition) => {
2208
+ let audioCodecs;
2209
+ for (const stream of streams) if (stream.audioGroupId === rendition.groupId && stream.codecs) {
2210
+ const codecs = parseCodecs(stream.codecs);
2211
+ if (codecs.audio) {
2212
+ audioCodecs = [codecs.audio];
2213
+ break;
2214
+ }
2215
+ }
2216
+ const track = {
2217
+ type: "audio",
2218
+ id: generateId(),
2219
+ url: rendition.uri ?? "",
2220
+ groupId: rendition.groupId,
2221
+ name: rendition.name,
2222
+ mimeType: "audio/mp4",
2223
+ bandwidth: 0,
2224
+ sampleRate: 48e3,
2225
+ channels: 2,
2226
+ codecs: []
2227
+ };
2228
+ if (rendition.language) track.language = rendition.language;
2229
+ if (audioCodecs) track.codecs = audioCodecs;
2230
+ if (rendition.default) track.default = rendition.default;
2231
+ if (rendition.autoselect) track.autoselect = rendition.autoselect;
2232
+ return track;
2233
+ }), ...audioOnlyTracks];
2234
+ const textTracks = subtitleRenditions.map((rendition) => {
2235
+ const track = {
2236
+ type: "text",
2237
+ id: generateId(),
2238
+ url: rendition.uri,
2239
+ groupId: rendition.groupId,
2240
+ label: rendition.name,
2241
+ kind: "subtitles",
2242
+ mimeType: "text/vtt",
2243
+ bandwidth: 0
2244
+ };
2245
+ if (rendition.language) track.language = rendition.language;
2246
+ if (rendition.default && rendition.autoselect) track.default = true;
2247
+ if (rendition.autoselect) track.autoselect = rendition.autoselect;
2248
+ if (rendition.forced) track.forced = rendition.forced;
2249
+ return track;
2250
+ });
2251
+ const selectionSets = [];
2252
+ if (videoTracks.length > 0) {
2253
+ const videoSwitchingSet = {
2254
+ id: generateId(),
2255
+ type: "video",
2256
+ tracks: videoTracks
2257
+ };
2258
+ const videoSelectionSet = {
2259
+ id: generateId(),
2260
+ type: "video",
2261
+ switchingSets: [videoSwitchingSet]
2262
+ };
2263
+ selectionSets.push(videoSelectionSet);
2264
+ }
2265
+ if (audioTracks.length > 0) {
2266
+ const audioSwitchingSet = {
2267
+ id: generateId(),
2268
+ type: "audio",
2269
+ tracks: audioTracks
2270
+ };
2271
+ const audioSelectionSet = {
2272
+ id: generateId(),
2273
+ type: "audio",
2274
+ switchingSets: [audioSwitchingSet]
2275
+ };
2276
+ selectionSets.push(audioSelectionSet);
2277
+ }
2278
+ if (textTracks.length > 0) {
2279
+ const textSwitchingSet = {
2280
+ id: generateId(),
2281
+ type: "text",
2282
+ tracks: textTracks
2283
+ };
2284
+ const textSelectionSet = {
2285
+ id: generateId(),
2286
+ type: "text",
2287
+ switchingSets: [textSwitchingSet]
2288
+ };
2289
+ selectionSets.push(textSelectionSet);
2290
+ }
2291
+ return {
2292
+ id: generateId(),
2293
+ url: unresolved.url,
2294
+ startTime: 0,
2295
+ selectionSets
2049
2296
  };
2050
2297
  }
2051
2298
 
2052
2299
  //#endregion
2053
- //#region ../spf/dist/dev/dom/features/track-current-time.js
2300
+ //#region ../spf/dist/dev/core/features/resolve-presentation.js
2054
2301
  /**
2055
- * Track current playback position from the media element.
2056
- *
2057
- * Mirrors `mediaElement.currentTime` into reactive state on:
2058
- * - `timeupdate` fires during playback (~4 Hz)
2059
- * - `seeking` — fires when a seek begins; per spec, `currentTime` is
2060
- * already at the new position when this event dispatches, so buffer
2061
- * management can react immediately rather than waiting for `timeupdate`,
2062
- * which does not fire while paused.
2302
+ * Type guard to check if presentation is unresolved.
2303
+ */
2304
+ function isUnresolved(presentation) {
2305
+ return presentation !== void 0 && "url" in presentation && !("id" in presentation);
2306
+ }
2307
+ function canResolve$1(state) {
2308
+ return isUnresolved(state.presentation);
2309
+ }
2310
+ /**
2311
+ * Determines if resolution conditions are met based on preload policy and playback state.
2063
2312
  *
2064
- * Also syncs immediately when a media element becomes available.
2313
+ * Resolution conditions:
2314
+ * - State-driven: preload is 'auto' or 'metadata'
2315
+ * - Playback-driven: playbackInitiated is true
2065
2316
  *
2066
- * @example
2067
- * const cleanup = trackCurrentTime({ state, owners });
2317
+ * @param state - Current presentation state
2318
+ * @returns true if resolution conditions are met
2068
2319
  */
2069
- function trackCurrentTime({ state, owners }) {
2070
- let lastMediaElement;
2071
- let removeListeners = null;
2072
- const unsubscribe = owners.subscribe((currentOwners) => {
2073
- const { mediaElement } = currentOwners;
2074
- if (mediaElement === lastMediaElement) return;
2075
- removeListeners?.();
2076
- removeListeners = null;
2077
- lastMediaElement = mediaElement;
2078
- if (!mediaElement) return;
2079
- state.patch({ currentTime: mediaElement.currentTime });
2080
- const sync = () => state.patch({ currentTime: mediaElement.currentTime });
2081
- const removeTimeupdate = listen(mediaElement, "timeupdate", sync);
2082
- const removeSeeking = listen(mediaElement, "seeking", sync);
2083
- removeListeners = () => {
2084
- removeTimeupdate();
2085
- removeSeeking();
2086
- };
2087
- });
2088
- return () => {
2089
- removeListeners?.();
2090
- unsubscribe();
2091
- };
2320
+ function shouldResolve(state) {
2321
+ const { preload, playbackInitiated } = state;
2322
+ return ["auto", "metadata"].includes(preload) || !!playbackInitiated;
2092
2323
  }
2093
-
2094
- //#endregion
2095
- //#region ../spf/dist/dev/dom/features/track-playback-initiated.js
2096
2324
  /**
2097
- * Track whether playback has been initiated by the user.
2325
+ * Resolves unresolved presentations using reactive composition.
2326
+ *
2327
+ * Triggers resolution when:
2328
+ * - State-driven: Unresolved presentation + preload allows (auto/metadata)
2329
+ * - Playback-driven: playbackInitiated is true
2098
2330
  *
2099
- * Sets `state.playbackInitiated = true` when the media element fires a `play`
2100
- * event (via `element.play()`, native controls, or autoplay) and simultaneously
2101
- * dispatches `{ type: 'play' }` to the event stream so `resolvePresentation`
2102
- * can react.
2331
+ * @example
2332
+ * ```ts
2333
+ * const state = signal({ presentation: undefined, preload: 'auto', playbackInitiated: false });
2103
2334
  *
2104
- * Resets `state.playbackInitiated = false` when `presentation.url` changes,
2105
- * so a new source with `preload="none"` won't load segments until play is
2106
- * triggered again.
2335
+ * const cleanup = resolvePresentation({ state });
2107
2336
  *
2108
- * This flag is used by `shouldLoadSegments` to allow segment loading after
2109
- * play is initiated regardless of the initial `preload` setting — `preload`
2110
- * is a startup hint, not a runtime gate.
2337
+ * // State-driven: resolves immediately when preload allows
2338
+ * state.set({ ...state.get(), presentation: { url: 'http://example.com/playlist.m3u8' } });
2111
2339
  *
2112
- * @example
2113
- * const cleanup = trackPlaybackInitiated({ state, owners, events });
2340
+ * // Playback-driven: resolves when playbackInitiated is set
2341
+ * state.set({ ...state.get(), preload: 'none', presentation: { url: '...' }, playbackInitiated: true });
2342
+ * ```
2114
2343
  */
2115
- function trackPlaybackInitiated({ state, owners, events }) {
2116
- let lastMediaElement;
2117
- let removeListener = null;
2118
- let lastPresentationUrl;
2119
- const unsubscribeState = state.subscribe((currentState) => {
2120
- const url = currentState.presentation?.url;
2121
- if (url !== lastPresentationUrl) {
2122
- if (lastPresentationUrl !== void 0) state.patch({ playbackInitiated: false });
2123
- lastPresentationUrl = url;
2124
- }
2125
- });
2126
- const unsubscribeOwners = owners.subscribe((currentOwners) => {
2127
- const { mediaElement } = currentOwners;
2128
- if (mediaElement === lastMediaElement) return;
2129
- removeListener?.();
2130
- removeListener = null;
2131
- lastMediaElement = mediaElement;
2132
- if (!mediaElement) return;
2133
- removeListener = listen(mediaElement, "play", () => {
2134
- state.patch({ playbackInitiated: true });
2135
- events.dispatch({ type: "play" });
2344
+ function resolvePresentation({ state }) {
2345
+ const canResolveSignal = computed(() => canResolve$1(state.get()));
2346
+ const shouldResolveSignal = computed(() => shouldResolve(state.get()));
2347
+ let resolving = false;
2348
+ let abortController = null;
2349
+ const cleanupEffect = effect(() => {
2350
+ if (!canResolveSignal.get() || !shouldResolveSignal.get() || resolving) return;
2351
+ const presentation = state.get().presentation;
2352
+ resolving = true;
2353
+ abortController = new AbortController();
2354
+ fetchResolvable(presentation, { signal: abortController.signal }).then((response) => getResponseText(response)).then((text) => {
2355
+ update(state, { presentation: parseMultivariantPlaylist(text, presentation) });
2356
+ }).catch((error) => {
2357
+ if (error instanceof Error && error.name === "AbortError") return;
2358
+ throw error;
2359
+ }).finally(() => {
2360
+ resolving = false;
2361
+ abortController = null;
2136
2362
  });
2137
2363
  });
2138
2364
  return () => {
2139
- removeListener?.();
2140
- unsubscribeState();
2141
- unsubscribeOwners();
2365
+ abortController?.abort();
2366
+ cleanupEffect();
2142
2367
  };
2143
2368
  }
2144
2369
 
2145
2370
  //#endregion
2146
- //#region ../spf/dist/dev/dom/media/append-segment.js
2371
+ //#region ../spf/dist/dev/core/hls/parse-media-playlist.js
2147
2372
  /**
2148
- * Append media data to a SourceBuffer.
2373
+ * Parse HLS media playlist and resolve track with segments.
2149
2374
  *
2150
- * Accepts either a full ArrayBuffer (single append) or an AsyncIterable of
2151
- * Uint8Array chunks (one append per chunk, in order). Waits for `updateend`
2152
- * between each call so appends are serialized correctly.
2375
+ * Takes an unresolved track (from multivariant playlist) and media playlist text,
2376
+ * returns a HAM-compliant resolved track with segments.
2153
2377
  *
2154
- * Errors from the SourceBuffer (`error` event) or from the iterable are
2155
- * propagated as rejections.
2378
+ * @param text - Media playlist text content
2379
+ * @param unresolved - Unresolved track from parseMultivariantPlaylist
2380
+ * @returns Resolved track with segments (type inferred from input)
2156
2381
  */
2157
- async function appendSegment(sourceBuffer, data, signal) {
2158
- if (data instanceof ArrayBuffer) await appendChunk(sourceBuffer, data);
2159
- else try {
2160
- for await (const chunk of data) {
2161
- if (signal?.aborted) throw signal.reason ?? new DOMException("Aborted", "AbortError");
2162
- await appendChunk(sourceBuffer, chunk);
2163
- }
2164
- } catch (e) {
2165
- if (e instanceof DOMException && e.name === "AbortError" && !sourceBuffer.updating) try {
2166
- sourceBuffer.abort();
2167
- } catch {}
2168
- throw e;
2169
- }
2170
- }
2171
- async function appendChunk(sourceBuffer, data) {
2172
- if (sourceBuffer.updating) await new Promise((resolve) => {
2173
- const onUpdateEnd = () => {
2174
- sourceBuffer.removeEventListener("updateend", onUpdateEnd);
2175
- resolve();
2176
- };
2177
- sourceBuffer.addEventListener("updateend", onUpdateEnd);
2178
- });
2179
- return new Promise((resolve, reject) => {
2180
- const onUpdateEnd = () => {
2181
- cleanup();
2182
- resolve();
2183
- };
2184
- const onError = (event) => {
2185
- cleanup();
2186
- reject(/* @__PURE__ */ new Error(`SourceBuffer append error: ${event.type}`));
2187
- };
2188
- const cleanup = () => {
2189
- sourceBuffer.removeEventListener("updateend", onUpdateEnd);
2190
- sourceBuffer.removeEventListener("error", onError);
2191
- };
2192
- sourceBuffer.addEventListener("updateend", onUpdateEnd);
2193
- sourceBuffer.addEventListener("error", onError);
2194
- try {
2195
- sourceBuffer.appendBuffer(data);
2196
- } catch (error) {
2197
- cleanup();
2198
- reject(error);
2382
+ function parseMediaPlaylist(text, unresolved) {
2383
+ const lines = text.split(/\r?\n/);
2384
+ const baseUrl = unresolved.url;
2385
+ const segments = [];
2386
+ let initSegmentUrl;
2387
+ let initSegmentByteRange;
2388
+ let currentDuration = 0;
2389
+ let currentByteRange;
2390
+ let currentTime = 0;
2391
+ let segmentIndex = 0;
2392
+ let previousByteRangeEnd;
2393
+ for (const line of lines) {
2394
+ const trimmed = line.trim();
2395
+ if (!trimmed || trimmed.startsWith("#") && !trimmed.startsWith("#EXT")) continue;
2396
+ if (trimmed === "#EXTM3U" || trimmed.startsWith("#EXT-X-VERSION:") || trimmed.startsWith("#EXT-X-TARGETDURATION:") || trimmed.startsWith("#EXT-X-PLAYLIST-TYPE:") || trimmed.startsWith("#EXT-X-INDEPENDENT-SEGMENTS")) continue;
2397
+ const mapAttrs = matchTag(trimmed, "EXT-X-MAP");
2398
+ if (mapAttrs) {
2399
+ const uri = mapAttrs.get("URI");
2400
+ if (uri) {
2401
+ initSegmentUrl = resolveUrl(uri, baseUrl);
2402
+ const byteRangeStr = mapAttrs.get("BYTERANGE");
2403
+ if (byteRangeStr) initSegmentByteRange = parseByteRange(byteRangeStr, 0) ?? void 0;
2404
+ }
2405
+ continue;
2199
2406
  }
2200
- });
2201
- }
2202
-
2203
- //#endregion
2204
- //#region ../spf/dist/dev/dom/media/buffer-flusher.js
2205
- /**
2206
- * Buffer flusher helper (P12)
2207
- *
2208
- * Removes a time range from a SourceBuffer to manage memory.
2209
- */
2210
- /**
2211
- * Remove a time range from a SourceBuffer.
2212
- *
2213
- * Waits for the SourceBuffer to be ready (not updating), then removes
2214
- * the specified range. Returns a promise that resolves when removal completes.
2215
- *
2216
- * @param sourceBuffer - The SourceBuffer to remove data from
2217
- * @param start - Start of the time range to remove (seconds)
2218
- * @param end - End of the time range to remove (seconds)
2219
- * @returns Promise that resolves when removal completes
2220
- *
2221
- * @example
2222
- * await flushBuffer(videoSourceBuffer, 0, 30);
2223
- */
2224
- async function flushBuffer(sourceBuffer, start, end) {
2225
- if (sourceBuffer.updating) await new Promise((resolve) => {
2226
- const onUpdateEnd = () => {
2227
- sourceBuffer.removeEventListener("updateend", onUpdateEnd);
2228
- resolve();
2229
- };
2230
- sourceBuffer.addEventListener("updateend", onUpdateEnd);
2231
- });
2232
- return new Promise((resolve, reject) => {
2233
- const onUpdateEnd = () => {
2234
- cleanup();
2235
- resolve();
2236
- };
2237
- const onError = (event) => {
2238
- cleanup();
2239
- reject(/* @__PURE__ */ new Error(`SourceBuffer remove error: ${event.type}`));
2240
- };
2241
- const cleanup = () => {
2242
- sourceBuffer.removeEventListener("updateend", onUpdateEnd);
2243
- sourceBuffer.removeEventListener("error", onError);
2244
- };
2245
- sourceBuffer.addEventListener("updateend", onUpdateEnd);
2246
- sourceBuffer.addEventListener("error", onError);
2247
- try {
2248
- sourceBuffer.remove(start, end);
2249
- } catch (error) {
2250
- cleanup();
2251
- reject(error);
2407
+ if (trimmed.startsWith("#EXTINF:")) {
2408
+ currentDuration = parseExtInfDuration(trimmed.slice(8));
2409
+ continue;
2252
2410
  }
2253
- });
2254
- }
2255
-
2256
- //#endregion
2257
- //#region ../spf/dist/dev/core/features/calculate-presentation-duration.js
2258
- /**
2259
- * Check if we can calculate presentation duration (have required data).
2260
- */
2261
- function canCalculateDuration(state) {
2262
- if (!state.presentation) return false;
2263
- return !!(state.selectedVideoTrackId || state.selectedAudioTrackId);
2264
- }
2265
- /**
2266
- * Check if we should calculate presentation duration (conditions met).
2267
- */
2268
- function shouldCalculateDuration(state) {
2269
- if (!canCalculateDuration(state)) return false;
2270
- const { presentation } = state;
2271
- if (presentation.duration !== void 0) return false;
2272
- const videoTrack = state.selectedVideoTrackId ? getSelectedTrack(state, "video") : void 0;
2273
- const audioTrack = state.selectedAudioTrackId ? getSelectedTrack(state, "audio") : void 0;
2274
- return !!(videoTrack && isResolvedTrack(videoTrack) || audioTrack && isResolvedTrack(audioTrack));
2275
- }
2276
- /**
2277
- * Get duration from the first resolved track (prefer video, fallback to audio).
2278
- */
2279
- function getDurationFromResolvedTracks(state) {
2280
- const videoTrack = state.selectedVideoTrackId ? getSelectedTrack(state, "video") : void 0;
2281
- if (videoTrack && isResolvedTrack(videoTrack)) return videoTrack.duration;
2282
- const audioTrack = state.selectedAudioTrackId ? getSelectedTrack(state, "audio") : void 0;
2283
- if (audioTrack && isResolvedTrack(audioTrack)) return audioTrack.duration;
2284
- }
2285
- /**
2286
- * Calculate and set presentation duration from resolved tracks.
2287
- */
2288
- function calculatePresentationDuration({ state }) {
2289
- return combineLatest([state]).subscribe(([currentState]) => {
2290
- if (!shouldCalculateDuration(currentState)) return;
2291
- const duration = getDurationFromResolvedTracks(currentState);
2292
- if (duration === void 0 || !Number.isFinite(duration)) return;
2293
- const { presentation } = currentState;
2294
- state.patch({ presentation: {
2295
- ...presentation,
2296
- duration
2297
- } });
2298
- });
2411
+ if (trimmed.startsWith("#EXT-X-BYTERANGE:")) {
2412
+ currentByteRange = parseByteRange(trimmed.slice(17), previousByteRangeEnd) ?? void 0;
2413
+ continue;
2414
+ }
2415
+ if (trimmed === "#EXT-X-ENDLIST") continue;
2416
+ if (!trimmed.startsWith("#") && currentDuration > 0) {
2417
+ const segment = {
2418
+ id: `segment-${segmentIndex}`,
2419
+ url: resolveUrl(trimmed, baseUrl),
2420
+ duration: currentDuration,
2421
+ startTime: currentTime
2422
+ };
2423
+ if (currentByteRange) {
2424
+ segment.byteRange = currentByteRange;
2425
+ previousByteRangeEnd = currentByteRange.end + 1;
2426
+ } else previousByteRangeEnd = void 0;
2427
+ segments.push(segment);
2428
+ currentTime += currentDuration;
2429
+ segmentIndex++;
2430
+ currentDuration = 0;
2431
+ currentByteRange = void 0;
2432
+ }
2433
+ }
2434
+ const totalDuration = currentTime;
2435
+ const initialization = unresolved.type === "text" && !initSegmentUrl ? void 0 : initSegmentUrl ? {
2436
+ url: initSegmentUrl,
2437
+ ...initSegmentByteRange ? { byteRange: initSegmentByteRange } : {}
2438
+ } : { url: "" };
2439
+ return {
2440
+ ...unresolved,
2441
+ startTime: 0,
2442
+ duration: totalDuration,
2443
+ segments,
2444
+ initialization
2445
+ };
2299
2446
  }
2300
2447
 
2301
2448
  //#endregion
@@ -2324,7 +2471,7 @@ var Task = class {
2324
2471
  this.#runFn = runFn;
2325
2472
  const rawId = config?.id;
2326
2473
  this.id = typeof rawId === "function" ? rawId() : rawId ?? generateId();
2327
- this.#signal = config?.signal ? AbortSignal.any([this.#abortController.signal, config.signal]) : this.#abortController.signal;
2474
+ this.#signal = config?.signal ? anyAbortSignal([this.#abortController.signal, config.signal]) : this.#abortController.signal;
2328
2475
  }
2329
2476
  get status() {
2330
2477
  return this.#status;
@@ -2424,19 +2571,6 @@ function canResolve(state, config) {
2424
2571
  return !isResolvedTrack(track);
2425
2572
  }
2426
2573
  /**
2427
- * Determines if track resolution conditions are met.
2428
- *
2429
- * Currently always returns true - conditions are checked by canResolveTrack()
2430
- * and resolving flag. Kept as placeholder for future conditional logic.
2431
- *
2432
- * @param state - Current track resolution state
2433
- * @param event - Current action/event
2434
- * @returns true (conditions checked elsewhere)
2435
- */
2436
- function shouldResolve(_state, _event) {
2437
- return true;
2438
- }
2439
- /**
2440
2574
  * Updates a track within a presentation (immutably).
2441
2575
  * Generic - works for video, audio, or text tracks.
2442
2576
  */
@@ -2456,26 +2590,29 @@ function updateTrackInPresentation(presentation, resolvedTrack) {
2456
2590
  /**
2457
2591
  * Resolves unresolved tracks using reactive composition.
2458
2592
  *
2459
- * The subscribe closure is pure scheduling logic: it checks conditions and
2460
- * creates a Task for the selected track when appropriate. The ConcurrentRunner
2461
- * handles all concurrency concerns — deduplication, parallel execution, and
2462
- * cleanup.
2593
+ * Reacts to state changes and schedules fetch tasks via ConcurrentRunner when
2594
+ * a selected track is unresolved. The ConcurrentRunner handles deduplication,
2595
+ * parallel execution, and cleanup.
2463
2596
  *
2464
2597
  * Generic version that works for video, audio, or text tracks based on config.
2465
2598
  * Type parameter T is inferred from config.type (use 'as const' for inference).
2466
2599
  */
2467
- function resolveTrack({ state, events }, config) {
2600
+ function resolveTrack({ state }, config) {
2468
2601
  const runner = new ConcurrentRunner();
2469
- const cleanup = combineLatest([state, events]).subscribe(([currentState, event]) => {
2470
- if (!canResolve(currentState, config) || !shouldResolve(currentState, event)) return;
2602
+ const cleanup = effect(() => {
2603
+ const currentState = state.get();
2604
+ if (!canResolve(currentState, config)) return;
2471
2605
  const track = getSelectedTrack(currentState, config.type);
2472
2606
  if (!track) return;
2473
2607
  const resolvedTrack = track;
2474
2608
  runner.schedule(new Task(async (signal) => {
2475
2609
  const mediaTrack = parseMediaPlaylist(await getResponseText(await fetchResolvable(resolvedTrack, { signal })), resolvedTrack);
2476
- const latestPresentation = state.current.presentation;
2477
- const updatedPresentation = updateTrackInPresentation(latestPresentation, mediaTrack);
2478
- state.patch({ presentation: updatedPresentation });
2610
+ const latest = state.get();
2611
+ const updatedPresentation = updateTrackInPresentation(latest.presentation, mediaTrack);
2612
+ state.set({
2613
+ ...latest,
2614
+ presentation: updatedPresentation
2615
+ });
2479
2616
  }, { id: track.id }));
2480
2617
  });
2481
2618
  return () => {
@@ -2558,19 +2695,11 @@ function shouldSelectTrack(state, config) {
2558
2695
  * );
2559
2696
  */
2560
2697
  function selectVideoTrack({ state }, config = { type: "video" }) {
2561
- let selecting = false;
2562
- return state.subscribe(async (currentState) => {
2563
- if (!canSelectTrack(currentState, config) || !shouldSelectTrack(currentState, config) || selecting) return;
2564
- try {
2565
- selecting = true;
2566
- const selectedTrackId = currentState.presentation?.selectionSets.find(({ type }) => type === config.type)?.switchingSets[0]?.tracks[0]?.id;
2567
- if (selectedTrackId) {
2568
- const selectedTrackKey = SelectedTrackIdKeyByType[config.type];
2569
- state.patch({ [selectedTrackKey]: selectedTrackId });
2570
- }
2571
- } finally {
2572
- selecting = false;
2573
- }
2698
+ return effect(() => {
2699
+ const currentState = state.get();
2700
+ if (!canSelectTrack(currentState, config) || !shouldSelectTrack(currentState, config)) return;
2701
+ const selectedTrackId = currentState.presentation?.selectionSets.find(({ type }) => type === config.type)?.switchingSets[0]?.tracks[0]?.id;
2702
+ if (selectedTrackId) update(state, { [SelectedTrackIdKeyByType[config.type]]: selectedTrackId });
2574
2703
  });
2575
2704
  }
2576
2705
  /**
@@ -2589,16 +2718,11 @@ function selectVideoTrack({ state }, config = { type: "video" }) {
2589
2718
  * );
2590
2719
  */
2591
2720
  function selectAudioTrack({ state }, config = { type: "audio" }) {
2592
- let selecting = false;
2593
- return state.subscribe(async (currentState) => {
2594
- if (!canSelectTrack(currentState, config) || !shouldSelectTrack(currentState, config) || selecting) return;
2595
- try {
2596
- selecting = true;
2597
- const selectedTrackId = currentState.presentation?.selectionSets.find(({ type }) => type === "audio")?.switchingSets[0]?.tracks[0]?.id;
2598
- if (selectedTrackId) state.patch({ selectedAudioTrackId: selectedTrackId });
2599
- } finally {
2600
- selecting = false;
2601
- }
2721
+ return effect(() => {
2722
+ const currentState = state.get();
2723
+ if (!canSelectTrack(currentState, config) || !shouldSelectTrack(currentState, config)) return;
2724
+ const selectedTrackId = currentState.presentation?.selectionSets.find(({ type }) => type === "audio")?.switchingSets[0]?.tracks[0]?.id;
2725
+ if (selectedTrackId) update(state, { selectedAudioTrackId: selectedTrackId });
2602
2726
  });
2603
2727
  }
2604
2728
  /**
@@ -2614,16 +2738,33 @@ function selectAudioTrack({ state }, config = { type: "audio" }) {
2614
2738
  * const cleanup = selectTextTrack({ state, owners, events }, {});
2615
2739
  */
2616
2740
  function selectTextTrack({ state }, config = { type: "text" }) {
2617
- let selecting = false;
2618
- return state.subscribe(async (currentState) => {
2619
- if (!canSelectTrack(currentState, config) || !shouldSelectTrack(currentState, config) || selecting) return;
2620
- try {
2621
- selecting = true;
2622
- const selectedTextTrackId = pickTextTrack(currentState.presentation, config);
2623
- if (selectedTextTrackId) state.patch({ selectedTextTrackId });
2624
- } finally {
2625
- selecting = false;
2626
- }
2741
+ return effect(() => {
2742
+ const currentState = state.get();
2743
+ if (!canSelectTrack(currentState, config) || !shouldSelectTrack(currentState, config)) return;
2744
+ const selectedTextTrackId = pickTextTrack(currentState.presentation, config);
2745
+ if (selectedTextTrackId) update(state, { selectedTextTrackId });
2746
+ });
2747
+ }
2748
+
2749
+ //#endregion
2750
+ //#region ../spf/dist/dev/core/features/sync-preload-attribute.js
2751
+ /**
2752
+ * Syncs preload attribute from mediaElement to state.
2753
+ *
2754
+ * Watches the owners signal for mediaElement changes and copies the
2755
+ * preload attribute to state when no explicit value has been set.
2756
+ * An explicit value (set via SpfMedia.preload) always wins.
2757
+ *
2758
+ * @example
2759
+ * const cleanup = syncPreloadAttribute({ state, owners });
2760
+ */
2761
+ function syncPreloadAttribute({ state, owners }) {
2762
+ const mediaElement = computed(() => owners.get().mediaElement);
2763
+ return effect(() => {
2764
+ if (state.get().preload !== void 0) return;
2765
+ const preload = mediaElement.get()?.preload || void 0;
2766
+ if (preload === void 0) return;
2767
+ update(state, { preload });
2627
2768
  });
2628
2769
  }
2629
2770
 
@@ -2640,7 +2781,7 @@ function isLastSegmentAppended(segments, actor) {
2640
2781
  if (segments.length === 0) return true;
2641
2782
  const lastSeg = segments[segments.length - 1];
2642
2783
  if (!lastSeg) return false;
2643
- return actor?.snapshot.context.segments.some((s) => s.id === lastSeg.id && !s.partial) ?? false;
2784
+ return actor?.snapshot.get().context.segments.some((s) => s.id === lastSeg.id && !s.partial) ?? false;
2644
2785
  }
2645
2786
  /**
2646
2787
  * Check if the last segment has been appended for each selected track.
@@ -2672,15 +2813,15 @@ function canEndStream(state, owners) {
2672
2813
  */
2673
2814
  function shouldEndStream(state, owners) {
2674
2815
  if (!canEndStream(state, owners)) return false;
2675
- const { mediaSource, mediaElement } = owners;
2676
- if (mediaSource.readyState !== "open") return false;
2816
+ const { mediaElement } = owners;
2817
+ if ((owners.mediaSourceReadyState?.get() ?? owners.mediaSource?.readyState) !== "open") return false;
2677
2818
  if (mediaElement && mediaElement.readyState < HTMLMediaElement.HAVE_METADATA) return false;
2678
2819
  const hasVideoTrack = !!state.selectedVideoTrackId;
2679
2820
  const hasAudioTrack = !!state.selectedAudioTrackId;
2680
2821
  if (hasVideoTrack && !owners.videoBuffer) return false;
2681
2822
  if (hasAudioTrack && !owners.audioBuffer) return false;
2682
- if (owners.videoBufferActor?.snapshot.status === "updating") return false;
2683
- if (owners.audioBufferActor?.snapshot.status === "updating") return false;
2823
+ if (owners.videoBufferActor?.snapshot.get().status === "updating") return false;
2824
+ if (owners.audioBufferActor?.snapshot.get().status === "updating") return false;
2684
2825
  if (!hasLastSegmentLoaded(state, owners)) return false;
2685
2826
  if (mediaElement) {
2686
2827
  const videoTrack = hasVideoTrack ? getSelectedTrack(state, "video") : void 0;
@@ -2699,13 +2840,18 @@ function shouldEndStream(state, owners) {
2699
2840
  * aligned with the same abstraction that owns all buffer operations.
2700
2841
  */
2701
2842
  function waitForSourceBuffersReady$1(owners) {
2702
- const updatingActors = [owners.videoBufferActor, owners.audioBufferActor].filter((actor) => actor !== void 0 && actor.snapshot.status === "updating");
2843
+ const updatingActors = [owners.videoBufferActor, owners.audioBufferActor].filter((actor) => actor !== void 0 && actor.snapshot.get().status === "updating");
2703
2844
  if (updatingActors.length === 0) return Promise.resolve();
2704
2845
  return Promise.all(updatingActors.map((actor) => new Promise((resolve) => {
2705
- const unsub = actor.subscribe((snapshot) => {
2706
- if (snapshot.status !== "updating") {
2707
- unsub();
2708
- resolve();
2846
+ let cleanup;
2847
+ let resolved = false;
2848
+ cleanup = effect(() => {
2849
+ if (actor.snapshot.get().status !== "updating") {
2850
+ if (!resolved) {
2851
+ resolved = true;
2852
+ resolve();
2853
+ }
2854
+ queueMicrotask(() => cleanup?.());
2709
2855
  }
2710
2856
  });
2711
2857
  }))).then(() => void 0);
@@ -2747,64 +2893,165 @@ const endOfStreamTask = async ({ currentOwners }, _context) => {
2747
2893
  * and MediaSource.duration updates.
2748
2894
  */
2749
2895
  function endOfStream({ state, owners }) {
2896
+ const shouldEnd = computed(() => shouldEndStream(state.get(), owners.get()));
2750
2897
  let hasEnded = false;
2751
- let destroyed = false;
2752
- const activeActorUnsubs = [];
2753
- const runEvaluate = async () => {
2754
- if (destroyed) return;
2755
- const currentState = state.current;
2756
- const currentOwners = owners.current;
2898
+ return effect(() => {
2899
+ if (!shouldEnd.get()) return;
2900
+ const currentOwners = owners.get();
2757
2901
  if (hasEnded) {
2758
- if (currentOwners.mediaSource?.readyState !== "open") return;
2902
+ if ((currentOwners.mediaSourceReadyState?.get() ?? currentOwners.mediaSource?.readyState) !== "open") return;
2759
2903
  hasEnded = false;
2760
2904
  }
2761
- if (!shouldEndStream(currentState, currentOwners)) return;
2762
2905
  hasEnded = true;
2763
- try {
2764
- await endOfStreamTask({ currentOwners }, {});
2765
- } catch (error) {
2766
- console.error("Failed to call endOfStream:", error);
2767
- }
2768
- };
2769
- const cleanupOwners = owners.subscribe((currentOwners) => {
2770
- activeActorUnsubs.forEach((u) => u());
2771
- activeActorUnsubs.length = 0;
2772
- for (const actor of [currentOwners.videoBufferActor, currentOwners.audioBufferActor]) {
2773
- if (!actor) continue;
2774
- let isFirst = true;
2775
- activeActorUnsubs.push(actor.subscribe(() => {
2776
- if (isFirst) {
2777
- isFirst = false;
2778
- return;
2779
- }
2780
- runEvaluate();
2781
- }));
2782
- }
2906
+ endOfStreamTask({ currentOwners }, {}).catch((error) => console.error("Failed to call endOfStream:", error));
2783
2907
  });
2784
- const cleanupCombineLatest = combineLatest([state, owners]).subscribe(async () => runEvaluate());
2785
- return () => {
2786
- destroyed = true;
2787
- activeActorUnsubs.forEach((u) => u());
2788
- cleanupOwners();
2789
- cleanupCombineLatest();
2790
- };
2791
2908
  }
2792
2909
 
2793
2910
  //#endregion
2794
- //#region ../spf/dist/dev/dom/features/setup-mediasource.js
2911
+ //#region ../spf/dist/dev/dom/media/mediasource-setup.js
2912
+ /**
2913
+ * MediaSource Setup
2914
+ *
2915
+ * Utilities for creating and configuring MediaSource/ManagedMediaSource
2916
+ * for MSE (Media Source Extensions) playback.
2917
+ *
2918
+ * Global ManagedMediaSource types are defined in ./mediasource.d.ts
2919
+ */
2920
+ /**
2921
+ * Check if MediaSource API is supported.
2922
+ */
2923
+ function supportsMediaSource() {
2924
+ return typeof MediaSource !== "undefined";
2925
+ }
2926
+ /**
2927
+ * Check if ManagedMediaSource API is supported.
2928
+ * ManagedMediaSource is a newer Safari API with better lifecycle management.
2929
+ */
2930
+ function supportsManagedMediaSource() {
2931
+ return typeof ManagedMediaSource !== "undefined";
2932
+ }
2933
+ /**
2934
+ * Create a MediaSource or ManagedMediaSource instance.
2935
+ *
2936
+ * @param options - Creation options
2937
+ * @returns A MediaSource or ManagedMediaSource instance
2938
+ * @throws Error if no MediaSource API is available
2939
+ *
2940
+ * @example
2941
+ * const mediaSource = createMediaSource();
2942
+ * const mediaElement = document.querySelector('video');
2943
+ * attachMediaSource(mediaSource, mediaElement);
2944
+ */
2945
+ function createMediaSource(options = {}) {
2946
+ const { preferManaged = false } = options;
2947
+ if (preferManaged && supportsManagedMediaSource()) return new ManagedMediaSource();
2948
+ if (supportsMediaSource()) return new MediaSource();
2949
+ throw new Error("MediaSource API is not supported");
2950
+ }
2951
+ /**
2952
+ * Attach a MediaSource to an HTMLMediaElement.
2953
+ *
2954
+ * Uses srcObject for ManagedMediaSource (Safari), or createObjectURL for regular MediaSource.
2955
+ *
2956
+ * @param mediaSource - The MediaSource to attach
2957
+ * @param mediaElement - The media element to attach to
2958
+ * @returns Object with URL and detach function
2959
+ *
2960
+ * @example
2961
+ * const mediaSource = createMediaSource();
2962
+ * const { detach } = attachMediaSource(mediaSource, videoElement);
2963
+ * // Use mediaSource...
2964
+ * // Later, to clean up:
2965
+ * detach();
2966
+ */
2967
+ function attachMediaSource(mediaSource, mediaElement) {
2968
+ if (supportsManagedMediaSource() && mediaSource instanceof ManagedMediaSource) {
2969
+ mediaElement.disableRemotePlayback = true;
2970
+ mediaElement.srcObject = mediaSource;
2971
+ const detach = () => {
2972
+ mediaElement.srcObject = null;
2973
+ mediaElement.load();
2974
+ };
2975
+ return {
2976
+ url: "",
2977
+ detach
2978
+ };
2979
+ }
2980
+ const url = URL.createObjectURL(mediaSource);
2981
+ mediaElement.src = url;
2982
+ const detach = () => {
2983
+ mediaElement.removeAttribute("src");
2984
+ mediaElement.load();
2985
+ URL.revokeObjectURL(url);
2986
+ };
2987
+ return {
2988
+ url,
2989
+ detach
2990
+ };
2991
+ }
2795
2992
  /**
2796
- * Check if we have the minimum requirements to create MediaSource.
2993
+ * Create a SourceBuffer on a MediaSource.
2994
+ *
2995
+ * @param mediaSource - The MediaSource (must be in 'open' state)
2996
+ * @param mimeCodec - MIME type with codecs (e.g., 'video/mp4; codecs="avc1.42E01E"')
2997
+ * @returns The created SourceBuffer
2998
+ * @throws Error if MediaSource is not open or codec is unsupported
2999
+ *
3000
+ * @example
3001
+ * const buffer = createSourceBuffer(mediaSource, 'video/mp4; codecs="avc1.42E01E"');
2797
3002
  */
2798
- function canSetup(state, owners) {
2799
- return !isNil(owners.mediaElement) && !isNil(state.presentation?.url);
3003
+ function createSourceBuffer(mediaSource, mimeCodec) {
3004
+ if (mediaSource.readyState !== "open") throw new Error("MediaSource is not open");
3005
+ if (!isCodecSupported(mimeCodec)) throw new Error(`Codec not supported: ${mimeCodec}`);
3006
+ return mediaSource.addSourceBuffer(mimeCodec);
2800
3007
  }
2801
3008
  /**
2802
- * Check if we should proceed with MediaSource creation.
2803
- * Placeholder for future conditions (e.g., checking if already created).
3009
+ * Check if a codec is supported.
3010
+ *
3011
+ * @param mimeCodec - MIME type with codecs string
3012
+ * @returns True if the codec is supported
3013
+ *
3014
+ * @example
3015
+ * if (isCodecSupported('video/mp4; codecs="avc1.42E01E"')) {
3016
+ * // Create source buffer
3017
+ * }
2804
3018
  */
2805
- function shouldSetup(_state, owners) {
2806
- return isNil(owners.mediaSource);
3019
+ function isCodecSupported(mimeCodec) {
3020
+ if (!supportsMediaSource()) return false;
3021
+ return MediaSource.isTypeSupported(mimeCodec);
3022
+ }
3023
+ /**
3024
+ * Create a reactive signal that mirrors `mediaSource.readyState`.
3025
+ *
3026
+ * Listens to `sourceopen`, `sourceended`, and `sourceclose` events and updates
3027
+ * the signal accordingly, making readyState visible to the TC39 signal graph.
3028
+ * Listeners are automatically removed when `signal` is aborted.
3029
+ *
3030
+ * @param mediaSource - The MediaSource to observe
3031
+ * @param signal - AbortSignal that controls listener lifetime
3032
+ * @returns A `Signal.ReadonlyState` that reflects the current `readyState`
3033
+ *
3034
+ * @example
3035
+ * const controller = new AbortController();
3036
+ * const readyState = observeMediaSourceReadyState(mediaSource, controller.signal);
3037
+ * effect(() => {
3038
+ * if (readyState.get() === 'open') { ... }
3039
+ * });
3040
+ * // Later:
3041
+ * controller.abort();
3042
+ */
3043
+ function observeMediaSourceReadyState(mediaSource, abortSignal) {
3044
+ const readyState = signal(mediaSource.readyState);
3045
+ const update = () => readyState.set(mediaSource.readyState);
3046
+ const options = { signal: abortSignal };
3047
+ mediaSource.addEventListener("sourceopen", update, options);
3048
+ mediaSource.addEventListener("sourceended", update, options);
3049
+ mediaSource.addEventListener("sourceclose", update, options);
3050
+ return readyState;
2807
3051
  }
3052
+
3053
+ //#endregion
3054
+ //#region ../spf/dist/dev/dom/features/setup-mediasource.js
2808
3055
  /**
2809
3056
  * Setup MediaSource orchestration.
2810
3057
  *
@@ -2815,27 +3062,34 @@ function shouldSetup(_state, owners) {
2815
3062
  * Updates owners.mediaSource after successful setup.
2816
3063
  */
2817
3064
  function setupMediaSource({ state, owners }) {
2818
- let settingUp = false;
2819
- let abortController = null;
2820
- const unsubscribe = combineLatest([state, owners]).subscribe(async ([currentState, currentOwners]) => {
2821
- if (!canSetup(currentState, currentOwners) || !shouldSetup(currentState, currentOwners) || settingUp) return;
2822
- try {
2823
- settingUp = true;
2824
- abortController = new AbortController();
2825
- const mediaSource = createMediaSource({ preferManaged: true });
2826
- attachMediaSource(mediaSource, currentOwners.mediaElement);
2827
- await waitForSourceOpen(mediaSource, abortController.signal);
2828
- owners.patch({ mediaSource });
2829
- } catch (error) {
2830
- if (error instanceof DOMException && error.name === "AbortError") return;
2831
- throw error;
2832
- } finally {
2833
- settingUp = false;
2834
- }
3065
+ const abortController = new AbortController();
3066
+ const mediaElementSignal = computed(() => owners.get().mediaElement);
3067
+ const presentationUrlSignal = computed(() => state.get().presentation?.url);
3068
+ const canSetupSignal = computed(() => !!mediaElementSignal.get() && !!presentationUrlSignal.get());
3069
+ const mediaElementSrcSignal = computed(() => mediaElementSignal.get()?.src);
3070
+ const mediaSourceSignal = computed(() => owners.get().mediaSource);
3071
+ const shouldSetupSignal = computed(() => !mediaElementSrcSignal.get());
3072
+ const cleanupEffect = effect(() => {
3073
+ if (!canSetupSignal.get() || !shouldSetupSignal.get()) return;
3074
+ const mediaElement = mediaElementSignal.get();
3075
+ const { signal } = abortController;
3076
+ const mediaSource = createMediaSource({ preferManaged: true });
3077
+ const mediaSourceReadyState = observeMediaSourceReadyState(mediaSource, signal);
3078
+ attachMediaSource(mediaSource, mediaElement);
3079
+ const cleanupOwnersUpdateEffect = effect(() => {
3080
+ if (!!mediaSourceSignal.get() || mediaSourceReadyState.get() !== "open") return;
3081
+ owners.set(Object.assign({}, owners.get(), {
3082
+ mediaSource,
3083
+ mediaSourceReadyState
3084
+ }));
3085
+ });
3086
+ return () => {
3087
+ cleanupOwnersUpdateEffect();
3088
+ };
2835
3089
  });
2836
3090
  return () => {
2837
3091
  abortController?.abort();
2838
- unsubscribe();
3092
+ cleanupEffect();
2839
3093
  };
2840
3094
  }
2841
3095
 
@@ -2930,7 +3184,7 @@ function messageToTask(message, options) {
2930
3184
  return factory(message, options);
2931
3185
  }
2932
3186
  function createSourceBufferActor(sourceBuffer, initialContext) {
2933
- const state = createState({
3187
+ const snapshotSignal = signal({
2934
3188
  status: "idle",
2935
3189
  context: {
2936
3190
  segments: [],
@@ -2941,55 +3195,47 @@ function createSourceBufferActor(sourceBuffer, initialContext) {
2941
3195
  });
2942
3196
  const runner = new SerialRunner();
2943
3197
  function applyResult(newContext) {
2944
- const status = state.current.status === "destroyed" ? "destroyed" : "idle";
2945
- state.patch({
3198
+ const status = snapshotSignal.get().status === "destroyed" ? "destroyed" : "idle";
3199
+ snapshotSignal.set({
2946
3200
  status,
2947
3201
  context: newContext
2948
3202
  });
2949
- state.flush();
2950
3203
  }
2951
3204
  function handleError(e) {
2952
- const status = state.current.status === "destroyed" ? "destroyed" : "idle";
2953
- state.patch({ status });
2954
- state.flush();
3205
+ update(snapshotSignal, { status: snapshotSignal.get().status === "destroyed" ? "destroyed" : "idle" });
2955
3206
  throw e;
2956
3207
  }
2957
3208
  return {
2958
3209
  get snapshot() {
2959
- return state.current;
2960
- },
2961
- subscribe(listener) {
2962
- return state.subscribe(listener);
3210
+ return snapshotSignal;
2963
3211
  },
2964
3212
  send(message, signal) {
2965
- if (state.current.status !== "idle") return Promise.reject(new SourceBufferActorError(`send() called while actor is ${state.current.status}`));
2966
- state.patch({ status: "updating" });
3213
+ if (snapshotSignal.get().status !== "idle") return Promise.reject(new SourceBufferActorError(`send() called while actor is ${snapshotSignal.get().status}`));
3214
+ update(snapshotSignal, { status: "updating" });
2967
3215
  const onPartialContext = (ctx) => {
2968
- state.patch({
3216
+ snapshotSignal.set({
2969
3217
  status: "updating",
2970
3218
  context: ctx
2971
3219
  });
2972
- state.flush();
2973
3220
  };
2974
3221
  const task = messageToTask(message, {
2975
3222
  signal,
2976
- getCtx: () => state.current.context,
3223
+ getCtx: () => snapshotSignal.get().context,
2977
3224
  sourceBuffer,
2978
3225
  onPartialContext
2979
3226
  });
2980
3227
  return runner.schedule(task).then(applyResult).catch(handleError);
2981
3228
  },
2982
3229
  batch(messages, signal) {
2983
- if (state.current.status !== "idle") return Promise.reject(new SourceBufferActorError(`batch() called while actor is ${state.current.status}`));
3230
+ if (snapshotSignal.get().status !== "idle") return Promise.reject(new SourceBufferActorError(`batch() called while actor is ${snapshotSignal.get().status}`));
2984
3231
  if (messages.length === 0) return Promise.resolve();
2985
- state.patch({ status: "updating" });
2986
- let workingCtx = state.current.context;
3232
+ update(snapshotSignal, { status: "updating" });
3233
+ let workingCtx = snapshotSignal.get().context;
2987
3234
  const onPartialContext = (ctx) => {
2988
- state.patch({
3235
+ snapshotSignal.set({
2989
3236
  status: "updating",
2990
3237
  context: ctx
2991
3238
  });
2992
- state.flush();
2993
3239
  };
2994
3240
  for (const message of messages.slice(0, -1)) {
2995
3241
  const task = messageToTask(message, {
@@ -3011,8 +3257,7 @@ function createSourceBufferActor(sourceBuffer, initialContext) {
3011
3257
  return runner.schedule(lastTask).then(applyResult).catch(handleError);
3012
3258
  },
3013
3259
  destroy() {
3014
- state.patch({ status: "destroyed" });
3015
- state.flush();
3260
+ update(snapshotSignal, { status: "destroyed" });
3016
3261
  runner.destroy();
3017
3262
  }
3018
3263
  };
@@ -3020,6 +3265,10 @@ function createSourceBufferActor(sourceBuffer, initialContext) {
3020
3265
 
3021
3266
  //#endregion
3022
3267
  //#region ../spf/dist/dev/dom/features/setup-sourcebuffer.js
3268
+ const ActorKeyByType = {
3269
+ video: "videoBufferActor",
3270
+ audio: "audioBufferActor"
3271
+ };
3023
3272
  /**
3024
3273
  * Build MIME codec string from track metadata.
3025
3274
  *
@@ -3037,76 +3286,56 @@ function buildMimeCodec(track) {
3037
3286
  /**
3038
3287
  * Setup all needed SourceBuffers as a single coordinated operation.
3039
3288
  *
3040
- * Waits until ALL selected tracks (video and/or audio) are resolved with
3041
- * codecs, then creates every SourceBuffer in one synchronous block before
3042
- * patching owners. This guarantees that downstream consumers (e.g.
3043
- * loadSegments) never see a partial set of SourceBuffers — preventing the
3044
- * Firefox bug where appending to a video SourceBuffer before the audio
3045
- * SourceBuffer exists causes mozHasAudio to be permanently false.
3289
+ * Waits until ALL media tracks in the presentation are resolved with codecs,
3290
+ * then creates every SourceBuffer in one synchronous block before setting
3291
+ * owners. This guarantees that downstream consumers (e.g. loadSegments) never
3292
+ * see a partial set of SourceBuffers — preventing the Firefox bug where
3293
+ * appending to a video SourceBuffer before the audio SourceBuffer exists
3294
+ * causes mozHasAudio to be permanently false.
3046
3295
  *
3047
3296
  * Handles video-only, audio-only, and combined presentations correctly:
3048
- * only the tracks that are actually selected are waited on and created.
3297
+ * track types are derived from the presentation rather than hardcoded.
3049
3298
  *
3050
3299
  * @example
3051
3300
  * const cleanup = setupSourceBuffers({ state, owners });
3052
3301
  */
3053
3302
  function setupSourceBuffers({ state, owners }) {
3054
- let setupDone = false;
3055
- return combineLatest([state, owners]).subscribe(async ([currentState, currentOwners]) => {
3056
- if (setupDone) return;
3057
- if (!currentOwners.mediaSource) return;
3058
- const videoSelected = !!currentState.selectedVideoTrackId;
3059
- const audioSelected = !!currentState.selectedAudioTrackId;
3060
- if (!videoSelected && !audioSelected) return;
3061
- const videoTrack = videoSelected ? getSelectedTrack(currentState, "video") : null;
3062
- const audioTrack = audioSelected ? getSelectedTrack(currentState, "audio") : null;
3063
- if (videoSelected && (!videoTrack || !isResolvedTrack(videoTrack) || !videoTrack.codecs?.length)) return;
3064
- if (audioSelected && (!audioTrack || !isResolvedTrack(audioTrack) || !audioTrack.codecs?.length)) return;
3065
- setupDone = true;
3303
+ const presentationTypesSignal = computed(() => {
3304
+ const { presentation } = state.get();
3305
+ if (!presentation || !("selectionSets" in presentation)) return [];
3306
+ return presentation.selectionSets.map(({ type }) => type).filter((type) => type === "video" || type === "audio");
3307
+ });
3308
+ const canSetupSignal = computed(() => {
3309
+ const types = presentationTypesSignal.get();
3310
+ if (!owners.get().mediaSource || types.length === 0) return false;
3311
+ const s = state.get();
3312
+ return types.every((type) => {
3313
+ const track = getSelectedTrack(s, type);
3314
+ return track && isResolvedTrack(track) && !!track.codecs?.length;
3315
+ });
3316
+ });
3317
+ const shouldSetupSignal = computed(() => {
3318
+ const o = owners.get();
3319
+ return presentationTypesSignal.get().every((type) => !o[BufferKeyByType[type]]);
3320
+ });
3321
+ return effect(() => {
3322
+ if (!canSetupSignal.get() || !shouldSetupSignal.get()) return;
3323
+ const s = state.get();
3324
+ const o = owners.get();
3066
3325
  const patch = {};
3067
- if (videoSelected && videoTrack && isResolvedTrack(videoTrack)) {
3068
- const buffer = createSourceBuffer(currentOwners.mediaSource, buildMimeCodec(videoTrack));
3069
- patch.videoBuffer = buffer;
3070
- patch.videoBufferActor = createSourceBufferActor(buffer);
3071
- }
3072
- if (audioSelected && audioTrack && isResolvedTrack(audioTrack)) {
3073
- const buffer = createSourceBuffer(currentOwners.mediaSource, buildMimeCodec(audioTrack));
3074
- patch.audioBuffer = buffer;
3075
- patch.audioBufferActor = createSourceBufferActor(buffer);
3326
+ for (const type of presentationTypesSignal.get()) {
3327
+ const track = getSelectedTrack(s, type);
3328
+ const buffer = createSourceBuffer(o.mediaSource, buildMimeCodec(track));
3329
+ patch[BufferKeyByType[type]] = buffer;
3330
+ patch[ActorKeyByType[type]] = createSourceBufferActor(buffer);
3076
3331
  }
3077
- owners.patch(patch);
3078
- await new Promise((resolve) => requestAnimationFrame(resolve));
3332
+ update(owners, patch);
3079
3333
  });
3080
3334
  }
3081
3335
 
3082
3336
  //#endregion
3083
3337
  //#region ../spf/dist/dev/dom/features/setup-text-tracks.js
3084
3338
  /**
3085
- * Get all text tracks from presentation.
3086
- */
3087
- function getTextTracks(presentation) {
3088
- if (!presentation?.selectionSets) return [];
3089
- const textSet = presentation.selectionSets.find((set) => set.type === "text");
3090
- if (!textSet?.switchingSets?.[0]?.tracks) return [];
3091
- return textSet.switchingSets[0].tracks;
3092
- }
3093
- /**
3094
- * Check if we can setup text tracks.
3095
- *
3096
- * Requires:
3097
- * - mediaElement exists
3098
- * - presentation has text tracks to setup
3099
- */
3100
- function canSetupTextTracks(state, owners) {
3101
- return !!owners.mediaElement && !!getTextTracks(state.presentation).length;
3102
- }
3103
- /**
3104
- * Check if we should setup text tracks (not already set up).
3105
- */
3106
- function shouldSetupTextTracks(owners) {
3107
- return !owners.textTracks;
3108
- }
3109
- /**
3110
3339
  * Create a track element for a text track.
3111
3340
  *
3112
3341
  * Note: We use DOM <track> elements instead of the TextTrack JS API
@@ -3139,27 +3368,31 @@ function createTrackElement(track) {
3139
3368
  * const cleanup = setupTextTracks({ state, owners });
3140
3369
  */
3141
3370
  function setupTextTracks({ state, owners }) {
3142
- let hasSetup = false;
3143
- let createdTracks = [];
3144
- const unsubscribe = combineLatest([state, owners]).subscribe(([s, o]) => {
3145
- if (hasSetup) return;
3146
- if (!canSetupTextTracks(s, o) || !shouldSetupTextTracks(o)) return;
3147
- hasSetup = true;
3148
- const textTracks = getTextTracks(s.presentation);
3149
- if (textTracks.length === 0) return;
3371
+ const modelTextTracksSignal = computed(() => state.get().presentation?.selectionSets?.find((selectionSet) => selectionSet.type === "text")?.switchingSets[0]?.tracks, { equals(prevTextTracks, nextTextTracks) {
3372
+ if (prevTextTracks === nextTextTracks) return true;
3373
+ if (typeof prevTextTracks !== typeof nextTextTracks) return false;
3374
+ if (prevTextTracks?.length !== nextTextTracks?.length) return false;
3375
+ return !!nextTextTracks && nextTextTracks.every((nextTextTrack) => prevTextTracks?.some((prevTextTrack) => prevTextTrack.id === nextTextTrack.id));
3376
+ } });
3377
+ const ownerTextTracksSignal = computed(() => owners.get().textTracks);
3378
+ const mediaElementSignal = computed(() => owners.get().mediaElement);
3379
+ const canSetupTextTracksSignal = computed(() => !!mediaElementSignal.get() && modelTextTracksSignal.get()?.length);
3380
+ const shouldSetupTextTracksSignal = computed(() => !ownerTextTracksSignal.get());
3381
+ const cleanupEffect = effect(() => {
3382
+ if (!canSetupTextTracksSignal.get() || !shouldSetupTextTracksSignal.get()) return;
3383
+ const mediaElement = mediaElementSignal.get();
3384
+ const modelTextTracks = modelTextTracksSignal.get();
3150
3385
  const trackMap = /* @__PURE__ */ new Map();
3151
- for (const track of textTracks) {
3152
- const trackElement = createTrackElement(track);
3153
- o.mediaElement.appendChild(trackElement);
3154
- trackMap.set(track.id, trackElement);
3155
- createdTracks.push(trackElement);
3156
- }
3157
- owners.patch({ textTracks: trackMap });
3386
+ modelTextTracks.forEach((modelTextTrack) => {
3387
+ const trackElement = createTrackElement(modelTextTrack);
3388
+ mediaElement.appendChild(trackElement);
3389
+ trackMap.set(modelTextTrack.id, trackElement);
3390
+ });
3391
+ if (trackMap.size) update(owners, { textTracks: trackMap });
3158
3392
  });
3159
3393
  return () => {
3160
- for (const trackElement of createdTracks) trackElement.remove();
3161
- createdTracks = [];
3162
- unsubscribe();
3394
+ owners.get().textTracks?.forEach((trackElement) => trackElement.remove());
3395
+ cleanupEffect();
3163
3396
  };
3164
3397
  }
3165
3398
 
@@ -3188,52 +3421,33 @@ function setupTextTracks({ state, owners }) {
3188
3421
  * const cleanup = syncSelectedTextTrackFromDom({ state, owners });
3189
3422
  */
3190
3423
  function syncSelectedTextTrackFromDom({ state, owners }) {
3191
- let lastMediaElement;
3192
- let removeListener = null;
3193
- const unsubscribe = owners.subscribe((currentOwners) => {
3194
- const { mediaElement } = currentOwners;
3195
- if (mediaElement === lastMediaElement) return;
3196
- removeListener?.();
3197
- removeListener = null;
3198
- lastMediaElement = mediaElement;
3199
- if (!mediaElement) return;
3200
- const sync = () => {
3201
- const newId = Array.from(mediaElement.textTracks).find((t) => t.mode === "showing" && (t.kind === "subtitles" || t.kind === "captions"))?.id || void 0;
3202
- const current = state.current;
3424
+ const mediaElement = computed(() => owners.get().mediaElement);
3425
+ return effect(() => {
3426
+ const el = mediaElement.get();
3427
+ if (!el) return;
3428
+ return listen(el.textTracks, "change", () => {
3429
+ const newId = Array.from(el.textTracks).find((t) => t.mode === "showing" && (t.kind === "subtitles" || t.kind === "captions"))?.id || void 0;
3430
+ const current = state.get();
3203
3431
  if (current.selectedTextTrackId === newId) return;
3204
- if (newId) state.patch({ selectedTextTrackId: newId });
3432
+ if (newId) update(state, { selectedTextTrackId: newId });
3205
3433
  else {
3206
3434
  const prevId = current.selectedTextTrackId;
3207
3435
  if (prevId && current.textBufferState?.[prevId]) {
3208
3436
  const next = { ...current.textBufferState };
3209
3437
  delete next[prevId];
3210
- state.patch({
3438
+ update(state, {
3211
3439
  selectedTextTrackId: void 0,
3212
3440
  textBufferState: next
3213
3441
  });
3214
- } else state.patch({ selectedTextTrackId: void 0 });
3442
+ } else update(state, { selectedTextTrackId: void 0 });
3215
3443
  }
3216
- };
3217
- removeListener = listen(mediaElement.textTracks, "change", sync);
3444
+ });
3218
3445
  });
3219
- return () => {
3220
- removeListener?.();
3221
- unsubscribe();
3222
- };
3223
3446
  }
3224
3447
 
3225
3448
  //#endregion
3226
3449
  //#region ../spf/dist/dev/dom/features/sync-text-track-modes.js
3227
3450
  /**
3228
- * Check if we can sync text track modes.
3229
- *
3230
- * Requires:
3231
- * - textTracks map exists (track elements created)
3232
- */
3233
- function canSyncTextTrackModes(owners) {
3234
- return !!owners.textTracks && owners.textTracks.size > 0;
3235
- }
3236
- /**
3237
3451
  * Sync text track modes orchestration.
3238
3452
  *
3239
3453
  * Manages track element modes based on selectedTextTrackId:
@@ -3248,11 +3462,15 @@ function canSyncTextTrackModes(owners) {
3248
3462
  * const cleanup = syncTextTrackModes({ state, owners });
3249
3463
  */
3250
3464
  function syncTextTrackModes({ state, owners }) {
3251
- return combineLatest([state, owners]).subscribe(([s, o]) => {
3252
- if (!canSyncTextTrackModes(o)) return;
3253
- const selectedId = s.selectedTextTrackId;
3254
- for (const [trackId, trackElement] of o.textTracks) if (trackId === selectedId) trackElement.track.mode = "showing";
3255
- else trackElement.track.mode = "hidden";
3465
+ const textTracksSignal = computed(() => owners.get().textTracks);
3466
+ const selectedTextTrackIdSignal = computed(() => state.get().selectedTextTrackId);
3467
+ const canSyncTextTrackModes = computed(() => !!textTracksSignal.get()?.size);
3468
+ return effect(() => {
3469
+ if (!canSyncTextTrackModes.get()) return;
3470
+ /** @TODO refactor TextTracks owners model. Should simply use id. Also should use corresponding TextTrack (JS) element if possible (CJP) */
3471
+ const textTracks = textTracksSignal.get();
3472
+ const selectedTextTrackId = selectedTextTrackIdSignal.get();
3473
+ for (const [trackId, trackElement] of textTracks) trackElement.track.mode = trackId === selectedTextTrackId ? "showing" : "hidden";
3256
3474
  });
3257
3475
  }
3258
3476
 
@@ -3286,7 +3504,7 @@ function shouldUpdateDuration(state, owners) {
3286
3504
  if (!canUpdateDuration(state, owners)) return false;
3287
3505
  const { mediaSource } = owners;
3288
3506
  const { presentation } = state;
3289
- if (mediaSource.readyState !== "open") return false;
3507
+ if ((owners.mediaSourceReadyState?.get() ?? owners.mediaSource?.readyState) !== "open") return false;
3290
3508
  const duration = presentation.duration;
3291
3509
  if (!Number.isFinite(duration) || Number.isNaN(duration) || duration <= 0) return false;
3292
3510
  return Number.isNaN(mediaSource.duration);
@@ -3307,19 +3525,26 @@ function waitForSourceBuffersReady(owners) {
3307
3525
  */
3308
3526
  function updateDuration({ state, owners }) {
3309
3527
  let destroyed = false;
3310
- const unsubscribe = combineLatest([state, owners]).subscribe(async ([currentState, currentOwners]) => {
3311
- if (!shouldUpdateDuration(currentState, currentOwners)) return;
3528
+ let running = false;
3529
+ const cleanupEffect = effect(() => {
3530
+ const currentState = state.get();
3531
+ const currentOwners = owners.get();
3532
+ if (!shouldUpdateDuration(currentState, currentOwners) || running) return;
3312
3533
  const { mediaSource } = currentOwners;
3313
- await waitForSourceBuffersReady(currentOwners);
3314
- if (destroyed || mediaSource.readyState !== "open") return;
3315
- let duration = currentState.presentation.duration;
3316
- const maxBufferedEnd = getMaxBufferedEnd(currentOwners);
3317
- if (maxBufferedEnd > duration) duration = maxBufferedEnd;
3318
- mediaSource.duration = duration;
3534
+ running = true;
3535
+ waitForSourceBuffersReady(currentOwners).then(() => {
3536
+ if (destroyed || mediaSource.readyState !== "open") return;
3537
+ let duration = currentState.presentation.duration;
3538
+ const maxBufferedEnd = getMaxBufferedEnd(currentOwners);
3539
+ if (maxBufferedEnd > duration) duration = maxBufferedEnd;
3540
+ mediaSource.duration = duration;
3541
+ }).finally(() => {
3542
+ running = false;
3543
+ });
3319
3544
  });
3320
3545
  return () => {
3321
3546
  destroyed = true;
3322
- unsubscribe();
3547
+ cleanupEffect();
3323
3548
  };
3324
3549
  }
3325
3550
 
@@ -3346,78 +3571,56 @@ function updateDuration({ state, owners }) {
3346
3571
  * preferredAudioLanguage: 'en',
3347
3572
  * });
3348
3573
  *
3349
- * // Initialize by patching state and owners
3350
- * engine.owners.patch({ mediaElement: document.querySelector('video') });
3351
- * engine.state.patch({
3574
+ * // Initialize by setting state and owners
3575
+ * engine.owners.set({ ...engine.owners.get(), mediaElement: document.querySelector('video') });
3576
+ * engine.state.set({
3577
+ * ...engine.state.get(),
3352
3578
  * presentation: { url: 'https://example.com/playlist.m3u8' },
3353
3579
  * preload: 'auto',
3354
3580
  * });
3355
3581
  *
3356
3582
  * // Inspect state
3357
- * console.log(engine.state.current);
3583
+ * console.log(engine.state.get());
3358
3584
  *
3359
3585
  * // Cleanup
3360
3586
  * engine.destroy();
3361
3587
  */
3362
3588
  function createPlaybackEngine(config = {}) {
3363
- const state = createState({ bandwidthState: {
3589
+ const state = signal({ bandwidthState: {
3364
3590
  fastEstimate: 0,
3365
3591
  fastTotalWeight: 0,
3366
3592
  slowEstimate: 0,
3367
3593
  slowTotalWeight: 0,
3368
3594
  bytesSampled: 0
3369
3595
  } });
3370
- const owners = createState({});
3371
- const events = createEventStream();
3596
+ const owners = signal({});
3372
3597
  const cleanups = [
3373
- syncPreloadAttribute(state, owners),
3374
- trackPlaybackInitiated({
3598
+ syncPreloadAttribute({
3375
3599
  state,
3376
- owners,
3377
- events
3600
+ owners
3378
3601
  }),
3379
- resolvePresentation({
3602
+ trackPlaybackInitiated({
3380
3603
  state,
3381
- events
3604
+ owners
3382
3605
  }),
3383
- selectVideoTrack({
3384
- state,
3385
- owners,
3386
- events
3387
- }, {
3606
+ resolvePresentation({ state }),
3607
+ selectVideoTrack({ state }, {
3388
3608
  type: "video",
3389
3609
  ...config.initialBandwidth !== void 0 && { initialBandwidth: config.initialBandwidth }
3390
3610
  }),
3391
- selectAudioTrack({
3392
- state,
3393
- owners,
3394
- events
3395
- }, {
3611
+ selectAudioTrack({ state }, {
3396
3612
  type: "audio",
3397
3613
  ...config.preferredAudioLanguage !== void 0 && { preferredAudioLanguage: config.preferredAudioLanguage }
3398
3614
  }),
3399
- selectTextTrack({
3400
- state,
3401
- owners,
3402
- events
3403
- }, {
3615
+ selectTextTrack({ state }, {
3404
3616
  type: "text",
3405
3617
  ...config.preferredSubtitleLanguage !== void 0 && { preferredSubtitleLanguage: config.preferredSubtitleLanguage },
3406
3618
  ...config.includeForcedTracks !== void 0 && { includeForcedTracks: config.includeForcedTracks },
3407
3619
  ...config.enableDefaultTrack !== void 0 && { enableDefaultTrack: config.enableDefaultTrack }
3408
3620
  }),
3409
- resolveTrack({
3410
- state,
3411
- events
3412
- }, { type: "video" }),
3413
- resolveTrack({
3414
- state,
3415
- events
3416
- }, { type: "audio" }),
3417
- resolveTrack({
3418
- state,
3419
- events
3420
- }, { type: "text" }),
3621
+ resolveTrack({ state }, { type: "video" }),
3622
+ resolveTrack({ state }, { type: "audio" }),
3623
+ resolveTrack({ state }, { type: "text" }),
3421
3624
  calculatePresentationDuration({ state }),
3422
3625
  setupMediaSource({
3423
3626
  state,
@@ -3435,7 +3638,7 @@ function createPlaybackEngine(config = {}) {
3435
3638
  state,
3436
3639
  owners
3437
3640
  }),
3438
- switchQuality({ state }),
3641
+ switchQuality({ state }, config.initialBandwidth !== void 0 ? { defaultBandwidth: config.initialBandwidth } : {}),
3439
3642
  loadSegments({
3440
3643
  state,
3441
3644
  owners
@@ -3465,11 +3668,9 @@ function createPlaybackEngine(config = {}) {
3465
3668
  owners
3466
3669
  })
3467
3670
  ];
3468
- events.dispatch({ type: "@@INITIALIZE@@" });
3469
3671
  return {
3470
3672
  state,
3471
3673
  owners,
3472
- events,
3473
3674
  destroy: () => {
3474
3675
  cleanups.forEach((cleanup) => cleanup());
3475
3676
  destroyVttParser();
@@ -3515,11 +3716,11 @@ var SpfMedia = class {
3515
3716
  return this.#engine;
3516
3717
  }
3517
3718
  attach(mediaElement) {
3518
- this.#engine.owners.patch({ mediaElement });
3719
+ update(this.#engine.owners, { mediaElement });
3519
3720
  }
3520
3721
  detach() {
3521
3722
  this.#cancelPendingPlay();
3522
- this.#engine.owners.patch({ mediaElement: void 0 });
3723
+ update(this.#engine.owners, { mediaElement: void 0 });
3523
3724
  }
3524
3725
  destroy() {
3525
3726
  this.#cancelPendingPlay();
@@ -3530,24 +3731,24 @@ var SpfMedia = class {
3530
3731
  }
3531
3732
  set preload(value) {
3532
3733
  this.#preload = value;
3533
- if (value) this.#engine.state.patch({ preload: value });
3734
+ if (value) update(this.#engine.state, { preload: value });
3534
3735
  }
3535
3736
  get src() {
3536
- return this.#engine.state.current.presentation?.url ?? "";
3737
+ return this.#engine.state.get().presentation?.url ?? "";
3537
3738
  }
3538
3739
  set src(value) {
3539
- const prevMediaElement = this.#engine.owners.current.mediaElement;
3740
+ const prevMediaElement = this.#engine.owners.get().mediaElement;
3540
3741
  this.#cancelPendingPlay();
3541
3742
  this.#engine.destroy();
3542
3743
  this.#engine = createPlaybackEngine(this.#config);
3543
- if (this.#preload) this.#engine.state.patch({ preload: this.#preload });
3544
- if (prevMediaElement) this.#engine.owners.patch({ mediaElement: prevMediaElement });
3545
- if (value) this.#engine.state.patch({ presentation: { url: value } });
3744
+ if (this.#preload) update(this.#engine.state, { preload: this.#preload });
3745
+ if (prevMediaElement) update(this.#engine.owners, { mediaElement: prevMediaElement });
3746
+ if (value) update(this.#engine.state, { presentation: { url: value } });
3546
3747
  }
3547
3748
  play() {
3548
- const { mediaElement } = this.#engine.owners.current;
3749
+ const { mediaElement } = this.#engine.owners.get();
3549
3750
  if (!mediaElement) return Promise.reject(/* @__PURE__ */ new Error("SpfMedia: no media element attached"));
3550
- this.#engine.state.patch({ playbackInitiated: true });
3751
+ update(this.#engine.state, { playbackInitiated: true });
3551
3752
  return mediaElement.play().catch((err) => {
3552
3753
  if (this.src) return new Promise((resolve, reject) => {
3553
3754
  const listener = () => {
@@ -3562,7 +3763,7 @@ var SpfMedia = class {
3562
3763
  }
3563
3764
  #cancelPendingPlay() {
3564
3765
  if (!this.#loadstartListener) return;
3565
- const { mediaElement } = this.#engine.owners.current;
3766
+ const { mediaElement } = this.#engine.owners.get();
3566
3767
  mediaElement?.removeEventListener("loadstart", this.#loadstartListener);
3567
3768
  this.#loadstartListener = null;
3568
3769
  }
@@ -3570,24 +3771,15 @@ var SpfMedia = class {
3570
3771
 
3571
3772
  //#endregion
3572
3773
  //#region ../core/dist/dev/dom/media/simple-hls/index.js
3573
- var SimpleHlsCustomMedia = class extends DelegateMixin(CustomMediaMixin(globalThis.HTMLElement ?? class {}, { tag: "video" }), SpfMedia) {};
3774
+ var SimpleHlsCustomMedia = class extends DelegateMixin(CustomVideoElement, SpfMedia) {};
3574
3775
 
3575
3776
  //#endregion
3576
3777
  //#region src/media/simple-hls-video/index.ts
3577
- var SimpleHlsVideo = class extends MediaAttachMixin(SimpleHlsCustomMedia) {
3578
- static getTemplateHTML(attrs) {
3579
- const { src, ...rest } = attrs;
3580
- return super.getTemplateHTML(rest);
3581
- }
3778
+ var SimpleHlsVideo = class extends MediaPropsMixin(MediaAttachMixin(SimpleHlsCustomMedia), SpfMedia) {
3582
3779
  constructor() {
3583
3780
  super();
3584
3781
  this.attach(this.target);
3585
3782
  }
3586
- attributeChangedCallback(attrName, oldValue, newValue) {
3587
- if (attrName !== "src") super.attributeChangedCallback(attrName, oldValue, newValue);
3588
- if (attrName === "src" && oldValue !== newValue) this.src = newValue ?? "";
3589
- if (attrName === "preload" && oldValue !== newValue) this.preload = newValue ?? "";
3590
- }
3591
3783
  };
3592
3784
 
3593
3785
  //#endregion
@@ -3597,7 +3789,7 @@ var SimpleHlsVideoElement = class extends SimpleHlsVideo {
3597
3789
  this.tagName = "simple-hls-video";
3598
3790
  }
3599
3791
  };
3600
- customElements.define(SimpleHlsVideoElement.tagName, SimpleHlsVideoElement);
3792
+ safeDefine(SimpleHlsVideoElement);
3601
3793
 
3602
3794
  //#endregion
3603
3795
  //# sourceMappingURL=simple-hls-video.dev.js.map