@videojs/html 10.0.0-beta.15 → 10.0.0-beta.16

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 (299) hide show
  1. package/cdn/audio-minimal-ui.dev.js +4 -5
  2. package/cdn/audio-minimal-ui.js +1 -1
  3. package/cdn/audio-minimal.dev.js +7 -8
  4. package/cdn/audio-minimal.dev.js.map +1 -1
  5. package/cdn/audio-minimal.js +1 -1
  6. package/cdn/audio-minimal.js.map +1 -1
  7. package/cdn/audio-ui.dev.js +5 -5
  8. package/cdn/audio-ui.js +1 -1
  9. package/cdn/audio.dev.js +23 -7
  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 -3
  14. package/cdn/background.js +1 -1
  15. package/cdn/background.js.map +1 -1
  16. package/cdn/context-C-IpVHEt.js +14 -0
  17. package/cdn/context-C-IpVHEt.js.map +1 -0
  18. package/cdn/{context-BjrbhiSk.js → context-CvJ83xGk.js} +2 -83
  19. package/cdn/context-CvJ83xGk.js.map +1 -0
  20. package/cdn/create-player-B_oNyyb5.js +7 -0
  21. package/cdn/create-player-B_oNyyb5.js.map +1 -0
  22. package/cdn/{create-player-p9xCO0yC.js → create-player-DckZq-sE.js} +56 -1416
  23. package/cdn/create-player-DckZq-sE.js.map +1 -0
  24. package/cdn/{hls-BriQI4q1.js → hls-CJCXZSGG.js} +8 -11
  25. package/cdn/{hls-BriQI4q1.js.map → hls-CJCXZSGG.js.map} +1 -1
  26. package/cdn/{hls-26YJVjvD.js → hls-lPH0rWmB.js} +3 -3
  27. package/cdn/{hls-26YJVjvD.js.map → hls-lPH0rWmB.js.map} +1 -1
  28. package/cdn/hotkey-B7OaweHa.js +154 -0
  29. package/cdn/hotkey-B7OaweHa.js.map +1 -0
  30. package/cdn/hotkey-BLEt77Z0.js +2 -0
  31. package/cdn/hotkey-BLEt77Z0.js.map +1 -0
  32. package/cdn/media/dash-video.dev.js +3 -4
  33. package/cdn/media/dash-video.dev.js.map +1 -1
  34. package/cdn/media/dash-video.js +2 -2
  35. package/cdn/media/hls-video.dev.js +4 -5
  36. package/cdn/media/hls-video.dev.js.map +1 -1
  37. package/cdn/media/hls-video.js +1 -1
  38. package/cdn/media/hls-video.js.map +1 -1
  39. package/cdn/media/mux-audio.dev.d.ts +1 -0
  40. package/cdn/media/mux-audio.dev.js +29 -0
  41. package/cdn/media/mux-audio.dev.js.map +1 -0
  42. package/cdn/media/mux-audio.js +2 -0
  43. package/cdn/media/mux-audio.js.map +1 -0
  44. package/cdn/media/mux-video.dev.js +5 -3099
  45. package/cdn/media/mux-video.dev.js.map +1 -1
  46. package/cdn/media/mux-video.js +1 -24
  47. package/cdn/media/mux-video.js.map +1 -1
  48. package/cdn/media/native-hls-video.dev.js +4 -4
  49. package/cdn/media/native-hls-video.js +1 -1
  50. package/cdn/media/simple-hls-video.dev.js +965 -680
  51. package/cdn/media/simple-hls-video.dev.js.map +1 -1
  52. package/cdn/media/simple-hls-video.js +6 -6
  53. package/cdn/media/simple-hls-video.js.map +1 -1
  54. package/cdn/{media-attach-mixin-DfKkofgF.js → media-attach-mixin-CMH1ziXP.js} +2 -2
  55. package/cdn/{media-attach-mixin-DfKkofgF.js.map → media-attach-mixin-CMH1ziXP.js.map} +1 -1
  56. package/cdn/{media-attach-mixin-Dv2D43tb.js → media-attach-mixin-DUzmXEJ3.js} +2 -2
  57. package/cdn/{media-attach-mixin-Dv2D43tb.js.map → media-attach-mixin-DUzmXEJ3.js.map} +1 -1
  58. package/cdn/{media-props-mixin-Dc5dpCV4.js → media-props-mixin-CZLZIhGT.js} +92 -8
  59. package/cdn/media-props-mixin-CZLZIhGT.js.map +1 -0
  60. package/cdn/media-props-mixin-DrjlZybt.js +47 -0
  61. package/cdn/media-props-mixin-DrjlZybt.js.map +1 -0
  62. package/cdn/{minimal-BUUGNZK7.js → minimal-D9Dw-L2P.js} +1 -1
  63. package/cdn/{minimal-BUUGNZK7.js.map → minimal-D9Dw-L2P.js.map} +1 -1
  64. package/cdn/{minimal-CJGOJ6pE.js → minimal-Dc5nNl_-.js} +1 -1
  65. package/cdn/{minimal-CJGOJ6pE.js.map → minimal-Dc5nNl_-.js.map} +1 -1
  66. package/cdn/mux-CBXn2z5k.js +3102 -0
  67. package/cdn/mux-CBXn2z5k.js.map +1 -0
  68. package/cdn/mux-DmGzBNIq.js +25 -0
  69. package/cdn/mux-DmGzBNIq.js.map +1 -0
  70. package/cdn/{native-hls-D2rfyyyW.js → native-hls-BCOLtRUl.js} +2 -3
  71. package/cdn/{native-hls-D2rfyyyW.js.map → native-hls-BCOLtRUl.js.map} +1 -1
  72. package/cdn/native-hls-BU8--1Kg.js +2 -0
  73. package/cdn/{native-hls-CjRvSXAJ.js.map → native-hls-BU8--1Kg.js.map} +1 -1
  74. package/cdn/player-DAmuLGE1.js +2 -0
  75. package/cdn/{player-Cz9-_j7J.js.map → player-DAmuLGE1.js.map} +1 -1
  76. package/cdn/{player-D0bhjT8X.js → player-Dbl1MUJ2.js} +3 -3
  77. package/cdn/{player-D0bhjT8X.js.map → player-Dbl1MUJ2.js.map} +1 -1
  78. package/cdn/poster-B9COb7AR.js +2 -0
  79. package/cdn/poster-B9COb7AR.js.map +1 -0
  80. package/cdn/poster-qAhnKWea.js +480 -0
  81. package/cdn/poster-qAhnKWea.js.map +1 -0
  82. package/cdn/{skin-element-DB-cKAAx.js → skin-element-DbmY-ske.js} +2 -2
  83. package/cdn/{skin-element-DB-cKAAx.js.map → skin-element-DbmY-ske.js.map} +1 -1
  84. package/cdn/{skin-element-zakTHELS.js → skin-element-ih3VrnYx.js} +2 -2
  85. package/cdn/{skin-element-zakTHELS.js.map → skin-element-ih3VrnYx.js.map} +1 -1
  86. package/cdn/video-minimal-ui.dev.js +4 -5
  87. package/cdn/video-minimal-ui.js +1 -1
  88. package/cdn/video-minimal.dev.js +7 -8
  89. package/cdn/video-minimal.dev.js.map +1 -1
  90. package/cdn/video-minimal.js +1 -1
  91. package/cdn/video-minimal.js.map +1 -1
  92. package/cdn/video-ui.dev.js +5 -5
  93. package/cdn/video-ui.js +1 -1
  94. package/cdn/video.dev.js +26 -7
  95. package/cdn/video.dev.js.map +1 -1
  96. package/cdn/video.js +1 -1
  97. package/cdn/video.js.map +1 -1
  98. package/cdn/volume-slider-BbIc1N4U.js +8 -0
  99. package/cdn/volume-slider-BbIc1N4U.js.map +1 -0
  100. package/cdn/{volume-slider-Co5X4Z1t.js → volume-slider-C6ek7oC9.js} +1855 -386
  101. package/cdn/volume-slider-C6ek7oC9.js.map +1 -0
  102. package/dist/default/_virtual/inline-css_src/define/audio/minimal-skin.js +1 -1
  103. package/dist/default/_virtual/inline-css_src/define/audio/minimal-skin.js.map +1 -1
  104. package/dist/default/_virtual/inline-css_src/define/audio/skin.js +1 -1
  105. package/dist/default/_virtual/inline-css_src/define/audio/skin.js.map +1 -1
  106. package/dist/default/_virtual/inline-css_src/define/video/minimal-skin.js +1 -1
  107. package/dist/default/_virtual/inline-css_src/define/video/minimal-skin.js.map +1 -1
  108. package/dist/default/_virtual/inline-css_src/define/video/skin.js +1 -1
  109. package/dist/default/_virtual/inline-css_src/define/video/skin.js.map +1 -1
  110. package/dist/default/define/audio/minimal-skin.css +23 -0
  111. package/dist/default/define/audio/skin.css +23 -0
  112. package/dist/default/define/audio/skin.js +1 -1
  113. package/dist/default/define/audio/skin.js.map +1 -1
  114. package/dist/default/define/audio/ui.js +1 -0
  115. package/dist/default/define/background/skin.js +1 -1
  116. package/dist/default/define/media/mux-audio.js +13 -0
  117. package/dist/default/define/media/mux-audio.js.map +1 -0
  118. package/dist/default/define/skin-element.js +1 -1
  119. package/dist/default/define/ui/hotkey.js +7 -0
  120. package/dist/default/define/ui/hotkey.js.map +1 -0
  121. package/dist/default/define/ui/slider.js +1 -1
  122. package/dist/default/define/ui/time-slider.js +1 -1
  123. package/dist/default/define/ui/volume-slider.js +1 -1
  124. package/dist/default/define/video/minimal-skin.css +23 -0
  125. package/dist/default/define/video/skin.css +23 -0
  126. package/dist/default/define/video/skin.js +1 -1
  127. package/dist/default/define/video/skin.js.map +1 -1
  128. package/dist/default/define/video/ui.js +1 -0
  129. package/dist/default/index.js +5 -1
  130. package/dist/default/media/container-element.js +16 -0
  131. package/dist/default/media/container-element.js.map +1 -1
  132. package/dist/default/media/mux-audio/index.js +18 -0
  133. package/dist/default/media/mux-audio/index.js.map +1 -0
  134. package/dist/default/player/context.js.map +1 -1
  135. package/dist/default/ui/alert-dialog/alert-dialog-element.js +1 -0
  136. package/dist/default/ui/alert-dialog/alert-dialog-element.js.map +1 -1
  137. package/dist/default/ui/buffering-indicator/buffering-indicator-element.js +1 -0
  138. package/dist/default/ui/buffering-indicator/buffering-indicator-element.js.map +1 -1
  139. package/dist/default/ui/captions-button/captions-button-element.js +1 -0
  140. package/dist/default/ui/captions-button/captions-button-element.js.map +1 -1
  141. package/dist/default/ui/error-dialog/error-dialog-element.js +1 -0
  142. package/dist/default/ui/error-dialog/error-dialog-element.js.map +1 -1
  143. package/dist/default/ui/fullscreen-button/fullscreen-button-element.js +1 -0
  144. package/dist/default/ui/fullscreen-button/fullscreen-button-element.js.map +1 -1
  145. package/dist/default/ui/hotkey/aria-key-shortcuts-controller.js +28 -0
  146. package/dist/default/ui/hotkey/aria-key-shortcuts-controller.js.map +1 -0
  147. package/dist/default/ui/hotkey/hotkey-element.js +81 -0
  148. package/dist/default/ui/hotkey/hotkey-element.js.map +1 -0
  149. package/dist/default/ui/media-button-element.js +9 -1
  150. package/dist/default/ui/media-button-element.js.map +1 -1
  151. package/dist/default/ui/mute-button/mute-button-element.js +1 -0
  152. package/dist/default/ui/mute-button/mute-button-element.js.map +1 -1
  153. package/dist/default/ui/pip-button/pip-button-element.js +1 -0
  154. package/dist/default/ui/pip-button/pip-button-element.js.map +1 -1
  155. package/dist/default/ui/play-button/play-button-element.js +1 -0
  156. package/dist/default/ui/play-button/play-button-element.js.map +1 -1
  157. package/dist/default/ui/popover/popover-element.js +7 -55
  158. package/dist/default/ui/popover/popover-element.js.map +1 -1
  159. package/dist/default/ui/position-controller.js +76 -0
  160. package/dist/default/ui/position-controller.js.map +1 -0
  161. package/dist/default/ui/slider/slider-element.js +1 -1
  162. package/dist/default/ui/slider/slider-preview-element.js +1 -1
  163. package/dist/default/ui/time-slider/time-slider-element.js +5 -5
  164. package/dist/default/ui/time-slider/time-slider-element.js.map +1 -1
  165. package/dist/default/ui/tooltip/tooltip-element.js +8 -54
  166. package/dist/default/ui/tooltip/tooltip-element.js.map +1 -1
  167. package/dist/default/ui/volume-slider/volume-slider-element.js +1 -1
  168. package/dist/dev/_virtual/inline-css_src/define/audio/minimal-skin.js +1 -1
  169. package/dist/dev/_virtual/inline-css_src/define/audio/minimal-skin.js.map +1 -1
  170. package/dist/dev/_virtual/inline-css_src/define/audio/skin.js +1 -1
  171. package/dist/dev/_virtual/inline-css_src/define/audio/skin.js.map +1 -1
  172. package/dist/dev/_virtual/inline-css_src/define/video/minimal-skin.js +1 -1
  173. package/dist/dev/_virtual/inline-css_src/define/video/minimal-skin.js.map +1 -1
  174. package/dist/dev/_virtual/inline-css_src/define/video/skin.js +1 -1
  175. package/dist/dev/_virtual/inline-css_src/define/video/skin.js.map +1 -1
  176. package/dist/dev/define/audio/minimal-skin.css +23 -0
  177. package/dist/dev/define/audio/skin.css +23 -0
  178. package/dist/dev/define/audio/skin.d.ts.map +1 -1
  179. package/dist/dev/define/audio/skin.js +16 -0
  180. package/dist/dev/define/audio/skin.js.map +1 -1
  181. package/dist/dev/define/audio/ui.js +1 -0
  182. package/dist/dev/define/background/skin.js +1 -1
  183. package/dist/dev/define/media/mux-audio.d.ts +14 -0
  184. package/dist/dev/define/media/mux-audio.d.ts.map +1 -0
  185. package/dist/dev/define/media/mux-audio.js +13 -0
  186. package/dist/dev/define/media/mux-audio.js.map +1 -0
  187. package/dist/dev/define/skin-element.d.ts +1 -1
  188. package/dist/dev/define/skin-element.js +1 -1
  189. package/dist/dev/define/ui/hotkey.d.ts +9 -0
  190. package/dist/dev/define/ui/hotkey.d.ts.map +1 -0
  191. package/dist/dev/define/ui/hotkey.js +7 -0
  192. package/dist/dev/define/ui/hotkey.js.map +1 -0
  193. package/dist/dev/define/ui/slider.js +1 -1
  194. package/dist/dev/define/ui/time-slider.js +1 -1
  195. package/dist/dev/define/ui/volume-slider.js +1 -1
  196. package/dist/dev/define/video/minimal-skin.css +23 -0
  197. package/dist/dev/define/video/skin.css +23 -0
  198. package/dist/dev/define/video/skin.d.ts.map +1 -1
  199. package/dist/dev/define/video/skin.js +19 -0
  200. package/dist/dev/define/video/skin.js.map +1 -1
  201. package/dist/dev/define/video/ui.js +1 -0
  202. package/dist/dev/index.d.ts +6 -2
  203. package/dist/dev/index.js +5 -1
  204. package/dist/dev/media/container-element.d.ts +3 -0
  205. package/dist/dev/media/container-element.d.ts.map +1 -1
  206. package/dist/dev/media/container-element.js +16 -0
  207. package/dist/dev/media/container-element.js.map +1 -1
  208. package/dist/dev/media/mux-audio/index.d.ts +11 -0
  209. package/dist/dev/media/mux-audio/index.d.ts.map +1 -0
  210. package/dist/dev/media/mux-audio/index.js +18 -0
  211. package/dist/dev/media/mux-audio/index.js.map +1 -0
  212. package/dist/dev/player/context.d.ts +4 -2
  213. package/dist/dev/player/context.d.ts.map +1 -1
  214. package/dist/dev/player/context.js.map +1 -1
  215. package/dist/dev/ui/alert-dialog/alert-dialog-element.d.ts.map +1 -1
  216. package/dist/dev/ui/alert-dialog/alert-dialog-element.js +1 -0
  217. package/dist/dev/ui/alert-dialog/alert-dialog-element.js.map +1 -1
  218. package/dist/dev/ui/buffering-indicator/buffering-indicator-element.d.ts.map +1 -1
  219. package/dist/dev/ui/buffering-indicator/buffering-indicator-element.js +1 -0
  220. package/dist/dev/ui/buffering-indicator/buffering-indicator-element.js.map +1 -1
  221. package/dist/dev/ui/captions-button/captions-button-element.d.ts +1 -0
  222. package/dist/dev/ui/captions-button/captions-button-element.d.ts.map +1 -1
  223. package/dist/dev/ui/captions-button/captions-button-element.js +1 -0
  224. package/dist/dev/ui/captions-button/captions-button-element.js.map +1 -1
  225. package/dist/dev/ui/context-part-element.d.ts +1 -1
  226. package/dist/dev/ui/error-dialog/error-dialog-element.d.ts.map +1 -1
  227. package/dist/dev/ui/error-dialog/error-dialog-element.js +1 -0
  228. package/dist/dev/ui/error-dialog/error-dialog-element.js.map +1 -1
  229. package/dist/dev/ui/fullscreen-button/fullscreen-button-element.d.ts +1 -0
  230. package/dist/dev/ui/fullscreen-button/fullscreen-button-element.d.ts.map +1 -1
  231. package/dist/dev/ui/fullscreen-button/fullscreen-button-element.js +1 -0
  232. package/dist/dev/ui/fullscreen-button/fullscreen-button-element.js.map +1 -1
  233. package/dist/dev/ui/hotkey/aria-key-shortcuts-controller.d.ts +15 -0
  234. package/dist/dev/ui/hotkey/aria-key-shortcuts-controller.d.ts.map +1 -0
  235. package/dist/dev/ui/hotkey/aria-key-shortcuts-controller.js +28 -0
  236. package/dist/dev/ui/hotkey/aria-key-shortcuts-controller.js.map +1 -0
  237. package/dist/dev/ui/hotkey/hotkey-element.d.ts +21 -0
  238. package/dist/dev/ui/hotkey/hotkey-element.d.ts.map +1 -0
  239. package/dist/dev/ui/hotkey/hotkey-element.js +81 -0
  240. package/dist/dev/ui/hotkey/hotkey-element.js.map +1 -0
  241. package/dist/dev/ui/media-button-element.d.ts +2 -0
  242. package/dist/dev/ui/media-button-element.d.ts.map +1 -1
  243. package/dist/dev/ui/media-button-element.js +9 -1
  244. package/dist/dev/ui/media-button-element.js.map +1 -1
  245. package/dist/dev/ui/mute-button/mute-button-element.d.ts +1 -0
  246. package/dist/dev/ui/mute-button/mute-button-element.d.ts.map +1 -1
  247. package/dist/dev/ui/mute-button/mute-button-element.js +1 -0
  248. package/dist/dev/ui/mute-button/mute-button-element.js.map +1 -1
  249. package/dist/dev/ui/pip-button/pip-button-element.d.ts +1 -0
  250. package/dist/dev/ui/pip-button/pip-button-element.d.ts.map +1 -1
  251. package/dist/dev/ui/pip-button/pip-button-element.js +1 -0
  252. package/dist/dev/ui/pip-button/pip-button-element.js.map +1 -1
  253. package/dist/dev/ui/play-button/play-button-element.d.ts +1 -0
  254. package/dist/dev/ui/play-button/play-button-element.d.ts.map +1 -1
  255. package/dist/dev/ui/play-button/play-button-element.js +1 -0
  256. package/dist/dev/ui/play-button/play-button-element.js.map +1 -1
  257. package/dist/dev/ui/popover/popover-element.d.ts.map +1 -1
  258. package/dist/dev/ui/popover/popover-element.js +7 -55
  259. package/dist/dev/ui/popover/popover-element.js.map +1 -1
  260. package/dist/dev/ui/position-controller.js +76 -0
  261. package/dist/dev/ui/position-controller.js.map +1 -0
  262. package/dist/dev/ui/slider/slider-element.js +1 -1
  263. package/dist/dev/ui/slider/slider-preview-element.d.ts +21 -0
  264. package/dist/dev/ui/slider/slider-preview-element.d.ts.map +1 -0
  265. package/dist/dev/ui/slider/slider-preview-element.js +1 -1
  266. package/dist/dev/ui/time-slider/time-slider-element.d.ts +2 -2
  267. package/dist/dev/ui/time-slider/time-slider-element.js +5 -5
  268. package/dist/dev/ui/time-slider/time-slider-element.js.map +1 -1
  269. package/dist/dev/ui/tooltip/tooltip-element.d.ts.map +1 -1
  270. package/dist/dev/ui/tooltip/tooltip-element.js +8 -54
  271. package/dist/dev/ui/tooltip/tooltip-element.js.map +1 -1
  272. package/dist/dev/ui/volume-slider/volume-slider-element.js +1 -1
  273. package/package.json +8 -8
  274. package/cdn/context-BjrbhiSk.js.map +0 -1
  275. package/cdn/context-CdkhXQEw.js +0 -14
  276. package/cdn/context-CdkhXQEw.js.map +0 -1
  277. package/cdn/create-player-BqU8r8H4.js +0 -7
  278. package/cdn/create-player-BqU8r8H4.js.map +0 -1
  279. package/cdn/create-player-p9xCO0yC.js.map +0 -1
  280. package/cdn/delegate-B626t_Va.js +0 -2
  281. package/cdn/delegate-B626t_Va.js.map +0 -1
  282. package/cdn/delegate-BCw-LjP_.js +0 -56
  283. package/cdn/delegate-BCw-LjP_.js.map +0 -1
  284. package/cdn/media-props-mixin-Bd_N2HWy.js +0 -47
  285. package/cdn/media-props-mixin-Bd_N2HWy.js.map +0 -1
  286. package/cdn/media-props-mixin-Dc5dpCV4.js.map +0 -1
  287. package/cdn/native-hls-CjRvSXAJ.js +0 -2
  288. package/cdn/player-Cz9-_j7J.js +0 -2
  289. package/cdn/poster-2FPYTjyv.js +0 -2
  290. package/cdn/poster-2FPYTjyv.js.map +0 -1
  291. package/cdn/poster-gVT-J1-z.js +0 -215
  292. package/cdn/poster-gVT-J1-z.js.map +0 -1
  293. package/cdn/volume-slider-Co5X4Z1t.js.map +0 -1
  294. package/cdn/volume-slider-D18sXnIA.js +0 -8
  295. package/cdn/volume-slider-D18sXnIA.js.map +0 -1
  296. /package/cdn/{minimal-ui-BL9eBXck.js → minimal-ui-DXFwjywa.js} +0 -0
  297. /package/cdn/{minimal-ui-a1UOKF79.js → minimal-ui-jrmrxIOn.js} +0 -0
  298. /package/cdn/{ui-CFIgmrlA.js → ui-18UYtw8D.js} +0 -0
  299. /package/cdn/{ui-Ccp_LyCf.js → ui-8HVY3XSt.js} +0 -0
