@videojs/spf 10.0.0-beta.25 → 10.0.0-beta.27

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 (489) hide show
  1. package/README.md +5 -0
  2. package/dist/default/core/actors/create-machine-actor.js.map +1 -1
  3. package/dist/default/core/actors/create-transition-actor.js.map +1 -1
  4. package/dist/default/core/composition/create-composition.js +1 -1
  5. package/dist/default/core/composition/create-composition.js.map +1 -1
  6. package/dist/default/core/composition/share-signals.js.map +1 -1
  7. package/dist/default/core/machine.js.map +1 -1
  8. package/dist/default/core/reactors/create-machine-reactor.js.map +1 -1
  9. package/dist/default/core/signals/effect.js.map +1 -1
  10. package/dist/default/core/signals/primitives.js.map +1 -1
  11. package/dist/default/core/signals/when.js +38 -0
  12. package/dist/default/core/signals/when.js.map +1 -0
  13. package/dist/default/core/tasks/delayed-reschedule.js +31 -0
  14. package/dist/default/core/tasks/delayed-reschedule.js.map +1 -0
  15. package/dist/default/core/tasks/task.js +133 -3
  16. package/dist/default/core/tasks/task.js.map +1 -1
  17. package/dist/default/hls-audio.js +3 -0
  18. package/dist/default/hls-background-video.js +3 -0
  19. package/dist/default/hls-video.js +4 -0
  20. package/dist/default/hls.js +7 -5
  21. package/dist/default/media/abr/quality-selection.js.map +1 -1
  22. package/dist/default/media/buffer/back-buffer.js.map +1 -1
  23. package/dist/default/media/buffer/forward-buffer.js +37 -3
  24. package/dist/default/media/buffer/forward-buffer.js.map +1 -1
  25. package/dist/default/media/dom/capabilities.js +2 -0
  26. package/dist/default/media/dom/capabilities.js.map +1 -1
  27. package/dist/default/media/dom/mse/append-segment.js.map +1 -1
  28. package/dist/default/media/dom/mse/buffer-flusher.js.map +1 -1
  29. package/dist/default/media/dom/mse/duration.js +27 -10
  30. package/dist/default/media/dom/mse/duration.js.map +1 -1
  31. package/dist/default/media/dom/mse/end-of-stream.js.map +1 -1
  32. package/dist/default/media/dom/mse/mediasource-setup.js +68 -17
  33. package/dist/default/media/dom/mse/mediasource-setup.js.map +1 -1
  34. package/dist/default/media/dom/text/resolve-vtt-segment.js +1 -7
  35. package/dist/default/media/dom/text/resolve-vtt-segment.js.map +1 -1
  36. package/dist/default/media/dom/text/text-track-slots.js.map +1 -1
  37. package/dist/default/media/errors.js +62 -0
  38. package/dist/default/media/errors.js.map +1 -0
  39. package/dist/default/media/hls/parse-attributes.js +1 -1
  40. package/dist/default/media/hls/parse-attributes.js.map +1 -1
  41. package/dist/default/media/hls/parse-media-playlist.js +175 -19
  42. package/dist/default/media/hls/parse-media-playlist.js.map +1 -1
  43. package/dist/default/media/hls/parse-multivariant.js +14 -3
  44. package/dist/default/media/hls/parse-multivariant.js.map +1 -1
  45. package/dist/default/media/hls/reload-policy.js +71 -0
  46. package/dist/default/media/hls/reload-policy.js.map +1 -0
  47. package/dist/default/media/hls/resolve-url.js.map +1 -1
  48. package/dist/default/media/live-window.js +36 -0
  49. package/dist/default/media/live-window.js.map +1 -0
  50. package/dist/default/media/media-tracks/media-tracks.js +101 -0
  51. package/dist/default/media/media-tracks/media-tracks.js.map +1 -0
  52. package/dist/default/media/mp4/box.js +58 -0
  53. package/dist/default/media/mp4/box.js.map +1 -0
  54. package/dist/default/media/mp4/timestamp-origin.js +107 -0
  55. package/dist/default/media/mp4/timestamp-origin.js.map +1 -0
  56. package/dist/default/media/primitives/select-tracks.js +1 -24
  57. package/dist/default/media/primitives/select-tracks.js.map +1 -1
  58. package/dist/default/media/text/parse-vtt-timestamp-map.js +42 -0
  59. package/dist/default/media/text/parse-vtt-timestamp-map.js.map +1 -0
  60. package/dist/default/media/text/resolve-vtt-metadata.js +23 -0
  61. package/dist/default/media/text/resolve-vtt-metadata.js.map +1 -0
  62. package/dist/default/media/types/index.js +15 -1
  63. package/dist/default/media/types/index.js.map +1 -1
  64. package/dist/default/media/utils/cdn.js.map +1 -1
  65. package/dist/default/media/utils/preload.js.map +1 -1
  66. package/dist/default/media/utils/track-selection.js +1 -1
  67. package/dist/default/media/utils/track-selection.js.map +1 -1
  68. package/dist/default/media/utils/tracks.js +17 -3
  69. package/dist/default/media/utils/tracks.js.map +1 -1
  70. package/dist/default/media-tracks.js +2 -0
  71. package/dist/default/mux-audio.js +3 -0
  72. package/dist/default/mux-background-video.js +3 -0
  73. package/dist/default/mux-video.js +3 -0
  74. package/dist/default/network/bandwidth-estimator.js.map +1 -1
  75. package/dist/default/network/chunked-stream-iterable.js.map +1 -1
  76. package/dist/default/network/ewma.js +1 -1
  77. package/dist/default/network/ewma.js.map +1 -1
  78. package/dist/default/network/fetch.js.map +1 -1
  79. package/dist/default/playback/actors/dom/segment-loader.js +61 -95
  80. package/dist/default/playback/actors/dom/segment-loader.js.map +1 -1
  81. package/dist/default/playback/actors/dom/source-buffer.js +1 -0
  82. package/dist/default/playback/actors/dom/source-buffer.js.map +1 -1
  83. package/dist/default/playback/actors/dom/text-tracks.js.map +1 -1
  84. package/dist/default/playback/actors/text-track-segment-loader.js +30 -16
  85. package/dist/default/playback/actors/text-track-segment-loader.js.map +1 -1
  86. package/dist/default/playback/adapters/hls-audio/adapter.js +186 -0
  87. package/dist/default/playback/adapters/hls-audio/adapter.js.map +1 -0
  88. package/dist/default/playback/adapters/hls-audio/media.js +9 -0
  89. package/dist/default/playback/adapters/hls-audio/media.js.map +1 -0
  90. package/dist/default/playback/adapters/hls-background-video/adapter.js +123 -0
  91. package/dist/default/playback/adapters/hls-background-video/adapter.js.map +1 -0
  92. package/dist/default/playback/adapters/hls-background-video/host.js +60 -0
  93. package/dist/default/playback/adapters/hls-background-video/host.js.map +1 -0
  94. package/dist/default/playback/adapters/hls-background-video/media.js +21 -0
  95. package/dist/default/playback/adapters/hls-background-video/media.js.map +1 -0
  96. package/dist/default/playback/adapters/hls-video/adapter.js +316 -0
  97. package/dist/default/playback/adapters/hls-video/adapter.js.map +1 -0
  98. package/dist/default/playback/adapters/hls-video/error-surface.js +51 -0
  99. package/dist/default/playback/adapters/hls-video/error-surface.js.map +1 -0
  100. package/dist/default/playback/adapters/hls-video/media-tracks.js +126 -0
  101. package/dist/default/playback/adapters/hls-video/media-tracks.js.map +1 -0
  102. package/dist/default/playback/adapters/hls-video/media.js +11 -0
  103. package/dist/default/playback/adapters/hls-video/media.js.map +1 -0
  104. package/dist/default/playback/adapters/mux-audio/media.js +26 -0
  105. package/dist/default/playback/adapters/mux-audio/media.js.map +1 -0
  106. package/dist/default/playback/adapters/mux-video/adapter.js +92 -0
  107. package/dist/default/playback/adapters/mux-video/adapter.js.map +1 -0
  108. package/dist/default/playback/adapters/mux-video/media.js +23 -0
  109. package/dist/default/playback/adapters/mux-video/media.js.map +1 -0
  110. package/dist/default/playback/behaviors/calculate-presentation-duration.js.map +1 -1
  111. package/dist/default/playback/behaviors/collect-errors.js +77 -0
  112. package/dist/default/playback/behaviors/collect-errors.js.map +1 -0
  113. package/dist/default/playback/behaviors/derive-cdn-priority.js.map +1 -1
  114. package/dist/default/playback/behaviors/dom/airplay.js +211 -0
  115. package/dist/default/playback/behaviors/dom/airplay.js.map +1 -0
  116. package/dist/default/playback/behaviors/dom/apply-start-position.js +93 -0
  117. package/dist/default/playback/behaviors/dom/apply-start-position.js.map +1 -0
  118. package/dist/default/playback/behaviors/dom/end-of-stream.js +13 -1
  119. package/dist/default/playback/behaviors/dom/end-of-stream.js.map +1 -1
  120. package/dist/default/playback/behaviors/dom/load-segments.js +5 -1
  121. package/dist/default/playback/behaviors/dom/load-segments.js.map +1 -1
  122. package/dist/default/playback/behaviors/dom/recover-end-stall.js +77 -0
  123. package/dist/default/playback/behaviors/dom/recover-end-stall.js.map +1 -0
  124. package/dist/default/playback/behaviors/dom/seek-to-live-edge.js +132 -0
  125. package/dist/default/playback/behaviors/dom/seek-to-live-edge.js.map +1 -0
  126. package/dist/default/playback/behaviors/dom/setup-buffer-actors.js +17 -4
  127. package/dist/default/playback/behaviors/dom/setup-buffer-actors.js.map +1 -1
  128. package/dist/default/playback/behaviors/dom/setup-mediasource.js +36 -11
  129. package/dist/default/playback/behaviors/dom/setup-mediasource.js.map +1 -1
  130. package/dist/default/playback/behaviors/dom/setup-text-track-actors.js +11 -25
  131. package/dist/default/playback/behaviors/dom/setup-text-track-actors.js.map +1 -1
  132. package/dist/default/playback/behaviors/dom/sync-live-seekable-range.js +26 -0
  133. package/dist/default/playback/behaviors/dom/sync-live-seekable-range.js.map +1 -0
  134. package/dist/default/playback/behaviors/dom/sync-text-tracks.js.map +1 -1
  135. package/dist/default/playback/behaviors/dom/track-current-time.js.map +1 -1
  136. package/dist/default/playback/behaviors/dom/track-load-triggers.js +4 -4
  137. package/dist/default/playback/behaviors/dom/track-load-triggers.js.map +1 -1
  138. package/dist/default/playback/behaviors/dom/track-playback-rate.js.map +1 -1
  139. package/dist/default/playback/behaviors/dom/update-mediasource-duration.js +31 -8
  140. package/dist/default/playback/behaviors/dom/update-mediasource-duration.js.map +1 -1
  141. package/dist/default/playback/behaviors/establish-start-media-time.js +197 -0
  142. package/dist/default/playback/behaviors/establish-start-media-time.js.map +1 -0
  143. package/dist/default/playback/behaviors/resolve-presentation.js.map +1 -1
  144. package/dist/default/playback/behaviors/resolve-track.js +29 -8
  145. package/dist/default/playback/behaviors/resolve-track.js.map +1 -1
  146. package/dist/default/playback/behaviors/select-tracks.js.map +1 -1
  147. package/dist/default/playback/behaviors/setup-failover-monitor.js +1 -1
  148. package/dist/default/playback/behaviors/setup-failover-monitor.js.map +1 -1
  149. package/dist/default/playback/behaviors/sync-preload.js.map +1 -1
  150. package/dist/default/playback/behaviors/track-switching.js +76 -5
  151. package/dist/default/playback/behaviors/track-switching.js.map +1 -1
  152. package/dist/default/playback/engines/hls/engine-audio-only.js +25 -7
  153. package/dist/default/playback/engines/hls/engine-audio-only.js.map +1 -1
  154. package/dist/{dev/playback/engines/background-video/engine.js → default/playback/engines/hls/engine-background-video.js} +3 -3
  155. package/dist/default/playback/engines/hls/engine-background-video.js.map +1 -0
  156. package/dist/default/playback/engines/hls/engine.js +36 -6
  157. package/dist/default/playback/engines/hls/engine.js.map +1 -1
  158. package/dist/default/playback/primitives/error-messages.js +39 -0
  159. package/dist/default/playback/primitives/error-messages.js.map +1 -0
  160. package/dist/default/playback/primitives/failover-fetch.js.map +1 -1
  161. package/dist/default/playback/primitives/head-peek.js +44 -0
  162. package/dist/default/playback/primitives/head-peek.js.map +1 -0
  163. package/dist/default/playback/primitives/live-window.js +66 -0
  164. package/dist/default/playback/primitives/live-window.js.map +1 -0
  165. package/dist/default/playback/primitives/relocation-pipelines.js +203 -0
  166. package/dist/default/playback/primitives/relocation-pipelines.js.map +1 -0
  167. package/dist/default/playback/primitives/report-track-conditions.js +86 -0
  168. package/dist/default/playback/primitives/report-track-conditions.js.map +1 -0
  169. package/dist/default/playback/primitives/segment-load-pipeline.js +96 -0
  170. package/dist/default/playback/primitives/segment-load-pipeline.js.map +1 -0
  171. package/dist/default/playback/primitives/text-segment-load-pipeline.js +37 -0
  172. package/dist/default/playback/primitives/text-segment-load-pipeline.js.map +1 -0
  173. package/dist/default/playback/primitives/track-types.js.map +1 -1
  174. package/dist/dev/core/actors/actor.d.ts +0 -1
  175. package/dist/dev/core/actors/actor.d.ts.map +1 -1
  176. package/dist/dev/core/actors/create-machine-actor.d.ts +9 -6
  177. package/dist/dev/core/actors/create-machine-actor.d.ts.map +1 -1
  178. package/dist/dev/core/actors/create-machine-actor.js.map +1 -1
  179. package/dist/dev/core/actors/create-transition-actor.d.ts +0 -1
  180. package/dist/dev/core/actors/create-transition-actor.d.ts.map +1 -1
  181. package/dist/dev/core/actors/create-transition-actor.js.map +1 -1
  182. package/dist/dev/core/composition/create-composition.d.ts +10 -11
  183. package/dist/dev/core/composition/create-composition.d.ts.map +1 -1
  184. package/dist/dev/core/composition/create-composition.js +1 -1
  185. package/dist/dev/core/composition/create-composition.js.map +1 -1
  186. package/dist/dev/core/composition/share-signals.d.ts +0 -1
  187. package/dist/dev/core/composition/share-signals.d.ts.map +1 -1
  188. package/dist/dev/core/composition/share-signals.js.map +1 -1
  189. package/dist/dev/core/machine.d.ts +1 -0
  190. package/dist/dev/core/machine.d.ts.map +1 -1
  191. package/dist/dev/core/machine.js.map +1 -1
  192. package/dist/dev/core/reactors/create-machine-reactor.d.ts +2 -2
  193. package/dist/dev/core/reactors/create-machine-reactor.d.ts.map +1 -1
  194. package/dist/dev/core/reactors/create-machine-reactor.js.map +1 -1
  195. package/dist/dev/core/signals/effect.d.ts.map +1 -1
  196. package/dist/dev/core/signals/effect.js.map +1 -1
  197. package/dist/dev/core/signals/primitives.d.ts +1 -2
  198. package/dist/dev/core/signals/primitives.d.ts.map +1 -1
  199. package/dist/dev/core/signals/primitives.js.map +1 -1
  200. package/dist/dev/core/signals/when.js +38 -0
  201. package/dist/dev/core/signals/when.js.map +1 -0
  202. package/dist/dev/core/tasks/delayed-reschedule.js +31 -0
  203. package/dist/dev/core/tasks/delayed-reschedule.js.map +1 -0
  204. package/dist/dev/core/tasks/task.d.ts +53 -2
  205. package/dist/dev/core/tasks/task.d.ts.map +1 -1
  206. package/dist/dev/core/tasks/task.js +133 -3
  207. package/dist/dev/core/tasks/task.js.map +1 -1
  208. package/dist/dev/hls-audio.d.ts +3 -0
  209. package/dist/dev/hls-audio.js +3 -0
  210. package/dist/dev/hls-background-video.d.ts +3 -0
  211. package/dist/dev/hls-background-video.js +3 -0
  212. package/dist/dev/hls-video.d.ts +5 -0
  213. package/dist/dev/hls-video.js +4 -0
  214. package/dist/dev/hls.d.ts +8 -5
  215. package/dist/dev/hls.js +7 -5
  216. package/dist/dev/index.d.ts +0 -1
  217. package/dist/dev/index.d.ts.map +1 -1
  218. package/dist/dev/media/abr/quality-selection.d.ts.map +1 -1
  219. package/dist/dev/media/abr/quality-selection.js.map +1 -1
  220. package/dist/dev/media/buffer/back-buffer.d.ts.map +1 -1
  221. package/dist/dev/media/buffer/back-buffer.js.map +1 -1
  222. package/dist/dev/media/buffer/forward-buffer.d.ts.map +1 -1
  223. package/dist/dev/media/buffer/forward-buffer.js +37 -3
  224. package/dist/dev/media/buffer/forward-buffer.js.map +1 -1
  225. package/dist/dev/media/dom/capabilities.js +2 -0
  226. package/dist/dev/media/dom/capabilities.js.map +1 -1
  227. package/dist/dev/media/dom/mse/append-segment.d.ts +3 -9
  228. package/dist/dev/media/dom/mse/append-segment.d.ts.map +1 -1
  229. package/dist/dev/media/dom/mse/append-segment.js.map +1 -1
  230. package/dist/dev/media/dom/mse/buffer-flusher.d.ts.map +1 -1
  231. package/dist/dev/media/dom/mse/buffer-flusher.js.map +1 -1
  232. package/dist/dev/media/dom/mse/duration.js +27 -10
  233. package/dist/dev/media/dom/mse/duration.js.map +1 -1
  234. package/dist/dev/media/dom/mse/end-of-stream.js.map +1 -1
  235. package/dist/dev/media/dom/mse/mediasource-setup.js +68 -17
  236. package/dist/dev/media/dom/mse/mediasource-setup.js.map +1 -1
  237. package/dist/dev/media/dom/text/resolve-vtt-segment.d.ts +0 -6
  238. package/dist/dev/media/dom/text/resolve-vtt-segment.d.ts.map +1 -1
  239. package/dist/dev/media/dom/text/resolve-vtt-segment.js +1 -7
  240. package/dist/dev/media/dom/text/resolve-vtt-segment.js.map +1 -1
  241. package/dist/dev/media/dom/text/text-track-slots.d.ts +0 -1
  242. package/dist/dev/media/dom/text/text-track-slots.d.ts.map +1 -1
  243. package/dist/dev/media/dom/text/text-track-slots.js.map +1 -1
  244. package/dist/dev/media/errors.d.ts +82 -0
  245. package/dist/dev/media/errors.d.ts.map +1 -0
  246. package/dist/dev/media/errors.js +62 -0
  247. package/dist/dev/media/errors.js.map +1 -0
  248. package/dist/dev/media/hls/parse-attributes.js +1 -1
  249. package/dist/dev/media/hls/parse-attributes.js.map +1 -1
  250. package/dist/dev/media/hls/parse-media-playlist.js +175 -19
  251. package/dist/dev/media/hls/parse-media-playlist.js.map +1 -1
  252. package/dist/dev/media/hls/parse-multivariant.js +14 -3
  253. package/dist/dev/media/hls/parse-multivariant.js.map +1 -1
  254. package/dist/dev/media/hls/reload-policy.js +71 -0
  255. package/dist/dev/media/hls/reload-policy.js.map +1 -0
  256. package/dist/dev/media/hls/resolve-url.js.map +1 -1
  257. package/dist/dev/media/live-window.js +36 -0
  258. package/dist/dev/media/live-window.js.map +1 -0
  259. package/dist/dev/media/media-tracks/media-tracks.d.ts +51 -0
  260. package/dist/dev/media/media-tracks/media-tracks.d.ts.map +1 -0
  261. package/dist/dev/media/media-tracks/media-tracks.js +101 -0
  262. package/dist/dev/media/media-tracks/media-tracks.js.map +1 -0
  263. package/dist/dev/media/mp4/box.js +58 -0
  264. package/dist/dev/media/mp4/box.js.map +1 -0
  265. package/dist/dev/media/mp4/timestamp-origin.js +107 -0
  266. package/dist/dev/media/mp4/timestamp-origin.js.map +1 -0
  267. package/dist/dev/media/primitives/select-tracks.d.ts +0 -1
  268. package/dist/dev/media/primitives/select-tracks.d.ts.map +1 -1
  269. package/dist/dev/media/primitives/select-tracks.js +1 -24
  270. package/dist/dev/media/primitives/select-tracks.js.map +1 -1
  271. package/dist/dev/media/text/parse-vtt-timestamp-map.js +42 -0
  272. package/dist/dev/media/text/parse-vtt-timestamp-map.js.map +1 -0
  273. package/dist/dev/media/text/resolve-vtt-metadata.js +23 -0
  274. package/dist/dev/media/text/resolve-vtt-metadata.js.map +1 -0
  275. package/dist/dev/media/types/index.d.ts +163 -4
  276. package/dist/dev/media/types/index.d.ts.map +1 -1
  277. package/dist/dev/media/types/index.js +15 -1
  278. package/dist/dev/media/types/index.js.map +1 -1
  279. package/dist/dev/media/utils/cdn.d.ts.map +1 -1
  280. package/dist/dev/media/utils/cdn.js.map +1 -1
  281. package/dist/dev/media/utils/preload.js.map +1 -1
  282. package/dist/dev/media/utils/track-selection.js +1 -1
  283. package/dist/dev/media/utils/track-selection.js.map +1 -1
  284. package/dist/dev/media/utils/tracks.js +17 -3
  285. package/dist/dev/media/utils/tracks.js.map +1 -1
  286. package/dist/dev/media-tracks.d.ts +3 -0
  287. package/dist/dev/media-tracks.js +2 -0
  288. package/dist/dev/mux-audio.d.ts +4 -0
  289. package/dist/dev/mux-audio.js +3 -0
  290. package/dist/dev/mux-background-video.d.ts +3 -0
  291. package/dist/dev/mux-background-video.js +3 -0
  292. package/dist/dev/mux-video.d.ts +4 -0
  293. package/dist/dev/mux-video.js +3 -0
  294. package/dist/dev/network/bandwidth-estimator.d.ts.map +1 -1
  295. package/dist/dev/network/bandwidth-estimator.js.map +1 -1
  296. package/dist/dev/network/chunked-stream-iterable.js.map +1 -1
  297. package/dist/dev/network/ewma.js +1 -1
  298. package/dist/dev/network/ewma.js.map +1 -1
  299. package/dist/dev/network/fetch.js.map +1 -1
  300. package/dist/dev/playback/actors/dom/segment-loader.d.ts +16 -4
  301. package/dist/dev/playback/actors/dom/segment-loader.d.ts.map +1 -1
  302. package/dist/dev/playback/actors/dom/segment-loader.js +61 -95
  303. package/dist/dev/playback/actors/dom/segment-loader.js.map +1 -1
  304. package/dist/dev/playback/actors/dom/source-buffer.d.ts +3 -33
  305. package/dist/dev/playback/actors/dom/source-buffer.d.ts.map +1 -1
  306. package/dist/dev/playback/actors/dom/source-buffer.js +1 -0
  307. package/dist/dev/playback/actors/dom/source-buffer.js.map +1 -1
  308. package/dist/dev/playback/actors/dom/text-tracks.d.ts +3 -1
  309. package/dist/dev/playback/actors/dom/text-tracks.js.map +1 -1
  310. package/dist/dev/playback/actors/text-track-segment-loader.d.ts +7 -12
  311. package/dist/dev/playback/actors/text-track-segment-loader.d.ts.map +1 -1
  312. package/dist/dev/playback/actors/text-track-segment-loader.js +30 -16
  313. package/dist/dev/playback/actors/text-track-segment-loader.js.map +1 -1
  314. package/dist/dev/playback/actors/text-tracks.d.ts +3 -12
  315. package/dist/dev/playback/actors/text-tracks.d.ts.map +1 -1
  316. package/dist/dev/playback/adapters/hls-audio/adapter.d.ts +54 -0
  317. package/dist/dev/playback/adapters/hls-audio/adapter.d.ts.map +1 -0
  318. package/dist/dev/playback/adapters/hls-audio/adapter.js +186 -0
  319. package/dist/dev/playback/adapters/hls-audio/adapter.js.map +1 -0
  320. package/dist/dev/playback/adapters/hls-audio/media.d.ts +10 -0
  321. package/dist/dev/playback/adapters/hls-audio/media.d.ts.map +1 -0
  322. package/dist/dev/playback/adapters/hls-audio/media.js +9 -0
  323. package/dist/dev/playback/adapters/hls-audio/media.js.map +1 -0
  324. package/dist/dev/playback/adapters/hls-background-video/adapter.d.ts +59 -0
  325. package/dist/dev/playback/adapters/hls-background-video/adapter.d.ts.map +1 -0
  326. package/dist/dev/playback/adapters/hls-background-video/adapter.js +123 -0
  327. package/dist/dev/playback/adapters/hls-background-video/adapter.js.map +1 -0
  328. package/dist/dev/playback/adapters/hls-background-video/host.d.ts +38 -0
  329. package/dist/dev/playback/adapters/hls-background-video/host.d.ts.map +1 -0
  330. package/dist/dev/playback/adapters/hls-background-video/host.js +60 -0
  331. package/dist/dev/playback/adapters/hls-background-video/host.js.map +1 -0
  332. package/dist/dev/playback/adapters/hls-background-video/media.d.ts +20 -0
  333. package/dist/dev/playback/adapters/hls-background-video/media.d.ts.map +1 -0
  334. package/dist/dev/playback/adapters/hls-background-video/media.js +21 -0
  335. package/dist/dev/playback/adapters/hls-background-video/media.js.map +1 -0
  336. package/dist/dev/playback/adapters/hls-video/adapter.d.ts +63 -0
  337. package/dist/dev/playback/adapters/hls-video/adapter.d.ts.map +1 -0
  338. package/dist/dev/playback/adapters/hls-video/adapter.js +316 -0
  339. package/dist/dev/playback/adapters/hls-video/adapter.js.map +1 -0
  340. package/dist/dev/playback/adapters/hls-video/error-surface.d.ts +18 -0
  341. package/dist/dev/playback/adapters/hls-video/error-surface.d.ts.map +1 -0
  342. package/dist/dev/playback/adapters/hls-video/error-surface.js +51 -0
  343. package/dist/dev/playback/adapters/hls-video/error-surface.js.map +1 -0
  344. package/dist/dev/playback/adapters/hls-video/media-tracks.d.ts +22 -0
  345. package/dist/dev/playback/adapters/hls-video/media-tracks.d.ts.map +1 -0
  346. package/dist/dev/playback/adapters/hls-video/media-tracks.js +126 -0
  347. package/dist/dev/playback/adapters/hls-video/media-tracks.js.map +1 -0
  348. package/dist/dev/playback/adapters/hls-video/media.d.ts +10 -0
  349. package/dist/dev/playback/adapters/hls-video/media.d.ts.map +1 -0
  350. package/dist/dev/playback/adapters/hls-video/media.js +11 -0
  351. package/dist/dev/playback/adapters/hls-video/media.js.map +1 -0
  352. package/dist/dev/playback/adapters/mux-audio/media.d.ts +28 -0
  353. package/dist/dev/playback/adapters/mux-audio/media.d.ts.map +1 -0
  354. package/dist/dev/playback/adapters/mux-audio/media.js +26 -0
  355. package/dist/dev/playback/adapters/mux-audio/media.js.map +1 -0
  356. package/dist/dev/playback/adapters/mux-video/adapter.d.ts +33 -0
  357. package/dist/dev/playback/adapters/mux-video/adapter.d.ts.map +1 -0
  358. package/dist/dev/playback/adapters/mux-video/adapter.js +92 -0
  359. package/dist/dev/playback/adapters/mux-video/adapter.js.map +1 -0
  360. package/dist/dev/playback/adapters/mux-video/media.d.ts +24 -0
  361. package/dist/dev/playback/adapters/mux-video/media.d.ts.map +1 -0
  362. package/dist/dev/playback/adapters/mux-video/media.js +23 -0
  363. package/dist/dev/playback/adapters/mux-video/media.js.map +1 -0
  364. package/dist/dev/playback/behaviors/calculate-presentation-duration.d.ts +3 -2
  365. package/dist/dev/playback/behaviors/calculate-presentation-duration.d.ts.map +1 -1
  366. package/dist/dev/playback/behaviors/calculate-presentation-duration.js.map +1 -1
  367. package/dist/dev/playback/behaviors/collect-errors.js +77 -0
  368. package/dist/dev/playback/behaviors/collect-errors.js.map +1 -0
  369. package/dist/dev/playback/behaviors/derive-cdn-priority.js.map +1 -1
  370. package/dist/dev/playback/behaviors/dom/airplay.js +211 -0
  371. package/dist/dev/playback/behaviors/dom/airplay.js.map +1 -0
  372. package/dist/dev/playback/behaviors/dom/apply-start-position.js +93 -0
  373. package/dist/dev/playback/behaviors/dom/apply-start-position.js.map +1 -0
  374. package/dist/dev/playback/behaviors/dom/end-of-stream.js +13 -1
  375. package/dist/dev/playback/behaviors/dom/end-of-stream.js.map +1 -1
  376. package/dist/dev/playback/behaviors/dom/load-segments.d.ts +14 -4
  377. package/dist/dev/playback/behaviors/dom/load-segments.d.ts.map +1 -1
  378. package/dist/dev/playback/behaviors/dom/load-segments.js +5 -1
  379. package/dist/dev/playback/behaviors/dom/load-segments.js.map +1 -1
  380. package/dist/dev/playback/behaviors/dom/recover-end-stall.js +77 -0
  381. package/dist/dev/playback/behaviors/dom/recover-end-stall.js.map +1 -0
  382. package/dist/dev/playback/behaviors/dom/seek-to-live-edge.js +132 -0
  383. package/dist/dev/playback/behaviors/dom/seek-to-live-edge.js.map +1 -0
  384. package/dist/dev/playback/behaviors/dom/setup-buffer-actors.js +17 -4
  385. package/dist/dev/playback/behaviors/dom/setup-buffer-actors.js.map +1 -1
  386. package/dist/dev/playback/behaviors/dom/setup-mediasource.js +36 -11
  387. package/dist/dev/playback/behaviors/dom/setup-mediasource.js.map +1 -1
  388. package/dist/dev/playback/behaviors/dom/setup-text-track-actors.d.ts +16 -19
  389. package/dist/dev/playback/behaviors/dom/setup-text-track-actors.d.ts.map +1 -1
  390. package/dist/dev/playback/behaviors/dom/setup-text-track-actors.js +11 -25
  391. package/dist/dev/playback/behaviors/dom/setup-text-track-actors.js.map +1 -1
  392. package/dist/dev/playback/behaviors/dom/sync-live-seekable-range.js +26 -0
  393. package/dist/dev/playback/behaviors/dom/sync-live-seekable-range.js.map +1 -0
  394. package/dist/dev/playback/behaviors/dom/sync-text-tracks.js.map +1 -1
  395. package/dist/dev/playback/behaviors/dom/track-current-time.d.ts +1 -2
  396. package/dist/dev/playback/behaviors/dom/track-current-time.d.ts.map +1 -1
  397. package/dist/dev/playback/behaviors/dom/track-current-time.js.map +1 -1
  398. package/dist/dev/playback/behaviors/dom/track-load-triggers.d.ts +1 -2
  399. package/dist/dev/playback/behaviors/dom/track-load-triggers.d.ts.map +1 -1
  400. package/dist/dev/playback/behaviors/dom/track-load-triggers.js +4 -4
  401. package/dist/dev/playback/behaviors/dom/track-load-triggers.js.map +1 -1
  402. package/dist/dev/playback/behaviors/dom/track-playback-rate.d.ts +1 -2
  403. package/dist/dev/playback/behaviors/dom/track-playback-rate.d.ts.map +1 -1
  404. package/dist/dev/playback/behaviors/dom/track-playback-rate.js.map +1 -1
  405. package/dist/dev/playback/behaviors/dom/update-mediasource-duration.js +31 -8
  406. package/dist/dev/playback/behaviors/dom/update-mediasource-duration.js.map +1 -1
  407. package/dist/dev/playback/behaviors/establish-start-media-time.d.ts +36 -0
  408. package/dist/dev/playback/behaviors/establish-start-media-time.d.ts.map +1 -0
  409. package/dist/dev/playback/behaviors/establish-start-media-time.js +197 -0
  410. package/dist/dev/playback/behaviors/establish-start-media-time.js.map +1 -0
  411. package/dist/dev/playback/behaviors/resolve-presentation.d.ts +2 -1
  412. package/dist/dev/playback/behaviors/resolve-presentation.d.ts.map +1 -1
  413. package/dist/dev/playback/behaviors/resolve-presentation.js.map +1 -1
  414. package/dist/dev/playback/behaviors/resolve-track.js +29 -8
  415. package/dist/dev/playback/behaviors/resolve-track.js.map +1 -1
  416. package/dist/dev/playback/behaviors/select-tracks.d.ts +2 -1
  417. package/dist/dev/playback/behaviors/select-tracks.d.ts.map +1 -1
  418. package/dist/dev/playback/behaviors/select-tracks.js.map +1 -1
  419. package/dist/dev/playback/behaviors/setup-failover-monitor.d.ts +2 -0
  420. package/dist/dev/playback/behaviors/setup-failover-monitor.d.ts.map +1 -1
  421. package/dist/dev/playback/behaviors/setup-failover-monitor.js +1 -1
  422. package/dist/dev/playback/behaviors/setup-failover-monitor.js.map +1 -1
  423. package/dist/dev/playback/behaviors/sync-preload.js.map +1 -1
  424. package/dist/dev/playback/behaviors/track-switching.js +76 -5
  425. package/dist/dev/playback/behaviors/track-switching.js.map +1 -1
  426. package/dist/dev/playback/engines/hls/engine-audio-only.d.ts +63 -17
  427. package/dist/dev/playback/engines/hls/engine-audio-only.d.ts.map +1 -1
  428. package/dist/dev/playback/engines/hls/engine-audio-only.js +25 -7
  429. package/dist/dev/playback/engines/hls/engine-audio-only.js.map +1 -1
  430. package/dist/dev/playback/engines/{background-video/engine.d.ts → hls/engine-background-video.d.ts} +8 -9
  431. package/dist/dev/playback/engines/hls/engine-background-video.d.ts.map +1 -0
  432. package/dist/{default/playback/engines/background-video/engine.js → dev/playback/engines/hls/engine-background-video.js} +3 -3
  433. package/dist/dev/playback/engines/hls/engine-background-video.js.map +1 -0
  434. package/dist/dev/playback/engines/hls/engine.d.ts +90 -16
  435. package/dist/dev/playback/engines/hls/engine.d.ts.map +1 -1
  436. package/dist/dev/playback/engines/hls/engine.js +36 -6
  437. package/dist/dev/playback/engines/hls/engine.js.map +1 -1
  438. package/dist/dev/playback/primitives/derive-start-media-time.d.ts +16 -0
  439. package/dist/dev/playback/primitives/derive-start-media-time.d.ts.map +1 -0
  440. package/dist/dev/playback/primitives/error-messages.js +39 -0
  441. package/dist/dev/playback/primitives/error-messages.js.map +1 -0
  442. package/dist/dev/playback/primitives/failover-fetch.js.map +1 -1
  443. package/dist/dev/playback/primitives/head-peek.js +44 -0
  444. package/dist/dev/playback/primitives/head-peek.js.map +1 -0
  445. package/dist/dev/playback/primitives/live-window.js +66 -0
  446. package/dist/dev/playback/primitives/live-window.js.map +1 -0
  447. package/dist/dev/playback/primitives/relocation-pipelines.js +203 -0
  448. package/dist/dev/playback/primitives/relocation-pipelines.js.map +1 -0
  449. package/dist/dev/playback/primitives/report-track-conditions.d.ts +11 -0
  450. package/dist/dev/playback/primitives/report-track-conditions.d.ts.map +1 -0
  451. package/dist/dev/playback/primitives/report-track-conditions.js +86 -0
  452. package/dist/dev/playback/primitives/report-track-conditions.js.map +1 -0
  453. package/dist/dev/playback/primitives/segment-load-pipeline.d.ts +2 -0
  454. package/dist/dev/playback/primitives/segment-load-pipeline.js +96 -0
  455. package/dist/dev/playback/primitives/segment-load-pipeline.js.map +1 -0
  456. package/dist/dev/playback/primitives/source-buffer-messages.d.ts +43 -0
  457. package/dist/dev/playback/primitives/source-buffer-messages.d.ts.map +1 -0
  458. package/dist/dev/playback/primitives/text-segment-load-pipeline.d.ts +65 -0
  459. package/dist/dev/playback/primitives/text-segment-load-pipeline.d.ts.map +1 -0
  460. package/dist/dev/playback/primitives/text-segment-load-pipeline.js +37 -0
  461. package/dist/dev/playback/primitives/text-segment-load-pipeline.js.map +1 -0
  462. package/dist/dev/playback/primitives/text-track-messages.d.ts +14 -0
  463. package/dist/dev/playback/primitives/text-track-messages.d.ts.map +1 -0
  464. package/dist/dev/playback/primitives/track-types.js.map +1 -1
  465. package/package.json +42 -11
  466. package/dist/default/background-video.js +0 -3
  467. package/dist/default/playback/engines/background-video/adapter.js +0 -158
  468. package/dist/default/playback/engines/background-video/adapter.js.map +0 -1
  469. package/dist/default/playback/engines/background-video/engine.js.map +0 -1
  470. package/dist/default/playback/engines/hls/adapter-audio-only.js +0 -119
  471. package/dist/default/playback/engines/hls/adapter-audio-only.js.map +0 -1
  472. package/dist/default/playback/engines/hls/adapter.js +0 -118
  473. package/dist/default/playback/engines/hls/adapter.js.map +0 -1
  474. package/dist/dev/background-video.d.ts +0 -3
  475. package/dist/dev/background-video.js +0 -3
  476. package/dist/dev/playback/engines/background-video/adapter.d.ts +0 -60
  477. package/dist/dev/playback/engines/background-video/adapter.d.ts.map +0 -1
  478. package/dist/dev/playback/engines/background-video/adapter.js +0 -158
  479. package/dist/dev/playback/engines/background-video/adapter.js.map +0 -1
  480. package/dist/dev/playback/engines/background-video/engine.d.ts.map +0 -1
  481. package/dist/dev/playback/engines/background-video/engine.js.map +0 -1
  482. package/dist/dev/playback/engines/hls/adapter-audio-only.d.ts +0 -45
  483. package/dist/dev/playback/engines/hls/adapter-audio-only.d.ts.map +0 -1
  484. package/dist/dev/playback/engines/hls/adapter-audio-only.js +0 -119
  485. package/dist/dev/playback/engines/hls/adapter-audio-only.js.map +0 -1
  486. package/dist/dev/playback/engines/hls/adapter.d.ts +0 -44
  487. package/dist/dev/playback/engines/hls/adapter.d.ts.map +0 -1
  488. package/dist/dev/playback/engines/hls/adapter.js +0 -118
  489. package/dist/dev/playback/engines/hls/adapter.js.map +0 -1