@@ -1,9 +1,8 @@
1
1
  import { t as listen } from "../listen-w-Aun0yN.js";
2
- import { s as safeDefine } from "../context-BjrbhiSk.js";
2
+ import { o as safeDefine } from "../context-CvJ83xGk.js";
3
3
  import { t as anyAbortSignal } from "../abort-CzqmAxxH.js";
4
- import { t as DelegateMixin } from "../delegate-BCw-LjP_.js";
5
- import { t as MediaAttachMixin } from "../media-attach-mixin-DfKkofgF.js";
6
- import { n as CustomVideoElement, t as MediaPropsMixin } from "../media-props-mixin-Dc5dpCV4.js";
4
+ import { i as DelegateMixin, r as CustomVideoElement, t as MediaPropsMixin } from "../media-props-mixin-CZLZIhGT.js";
5
+ import { t as MediaAttachMixin } from "../media-attach-mixin-CMH1ziXP.js";
7
6
 
8
7
  //#region ../spf/dist/dev/core/abr/ewma.js
9
8
  /**
@@ -981,19 +980,436 @@ function calculateBackBufferFlushPoint(segments, currentTime, config = DEFAULT_B
981
980
  return segmentsBefore[segmentsToFlush].startTime;
982
981
  }
983
982
 
983
+ //#endregion
984
+ //#region ../spf/dist/dev/core/machine.js
985
+ /**
986
+ * Provisions the shared mechanics for all machine-like primitives: a snapshot
987
+ * signal, an untracked state reader, and a transition function.
988
+ *
989
+ * Internal — consumed by `createMachineActor` and `createMachineReactor`. Not part of the
990
+ * public API.
991
+ */
992
+ function createMachineCore(initialSnapshot) {
993
+ const snapshotSignal = signal(initialSnapshot);
994
+ const getState = () => untrack(() => snapshotSignal.get().value);
995
+ const transition = (to) => update(snapshotSignal, (current) => ({
996
+ ...current,
997
+ value: to
998
+ }));
999
+ return {
1000
+ snapshotSignal,
1001
+ getState,
1002
+ transition
1003
+ };
1004
+ }
1005
+
1006
+ //#endregion
1007
+ //#region ../spf/dist/dev/core/create-machine-actor.js
1008
+ /**
1009
+ * Creates a message-driven actor from a declarative definition.
1010
+ *
1011
+ * The actor owns a reactive snapshot signal (state + context), an optional
1012
+ * runner, and dispatches incoming messages to per-state handlers. `'destroyed'`
1013
+ * is always the implicit terminal state — `destroy()` transitions there
1014
+ * unconditionally and all subsequent `send()` calls are no-ops.
1015
+ *
1016
+ * When a state declares `onSettled`, the framework calls `runner.whenSettled()`
1017
+ * after the handler returns. The runner owns the generation-token logic — if
1018
+ * new tasks are scheduled before the current batch settles, the callback is
1019
+ * automatically superseded.
1020
+ *
1021
+ * @example
1022
+ * const actor = createMachineActor({
1023
+ * runner: () => new SerialRunner(),
1024
+ * initial: 'idle',
1025
+ * context: {},
1026
+ * states: {
1027
+ * idle: {
1028
+ * on: {
1029
+ * load: (msg, { transition, runner }) => {
1030
+ * segments.forEach(s => runner.schedule(new Task(...)));
1031
+ * transition('loading');
1032
+ * }
1033
+ * }
1034
+ * },
1035
+ * loading: {
1036
+ * onSettled: 'idle',
1037
+ * on: {
1038
+ * load: (msg, { runner }) => {
1039
+ * runner.abortAll();
1040
+ * segments.forEach(s => runner.schedule(new Task(...)));
1041
+ * }
1042
+ * }
1043
+ * }
1044
+ * }
1045
+ * });
1046
+ */
1047
+ function createMachineActor(def) {
1048
+ const runner = def.runner?.();
1049
+ const { snapshotSignal, getState, transition } = createMachineCore({
1050
+ value: def.initial,
1051
+ context: def.context
1052
+ });
1053
+ const getContext = () => untrack(() => snapshotSignal.get().context);
1054
+ const setContext = (context) => {
1055
+ update(snapshotSignal, { context });
1056
+ };
1057
+ return {
1058
+ get snapshot() {
1059
+ return snapshotSignal;
1060
+ },
1061
+ send(message) {
1062
+ const state = getState();
1063
+ if (state === "destroyed") return;
1064
+ const handler = def.states[state]?.on?.[message.type];
1065
+ if (!handler) return;
1066
+ handler(message, {
1067
+ context: getContext(),
1068
+ getContext,
1069
+ transition: (to) => transition(to),
1070
+ setContext,
1071
+ ...runner ? { runner } : {}
1072
+ });
1073
+ const newState = getState();
1074
+ if (newState !== "destroyed") {
1075
+ const newStateDef = def.states[newState];
1076
+ if (newStateDef?.onSettled && runner) {
1077
+ const targetState = newStateDef.onSettled;
1078
+ runner.whenSettled(() => {
1079
+ if (getState() !== newState) return;
1080
+ transition(targetState);
1081
+ });
1082
+ }
1083
+ }
1084
+ },
1085
+ destroy() {
1086
+ if (getState() === "destroyed") return;
1087
+ runner?.destroy();
1088
+ transition("destroyed");
1089
+ }
1090
+ };
1091
+ }
1092
+
1093
+ //#endregion
1094
+ //#region ../spf/dist/dev/core/utils/generate-id.js
1095
+ /**
1096
+ * Generate unique ID for HAM objects.
1097
+ *
1098
+ * Uses timestamp + random number for sufficient uniqueness.
1099
+ * IDs are strings without decimals.
1100
+ *
1101
+ * @returns Unique string ID in format: timestamp-random
1102
+ *
1103
+ * @example
1104
+ * ```ts
1105
+ * const id = generateId(); // "1738423156789-542891"
1106
+ * ```
1107
+ */
1108
+ function generateId() {
1109
+ return `${Date.now()}-${Math.floor(Math.random() * 1e6)}`;
1110
+ }
1111
+
1112
+ //#endregion
1113
+ //#region ../spf/dist/dev/core/task.js
1114
+ /**
1115
+ * Generic reusable task that wraps an async run function.
1116
+ *
1117
+ * Owns its own AbortController so it can always be aborted independently.
1118
+ * Optionally composes an external AbortSignal so that a parent's cancellation
1119
+ * propagates into the task's work without requiring the caller to track the
1120
+ * task separately.
1121
+ *
1122
+ * Ordering guarantee: `value` is written before `status` transitions to `'done'`;
1123
+ * `error` is written before `status` transitions to `'error'`. Any reader
1124
+ * observing `status === 'done'` is guaranteed `value` is already present.
1125
+ */
1126
+ var Task = class {
1127
+ id;
1128
+ #runFn;
1129
+ #abortController = new AbortController();
1130
+ #signal;
1131
+ #status = "pending";
1132
+ #value = void 0;
1133
+ #error = void 0;
1134
+ constructor(runFn, config) {
1135
+ this.#runFn = runFn;
1136
+ const rawId = config?.id;
1137
+ this.id = typeof rawId === "function" ? rawId() : rawId ?? generateId();
1138
+ this.#signal = config?.signal ? anyAbortSignal([this.#abortController.signal, config.signal]) : this.#abortController.signal;
1139
+ }
1140
+ get status() {
1141
+ return this.#status;
1142
+ }
1143
+ get value() {
1144
+ return this.#value;
1145
+ }
1146
+ get error() {
1147
+ return this.#error;
1148
+ }
1149
+ async run() {
1150
+ this.#status = "running";
1151
+ try {
1152
+ const result = await this.#runFn(this.#signal);
1153
+ this.#value = result;
1154
+ this.#status = "done";
1155
+ return result;
1156
+ } catch (e) {
1157
+ this.#error = e;
1158
+ this.#status = "error";
1159
+ throw e;
1160
+ }
1161
+ }
1162
+ abort() {
1163
+ this.#abortController.abort();
1164
+ }
1165
+ };
1166
+ /**
1167
+ * Runs tasks concurrently, deduplicated by task id.
1168
+ *
1169
+ * If a task with a given id is already in flight, subsequent schedule() calls
1170
+ * for that id are silently ignored until the first completes. Tasks are stored
1171
+ * so abortAll() can cancel any in-flight work (e.g. on engine cleanup).
1172
+ */
1173
+ var ConcurrentRunner = class {
1174
+ #pending = /* @__PURE__ */ new Map();
1175
+ #settled = Promise.resolve();
1176
+ #resolveSettled = null;
1177
+ #destroyed = false;
1178
+ schedule(task) {
1179
+ if (this.#destroyed) return Promise.resolve();
1180
+ const existing = this.#pending.get(task.id);
1181
+ if (existing) return existing.promise;
1182
+ if (this.#pending.size === 0) this.#settled = new Promise((resolve) => {
1183
+ this.#resolveSettled = resolve;
1184
+ });
1185
+ const promise = task.run();
1186
+ promise.catch(() => {});
1187
+ const cleanup = () => {
1188
+ this.#pending.delete(task.id);
1189
+ if (this.#pending.size === 0) {
1190
+ this.#resolveSettled?.();
1191
+ this.#resolveSettled = null;
1192
+ }
1193
+ };
1194
+ promise.then(cleanup, cleanup);
1195
+ this.#pending.set(task.id, {
1196
+ task,
1197
+ promise
1198
+ });
1199
+ return promise;
1200
+ }
1201
+ /**
1202
+ * Registers a callback to fire when all currently in-flight tasks settle.
1203
+ * If the runner is already idle, the callback is never called. If abortAll()
1204
+ * is called before the batch settles, the callback is superseded and silently
1205
+ * dropped — no stale callbacks, no generation token required by the caller.
1206
+ */
1207
+ whenSettled(callback) {
1208
+ if (this.#pending.size === 0) return;
1209
+ const captured = this.#settled;
1210
+ captured.then(() => {
1211
+ if (this.#settled !== captured) return;
1212
+ callback();
1213
+ }, () => {});
1214
+ }
1215
+ abortAll() {
1216
+ for (const { task } of this.#pending.values()) task.abort();
1217
+ this.#pending.clear();
1218
+ this.#resolveSettled?.();
1219
+ this.#resolveSettled = null;
1220
+ this.#settled = Promise.resolve();
1221
+ }
1222
+ destroy() {
1223
+ this.#destroyed = true;
1224
+ this.abortAll();
1225
+ }
1226
+ };
1227
+ /**
1228
+ * Runs tasks one at a time in submission order.
1229
+ *
1230
+ * Each schedule() call returns a Promise that resolves or rejects with the
1231
+ * task's result when it is eventually executed. Tasks wait in queue until the
1232
+ * prior task completes.
1233
+ *
1234
+ * Serialization is achieved by chaining each task's run() onto the tail of a
1235
+ * shared promise chain — no explicit queue or drain loop needed.
1236
+ *
1237
+ * abortAll() aborts all pending (not yet started) tasks and the currently
1238
+ * in-flight task. Pending tasks still run briefly but receive an aborted
1239
+ * signal and are expected to exit early.
1240
+ */
1241
+ var SerialRunner = class {
1242
+ #chain = Promise.resolve();
1243
+ #pending = /* @__PURE__ */ new Set();
1244
+ #current = null;
1245
+ #destroyed = false;
1246
+ schedule(task) {
1247
+ if (this.#destroyed) return Promise.resolve();
1248
+ const t = task;
1249
+ this.#pending.add(t);
1250
+ const result = this.#chain.then(() => {
1251
+ this.#pending.delete(t);
1252
+ this.#current = t;
1253
+ return task.run();
1254
+ }).finally(() => {
1255
+ this.#current = null;
1256
+ });
1257
+ this.#chain = result.then(() => {}, () => {});
1258
+ return result;
1259
+ }
1260
+ /**
1261
+ * A promise that resolves when all currently-scheduled tasks have settled.
1262
+ * Use the reference as a generation token: capture it after scheduling a
1263
+ * batch, then check identity in the resolution callback to detect whether
1264
+ * a subsequent abortAll() + new batch has superseded this one.
1265
+ */
1266
+ get settled() {
1267
+ return this.#chain;
1268
+ }
1269
+ /**
1270
+ * Registers a callback to fire when all currently-pending tasks settle.
1271
+ * If the runner is already idle (no pending or running tasks), the callback
1272
+ * is never called. If new tasks are scheduled before the current batch
1273
+ * settles, the callback is superseded and silently dropped — no stale
1274
+ * callbacks, no generation token required by the caller.
1275
+ */
1276
+ whenSettled(callback) {
1277
+ if (this.#pending.size === 0 && this.#current === null) return;
1278
+ const currentChain = this.#chain;
1279
+ currentChain.then(() => {
1280
+ if (this.#chain !== currentChain) return;
1281
+ callback();
1282
+ }, () => {});
1283
+ }
1284
+ /** Aborts and clears queued tasks without touching the in-flight task. */
1285
+ abortPending() {
1286
+ for (const task of this.#pending) task.abort();
1287
+ this.#pending.clear();
1288
+ }
1289
+ abortAll() {
1290
+ this.abortPending();
1291
+ this.#current?.abort();
1292
+ }
1293
+ destroy() {
1294
+ this.#destroyed = true;
1295
+ this.abortAll();
1296
+ }
1297
+ };
1298
+
984
1299
  //#endregion
985
1300
  //#region ../spf/dist/dev/dom/features/segment-loader-actor.js
986
1301
  /**
1302
+ * Resolves when the SourceBufferActor snapshot reaches 'idle'.
1303
+ * Rejects if the signal is aborted or the actor is destroyed.
1304
+ *
1305
+ * Used to sequence SourceBufferActor operations without awaiting send()
1306
+ * directly — send() is fire-and-forget; callers observe completion via
1307
+ * state transition.
1308
+ */
1309
+ function waitForIdle(snapshot, signal) {
1310
+ return new Promise((resolve, reject) => {
1311
+ if (snapshot.get().value === "idle") {
1312
+ resolve();
1313
+ return;
1314
+ }
1315
+ if (snapshot.get().value === "destroyed") {
1316
+ reject(new DOMException("Aborted", "AbortError"));
1317
+ return;
1318
+ }
1319
+ if (signal.aborted) {
1320
+ reject(signal.reason);
1321
+ return;
1322
+ }
1323
+ let stop;
1324
+ const cleanup = (fn) => {
1325
+ stop?.();
1326
+ signal.removeEventListener("abort", onAbort);
1327
+ fn();
1328
+ };
1329
+ const onAbort = () => cleanup(() => reject(signal.reason));
1330
+ stop = effect(() => {
1331
+ const value = snapshot.get().value;
1332
+ if (value === "idle") cleanup(resolve);
1333
+ else if (value === "destroyed") cleanup(() => reject(new DOMException("Aborted", "AbortError")));
1334
+ });
1335
+ signal.addEventListener("abort", onAbort, { once: true });
1336
+ });
1337
+ }
1338
+ /**
1339
+ * Wraps a LoadTask descriptor into a Task that fetches (if needed) and
1340
+ * forwards to SourceBufferActor. Updates in-flight context around async
1341
+ * operations so the loading handler can make accurate continue/preempt
1342
+ * decisions at any point.
1343
+ */
1344
+ function makeLoadTask(op, { getContext, setContext, fetchBytes, sourceBufferActor }) {
1345
+ return new Task(async (taskSignal) => {
1346
+ if (taskSignal.aborted) return;
1347
+ if (op.type === "remove") {
1348
+ sourceBufferActor.send(op);
1349
+ await waitForIdle(sourceBufferActor.snapshot, taskSignal);
1350
+ return;
1351
+ }
1352
+ if (op.type === "append-init") {
1353
+ setContext({
1354
+ ...getContext(),
1355
+ inFlightInitTrackId: op.meta.trackId
1356
+ });
1357
+ try {
1358
+ const data = await fetchBytes(op, {
1359
+ signal: taskSignal,
1360
+ minChunkSize: Infinity
1361
+ });
1362
+ if (!taskSignal.aborted) {
1363
+ sourceBufferActor.send({
1364
+ type: "append-init",
1365
+ data,
1366
+ meta: op.meta
1367
+ });
1368
+ await waitForIdle(sourceBufferActor.snapshot, taskSignal);
1369
+ }
1370
+ } finally {
1371
+ setContext({
1372
+ ...getContext(),
1373
+ inFlightInitTrackId: null
1374
+ });
1375
+ }
1376
+ return;
1377
+ }
1378
+ setContext({
1379
+ ...getContext(),
1380
+ inFlightSegmentId: op.meta.id
1381
+ });
1382
+ try {
1383
+ const stream = await fetchBytes(op, { signal: taskSignal });
1384
+ if (!taskSignal.aborted) {
1385
+ sourceBufferActor.send({
1386
+ type: "append-segment",
1387
+ data: stream,
1388
+ meta: op.meta
1389
+ });
1390
+ await waitForIdle(sourceBufferActor.snapshot, taskSignal);
1391
+ }
1392
+ } finally {
1393
+ setContext({
1394
+ ...getContext(),
1395
+ inFlightSegmentId: null
1396
+ });
1397
+ }
1398
+ });
1399
+ }
1400
+ /**
987
1401
  * Creates a SegmentLoaderActor for one track type (video or audio).
988
1402
  *
989
1403
  * Receives load assignments via `send()` and owns all execution: planning,
990
1404
  * removes, fetches, and appends. Coordinates with the SourceBufferActor for
991
1405
  * all physical SourceBuffer operations.
992
1406
  *
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.
1407
+ * Planning (Cases 1–3) happens in the `load` handler on every incoming
1408
+ * message, producing an ordered LoadTask list. The runner drains that list
1409
+ * sequentially via SerialRunner. When a new message arrives mid-run, the
1410
+ * handler replans and either continues the in-flight operation (abortPending
1411
+ * + schedule new remainder) or preempts it (abortAll + cancel SourceBuffer
1412
+ * if needed + schedule new plan).
997
1413
  *
998
1414
  * @param sourceBufferActor - Shared SourceBufferActor reference (not owned)
999
1415
  * @param fetchBytes - Tracked fetch closure (owns throughput sampling for segments).
@@ -1001,19 +1417,13 @@ function calculateBackBufferFlushPoint(segments, currentTime, config = DEFAULT_B
1001
1417
  * so the entire body accumulates as one chunk before appending.
1002
1418
  */
1003
1419
  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
1420
  const getBufferedSegments = (allSegments) => {
1011
1421
  const bufferedIds = new Set(sourceBufferActor.snapshot.get().context.segments.filter((s) => !s.partial).map((s) => s.id));
1012
1422
  return allSegments.filter((s) => bufferedIds.has(s.id));
1013
1423
  };
1014
1424
  /**
1015
1425
  * Translate a load message into an ordered LoadTask list based on committed
1016
- * actor state. In-flight awareness is handled separately in send().
1426
+ * actor state. In-flight awareness is handled separately in the load handler.
1017
1427
  *
1018
1428
  * @todo Rename alongside LoadTask (e.g. planOps).
1019
1429
  *
@@ -1075,102 +1485,51 @@ function createSegmentLoaderActor(sourceBufferActor, fetchBytes) {
1075
1485
  }
1076
1486
  return tasks;
1077
1487
  };
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
- }
1124
- };
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;
1150
- }
1151
- abortController = null;
1152
- running = false;
1488
+ const scheduleAll = (tasks, { getContext, setContext, runner }) => {
1489
+ tasks.forEach((op) => {
1490
+ runner.schedule(makeLoadTask(op, {
1491
+ getContext,
1492
+ setContext,
1493
+ fetchBytes,
1494
+ sourceBufferActor
1495
+ })).then(void 0, (e) => {
1496
+ if (e instanceof Error && e.name === "AbortError") return;
1497
+ console.error("Unexpected error in segment loader:", e);
1498
+ runner.abortPending();
1499
+ });
1500
+ });
1153
1501
  };
1154
- return {
1155
- send(message) {
1156
- if (destroyed) return;
1157
- const allTasks = planTasks(message);
1158
- if (!running) {
1502
+ return createMachineActor({
1503
+ runner: () => new SerialRunner(),
1504
+ initial: "idle",
1505
+ context: {
1506
+ inFlightInitTrackId: null,
1507
+ inFlightSegmentId: null
1508
+ },
1509
+ states: {
1510
+ idle: { on: { load: (msg, ctx) => {
1511
+ const allTasks = planTasks(msg);
1159
1512
  if (allTasks.length === 0) return;
1160
- runScheduled(allTasks);
1161
- return;
1162
- }
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();
1513
+ ctx.transition("loading");
1514
+ scheduleAll(allTasks, ctx);
1515
+ } } },
1516
+ loading: {
1517
+ onSettled: "idle",
1518
+ on: { load: (msg, ctx) => {
1519
+ const { context, runner } = ctx;
1520
+ const allTasks = planTasks(msg);
1521
+ if (context.inFlightSegmentId !== null && allTasks.some((t) => t.type === "append-segment" && t.meta.id === context.inFlightSegmentId) || context.inFlightInitTrackId !== null && allTasks.some((t) => t.type === "append-init" && t.meta.trackId === context.inFlightInitTrackId)) {
1522
+ runner.abortPending();
1523
+ scheduleAll(allTasks.filter((t) => !(t.type === "append-segment" && t.meta.id === context.inFlightSegmentId) && !(t.type === "append-init" && t.meta.trackId === context.inFlightInitTrackId)), ctx);
1524
+ } else {
1525
+ runner.abortAll();
1526
+ if (context.inFlightSegmentId !== null || context.inFlightInitTrackId !== null && allTasks.some((t) => t.type === "append-init" && t.meta.trackId !== context.inFlightInitTrackId)) sourceBufferActor.send({ type: "cancel" });
1527
+ scheduleAll(allTasks, ctx);
1528
+ }
1529
+ } }
1167
1530
  }
1168
- },
1169
- destroy() {
1170
- destroyed = true;
1171
- abortController?.abort();
1172
1531
  }
1173
- };
1532
+ });
1174
1533
  }
1175
1534
 
1176
1535
  //#endregion
@@ -1409,157 +1768,308 @@ function destroyVttParser() {
1409
1768
  }
1410
1769
 
1411
1770
  //#endregion
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
- };
1429
- /**
1430
- * Load text track cues task (composite - orchestrates VTT segment subtasks).
1431
- */
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);
1458
- }
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
- };
1771
+ //#region ../spf/dist/dev/core/create-machine-reactor.js
1772
+ const toArray = (x) => x === void 0 ? [] : Array.isArray(x) ? x : [x];
1465
1773
  /**
1466
- * Find the selected text track in the presentation.
1467
- */
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);
1473
- }
1474
- /**
1475
- * Get the browser's TextTrack object for the selected text track.
1774
+ * Creates a reactive Reactor from a declarative definition.
1476
1775
  *
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.
1776
+ * A Reactor is driven by subscriptions to external signals rather than
1777
+ * imperative messages. Each state holds an array of effect functions
1778
+ * every element becomes one independent `effect()` call gated on that state,
1779
+ * with its own dependency tracking and cleanup lifecycle.
1479
1780
  *
1480
- * Note: Returns the DOM TextTrack interface (HTMLTrackElement.track),
1481
- * not the presentation Track metadata type.
1781
+ * `'destroying'` and `'destroyed'` are always implicit terminal states.
1782
+ * `destroy()` transitions through both in sequence: `'destroying'` first (for
1783
+ * potential async teardown in a future extension), then immediately `'destroyed'`
1784
+ * for the synchronous base case. Active effect cleanups fire via disposal.
1482
1785
  *
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
1786
+ * @example
1787
+ * const reactor = createMachineReactor({
1788
+ * initial: 'waiting',
1789
+ * monitor: () => srcSignal.get() ? 'active' : 'waiting',
1790
+ * states: {
1791
+ * active: {
1792
+ * // entry: runs once on state entry; fn body is automatically untracked.
1793
+ * entry: () => listen(el, 'play', handler),
1794
+ * // effects: re-runs whenever tracked signals change.
1795
+ * effects: () => { currentTimeSignal.get(); return cleanup; },
1796
+ * },
1797
+ * waiting: {},
1798
+ * }
1799
+ * });
1486
1800
  */
1487
- function getSelectedTextTrackFromOwners(state, owners) {
1488
- const trackId = state.selectedTextTrackId;
1489
- if (!trackId || !owners.textTracks) return;
1490
- return owners.textTracks.get(trackId)?.track;
1801
+ function createMachineReactor(def) {
1802
+ const { snapshotSignal, getState, transition } = createMachineCore({ value: def.initial });
1803
+ const effectDisposals = [];
1804
+ const wrapResult = (result) => {
1805
+ if (!result) return void 0;
1806
+ if (typeof result === "function") return result;
1807
+ return () => result.abort();
1808
+ };
1809
+ const untracked = (baseCall) => () => untrack(baseCall);
1810
+ const isTerminal = (snapshot) => snapshot.value === "destroying" || snapshot.value === "destroyed";
1811
+ const descriptors = [...toArray(def.monitor).map((fn) => ({
1812
+ fn: () => {
1813
+ const target = fn();
1814
+ if (target !== getState()) transition(target);
1815
+ },
1816
+ shouldSkip: isTerminal
1817
+ })), ...Object.entries(def.states).flatMap(([state, stateDef]) => {
1818
+ const isNotState = (snapshot) => snapshot.value !== state;
1819
+ return [...toArray(stateDef.entry).map((fn) => ({
1820
+ fn,
1821
+ shouldSkip: isNotState,
1822
+ toFnCall: untracked
1823
+ })), ...toArray(stateDef.effects).map((fn) => ({
1824
+ fn,
1825
+ shouldSkip: isNotState
1826
+ }))];
1827
+ })];
1828
+ const toEffect = ({ fn, shouldSkip, toFnCall = (baseCall) => baseCall }) => effect(() => {
1829
+ if (shouldSkip(snapshotSignal.get())) return;
1830
+ const baseCall = () => fn();
1831
+ return wrapResult(toFnCall(baseCall)());
1832
+ });
1833
+ effectDisposals.push(...descriptors.map(toEffect));
1834
+ return {
1835
+ get snapshot() {
1836
+ return snapshotSignal;
1837
+ },
1838
+ destroy() {
1839
+ const state = getState();
1840
+ if (state === "destroying" || state === "destroyed") return;
1841
+ transition("destroying");
1842
+ transition("destroyed");
1843
+ for (const dispose of effectDisposals) dispose();
1844
+ }
1845
+ };
1491
1846
  }
1847
+
1848
+ //#endregion
1849
+ //#region ../spf/dist/dev/dom/features/text-track-segment-loader-actor.js
1492
1850
  /**
1493
- * Check if we can load text track cues.
1851
+ * Loads VTT segments for a text track and delegates cue management to a
1852
+ * TextTracksActor. Mirrors the SegmentLoaderActor/SourceBufferActor pattern
1853
+ * for the text track equivalent.
1494
1854
  *
1495
- * Requires:
1496
- * - Selected text track ID exists
1497
- * - Track elements map exists
1498
- * - Track element exists for selected track
1855
+ * Planning is done in the load handler: segments already recorded in
1856
+ * TextTracksActor's context are skipped. Each load preempts in-flight work
1857
+ * via abortAll() before scheduling fresh tasks.
1499
1858
  */
1500
- function canLoadTextTrackCues(state, owners) {
1501
- return !!state.selectedTextTrackId && !!owners.textTracks && owners.textTracks.has(state.selectedTextTrackId);
1859
+ function createTextTrackSegmentLoaderActor(textTracksActor) {
1860
+ const runner = new SerialRunner();
1861
+ let destroyed = false;
1862
+ return {
1863
+ send({ track, currentTime }) {
1864
+ if (destroyed) return;
1865
+ const trackId = track.id;
1866
+ const bufferedSegments = untrack(() => textTracksActor.snapshot.get().context.segments[trackId] ?? []);
1867
+ const segmentsToLoad = getSegmentsToLoad(track.segments, bufferedSegments, currentTime);
1868
+ runner.abortAll();
1869
+ for (const segment of segmentsToLoad) runner.schedule(new Task(async (signal) => {
1870
+ if (signal.aborted) return;
1871
+ try {
1872
+ const cues = await parseVttSegment(segment.url);
1873
+ if (signal.aborted) return;
1874
+ textTracksActor.send({
1875
+ type: "add-cues",
1876
+ meta: {
1877
+ trackId,
1878
+ id: segment.id,
1879
+ startTime: segment.startTime,
1880
+ duration: segment.duration
1881
+ },
1882
+ cues
1883
+ });
1884
+ } catch (error) {
1885
+ console.error("Failed to load VTT segment:", error);
1886
+ }
1887
+ }));
1888
+ },
1889
+ destroy() {
1890
+ if (destroyed) return;
1891
+ destroyed = true;
1892
+ runner.destroy();
1893
+ }
1894
+ };
1502
1895
  }
1896
+
1897
+ //#endregion
1898
+ //#region ../spf/dist/dev/core/create-transition-actor.js
1503
1899
  /**
1504
- * Check if we should load text track cues.
1900
+ * Creates a reducer-shaped actor from an initial context and a reducer function.
1505
1901
  *
1506
- * Only load if:
1507
- * - Track is resolved (has segments)
1508
- * - Track has at least one segment
1509
- * - Track element exists
1902
+ * The reducer receives the current context and a message and returns the next
1903
+ * context. Returning the same reference (by identity) skips the signal update —
1904
+ * so early-returning `context` unchanged is both the no-op and the optimization.
1905
+ *
1906
+ * Side effects (e.g. DOM mutations) may be performed inside the reducer.
1907
+ * They run synchronously before the signal is updated.
1908
+ *
1909
+ * @example
1910
+ * const actor = createTransitionActor(
1911
+ * { count: 0 },
1912
+ * (context, message: { type: 'increment' }) => ({ count: context.count + 1 })
1913
+ * );
1510
1914
  */
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;
1915
+ function createTransitionActor(initialContext, reducer) {
1916
+ const { snapshotSignal, getState, transition } = createMachineCore({
1917
+ value: "active",
1918
+ context: initialContext
1919
+ });
1920
+ const getContext = () => untrack(() => snapshotSignal.get().context);
1921
+ const setContext = (context) => update(snapshotSignal, { context });
1922
+ return {
1923
+ get snapshot() {
1924
+ return snapshotSignal;
1925
+ },
1926
+ send(message) {
1927
+ if (getState() === "destroyed") return;
1928
+ const context = getContext();
1929
+ const newContext = reducer(context, message);
1930
+ if (newContext !== context) setContext(newContext);
1931
+ },
1932
+ destroy() {
1933
+ if (getState() === "destroyed") return;
1934
+ transition("destroyed");
1935
+ }
1936
+ };
1937
+ }
1938
+
1939
+ //#endregion
1940
+ //#region ../spf/dist/dev/dom/features/text-tracks-actor.js
1941
+ function isDuplicateCue(cue, existing) {
1942
+ return existing.some((r) => r.startTime === cue.startTime && r.endTime === cue.endTime && r.text === cue.text);
1943
+ }
1944
+ /** TextTrack actor: wraps all text tracks on a media element, owns cue operations. */
1945
+ function createTextTracksActor(mediaElement) {
1946
+ return createTransitionActor({
1947
+ loaded: {},
1948
+ segments: {}
1949
+ }, (context, message) => {
1950
+ const { meta, cues } = message;
1951
+ const { trackId, id: segmentId, startTime, duration } = meta;
1952
+ const textTrack = Array.from(mediaElement.textTracks).find((t) => t.id === trackId);
1953
+ if (!textTrack) return context;
1954
+ const existingCues = context.loaded[trackId] ?? [];
1955
+ const existingSegments = context.segments[trackId] ?? [];
1956
+ const prunedCues = cues.filter((cue) => !isDuplicateCue(cue, existingCues));
1957
+ const segmentAlreadyLoaded = existingSegments.some((s) => s.id === segmentId);
1958
+ if (prunedCues.length === 0 && segmentAlreadyLoaded) return context;
1959
+ for (const cue of prunedCues) textTrack.addCue(cue);
1960
+ return {
1961
+ ...context,
1962
+ loaded: {
1963
+ ...context.loaded,
1964
+ [trackId]: [...existingCues, ...prunedCues]
1965
+ },
1966
+ segments: segmentAlreadyLoaded ? context.segments : {
1967
+ ...context.segments,
1968
+ [trackId]: [...existingSegments, {
1969
+ id: segmentId,
1970
+ startTime,
1971
+ duration
1972
+ }]
1973
+ }
1974
+ };
1975
+ });
1976
+ }
1977
+
1978
+ //#endregion
1979
+ //#region ../spf/dist/dev/dom/features/load-text-track-cues.js
1980
+ function getTextTracks(presentation) {
1981
+ return presentation?.selectionSets?.find((s) => s.type === "text")?.switchingSets[0]?.tracks;
1982
+ }
1983
+ function findSelectedTrack(state) {
1984
+ const track = getTextTracks(state.presentation)?.find((t) => t.id === state.selectedTextTrackId);
1985
+ return track && isResolvedTrack(track) ? track : void 0;
1986
+ }
1987
+ /**
1988
+ * Derives the correct state from current state and owners.
1989
+ *
1990
+ * States are mutually exclusive and exhaustive:
1991
+ * - `'preconditions-unmet'`: no mediaElement, or no resolved presentation with text tracks
1992
+ * - `'setting-up'`: preconditions met; actors not yet in owners
1993
+ * - `'pending'`: actors alive; no selection, or selected track not yet resolved/in DOM
1994
+ * - `'monitoring-for-loads'`: selected track resolved, in DOM — ready to dispatch load messages
1995
+ */
1996
+ function deriveState$2(state, owners) {
1997
+ if (!owners.mediaElement || !getTextTracks(state.presentation)?.length) return "preconditions-unmet";
1998
+ if (!owners.textTracksActor || !owners.segmentLoaderActor) return "setting-up";
1999
+ const track = findSelectedTrack(state);
2000
+ if (!track || track.segments.length === 0) return "pending";
2001
+ if (!Array.from(owners.mediaElement.textTracks).some((t) => t.id === state.selectedTextTrackId)) return "pending";
2002
+ return "monitoring-for-loads";
2003
+ }
2004
+ function teardownActors(owners) {
2005
+ const { textTracksActor, segmentLoaderActor } = untrack(() => owners.get());
2006
+ if (!textTracksActor && !segmentLoaderActor) return;
2007
+ textTracksActor?.destroy();
2008
+ segmentLoaderActor?.destroy();
2009
+ update(owners, {
2010
+ textTracksActor: void 0,
2011
+ segmentLoaderActor: void 0
2012
+ });
1517
2013
  }
1518
2014
  /**
1519
- * Load text track cues orchestration.
2015
+ * Text track cue loading orchestration.
2016
+ *
2017
+ * A single `always` monitor keeps the reactor in sync with conditions.
2018
+ * Actor lifecycle is managed across two states:
1520
2019
  *
1521
- * Triggers when:
1522
- * - Text track is selected
1523
- * - Track is resolved (has segments)
1524
- * - Track element exists
2020
+ * - **`'setting-up'`** — creates `TextTracksActor` and `TextTrackSegmentLoaderActor`
2021
+ * and writes them to owners. Entry resets any stale actors first.
2022
+ * - **`'preconditions-unmet'`** destroys any actors in owners and resets them to
2023
+ * `undefined`. Handles all paths back from active states.
2024
+ * - **`'monitoring-for-loads'`** — reactive dispatch: re-runs whenever `state`
2025
+ * changes (selection, currentTime, presentation) and sends a `load` message to
2026
+ * the segment loader.
1525
2027
  *
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.
2028
+ * **Destroy note:** actors written to owners are NOT auto-destroyed when
2029
+ * `reactor.destroy()` is called. Callers must read them from owners and destroy
2030
+ * them explicitly alongside the reactor.
1529
2031
  *
1530
2032
  * @example
1531
- * const cleanup = loadTextTrackCues({ state, owners });
2033
+ * const reactor = loadTextTrackCues({ state, owners });
2034
+ * // later:
2035
+ * const { textTracksActor, segmentLoaderActor } = owners.get();
2036
+ * textTracksActor?.destroy();
2037
+ * segmentLoaderActor?.destroy();
2038
+ * reactor.destroy();
1532
2039
  */
1533
2040
  function loadTextTrackCues({ state, owners }) {
1534
- let currentTask = null;
1535
- let 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;
2041
+ const derivedStateSignal = computed(() => deriveState$2(state.get(), owners.get()));
2042
+ const currentTimeSignal = computed(() => state.get().currentTime ?? 0);
2043
+ const selectedTrackSignal = computed(() => findSelectedTrack(state.get()));
2044
+ return createMachineReactor({
2045
+ initial: "preconditions-unmet",
2046
+ monitor: () => derivedStateSignal.get(),
2047
+ states: {
2048
+ "preconditions-unmet": { entry: () => {
2049
+ teardownActors(owners);
2050
+ } },
2051
+ "setting-up": { entry: () => {
2052
+ teardownActors(owners);
2053
+ const mediaElement = owners.get().mediaElement;
2054
+ const textTracksActor = createTextTracksActor(mediaElement);
2055
+ update(owners, {
2056
+ textTracksActor,
2057
+ segmentLoaderActor: createTextTrackSegmentLoaderActor(textTracksActor)
2058
+ });
2059
+ } },
2060
+ pending: {},
2061
+ "monitoring-for-loads": { effects: () => {
2062
+ const currentTime = currentTimeSignal.get();
2063
+ const track = selectedTrackSignal.get();
2064
+ const { segmentLoaderActor } = untrack(() => owners.get());
2065
+ segmentLoaderActor.send({
2066
+ type: "load",
2067
+ track,
2068
+ currentTime
2069
+ });
2070
+ } }
1545
2071
  }
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
- });
1558
2072
  });
1559
- return () => {
1560
- abortController?.abort();
1561
- cleanupEffect();
1562
- };
1563
2073
  }