@@ -9,6 +9,7 @@ import { createTrackedFetch, fetchStream } from "../../../network/fetch.js";
9
9
  import { createSegmentLoaderActor } from "../../actors/dom/segment-loader.js";
10
10
  import { createSourceBufferActor } from "../../actors/dom/source-buffer.js";
11
11
  import { failoverFetch } from "../../primitives/failover-fetch.js";
12
+ import { listen } from "@videojs/utils/dom";
12
13
  //#region src/playback/behaviors/dom/setup-buffer-actors.ts
13
14
  /**
14
15
  * **Per-type buffer + segment-loader actor setup.** Per available track
@@ -72,7 +73,7 @@ import { failoverFetch } from "../../primitives/failover-fetch.js";
72
73
  * `updateMediaSourceDuration`) only read these slots.
73
74
  */
74
75
  function setupBufferActors({ state, context, config }) {
75
- const { type, selectedKey, actorKey, loaderKey, fetch, forwardBuffer, backBuffer } = config;
76
+ const { type, selectedKey, actorKey, loaderKey, fetch, forwardBuffer, backBuffer, messagePipelines } = config;
76
77
  const derivedStateSignal = computed(() => {
77
78
  if (!context.mediaSource.get()) return "preconditions-unmet";
78
79
  return hasCodecs(getSelectedTrack({
@@ -86,22 +87,32 @@ function setupBufferActors({ state, context, config }) {
86
87
  states: {
87
88
  "preconditions-unmet": {},
88
89
  "buffer-ready": { entry: () => {
89
- const bufferActor = createSourceBufferActor(createSourceBuffer(context.mediaSource.get(), buildMimeCodec(getSelectedTrack({
90
+ const mediaSource = context.mediaSource.get();
91
+ const bufferActor = createSourceBufferActor(createSourceBuffer(mediaSource, buildMimeCodec(getSelectedTrack({
90
92
  presentation: state.presentation.get(),
91
93
  [selectedKey]: state[selectedKey].get()
92
94
  }, type))));
93
95
  const segmentLoader = createSegmentLoaderActor(bufferActor, fetch, {
94
96
  forwardBuffer,
95
- backBuffer
97
+ backBuffer,
98
+ messagePipelines
99
+ }, {
100
+ state,
101
+ context,
102
+ config
96
103
  });
97
104
  context[actorKey].set(bufferActor);
98
105
  context[loaderKey].set(segmentLoader);
99
- return () => {
106
+ const disconnect = new AbortController();
107
+ const teardownActors = () => {
100
108
  segmentLoader.destroy();
101
109
  bufferActor.destroy();
102
110
  context[loaderKey].set(void 0);
103
111
  context[actorKey].set(void 0);
112
+ disconnect.abort();
104
113
  };
114
+ listen(mediaSource, "sourceclose", teardownActors, { signal: disconnect.signal });
115
+ return teardownActors;
105
116
  } }
106
117
  }
107
118
  });
@@ -141,6 +152,7 @@ const setupVideoBufferActors = defineBehavior({
141
152
  context,
142
153
  config: {
143
154
  ...typeConfig,
155
+ messagePipelines: config.videoMessagePipelines,
144
156
  fetch: failoverFetch(trackedFetch, state, typeConfig)
145
157
  }
146
158
  });
@@ -181,6 +193,7 @@ const setupAudioBufferActors = defineBehavior({
181
193
  context,
182
194
  config: {
183
195
  ...typeConfig,
196
+ messagePipelines: config.audioMessagePipelines,
184
197
  fetch: failoverFetch(fetchStream, state, typeConfig)
185
198
  }
186
199
  });
@@ -1 +1 @@
1
- {"version":3,"file":"setup-buffer-actors.js","names":[],"sources":["../../../../../src/playback/behaviors/dom/setup-buffer-actors.ts"],"sourcesContent":["/**\n * **Per-type buffer + segment-loader actor setup.** Per available track\n * type (video / audio), when `mediaSource` is attached and the selected\n * track of that type is present in the presentation with codecs (partial\n * resolution from the multivariant playlist is enough — codecs live on\n * the `EXT-X-STREAM-INF` line, not in the per-type media playlist),\n * creates a `SourceBuffer`, a `SourceBufferActor`, and a\n * `SegmentLoaderActor` bound to that buffer-actor; publishes the per-type\n * actor slots. On `mediaSource` detach or behavior destroy, destroys both\n * actors in reverse order and clears the per-type slots so the next\n * source starts fresh.\n *\n * Each per-type variant (`setupVideoBufferActors` /\n * `setupAudioBufferActors`) is a single-positive-state reactor\n * (`'preconditions-unmet'` ↔ `'buffer-ready'`) gating only on its own\n * type. No cross-type coupling in `stateKeys` —\n * `setupVideoBufferActors` carries only `selectedVideoTrackId` (plus\n * `bandwidthState`, written by its trackedFetch), and audio mirrors.\n *\n * # Firefox `mozHasAudio` invariant\n *\n * Appending to a video `SourceBuffer` before the audio `SourceBuffer`\n * exists causes `mozHasAudio` to be permanently false in Firefox. With\n * the two per-type variants decoupled, the invariant is no longer\n * structural to a single `entry` body (as it was when both buffers were\n * created in one synchronous block inside a merged behavior). It's now\n * preserved by a chain of assumptions about how this behavior composes\n * with its upstream and downstream siblings:\n *\n * 1. **Upstream — default selections land in one `runPending`.**\n * `selectAudioTrack` (default audio) and `switchVideoTrack`\n * (default video) both subscribe to `state.presentation` flipping to\n * resolved; their effects run in the same `runPending` iteration and\n * write `selectedAudioTrackId` + `selectedVideoTrackId` within it.\n * 2. **Self — both per-type monitors flip in one `runPending`.**\n * After (1), both monitors re-evaluate and flip to `'buffer-ready'`\n * in the next `runPending`. Both `entry` bodies run synchronously\n * within that iteration — both `addSourceBuffer` calls land before\n * the iteration ends.\n * 3. **Downstream — `appendBuffer` is async.** `loadVideoSegments` /\n * `loadAudioSegments` read the per-type `xSegmentLoaderActor` slots;\n * their effects fire in the *next* `runPending` and the actual\n * `appendBuffer` requires a network round-trip via the\n * `SegmentLoaderActor` — many microtasks past both `addSourceBuffer`\n * calls.\n *\n * The cross-tick failure mode — a user-initiated audio track switch\n * *after* video segments have begun appending — is out of scope for\n * this behavior and would be addressed in the buffer/segment-loading\n * path via `changeType`-aware logic.\n *\n * # Sole writer\n *\n * `setupVideoBufferActors` is sole writer of `videoBufferActor` +\n * `videoSegmentLoaderActor` (and `bandwidthState` via its\n * trackedFetch); `setupAudioBufferActors` is sole writer of\n * `audioBufferActor` + `audioSegmentLoaderActor`. Both read\n * `mediaSource` from `setupMediaSource`. Downstream MSE behaviors\n * (`loadVideoSegments`, `loadAudioSegments`, `endOfStream`,\n * `updateMediaSourceDuration`) only read these slots.\n */\nimport { defineBehavior } from '../../../core/composition/create-composition';\nimport type { Reactor } from '../../../core/reactors/create-machine-reactor';\nimport { createMachineReactor } from '../../../core/reactors/create-machine-reactor';\nimport { computed, type ReadonlySignal, type Signal } from '../../../core/signals/primitives';\nimport { buildMimeCodec, createSourceBuffer } from '../../../media/dom/mse/mediasource-setup';\nimport type { MaybeResolvedPresentation, PartiallyResolvedTrack } from '../../../media/types';\nimport type { GetCdnId } from '../../../media/utils/cdn';\nimport { getSelectedTrack, type TrackSelectionState } from '../../../media/utils/track-selection';\nimport { hasCodecs } from '../../../media/utils/tracks';\nimport type { BandwidthState } from '../../../network/bandwidth-estimator';\nimport { createTrackedFetch, type FetchBytes, fetchStream } from '../../../network/fetch';\nimport {\n createSegmentLoaderActor,\n type SegmentLoaderActor,\n type SegmentLoaderActorConfig,\n} from '../../actors/dom/segment-loader';\nimport { createSourceBufferActor, type SourceBufferActor } from '../../actors/dom/source-buffer';\nimport { failoverFetch } from '../../primitives/failover-fetch';\nimport { AUDIO_TYPE_CONFIG, VIDEO_TYPE_CONFIG } from '../../primitives/track-types';\n\n/**\n * Media track type for MSE buffer setup.\n * Text tracks are excluded as they don't use MSE SourceBuffers.\n */\nexport type MediaTrackType = 'video' | 'audio';\n\nexport interface BufferActorsState {\n presentation?: MaybeResolvedPresentation;\n selectedVideoTrackId?: string;\n selectedAudioTrackId?: string;\n bandwidthState?: BandwidthState;\n}\n\nexport interface BufferActorsContext {\n mediaSource?: MediaSource;\n videoBufferActor?: SourceBufferActor;\n audioBufferActor?: SourceBufferActor;\n videoSegmentLoaderActor?: SegmentLoaderActor;\n audioSegmentLoaderActor?: SegmentLoaderActor;\n}\n\ntype BufferActorsFsmState = 'preconditions-unmet' | 'buffer-ready';\n\ntype SelectedTrackKey = 'selectedVideoTrackId' | 'selectedAudioTrackId';\ntype BufferActorKey = 'videoBufferActor' | 'audioBufferActor';\ntype SegmentLoaderActorKey = 'videoSegmentLoaderActor' | 'audioSegmentLoaderActor';\n\ntype BufferActorsStateMap<K extends SelectedTrackKey> = {\n presentation: ReadonlySignal<BufferActorsState['presentation']>;\n} & { [P in K]: ReadonlySignal<BufferActorsState[P]> };\n\ntype BufferActorsContextMap<A extends BufferActorKey, L extends SegmentLoaderActorKey> = {\n mediaSource: ReadonlySignal<BufferActorsContext['mediaSource']>;\n} & { [P in A]: Signal<SourceBufferActor | undefined> } & {\n [P in L]: Signal<SegmentLoaderActor | undefined>;\n};\n\n// ============================================================================\n// Specialization helper\n//\n// `setupBufferActors` has the same shape as a Behavior `setup` function:\n// `({ state, context, config }) => reactor`. Each `setupXBufferActors`\n// export below calls it from inside its own `defineBehavior` setup,\n// passing its narrowed `state` / `context` through directly and supplying\n// the per-type `selectedKey`, `actorKey`, `loaderKey`, `type` discriminator,\n// and `fetch` inline.\n//\n// Gating is per-type only: no cross-type coupling. See the file-level\n// JSDoc for how the Firefox `mozHasAudio` invariant survives the split\n// via SPF's effect coalescing.\n// ============================================================================\n\nfunction setupBufferActors<K extends SelectedTrackKey, A extends BufferActorKey, L extends SegmentLoaderActorKey>({\n state,\n context,\n config,\n}: {\n state: BufferActorsStateMap<K>;\n context: BufferActorsContextMap<A, L>;\n config: {\n type: MediaTrackType;\n selectedKey: K;\n actorKey: A;\n loaderKey: L;\n fetch: FetchBytes;\n } & SegmentLoaderActorConfig;\n}): Reactor<BufferActorsFsmState | 'destroying' | 'destroyed'> {\n const { type, selectedKey, actorKey, loaderKey, fetch, forwardBuffer, backBuffer } = config;\n const derivedStateSignal = computed<BufferActorsFsmState>(() => {\n if (!context.mediaSource.get()) return 'preconditions-unmet';\n const selection: TrackSelectionState = {\n presentation: state.presentation.get(),\n [selectedKey]: state[selectedKey].get(),\n };\n return hasCodecs(getSelectedTrack(selection, type)) ? 'buffer-ready' : 'preconditions-unmet';\n });\n\n return createMachineReactor<BufferActorsFsmState>({\n initial: 'preconditions-unmet',\n monitor: () => derivedStateSignal.get(),\n states: {\n 'preconditions-unmet': {},\n\n 'buffer-ready': {\n entry: () => {\n const mediaSource = context.mediaSource.get()!;\n const selection: TrackSelectionState = {\n presentation: state.presentation.get(),\n [selectedKey]: state[selectedKey].get(),\n };\n const track = getSelectedTrack(selection, type) as PartiallyResolvedTrack;\n const buffer = createSourceBuffer(mediaSource, buildMimeCodec(track));\n const bufferActor = createSourceBufferActor(buffer);\n const segmentLoader = createSegmentLoaderActor(bufferActor, fetch, { forwardBuffer, backBuffer });\n\n // Synchronous slot writes — load-bearing for the Firefox\n // `mozHasAudio` invariant (see file-level JSDoc). Both per-type\n // entries' `addSourceBuffer` calls must land in the same\n // `runPending` iteration; synchronous writes keep the contiguous\n // JS frame. Downstream `loadXSegments` effects fire in the\n // *next* `runPending`.\n context[actorKey].set(bufferActor);\n context[loaderKey].set(segmentLoader);\n\n // State-exit cleanup — fires when `mediaSource` detaches, the\n // selection unsets, or the behavior is destroyed. Destroy the\n // loader before its upstream buffer-actor so any in-flight\n // `appendBuffer` is aborted before the buffer-actor's own\n // teardown.\n return () => {\n segmentLoader.destroy();\n bufferActor.destroy();\n context[loaderKey].set(undefined);\n context[actorKey].set(undefined);\n };\n },\n },\n },\n });\n}\n\n// ============================================================================\n// Specialized exports — one per media type\n// ============================================================================\n\n/**\n * Set up the video `SourceBufferActor` + `SegmentLoaderActor`. Fires\n * when `mediaSource` is attached and the selected video track is\n * present in the presentation with codecs. Gates only on video state —\n * no cross-type coupling. Owns a bandwidth-sampling `trackedFetch` and\n * is sole writer of `state.bandwidthState`.\n */\nexport const setupVideoBufferActors = defineBehavior({\n stateKeys: ['presentation', 'selectedVideoTrackId', 'bandwidthState'] as const,\n contextKeys: ['mediaSource', 'videoBufferActor', 'videoSegmentLoaderActor'] as const,\n setup: ({\n state,\n context,\n config = {},\n }: {\n state: BufferActorsStateMap<'selectedVideoTrackId'> & {\n bandwidthState: Signal<BufferActorsState['bandwidthState']>;\n };\n context: BufferActorsContextMap<'videoBufferActor', 'videoSegmentLoaderActor'>;\n config?: SegmentLoaderActorConfig & { getCdnId?: GetCdnId };\n }) => {\n // Bandwidth-sampling fetch. The factory accumulates EWMA state\n // internally; the callback bridges samples to engine state for ABR.\n // Created once at behavior setup time so the EWMA accumulates across\n // source resets (each per-source loader-actor receives the same\n // tracked fetch when reconstructed in `buffer-ready` entry).\n const trackedFetch = createTrackedFetch(\n state.bandwidthState.get() ?? {\n fastEstimate: 0,\n fastTotalWeight: 0,\n slowEstimate: 0,\n slowTotalWeight: 0,\n bytesSampled: 0,\n },\n (next) => state.bandwidthState.set(next)\n );\n // Engine `config` layers over the per-type defaults; `failoverFetch` reads\n // its `selectedKey` + `getCdnId` from the merged result.\n const typeConfig = { ...VIDEO_TYPE_CONFIG, ...config };\n return setupBufferActors({\n state,\n context,\n config: { ...typeConfig, fetch: failoverFetch(trackedFetch, state, typeConfig) },\n });\n },\n});\n\n/**\n * Set up the audio `SourceBufferActor` + `SegmentLoaderActor`. Same\n * shape as `setupVideoBufferActors`, narrowed to audio. Today supplies\n * a non-sampling `fetchStream` (no audio ABR); adding audio ABR is a\n * localized change to this setup body (swap `fetchStream` for a\n * `createTrackedFetch` call + declare `bandwidthState` writable here)\n * without touching the shared helper. See\n * `internal/design/spf/features/audio-abr.md` for the design surface\n * (bandwidth-state sharing, multi-writer coordination, EWMA mixed-\n * source sampling).\n *\n * **Mid-stream audio track switching is NOT this behavior's concern.**\n * Slot writes to `selectedAudioTrackId` (default selection, programmatic\n * filter-driven, future ABR) are owned by `switchAudioTrack` / future\n * `switchAudioQuality` in `track-switching.ts`. Flush orchestration on\n * track change is dispatched from there via `audioBufferActor.send(...)`\n * — keeping this setup behavior focused on per-source actor lifecycle.\n */\nexport const setupAudioBufferActors = defineBehavior({\n stateKeys: ['presentation', 'selectedAudioTrackId'] as const,\n contextKeys: ['mediaSource', 'audioBufferActor', 'audioSegmentLoaderActor'] as const,\n setup: ({\n state,\n context,\n config = {},\n }: {\n state: BufferActorsStateMap<'selectedAudioTrackId'>;\n context: BufferActorsContextMap<'audioBufferActor', 'audioSegmentLoaderActor'>;\n config?: SegmentLoaderActorConfig & { getCdnId?: GetCdnId };\n }) => {\n // Key order mirrors setupVideoBufferActors.\n const typeConfig = { ...AUDIO_TYPE_CONFIG, ...config };\n return setupBufferActors({\n state,\n context,\n config: { ...typeConfig, fetch: failoverFetch(fetchStream, state, typeConfig) },\n });\n },\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqIA,SAAS,kBAAyG,EAChH,OACA,SACA,UAW6D;CAC7D,MAAM,EAAE,MAAM,aAAa,UAAU,WAAW,OAAO,eAAe,eAAe;CACrF,MAAM,qBAAqB,eAAqC;AAC9D,MAAI,CAAC,QAAQ,YAAY,KAAK,CAAE,QAAO;AAKvC,SAAO,UAAU,iBAJsB;GACrC,cAAc,MAAM,aAAa,KAAK;IACrC,cAAc,MAAM,aAAa,KAAK;GACxC,EAC4C,KAAK,CAAC,GAAG,iBAAiB;GACvE;AAEF,QAAO,qBAA2C;EAChD,SAAS;EACT,eAAe,mBAAmB,KAAK;EACvC,QAAQ;GACN,uBAAuB,EAAE;GAEzB,gBAAgB,EACd,aAAa;IAQX,MAAM,cAAc,wBADL,mBANK,QAAQ,YAAY,KAAK,EAME,eADjC,iBAJyB;KACrC,cAAc,MAAM,aAAa,KAAK;MACrC,cAAc,MAAM,aAAa,KAAK;KACxC,EACyC,KAAK,CACqB,CAAC,CAClB;IACnD,MAAM,gBAAgB,yBAAyB,aAAa,OAAO;KAAE;KAAe;KAAY,CAAC;AAQjG,YAAQ,UAAU,IAAI,YAAY;AAClC,YAAQ,WAAW,IAAI,cAAc;AAOrC,iBAAa;AACX,mBAAc,SAAS;AACvB,iBAAY,SAAS;AACrB,aAAQ,WAAW,IAAI,KAAA,EAAU;AACjC,aAAQ,UAAU,IAAI,KAAA,EAAU;;MAGrC;GACF;EACF,CAAC;;;;;;;;;AAcJ,MAAa,yBAAyB,eAAe;CACnD,WAAW;EAAC;EAAgB;EAAwB;EAAiB;CACrE,aAAa;EAAC;EAAe;EAAoB;EAA0B;CAC3E,QAAQ,EACN,OACA,SACA,SAAS,EAAE,OAOP;EAMJ,MAAM,eAAe,mBACnB,MAAM,eAAe,KAAK,IAAI;GAC5B,cAAc;GACd,iBAAiB;GACjB,cAAc;GACd,iBAAiB;GACjB,cAAc;GACf,GACA,SAAS,MAAM,eAAe,IAAI,KAAK,CACzC;EAGD,MAAM,aAAa;GAAE,GAAG;GAAmB,GAAG;GAAQ;AACtD,SAAO,kBAAkB;GACvB;GACA;GACA,QAAQ;IAAE,GAAG;IAAY,OAAO,cAAc,cAAc,OAAO,WAAW;IAAE;GACjF,CAAC;;CAEL,CAAC;;;;;;;;;;;;;;;;;;;AAoBF,MAAa,yBAAyB,eAAe;CACnD,WAAW,CAAC,gBAAgB,uBAAuB;CACnD,aAAa;EAAC;EAAe;EAAoB;EAA0B;CAC3E,QAAQ,EACN,OACA,SACA,SAAS,EAAE,OAKP;EAEJ,MAAM,aAAa;GAAE,GAAG;GAAmB,GAAG;GAAQ;AACtD,SAAO,kBAAkB;GACvB;GACA;GACA,QAAQ;IAAE,GAAG;IAAY,OAAO,cAAc,aAAa,OAAO,WAAW;IAAE;GAChF,CAAC;;CAEL,CAAC"}
1
+ {"version":3,"file":"setup-buffer-actors.js","names":[],"sources":["../../../../../src/playback/behaviors/dom/setup-buffer-actors.ts"],"sourcesContent":["/**\n * **Per-type buffer + segment-loader actor setup.** Per available track\n * type (video / audio), when `mediaSource` is attached and the selected\n * track of that type is present in the presentation with codecs (partial\n * resolution from the multivariant playlist is enough — codecs live on\n * the `EXT-X-STREAM-INF` line, not in the per-type media playlist),\n * creates a `SourceBuffer`, a `SourceBufferActor`, and a\n * `SegmentLoaderActor` bound to that buffer-actor; publishes the per-type\n * actor slots. On `mediaSource` detach or behavior destroy, destroys both\n * actors in reverse order and clears the per-type slots so the next\n * source starts fresh.\n *\n * Each per-type variant (`setupVideoBufferActors` /\n * `setupAudioBufferActors`) is a single-positive-state reactor\n * (`'preconditions-unmet'` ↔ `'buffer-ready'`) gating only on its own\n * type. No cross-type coupling in `stateKeys` —\n * `setupVideoBufferActors` carries only `selectedVideoTrackId` (plus\n * `bandwidthState`, written by its trackedFetch), and audio mirrors.\n *\n * # Firefox `mozHasAudio` invariant\n *\n * Appending to a video `SourceBuffer` before the audio `SourceBuffer`\n * exists causes `mozHasAudio` to be permanently false in Firefox. With\n * the two per-type variants decoupled, the invariant is no longer\n * structural to a single `entry` body (as it was when both buffers were\n * created in one synchronous block inside a merged behavior). It's now\n * preserved by a chain of assumptions about how this behavior composes\n * with its upstream and downstream siblings:\n *\n * 1. **Upstream — default selections land in one `runPending`.**\n * `selectAudioTrack` (default audio) and `switchVideoTrack`\n * (default video) both subscribe to `state.presentation` flipping to\n * resolved; their effects run in the same `runPending` iteration and\n * write `selectedAudioTrackId` + `selectedVideoTrackId` within it.\n * 2. **Self — both per-type monitors flip in one `runPending`.**\n * After (1), both monitors re-evaluate and flip to `'buffer-ready'`\n * in the next `runPending`. Both `entry` bodies run synchronously\n * within that iteration — both `addSourceBuffer` calls land before\n * the iteration ends.\n * 3. **Downstream — `appendBuffer` is async.** `loadVideoSegments` /\n * `loadAudioSegments` read the per-type `xSegmentLoaderActor` slots;\n * their effects fire in the *next* `runPending` and the actual\n * `appendBuffer` requires a network round-trip via the\n * `SegmentLoaderActor` — many microtasks past both `addSourceBuffer`\n * calls.\n *\n * The cross-tick failure mode — a user-initiated audio track switch\n * *after* video segments have begun appending — is out of scope for\n * this behavior and would be addressed in the buffer/segment-loading\n * path via `changeType`-aware logic.\n *\n * # Sole writer\n *\n * `setupVideoBufferActors` is sole writer of `videoBufferActor` +\n * `videoSegmentLoaderActor` (and `bandwidthState` via its\n * trackedFetch); `setupAudioBufferActors` is sole writer of\n * `audioBufferActor` + `audioSegmentLoaderActor`. Both read\n * `mediaSource` from `setupMediaSource`. Downstream MSE behaviors\n * (`loadVideoSegments`, `loadAudioSegments`, `endOfStream`,\n * `updateMediaSourceDuration`) only read these slots.\n */\nimport { listen } from '@videojs/utils/dom';\nimport { defineBehavior } from '../../../core/composition/create-composition';\nimport type { Reactor } from '../../../core/reactors/create-machine-reactor';\nimport { createMachineReactor } from '../../../core/reactors/create-machine-reactor';\nimport { computed, type ReadonlySignal, type Signal } from '../../../core/signals/primitives';\nimport { buildMimeCodec, createSourceBuffer } from '../../../media/dom/mse/mediasource-setup';\nimport type { MaybeResolvedPresentation, PartiallyResolvedTrack } from '../../../media/types';\nimport type { GetCdnId } from '../../../media/utils/cdn';\nimport { getSelectedTrack, type TrackSelectionState } from '../../../media/utils/track-selection';\nimport { hasCodecs } from '../../../media/utils/tracks';\nimport type { BandwidthState } from '../../../network/bandwidth-estimator';\nimport { createTrackedFetch, type FetchBytes, fetchStream } from '../../../network/fetch';\nimport {\n createSegmentLoaderActor,\n type SegmentLoaderActor,\n type SegmentLoaderActorConfig,\n} from '../../actors/dom/segment-loader';\nimport { createSourceBufferActor, type SourceBufferActor } from '../../actors/dom/source-buffer';\nimport { failoverFetch } from '../../primitives/failover-fetch';\nimport type { MessagePipelines } from '../../primitives/segment-load-pipeline';\nimport { AUDIO_TYPE_CONFIG, VIDEO_TYPE_CONFIG } from '../../primitives/track-types';\n\n/**\n * Media track type for MSE buffer setup.\n * Text tracks are excluded as they don't use MSE SourceBuffers.\n */\nexport type MediaTrackType = 'video' | 'audio';\n\nexport interface BufferActorsState {\n presentation?: MaybeResolvedPresentation;\n selectedVideoTrackId?: string;\n selectedAudioTrackId?: string;\n bandwidthState?: BandwidthState;\n}\n\nexport interface BufferActorsContext {\n mediaSource?: MediaSource;\n videoBufferActor?: SourceBufferActor;\n audioBufferActor?: SourceBufferActor;\n videoSegmentLoaderActor?: SegmentLoaderActor;\n audioSegmentLoaderActor?: SegmentLoaderActor;\n}\n\ntype BufferActorsFsmState = 'preconditions-unmet' | 'buffer-ready';\n\ntype SelectedTrackKey = 'selectedVideoTrackId' | 'selectedAudioTrackId';\ntype BufferActorKey = 'videoBufferActor' | 'audioBufferActor';\ntype SegmentLoaderActorKey = 'videoSegmentLoaderActor' | 'audioSegmentLoaderActor';\n\ntype BufferActorsStateMap<K extends SelectedTrackKey> = {\n presentation: ReadonlySignal<BufferActorsState['presentation']>;\n} & { [P in K]: ReadonlySignal<BufferActorsState[P]> };\n\ntype BufferActorsContextMap<A extends BufferActorKey, L extends SegmentLoaderActorKey> = {\n mediaSource: ReadonlySignal<BufferActorsContext['mediaSource']>;\n} & { [P in A]: Signal<SourceBufferActor | undefined> } & {\n [P in L]: Signal<SegmentLoaderActor | undefined>;\n};\n\n// ============================================================================\n// Specialization helper\n//\n// `setupBufferActors` has the same shape as a Behavior `setup` function:\n// `({ state, context, config }) => reactor`. Each `setupXBufferActors`\n// export below calls it from inside its own `defineBehavior` setup,\n// passing its narrowed `state` / `context` through directly and supplying\n// the per-type `selectedKey`, `actorKey`, `loaderKey`, `type` discriminator,\n// and `fetch` inline.\n//\n// Gating is per-type only: no cross-type coupling. See the file-level\n// JSDoc for how the Firefox `mozHasAudio` invariant survives the split\n// via SPF's effect coalescing.\n// ============================================================================\n\nfunction setupBufferActors<K extends SelectedTrackKey, A extends BufferActorKey, L extends SegmentLoaderActorKey>({\n state,\n context,\n config,\n}: {\n state: BufferActorsStateMap<K>;\n context: BufferActorsContextMap<A, L>;\n config: {\n type: MediaTrackType;\n selectedKey: K;\n actorKey: A;\n loaderKey: L;\n fetch: FetchBytes;\n } & SegmentLoaderActorConfig;\n}): Reactor<BufferActorsFsmState | 'destroying' | 'destroyed'> {\n const { type, selectedKey, actorKey, loaderKey, fetch, forwardBuffer, backBuffer, messagePipelines } = config;\n const derivedStateSignal = computed<BufferActorsFsmState>(() => {\n if (!context.mediaSource.get()) return 'preconditions-unmet';\n const selection: TrackSelectionState = {\n presentation: state.presentation.get(),\n [selectedKey]: state[selectedKey].get(),\n };\n return hasCodecs(getSelectedTrack(selection, type)) ? 'buffer-ready' : 'preconditions-unmet';\n });\n\n return createMachineReactor<BufferActorsFsmState>({\n initial: 'preconditions-unmet',\n monitor: () => derivedStateSignal.get(),\n states: {\n 'preconditions-unmet': {},\n\n 'buffer-ready': {\n entry: () => {\n const mediaSource = context.mediaSource.get()!;\n const selection: TrackSelectionState = {\n presentation: state.presentation.get(),\n [selectedKey]: state[selectedKey].get(),\n };\n const track = getSelectedTrack(selection, type) as PartiallyResolvedTrack;\n const buffer = createSourceBuffer(mediaSource, buildMimeCodec(track));\n const bufferActor = createSourceBufferActor(buffer);\n const segmentLoader = createSegmentLoaderActor(\n bufferActor,\n fetch,\n { forwardBuffer, backBuffer, messagePipelines },\n // Thread the composition deps opaquely to steps (relocation reads\n // `state.mediaContainerData`); the loader/this behavior never read them.\n { state, context, config }\n );\n\n // Synchronous slot writes — load-bearing for the Firefox\n // `mozHasAudio` invariant (see file-level JSDoc). Both per-type\n // entries' `addSourceBuffer` calls must land in the same\n // `runPending` iteration; synchronous writes keep the contiguous\n // JS frame. Downstream `loadXSegments` effects fire in the\n // *next* `runPending`.\n context[actorKey].set(bufferActor);\n context[loaderKey].set(segmentLoader);\n\n // State-exit cleanup — fires when `mediaSource` detaches, the\n // selection unsets, or the behavior is destroyed. Destroy the\n // loader before its upstream buffer-actor so any in-flight\n // `appendBuffer` is aborted before the buffer-actor's own\n // teardown. Idempotent, so the sourceclose listener below shares it.\n const disconnect = new AbortController();\n const teardownActors = () => {\n segmentLoader.destroy();\n bufferActor.destroy();\n context[loaderKey].set(undefined);\n context[actorKey].set(undefined);\n disconnect.abort();\n };\n\n // Also tear down when the UA closes the MediaSource out from under\n // us (notably an AirPlay handoff — see `setupAirPlay`). Must be a\n // DOM listener, not reactive cleanup: already-queued append\n // continuations outrun any effect flush, and only a synchronous\n // abort keeps them off the dead SourceBuffer (InvalidStateError).\n listen(mediaSource, 'sourceclose', teardownActors, { signal: disconnect.signal });\n\n return teardownActors;\n },\n },\n },\n });\n}\n\n// ============================================================================\n// Specialized exports — one per media type\n// ============================================================================\n\n/**\n * Set up the video `SourceBufferActor` + `SegmentLoaderActor`. Fires\n * when `mediaSource` is attached and the selected video track is\n * present in the presentation with codecs. Gates only on video state —\n * no cross-type coupling. Owns a bandwidth-sampling `trackedFetch` and\n * is sole writer of `state.bandwidthState`.\n */\nexport const setupVideoBufferActors = defineBehavior({\n stateKeys: ['presentation', 'selectedVideoTrackId', 'bandwidthState'] as const,\n contextKeys: ['mediaSource', 'videoBufferActor', 'videoSegmentLoaderActor'] as const,\n setup: ({\n state,\n context,\n config = {},\n }: {\n state: BufferActorsStateMap<'selectedVideoTrackId'> & {\n bandwidthState: Signal<BufferActorsState['bandwidthState']>;\n };\n context: BufferActorsContextMap<'videoBufferActor', 'videoSegmentLoaderActor'>;\n config?: SegmentLoaderActorConfig & {\n getCdnId?: GetCdnId;\n /** Optional non-zero-PTS relocation pipelines (Tier-1); the loader uses its Tier-0 default when absent. */\n videoMessagePipelines?: MessagePipelines;\n };\n }) => {\n // Bandwidth-sampling fetch. The factory accumulates EWMA state\n // internally; the callback bridges samples to engine state for ABR.\n // Created once at behavior setup time so the EWMA accumulates across\n // source resets (each per-source loader-actor receives the same\n // tracked fetch when reconstructed in `buffer-ready` entry).\n const trackedFetch = createTrackedFetch(\n state.bandwidthState.get() ?? {\n fastEstimate: 0,\n fastTotalWeight: 0,\n slowEstimate: 0,\n slowTotalWeight: 0,\n bytesSampled: 0,\n },\n (next) => state.bandwidthState.set(next)\n );\n // Engine `config` layers over the per-type defaults; `failoverFetch` reads\n // its `selectedKey` + `getCdnId` from the merged result.\n const typeConfig = { ...VIDEO_TYPE_CONFIG, ...config };\n return setupBufferActors({\n state,\n context,\n config: {\n ...typeConfig,\n messagePipelines: config.videoMessagePipelines,\n fetch: failoverFetch(trackedFetch, state, typeConfig),\n },\n });\n },\n});\n\n/**\n * Set up the audio `SourceBufferActor` + `SegmentLoaderActor`. Same\n * shape as `setupVideoBufferActors`, narrowed to audio. Today supplies\n * a non-sampling `fetchStream` (no audio ABR); adding audio ABR is a\n * localized change to this setup body (swap `fetchStream` for a\n * `createTrackedFetch` call + declare `bandwidthState` writable here)\n * without touching the shared helper. See\n * `internal/design/spf/features/audio-abr.md` for the design surface\n * (bandwidth-state sharing, multi-writer coordination, EWMA mixed-\n * source sampling).\n *\n * **Mid-stream audio track switching is NOT this behavior's concern.**\n * Slot writes to `selectedAudioTrackId` (default selection, programmatic\n * filter-driven, future ABR) are owned by `switchAudioTrack` / future\n * `switchAudioQuality` in `track-switching.ts`. Flush orchestration on\n * track change is dispatched from there via `audioBufferActor.send(...)`\n * — keeping this setup behavior focused on per-source actor lifecycle.\n */\nexport const setupAudioBufferActors = defineBehavior({\n stateKeys: ['presentation', 'selectedAudioTrackId'] as const,\n contextKeys: ['mediaSource', 'audioBufferActor', 'audioSegmentLoaderActor'] as const,\n setup: ({\n state,\n context,\n config = {},\n }: {\n state: BufferActorsStateMap<'selectedAudioTrackId'>;\n context: BufferActorsContextMap<'audioBufferActor', 'audioSegmentLoaderActor'>;\n config?: SegmentLoaderActorConfig & {\n getCdnId?: GetCdnId;\n /** Optional non-zero-PTS relocation pipelines (Tier-1); the loader uses its Tier-0 default when absent. */\n audioMessagePipelines?: MessagePipelines;\n };\n }) => {\n // Key order mirrors setupVideoBufferActors.\n const typeConfig = { ...AUDIO_TYPE_CONFIG, ...config };\n return setupBufferActors({\n state,\n context,\n config: {\n ...typeConfig,\n messagePipelines: config.audioMessagePipelines,\n fetch: failoverFetch(fetchStream, state, typeConfig),\n },\n });\n },\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuIA,SAAS,kBAAyG,EAChH,OACA,SACA,UAW6D;CAC7D,MAAM,EAAE,MAAM,aAAa,UAAU,WAAW,OAAO,eAAe,YAAY,qBAAqB;CACvG,MAAM,qBAAqB,eAAqC;EAC9D,IAAI,CAAC,QAAQ,YAAY,IAAI,GAAG,OAAO;EAKvC,OAAO,UAAU,iBAAiB;GAHhC,cAAc,MAAM,aAAa,IAAI;IACpC,cAAc,MAAM,YAAY,CAAC,IAAI;EAEE,GAAG,IAAI,CAAC,IAAI,iBAAiB;CACzE,CAAC;CAED,OAAO,qBAA2C;EAChD,SAAS;EACT,eAAe,mBAAmB,IAAI;EACtC,QAAQ;GACN,uBAAuB,CAAC;GAExB,gBAAgB,EACd,aAAa;IACX,MAAM,cAAc,QAAQ,YAAY,IAAI;IAO5C,MAAM,cAAc,wBADL,mBAAmB,aAAa,eADjC,iBAAiB;KAH7B,cAAc,MAAM,aAAa,IAAI;MACpC,cAAc,MAAM,YAAY,CAAC,IAAI;IAED,GAAG,IACwB,CAAC,CAClB,CAAC;IAClD,MAAM,gBAAgB,yBACpB,aACA,OACA;KAAE;KAAe;KAAY;IAAiB,GAG9C;KAAE;KAAO;KAAS;IAAO,CAC3B;IAQA,QAAQ,SAAS,CAAC,IAAI,WAAW;IACjC,QAAQ,UAAU,CAAC,IAAI,aAAa;IAOpC,MAAM,aAAa,IAAI,gBAAgB;IACvC,MAAM,uBAAuB;KAC3B,cAAc,QAAQ;KACtB,YAAY,QAAQ;KACpB,QAAQ,UAAU,CAAC,IAAI,KAAA,CAAS;KAChC,QAAQ,SAAS,CAAC,IAAI,KAAA,CAAS;KAC/B,WAAW,MAAM;IACnB;IAOA,OAAO,aAAa,eAAe,gBAAgB,EAAE,QAAQ,WAAW,OAAO,CAAC;IAEhF,OAAO;GACT,EACF;EACF;CACF,CAAC;AACH;;;;;;;;AAaA,MAAa,yBAAyB,eAAe;CACnD,WAAW;EAAC;EAAgB;EAAwB;CAAgB;CACpE,aAAa;EAAC;EAAe;EAAoB;CAAyB;CAC1E,QAAQ,EACN,OACA,SACA,SAAS,CAAC,QAWN;EAMJ,MAAM,eAAe,mBACnB,MAAM,eAAe,IAAI,KAAK;GAC5B,cAAc;GACd,iBAAiB;GACjB,cAAc;GACd,iBAAiB;GACjB,cAAc;EAChB,IACC,SAAS,MAAM,eAAe,IAAI,IAAI,CACzC;EAGA,MAAM,aAAa;GAAE,GAAG;GAAmB,GAAG;EAAO;EACrD,OAAO,kBAAkB;GACvB;GACA;GACA,QAAQ;IACN,GAAG;IACH,kBAAkB,OAAO;IACzB,OAAO,cAAc,cAAc,OAAO,UAAU;GACtD;EACF,CAAC;CACH;AACF,CAAC;;;;;;;;;;;;;;;;;;;AAoBD,MAAa,yBAAyB,eAAe;CACnD,WAAW,CAAC,gBAAgB,sBAAsB;CAClD,aAAa;EAAC;EAAe;EAAoB;CAAyB;CAC1E,QAAQ,EACN,OACA,SACA,SAAS,CAAC,QASN;EAEJ,MAAM,aAAa;GAAE,GAAG;GAAmB,GAAG;EAAO;EACrD,OAAO,kBAAkB;GACvB;GACA;GACA,QAAQ;IACN,GAAG;IACH,kBAAkB,OAAO;IACzB,OAAO,cAAc,aAAa,OAAO,UAAU;GACrD;EACF,CAAC;CACH;AACF,CAAC"}
@@ -1,8 +1,9 @@
1
- import { computed } from "../../../core/signals/primitives.js";
1
+ import { computed, signal } from "../../../core/signals/primitives.js";
2
2
  import { defineBehavior } from "../../../core/composition/create-composition.js";
3
3
  import { createMachineReactor } from "../../../core/reactors/create-machine-reactor.js";
4
4
  import { isResolvedPresentation } from "../../../media/types/index.js";
5
5
  import { attachMediaSource, createMediaSource, waitForMediaSourceOpen } from "../../../media/dom/mse/mediasource-setup.js";
6
+ import { listen } from "@videojs/utils/dom";
6
7
  //#region src/playback/behaviors/dom/setup-mediasource.ts
7
8
  /**
8
9
  * **Own the MediaSource lifecycle for the current source.** When a resolved
@@ -38,27 +39,55 @@ import { attachMediaSource, createMediaSource, waitForMediaSourceOpen } from "..
38
39
  * and clears `context.mediaSource`. Order: abort first (prevents a late
39
40
  * publish racing the slot clear), then detach, then clear.
40
41
  *
42
+ * # Sourceclose recovery
43
+ *
44
+ * The behavior owns one **unclosed** MediaSource per source identity. The UA can
45
+ * close the attached MediaSource out from under the engine (Safari on an
46
+ * AirPlay handoff — see `setupAirPlay` — or a ManagedMediaSource evicted
47
+ * under memory pressure), and a closed MediaSource can never reopen. The
48
+ * `sourceclose` listener tears the attachment down synchronously; every
49
+ * teardown records a local close-fact, which holds the machine out until
50
+ * the fact is consumed — then the re-derive comes back in with a fresh
51
+ * MediaSource for the *same* source. Cause-agnostic. Consumption honors an
52
+ * observed `loadingSuspended` (attaching runs `element.load()` — new loading
53
+ * work, e.g. resource selection under an active AirPlay receiver), and
54
+ * happens only while the machine is out, so a suspension can never tear
55
+ * down an existing attachment.
56
+ *
41
57
  * Sole writer of `context.mediaSource`; other MSE behaviors
42
58
  * (`setupVideoBufferActors`, `setupAudioBufferActors`,
43
59
  * `updateMediaSourceDuration`, `endOfStream`, `loadVideoSegments`) only
44
60
  * read.
45
61
  */
46
- function deriveState(presentation, mediaElement) {
62
+ function deriveState(presentation, mediaElement, mediaSourceClosed) {
47
63
  if (!mediaElement || !isResolvedPresentation(presentation)) return "preconditions-unmet";
64
+ if (mediaSourceClosed) return "preconditions-unmet";
48
65
  return "mediasource-attached";
49
66
  }
50
- function setupMediaSourceSetup({ state, context }) {
51
- const derivedStateSignal = computed(() => deriveState(state.presentation.get(), context.mediaElement.get()));
67
+ function setupMediaSourceSetup({ state, context, config = {} }) {
68
+ const attach = config.attachMediaSource ?? attachMediaSource;
69
+ const loadingSuspended = state.loadingSuspended;
70
+ const mediaSourceClosed = signal(false);
71
+ const derivedStateSignal = computed(() => deriveState(state.presentation.get(), context.mediaElement.get(), mediaSourceClosed.get()));
52
72
  return createMachineReactor({
53
73
  initial: "preconditions-unmet",
54
74
  monitor: () => derivedStateSignal.get(),
55
75
  states: {
56
- "preconditions-unmet": {},
76
+ "preconditions-unmet": { effects: () => {
77
+ if (mediaSourceClosed.get() && !loadingSuspended?.get()) mediaSourceClosed.set(false);
78
+ } },
57
79
  "mediasource-attached": { entry: () => {
58
80
  const mediaElement = context.mediaElement.get();
59
81
  const controller = new AbortController();
60
82
  const mediaSource = createMediaSource({ preferManaged: true });
61
- const { detach } = attachMediaSource(mediaSource, mediaElement);
83
+ const { detach } = attach(mediaSource, mediaElement);
84
+ const teardown = () => {
85
+ mediaSourceClosed.set(true);
86
+ controller.abort();
87
+ context.mediaSource.set(void 0);
88
+ detach({ deferReset: true });
89
+ };
90
+ listen(mediaSource, "sourceclose", teardown, { signal: controller.signal });
62
91
  const publishWhenOpen = async () => {
63
92
  await waitForMediaSourceOpen(mediaSource, controller.signal);
64
93
  if (controller.signal.aborted) return;
@@ -69,11 +98,7 @@ function setupMediaSourceSetup({ state, context }) {
69
98
  context.mediaSource.set(mediaSource);
70
99
  };
71
100
  publishWhenOpen().catch((err) => console.error("[setupMediaSource] failed to publish MediaSource:", err));
72
- return () => {
73
- controller.abort();
74
- detach();
75
- context.mediaSource.set(void 0);
76
- };
101
+ return teardown;
77
102
  } }
78
103
  }
79
104
  });
@@ -1 +1 @@
1
- {"version":3,"file":"setup-mediasource.js","names":[],"sources":["../../../../../src/playback/behaviors/dom/setup-mediasource.ts"],"sourcesContent":["/**\n * **Own the MediaSource lifecycle for the current source.** When a resolved\n * presentation and a mediaElement are both in scope, creates a MediaSource,\n * attaches it to the element, waits for `'open'`, and publishes it on\n * `context.mediaSource`. On source change or behavior destroy, detaches the\n * MediaSource and clears the slot so the next source starts fresh.\n *\n * Single-positive-state reactor (`'preconditions-unmet'` ↔ `'mediasource-attached'`):\n * state derivation gates on `mediaElement + isResolvedPresentation`. Riding the\n * resolver's resolved/unresolved lifecycle makes direct URL replacement\n * structural — `resolvePresentation` routes the presentation back through\n * unresolved on URL change, which drives this reactor through\n * `'preconditions-unmet'` so the entry's state-exit cleanup detaches the old\n * MediaSource before the new one is built.\n *\n * The entry resolves preconditions in sequence before publishing:\n *\n * 1. **Create + attach** — `createMediaSource` + `attachMediaSource` run\n * synchronously on entry. The `detach` closure returned by\n * `attachMediaSource` is captured for state-exit cleanup, so the cleanup\n * is always bound to its setup even if the wait below is aborted.\n * 2. **Wait for `'open'`** — `waitForMediaSourceOpen` defers until the first\n * `sourceopen` event (or any readyState transition out of `'closed'`).\n * 3. **Publish on `'open'`** — re-check `readyState === 'open'` after the\n * await (covers `'ended'` / `'closed'` race) before writing to\n * `context.mediaSource`. Downstream `setupVideoBufferActors` /\n * `setupAudioBufferActors` call `addSourceBuffer` directly, which\n * throws on non-open, so publish-only-when-open is the load-bearing\n * contract.\n *\n * State-exit cleanup aborts the in-flight wait, detaches the MediaSource,\n * and clears `context.mediaSource`. Order: abort first (prevents a late\n * publish racing the slot clear), then detach, then clear.\n *\n * Sole writer of `context.mediaSource`; other MSE behaviors\n * (`setupVideoBufferActors`, `setupAudioBufferActors`,\n * `updateMediaSourceDuration`, `endOfStream`, `loadVideoSegments`) only\n * read.\n */\nimport { defineBehavior } from '../../../core/composition/create-composition';\nimport type { Reactor } from '../../../core/reactors/create-machine-reactor';\nimport { createMachineReactor } from '../../../core/reactors/create-machine-reactor';\nimport { computed, type ReadonlySignal, type Signal } from '../../../core/signals/primitives';\nimport { attachMediaSource, createMediaSource, waitForMediaSourceOpen } from '../../../media/dom/mse/mediasource-setup';\nimport { isResolvedPresentation, type MaybeResolvedPresentation } from '../../../media/types';\n\n/**\n * State shape required for MediaSource setup.\n */\nexport interface MediaSourceState {\n presentation?: MaybeResolvedPresentation;\n}\n\n/**\n * Context shape for MediaSource setup.\n */\nexport interface MediaSourceContext {\n mediaElement?: HTMLMediaElement | undefined;\n mediaSource?: MediaSource;\n}\n\ntype MediaSourceFsmState = 'preconditions-unmet' | 'mediasource-attached';\n\nfunction deriveState(\n presentation: MaybeResolvedPresentation | undefined,\n mediaElement: HTMLMediaElement | undefined\n): MediaSourceFsmState {\n if (!mediaElement || !isResolvedPresentation(presentation)) return 'preconditions-unmet';\n return 'mediasource-attached';\n}\n\nfunction setupMediaSourceSetup({\n state,\n context,\n}: {\n state: {\n presentation: ReadonlySignal<MediaSourceState['presentation']>;\n };\n context: {\n mediaElement: ReadonlySignal<MediaSourceContext['mediaElement']>;\n mediaSource: Signal<MediaSourceContext['mediaSource']>;\n };\n}): Reactor<MediaSourceFsmState | 'destroying' | 'destroyed'> {\n const derivedStateSignal = computed(() => deriveState(state.presentation.get(), context.mediaElement.get()));\n\n return createMachineReactor<MediaSourceFsmState>({\n initial: 'preconditions-unmet',\n monitor: () => derivedStateSignal.get(),\n states: {\n 'preconditions-unmet': {},\n\n 'mediasource-attached': {\n // entry body is auto-untracked. deriveState handles source resets via\n // the resolver's resolved/unresolved transitions; this entry creates\n // and attaches the MediaSource, awaits `'open'`, publishes on\n // context, and binds detach + slot clear to state exit + destroy.\n entry: () => {\n const mediaElement = context.mediaElement.get()!;\n const controller = new AbortController();\n\n const mediaSource = createMediaSource({ preferManaged: true });\n // Sync attach: the returned `detach` closes over the element +\n // object-URL captured at this moment, so state-exit cleanup tears\n // down exactly this attachment regardless of how the wait below\n // resolves.\n const { detach } = attachMediaSource(mediaSource, mediaElement);\n\n const publishWhenOpen = async () => {\n await waitForMediaSourceOpen(mediaSource, controller.signal);\n if (controller.signal.aborted) return;\n // `waitForMediaSourceOpen` resolves on any readyState transition\n // out of `'closed'`; if we landed in `'ended'` / `'closed'`\n // instead of `'open'`, the attach window is gone and we leave\n // the slot unpublished. The session is dead within this source\n // either way — publishing wouldn't help because\n // `setupVideoBufferActors` / `setupAudioBufferActors` calls\n // `addSourceBuffer` which throws on non-open. The next\n // source-reset re-enters this state with a fresh MediaSource\n // and recovers. Warn so the case is at least diagnosable; in\n // practice it requires the MediaSource to close/end before its\n // very first `sourceopen`, which a freshly attached MS\n // shouldn't do.\n if (mediaSource.readyState !== 'open') {\n console.warn(\n `[setupMediaSource] MediaSource transitioned to '${mediaSource.readyState}' before first 'sourceopen' — slot left unpublished; recoverable on next source reset.`\n );\n return;\n }\n context.mediaSource.set(mediaSource);\n };\n\n publishWhenOpen().catch((err) => console.error('[setupMediaSource] failed to publish MediaSource:', err));\n\n return () => {\n // Order matters: abort the wait first so a late publish can't\n // race the slot clear; then detach to release the element; then\n // clear the slot so downstream behaviors see no MediaSource.\n controller.abort();\n detach();\n context.mediaSource.set(undefined);\n };\n },\n },\n },\n });\n}\n\nexport const setupMediaSource = defineBehavior({\n stateKeys: ['presentation'],\n contextKeys: ['mediaElement', 'mediaSource'],\n setup: setupMediaSourceSetup,\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+DA,SAAS,YACP,cACA,cACqB;AACrB,KAAI,CAAC,gBAAgB,CAAC,uBAAuB,aAAa,CAAE,QAAO;AACnE,QAAO;;AAGT,SAAS,sBAAsB,EAC7B,OACA,WAS4D;CAC5D,MAAM,qBAAqB,eAAe,YAAY,MAAM,aAAa,KAAK,EAAE,QAAQ,aAAa,KAAK,CAAC,CAAC;AAE5G,QAAO,qBAA0C;EAC/C,SAAS;EACT,eAAe,mBAAmB,KAAK;EACvC,QAAQ;GACN,uBAAuB,EAAE;GAEzB,wBAAwB,EAKtB,aAAa;IACX,MAAM,eAAe,QAAQ,aAAa,KAAK;IAC/C,MAAM,aAAa,IAAI,iBAAiB;IAExC,MAAM,cAAc,kBAAkB,EAAE,eAAe,MAAM,CAAC;IAK9D,MAAM,EAAE,WAAW,kBAAkB,aAAa,aAAa;IAE/D,MAAM,kBAAkB,YAAY;AAClC,WAAM,uBAAuB,aAAa,WAAW,OAAO;AAC5D,SAAI,WAAW,OAAO,QAAS;AAa/B,SAAI,YAAY,eAAe,QAAQ;AACrC,cAAQ,KACN,mDAAmD,YAAY,WAAW,wFAC3E;AACD;;AAEF,aAAQ,YAAY,IAAI,YAAY;;AAGtC,qBAAiB,CAAC,OAAO,QAAQ,QAAQ,MAAM,qDAAqD,IAAI,CAAC;AAEzG,iBAAa;AAIX,gBAAW,OAAO;AAClB,aAAQ;AACR,aAAQ,YAAY,IAAI,KAAA,EAAU;;MAGvC;GACF;EACF,CAAC;;AAGJ,MAAa,mBAAmB,eAAe;CAC7C,WAAW,CAAC,eAAe;CAC3B,aAAa,CAAC,gBAAgB,cAAc;CAC5C,OAAO;CACR,CAAC"}
1
+ {"version":3,"file":"setup-mediasource.js","names":[],"sources":["../../../../../src/playback/behaviors/dom/setup-mediasource.ts"],"sourcesContent":["/**\n * **Own the MediaSource lifecycle for the current source.** When a resolved\n * presentation and a mediaElement are both in scope, creates a MediaSource,\n * attaches it to the element, waits for `'open'`, and publishes it on\n * `context.mediaSource`. On source change or behavior destroy, detaches the\n * MediaSource and clears the slot so the next source starts fresh.\n *\n * Single-positive-state reactor (`'preconditions-unmet'` ↔ `'mediasource-attached'`):\n * state derivation gates on `mediaElement + isResolvedPresentation`. Riding the\n * resolver's resolved/unresolved lifecycle makes direct URL replacement\n * structural — `resolvePresentation` routes the presentation back through\n * unresolved on URL change, which drives this reactor through\n * `'preconditions-unmet'` so the entry's state-exit cleanup detaches the old\n * MediaSource before the new one is built.\n *\n * The entry resolves preconditions in sequence before publishing:\n *\n * 1. **Create + attach** — `createMediaSource` + `attachMediaSource` run\n * synchronously on entry. The `detach` closure returned by\n * `attachMediaSource` is captured for state-exit cleanup, so the cleanup\n * is always bound to its setup even if the wait below is aborted.\n * 2. **Wait for `'open'`** — `waitForMediaSourceOpen` defers until the first\n * `sourceopen` event (or any readyState transition out of `'closed'`).\n * 3. **Publish on `'open'`** — re-check `readyState === 'open'` after the\n * await (covers `'ended'` / `'closed'` race) before writing to\n * `context.mediaSource`. Downstream `setupVideoBufferActors` /\n * `setupAudioBufferActors` call `addSourceBuffer` directly, which\n * throws on non-open, so publish-only-when-open is the load-bearing\n * contract.\n *\n * State-exit cleanup aborts the in-flight wait, detaches the MediaSource,\n * and clears `context.mediaSource`. Order: abort first (prevents a late\n * publish racing the slot clear), then detach, then clear.\n *\n * # Sourceclose recovery\n *\n * The behavior owns one **unclosed** MediaSource per source identity. The UA can\n * close the attached MediaSource out from under the engine (Safari on an\n * AirPlay handoff — see `setupAirPlay` — or a ManagedMediaSource evicted\n * under memory pressure), and a closed MediaSource can never reopen. The\n * `sourceclose` listener tears the attachment down synchronously; every\n * teardown records a local close-fact, which holds the machine out until\n * the fact is consumed — then the re-derive comes back in with a fresh\n * MediaSource for the *same* source. Cause-agnostic. Consumption honors an\n * observed `loadingSuspended` (attaching runs `element.load()` — new loading\n * work, e.g. resource selection under an active AirPlay receiver), and\n * happens only while the machine is out, so a suspension can never tear\n * down an existing attachment.\n *\n * Sole writer of `context.mediaSource`; other MSE behaviors\n * (`setupVideoBufferActors`, `setupAudioBufferActors`,\n * `updateMediaSourceDuration`, `endOfStream`, `loadVideoSegments`) only\n * read.\n */\nimport { listen } from '@videojs/utils/dom';\nimport { defineBehavior } from '../../../core/composition/create-composition';\nimport type { Reactor } from '../../../core/reactors/create-machine-reactor';\nimport { createMachineReactor } from '../../../core/reactors/create-machine-reactor';\nimport { computed, type ReadonlySignal, type Signal, signal } from '../../../core/signals/primitives';\nimport { attachMediaSource, createMediaSource, waitForMediaSourceOpen } from '../../../media/dom/mse/mediasource-setup';\nimport { isResolvedPresentation, type MaybeResolvedPresentation } from '../../../media/types';\n\n/**\n * State shape required for MediaSource setup.\n */\nexport interface MediaSourceState {\n presentation?: MaybeResolvedPresentation;\n}\n\n/**\n * Context shape for MediaSource setup.\n */\nexport interface MediaSourceContext {\n mediaElement?: HTMLMediaElement | undefined;\n mediaSource?: MediaSource;\n}\n\n/**\n * Config for MediaSource setup.\n */\nexport interface MediaSourceSetupConfig {\n /**\n * Attach strategy — a composition-supplied implementation. Defaults to\n * `attachMediaSource` (object URL on the `src` attribute). Compositions\n * that pair MSE with sibling `<source>` alternatives wire\n * `attachMediaSourceAsSourceElement` (e.g. the HLS engine, for\n * `setupAirPlay`'s native fallback source).\n */\n attachMediaSource?: typeof attachMediaSource;\n}\n\ntype MediaSourceFsmState = 'preconditions-unmet' | 'mediasource-attached';\n\nfunction deriveState(\n presentation: MaybeResolvedPresentation | undefined,\n mediaElement: HTMLMediaElement | undefined,\n mediaSourceClosed: boolean\n): MediaSourceFsmState {\n if (!mediaElement || !isResolvedPresentation(presentation)) return 'preconditions-unmet';\n // The current attachment was torn down (typically a UA-fired\n // `sourceclose`). The fact stands until the `'preconditions-unmet'`\n // effects consume it (see below); the reset then re-derives into a fresh\n // attach for the same source.\n if (mediaSourceClosed) return 'preconditions-unmet';\n return 'mediasource-attached';\n}\n\nfunction setupMediaSourceSetup({\n state,\n context,\n config = {},\n}: {\n state: {\n presentation: ReadonlySignal<MediaSourceState['presentation']>;\n };\n context: {\n mediaElement: ReadonlySignal<MediaSourceContext['mediaElement']>;\n mediaSource: Signal<MediaSourceContext['mediaSource']>;\n };\n config?: MediaSourceSetupConfig;\n}): Reactor<MediaSourceFsmState | 'destroying' | 'destroyed'> {\n const attach = config.attachMediaSource ?? attachMediaSource;\n // `loadingSuspended` is observed, never declared: the slot exists only in\n // compositions where a feature behavior (e.g. `setupAirPlay`) declares and\n // writes it, so it lives behind a cast rather than in the typed param\n // above — an optional param key would force declaring it in `stateKeys`,\n // which is what materializes a slot. Shape redefined locally (canonical:\n // `SegmentLoadingState['loadingSuspended']`) to avoid a load-segments\n // module dependency.\n const loadingSuspended = (state as { loadingSuspended?: ReadonlySignal<boolean | undefined> }).loadingSuspended;\n\n // Close-fact for the currently-owned MediaSource, flipped by the\n // entry's shared teardown. Consumed in `'preconditions-unmet'`.\n const mediaSourceClosed = signal(false);\n\n const derivedStateSignal = computed(() =>\n deriveState(state.presentation.get(), context.mediaElement.get(), mediaSourceClosed.get())\n );\n\n return createMachineReactor<MediaSourceFsmState>({\n initial: 'preconditions-unmet',\n monitor: () => derivedStateSignal.get(),\n states: {\n 'preconditions-unmet': {\n // Consume a recorded close once new loading work may initiate — the\n // reset re-derives into `'mediasource-attached'`, rebuilding a fresh\n // MediaSource for the same source. While an observed\n // `loadingSuspended` holds, the fact stands and the rebuild waits:\n // attaching runs `element.load()`, which is exactly the loading work\n // the suspension forbids. Consuming only from this state means a\n // suspension can never tear down an existing attachment — there is\n // no code path from `loadingSuspended` to an exit.\n effects: () => {\n // `mediaSourceClosed` is tracked, not peeked: a new state's effects\n // can run BEFORE the old state's exit cleanup (effects run in\n // states-declaration order), so a teardown's fact-write can land\n // after this effect's entry run — tracking re-fires the consume.\n // Not suspended = slot absent (no writer composed) or value falsy.\n if (mediaSourceClosed.get() && !loadingSuspended?.get()) mediaSourceClosed.set(false);\n },\n },\n\n 'mediasource-attached': {\n // entry body is auto-untracked. deriveState handles source resets via\n // the resolver's resolved/unresolved transitions; this entry creates\n // and attaches the MediaSource, awaits `'open'`, publishes on\n // context, and binds detach + slot clear to state exit + destroy.\n entry: () => {\n const mediaElement = context.mediaElement.get()!;\n const controller = new AbortController();\n\n const mediaSource = createMediaSource({ preferManaged: true });\n // Sync attach: the returned `detach` closes over the element +\n // object-URL captured at this moment, so state-exit cleanup tears\n // down exactly this attachment regardless of how the wait below\n // resolves.\n const { detach } = attach(mediaSource, mediaElement);\n\n // One complete teardown, shared by both triggers (the sourceclose\n // listener below and the state-exit cleanup) — whichever fires\n // first does the work; a re-run is harmless. Order matters:\n //\n // 1. Abort first — kills the open-wait and the listener, so a late\n // publish can't race the slot clear.\n // 2. Clear the slot, which stops the downstream MSE pipeline and\n // dirties the effects of behaviors owning sibling `<source>`\n // children (`setupAirPlay`'s native-HLS fallback), queueing their\n // removal pass. Kept ahead of the detach so that queueing is\n // explicit: the `mediaSourceClosed` write above happens to queue\n // the same pass today, but depending on it would couple this to\n // an unrelated write.\n // 3. Detach with `deferReset`, which queues detach's `load()` reset\n // behind that pass. A synchronous reset would run resource\n // selection while the fallback is still in the DOM and commit the\n // element to the outgoing manifest — starting native HLS playback\n // of the source being torn down. Removing our own `<source>`\n // doesn't re-run selection on its own, so nothing starts in the\n // gap. The ownership guard re-checks at fire time, so a re-attach\n // landing in between suppresses the reset.\n const teardown = () => {\n mediaSourceClosed.set(true);\n controller.abort();\n context.mediaSource.set(undefined);\n detach({ deferReset: true });\n };\n\n // Sourceclose recovery: the UA closing this MediaSource (AirPlay\n // handoff, MMS eviction) tears the attachment down synchronously —\n // detach skips\n // its `load()` reset for a closed MediaSource (see\n // `attachMediaSource`), leaving the element as the session or\n // eviction left it.\n listen(mediaSource, 'sourceclose', teardown, { signal: controller.signal });\n\n const publishWhenOpen = async () => {\n await waitForMediaSourceOpen(mediaSource, controller.signal);\n if (controller.signal.aborted) return;\n // `waitForMediaSourceOpen` resolves on any readyState transition\n // out of `'closed'`; if we landed in `'ended'` / `'closed'`\n // instead of `'open'`, the attach window is gone and we leave\n // the slot unpublished. The session is dead within this source\n // either way — publishing wouldn't help because\n // `setupVideoBufferActors` / `setupAudioBufferActors` calls\n // `addSourceBuffer` which throws on non-open. The next\n // source-reset re-enters this state with a fresh MediaSource\n // and recovers. Warn so the case is at least diagnosable; in\n // practice it requires the MediaSource to close/end before its\n // very first `sourceopen`, which a freshly attached MS\n // shouldn't do.\n if (mediaSource.readyState !== 'open') {\n console.warn(\n `[setupMediaSource] MediaSource transitioned to '${mediaSource.readyState}' before first 'sourceopen' — slot left unpublished; recoverable on next source reset.`\n );\n return;\n }\n context.mediaSource.set(mediaSource);\n };\n\n publishWhenOpen().catch((err) => console.error('[setupMediaSource] failed to publish MediaSource:', err));\n\n // State-exit cleanup — fires on source unload, element detach, or\n // behavior destroy; a harmless re-run after a `sourceclose`\n // already tore down.\n return teardown;\n },\n },\n },\n });\n}\n\nexport const setupMediaSource = defineBehavior({\n stateKeys: ['presentation'],\n contextKeys: ['mediaElement', 'mediaSource'],\n setup: setupMediaSourceSetup,\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6FA,SAAS,YACP,cACA,cACA,mBACqB;CACrB,IAAI,CAAC,gBAAgB,CAAC,uBAAuB,YAAY,GAAG,OAAO;CAKnE,IAAI,mBAAmB,OAAO;CAC9B,OAAO;AACT;AAEA,SAAS,sBAAsB,EAC7B,OACA,SACA,SAAS,CAAC,KAUkD;CAC5D,MAAM,SAAS,OAAO,qBAAqB;CAQ3C,MAAM,mBAAoB,MAAqE;CAI/F,MAAM,oBAAoB,OAAO,KAAK;CAEtC,MAAM,qBAAqB,eACzB,YAAY,MAAM,aAAa,IAAI,GAAG,QAAQ,aAAa,IAAI,GAAG,kBAAkB,IAAI,CAAC,CAC3F;CAEA,OAAO,qBAA0C;EAC/C,SAAS;EACT,eAAe,mBAAmB,IAAI;EACtC,QAAQ;GACN,uBAAuB,EASrB,eAAe;IAMb,IAAI,kBAAkB,IAAI,KAAK,CAAC,kBAAkB,IAAI,GAAG,kBAAkB,IAAI,KAAK;GACtF,EACF;GAEA,wBAAwB,EAKtB,aAAa;IACX,MAAM,eAAe,QAAQ,aAAa,IAAI;IAC9C,MAAM,aAAa,IAAI,gBAAgB;IAEvC,MAAM,cAAc,kBAAkB,EAAE,eAAe,KAAK,CAAC;IAK7D,MAAM,EAAE,WAAW,OAAO,aAAa,YAAY;IAuBnD,MAAM,iBAAiB;KACrB,kBAAkB,IAAI,IAAI;KAC1B,WAAW,MAAM;KACjB,QAAQ,YAAY,IAAI,KAAA,CAAS;KACjC,OAAO,EAAE,YAAY,KAAK,CAAC;IAC7B;IAQA,OAAO,aAAa,eAAe,UAAU,EAAE,QAAQ,WAAW,OAAO,CAAC;IAE1E,MAAM,kBAAkB,YAAY;KAClC,MAAM,uBAAuB,aAAa,WAAW,MAAM;KAC3D,IAAI,WAAW,OAAO,SAAS;KAa/B,IAAI,YAAY,eAAe,QAAQ;MACrC,QAAQ,KACN,mDAAmD,YAAY,WAAW,uFAC5E;MACA;KACF;KACA,QAAQ,YAAY,IAAI,WAAW;IACrC;IAEA,gBAAgB,CAAC,CAAC,OAAO,QAAQ,QAAQ,MAAM,qDAAqD,GAAG,CAAC;IAKxG,OAAO;GACT,EACF;EACF;CACF,CAAC;AACH;AAEA,MAAa,mBAAmB,eAAe;CAC7C,WAAW,CAAC,cAAc;CAC1B,aAAa,CAAC,gBAAgB,aAAa;CAC3C,OAAO;AACT,CAAC"}
@@ -1,34 +1,20 @@
1
- import { defineBehavior } from "../../../core/composition/create-composition.js";
2
1
  import { effect } from "../../../core/signals/effect.js";
3
2
  import { createTextTracksActor } from "../../actors/dom/text-tracks.js";
4
3
  import { createTextTrackSegmentLoaderActor } from "../../actors/text-track-segment-loader.js";
5
4
  //#region src/playback/behaviors/dom/setup-text-track-actors.ts
6
- /**
7
- * **Own the TextTracks actor pair for the current `mediaElement`.** When a
8
- * `mediaElement` is in scope, creates the `TextTracksActor` (bound to the
9
- * element's `textTracks`) and the `TextTrackSegmentLoaderActor` (bound to
10
- * that actor + the injected cue resolver), and publishes both on
11
- * `context`. On element identity change or behavior destroy, destroys both
12
- * actors and clears the slots.
13
- *
14
- * Single-resource synchronous create/destroy driven by one signal — the
15
- * simple-effect form is the right shape (per `behaviors.md` → "Where both
16
- * shapes are legitimate": criterion 4b applies; sole writer of
17
- * `textTracksActor` / `textTrackSegmentLoaderActor`, the effect's cleanup
18
- * return handles destroy + slot clear structurally).
19
- *
20
- * Pairs with the `loadTextTrackSegments` behavior (a per-type variant of
21
- * `setupSegmentLoading` in `load-segments.ts`), which only reads
22
- * `textTrackSegmentLoaderActor`. The cue resolver is injected via
23
- * `config` so this behavior owns the DOM-bound part of the text-track
24
- * pipeline.
25
- */
26
- function setupTextTrackActorsSetup({ context, config }) {
5
+ function setupTextTrackActorsSetup({ state, context, config }) {
27
6
  return effect(() => {
28
7
  const mediaElement = context.mediaElement.get();
29
8
  if (!mediaElement) return;
30
9
  const textTracksActor = createTextTracksActor(mediaElement);
31
- const textTrackSegmentLoaderActor = createTextTrackSegmentLoaderActor(textTracksActor, config.resolveTextTrackSegment, { forwardBuffer: config.forwardBuffer });
10
+ const textTrackSegmentLoaderActor = createTextTrackSegmentLoaderActor(textTracksActor, config.resolveTextTrackSegment, {
11
+ forwardBuffer: config.forwardBuffer,
12
+ messagePipelines: config.textMessagePipelines
13
+ }, {
14
+ state,
15
+ context,
16
+ config
17
+ });
32
18
  context.textTracksActor.set(textTracksActor);
33
19
  context.textTrackSegmentLoaderActor.set(textTrackSegmentLoaderActor);
34
20
  return () => {
@@ -39,7 +25,7 @@ function setupTextTrackActorsSetup({ context, config }) {
39
25
  };
40
26
  });
41
27
  }
42
- const setupTextTrackActors = defineBehavior({
28
+ const setupTextTrackActors = {
43
29
  stateKeys: [],
44
30
  contextKeys: [
45
31
  "mediaElement",
@@ -47,7 +33,7 @@ const setupTextTrackActors = defineBehavior({
47
33
  "textTrackSegmentLoaderActor"
48
34
  ],
49
35
  setup: setupTextTrackActorsSetup
50
- });
36
+ };
51
37
  //#endregion
52
38
  export { setupTextTrackActors };
53
39
 
@@ -1 +1 @@
1
- {"version":3,"file":"setup-text-track-actors.js","names":[],"sources":["../../../../../src/playback/behaviors/dom/setup-text-track-actors.ts"],"sourcesContent":["/**\n * **Own the TextTracks actor pair for the current `mediaElement`.** When a\n * `mediaElement` is in scope, creates the `TextTracksActor` (bound to the\n * element's `textTracks`) and the `TextTrackSegmentLoaderActor` (bound to\n * that actor + the injected cue resolver), and publishes both on\n * `context`. On element identity change or behavior destroy, destroys both\n * actors and clears the slots.\n *\n * Single-resource synchronous create/destroy driven by one signal — the\n * simple-effect form is the right shape (per `behaviors.md` → \"Where both\n * shapes are legitimate\": criterion 4b applies; sole writer of\n * `textTracksActor` / `textTrackSegmentLoaderActor`, the effect's cleanup\n * return handles destroy + slot clear structurally).\n *\n * Pairs with the `loadTextTrackSegments` behavior (a per-type variant of\n * `setupSegmentLoading` in `load-segments.ts`), which only reads\n * `textTrackSegmentLoaderActor`. The cue resolver is injected via\n * `config` so this behavior owns the DOM-bound part of the text-track\n * pipeline.\n */\nimport { defineBehavior } from '../../../core/composition/create-composition';\nimport { effect } from '../../../core/signals/effect';\nimport type { ReadonlySignal, Signal } from '../../../core/signals/primitives';\nimport { createTextTracksActor } from '../../actors/dom/text-tracks';\nimport {\n createTextTrackSegmentLoaderActor,\n type TextTrackSegmentLoaderActor,\n type TextTrackSegmentLoaderActorConfig,\n type TextTrackSegmentResolver,\n} from '../../actors/text-track-segment-loader';\nimport type { TextTracksActor } from '../../actors/text-tracks';\n\nexport interface TextTrackActorsContext {\n mediaElement?: HTMLMediaElement | undefined;\n textTracksActor?: TextTracksActor<VTTCue> | undefined;\n textTrackSegmentLoaderActor?: TextTrackSegmentLoaderActor | undefined;\n}\n\nexport interface TextTrackActorsConfig extends TextTrackSegmentLoaderActorConfig {\n resolveTextTrackSegment: TextTrackSegmentResolver<VTTCue>;\n}\n\nfunction setupTextTrackActorsSetup({\n context,\n config,\n}: {\n context: {\n mediaElement: ReadonlySignal<TextTrackActorsContext['mediaElement']>;\n textTracksActor: Signal<TextTrackActorsContext['textTracksActor']>;\n textTrackSegmentLoaderActor: Signal<TextTrackActorsContext['textTrackSegmentLoaderActor']>;\n };\n config: TextTrackActorsConfig;\n}): () => void {\n return effect(() => {\n const mediaElement = context.mediaElement.get();\n if (!mediaElement) return;\n\n const textTracksActor = createTextTracksActor(mediaElement);\n const textTrackSegmentLoaderActor = createTextTrackSegmentLoaderActor(\n textTracksActor,\n config.resolveTextTrackSegment,\n { forwardBuffer: config.forwardBuffer }\n );\n context.textTracksActor.set(textTracksActor);\n context.textTrackSegmentLoaderActor.set(textTrackSegmentLoaderActor);\n\n return () => {\n textTracksActor.destroy();\n textTrackSegmentLoaderActor.destroy();\n context.textTracksActor.set(undefined);\n context.textTrackSegmentLoaderActor.set(undefined);\n };\n });\n}\n\nexport const setupTextTrackActors = defineBehavior({\n stateKeys: [],\n contextKeys: ['mediaElement', 'textTracksActor', 'textTrackSegmentLoaderActor'],\n setup: setupTextTrackActorsSetup,\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AA0CA,SAAS,0BAA0B,EACjC,SACA,UAQa;AACb,QAAO,aAAa;EAClB,MAAM,eAAe,QAAQ,aAAa,KAAK;AAC/C,MAAI,CAAC,aAAc;EAEnB,MAAM,kBAAkB,sBAAsB,aAAa;EAC3D,MAAM,8BAA8B,kCAClC,iBACA,OAAO,yBACP,EAAE,eAAe,OAAO,eAAe,CACxC;AACD,UAAQ,gBAAgB,IAAI,gBAAgB;AAC5C,UAAQ,4BAA4B,IAAI,4BAA4B;AAEpE,eAAa;AACX,mBAAgB,SAAS;AACzB,+BAA4B,SAAS;AACrC,WAAQ,gBAAgB,IAAI,KAAA,EAAU;AACtC,WAAQ,4BAA4B,IAAI,KAAA,EAAU;;GAEpD;;AAGJ,MAAa,uBAAuB,eAAe;CACjD,WAAW,EAAE;CACb,aAAa;EAAC;EAAgB;EAAmB;EAA8B;CAC/E,OAAO;CACR,CAAC"}
1
+ {"version":3,"file":"setup-text-track-actors.js","names":[],"sources":["../../../../../src/playback/behaviors/dom/setup-text-track-actors.ts"],"sourcesContent":["/**\n * **Own the TextTracks actor pair for the current `mediaElement`.** When a\n * `mediaElement` is in scope, creates the `TextTracksActor` (bound to the\n * element's `textTracks`) and the `TextTrackSegmentLoaderActor` (bound to\n * that actor + the injected cue resolver), and publishes both on\n * `context`. On element identity change or behavior destroy, destroys both\n * actors and clears the slots.\n *\n * Single-resource synchronous create/destroy driven by one signal — the\n * simple-effect form is the right shape (per `behaviors.md` → \"Where both\n * shapes are legitimate\": criterion 4b applies; sole writer of\n * `textTracksActor` / `textTrackSegmentLoaderActor`, the effect's cleanup\n * return handles destroy + slot clear structurally).\n *\n * Pairs with the `loadTextTrackSegments` behavior (a per-type variant of\n * `setupSegmentLoading` in `load-segments.ts`), which only reads\n * `textTrackSegmentLoaderActor`. The cue resolver is injected via\n * `config` so this behavior owns the DOM-bound part of the text-track\n * pipeline.\n */\nimport type { AnySlotMap, Behavior } from '../../../core/composition/create-composition';\nimport { effect } from '../../../core/signals/effect';\nimport type { ReadonlySignal, Signal } from '../../../core/signals/primitives';\nimport { createTextTracksActor } from '../../actors/dom/text-tracks';\nimport {\n createTextTrackSegmentLoaderActor,\n type TextTrackSegmentLoaderActor,\n type TextTrackSegmentLoaderActorConfig,\n} from '../../actors/text-track-segment-loader';\nimport type { TextTracksActor } from '../../actors/text-tracks';\nimport type { TextMessagePipelines, TextTrackSegmentResolver } from '../../primitives/text-segment-load-pipeline';\n\nexport interface TextTrackActorsContext {\n mediaElement?: HTMLMediaElement | undefined;\n textTracksActor?: TextTracksActor<VTTCue> | undefined;\n textTrackSegmentLoaderActor?: TextTrackSegmentLoaderActor | undefined;\n}\n\nexport interface TextTrackActorsConfig extends Pick<TextTrackSegmentLoaderActorConfig<VTTCue>, 'forwardBuffer'> {\n resolveTextTrackSegment: TextTrackSegmentResolver<VTTCue>;\n /**\n * Ordered text step pipeline, mapped to the loader's `messagePipelines`. Named\n * with the `text` domain prefix to mirror the v/a `video`/`audioMessagePipelines`\n * composition-config slots. Defaults (in the loader) to `resolveCues → dispatchCues`.\n */\n textMessagePipelines?: TextMessagePipelines<VTTCue>;\n}\n\nfunction setupTextTrackActorsSetup({\n state,\n context,\n config,\n}: {\n // Forwarded opaquely into the loader's steps (relocation reads composition state);\n // this behavior owns no state of its own (stateKeys: []).\n state: AnySlotMap;\n context: {\n mediaElement: ReadonlySignal<TextTrackActorsContext['mediaElement']>;\n textTracksActor: Signal<TextTrackActorsContext['textTracksActor']>;\n textTrackSegmentLoaderActor: Signal<TextTrackActorsContext['textTrackSegmentLoaderActor']>;\n };\n config: TextTrackActorsConfig;\n}): () => void {\n return effect(() => {\n const mediaElement = context.mediaElement.get();\n if (!mediaElement) return;\n\n const textTracksActor = createTextTracksActor(mediaElement);\n const textTrackSegmentLoaderActor = createTextTrackSegmentLoaderActor(\n textTracksActor,\n config.resolveTextTrackSegment,\n { forwardBuffer: config.forwardBuffer, messagePipelines: config.textMessagePipelines },\n // Composition deps forwarded into each step (relocation reads the primary A/V origin).\n { state, context, config }\n );\n context.textTracksActor.set(textTracksActor);\n context.textTrackSegmentLoaderActor.set(textTrackSegmentLoaderActor);\n\n return () => {\n textTracksActor.destroy();\n textTrackSegmentLoaderActor.destroy();\n context.textTracksActor.set(undefined);\n context.textTrackSegmentLoaderActor.set(undefined);\n };\n });\n}\n\n// Manual `Behavior` literal (like `end-of-stream`): no state of its own\n// (`stateKeys: []`), but the setup forwards the composition `state` to the resolver\n// opaquely. A literal (not `defineBehavior`) so `stateKeys: []` can coexist with a\n// setup that reads `state`.\nexport const setupTextTrackActors: Behavior<\n Record<never, never>,\n {\n mediaElement: ReadonlySignal<TextTrackActorsContext['mediaElement']>;\n textTracksActor: Signal<TextTrackActorsContext['textTracksActor']>;\n textTrackSegmentLoaderActor: Signal<TextTrackActorsContext['textTrackSegmentLoaderActor']>;\n },\n TextTrackActorsConfig\n> = {\n stateKeys: [],\n contextKeys: ['mediaElement', 'textTracksActor', 'textTrackSegmentLoaderActor'],\n setup: setupTextTrackActorsSetup,\n};\n"],"mappings":";;;;AAgDA,SAAS,0BAA0B,EACjC,OACA,SACA,UAWa;CACb,OAAO,aAAa;EAClB,MAAM,eAAe,QAAQ,aAAa,IAAI;EAC9C,IAAI,CAAC,cAAc;EAEnB,MAAM,kBAAkB,sBAAsB,YAAY;EAC1D,MAAM,8BAA8B,kCAClC,iBACA,OAAO,yBACP;GAAE,eAAe,OAAO;GAAe,kBAAkB,OAAO;EAAqB,GAErF;GAAE;GAAO;GAAS;EAAO,CAC3B;EACA,QAAQ,gBAAgB,IAAI,eAAe;EAC3C,QAAQ,4BAA4B,IAAI,2BAA2B;EAEnE,aAAa;GACX,gBAAgB,QAAQ;GACxB,4BAA4B,QAAQ;GACpC,QAAQ,gBAAgB,IAAI,KAAA,CAAS;GACrC,QAAQ,4BAA4B,IAAI,KAAA,CAAS;EACnD;CACF,CAAC;AACH;AAMA,MAAa,uBAQT;CACF,WAAW,CAAC;CACZ,aAAa;EAAC;EAAgB;EAAmB;CAA6B;CAC9E,OAAO;AACT"}
@@ -0,0 +1,26 @@
1
+ import { effect } from "../../../core/signals/effect.js";
2
+ import { liveWindowFromState } from "../../primitives/live-window.js";
3
+ //#region src/playback/behaviors/dom/sync-live-seekable-range.ts
4
+ function syncLiveSeekableRangeSetup({ state, context }) {
5
+ return effect(() => {
6
+ const mediaSource = context.mediaSource.get();
7
+ const liveWindow = liveWindowFromState(state);
8
+ if (!mediaSource || !liveWindow) return;
9
+ mediaSource.setLiveSeekableRange(liveWindow.start, liveWindow.end);
10
+ });
11
+ }
12
+ /**
13
+ * Manual `Behavior<>` literal (like `seekToLiveEdge`): declares only
14
+ * `presentation` in stateKeys while reading `selectedVideoTrackId` /
15
+ * `selectedAudioTrackId` defensively (contributed by the switch* behaviors), so
16
+ * it composes without a stateKeys/type conflict.
17
+ */
18
+ const syncLiveSeekableRange = {
19
+ stateKeys: ["presentation"],
20
+ contextKeys: ["mediaSource"],
21
+ setup: syncLiveSeekableRangeSetup
22
+ };
23
+ //#endregion
24
+ export { syncLiveSeekableRange };
25
+
26
+ //# sourceMappingURL=sync-live-seekable-range.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync-live-seekable-range.js","names":[],"sources":["../../../../../src/playback/behaviors/dom/sync-live-seekable-range.ts"],"sourcesContent":["/**\n * Mirror the live window into the MediaSource's seekable range. On every\n * window update — **including while paused** (the seekable range must stay\n * current as the window slides, regardless of play state) — declare\n * `setLiveSeekableRange(start, end)` so the browser's `HTMLMediaElement.seekable`\n * reflects the live window (without it, `seekable` is empty under\n * `duration === Infinity`).\n *\n * The live window comes from `liveWindowFromState` (the shared derivation —\n * the intersection over the selected A/V tracks' windows); inert when it\n * returns `null` (VoD / ended live). Composed *before* `seekToLiveEdge`\n * so the range is declared before that behavior seeks the playhead into it (a\n * seek outside `seekable` is clamped).\n *\n * Duration is owned solely by `updateMediaSourceDuration`; this behavior only\n * declares the seekable range (`setLiveSeekableRange` requires only\n * `readyState === 'open'` per the W3C MSE spec, not a set `duration`).\n *\n * No `clearLiveSeekableRange()` on termination, by design: the MSE spec consults\n * the live seekable range *only* while `duration === Infinity`. When a live\n * stream ends, `endOfStream()` sets a finite duration and the UA derives\n * `seekable` from buffered + duration, ignoring the live range — so clearing is\n * unnecessary. Clearing on the `ENDLIST`→finite-`Track.duration` transition\n * (when the window goes `null`) would also be premature: `duration` is still\n * `Infinity` until `endOfStream`, so clearing would shrink `seekable` to\n * buffered-only while the stream is still effectively live.\n */\nimport type { Behavior } from '../../../core/composition/create-composition';\nimport { effect } from '../../../core/signals/effect';\nimport type { ReadonlySignal } from '../../../core/signals/primitives';\nimport type { MaybeResolvedPresentation } from '../../../media/types';\nimport { liveWindowFromState } from '../../primitives/live-window';\n\nexport interface SyncLiveSeekableRangeState {\n presentation?: MaybeResolvedPresentation;\n selectedVideoTrackId?: string;\n selectedAudioTrackId?: string;\n}\n\nexport interface SyncLiveSeekableRangeContext {\n mediaSource?: MediaSource;\n}\n\nfunction syncLiveSeekableRangeSetup({\n state,\n context,\n}: {\n state: {\n presentation: ReadonlySignal<SyncLiveSeekableRangeState['presentation']>;\n selectedVideoTrackId?: ReadonlySignal<SyncLiveSeekableRangeState['selectedVideoTrackId']>;\n selectedAudioTrackId?: ReadonlySignal<SyncLiveSeekableRangeState['selectedAudioTrackId']>;\n };\n context: {\n mediaSource: ReadonlySignal<SyncLiveSeekableRangeContext['mediaSource']>;\n };\n}): () => void {\n return effect(() => {\n const mediaSource = context.mediaSource.get();\n const liveWindow = liveWindowFromState(state);\n if (!mediaSource || !liveWindow) return;\n\n // Re-declared as the window slides so seekable tracks the live window\n // (the full DVR range remains seekable; seek-to-live-edge starts near the edge).\n // No readyState check, no try/catch: `setLiveSeekableRange` throws only on a\n // non-'open' readyState or an invalid range, and neither can occur here —\n // `setupMediaSource` publishes `context.mediaSource` only while open, and a\n // non-null live window means the timeline-bearing track is still `Infinity`\n // (so `endOfStream` hasn't ended the MS); `liveWindowFromState` guarantees\n // 0 ≤ start < end (the reference type's window starts ≥ 0, and the A/V\n // intersection `max`-clamps a non-reference track's negative early start).\n mediaSource.setLiveSeekableRange(liveWindow.start, liveWindow.end);\n });\n}\n\n/**\n * Manual `Behavior<>` literal (like `seekToLiveEdge`): declares only\n * `presentation` in stateKeys while reading `selectedVideoTrackId` /\n * `selectedAudioTrackId` defensively (contributed by the switch* behaviors), so\n * it composes without a stateKeys/type conflict.\n */\nexport const syncLiveSeekableRange: Behavior<\n { presentation: ReadonlySignal<SyncLiveSeekableRangeState['presentation']> },\n { mediaSource: ReadonlySignal<SyncLiveSeekableRangeContext['mediaSource']> },\n object\n> = {\n stateKeys: ['presentation'],\n contextKeys: ['mediaSource'],\n setup: syncLiveSeekableRangeSetup,\n};\n"],"mappings":";;;AA2CA,SAAS,2BAA2B,EAClC,OACA,WAUa;CACb,OAAO,aAAa;EAClB,MAAM,cAAc,QAAQ,YAAY,IAAI;EAC5C,MAAM,aAAa,oBAAoB,KAAK;EAC5C,IAAI,CAAC,eAAe,CAAC,YAAY;EAWjC,YAAY,qBAAqB,WAAW,OAAO,WAAW,GAAG;CACnE,CAAC;AACH;;;;;;;AAQA,MAAa,wBAIT;CACF,WAAW,CAAC,cAAc;CAC1B,aAAa,CAAC,aAAa;CAC3B,OAAO;AACT"}
@@ -1 +1 @@
1
- {"version":3,"file":"sync-text-tracks.js","names":[],"sources":["../../../../../src/playback/behaviors/dom/sync-text-tracks.ts"],"sourcesContent":["/**\n * **Own the text-track slots on the host media element, mirroring the SPF\n * model.** When a presentation is resolved and a media element is\n * available, allocate one slot in `mediaElement.textTracks` per model text\n * track — via creating `<track>` children, since that's the only spec\n * mechanism for adding *and* removing entries to `textTracks` (no\n * `removeTextTrack` API exists). Once slots are provisioned, mirror the\n * resolved `selectedTextTrackId` into their `mode`s (one-way: state → DOM),\n * and propagate user-initiated DOM `change` events back to\n * `userTextTrackSelection` — the standing *intent* (a language-based partial,\n * or `'off'`) that `switchTextTrack` resolves into `selectedTextTrackId`. So\n * non-SPF consumers (host-page captions buttons, browser native UI, video.js\n * store) drive selection by expressing intent, not by writing the resolved id.\n *\n * Single-positive-state reactor (`'preconditions-unmet'` ↔ `'sync-active'`):\n * the entry allocates the slots, applies the initial selection, attaches\n * the `change` listener, and opens a brief Chromium settling-window guard —\n * all transition-driven, fire-once on state entry, with paired cleanup on\n * state exit. A single `effects:` mirrors subsequent\n * `selectedTextTrackId` changes into `mode`s; that's the only\n * continuous-reactivity concern.\n *\n * State-exit cleanup also sends a `'clear'` message to the\n * `TextTracksActor` so its cue+segment cache (keyed by trackId) is\n * dropped alongside the DOM `<track>` slots. The actor itself is owned\n * by `setupTextTrackActors` and bound to mediaElement, not presentation,\n * so it survives source resets; clearing its context here keeps the\n * cache consistent with the DOM. Without this, a subsequent\n * presentation reusing a trackId would have `getSegmentsToLoad` treat\n * its segments as already-buffered and skip loading them.\n *\n * Single-writer separation: `selectedTextTrackId` is the resolved *output*\n * owned solely by `switchTextTrack`; this behavior only reads it (to mirror\n * modes). The write path here is `userTextTrackSelection` — the user-intent\n * *input* — so DOM action and the resolver never contend for one slot. The\n * intent isn't cleared on source unload (it's a standing preference, like\n * `userAudioTrackSelection`); `'off'` is written when the user disables all\n * tracks via native UI.\n *\n * Echo guard: `selectedTextTrackId` is exactly the id this behavior last drove\n * into the DOM, so a `change` event still showing it is our own echo (or a\n * resolver-driven correction — e.g. the picked track's CDN failed and the\n * resolver disabled it) and is ignored, never written back as a spurious user\n * action. Only a showing id that *differs* from the resolved id is a real user\n * pick. The settling-window guard additionally swallows Chromium's init-time\n * auto-selection before the resolved selection has settled.\n */\n\nimport { listen } from '@videojs/utils/dom';\nimport { defineBehavior } from '../../../core/composition/create-composition';\nimport type { Reactor } from '../../../core/reactors/create-machine-reactor';\nimport { createMachineReactor } from '../../../core/reactors/create-machine-reactor';\nimport { computed, peek, type ReadonlySignal, type Signal } from '../../../core/signals/primitives';\nimport { syncTextTrackModes } from '../../../media/dom/text/text-track-slots';\nimport type { MaybeResolvedPresentation, PartiallyResolvedTextTrack, TextTrack } from '../../../media/types';\nimport { getTracksByType } from '../../../media/utils/tracks';\nimport type { TextTracksActor } from '../../actors/text-tracks';\n\ntype SyncTextTracksFsmState = 'preconditions-unmet' | 'sync-active';\n\nexport interface SyncTextTracksConfig {\n /**\n * Create and append SPF-owned `<track>` slots on `mediaElement`, one per\n * model text track. Implementation tags each element so the read/remove\n * helpers can scope to SPF-owned slots. **Required** — the behavior is\n * DOM-binding-neutral and the composing engine supplies the integration.\n */\n addSubtitlesTracksToMedia: (\n mediaElement: HTMLMediaElement,\n modelTextTracks: readonly (PartiallyResolvedTextTrack | TextTrack)[]\n ) => void;\n /**\n * Return the SPF-owned subtitle/caption `TextTrack` currently in `'showing'`\n * mode, or `undefined` if none. Used by the DOM `change` bridge to mirror\n * native-UI selection back into `selectedTextTrackId`.\n */\n getShowingSubtitlesTrackFromMedia: (mediaElement: HTMLMediaElement) => globalThis.TextTrack | undefined;\n /**\n * Remove every SPF-owned `<track>` child from `mediaElement`. Called on\n * state exit (source unload, behavior destroy) to evict slots.\n */\n removeAllSubtitlesTracksFromMedia: (mediaElement: HTMLMediaElement) => void;\n}\n\nfunction deriveState(\n presentation: MaybeResolvedPresentation | undefined,\n mediaElement: HTMLMediaElement | undefined\n): SyncTextTracksFsmState {\n if (!mediaElement || !presentation) return 'preconditions-unmet';\n return getTracksByType(presentation, 'text').length > 0 ? 'sync-active' : 'preconditions-unmet';\n}\n\n/**\n * Map the DOM-showing track back to standing user intent. No showing track is an\n * explicit `'off'`. Otherwise prefer a language-based partial (so the pick\n * persists across source changes); fall back to `{ id }` for a track without a\n * language (precise within a source, just not portable).\n */\nfunction deriveTextTrackIntent(\n showingId: string | undefined,\n modelTextTracks: readonly (PartiallyResolvedTextTrack | TextTrack)[]\n): Partial<TextTrack> | 'off' {\n if (!showingId) return 'off';\n const language = modelTextTracks.find((track) => track.id === showingId)?.language;\n return language ? { language } : { id: showingId };\n}\n\nfunction syncTextTracksSetup({\n state,\n context,\n config,\n}: {\n state: {\n presentation: ReadonlySignal<MaybeResolvedPresentation | undefined>;\n // Read-only here: the resolved output owned by `switchTextTrack`, mirrored\n // into DOM modes and used as the echo-guard reference.\n selectedTextTrackId: ReadonlySignal<string | undefined>;\n // The write path: standing user intent the DOM bridge feeds.\n userTextTrackSelection: Signal<Partial<TextTrack> | 'off' | undefined>;\n };\n context: {\n mediaElement: ReadonlySignal<HTMLMediaElement | undefined>;\n textTracksActor: ReadonlySignal<TextTracksActor<VTTCue> | undefined>;\n };\n config: SyncTextTracksConfig;\n}): Reactor<SyncTextTracksFsmState | 'destroying' | 'destroyed'> {\n const { addSubtitlesTracksToMedia, getShowingSubtitlesTrackFromMedia, removeAllSubtitlesTracksFromMedia } = config;\n\n const derivedStateSignal = computed(() => deriveState(state.presentation.get(), context.mediaElement.get()));\n\n return createMachineReactor<SyncTextTracksFsmState>({\n initial: 'preconditions-unmet',\n monitor: () => derivedStateSignal.get(),\n states: {\n 'preconditions-unmet': {},\n\n 'sync-active': {\n // Allocate slots, apply the initial selection, attach the change\n // listener, and open the settling window — all transition-driven\n // (fire once per state entry). Entry is auto-untracked, so `.get()`\n // here doesn't subscribe; the returned cleanup detaches and evicts\n // on state exit (src reset through 'preconditions-unmet') and on\n // destroy.\n entry: () => {\n const mediaElement = context.mediaElement.get()!;\n // `getTracksByType('text', ...)` returns text tracks only — the\n // selection-set filter inside the helper ensures that — but its\n // declared return is the wide track union. Mirror the cast\n // pattern used by `track-switching` for the video branch.\n const modelTextTracks = getTracksByType(state.presentation.get()!, 'text') as readonly (\n | PartiallyResolvedTextTrack\n | TextTrack\n )[];\n\n addSubtitlesTracksToMedia(mediaElement, modelTextTracks);\n // Apply our selection synchronously so the change-event tasks\n // these mode writes queue land in the macrotask queue *before*\n // the settling-close `setTimeout(0)` queued below. This keeps\n // the settling window open long enough to swallow Chromium's\n // own auto-selection task (also scheduled in the next tick after\n // `<track>` insertion); if the order flipped, our handler would\n // treat Chromium's auto-pick as a user action. The `effects:`\n // block below handles subsequent `selectedTextTrackId` changes.\n syncTextTrackModes(mediaElement.textTracks, state.selectedTextTrackId.get());\n\n // Chromium re-applies its own selection across the next task tick\n // after `<track>` insertion (default-track auto-pick, language\n // preference). During this window, the `change` event may fire\n // with browser-chosen modes that don't reflect a real user action;\n // re-apply our modes silently rather than writing them back.\n let inSettlingWindow = true;\n const settlingTimeout = setTimeout(() => {\n inSettlingWindow = false;\n }, 0);\n\n const onChange = (): void => {\n if (inSettlingWindow) {\n syncTextTrackModes(mediaElement.textTracks, state.selectedTextTrackId.get());\n return;\n }\n const showingTrack = getShowingSubtitlesTrackFromMedia(mediaElement);\n // `showingTrack.id` matches the SPF id we set when the slot was\n // allocated. Empty-string ids fall through to `undefined`.\n const showingId = showingTrack?.id || undefined;\n // Echo guard: selectedTextTrackId is the id we last drove into the\n // DOM (mirror / resolver correction). A change still showing it is our\n // own echo — ignore it rather than write spurious intent.\n if (showingId === state.selectedTextTrackId.get()) return;\n // Genuine user action → write intent (resolved into selectedTextTrackId\n // by switchTextTrack), not the resolved id.\n state.userTextTrackSelection.set(deriveTextTrackIntent(showingId, modelTextTracks));\n };\n\n const unlisten = listen(mediaElement.textTracks, 'change', onChange);\n\n return () => {\n unlisten();\n clearTimeout(settlingTimeout);\n removeAllSubtitlesTracksFromMedia(mediaElement);\n // Clear the TextTracksActor's cue+segment cache, which is\n // keyed by trackId. If we don't, a subsequent presentation\n // reusing a trackId would have `getSegmentsToLoad` treat its\n // segments as already-buffered. The DOM cleanup above\n // already evicted the live cues; this drops the cache that\n // tracked them. The actor itself is owned by\n // `setupTextTrackActors` (mediaElement-bound lifecycle), so\n // we send rather than destroy.\n peek(context.textTracksActor)?.send({ type: 'clear' });\n };\n },\n\n // Mirror selection changes into mode. The state machine handles\n // mediaElement/presentation changes via 'preconditions-unmet'\n // round-trips, so we peek mediaElement here.\n effects: () => {\n const mediaElement = peek(context.mediaElement)!;\n syncTextTrackModes(mediaElement.textTracks, state.selectedTextTrackId.get());\n },\n },\n },\n });\n}\n\nexport const syncTextTracks = defineBehavior({\n stateKeys: ['presentation', 'selectedTextTrackId', 'userTextTrackSelection'],\n contextKeys: ['mediaElement', 'textTracksActor'],\n setup: syncTextTracksSetup,\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoFA,SAAS,YACP,cACA,cACwB;AACxB,KAAI,CAAC,gBAAgB,CAAC,aAAc,QAAO;AAC3C,QAAO,gBAAgB,cAAc,OAAO,CAAC,SAAS,IAAI,gBAAgB;;;;;;;;AAS5E,SAAS,sBACP,WACA,iBAC4B;AAC5B,KAAI,CAAC,UAAW,QAAO;CACvB,MAAM,WAAW,gBAAgB,MAAM,UAAU,MAAM,OAAO,UAAU,EAAE;AAC1E,QAAO,WAAW,EAAE,UAAU,GAAG,EAAE,IAAI,WAAW;;AAGpD,SAAS,oBAAoB,EAC3B,OACA,SACA,UAe+D;CAC/D,MAAM,EAAE,2BAA2B,mCAAmC,sCAAsC;CAE5G,MAAM,qBAAqB,eAAe,YAAY,MAAM,aAAa,KAAK,EAAE,QAAQ,aAAa,KAAK,CAAC,CAAC;AAE5G,QAAO,qBAA6C;EAClD,SAAS;EACT,eAAe,mBAAmB,KAAK;EACvC,QAAQ;GACN,uBAAuB,EAAE;GAEzB,eAAe;IAOb,aAAa;KACX,MAAM,eAAe,QAAQ,aAAa,KAAK;KAK/C,MAAM,kBAAkB,gBAAgB,MAAM,aAAa,KAAK,EAAG,OAAO;AAK1E,+BAA0B,cAAc,gBAAgB;AASxD,wBAAmB,aAAa,YAAY,MAAM,oBAAoB,KAAK,CAAC;KAO5E,IAAI,mBAAmB;KACvB,MAAM,kBAAkB,iBAAiB;AACvC,yBAAmB;QAClB,EAAE;KAEL,MAAM,iBAAuB;AAC3B,UAAI,kBAAkB;AACpB,0BAAmB,aAAa,YAAY,MAAM,oBAAoB,KAAK,CAAC;AAC5E;;MAKF,MAAM,YAHe,kCAAkC,aAAa,EAGpC,MAAM,KAAA;AAItC,UAAI,cAAc,MAAM,oBAAoB,KAAK,CAAE;AAGnD,YAAM,uBAAuB,IAAI,sBAAsB,WAAW,gBAAgB,CAAC;;KAGrF,MAAM,WAAW,OAAO,aAAa,YAAY,UAAU,SAAS;AAEpE,kBAAa;AACX,gBAAU;AACV,mBAAa,gBAAgB;AAC7B,wCAAkC,aAAa;AAS/C,WAAK,QAAQ,gBAAgB,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;;;IAO1D,eAAe;AAEb,wBADqB,KAAK,QAAQ,aAAa,CACf,YAAY,MAAM,oBAAoB,KAAK,CAAC;;IAE/E;GACF;EACF,CAAC;;AAGJ,MAAa,iBAAiB,eAAe;CAC3C,WAAW;EAAC;EAAgB;EAAuB;EAAyB;CAC5E,aAAa,CAAC,gBAAgB,kBAAkB;CAChD,OAAO;CACR,CAAC"}
1
+ {"version":3,"file":"sync-text-tracks.js","names":[],"sources":["../../../../../src/playback/behaviors/dom/sync-text-tracks.ts"],"sourcesContent":["/**\n * **Own the text-track slots on the host media element, mirroring the SPF\n * model.** When a presentation is resolved and a media element is\n * available, allocate one slot in `mediaElement.textTracks` per model text\n * track — via creating `<track>` children, since that's the only spec\n * mechanism for adding *and* removing entries to `textTracks` (no\n * `removeTextTrack` API exists). Once slots are provisioned, mirror the\n * resolved `selectedTextTrackId` into their `mode`s (one-way: state → DOM),\n * and propagate user-initiated DOM `change` events back to\n * `userTextTrackSelection` — the standing *intent* (a language-based partial,\n * or `'off'`) that `switchTextTrack` resolves into `selectedTextTrackId`. So\n * non-SPF consumers (host-page captions buttons, browser native UI, video.js\n * store) drive selection by expressing intent, not by writing the resolved id.\n *\n * Single-positive-state reactor (`'preconditions-unmet'` ↔ `'sync-active'`):\n * the entry allocates the slots, applies the initial selection, attaches\n * the `change` listener, and opens a brief Chromium settling-window guard —\n * all transition-driven, fire-once on state entry, with paired cleanup on\n * state exit. A single `effects:` mirrors subsequent\n * `selectedTextTrackId` changes into `mode`s; that's the only\n * continuous-reactivity concern.\n *\n * State-exit cleanup also sends a `'clear'` message to the\n * `TextTracksActor` so its cue+segment cache (keyed by trackId) is\n * dropped alongside the DOM `<track>` slots. The actor itself is owned\n * by `setupTextTrackActors` and bound to mediaElement, not presentation,\n * so it survives source resets; clearing its context here keeps the\n * cache consistent with the DOM. Without this, a subsequent\n * presentation reusing a trackId would have `getSegmentsToLoad` treat\n * its segments as already-buffered and skip loading them.\n *\n * Single-writer separation: `selectedTextTrackId` is the resolved *output*\n * owned solely by `switchTextTrack`; this behavior only reads it (to mirror\n * modes). The write path here is `userTextTrackSelection` — the user-intent\n * *input* — so DOM action and the resolver never contend for one slot. The\n * intent isn't cleared on source unload (it's a standing preference, like\n * `userAudioTrackSelection`); `'off'` is written when the user disables all\n * tracks via native UI.\n *\n * Echo guard: `selectedTextTrackId` is exactly the id this behavior last drove\n * into the DOM, so a `change` event still showing it is our own echo (or a\n * resolver-driven correction — e.g. the picked track's CDN failed and the\n * resolver disabled it) and is ignored, never written back as a spurious user\n * action. Only a showing id that *differs* from the resolved id is a real user\n * pick. The settling-window guard additionally swallows Chromium's init-time\n * auto-selection before the resolved selection has settled.\n */\n\nimport { listen } from '@videojs/utils/dom';\nimport { defineBehavior } from '../../../core/composition/create-composition';\nimport type { Reactor } from '../../../core/reactors/create-machine-reactor';\nimport { createMachineReactor } from '../../../core/reactors/create-machine-reactor';\nimport { computed, peek, type ReadonlySignal, type Signal } from '../../../core/signals/primitives';\nimport { syncTextTrackModes } from '../../../media/dom/text/text-track-slots';\nimport type { MaybeResolvedPresentation, PartiallyResolvedTextTrack, TextTrack } from '../../../media/types';\nimport { getTracksByType } from '../../../media/utils/tracks';\nimport type { TextTracksActor } from '../../actors/text-tracks';\n\ntype SyncTextTracksFsmState = 'preconditions-unmet' | 'sync-active';\n\nexport interface SyncTextTracksConfig {\n /**\n * Create and append SPF-owned `<track>` slots on `mediaElement`, one per\n * model text track. Implementation tags each element so the read/remove\n * helpers can scope to SPF-owned slots. **Required** — the behavior is\n * DOM-binding-neutral and the composing engine supplies the integration.\n */\n addSubtitlesTracksToMedia: (\n mediaElement: HTMLMediaElement,\n modelTextTracks: readonly (PartiallyResolvedTextTrack | TextTrack)[]\n ) => void;\n /**\n * Return the SPF-owned subtitle/caption `TextTrack` currently in `'showing'`\n * mode, or `undefined` if none. Used by the DOM `change` bridge to mirror\n * native-UI selection back into `selectedTextTrackId`.\n */\n getShowingSubtitlesTrackFromMedia: (mediaElement: HTMLMediaElement) => globalThis.TextTrack | undefined;\n /**\n * Remove every SPF-owned `<track>` child from `mediaElement`. Called on\n * state exit (source unload, behavior destroy) to evict slots.\n */\n removeAllSubtitlesTracksFromMedia: (mediaElement: HTMLMediaElement) => void;\n}\n\nfunction deriveState(\n presentation: MaybeResolvedPresentation | undefined,\n mediaElement: HTMLMediaElement | undefined\n): SyncTextTracksFsmState {\n if (!mediaElement || !presentation) return 'preconditions-unmet';\n return getTracksByType(presentation, 'text').length > 0 ? 'sync-active' : 'preconditions-unmet';\n}\n\n/**\n * Map the DOM-showing track back to standing user intent. No showing track is an\n * explicit `'off'`. Otherwise prefer a language-based partial (so the pick\n * persists across source changes); fall back to `{ id }` for a track without a\n * language (precise within a source, just not portable).\n */\nfunction deriveTextTrackIntent(\n showingId: string | undefined,\n modelTextTracks: readonly (PartiallyResolvedTextTrack | TextTrack)[]\n): Partial<TextTrack> | 'off' {\n if (!showingId) return 'off';\n const language = modelTextTracks.find((track) => track.id === showingId)?.language;\n return language ? { language } : { id: showingId };\n}\n\nfunction syncTextTracksSetup({\n state,\n context,\n config,\n}: {\n state: {\n presentation: ReadonlySignal<MaybeResolvedPresentation | undefined>;\n // Read-only here: the resolved output owned by `switchTextTrack`, mirrored\n // into DOM modes and used as the echo-guard reference.\n selectedTextTrackId: ReadonlySignal<string | undefined>;\n // The write path: standing user intent the DOM bridge feeds.\n userTextTrackSelection: Signal<Partial<TextTrack> | 'off' | undefined>;\n };\n context: {\n mediaElement: ReadonlySignal<HTMLMediaElement | undefined>;\n textTracksActor: ReadonlySignal<TextTracksActor<VTTCue> | undefined>;\n };\n config: SyncTextTracksConfig;\n}): Reactor<SyncTextTracksFsmState | 'destroying' | 'destroyed'> {\n const { addSubtitlesTracksToMedia, getShowingSubtitlesTrackFromMedia, removeAllSubtitlesTracksFromMedia } = config;\n\n const derivedStateSignal = computed(() => deriveState(state.presentation.get(), context.mediaElement.get()));\n\n return createMachineReactor<SyncTextTracksFsmState>({\n initial: 'preconditions-unmet',\n monitor: () => derivedStateSignal.get(),\n states: {\n 'preconditions-unmet': {},\n\n 'sync-active': {\n // Allocate slots, apply the initial selection, attach the change\n // listener, and open the settling window — all transition-driven\n // (fire once per state entry). Entry is auto-untracked, so `.get()`\n // here doesn't subscribe; the returned cleanup detaches and evicts\n // on state exit (src reset through 'preconditions-unmet') and on\n // destroy.\n entry: () => {\n const mediaElement = context.mediaElement.get()!;\n // `getTracksByType('text', ...)` returns text tracks only — the\n // selection-set filter inside the helper ensures that — but its\n // declared return is the wide track union. Mirror the cast\n // pattern used by `track-switching` for the video branch.\n const modelTextTracks = getTracksByType(state.presentation.get()!, 'text') as readonly (\n | PartiallyResolvedTextTrack\n | TextTrack\n )[];\n\n addSubtitlesTracksToMedia(mediaElement, modelTextTracks);\n // Apply our selection synchronously so the change-event tasks\n // these mode writes queue land in the macrotask queue *before*\n // the settling-close `setTimeout(0)` queued below. This keeps\n // the settling window open long enough to swallow Chromium's\n // own auto-selection task (also scheduled in the next tick after\n // `<track>` insertion); if the order flipped, our handler would\n // treat Chromium's auto-pick as a user action. The `effects:`\n // block below handles subsequent `selectedTextTrackId` changes.\n syncTextTrackModes(mediaElement.textTracks, state.selectedTextTrackId.get());\n\n // Chromium re-applies its own selection across the next task tick\n // after `<track>` insertion (default-track auto-pick, language\n // preference). During this window, the `change` event may fire\n // with browser-chosen modes that don't reflect a real user action;\n // re-apply our modes silently rather than writing them back.\n let inSettlingWindow = true;\n const settlingTimeout = setTimeout(() => {\n inSettlingWindow = false;\n }, 0);\n\n const onChange = (): void => {\n if (inSettlingWindow) {\n syncTextTrackModes(mediaElement.textTracks, state.selectedTextTrackId.get());\n return;\n }\n const showingTrack = getShowingSubtitlesTrackFromMedia(mediaElement);\n // `showingTrack.id` matches the SPF id we set when the slot was\n // allocated. Empty-string ids fall through to `undefined`.\n const showingId = showingTrack?.id || undefined;\n // Echo guard: selectedTextTrackId is the id we last drove into the\n // DOM (mirror / resolver correction). A change still showing it is our\n // own echo — ignore it rather than write spurious intent.\n if (showingId === state.selectedTextTrackId.get()) return;\n // Genuine user action → write intent (resolved into selectedTextTrackId\n // by switchTextTrack), not the resolved id.\n state.userTextTrackSelection.set(deriveTextTrackIntent(showingId, modelTextTracks));\n };\n\n const unlisten = listen(mediaElement.textTracks, 'change', onChange);\n\n return () => {\n unlisten();\n clearTimeout(settlingTimeout);\n removeAllSubtitlesTracksFromMedia(mediaElement);\n // Clear the TextTracksActor's cue+segment cache, which is\n // keyed by trackId. If we don't, a subsequent presentation\n // reusing a trackId would have `getSegmentsToLoad` treat its\n // segments as already-buffered. The DOM cleanup above\n // already evicted the live cues; this drops the cache that\n // tracked them. The actor itself is owned by\n // `setupTextTrackActors` (mediaElement-bound lifecycle), so\n // we send rather than destroy.\n peek(context.textTracksActor)?.send({ type: 'clear' });\n };\n },\n\n // Mirror selection changes into mode. The state machine handles\n // mediaElement/presentation changes via 'preconditions-unmet'\n // round-trips, so we peek mediaElement here.\n effects: () => {\n const mediaElement = peek(context.mediaElement)!;\n syncTextTrackModes(mediaElement.textTracks, state.selectedTextTrackId.get());\n },\n },\n },\n });\n}\n\nexport const syncTextTracks = defineBehavior({\n stateKeys: ['presentation', 'selectedTextTrackId', 'userTextTrackSelection'],\n contextKeys: ['mediaElement', 'textTracksActor'],\n setup: syncTextTracksSetup,\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoFA,SAAS,YACP,cACA,cACwB;CACxB,IAAI,CAAC,gBAAgB,CAAC,cAAc,OAAO;CAC3C,OAAO,gBAAgB,cAAc,MAAM,CAAC,CAAC,SAAS,IAAI,gBAAgB;AAC5E;;;;;;;AAQA,SAAS,sBACP,WACA,iBAC4B;CAC5B,IAAI,CAAC,WAAW,OAAO;CACvB,MAAM,WAAW,gBAAgB,MAAM,UAAU,MAAM,OAAO,SAAS,CAAC,EAAE;CAC1E,OAAO,WAAW,EAAE,SAAS,IAAI,EAAE,IAAI,UAAU;AACnD;AAEA,SAAS,oBAAoB,EAC3B,OACA,SACA,UAe+D;CAC/D,MAAM,EAAE,2BAA2B,mCAAmC,sCAAsC;CAE5G,MAAM,qBAAqB,eAAe,YAAY,MAAM,aAAa,IAAI,GAAG,QAAQ,aAAa,IAAI,CAAC,CAAC;CAE3G,OAAO,qBAA6C;EAClD,SAAS;EACT,eAAe,mBAAmB,IAAI;EACtC,QAAQ;GACN,uBAAuB,CAAC;GAExB,eAAe;IAOb,aAAa;KACX,MAAM,eAAe,QAAQ,aAAa,IAAI;KAK9C,MAAM,kBAAkB,gBAAgB,MAAM,aAAa,IAAI,GAAI,MAAM;KAKzE,0BAA0B,cAAc,eAAe;KASvD,mBAAmB,aAAa,YAAY,MAAM,oBAAoB,IAAI,CAAC;KAO3E,IAAI,mBAAmB;KACvB,MAAM,kBAAkB,iBAAiB;MACvC,mBAAmB;KACrB,GAAG,CAAC;KAEJ,MAAM,iBAAuB;MAC3B,IAAI,kBAAkB;OACpB,mBAAmB,aAAa,YAAY,MAAM,oBAAoB,IAAI,CAAC;OAC3E;MACF;MAIA,MAAM,YAHe,kCAAkC,YAG1B,CAAC,EAAE,MAAM,KAAA;MAItC,IAAI,cAAc,MAAM,oBAAoB,IAAI,GAAG;MAGnD,MAAM,uBAAuB,IAAI,sBAAsB,WAAW,eAAe,CAAC;KACpF;KAEA,MAAM,WAAW,OAAO,aAAa,YAAY,UAAU,QAAQ;KAEnE,aAAa;MACX,SAAS;MACT,aAAa,eAAe;MAC5B,kCAAkC,YAAY;MAS9C,KAAK,QAAQ,eAAe,CAAC,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;KACvD;IACF;IAKA,eAAe;KAEb,mBADqB,KAAK,QAAQ,YACJ,CAAC,CAAC,YAAY,MAAM,oBAAoB,IAAI,CAAC;IAC7E;GACF;EACF;CACF,CAAC;AACH;AAEA,MAAa,iBAAiB,eAAe;CAC3C,WAAW;EAAC;EAAgB;EAAuB;CAAwB;CAC3E,aAAa,CAAC,gBAAgB,iBAAiB;CAC/C,OAAO;AACT,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"track-current-time.js","names":[],"sources":["../../../../../src/playback/behaviors/dom/track-current-time.ts"],"sourcesContent":["/**\n * Mirror `mediaElement.currentTime` into reactive state. Listens for:\n * - `timeupdate` — fires during playback (~4 Hz)\n * - `seeking` — fires when a seek begins; per spec, `currentTime` is already\n * at the new position when this event dispatches, so buffer management can\n * react immediately rather than waiting for `timeupdate`, which does not\n * fire while paused.\n * - `emptied` — fires when the resource selection algorithm tears down the\n * current media (e.g. a new `src` is set on the same element). Re-syncs so\n * downstream state doesn't retain the stale playback position from the\n * previous source when the engine is reused across src changes.\n *\n * Also syncs immediately when a media element becomes available.\n *\n * When no media element is attached, writes `config.defaultCurrentTime`\n * (default-default `0`, matching the HTMLMediaElement spec) so consumers\n * always see a defined position. Read-only mirror; does not push\n * `state.currentTime` back to the element.\n */\nimport { listen } from '@videojs/utils/dom';\nimport { defineBehavior } from '../../../core/composition/create-composition';\nimport { effect } from '../../../core/signals/effect';\nimport type { ReadonlySignal, Signal } from '../../../core/signals/primitives';\n\nexport interface CurrentTimeState {\n currentTime?: number;\n}\n\nexport interface CurrentTimeContext {\n mediaElement?: HTMLMediaElement | undefined;\n}\n\nexport interface TrackCurrentTimeConfig {\n /**\n * Value written to `state.currentTime` when no media element is attached.\n * Defaults to `0` — the HTMLMediaElement spec default.\n */\n defaultCurrentTime?: number;\n}\n\nfunction trackCurrentTimeSetup({\n state,\n context,\n config,\n}: {\n state: { currentTime: Signal<CurrentTimeState['currentTime']> };\n context: { mediaElement: ReadonlySignal<CurrentTimeContext['mediaElement']> };\n config?: TrackCurrentTimeConfig;\n}): () => void {\n const defaultCurrentTime = config?.defaultCurrentTime ?? 0;\n\n return effect(() => {\n const mediaElement = context.mediaElement.get();\n if (!mediaElement) {\n state.currentTime.set(defaultCurrentTime);\n return;\n }\n\n const sync = () => state.currentTime.set(mediaElement.currentTime);\n sync();\n const removeEmptied = listen(mediaElement, 'emptied', sync);\n const removeTimeupdate = listen(mediaElement, 'timeupdate', sync);\n const removeSeeking = listen(mediaElement, 'seeking', sync);\n return () => {\n removeEmptied();\n removeTimeupdate();\n removeSeeking();\n };\n });\n}\n\nexport const trackCurrentTime = defineBehavior({\n stateKeys: ['currentTime'],\n contextKeys: ['mediaElement'],\n setup: trackCurrentTimeSetup,\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAwCA,SAAS,sBAAsB,EAC7B,OACA,SACA,UAKa;CACb,MAAM,qBAAqB,QAAQ,sBAAsB;AAEzD,QAAO,aAAa;EAClB,MAAM,eAAe,QAAQ,aAAa,KAAK;AAC/C,MAAI,CAAC,cAAc;AACjB,SAAM,YAAY,IAAI,mBAAmB;AACzC;;EAGF,MAAM,aAAa,MAAM,YAAY,IAAI,aAAa,YAAY;AAClE,QAAM;EACN,MAAM,gBAAgB,OAAO,cAAc,WAAW,KAAK;EAC3D,MAAM,mBAAmB,OAAO,cAAc,cAAc,KAAK;EACjE,MAAM,gBAAgB,OAAO,cAAc,WAAW,KAAK;AAC3D,eAAa;AACX,kBAAe;AACf,qBAAkB;AAClB,kBAAe;;GAEjB;;AAGJ,MAAa,mBAAmB,eAAe;CAC7C,WAAW,CAAC,cAAc;CAC1B,aAAa,CAAC,eAAe;CAC7B,OAAO;CACR,CAAC"}
1
+ {"version":3,"file":"track-current-time.js","names":[],"sources":["../../../../../src/playback/behaviors/dom/track-current-time.ts"],"sourcesContent":["/**\n * Mirror `mediaElement.currentTime` into reactive state. Listens for:\n * - `timeupdate` — fires during playback (~4 Hz)\n * - `seeking` — fires when a seek begins; per spec, `currentTime` is already\n * at the new position when this event dispatches, so buffer management can\n * react immediately rather than waiting for `timeupdate`, which does not\n * fire while paused.\n * - `emptied` — fires when the resource selection algorithm tears down the\n * current media (e.g. a new `src` is set on the same element). Re-syncs so\n * downstream state doesn't retain the stale playback position from the\n * previous source when the engine is reused across src changes.\n *\n * Also syncs immediately when a media element becomes available.\n *\n * When no media element is attached, writes `config.defaultCurrentTime`\n * (default-default `0`, matching the HTMLMediaElement spec) so consumers\n * always see a defined position. Read-only mirror; does not push\n * `state.currentTime` back to the element.\n */\nimport { listen } from '@videojs/utils/dom';\nimport { defineBehavior } from '../../../core/composition/create-composition';\nimport { effect } from '../../../core/signals/effect';\nimport type { ReadonlySignal, Signal } from '../../../core/signals/primitives';\n\nexport interface CurrentTimeState {\n currentTime?: number;\n}\n\nexport interface CurrentTimeContext {\n mediaElement?: HTMLMediaElement | undefined;\n}\n\nexport interface TrackCurrentTimeConfig {\n /**\n * Value written to `state.currentTime` when no media element is attached.\n * Defaults to `0` — the HTMLMediaElement spec default.\n */\n defaultCurrentTime?: number;\n}\n\nfunction trackCurrentTimeSetup({\n state,\n context,\n config,\n}: {\n state: { currentTime: Signal<CurrentTimeState['currentTime']> };\n context: { mediaElement: ReadonlySignal<CurrentTimeContext['mediaElement']> };\n config?: TrackCurrentTimeConfig;\n}): () => void {\n const defaultCurrentTime = config?.defaultCurrentTime ?? 0;\n\n return effect(() => {\n const mediaElement = context.mediaElement.get();\n if (!mediaElement) {\n state.currentTime.set(defaultCurrentTime);\n return;\n }\n\n const sync = () => state.currentTime.set(mediaElement.currentTime);\n sync();\n const removeEmptied = listen(mediaElement, 'emptied', sync);\n const removeTimeupdate = listen(mediaElement, 'timeupdate', sync);\n const removeSeeking = listen(mediaElement, 'seeking', sync);\n return () => {\n removeEmptied();\n removeTimeupdate();\n removeSeeking();\n };\n });\n}\n\nexport const trackCurrentTime = defineBehavior({\n stateKeys: ['currentTime'],\n contextKeys: ['mediaElement'],\n setup: trackCurrentTimeSetup,\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAwCA,SAAS,sBAAsB,EAC7B,OACA,SACA,UAKa;CACb,MAAM,qBAAqB,QAAQ,sBAAsB;CAEzD,OAAO,aAAa;EAClB,MAAM,eAAe,QAAQ,aAAa,IAAI;EAC9C,IAAI,CAAC,cAAc;GACjB,MAAM,YAAY,IAAI,kBAAkB;GACxC;EACF;EAEA,MAAM,aAAa,MAAM,YAAY,IAAI,aAAa,WAAW;EACjE,KAAK;EACL,MAAM,gBAAgB,OAAO,cAAc,WAAW,IAAI;EAC1D,MAAM,mBAAmB,OAAO,cAAc,cAAc,IAAI;EAChE,MAAM,gBAAgB,OAAO,cAAc,WAAW,IAAI;EAC1D,aAAa;GACX,cAAc;GACd,iBAAiB;GACjB,cAAc;EAChB;CACF,CAAC;AACH;AAEA,MAAa,mBAAmB,eAAe;CAC7C,WAAW,CAAC,aAAa;CACzB,aAAa,CAAC,cAAc;CAC5B,OAAO;AACT,CAAC"}
@@ -32,9 +32,9 @@ function deriveState(presentation, mediaElement, loadActivated) {
32
32
  *
33
33
  * Writes `state.loadActivated = true` the first time a `play` or `seeking`
34
34
  * event fires on the attached media element for the current source — or
35
- * immediately on entry if the element is already in such a state
36
- * (`!el.paused` or `el.seeking`), mirroring autoplay / native-controls /
37
- * direct-DOM-`play()` scenarios.
35
+ * immediately on entry if the element is already committed to loading
36
+ * (`el.autoplay`, `!el.paused`, or `el.seeking`), covering autoplay,
37
+ * native-controls, and direct-DOM-`play()` scenarios.
38
38
  *
39
39
  * Sticky-true *within a source identity*: subsequent play/pause/seek
40
40
  * cycles don't flip back. Source identity = (mediaElement, presentation
@@ -64,7 +64,7 @@ function trackLoadTriggersSetup({ state, context }) {
64
64
  monitoring: { effects: () => {
65
65
  const el = context.mediaElement.get();
66
66
  const setLoadActivated = () => state.loadActivated.set(true);
67
- if (!el.paused || el.seeking) {
67
+ if (el.autoplay || !el.paused || el.seeking) {
68
68
  setLoadActivated();
69
69
  return;
70
70
  }
@@ -1 +1 @@
1
- {"version":3,"file":"track-load-triggers.js","names":[],"sources":["../../../../../src/playback/behaviors/dom/track-load-triggers.ts"],"sourcesContent":["import { listen } from '@videojs/utils/dom';\nimport { defineBehavior } from '../../../core/composition/create-composition';\nimport type { Reactor } from '../../../core/reactors/create-machine-reactor';\nimport { createMachineReactor } from '../../../core/reactors/create-machine-reactor';\nimport { computed, type ReadonlySignal, type Signal } from '../../../core/signals/primitives';\nimport type { MaybeResolvedPresentation } from '../../../media/types';\n\n/**\n * State shape for load-trigger tracking.\n */\nexport interface LoadTriggersState {\n /**\n * Sticky-true per source. Flips to `true` the first time an event occurs\n * that should switch loading behavior from preload-based to standard load\n * (currently DOM `play` / `seeking`). Reset to `false` on source change.\n *\n * Read by `resolvePresentation` and `loadVideoSegments` / `loadAudioSegments`\n * to mirror native `HTMLMediaElement` preload semantics — only meaningful\n * when `preload !== 'auto'`, which already loads fully regardless.\n */\n loadActivated?: boolean;\n /** Current presentation — URL is used to detect source changes. */\n presentation?: MaybeResolvedPresentation;\n}\n\n/**\n * Context shape for load-trigger tracking.\n */\nexport interface LoadTriggersContext {\n mediaElement?: HTMLMediaElement | undefined;\n}\n\ntype LoadTriggersFsmState = 'preconditions-unmet' | 'monitoring' | 'load-active';\n\n/**\n * Slot-driven FSM. The slot `loadActivated` is the canonical externally-\n * observable state; `deriveState` reads it (and the preconditions) to\n * derive the local FSM state. The slot is *both* the state and the data —\n * no separate internal bookkeeping.\n *\n * ```\n * 'preconditions-unmet' ⟷ 'monitoring' ⟷ 'load-active'\n *\n * preconditions-unmet → monitoring element + URL appear\n * monitoring → load-active slot flips true (listener fires\n * or external write)\n * load-active → monitoring within-state cleanup resets slot\n * (URL or element identity change)\n * any → preconditions-unmet element or URL → undefined\n *\n * any state → destroying → destroyed on destroy()\n * ```\n */\nfunction deriveState(\n presentation: MaybeResolvedPresentation | undefined,\n mediaElement: HTMLMediaElement | undefined,\n loadActivated: boolean | undefined\n): LoadTriggersFsmState {\n if (!mediaElement || !presentation?.url) return 'preconditions-unmet';\n if (loadActivated) return 'load-active';\n return 'monitoring';\n}\n\n/**\n * Track preload-overriding events per source.\n *\n * Writes `state.loadActivated = true` the first time a `play` or `seeking`\n * event fires on the attached media element for the current source — or\n * immediately on entry if the element is already in such a state\n * (`!el.paused` or `el.seeking`), mirroring autoplay / native-controls /\n * direct-DOM-`play()` scenarios.\n *\n * Sticky-true *within a source identity*: subsequent play/pause/seek\n * cycles don't flip back. Source identity = (mediaElement, presentation\n * URL). Either changing — including direct in-place swap with no\n * `undefined` intermediate — resets the slot to `false`.\n *\n * Multi-writer with `hls/adapter.ts:play()` (which writes `true` directly\n * on programmatic play) is intentional — different domains. The adapter\n * records programmatic intent; this behavior is the DOM-side observer.\n * Pre-existing `true` writes are honored because `deriveState` reads the\n * slot — a `true` value routes directly to `'load-active'` without\n * entering `'monitoring'`.\n *\n * @example\n * const reactor = trackLoadTriggers.setup({ state, context });\n * // later:\n * reactor.destroy();\n */\nfunction trackLoadTriggersSetup({\n state,\n context,\n}: {\n state: {\n loadActivated: Signal<LoadTriggersState['loadActivated']>;\n presentation: ReadonlySignal<LoadTriggersState['presentation']>;\n };\n context: { mediaElement: ReadonlySignal<LoadTriggersContext['mediaElement']> };\n}): Reactor<LoadTriggersFsmState | 'destroying' | 'destroyed'> {\n const derivedStateSignal = computed(() =>\n deriveState(state.presentation.get(), context.mediaElement.get(), state.loadActivated.get())\n );\n // Sparse-firing source-URL signal. Emits whenever `presentation?.url`\n // resolves to a different value — covering both URL string changes\n // (url_a → url_b) and transitions where the URL becomes defined or\n // undefined (presentation set, cleared, or replaced without a URL\n // field). Does NOT emit when `state.presentation` is rewritten with\n // the same URL: manifest parsing, duration calc, track resolution,\n // and other enrichment paths re-write the presentation object\n // frequently — tracking raw `state.presentation` here would spuriously\n // reset the 'load-active' slot.\n const urlSignal = computed(() => state.presentation.get()?.url);\n\n return createMachineReactor<LoadTriggersFsmState>({\n initial: 'preconditions-unmet',\n monitor: () => derivedStateSignal.get(),\n states: {\n 'preconditions-unmet': {},\n\n monitoring: {\n // effects (not entry) so element-identity change within this state\n // re-fires and re-attaches listeners to the new element. The monitor\n // guarantees mediaElement is truthy while we're in this state.\n effects: () => {\n const el = context.mediaElement.get()!;\n\n const setLoadActivated = () => state.loadActivated.set(true);\n if (!el.paused || el.seeking) {\n setLoadActivated();\n return;\n }\n const cleanupPlay = listen(el, 'play', setLoadActivated);\n const cleanupSeeking = listen(el, 'seeking', setLoadActivated);\n return () => {\n cleanupPlay();\n cleanupSeeking();\n };\n },\n },\n\n 'load-active': {\n // effects-based cleanup (not entry) because the cleanup must fire on\n // BOTH state exit AND within-state identity change (url_a → url_b,\n // element_a → element_b — both truthy, no state transition).\n // Re-firing tracked reads → cleanup writes loadActivated=false →\n // deriveState (which reads loadActivated) returns 'monitoring' →\n // state transitions → fresh listeners attached for the new source.\n effects: () => {\n context.mediaElement.get();\n urlSignal.get();\n return () => state.loadActivated.set(false);\n },\n },\n },\n });\n}\n\nexport const trackLoadTriggers = defineBehavior({\n stateKeys: ['loadActivated', 'presentation'],\n contextKeys: ['mediaElement'],\n setup: trackLoadTriggersSetup,\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAqDA,SAAS,YACP,cACA,cACA,eACsB;AACtB,KAAI,CAAC,gBAAgB,CAAC,cAAc,IAAK,QAAO;AAChD,KAAI,cAAe,QAAO;AAC1B,QAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BT,SAAS,uBAAuB,EAC9B,OACA,WAO6D;CAC7D,MAAM,qBAAqB,eACzB,YAAY,MAAM,aAAa,KAAK,EAAE,QAAQ,aAAa,KAAK,EAAE,MAAM,cAAc,KAAK,CAAC,CAC7F;CAUD,MAAM,YAAY,eAAe,MAAM,aAAa,KAAK,EAAE,IAAI;AAE/D,QAAO,qBAA2C;EAChD,SAAS;EACT,eAAe,mBAAmB,KAAK;EACvC,QAAQ;GACN,uBAAuB,EAAE;GAEzB,YAAY,EAIV,eAAe;IACb,MAAM,KAAK,QAAQ,aAAa,KAAK;IAErC,MAAM,yBAAyB,MAAM,cAAc,IAAI,KAAK;AAC5D,QAAI,CAAC,GAAG,UAAU,GAAG,SAAS;AAC5B,uBAAkB;AAClB;;IAEF,MAAM,cAAc,OAAO,IAAI,QAAQ,iBAAiB;IACxD,MAAM,iBAAiB,OAAO,IAAI,WAAW,iBAAiB;AAC9D,iBAAa;AACX,kBAAa;AACb,qBAAgB;;MAGrB;GAED,eAAe,EAOb,eAAe;AACb,YAAQ,aAAa,KAAK;AAC1B,cAAU,KAAK;AACf,iBAAa,MAAM,cAAc,IAAI,MAAM;MAE9C;GACF;EACF,CAAC;;AAGJ,MAAa,oBAAoB,eAAe;CAC9C,WAAW,CAAC,iBAAiB,eAAe;CAC5C,aAAa,CAAC,eAAe;CAC7B,OAAO;CACR,CAAC"}
1
+ {"version":3,"file":"track-load-triggers.js","names":[],"sources":["../../../../../src/playback/behaviors/dom/track-load-triggers.ts"],"sourcesContent":["import { listen } from '@videojs/utils/dom';\nimport { defineBehavior } from '../../../core/composition/create-composition';\nimport type { Reactor } from '../../../core/reactors/create-machine-reactor';\nimport { createMachineReactor } from '../../../core/reactors/create-machine-reactor';\nimport { computed, type ReadonlySignal, type Signal } from '../../../core/signals/primitives';\nimport type { MaybeResolvedPresentation } from '../../../media/types';\n\n/**\n * State shape for load-trigger tracking.\n */\nexport interface LoadTriggersState {\n /**\n * Sticky-true per source. Flips to `true` the first time an event occurs\n * that should switch loading behavior from preload-based to standard load\n * (currently DOM `play` / `seeking`). Reset to `false` on source change.\n *\n * Read by `resolvePresentation` and `loadVideoSegments` / `loadAudioSegments`\n * to mirror native `HTMLMediaElement` preload semantics — only meaningful\n * when `preload !== 'auto'`, which already loads fully regardless.\n */\n loadActivated?: boolean;\n /** Current presentation — URL is used to detect source changes. */\n presentation?: MaybeResolvedPresentation;\n}\n\n/**\n * Context shape for load-trigger tracking.\n */\nexport interface LoadTriggersContext {\n mediaElement?: HTMLMediaElement | undefined;\n}\n\ntype LoadTriggersFsmState = 'preconditions-unmet' | 'monitoring' | 'load-active';\n\n/**\n * Slot-driven FSM. The slot `loadActivated` is the canonical externally-\n * observable state; `deriveState` reads it (and the preconditions) to\n * derive the local FSM state. The slot is *both* the state and the data —\n * no separate internal bookkeeping.\n *\n * ```\n * 'preconditions-unmet' ⟷ 'monitoring' ⟷ 'load-active'\n *\n * preconditions-unmet → monitoring element + URL appear\n * monitoring → load-active slot flips true (listener fires\n * or external write)\n * load-active → monitoring within-state cleanup resets slot\n * (URL or element identity change)\n * any → preconditions-unmet element or URL → undefined\n *\n * any state → destroying → destroyed on destroy()\n * ```\n */\nfunction deriveState(\n presentation: MaybeResolvedPresentation | undefined,\n mediaElement: HTMLMediaElement | undefined,\n loadActivated: boolean | undefined\n): LoadTriggersFsmState {\n if (!mediaElement || !presentation?.url) return 'preconditions-unmet';\n if (loadActivated) return 'load-active';\n return 'monitoring';\n}\n\n/**\n * Track preload-overriding events per source.\n *\n * Writes `state.loadActivated = true` the first time a `play` or `seeking`\n * event fires on the attached media element for the current source — or\n * immediately on entry if the element is already committed to loading\n * (`el.autoplay`, `!el.paused`, or `el.seeking`), covering autoplay,\n * native-controls, and direct-DOM-`play()` scenarios.\n *\n * Sticky-true *within a source identity*: subsequent play/pause/seek\n * cycles don't flip back. Source identity = (mediaElement, presentation\n * URL). Either changing — including direct in-place swap with no\n * `undefined` intermediate — resets the slot to `false`.\n *\n * Multi-writer with `hls/adapter.ts:play()` (which writes `true` directly\n * on programmatic play) is intentional — different domains. The adapter\n * records programmatic intent; this behavior is the DOM-side observer.\n * Pre-existing `true` writes are honored because `deriveState` reads the\n * slot — a `true` value routes directly to `'load-active'` without\n * entering `'monitoring'`.\n *\n * @example\n * const reactor = trackLoadTriggers.setup({ state, context });\n * // later:\n * reactor.destroy();\n */\nfunction trackLoadTriggersSetup({\n state,\n context,\n}: {\n state: {\n loadActivated: Signal<LoadTriggersState['loadActivated']>;\n presentation: ReadonlySignal<LoadTriggersState['presentation']>;\n };\n context: { mediaElement: ReadonlySignal<LoadTriggersContext['mediaElement']> };\n}): Reactor<LoadTriggersFsmState | 'destroying' | 'destroyed'> {\n const derivedStateSignal = computed(() =>\n deriveState(state.presentation.get(), context.mediaElement.get(), state.loadActivated.get())\n );\n // Sparse-firing source-URL signal. Emits whenever `presentation?.url`\n // resolves to a different value — covering both URL string changes\n // (url_a → url_b) and transitions where the URL becomes defined or\n // undefined (presentation set, cleared, or replaced without a URL\n // field). Does NOT emit when `state.presentation` is rewritten with\n // the same URL: manifest parsing, duration calc, track resolution,\n // and other enrichment paths re-write the presentation object\n // frequently — tracking raw `state.presentation` here would spuriously\n // reset the 'load-active' slot.\n const urlSignal = computed(() => state.presentation.get()?.url);\n\n return createMachineReactor<LoadTriggersFsmState>({\n initial: 'preconditions-unmet',\n monitor: () => derivedStateSignal.get(),\n states: {\n 'preconditions-unmet': {},\n\n monitoring: {\n // effects (not entry) so element-identity change within this state\n // re-fires and re-attaches listeners to the new element. The monitor\n // guarantees mediaElement is truthy while we're in this state.\n effects: () => {\n const el = context.mediaElement.get()!;\n\n const setLoadActivated = () => state.loadActivated.set(true);\n if (el.autoplay || !el.paused || el.seeking) {\n setLoadActivated();\n return;\n }\n const cleanupPlay = listen(el, 'play', setLoadActivated);\n const cleanupSeeking = listen(el, 'seeking', setLoadActivated);\n return () => {\n cleanupPlay();\n cleanupSeeking();\n };\n },\n },\n\n 'load-active': {\n // effects-based cleanup (not entry) because the cleanup must fire on\n // BOTH state exit AND within-state identity change (url_a → url_b,\n // element_a → element_b — both truthy, no state transition).\n // Re-firing tracked reads → cleanup writes loadActivated=false →\n // deriveState (which reads loadActivated) returns 'monitoring' →\n // state transitions → fresh listeners attached for the new source.\n effects: () => {\n context.mediaElement.get();\n urlSignal.get();\n return () => state.loadActivated.set(false);\n },\n },\n },\n });\n}\n\nexport const trackLoadTriggers = defineBehavior({\n stateKeys: ['loadActivated', 'presentation'],\n contextKeys: ['mediaElement'],\n setup: trackLoadTriggersSetup,\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAqDA,SAAS,YACP,cACA,cACA,eACsB;CACtB,IAAI,CAAC,gBAAgB,CAAC,cAAc,KAAK,OAAO;CAChD,IAAI,eAAe,OAAO;CAC1B,OAAO;AACT;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BA,SAAS,uBAAuB,EAC9B,OACA,WAO6D;CAC7D,MAAM,qBAAqB,eACzB,YAAY,MAAM,aAAa,IAAI,GAAG,QAAQ,aAAa,IAAI,GAAG,MAAM,cAAc,IAAI,CAAC,CAC7F;CAUA,MAAM,YAAY,eAAe,MAAM,aAAa,IAAI,CAAC,EAAE,GAAG;CAE9D,OAAO,qBAA2C;EAChD,SAAS;EACT,eAAe,mBAAmB,IAAI;EACtC,QAAQ;GACN,uBAAuB,CAAC;GAExB,YAAY,EAIV,eAAe;IACb,MAAM,KAAK,QAAQ,aAAa,IAAI;IAEpC,MAAM,yBAAyB,MAAM,cAAc,IAAI,IAAI;IAC3D,IAAI,GAAG,YAAY,CAAC,GAAG,UAAU,GAAG,SAAS;KAC3C,iBAAiB;KACjB;IACF;IACA,MAAM,cAAc,OAAO,IAAI,QAAQ,gBAAgB;IACvD,MAAM,iBAAiB,OAAO,IAAI,WAAW,gBAAgB;IAC7D,aAAa;KACX,YAAY;KACZ,eAAe;IACjB;GACF,EACF;GAEA,eAAe,EAOb,eAAe;IACb,QAAQ,aAAa,IAAI;IACzB,UAAU,IAAI;IACd,aAAa,MAAM,cAAc,IAAI,KAAK;GAC5C,EACF;EACF;CACF,CAAC;AACH;AAEA,MAAa,oBAAoB,eAAe;CAC9C,WAAW,CAAC,iBAAiB,cAAc;CAC3C,aAAa,CAAC,cAAc;CAC5B,OAAO;AACT,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"track-playback-rate.js","names":[],"sources":["../../../../../src/playback/behaviors/dom/track-playback-rate.ts"],"sourcesContent":["/**\n * Mirror `mediaElement.playbackRate` into reactive state. On each `ratechange`\n * event, write the new value to `state.playbackRate`. Also syncs immediately\n * when a media element becomes available so consumers don't wait for the first\n * event.\n *\n * When no media element is attached, writes `config.defaultPlaybackRate`\n * (default-default `1`, matching the HTMLMediaElement spec) so consumers\n * always see the rate a freshly attached element would have. Read-only mirror;\n * does not push `state.playbackRate` back to the element.\n */\nimport { listen } from '@videojs/utils/dom';\nimport { defineBehavior } from '../../../core/composition/create-composition';\nimport { effect } from '../../../core/signals/effect';\nimport type { ReadonlySignal, Signal } from '../../../core/signals/primitives';\n\nexport interface PlaybackRateState {\n playbackRate?: number;\n}\n\nexport interface PlaybackRateContext {\n mediaElement?: HTMLMediaElement | undefined;\n}\n\nexport interface TrackPlaybackRateConfig {\n /**\n * Value written to `state.playbackRate` when no media element is attached.\n * Defaults to `1` — the HTMLMediaElement spec default.\n */\n defaultPlaybackRate?: number;\n}\n\nfunction trackPlaybackRateSetup({\n state,\n context,\n config,\n}: {\n state: { playbackRate: Signal<PlaybackRateState['playbackRate']> };\n context: { mediaElement: ReadonlySignal<PlaybackRateContext['mediaElement']> };\n config?: TrackPlaybackRateConfig;\n}): () => void {\n const defaultPlaybackRate = config?.defaultPlaybackRate ?? 1;\n\n return effect(() => {\n const mediaElement = context.mediaElement.get();\n if (!mediaElement) {\n state.playbackRate.set(defaultPlaybackRate);\n return;\n }\n\n const sync = () => state.playbackRate.set(mediaElement.playbackRate);\n sync();\n return listen(mediaElement, 'ratechange', sync);\n });\n}\n\nexport const trackPlaybackRate = defineBehavior({\n stateKeys: ['playbackRate'],\n contextKeys: ['mediaElement'],\n setup: trackPlaybackRateSetup,\n});\n"],"mappings":";;;;;;;;;;;;;;;AAgCA,SAAS,uBAAuB,EAC9B,OACA,SACA,UAKa;CACb,MAAM,sBAAsB,QAAQ,uBAAuB;AAE3D,QAAO,aAAa;EAClB,MAAM,eAAe,QAAQ,aAAa,KAAK;AAC/C,MAAI,CAAC,cAAc;AACjB,SAAM,aAAa,IAAI,oBAAoB;AAC3C;;EAGF,MAAM,aAAa,MAAM,aAAa,IAAI,aAAa,aAAa;AACpE,QAAM;AACN,SAAO,OAAO,cAAc,cAAc,KAAK;GAC/C;;AAGJ,MAAa,oBAAoB,eAAe;CAC9C,WAAW,CAAC,eAAe;CAC3B,aAAa,CAAC,eAAe;CAC7B,OAAO;CACR,CAAC"}
1
+ {"version":3,"file":"track-playback-rate.js","names":[],"sources":["../../../../../src/playback/behaviors/dom/track-playback-rate.ts"],"sourcesContent":["/**\n * Mirror `mediaElement.playbackRate` into reactive state. On each `ratechange`\n * event, write the new value to `state.playbackRate`. Also syncs immediately\n * when a media element becomes available so consumers don't wait for the first\n * event.\n *\n * When no media element is attached, writes `config.defaultPlaybackRate`\n * (default-default `1`, matching the HTMLMediaElement spec) so consumers\n * always see the rate a freshly attached element would have. Read-only mirror;\n * does not push `state.playbackRate` back to the element.\n */\nimport { listen } from '@videojs/utils/dom';\nimport { defineBehavior } from '../../../core/composition/create-composition';\nimport { effect } from '../../../core/signals/effect';\nimport type { ReadonlySignal, Signal } from '../../../core/signals/primitives';\n\nexport interface PlaybackRateState {\n playbackRate?: number;\n}\n\nexport interface PlaybackRateContext {\n mediaElement?: HTMLMediaElement | undefined;\n}\n\nexport interface TrackPlaybackRateConfig {\n /**\n * Value written to `state.playbackRate` when no media element is attached.\n * Defaults to `1` — the HTMLMediaElement spec default.\n */\n defaultPlaybackRate?: number;\n}\n\nfunction trackPlaybackRateSetup({\n state,\n context,\n config,\n}: {\n state: { playbackRate: Signal<PlaybackRateState['playbackRate']> };\n context: { mediaElement: ReadonlySignal<PlaybackRateContext['mediaElement']> };\n config?: TrackPlaybackRateConfig;\n}): () => void {\n const defaultPlaybackRate = config?.defaultPlaybackRate ?? 1;\n\n return effect(() => {\n const mediaElement = context.mediaElement.get();\n if (!mediaElement) {\n state.playbackRate.set(defaultPlaybackRate);\n return;\n }\n\n const sync = () => state.playbackRate.set(mediaElement.playbackRate);\n sync();\n return listen(mediaElement, 'ratechange', sync);\n });\n}\n\nexport const trackPlaybackRate = defineBehavior({\n stateKeys: ['playbackRate'],\n contextKeys: ['mediaElement'],\n setup: trackPlaybackRateSetup,\n});\n"],"mappings":";;;;;;;;;;;;;;;AAgCA,SAAS,uBAAuB,EAC9B,OACA,SACA,UAKa;CACb,MAAM,sBAAsB,QAAQ,uBAAuB;CAE3D,OAAO,aAAa;EAClB,MAAM,eAAe,QAAQ,aAAa,IAAI;EAC9C,IAAI,CAAC,cAAc;GACjB,MAAM,aAAa,IAAI,mBAAmB;GAC1C;EACF;EAEA,MAAM,aAAa,MAAM,aAAa,IAAI,aAAa,YAAY;EACnE,KAAK;EACL,OAAO,OAAO,cAAc,cAAc,IAAI;CAChD,CAAC;AACH;AAEA,MAAa,oBAAoB,eAAe;CAC9C,WAAW,CAAC,cAAc;CAC1B,aAAa,CAAC,cAAc;CAC5B,OAAO;AACT,CAAC"}