1564
2074
 
1565
2075
  //#endregion
@@ -1609,49 +2119,62 @@ function trackCurrentTime({ state, owners }) {
1609
2119
  //#endregion
1610
2120
  //#region ../spf/dist/dev/dom/features/track-playback-initiated.js
1611
2121
  /**
2122
+ * FSM states for playback initiation tracking.
2123
+ *
2124
+ * ```
2125
+ * 'preconditions-unmet' ──── element + URL ────→ 'monitoring'
2126
+ * ↑ ↑ │
2127
+ * │ preconditions lost │ play / !paused
2128
+ * │ │ ↓
2129
+ * └────────────── 'playback-initiated' ←──────┘
2130
+ * (exit cleanup resets state.playbackInitiated → false)
2131
+ *
2132
+ * any state ──── destroy() ────→ 'destroying' ────→ 'destroyed'
2133
+ * ```
2134
+ */
2135
+ function deriveState$1(state, owners) {
2136
+ if (!owners.mediaElement || !state.presentation?.url) return "preconditions-unmet";
2137
+ if (state.playbackInitiated) return "playback-initiated";
2138
+ return "monitoring";
2139
+ }
2140
+ /**
1612
2141
  * Track whether playback has been initiated for the current presentation URL.
1613
2142
  *
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.
2143
+ * A three-state Reactor FSM driven by `state.playbackInitiated` and the
2144
+ * `deriveState` pattern:
2145
+ * - `'preconditions-unmet'` no element or URL yet; no effects.
2146
+ * - `'monitoring'` — checks `!el.paused` on entry; listens for `play`.
2147
+ * - `'playback-initiated'` tracks element and URL; exit cleanup resets
2148
+ * `state.playbackInitiated` to `false` on any change or lost preconditions.
1621
2149
  *
1622
2150
  * @example
1623
- * const cleanup = trackPlaybackInitiated({ state, owners, events });
2151
+ * const reactor = trackPlaybackInitiated({ state, owners });
2152
+ * // later:
2153
+ * reactor.destroy();
1624
2154
  */
1625
2155
  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 });
2156
+ const derivedStateSignal = computed(() => deriveState$1(state.get(), owners.get()));
2157
+ const mediaElementSignal = computed(() => owners.get().mediaElement);
2158
+ const urlSignal = computed(() => state.get().presentation?.url);
2159
+ return createMachineReactor({
2160
+ initial: "preconditions-unmet",
2161
+ monitor: () => derivedStateSignal.get(),
2162
+ states: {
2163
+ "preconditions-unmet": {},
2164
+ monitoring: { entry: () => {
2165
+ const el = mediaElementSignal.get();
2166
+ update(state, { playbackInitiated: !el.paused });
2167
+ return listen(el, "play", () => {
2168
+ update(state, { playbackInitiated: !el.paused });
2169
+ });
2170
+ } },
2171
+ "playback-initiated": { effects: () => {
2172
+ mediaElementSignal.get();
2173
+ urlSignal.get();
2174
+ return () => update(state, { playbackInitiated: false });
2175
+ } }
2176
+ }
1649
2177
  });
1650
- return () => {
1651
- cleanupResetEffect();
1652
- cleanupPlayEffect();
1653
- cleanupMergeEffect();
1654
- };
1655
2178
  }
1656
2179
 
1657
2180
  //#endregion
@@ -1927,25 +2450,6 @@ function switchQuality({ state }, config = {}) {
1927
2450
  });
1928
2451
  }
1929
2452
 
1930
- //#endregion
1931
- //#region ../spf/dist/dev/core/utils/generate-id.js
1932
- /**
1933
- * Generate unique ID for HAM objects.
1934
- *
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
- * ```
1944
- */
1945
- function generateId() {
1946
- return `${Date.now()}-${Math.floor(Math.random() * 1e6)}`;
1947
- }
1948
-
1949
2453
  //#endregion
1950
2454
  //#region ../spf/dist/dev/core/hls/parse-attributes.js
1951
2455
  /**
@@ -2299,15 +2803,6 @@ function parseMultivariantPlaylist(text, unresolved) {
2299
2803
  //#endregion
2300
2804
  //#region ../spf/dist/dev/core/features/resolve-presentation.js
2301
2805
  /**
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
2806
  * Determines if resolution conditions are met based on preload policy and playback state.
2312
2807
  *
2313
2808
  * Resolution conditions:
@@ -2322,49 +2817,54 @@ function shouldResolve(state) {
2322
2817
  return ["auto", "metadata"].includes(preload) || !!playbackInitiated;
2323
2818
  }
2324
2819
  /**
2820
+ * Derives the correct state from current state conditions.
2821
+ *
2822
+ * States are mutually exclusive and exhaustive:
2823
+ * - `'preconditions-unmet'`: no presentation, or presentation has no URL
2824
+ * - `'idle'`: URL present, unresolved (no id), shouldResolve not met
2825
+ * - `'resolving'`: URL present, unresolved (no id), shouldResolve met
2826
+ * - `'resolved'`: URL present, resolved (has id)
2827
+ */
2828
+ function deriveState(state) {
2829
+ const { presentation } = state;
2830
+ if (!presentation || !("url" in presentation)) return "preconditions-unmet";
2831
+ if ("id" in presentation) return "resolved";
2832
+ return shouldResolve(state) ? "resolving" : "idle";
2833
+ }
2834
+ /**
2325
2835
  * Resolves unresolved presentations using reactive composition.
2326
2836
  *
2327
- * Triggers resolution when:
2328
- * - State-driven: Unresolved presentation + preload allows (auto/metadata)
2329
- * - Playback-driven: playbackInitiated is true
2837
+ * FSM driven by `deriveState` — a single `always` monitor keeps the state in
2838
+ * sync with conditions at all times. `'resolving'` additionally runs the fetch
2839
+ * task and returns an AbortController so the framework aborts it on state exit.
2330
2840
  *
2331
2841
  * @example
2332
- * ```ts
2333
- * const state = signal({ presentation: undefined, preload: 'auto', playbackInitiated: false });
2334
- *
2335
- * const cleanup = resolvePresentation({ state });
2336
- *
2337
- * // State-driven: resolves immediately when preload allows
2338
- * state.set({ ...state.get(), presentation: { url: 'http://example.com/playlist.m3u8' } });
2339
- *
2340
- * // Playback-driven: resolves when playbackInitiated is set
2341
- * state.set({ ...state.get(), preload: 'none', presentation: { url: '...' }, playbackInitiated: true });
2342
- * ```
2842
+ * const reactor = resolvePresentation({ state });
2843
+ * // later:
2844
+ * reactor.destroy();
2343
2845
  */
2344
2846
  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;
2362
- });
2847
+ const derivedStateSignal = computed(() => deriveState(state.get()));
2848
+ return createMachineReactor({
2849
+ initial: "preconditions-unmet",
2850
+ monitor: () => derivedStateSignal.get(),
2851
+ states: {
2852
+ "preconditions-unmet": {},
2853
+ idle: {},
2854
+ resolving: { entry: () => {
2855
+ const presentation = state.get().presentation;
2856
+ const ac = new AbortController();
2857
+ fetchResolvable(presentation, { signal: ac.signal }).then((response) => getResponseText(response)).then((text) => {
2858
+ update(state, { presentation: parseMultivariantPlaylist(text, presentation) });
2859
+ }).catch((error) => {
2860
+ if (error instanceof Error && error.name === "AbortError") return;
2861
+ throw error;
2862
+ });
2863
+ return ac;
2864
+ } },
2865
+ resolved: {}
2866
+ }
2363
2867
  });
2364
- return () => {
2365
- abortController?.abort();
2366
- cleanupEffect();
2367
- };
2368
2868
  }
2369
2869
 
2370
2870
  //#endregion
@@ -2445,124 +2945,6 @@ function parseMediaPlaylist(text, unresolved) {
2445
2945
  };
2446
2946
  }
2447
2947
 
2448
- //#endregion
2449
- //#region ../spf/dist/dev/core/task.js
2450
- /**
2451
- * Generic reusable task that wraps an async run function.
2452
- *
2453
- * Owns its own AbortController so it can always be aborted independently.
2454
- * Optionally composes an external AbortSignal so that a parent's cancellation
2455
- * propagates into the task's work without requiring the caller to track the
2456
- * task separately.
2457
- *
2458
- * Ordering guarantee: `value` is written before `status` transitions to `'done'`;
2459
- * `error` is written before `status` transitions to `'error'`. Any reader
2460
- * observing `status === 'done'` is guaranteed `value` is already present.
2461
- */
2462
- var Task = class {
2463
- id;
2464
- #runFn;
2465
- #abortController = new AbortController();
2466
- #signal;
2467
- #status = "pending";
2468
- #value = void 0;
2469
- #error = void 0;
2470
- constructor(runFn, config) {
2471
- this.#runFn = runFn;
2472
- const rawId = config?.id;
2473
- this.id = typeof rawId === "function" ? rawId() : rawId ?? generateId();
2474
- this.#signal = config?.signal ? anyAbortSignal([this.#abortController.signal, config.signal]) : this.#abortController.signal;
2475
- }
2476
- get status() {
2477
- return this.#status;
2478
- }
2479
- get value() {
2480
- return this.#value;
2481
- }
2482
- get error() {
2483
- return this.#error;
2484
- }
2485
- async run() {
2486
- this.#status = "running";
2487
- try {
2488
- const result = await this.#runFn(this.#signal);
2489
- this.#value = result;
2490
- this.#status = "done";
2491
- return result;
2492
- } catch (e) {
2493
- this.#error = e;
2494
- this.#status = "error";
2495
- throw e;
2496
- }
2497
- }
2498
- abort() {
2499
- this.#abortController.abort();
2500
- }
2501
- };
2502
- /**
2503
- * Runs tasks concurrently, deduplicated by task id.
2504
- *
2505
- * If a task with a given id is already in flight, subsequent schedule() calls
2506
- * for that id are silently ignored until the first completes. Tasks are stored
2507
- * so abortAll() can cancel any in-flight work (e.g. on engine cleanup).
2508
- */
2509
- var ConcurrentRunner = class {
2510
- #pending = /* @__PURE__ */ new Map();
2511
- schedule(task) {
2512
- if (this.#pending.has(task.id)) return;
2513
- this.#pending.set(task.id, task);
2514
- task.run().catch((error) => {
2515
- if (!(error instanceof Error && error.name === "AbortError")) throw error;
2516
- }).finally(() => {
2517
- this.#pending.delete(task.id);
2518
- });
2519
- }
2520
- abortAll() {
2521
- for (const task of this.#pending.values()) task.abort();
2522
- this.#pending.clear();
2523
- }
2524
- };
2525
- /**
2526
- * Runs tasks one at a time in submission order.
2527
- *
2528
- * Each schedule() call returns a Promise that resolves or rejects with the
2529
- * task's result when it is eventually executed. Tasks wait in queue until the
2530
- * prior task completes.
2531
- *
2532
- * Serialization is achieved by chaining each task's run() onto the tail of a
2533
- * shared promise chain — no explicit queue or drain loop needed.
2534
- *
2535
- * abortAll() aborts all pending (not yet started) tasks and the currently
2536
- * in-flight task. Pending tasks still run briefly but receive an aborted
2537
- * signal and are expected to exit early.
2538
- */
2539
- var SerialRunner = class {
2540
- #chain = Promise.resolve();
2541
- #pending = /* @__PURE__ */ new Set();
2542
- #current = null;
2543
- schedule(task) {
2544
- const t = task;
2545
- this.#pending.add(t);
2546
- const result = this.#chain.then(() => {
2547
- this.#pending.delete(t);
2548
- this.#current = t;
2549
- return task.run();
2550
- }).finally(() => {
2551
- this.#current = null;
2552
- });
2553
- this.#chain = result.then(() => {}, () => {});
2554
- return result;
2555
- }
2556
- abortAll() {
2557
- for (const task of this.#pending) task.abort();
2558
- this.#pending.clear();
2559
- this.#current?.abort();
2560
- }
2561
- destroy() {
2562
- this.abortAll();
2563
- }
2564
- };
2565
-
2566
2948
  //#endregion
2567
2949
  //#region ../spf/dist/dev/core/features/resolve-track.js
2568
2950
  function canResolve(state, config) {
@@ -2820,8 +3202,8 @@ function shouldEndStream(state, owners) {
2820
3202
  const hasAudioTrack = !!state.selectedAudioTrackId;
2821
3203
  if (hasVideoTrack && !owners.videoBuffer) return false;
2822
3204
  if (hasAudioTrack && !owners.audioBuffer) return false;
2823
- if (owners.videoBufferActor?.snapshot.get().status === "updating") return false;
2824
- if (owners.audioBufferActor?.snapshot.get().status === "updating") return false;
3205
+ if (owners.videoBufferActor?.snapshot.get().value === "updating") return false;
3206
+ if (owners.audioBufferActor?.snapshot.get().value === "updating") return false;
2825
3207
  if (!hasLastSegmentLoaded(state, owners)) return false;
2826
3208
  if (mediaElement) {
2827
3209
  const videoTrack = hasVideoTrack ? getSelectedTrack(state, "video") : void 0;
@@ -2836,17 +3218,17 @@ function shouldEndStream(state, owners) {
2836
3218
  }
2837
3219
  /**
2838
3220
  * Wait for all currently-updating SourceBufferActors to finish.
2839
- * Uses actor status rather than raw SourceBuffer.updating so the wait is
3221
+ * Uses actor state rather than raw SourceBuffer.updating so the wait is
2840
3222
  * aligned with the same abstraction that owns all buffer operations.
2841
3223
  */
2842
3224
  function waitForSourceBuffersReady$1(owners) {
2843
- const updatingActors = [owners.videoBufferActor, owners.audioBufferActor].filter((actor) => actor !== void 0 && actor.snapshot.get().status === "updating");
3225
+ const updatingActors = [owners.videoBufferActor, owners.audioBufferActor].filter((actor) => actor !== void 0 && actor.snapshot.get().value === "updating");
2844
3226
  if (updatingActors.length === 0) return Promise.resolve();
2845
3227
  return Promise.all(updatingActors.map((actor) => new Promise((resolve) => {
2846
3228
  let cleanup;
2847
3229
  let resolved = false;
2848
3230
  cleanup = effect(() => {
2849
- if (actor.snapshot.get().status !== "updating") {
3231
+ if (actor.snapshot.get().value !== "updating") {
2850
3232
  if (!resolved) {
2851
3233
  resolved = true;
2852
3234
  resolve();
@@ -3095,16 +3477,6 @@ function setupMediaSource({ state, owners }) {
3095
3477
 
3096
3478
  //#endregion
3097
3479
  //#region ../spf/dist/dev/dom/media/source-buffer-actor.js
3098
- /**
3099
- * Thrown when a message is sent to the actor in a state that does not
3100
- * accept messages (currently: 'updating').
3101
- */
3102
- var SourceBufferActorError = class extends Error {
3103
- constructor(message) {
3104
- super(message);
3105
- this.name = "SourceBufferActorError";
3106
- }
3107
- };
3108
3480
  function snapshotBuffered(buffered) {
3109
3481
  const ranges = [];
3110
3482
  for (let i = 0; i < buffered.length; i++) ranges.push({
@@ -3113,25 +3485,25 @@ function snapshotBuffered(buffered) {
3113
3485
  });
3114
3486
  return ranges;
3115
3487
  }
3116
- function appendInitTask(message, { signal, getCtx, sourceBuffer }) {
3488
+ function appendInitTask(message, { getContext, sourceBuffer }) {
3117
3489
  return new Task(async (taskSignal) => {
3118
- const ctx = getCtx();
3490
+ const ctx = getContext();
3119
3491
  if (taskSignal.aborted) return ctx;
3120
3492
  await appendSegment(sourceBuffer, message.data);
3121
3493
  return {
3122
3494
  ...ctx,
3123
3495
  initTrackId: message.meta.trackId
3124
3496
  };
3125
- }, { signal });
3497
+ });
3126
3498
  }
3127
- function appendSegmentTask(message, { signal, getCtx, sourceBuffer, onPartialContext }) {
3499
+ function appendSegmentTask(message, { getContext, sourceBuffer, setContext }) {
3128
3500
  return new Task(async (taskSignal) => {
3129
- const ctx = getCtx();
3501
+ const ctx = getContext();
3130
3502
  if (taskSignal.aborted) return ctx;
3131
3503
  const { meta } = message;
3132
3504
  const EPSILON = 1e-4;
3133
3505
  const filtered = ctx.segments.filter((s) => Math.abs(s.startTime - meta.startTime) >= EPSILON);
3134
- if (!(message.data instanceof ArrayBuffer)) onPartialContext({
3506
+ if (!(message.data instanceof ArrayBuffer)) setContext({
3135
3507
  ...ctx,
3136
3508
  segments: [...filtered, {
3137
3509
  id: meta.id,
@@ -3155,11 +3527,11 @@ function appendSegmentTask(message, { signal, getCtx, sourceBuffer, onPartialCon
3155
3527
  }],
3156
3528
  bufferedRanges: snapshotBuffered(sourceBuffer.buffered)
3157
3529
  };
3158
- }, { signal });
3530
+ });
3159
3531
  }
3160
- function removeTask(message, { signal, getCtx, sourceBuffer }) {
3532
+ function removeTask(message, { getContext, sourceBuffer }) {
3161
3533
  return new Task(async (taskSignal) => {
3162
- const ctx = getCtx();
3534
+ const ctx = getContext();
3163
3535
  if (taskSignal.aborted) return ctx;
3164
3536
  await flushBuffer(sourceBuffer, message.start, message.end);
3165
3537
  const bufferedRanges = snapshotBuffered(sourceBuffer.buffered);
@@ -3172,7 +3544,7 @@ function removeTask(message, { signal, getCtx, sourceBuffer }) {
3172
3544
  segments: filtered,
3173
3545
  bufferedRanges
3174
3546
  };
3175
- }, { signal });
3547
+ });
3176
3548
  }
3177
3549
  const messageTaskFactories = {
3178
3550
  "append-init": appendInitTask,
@@ -3184,83 +3556,54 @@ function messageToTask(message, options) {
3184
3556
  return factory(message, options);
3185
3557
  }
3186
3558
  function createSourceBufferActor(sourceBuffer, initialContext) {
3187
- const snapshotSignal = signal({
3188
- status: "idle",
3559
+ const handleError = (e) => {
3560
+ if (!(e instanceof Error && e.name === "AbortError")) console.error("SourceBuffer operation failed:", e);
3561
+ };
3562
+ const onMessage = (msg, { transition, setContext, getContext, runner }) => {
3563
+ transition("updating");
3564
+ const task = messageToTask(msg, {
3565
+ getContext,
3566
+ sourceBuffer,
3567
+ setContext
3568
+ });
3569
+ runner.schedule(task).then(setContext, handleError);
3570
+ };
3571
+ return createMachineActor({
3572
+ runner: () => new SerialRunner(),
3573
+ initial: "idle",
3189
3574
  context: {
3190
3575
  segments: [],
3191
3576
  bufferedRanges: [],
3192
3577
  initTrackId: void 0,
3193
3578
  ...initialContext
3194
- }
3195
- });
3196
- const runner = new SerialRunner();
3197
- function applyResult(newContext) {
3198
- const status = snapshotSignal.get().status === "destroyed" ? "destroyed" : "idle";
3199
- snapshotSignal.set({
3200
- status,
3201
- context: newContext
3202
- });
3203
- }
3204
- function handleError(e) {
3205
- update(snapshotSignal, { status: snapshotSignal.get().status === "destroyed" ? "destroyed" : "idle" });
3206
- throw e;
3207
- }
3208
- return {
3209
- get snapshot() {
3210
- return snapshotSignal;
3211
- },
3212
- send(message, signal) {
3213
- if (snapshotSignal.get().status !== "idle") return Promise.reject(new SourceBufferActorError(`send() called while actor is ${snapshotSignal.get().status}`));
3214
- update(snapshotSignal, { status: "updating" });
3215
- const onPartialContext = (ctx) => {
3216
- snapshotSignal.set({
3217
- status: "updating",
3218
- context: ctx
3219
- });
3220
- };
3221
- const task = messageToTask(message, {
3222
- signal,
3223
- getCtx: () => snapshotSignal.get().context,
3224
- sourceBuffer,
3225
- onPartialContext
3226
- });
3227
- return runner.schedule(task).then(applyResult).catch(handleError);
3228
3579
  },
3229
- batch(messages, signal) {
3230
- if (snapshotSignal.get().status !== "idle") return Promise.reject(new SourceBufferActorError(`batch() called while actor is ${snapshotSignal.get().status}`));
3231
- if (messages.length === 0) return Promise.resolve();
3232
- update(snapshotSignal, { status: "updating" });
3233
- let workingCtx = snapshotSignal.get().context;
3234
- const onPartialContext = (ctx) => {
3235
- snapshotSignal.set({
3236
- status: "updating",
3237
- context: ctx
3238
- });
3239
- };
3240
- for (const message of messages.slice(0, -1)) {
3241
- const task = messageToTask(message, {
3242
- signal,
3243
- getCtx: () => workingCtx,
3244
- sourceBuffer,
3245
- onPartialContext
3246
- });
3247
- runner.schedule(task).then((newCtx) => {
3248
- workingCtx = newCtx;
3249
- });
3580
+ states: {
3581
+ idle: { on: {
3582
+ "append-init": onMessage,
3583
+ "append-segment": onMessage,
3584
+ remove: onMessage,
3585
+ batch: (msg, { transition, setContext, getContext, runner }) => {
3586
+ const { messages } = msg;
3587
+ if (messages.length === 0) return;
3588
+ transition("updating");
3589
+ messages.forEach((msg) => {
3590
+ const task = messageToTask(msg, {
3591
+ getContext,
3592
+ sourceBuffer,
3593
+ setContext
3594
+ });
3595
+ runner.schedule(task).then(setContext, handleError);
3596
+ });
3597
+ }
3598
+ } },
3599
+ updating: {
3600
+ onSettled: "idle",
3601
+ on: { cancel: (_, { runner }) => {
3602
+ runner.abortAll();
3603
+ } }
3250
3604
  }
3251
- const lastTask = messageToTask(messages[messages.length - 1], {
3252
- signal,
3253
- getCtx: () => workingCtx,
3254
- sourceBuffer,
3255
- onPartialContext
3256
- });
3257
- return runner.schedule(lastTask).then(applyResult).catch(handleError);
3258
- },
3259
- destroy() {
3260
- update(snapshotSignal, { status: "destroyed" });
3261
- runner.destroy();
3262
3605
  }
3263
- };
3606
+ });
3264
3607
  }
3265
3608
 
3266
3609
  //#endregion
@@ -3334,143 +3677,92 @@ function setupSourceBuffers({ state, owners }) {
3334
3677
  }
3335
3678
 
3336
3679
  //#endregion
3337
- //#region ../spf/dist/dev/dom/features/setup-text-tracks.js
3338
- /**
3339
- * Create a track element for a text track.
3340
- *
3341
- * Note: We use DOM <track> elements instead of the TextTrack JS API
3342
- * because there's no way to remove TextTracks added via addTextTrack().
3343
- */
3680
+ //#region ../spf/dist/dev/dom/features/sync-text-tracks.js
3344
3681
  function createTrackElement(track) {
3345
- const trackElement = document.createElement("track");
3346
- trackElement.id = track.id;
3347
- trackElement.kind = track.kind;
3348
- trackElement.label = track.label;
3349
- if (track.language) trackElement.srclang = track.language;
3350
- if (track.default) trackElement.default = true;
3351
- trackElement.src = track.url;
3352
- return trackElement;
3682
+ const el = document.createElement("track");
3683
+ el.id = track.id;
3684
+ el.kind = track.kind;
3685
+ el.label = track.label;
3686
+ el.toggleAttribute("data-src-track", true);
3687
+ if (track.language) el.srclang = track.language;
3688
+ if (track.default) el.default = true;
3689
+ return el;
3690
+ }
3691
+ function getModelTextTracks(presentation) {
3692
+ return presentation?.selectionSets?.find((s) => s.type === "text")?.switchingSets[0]?.tracks;
3693
+ }
3694
+ function syncModes(textTracks, selectedId) {
3695
+ for (let i = 0; i < textTracks.length; i++) {
3696
+ const track = textTracks[i];
3697
+ if (track.kind !== "subtitles" && track.kind !== "captions") continue;
3698
+ track.mode = track.id === selectedId ? "showing" : "disabled";
3699
+ }
3353
3700
  }
3354
3701
  /**
3355
- * Setup text tracks orchestration.
3702
+ * Text track sync orchestration.
3356
3703
  *
3357
- * Triggers when:
3358
- * - mediaElement exists
3359
- * - presentation is resolved (has text tracks)
3704
+ * A single `always` monitor keeps the reactor in sync with preconditions.
3705
+ * `'set-up'` owns the full lifecycle of `<track>` elements:
3360
3706
  *
3361
- * Creates <track> elements for all text tracks and adds them as children
3362
- * to the media element. This allows the browser's native text track rendering.
3363
- *
3364
- * Note: Uses DOM track elements instead of TextTrack API because tracks
3365
- * added via addTextTrack() cannot be removed.
3707
+ * - **Effect 1** creates `<track>` elements on entry; exit cleanup removes
3708
+ * them and clears `selectedTextTrackId` on any outbound transition.
3709
+ * - **Effect 2** — owns mode sync, the Chromium settling-window guard, and
3710
+ * the `'change'` listener that bridges DOM state back to
3711
+ * `selectedTextTrackId`.
3366
3712
  *
3367
3713
  * @example
3368
- * const cleanup = setupTextTracks({ state, owners });
3714
+ * const reactor = syncTextTracks({ state, owners });
3715
+ * // later:
3716
+ * reactor.destroy();
3369
3717
  */
3370
- function setupTextTracks({ state, owners }) {
3371
- const modelTextTracksSignal = computed(() => state.get().presentation?.selectionSets?.find((selectionSet) => selectionSet.type === "text")?.switchingSets[0]?.tracks, { equals(prevTextTracks, nextTextTracks) {
3718
+ function syncTextTracks({ state, owners }) {
3719
+ const mediaElementSignal = computed(() => owners.get().mediaElement);
3720
+ const modelTextTracksSignal = computed(() => getModelTextTracks(state.get().presentation), { equals(prevTextTracks, nextTextTracks) {
3372
3721
  if (prevTextTracks === nextTextTracks) return true;
3373
3722
  if (typeof prevTextTracks !== typeof nextTextTracks) return false;
3374
3723
  if (prevTextTracks?.length !== nextTextTracks?.length) return false;
3375
3724
  return !!nextTextTracks && nextTextTracks.every((nextTextTrack) => prevTextTracks?.some((prevTextTrack) => prevTextTrack.id === nextTextTrack.id));
3376
3725
  } });
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();
3385
- const trackMap = /* @__PURE__ */ new Map();
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 });
3392
- });
3393
- return () => {
3394
- owners.get().textTracks?.forEach((trackElement) => trackElement.remove());
3395
- cleanupEffect();
3396
- };
3397
- }
3398
-
3399
- //#endregion
3400
- //#region ../spf/dist/dev/dom/features/sync-selected-text-track-from-dom.js
3401
- /**
3402
- * Sync selectedTextTrackId from DOM text track mode changes.
3403
- *
3404
- * Listens to the `change` event on `media.textTracks` and updates
3405
- * `selectedTextTrackId` when external code (e.g. the captions button via
3406
- * `toggleSubtitles()`) changes a subtitle/caption track mode to 'showing'.
3407
- *
3408
- * This bridges the core store's `toggleSubtitles()` with SPF's reactive text
3409
- * track pipeline (`syncTextTrackModes`, `loadTextTrackCues`). Without this
3410
- * bridge, direct DOM mode changes would be immediately overridden by
3411
- * `syncTextTrackModes` on the next SPF state update.
3412
- *
3413
- * When a subtitle/caption track's mode is 'showing', its DOM `id` — which
3414
- * matches the SPF track ID set by `setupTextTracks` — is written to
3415
- * `selectedTextTrackId`. When no subtitle/caption track is 'showing',
3416
- * `selectedTextTrackId` is cleared along with the deselected track's
3417
- * `textBufferState` entry — setting mode to 'disabled' clears native cues from
3418
- * the track element, so the buffer must be reset to re-fetch cues on re-enable.
3419
- *
3420
- * @example
3421
- * const cleanup = syncSelectedTextTrackFromDom({ state, owners });
3422
- */
3423
- function syncSelectedTextTrackFromDom({ state, owners }) {
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();
3431
- if (current.selectedTextTrackId === newId) return;
3432
- if (newId) update(state, { selectedTextTrackId: newId });
3433
- else {
3434
- const prevId = current.selectedTextTrackId;
3435
- if (prevId && current.textBufferState?.[prevId]) {
3436
- const next = { ...current.textBufferState };
3437
- delete next[prevId];
3438
- update(state, {
3439
- selectedTextTrackId: void 0,
3440
- textBufferState: next
3441
- });
3442
- } else update(state, { selectedTextTrackId: void 0 });
3443
- }
3444
- });
3445
- });
3446
- }
3447
-
3448
- //#endregion
3449
- //#region ../spf/dist/dev/dom/features/sync-text-track-modes.js
3450
- /**
3451
- * Sync text track modes orchestration.
3452
- *
3453
- * Manages track element modes based on selectedTextTrackId:
3454
- * - Selected track: mode = "showing"
3455
- * - Other tracks: mode = "hidden"
3456
- * - No selection: all tracks mode = "hidden"
3457
- *
3458
- * Note: Uses "hidden" instead of "disabled" for non-selected tracks
3459
- * so they remain available in the browser's track menu.
3460
- *
3461
- * @example
3462
- * const cleanup = syncTextTrackModes({ state, owners });
3463
- */
3464
- function syncTextTrackModes({ state, owners }) {
3465
- const textTracksSignal = computed(() => owners.get().textTracks);
3466
3726
  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";
3727
+ const preconditionsMetSignal = computed(() => !!mediaElementSignal.get() && !!modelTextTracksSignal.get()?.length);
3728
+ return createMachineReactor({
3729
+ initial: "preconditions-unmet",
3730
+ monitor: () => preconditionsMetSignal.get() ? "set-up" : "preconditions-unmet",
3731
+ states: {
3732
+ "preconditions-unmet": {},
3733
+ "set-up": {
3734
+ entry: () => {
3735
+ const mediaElement = mediaElementSignal.get();
3736
+ modelTextTracksSignal.get().forEach((track) => mediaElement.appendChild(createTrackElement(track)));
3737
+ return () => {
3738
+ mediaElement.querySelectorAll("track[data-src-track]:is([kind=\"subtitles\"],[kind=\"captions\"]").forEach((trackEl) => trackEl.remove());
3739
+ update(state, { selectedTextTrackId: void 0 });
3740
+ };
3741
+ },
3742
+ effects: () => {
3743
+ const mediaElement = untrack(() => mediaElementSignal.get());
3744
+ const selectedId = selectedTextTrackIdSignal.get();
3745
+ syncModes(mediaElement.textTracks, selectedId);
3746
+ let syncTimeout = setTimeout(() => {
3747
+ syncTimeout = void 0;
3748
+ }, 0);
3749
+ const onChange = () => {
3750
+ if (syncTimeout) {
3751
+ syncModes(mediaElement.textTracks, untrack(() => selectedTextTrackIdSignal.get()));
3752
+ return;
3753
+ }
3754
+ const newId = Array.from(mediaElement.textTracks).find((t) => t.mode === "showing" && (t.kind === "subtitles" || t.kind === "captions"))?.id;
3755
+ if (newId === untrack(() => selectedTextTrackIdSignal.get())) return;
3756
+ update(state, { selectedTextTrackId: newId });
3757
+ };
3758
+ const unlisten = listen(mediaElement.textTracks, "change", onChange);
3759
+ return () => {
3760
+ clearTimeout(syncTimeout ?? void 0);
3761
+ unlisten();
3762
+ };
3763
+ }
3764
+ }
3765
+ }
3474
3766
  });
3475
3767
  }
3476
3768
 
@@ -3651,15 +3943,7 @@ function createPlaybackEngine(config = {}) {
3651
3943
  state,
3652
3944
  owners
3653
3945
  }),
3654
- setupTextTracks({
3655
- state,
3656
- owners
3657
- }),
3658
- syncTextTrackModes({
3659
- state,
3660
- owners
3661
- }),
3662
- syncSelectedTextTrackFromDom({
3946
+ syncTextTracks({
3663
3947
  state,
3664
3948
  owners
3665
3949
  }),
@@ -3672,7 +3956,8 @@ function createPlaybackEngine(config = {}) {
3672
3956
  state,
3673
3957
  owners,
3674
3958
  destroy: () => {
3675
- cleanups.forEach((cleanup) => cleanup());
3959
+ cleanups.forEach((cleanup) => typeof cleanup === "function" ? cleanup() : cleanup.destroy());
3960
+ for (const value of Object.values(owners.get())) if (value !== null && typeof value === "object" && typeof value.destroy === "function") value.destroy();
3676
3961
  destroyVttParser();
3677
3962
  }
3678
3963
  };