@videojs/spf 10.0.0-beta.24 → 10.0.0-beta.26

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 (375) hide show
  1. package/README.md +5 -0
  2. package/dist/default/background-video.js +3 -0
  3. package/dist/default/core/actors/create-machine-actor.js.map +1 -1
  4. package/dist/default/core/actors/create-transition-actor.js.map +1 -1
  5. package/dist/default/core/composition/create-composition.js +2 -2
  6. package/dist/default/core/composition/create-composition.js.map +1 -1
  7. package/dist/default/core/composition/share-signals.js +13 -11
  8. package/dist/default/core/composition/share-signals.js.map +1 -1
  9. package/dist/default/core/machine.js.map +1 -1
  10. package/dist/default/core/reactors/create-machine-reactor.js.map +1 -1
  11. package/dist/default/core/signals/effect.js.map +1 -1
  12. package/dist/default/core/signals/primitives.js.map +1 -1
  13. package/dist/default/core/tasks/task.js.map +1 -1
  14. package/dist/default/hls.js +4 -1
  15. package/dist/default/media/abr/quality-selection.js +6 -68
  16. package/dist/default/media/abr/quality-selection.js.map +1 -1
  17. package/dist/default/media/buffer/back-buffer.js.map +1 -1
  18. package/dist/default/media/buffer/forward-buffer.js +37 -3
  19. package/dist/default/media/buffer/forward-buffer.js.map +1 -1
  20. package/dist/default/media/dom/capabilities.js +70 -0
  21. package/dist/default/media/dom/capabilities.js.map +1 -0
  22. package/dist/default/media/dom/mse/append-segment.js.map +1 -1
  23. package/dist/default/media/dom/mse/buffer-flusher.js.map +1 -1
  24. package/dist/default/media/dom/mse/duration.js +27 -10
  25. package/dist/default/media/dom/mse/duration.js.map +1 -1
  26. package/dist/default/media/dom/mse/end-of-stream.js.map +1 -1
  27. package/dist/default/media/dom/mse/mediasource-setup.js +68 -17
  28. package/dist/default/media/dom/mse/mediasource-setup.js.map +1 -1
  29. package/dist/default/media/dom/text/resolve-vtt-segment.js +1 -7
  30. package/dist/default/media/dom/text/resolve-vtt-segment.js.map +1 -1
  31. package/dist/default/media/dom/text/text-track-slots.js +3 -3
  32. package/dist/default/media/dom/text/text-track-slots.js.map +1 -1
  33. package/dist/default/media/hls/parse-attributes.js +17 -3
  34. package/dist/default/media/hls/parse-attributes.js.map +1 -1
  35. package/dist/default/media/hls/parse-media-playlist.js +29 -1
  36. package/dist/default/media/hls/parse-media-playlist.js.map +1 -1
  37. package/dist/default/media/hls/parse-multivariant.js +15 -6
  38. package/dist/default/media/hls/parse-multivariant.js.map +1 -1
  39. package/dist/default/media/hls/resolve-url.js.map +1 -1
  40. package/dist/default/media/media-tracks/media-tracks.js +101 -0
  41. package/dist/default/media/media-tracks/media-tracks.js.map +1 -0
  42. package/dist/default/media/mp4/box.js +58 -0
  43. package/dist/default/media/mp4/box.js.map +1 -0
  44. package/dist/default/media/mp4/timestamp-origin.js +107 -0
  45. package/dist/default/media/mp4/timestamp-origin.js.map +1 -0
  46. package/dist/default/media/primitives/select-tracks.js +98 -13
  47. package/dist/default/media/primitives/select-tracks.js.map +1 -1
  48. package/dist/default/media/text/parse-vtt-timestamp-map.js +42 -0
  49. package/dist/default/media/text/parse-vtt-timestamp-map.js.map +1 -0
  50. package/dist/default/media/text/resolve-vtt-metadata.js +23 -0
  51. package/dist/default/media/text/resolve-vtt-metadata.js.map +1 -0
  52. package/dist/default/media/types/index.js.map +1 -1
  53. package/dist/default/media/utils/cdn.js +56 -0
  54. package/dist/default/media/utils/cdn.js.map +1 -0
  55. package/dist/default/media/utils/preload.js.map +1 -1
  56. package/dist/default/media/utils/track-selection.js +1 -1
  57. package/dist/default/media/utils/track-selection.js.map +1 -1
  58. package/dist/default/media/utils/tracks.js +31 -4
  59. package/dist/default/media/utils/tracks.js.map +1 -1
  60. package/dist/default/media-tracks.js +2 -0
  61. package/dist/default/network/bandwidth-estimator.js.map +1 -1
  62. package/dist/default/network/chunked-stream-iterable.js.map +1 -1
  63. package/dist/default/network/ewma.js +1 -1
  64. package/dist/default/network/ewma.js.map +1 -1
  65. package/dist/default/network/fetch.js +7 -1
  66. package/dist/default/network/fetch.js.map +1 -1
  67. package/dist/default/playback/actors/dom/segment-loader.js +103 -106
  68. package/dist/default/playback/actors/dom/segment-loader.js.map +1 -1
  69. package/dist/default/playback/actors/dom/source-buffer.js +3 -1
  70. package/dist/default/playback/actors/dom/source-buffer.js.map +1 -1
  71. package/dist/default/playback/actors/dom/text-tracks.js.map +1 -1
  72. package/dist/default/playback/actors/text-track-segment-loader.js +30 -16
  73. package/dist/default/playback/actors/text-track-segment-loader.js.map +1 -1
  74. package/dist/default/playback/behaviors/calculate-presentation-duration.js +21 -43
  75. package/dist/default/playback/behaviors/calculate-presentation-duration.js.map +1 -1
  76. package/dist/default/playback/behaviors/derive-cdn-priority.js +68 -0
  77. package/dist/default/playback/behaviors/derive-cdn-priority.js.map +1 -0
  78. package/dist/default/playback/behaviors/dom/airplay.js +211 -0
  79. package/dist/default/playback/behaviors/dom/airplay.js.map +1 -0
  80. package/dist/default/playback/behaviors/dom/apply-start-position.js +93 -0
  81. package/dist/default/playback/behaviors/dom/apply-start-position.js.map +1 -0
  82. package/dist/default/playback/behaviors/dom/end-of-stream.js +25 -102
  83. package/dist/default/playback/behaviors/dom/end-of-stream.js.map +1 -1
  84. package/dist/default/playback/behaviors/dom/load-segments.js +6 -2
  85. package/dist/default/playback/behaviors/dom/load-segments.js.map +1 -1
  86. package/dist/default/playback/behaviors/dom/recover-end-stall.js +77 -0
  87. package/dist/default/playback/behaviors/dom/recover-end-stall.js.map +1 -0
  88. package/dist/default/playback/behaviors/dom/setup-buffer-actors.js +50 -18
  89. package/dist/default/playback/behaviors/dom/setup-buffer-actors.js.map +1 -1
  90. package/dist/default/playback/behaviors/dom/setup-mediasource.js +36 -11
  91. package/dist/default/playback/behaviors/dom/setup-mediasource.js.map +1 -1
  92. package/dist/default/playback/behaviors/dom/setup-text-track-actors.js +11 -25
  93. package/dist/default/playback/behaviors/dom/setup-text-track-actors.js.map +1 -1
  94. package/dist/default/playback/behaviors/dom/sync-text-tracks.js +43 -18
  95. package/dist/default/playback/behaviors/dom/sync-text-tracks.js.map +1 -1
  96. package/dist/default/playback/behaviors/dom/track-current-time.js +6 -0
  97. package/dist/default/playback/behaviors/dom/track-current-time.js.map +1 -1
  98. package/dist/default/playback/behaviors/dom/track-load-triggers.js +4 -4
  99. package/dist/default/playback/behaviors/dom/track-load-triggers.js.map +1 -1
  100. package/dist/default/playback/behaviors/dom/track-playback-rate.js.map +1 -1
  101. package/dist/default/playback/behaviors/dom/update-mediasource-duration.js +3 -2
  102. package/dist/default/playback/behaviors/dom/update-mediasource-duration.js.map +1 -1
  103. package/dist/default/playback/behaviors/establish-start-media-time.js +123 -0
  104. package/dist/default/playback/behaviors/establish-start-media-time.js.map +1 -0
  105. package/dist/default/playback/behaviors/resolve-presentation.js.map +1 -1
  106. package/dist/default/playback/behaviors/resolve-track.js +45 -22
  107. package/dist/default/playback/behaviors/resolve-track.js.map +1 -1
  108. package/dist/default/playback/behaviors/select-tracks.js +46 -77
  109. package/dist/default/playback/behaviors/select-tracks.js.map +1 -1
  110. package/dist/default/playback/behaviors/setup-failover-monitor.js +66 -0
  111. package/dist/default/playback/behaviors/setup-failover-monitor.js.map +1 -0
  112. package/dist/default/playback/behaviors/sync-preload.js.map +1 -1
  113. package/dist/default/playback/behaviors/track-switching.js +403 -0
  114. package/dist/default/playback/behaviors/track-switching.js.map +1 -0
  115. package/dist/default/playback/engines/background-video/adapter.js +158 -0
  116. package/dist/default/playback/engines/background-video/adapter.js.map +1 -0
  117. package/dist/default/playback/engines/background-video/engine.js +75 -0
  118. package/dist/default/playback/engines/background-video/engine.js.map +1 -0
  119. package/dist/default/playback/engines/hls/adapter-audio-only.js +124 -0
  120. package/dist/default/playback/engines/hls/adapter-audio-only.js.map +1 -0
  121. package/dist/default/playback/engines/hls/adapter.js +20 -11
  122. package/dist/default/playback/engines/hls/adapter.js.map +1 -1
  123. package/dist/default/playback/engines/hls/engine-audio-only.js +94 -0
  124. package/dist/default/playback/engines/hls/engine-audio-only.js.map +1 -0
  125. package/dist/default/playback/engines/hls/engine.js +38 -8
  126. package/dist/default/playback/engines/hls/engine.js.map +1 -1
  127. package/dist/default/playback/primitives/failover-fetch.js +42 -0
  128. package/dist/default/playback/primitives/failover-fetch.js.map +1 -0
  129. package/dist/default/playback/primitives/head-peek.js +44 -0
  130. package/dist/default/playback/primitives/head-peek.js.map +1 -0
  131. package/dist/default/playback/primitives/relocation-pipelines.js +203 -0
  132. package/dist/default/playback/primitives/relocation-pipelines.js.map +1 -0
  133. package/dist/default/playback/primitives/segment-load-pipeline.js +96 -0
  134. package/dist/default/playback/primitives/segment-load-pipeline.js.map +1 -0
  135. package/dist/default/playback/primitives/text-segment-load-pipeline.js +37 -0
  136. package/dist/default/playback/primitives/text-segment-load-pipeline.js.map +1 -0
  137. package/dist/{dev/playback/behaviors → default/playback/primitives}/track-types.js +1 -1
  138. package/dist/default/playback/primitives/track-types.js.map +1 -0
  139. package/dist/dev/background-video.d.ts +3 -0
  140. package/dist/dev/background-video.js +3 -0
  141. package/dist/dev/core/actors/actor.d.ts +0 -1
  142. package/dist/dev/core/actors/actor.d.ts.map +1 -1
  143. package/dist/dev/core/actors/create-machine-actor.d.ts +9 -6
  144. package/dist/dev/core/actors/create-machine-actor.d.ts.map +1 -1
  145. package/dist/dev/core/actors/create-machine-actor.js.map +1 -1
  146. package/dist/dev/core/actors/create-transition-actor.d.ts +0 -1
  147. package/dist/dev/core/actors/create-transition-actor.d.ts.map +1 -1
  148. package/dist/dev/core/actors/create-transition-actor.js.map +1 -1
  149. package/dist/dev/core/composition/create-composition.d.ts +10 -11
  150. package/dist/dev/core/composition/create-composition.d.ts.map +1 -1
  151. package/dist/dev/core/composition/create-composition.js +2 -2
  152. package/dist/dev/core/composition/create-composition.js.map +1 -1
  153. package/dist/dev/core/composition/share-signals.d.ts +11 -10
  154. package/dist/dev/core/composition/share-signals.d.ts.map +1 -1
  155. package/dist/dev/core/composition/share-signals.js +13 -11
  156. package/dist/dev/core/composition/share-signals.js.map +1 -1
  157. package/dist/dev/core/machine.d.ts +1 -0
  158. package/dist/dev/core/machine.d.ts.map +1 -1
  159. package/dist/dev/core/machine.js.map +1 -1
  160. package/dist/dev/core/reactors/create-machine-reactor.d.ts +2 -2
  161. package/dist/dev/core/reactors/create-machine-reactor.d.ts.map +1 -1
  162. package/dist/dev/core/reactors/create-machine-reactor.js.map +1 -1
  163. package/dist/dev/core/signals/effect.d.ts.map +1 -1
  164. package/dist/dev/core/signals/effect.js.map +1 -1
  165. package/dist/dev/core/signals/primitives.d.ts +1 -2
  166. package/dist/dev/core/signals/primitives.d.ts.map +1 -1
  167. package/dist/dev/core/signals/primitives.js.map +1 -1
  168. package/dist/dev/core/tasks/task.d.ts +2 -2
  169. package/dist/dev/core/tasks/task.d.ts.map +1 -1
  170. package/dist/dev/core/tasks/task.js.map +1 -1
  171. package/dist/dev/hls.d.ts +5 -1
  172. package/dist/dev/hls.js +4 -1
  173. package/dist/dev/index.d.ts +0 -1
  174. package/dist/dev/index.d.ts.map +1 -1
  175. package/dist/dev/media/abr/quality-selection.d.ts.map +1 -1
  176. package/dist/dev/media/abr/quality-selection.js +6 -68
  177. package/dist/dev/media/abr/quality-selection.js.map +1 -1
  178. package/dist/dev/media/buffer/back-buffer.d.ts.map +1 -1
  179. package/dist/dev/media/buffer/back-buffer.js.map +1 -1
  180. package/dist/dev/media/buffer/forward-buffer.d.ts.map +1 -1
  181. package/dist/dev/media/buffer/forward-buffer.js +37 -3
  182. package/dist/dev/media/buffer/forward-buffer.js.map +1 -1
  183. package/dist/dev/media/dom/capabilities.js +70 -0
  184. package/dist/dev/media/dom/capabilities.js.map +1 -0
  185. package/dist/dev/media/dom/mse/append-segment.d.ts +3 -9
  186. package/dist/dev/media/dom/mse/append-segment.d.ts.map +1 -1
  187. package/dist/dev/media/dom/mse/append-segment.js.map +1 -1
  188. package/dist/dev/media/dom/mse/buffer-flusher.d.ts.map +1 -1
  189. package/dist/dev/media/dom/mse/buffer-flusher.js.map +1 -1
  190. package/dist/dev/media/dom/mse/duration.js +27 -10
  191. package/dist/dev/media/dom/mse/duration.js.map +1 -1
  192. package/dist/dev/media/dom/mse/end-of-stream.js.map +1 -1
  193. package/dist/dev/media/dom/mse/mediasource-setup.js +68 -17
  194. package/dist/dev/media/dom/mse/mediasource-setup.js.map +1 -1
  195. package/dist/dev/media/dom/text/resolve-vtt-segment.d.ts +0 -6
  196. package/dist/dev/media/dom/text/resolve-vtt-segment.d.ts.map +1 -1
  197. package/dist/dev/media/dom/text/resolve-vtt-segment.js +1 -7
  198. package/dist/dev/media/dom/text/resolve-vtt-segment.js.map +1 -1
  199. package/dist/dev/media/dom/text/text-track-slots.d.ts +0 -1
  200. package/dist/dev/media/dom/text/text-track-slots.d.ts.map +1 -1
  201. package/dist/dev/media/dom/text/text-track-slots.js +3 -3
  202. package/dist/dev/media/dom/text/text-track-slots.js.map +1 -1
  203. package/dist/dev/media/hls/parse-attributes.js +17 -3
  204. package/dist/dev/media/hls/parse-attributes.js.map +1 -1
  205. package/dist/dev/media/hls/parse-media-playlist.js +29 -1
  206. package/dist/dev/media/hls/parse-media-playlist.js.map +1 -1
  207. package/dist/dev/media/hls/parse-multivariant.js +15 -6
  208. package/dist/dev/media/hls/parse-multivariant.js.map +1 -1
  209. package/dist/dev/media/hls/resolve-url.js.map +1 -1
  210. package/dist/dev/media/media-tracks/media-tracks.d.ts +51 -0
  211. package/dist/dev/media/media-tracks/media-tracks.d.ts.map +1 -0
  212. package/dist/dev/media/media-tracks/media-tracks.js +101 -0
  213. package/dist/dev/media/media-tracks/media-tracks.js.map +1 -0
  214. package/dist/dev/media/mp4/box.js +58 -0
  215. package/dist/dev/media/mp4/box.js.map +1 -0
  216. package/dist/dev/media/mp4/timestamp-origin.js +107 -0
  217. package/dist/dev/media/mp4/timestamp-origin.js.map +1 -0
  218. package/dist/dev/media/primitives/select-tracks.d.ts +35 -0
  219. package/dist/dev/media/primitives/select-tracks.d.ts.map +1 -0
  220. package/dist/dev/media/primitives/select-tracks.js +98 -13
  221. package/dist/dev/media/primitives/select-tracks.js.map +1 -1
  222. package/dist/dev/media/text/parse-vtt-timestamp-map.js +42 -0
  223. package/dist/dev/media/text/parse-vtt-timestamp-map.js.map +1 -0
  224. package/dist/dev/media/text/resolve-vtt-metadata.js +23 -0
  225. package/dist/dev/media/text/resolve-vtt-metadata.js.map +1 -0
  226. package/dist/dev/media/types/index.d.ts +69 -2
  227. package/dist/dev/media/types/index.d.ts.map +1 -1
  228. package/dist/dev/media/types/index.js.map +1 -1
  229. package/dist/dev/media/utils/cdn.d.ts +13 -0
  230. package/dist/dev/media/utils/cdn.d.ts.map +1 -0
  231. package/dist/dev/media/utils/cdn.js +56 -0
  232. package/dist/dev/media/utils/cdn.js.map +1 -0
  233. package/dist/dev/media/utils/preload.js.map +1 -1
  234. package/dist/dev/media/utils/track-selection.js +1 -1
  235. package/dist/dev/media/utils/track-selection.js.map +1 -1
  236. package/dist/dev/media/utils/tracks.js +31 -4
  237. package/dist/dev/media/utils/tracks.js.map +1 -1
  238. package/dist/dev/media-tracks.d.ts +3 -0
  239. package/dist/dev/media-tracks.js +2 -0
  240. package/dist/dev/network/bandwidth-estimator.d.ts.map +1 -1
  241. package/dist/dev/network/bandwidth-estimator.js.map +1 -1
  242. package/dist/dev/network/chunked-stream-iterable.js.map +1 -1
  243. package/dist/dev/network/ewma.js +1 -1
  244. package/dist/dev/network/ewma.js.map +1 -1
  245. package/dist/dev/network/fetch.js +7 -1
  246. package/dist/dev/network/fetch.js.map +1 -1
  247. package/dist/dev/playback/actors/dom/segment-loader.d.ts +16 -4
  248. package/dist/dev/playback/actors/dom/segment-loader.d.ts.map +1 -1
  249. package/dist/dev/playback/actors/dom/segment-loader.js +103 -106
  250. package/dist/dev/playback/actors/dom/segment-loader.js.map +1 -1
  251. package/dist/dev/playback/actors/dom/source-buffer.d.ts +10 -25
  252. package/dist/dev/playback/actors/dom/source-buffer.d.ts.map +1 -1
  253. package/dist/dev/playback/actors/dom/source-buffer.js +3 -1
  254. package/dist/dev/playback/actors/dom/source-buffer.js.map +1 -1
  255. package/dist/dev/playback/actors/dom/text-tracks.d.ts +3 -1
  256. package/dist/dev/playback/actors/dom/text-tracks.js.map +1 -1
  257. package/dist/dev/playback/actors/text-track-segment-loader.d.ts +7 -12
  258. package/dist/dev/playback/actors/text-track-segment-loader.d.ts.map +1 -1
  259. package/dist/dev/playback/actors/text-track-segment-loader.js +30 -16
  260. package/dist/dev/playback/actors/text-track-segment-loader.js.map +1 -1
  261. package/dist/dev/playback/actors/text-tracks.d.ts +3 -12
  262. package/dist/dev/playback/actors/text-tracks.d.ts.map +1 -1
  263. package/dist/dev/playback/behaviors/calculate-presentation-duration.d.ts +11 -2
  264. package/dist/dev/playback/behaviors/calculate-presentation-duration.d.ts.map +1 -1
  265. package/dist/dev/playback/behaviors/calculate-presentation-duration.js +21 -43
  266. package/dist/dev/playback/behaviors/calculate-presentation-duration.js.map +1 -1
  267. package/dist/dev/playback/behaviors/derive-cdn-priority.js +68 -0
  268. package/dist/dev/playback/behaviors/derive-cdn-priority.js.map +1 -0
  269. package/dist/dev/playback/behaviors/dom/airplay.js +211 -0
  270. package/dist/dev/playback/behaviors/dom/airplay.js.map +1 -0
  271. package/dist/dev/playback/behaviors/dom/apply-start-position.js +93 -0
  272. package/dist/dev/playback/behaviors/dom/apply-start-position.js.map +1 -0
  273. package/dist/dev/playback/behaviors/dom/end-of-stream.js +25 -102
  274. package/dist/dev/playback/behaviors/dom/end-of-stream.js.map +1 -1
  275. package/dist/dev/playback/behaviors/dom/load-segments.d.ts +14 -4
  276. package/dist/dev/playback/behaviors/dom/load-segments.d.ts.map +1 -1
  277. package/dist/dev/playback/behaviors/dom/load-segments.js +6 -2
  278. package/dist/dev/playback/behaviors/dom/load-segments.js.map +1 -1
  279. package/dist/dev/playback/behaviors/dom/recover-end-stall.js +77 -0
  280. package/dist/dev/playback/behaviors/dom/recover-end-stall.js.map +1 -0
  281. package/dist/dev/playback/behaviors/dom/setup-buffer-actors.js +50 -18
  282. package/dist/dev/playback/behaviors/dom/setup-buffer-actors.js.map +1 -1
  283. package/dist/dev/playback/behaviors/dom/setup-mediasource.js +36 -11
  284. package/dist/dev/playback/behaviors/dom/setup-mediasource.js.map +1 -1
  285. package/dist/dev/playback/behaviors/dom/setup-text-track-actors.d.ts +16 -19
  286. package/dist/dev/playback/behaviors/dom/setup-text-track-actors.d.ts.map +1 -1
  287. package/dist/dev/playback/behaviors/dom/setup-text-track-actors.js +11 -25
  288. package/dist/dev/playback/behaviors/dom/setup-text-track-actors.js.map +1 -1
  289. package/dist/dev/playback/behaviors/dom/sync-text-tracks.js +43 -18
  290. package/dist/dev/playback/behaviors/dom/sync-text-tracks.js.map +1 -1
  291. package/dist/dev/playback/behaviors/dom/track-current-time.d.ts +1 -2
  292. package/dist/dev/playback/behaviors/dom/track-current-time.d.ts.map +1 -1
  293. package/dist/dev/playback/behaviors/dom/track-current-time.js +6 -0
  294. package/dist/dev/playback/behaviors/dom/track-current-time.js.map +1 -1
  295. package/dist/dev/playback/behaviors/dom/track-load-triggers.d.ts +1 -2
  296. package/dist/dev/playback/behaviors/dom/track-load-triggers.d.ts.map +1 -1
  297. package/dist/dev/playback/behaviors/dom/track-load-triggers.js +4 -4
  298. package/dist/dev/playback/behaviors/dom/track-load-triggers.js.map +1 -1
  299. package/dist/dev/playback/behaviors/dom/track-playback-rate.d.ts +1 -2
  300. package/dist/dev/playback/behaviors/dom/track-playback-rate.d.ts.map +1 -1
  301. package/dist/dev/playback/behaviors/dom/track-playback-rate.js.map +1 -1
  302. package/dist/dev/playback/behaviors/dom/update-mediasource-duration.js +3 -2
  303. package/dist/dev/playback/behaviors/dom/update-mediasource-duration.js.map +1 -1
  304. package/dist/dev/playback/behaviors/establish-start-media-time.d.ts +31 -0
  305. package/dist/dev/playback/behaviors/establish-start-media-time.d.ts.map +1 -0
  306. package/dist/dev/playback/behaviors/establish-start-media-time.js +123 -0
  307. package/dist/dev/playback/behaviors/establish-start-media-time.js.map +1 -0
  308. package/dist/dev/playback/behaviors/resolve-presentation.d.ts +2 -1
  309. package/dist/dev/playback/behaviors/resolve-presentation.d.ts.map +1 -1
  310. package/dist/dev/playback/behaviors/resolve-presentation.js.map +1 -1
  311. package/dist/dev/playback/behaviors/resolve-track.js +45 -22
  312. package/dist/dev/playback/behaviors/resolve-track.js.map +1 -1
  313. package/dist/dev/playback/behaviors/select-tracks.d.ts +14 -0
  314. package/dist/dev/playback/behaviors/select-tracks.d.ts.map +1 -0
  315. package/dist/dev/playback/behaviors/select-tracks.js +46 -77
  316. package/dist/dev/playback/behaviors/select-tracks.js.map +1 -1
  317. package/dist/dev/playback/behaviors/setup-failover-monitor.d.ts +14 -0
  318. package/dist/dev/playback/behaviors/setup-failover-monitor.d.ts.map +1 -0
  319. package/dist/dev/playback/behaviors/setup-failover-monitor.js +66 -0
  320. package/dist/dev/playback/behaviors/setup-failover-monitor.js.map +1 -0
  321. package/dist/dev/playback/behaviors/sync-preload.js.map +1 -1
  322. package/dist/dev/playback/behaviors/track-switching.js +403 -0
  323. package/dist/dev/playback/behaviors/track-switching.js.map +1 -0
  324. package/dist/dev/playback/engines/background-video/adapter.d.ts +59 -0
  325. package/dist/dev/playback/engines/background-video/adapter.d.ts.map +1 -0
  326. package/dist/dev/playback/engines/background-video/adapter.js +158 -0
  327. package/dist/dev/playback/engines/background-video/adapter.js.map +1 -0
  328. package/dist/dev/playback/engines/background-video/engine.d.ts +106 -0
  329. package/dist/dev/playback/engines/background-video/engine.d.ts.map +1 -0
  330. package/dist/dev/playback/engines/background-video/engine.js +75 -0
  331. package/dist/dev/playback/engines/background-video/engine.js.map +1 -0
  332. package/dist/dev/playback/engines/hls/adapter-audio-only.d.ts +46 -0
  333. package/dist/dev/playback/engines/hls/adapter-audio-only.d.ts.map +1 -0
  334. package/dist/dev/playback/engines/hls/adapter-audio-only.js +124 -0
  335. package/dist/dev/playback/engines/hls/adapter-audio-only.js.map +1 -0
  336. package/dist/dev/playback/engines/hls/adapter.d.ts +3 -5
  337. package/dist/dev/playback/engines/hls/adapter.d.ts.map +1 -1
  338. package/dist/dev/playback/engines/hls/adapter.js +20 -11
  339. package/dist/dev/playback/engines/hls/adapter.js.map +1 -1
  340. package/dist/dev/playback/engines/hls/engine-audio-only.d.ts +156 -0
  341. package/dist/dev/playback/engines/hls/engine-audio-only.d.ts.map +1 -0
  342. package/dist/dev/playback/engines/hls/engine-audio-only.js +94 -0
  343. package/dist/dev/playback/engines/hls/engine-audio-only.js.map +1 -0
  344. package/dist/dev/playback/engines/hls/engine.d.ts +109 -4
  345. package/dist/dev/playback/engines/hls/engine.d.ts.map +1 -1
  346. package/dist/dev/playback/engines/hls/engine.js +38 -8
  347. package/dist/dev/playback/engines/hls/engine.js.map +1 -1
  348. package/dist/dev/playback/primitives/derive-start-media-time.d.ts +16 -0
  349. package/dist/dev/playback/primitives/derive-start-media-time.d.ts.map +1 -0
  350. package/dist/dev/playback/primitives/failover-fetch.js +42 -0
  351. package/dist/dev/playback/primitives/failover-fetch.js.map +1 -0
  352. package/dist/dev/playback/primitives/head-peek.js +44 -0
  353. package/dist/dev/playback/primitives/head-peek.js.map +1 -0
  354. package/dist/dev/playback/primitives/relocation-pipelines.js +203 -0
  355. package/dist/dev/playback/primitives/relocation-pipelines.js.map +1 -0
  356. package/dist/dev/playback/primitives/segment-load-pipeline.d.ts +2 -0
  357. package/dist/dev/playback/primitives/segment-load-pipeline.js +96 -0
  358. package/dist/dev/playback/primitives/segment-load-pipeline.js.map +1 -0
  359. package/dist/dev/playback/primitives/source-buffer-messages.d.ts +43 -0
  360. package/dist/dev/playback/primitives/source-buffer-messages.d.ts.map +1 -0
  361. package/dist/dev/playback/primitives/text-segment-load-pipeline.d.ts +65 -0
  362. package/dist/dev/playback/primitives/text-segment-load-pipeline.d.ts.map +1 -0
  363. package/dist/dev/playback/primitives/text-segment-load-pipeline.js +37 -0
  364. package/dist/dev/playback/primitives/text-segment-load-pipeline.js.map +1 -0
  365. package/dist/dev/playback/primitives/text-track-messages.d.ts +14 -0
  366. package/dist/dev/playback/primitives/text-track-messages.d.ts.map +1 -0
  367. package/dist/{default/playback/behaviors → dev/playback/primitives}/track-types.js +1 -1
  368. package/dist/dev/playback/primitives/track-types.js.map +1 -0
  369. package/package.json +17 -7
  370. package/dist/default/playback/behaviors/quality-switching.js +0 -96
  371. package/dist/default/playback/behaviors/quality-switching.js.map +0 -1
  372. package/dist/default/playback/behaviors/track-types.js.map +0 -1
  373. package/dist/dev/playback/behaviors/quality-switching.js +0 -96
  374. package/dist/dev/playback/behaviors/quality-switching.js.map +0 -1
  375. package/dist/dev/playback/behaviors/track-types.js.map +0 -1
@@ -3,7 +3,7 @@ import { defineBehavior } from "../../../core/composition/create-composition.js"
3
3
  import { createMachineReactor } from "../../../core/reactors/create-machine-reactor.js";
4
4
  import { DEFAULT_FORWARD_BUFFER_CONFIG, segmentStartForTime } from "../../../media/buffer/forward-buffer.js";
5
5
  import { findResolvedAudioTrack, findResolvedTextTrack, findResolvedVideoTrack } from "../../../media/utils/tracks.js";
6
- import { AUDIO_TYPE_CONFIG, TEXT_TYPE_CONFIG, VIDEO_TYPE_CONFIG } from "../track-types.js";
6
+ import { AUDIO_TYPE_CONFIG, TEXT_TYPE_CONFIG, VIDEO_TYPE_CONFIG } from "../../primitives/track-types.js";
7
7
  //#region src/playback/behaviors/dom/load-segments.ts
8
8
  /**
9
9
  * **Per-type segment loading dispatch.** Per available track type (video /
@@ -26,7 +26,10 @@ import { AUDIO_TYPE_CONFIG, TEXT_TYPE_CONFIG, VIDEO_TYPE_CONFIG } from "../track
26
26
  *
27
27
  * - `'preconditions-unmet'` — no loader actor in context, or the selected
28
28
  * track hasn't resolved.
29
- * - `'dormant'` — `preload === 'none' && !loadActivated`. Nothing fires.
29
+ * - `'dormant'` — loading disabled by policy: an observed `loadingSuspended`
30
+ * (highest precedence) or `preload === 'none' && !loadActivated`. Nothing
31
+ * fires; already-queued loader work drains. Auto-resumes into the derived
32
+ * state when the policy lifts.
30
33
  * - `'metadata-only'` — `!loadActivated && preload !== 'auto' && preload !== 'none'`.
31
34
  * Fires an init-segment-only `load` message **once on entry**. The
32
35
  * variant's loader actor decides what to do — v/a's actor fetches the
@@ -71,6 +74,7 @@ function setupSegmentLoading({ state, context, config }) {
71
74
  return segmentStartForTime(state.currentTime.get() ?? 0, track.segments);
72
75
  });
73
76
  const derivedStateSignal = computed(() => {
77
+ if (state.loadingSuspended?.get()) return "dormant";
74
78
  if (!context[loaderKey].get() || !selectedTrack.get()) return "preconditions-unmet";
75
79
  if (state.loadActivated.get() || state.preload.get() === "auto") return "full-range";
76
80
  if (state.preload.get() === "none") return "dormant";
@@ -1 +1 @@
1
- {"version":3,"file":"load-segments.js","names":[],"sources":["../../../../../src/playback/behaviors/dom/load-segments.ts"],"sourcesContent":["/**\n * **Per-type segment loading dispatch.** Per available track type (video /\n * audio / text), reads the per-type segment-loader actor from context and\n * sends typed `'load'` messages whenever a meaningful loading condition\n * changes (selected track, current time crossing a segment boundary).\n *\n * Loader-actor lifecycle is owned upstream:\n * - Video: `setupVideoBufferActors`\n * - Audio: `setupAudioBufferActors`\n * - Text: `setupTextTrackActors`\n *\n * This behavior is pure-consumer: it reads `context[loaderKey]` and\n * dispatches typed messages via the variant's per-type `findResolvedTrack`\n * resolver.\n *\n * # Load modes as reactor states\n *\n * Four states encode the load-gating policy directly:\n *\n * - `'preconditions-unmet'` — no loader actor in context, or the selected\n * track hasn't resolved.\n * - `'dormant'` — `preload === 'none' && !loadActivated`. Nothing fires.\n * - `'metadata-only'` — `!loadActivated && preload !== 'auto' && preload !== 'none'`.\n * Fires an init-segment-only `load` message **once on entry**. The\n * variant's loader actor decides what to do — v/a's actor fetches the\n * init segment; text's actor no-ops (no init concept).\n * - `'full-range'` — `loadActivated || preload === 'auto'`. Effect re-fires\n * on selected-track change and on segment-boundary crossing.\n *\n * # Per-type parameterization (inference-driven)\n *\n * The helper is generic over `Track` — the resolved-track type. Each\n * variant supplies its own `findResolvedTrack` resolver via config; TS\n * infers `Track` from the resolver's return type. The loader signal's\n * value type is constrained to `SegmentLoaderLike<Track>` — anything with\n * a `send` method accepting the `Track`-parameterized message. Concrete\n * actor types (`SegmentLoaderActor`, `TextTrackSegmentLoaderActor`)\n * satisfy this via function-parameter contravariance: an actor whose\n * `.send` accepts a wider track type is assignable to a slot expecting a\n * narrower track type.\n *\n * No widening of actor message types is needed; no casts inside the\n * helper. Per-variant wiring (right loader paired with right resolver)\n * is enforced at the variant call site.\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, peek, type ReadonlySignal } from '../../../core/signals/primitives';\nimport {\n DEFAULT_FORWARD_BUFFER_CONFIG,\n type ForwardBufferConfig,\n segmentStartForTime,\n} from '../../../media/buffer/forward-buffer';\nimport type { MaybeResolvedPresentation, Segment } from '../../../media/types';\nimport { findResolvedAudioTrack, findResolvedTextTrack, findResolvedVideoTrack } from '../../../media/utils/tracks';\nimport type { BufferState, SegmentLoaderActor, SourceBufferState } from '../../actors/dom/segment-loader';\nimport type { TextTrackSegmentLoaderActor } from '../../actors/text-track-segment-loader';\nimport { AUDIO_TYPE_CONFIG, TEXT_TYPE_CONFIG, VIDEO_TYPE_CONFIG } from '../track-types';\n\n// Re-export buffer state types for consumers that import them from this module.\nexport type { BufferState, SourceBufferState };\n\n// ============================================================================\n// STATE & CONTEXT\n// ============================================================================\n\n/** State shape for segment loading. */\nexport interface SegmentLoadingState {\n presentation?: MaybeResolvedPresentation;\n preload?: string;\n /** Current playback position in seconds. Defaults to 0 when undefined. */\n currentTime?: number;\n /** True once a preload-overriding event has fired for the current source. */\n loadActivated?: boolean;\n selectedVideoTrackId?: string;\n selectedAudioTrackId?: string;\n selectedTextTrackId?: string;\n}\n\n/** Context shape for segment loading. Each variant only consumes its own type's loader actor. */\nexport interface SegmentLoadingContext {\n videoSegmentLoaderActor?: SegmentLoaderActor;\n audioSegmentLoaderActor?: SegmentLoaderActor;\n textTrackSegmentLoaderActor?: TextTrackSegmentLoaderActor;\n}\n\n// ============================================================================\n// REACTOR\n// ============================================================================\n\ntype SegmentLoadingFsmState = 'preconditions-unmet' | 'dormant' | 'metadata-only' | 'full-range';\n\ntype SelectedTrackKey = 'selectedVideoTrackId' | 'selectedAudioTrackId' | 'selectedTextTrackId';\ntype SegmentLoaderActorKey = 'videoSegmentLoaderActor' | 'audioSegmentLoaderActor' | 'textTrackSegmentLoaderActor';\n\ntype SegmentLoadingStateMap<K extends SelectedTrackKey> = {\n presentation: ReadonlySignal<SegmentLoadingState['presentation']>;\n preload: ReadonlySignal<SegmentLoadingState['preload']>;\n currentTime: ReadonlySignal<SegmentLoadingState['currentTime']>;\n loadActivated: ReadonlySignal<SegmentLoadingState['loadActivated']>;\n} & { [P in K]: ReadonlySignal<SegmentLoadingState[P]> };\n\n/** Shared `'load'` message shape parameterized over the resolved track type. */\ninterface LoadMessage<Track> {\n type: 'load';\n track: Track;\n range?: { start: number; end: number };\n}\n\n/** Structural constraint for any segment-loader-style actor. */\ninterface SegmentLoaderLike<Track> {\n send: (message: LoadMessage<Track>) => void;\n}\n\n/**\n * Specialization helper. Generic over `Track` (inferred from\n * `findResolvedTrack`'s return type). The loader value type is\n * constrained to `SegmentLoaderLike<Track>` — concrete actor types\n * (whose `.send` accepts a wider track union) satisfy this via\n * function-parameter contravariance.\n *\n * Tracks that the helper handles must have a `segments` field — used by\n * `segmentBoundarySignal` to compute the load-anchor boundary. Each\n * variant's resolver narrows to the right resolved-track shape.\n */\nfunction setupSegmentLoading<\n K extends SelectedTrackKey,\n L extends SegmentLoaderActorKey,\n Track extends { segments: readonly Segment[] },\n>({\n state,\n context,\n config,\n}: {\n state: SegmentLoadingStateMap<K>;\n context: { [P in L]: ReadonlySignal<SegmentLoaderLike<Track> | undefined> };\n config: {\n selectedKey: K;\n loaderKey: L;\n findResolvedTrack: (\n presentation: MaybeResolvedPresentation | undefined,\n trackId: string | undefined\n ) => Track | undefined;\n forwardBuffer?: Partial<ForwardBufferConfig>;\n };\n}): Reactor<SegmentLoadingFsmState | 'destroying' | 'destroyed'> {\n const { selectedKey, loaderKey, findResolvedTrack } = config;\n const bufferDuration = config.forwardBuffer?.bufferDuration ?? DEFAULT_FORWARD_BUFFER_CONFIG.bufferDuration;\n\n const selectedTrack = computed<Track | undefined>(() =>\n findResolvedTrack(state.presentation.get(), state[selectedKey].get())\n );\n\n // Segment-boundary signal — `segmentStartForTime` returns the same number\n // while `currentTime` stays inside one segment, so signal-polyfill's\n // `Object.is` equality on this computed dedups within-segment ticks.\n const segmentBoundarySignal = computed(() => {\n const track = selectedTrack.get();\n if (!track) return undefined;\n return segmentStartForTime(state.currentTime.get() ?? 0, track.segments);\n });\n\n const derivedStateSignal = computed<SegmentLoadingFsmState>(() => {\n if (!context[loaderKey].get() || !selectedTrack.get()) return 'preconditions-unmet';\n if (state.loadActivated.get() || state.preload.get() === 'auto') return 'full-range';\n if (state.preload.get() === 'none') return 'dormant';\n return 'metadata-only';\n });\n\n return createMachineReactor<SegmentLoadingFsmState>({\n initial: 'preconditions-unmet',\n monitor: () => derivedStateSignal.get(),\n states: {\n 'preconditions-unmet': {},\n dormant: {},\n 'metadata-only': {\n // Fires once on entry. Matches HTMLMediaElement's preload='metadata'\n // semantics — load enough to surface metadata for the entry-time\n // selection. Each variant's loader decides what to do: v/a's actor\n // fetches the init segment; text's actor treats no-range as a\n // no-op. Selection changes while still in `'metadata-only'` are\n // intentionally not followed.\n entry: () => {\n const track = selectedTrack.get()!;\n context[loaderKey].get()!.send({ type: 'load', track });\n },\n },\n 'full-range': {\n // Re-fires on selected-track changes and on segment-boundary\n // crossings. The loader and `currentTime` are peeked — the\n // boundary-dedup signal already handles re-firing policy.\n effects: () => {\n const track = selectedTrack.get()!;\n segmentBoundarySignal.get();\n const currentTime = peek(state.currentTime) ?? 0;\n peek(context[loaderKey])!.send({\n type: 'load',\n track,\n range: { start: currentTime, end: currentTime + bufferDuration },\n });\n },\n },\n },\n });\n}\n\n// ============================================================================\n// Per-helper-per-type configs — defaults that variants spread engine config over\n// ============================================================================\n\nconst VIDEO_SEGMENT_LOADING_CONFIG = {\n ...VIDEO_TYPE_CONFIG,\n findResolvedTrack: findResolvedVideoTrack,\n} as const;\n\nconst AUDIO_SEGMENT_LOADING_CONFIG = {\n ...AUDIO_TYPE_CONFIG,\n findResolvedTrack: findResolvedAudioTrack,\n} as const;\n\nconst TEXT_SEGMENT_LOADING_CONFIG = {\n ...TEXT_TYPE_CONFIG,\n findResolvedTrack: findResolvedTextTrack,\n} as const;\n\n// ============================================================================\n// Specialized exports — one per media type\n// ============================================================================\n\nexport const loadVideoSegments = defineBehavior({\n stateKeys: ['presentation', 'preload', 'currentTime', 'loadActivated', 'selectedVideoTrackId'],\n contextKeys: ['videoSegmentLoaderActor'],\n setup: ({\n state,\n context,\n config = {},\n }: {\n state: SegmentLoadingStateMap<'selectedVideoTrackId'>;\n context: {\n videoSegmentLoaderActor: ReadonlySignal<SegmentLoadingContext['videoSegmentLoaderActor']>;\n };\n config?: object;\n }) =>\n setupSegmentLoading({\n state,\n context,\n config: { ...VIDEO_SEGMENT_LOADING_CONFIG, ...config },\n }),\n});\n\nexport const loadAudioSegments = defineBehavior({\n stateKeys: ['presentation', 'preload', 'currentTime', 'loadActivated', 'selectedAudioTrackId'],\n contextKeys: ['audioSegmentLoaderActor'],\n setup: ({\n state,\n context,\n config = {},\n }: {\n state: SegmentLoadingStateMap<'selectedAudioTrackId'>;\n context: {\n audioSegmentLoaderActor: ReadonlySignal<SegmentLoadingContext['audioSegmentLoaderActor']>;\n };\n config?: object;\n }) =>\n setupSegmentLoading({\n state,\n context,\n config: { ...AUDIO_SEGMENT_LOADING_CONFIG, ...config },\n }),\n});\n\nexport const loadTextTrackSegments = defineBehavior({\n stateKeys: ['presentation', 'preload', 'currentTime', 'loadActivated', 'selectedTextTrackId'],\n contextKeys: ['textTrackSegmentLoaderActor'],\n setup: ({\n state,\n context,\n config = {},\n }: {\n state: SegmentLoadingStateMap<'selectedTextTrackId'>;\n context: {\n textTrackSegmentLoaderActor: ReadonlySignal<SegmentLoadingContext['textTrackSegmentLoaderActor']>;\n };\n config?: object;\n }) =>\n setupSegmentLoading({\n state,\n context,\n config: { ...TEXT_SEGMENT_LOADING_CONFIG, ...config },\n }),\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8HA,SAAS,oBAIP,EACA,OACA,SACA,UAa+D;CAC/D,MAAM,EAAE,aAAa,WAAW,sBAAsB;CACtD,MAAM,iBAAiB,OAAO,eAAe,kBAAkB,8BAA8B;CAE7F,MAAM,gBAAgB,eACpB,kBAAkB,MAAM,aAAa,KAAK,EAAE,MAAM,aAAa,KAAK,CAAC,CACtE;CAKD,MAAM,wBAAwB,eAAe;EAC3C,MAAM,QAAQ,cAAc,KAAK;AACjC,MAAI,CAAC,MAAO,QAAO,KAAA;AACnB,SAAO,oBAAoB,MAAM,YAAY,KAAK,IAAI,GAAG,MAAM,SAAS;GACxE;CAEF,MAAM,qBAAqB,eAAuC;AAChE,MAAI,CAAC,QAAQ,WAAW,KAAK,IAAI,CAAC,cAAc,KAAK,CAAE,QAAO;AAC9D,MAAI,MAAM,cAAc,KAAK,IAAI,MAAM,QAAQ,KAAK,KAAK,OAAQ,QAAO;AACxE,MAAI,MAAM,QAAQ,KAAK,KAAK,OAAQ,QAAO;AAC3C,SAAO;GACP;AAEF,QAAO,qBAA6C;EAClD,SAAS;EACT,eAAe,mBAAmB,KAAK;EACvC,QAAQ;GACN,uBAAuB,EAAE;GACzB,SAAS,EAAE;GACX,iBAAiB,EAOf,aAAa;IACX,MAAM,QAAQ,cAAc,KAAK;AACjC,YAAQ,WAAW,KAAK,CAAE,KAAK;KAAE,MAAM;KAAQ;KAAO,CAAC;MAE1D;GACD,cAAc,EAIZ,eAAe;IACb,MAAM,QAAQ,cAAc,KAAK;AACjC,0BAAsB,KAAK;IAC3B,MAAM,cAAc,KAAK,MAAM,YAAY,IAAI;AAC/C,SAAK,QAAQ,WAAW,CAAE,KAAK;KAC7B,MAAM;KACN;KACA,OAAO;MAAE,OAAO;MAAa,KAAK,cAAc;MAAgB;KACjE,CAAC;MAEL;GACF;EACF,CAAC;;AAOJ,MAAM,+BAA+B;CACnC,GAAG;CACH,mBAAmB;CACpB;AAED,MAAM,+BAA+B;CACnC,GAAG;CACH,mBAAmB;CACpB;AAED,MAAM,8BAA8B;CAClC,GAAG;CACH,mBAAmB;CACpB;AAMD,MAAa,oBAAoB,eAAe;CAC9C,WAAW;EAAC;EAAgB;EAAW;EAAe;EAAiB;EAAuB;CAC9F,aAAa,CAAC,0BAA0B;CACxC,QAAQ,EACN,OACA,SACA,SAAS,EAAE,OAQX,oBAAoB;EAClB;EACA;EACA,QAAQ;GAAE,GAAG;GAA8B,GAAG;GAAQ;EACvD,CAAC;CACL,CAAC;AAEF,MAAa,oBAAoB,eAAe;CAC9C,WAAW;EAAC;EAAgB;EAAW;EAAe;EAAiB;EAAuB;CAC9F,aAAa,CAAC,0BAA0B;CACxC,QAAQ,EACN,OACA,SACA,SAAS,EAAE,OAQX,oBAAoB;EAClB;EACA;EACA,QAAQ;GAAE,GAAG;GAA8B,GAAG;GAAQ;EACvD,CAAC;CACL,CAAC;AAEF,MAAa,wBAAwB,eAAe;CAClD,WAAW;EAAC;EAAgB;EAAW;EAAe;EAAiB;EAAsB;CAC7F,aAAa,CAAC,8BAA8B;CAC5C,QAAQ,EACN,OACA,SACA,SAAS,EAAE,OAQX,oBAAoB;EAClB;EACA;EACA,QAAQ;GAAE,GAAG;GAA6B,GAAG;GAAQ;EACtD,CAAC;CACL,CAAC"}
1
+ {"version":3,"file":"load-segments.js","names":[],"sources":["../../../../../src/playback/behaviors/dom/load-segments.ts"],"sourcesContent":["/**\n * **Per-type segment loading dispatch.** Per available track type (video /\n * audio / text), reads the per-type segment-loader actor from context and\n * sends typed `'load'` messages whenever a meaningful loading condition\n * changes (selected track, current time crossing a segment boundary).\n *\n * Loader-actor lifecycle is owned upstream:\n * - Video: `setupVideoBufferActors`\n * - Audio: `setupAudioBufferActors`\n * - Text: `setupTextTrackActors`\n *\n * This behavior is pure-consumer: it reads `context[loaderKey]` and\n * dispatches typed messages via the variant's per-type `findResolvedTrack`\n * resolver.\n *\n * # Load modes as reactor states\n *\n * Four states encode the load-gating policy directly:\n *\n * - `'preconditions-unmet'` — no loader actor in context, or the selected\n * track hasn't resolved.\n * - `'dormant'` — loading disabled by policy: an observed `loadingSuspended`\n * (highest precedence) or `preload === 'none' && !loadActivated`. Nothing\n * fires; already-queued loader work drains. Auto-resumes into the derived\n * state when the policy lifts.\n * - `'metadata-only'` — `!loadActivated && preload !== 'auto' && preload !== 'none'`.\n * Fires an init-segment-only `load` message **once on entry**. The\n * variant's loader actor decides what to do — v/a's actor fetches the\n * init segment; text's actor no-ops (no init concept).\n * - `'full-range'` — `loadActivated || preload === 'auto'`. Effect re-fires\n * on selected-track change and on segment-boundary crossing.\n *\n * # Per-type parameterization (inference-driven)\n *\n * The helper is generic over `Track` — the resolved-track type. Each\n * variant supplies its own `findResolvedTrack` resolver via config; TS\n * infers `Track` from the resolver's return type. The loader signal's\n * value type is constrained to `SegmentLoaderLike<Track>` — anything with\n * a `send` method accepting the `Track`-parameterized message. Concrete\n * actor types (`SegmentLoaderActor`, `TextTrackSegmentLoaderActor`)\n * satisfy this via function-parameter contravariance: an actor whose\n * `.send` accepts a wider track type is assignable to a slot expecting a\n * narrower track type.\n *\n * No widening of actor message types is needed; no casts inside the\n * helper. Per-variant wiring (right loader paired with right resolver)\n * is enforced at the variant call site.\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, peek, type ReadonlySignal } from '../../../core/signals/primitives';\nimport {\n DEFAULT_FORWARD_BUFFER_CONFIG,\n type ForwardBufferConfig,\n segmentStartForTime,\n} from '../../../media/buffer/forward-buffer';\nimport type { MaybeResolvedPresentation, Segment } from '../../../media/types';\nimport { findResolvedAudioTrack, findResolvedTextTrack, findResolvedVideoTrack } from '../../../media/utils/tracks';\nimport type { BufferState, SegmentLoaderActor, SourceBufferState } from '../../actors/dom/segment-loader';\nimport type { TextTrackSegmentLoaderActor } from '../../actors/text-track-segment-loader';\nimport { AUDIO_TYPE_CONFIG, TEXT_TYPE_CONFIG, VIDEO_TYPE_CONFIG } from '../../primitives/track-types';\n\n// Re-export buffer state types for consumers that import them from this module.\nexport type { BufferState, SourceBufferState };\n\n// ============================================================================\n// STATE & CONTEXT\n// ============================================================================\n\n/** State shape for segment loading. */\nexport interface SegmentLoadingState {\n presentation?: MaybeResolvedPresentation;\n preload?: string;\n /** Current playback position in seconds. Defaults to 0 when undefined. */\n currentTime?: number;\n /** True once a preload-overriding event has fired for the current source. */\n loadActivated?: boolean;\n /**\n * Intent-level policy input: initiate no new loading work while `true`.\n * Read here by the `loadXSegments` dispatchers (park in `'dormant'`,\n * highest precedence) and by `setupMediaSource` (a pending MediaSource\n * rebuild waits — attach runs the element's load algorithm). **Observed,\n * never declared**: no reader lists this key in `stateKeys`, so the slot\n * exists only in compositions where a feature behavior declares and\n * writes it (e.g. `setupAirPlay`, while a remote-playback session owns\n * presentation). An absent slot means never suspended.\n */\n loadingSuspended?: boolean;\n selectedVideoTrackId?: string;\n selectedAudioTrackId?: string;\n selectedTextTrackId?: string;\n}\n\n/** Context shape for segment loading. Each variant only consumes its own type's loader actor. */\nexport interface SegmentLoadingContext {\n videoSegmentLoaderActor?: SegmentLoaderActor;\n audioSegmentLoaderActor?: SegmentLoaderActor;\n textTrackSegmentLoaderActor?: TextTrackSegmentLoaderActor;\n}\n\n// ============================================================================\n// REACTOR\n// ============================================================================\n\ntype SegmentLoadingFsmState = 'preconditions-unmet' | 'dormant' | 'metadata-only' | 'full-range';\n\ntype SelectedTrackKey = 'selectedVideoTrackId' | 'selectedAudioTrackId' | 'selectedTextTrackId';\ntype SegmentLoaderActorKey = 'videoSegmentLoaderActor' | 'audioSegmentLoaderActor' | 'textTrackSegmentLoaderActor';\n\ntype SegmentLoadingStateMap<K extends SelectedTrackKey> = {\n presentation: ReadonlySignal<SegmentLoadingState['presentation']>;\n preload: ReadonlySignal<SegmentLoadingState['preload']>;\n currentTime: ReadonlySignal<SegmentLoadingState['currentTime']>;\n loadActivated: ReadonlySignal<SegmentLoadingState['loadActivated']>;\n} & { [P in K]: ReadonlySignal<SegmentLoadingState[P]> };\n\n/** Shared `'load'` message shape parameterized over the resolved track type. */\ninterface LoadMessage<Track> {\n type: 'load';\n track: Track;\n range?: { start: number; end: number };\n}\n\n/** Structural constraint for any segment-loader-style actor. */\ninterface SegmentLoaderLike<Track> {\n send: (message: LoadMessage<Track>) => void;\n}\n\n/**\n * Specialization helper. Generic over `Track` (inferred from\n * `findResolvedTrack`'s return type). The loader value type is\n * constrained to `SegmentLoaderLike<Track>` — concrete actor types\n * (whose `.send` accepts a wider track union) satisfy this via\n * function-parameter contravariance.\n *\n * Tracks that the helper handles must have a `segments` field — used by\n * `segmentBoundarySignal` to compute the load-anchor boundary. Each\n * variant's resolver narrows to the right resolved-track shape.\n */\nfunction setupSegmentLoading<\n K extends SelectedTrackKey,\n L extends SegmentLoaderActorKey,\n Track extends { segments: readonly Segment[] },\n>({\n state,\n context,\n config,\n}: {\n // `loadingSuspended` is observed, never declared (see its state-shape doc):\n // optional here so variant maps — which omit it from their contracts — are\n // assignable as-is, while compositions with a writer expose the live slot.\n state: SegmentLoadingStateMap<K> & { loadingSuspended?: ReadonlySignal<SegmentLoadingState['loadingSuspended']> };\n context: { [P in L]: ReadonlySignal<SegmentLoaderLike<Track> | undefined> };\n config: {\n selectedKey: K;\n loaderKey: L;\n findResolvedTrack: (\n presentation: MaybeResolvedPresentation | undefined,\n trackId: string | undefined\n ) => Track | undefined;\n forwardBuffer?: Partial<ForwardBufferConfig>;\n };\n}): Reactor<SegmentLoadingFsmState | 'destroying' | 'destroyed'> {\n const { selectedKey, loaderKey, findResolvedTrack } = config;\n const bufferDuration = config.forwardBuffer?.bufferDuration ?? DEFAULT_FORWARD_BUFFER_CONFIG.bufferDuration;\n\n const selectedTrack = computed<Track | undefined>(() =>\n findResolvedTrack(state.presentation.get(), state[selectedKey].get())\n );\n\n // Segment-boundary signal — `segmentStartForTime` returns the same number\n // while `currentTime` stays inside one segment, so signal-polyfill's\n // `Object.is` equality on this computed dedups within-segment ticks.\n const segmentBoundarySignal = computed(() => {\n const track = selectedTrack.get();\n if (!track) return undefined;\n return segmentStartForTime(state.currentTime.get() ?? 0, track.segments);\n });\n\n const derivedStateSignal = computed<SegmentLoadingFsmState>(() => {\n // Policy-off wins even over preconditions: a loader arriving while\n // suspended must not dispatch.\n if (state.loadingSuspended?.get()) return 'dormant';\n if (!context[loaderKey].get() || !selectedTrack.get()) return 'preconditions-unmet';\n if (state.loadActivated.get() || state.preload.get() === 'auto') return 'full-range';\n if (state.preload.get() === 'none') return 'dormant';\n return 'metadata-only';\n });\n\n return createMachineReactor<SegmentLoadingFsmState>({\n initial: 'preconditions-unmet',\n monitor: () => derivedStateSignal.get(),\n states: {\n 'preconditions-unmet': {},\n dormant: {},\n 'metadata-only': {\n // Fires once on entry. Matches HTMLMediaElement's preload='metadata'\n // semantics — load enough to surface metadata for the entry-time\n // selection. Each variant's loader decides what to do: v/a's actor\n // fetches the init segment; text's actor treats no-range as a\n // no-op. Selection changes while still in `'metadata-only'` are\n // intentionally not followed.\n entry: () => {\n const track = selectedTrack.get()!;\n context[loaderKey].get()!.send({ type: 'load', track });\n },\n },\n 'full-range': {\n // Re-fires on selected-track changes and on segment-boundary\n // crossings. The loader and `currentTime` are peeked — the\n // boundary-dedup signal already handles re-firing policy.\n effects: () => {\n const track = selectedTrack.get()!;\n segmentBoundarySignal.get();\n const currentTime = peek(state.currentTime) ?? 0;\n peek(context[loaderKey])!.send({\n type: 'load',\n track,\n range: { start: currentTime, end: currentTime + bufferDuration },\n });\n },\n },\n },\n });\n}\n\n// ============================================================================\n// Per-helper-per-type configs — defaults that variants spread engine config over\n// ============================================================================\n\nconst VIDEO_SEGMENT_LOADING_CONFIG = {\n ...VIDEO_TYPE_CONFIG,\n findResolvedTrack: findResolvedVideoTrack,\n} as const;\n\nconst AUDIO_SEGMENT_LOADING_CONFIG = {\n ...AUDIO_TYPE_CONFIG,\n findResolvedTrack: findResolvedAudioTrack,\n} as const;\n\nconst TEXT_SEGMENT_LOADING_CONFIG = {\n ...TEXT_TYPE_CONFIG,\n findResolvedTrack: findResolvedTextTrack,\n} as const;\n\n// ============================================================================\n// Specialized exports — one per media type\n// ============================================================================\n\nexport const loadVideoSegments = defineBehavior({\n stateKeys: ['presentation', 'preload', 'currentTime', 'loadActivated', 'selectedVideoTrackId'],\n contextKeys: ['videoSegmentLoaderActor'],\n setup: ({\n state,\n context,\n config = {},\n }: {\n state: SegmentLoadingStateMap<'selectedVideoTrackId'>;\n context: {\n videoSegmentLoaderActor: ReadonlySignal<SegmentLoadingContext['videoSegmentLoaderActor']>;\n };\n config?: object;\n }) =>\n setupSegmentLoading({\n state,\n context,\n config: { ...VIDEO_SEGMENT_LOADING_CONFIG, ...config },\n }),\n});\n\nexport const loadAudioSegments = defineBehavior({\n stateKeys: ['presentation', 'preload', 'currentTime', 'loadActivated', 'selectedAudioTrackId'],\n contextKeys: ['audioSegmentLoaderActor'],\n setup: ({\n state,\n context,\n config = {},\n }: {\n state: SegmentLoadingStateMap<'selectedAudioTrackId'>;\n context: {\n audioSegmentLoaderActor: ReadonlySignal<SegmentLoadingContext['audioSegmentLoaderActor']>;\n };\n config?: object;\n }) =>\n setupSegmentLoading({\n state,\n context,\n config: { ...AUDIO_SEGMENT_LOADING_CONFIG, ...config },\n }),\n});\n\nexport const loadTextTrackSegments = defineBehavior({\n stateKeys: ['presentation', 'preload', 'currentTime', 'loadActivated', 'selectedTextTrackId'],\n contextKeys: ['textTrackSegmentLoaderActor'],\n setup: ({\n state,\n context,\n config = {},\n }: {\n state: SegmentLoadingStateMap<'selectedTextTrackId'>;\n context: {\n textTrackSegmentLoaderActor: ReadonlySignal<SegmentLoadingContext['textTrackSegmentLoaderActor']>;\n };\n config?: object;\n }) =>\n setupSegmentLoading({\n state,\n context,\n config: { ...TEXT_SEGMENT_LOADING_CONFIG, ...config },\n }),\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4IA,SAAS,oBAIP,EACA,OACA,SACA,UAgB+D;CAC/D,MAAM,EAAE,aAAa,WAAW,sBAAsB;CACtD,MAAM,iBAAiB,OAAO,eAAe,kBAAkB,8BAA8B;CAE7F,MAAM,gBAAgB,eACpB,kBAAkB,MAAM,aAAa,IAAI,GAAG,MAAM,YAAY,CAAC,IAAI,CAAC,CACtE;CAKA,MAAM,wBAAwB,eAAe;EAC3C,MAAM,QAAQ,cAAc,IAAI;EAChC,IAAI,CAAC,OAAO,OAAO,KAAA;EACnB,OAAO,oBAAoB,MAAM,YAAY,IAAI,KAAK,GAAG,MAAM,QAAQ;CACzE,CAAC;CAED,MAAM,qBAAqB,eAAuC;EAGhE,IAAI,MAAM,kBAAkB,IAAI,GAAG,OAAO;EAC1C,IAAI,CAAC,QAAQ,UAAU,CAAC,IAAI,KAAK,CAAC,cAAc,IAAI,GAAG,OAAO;EAC9D,IAAI,MAAM,cAAc,IAAI,KAAK,MAAM,QAAQ,IAAI,MAAM,QAAQ,OAAO;EACxE,IAAI,MAAM,QAAQ,IAAI,MAAM,QAAQ,OAAO;EAC3C,OAAO;CACT,CAAC;CAED,OAAO,qBAA6C;EAClD,SAAS;EACT,eAAe,mBAAmB,IAAI;EACtC,QAAQ;GACN,uBAAuB,CAAC;GACxB,SAAS,CAAC;GACV,iBAAiB,EAOf,aAAa;IACX,MAAM,QAAQ,cAAc,IAAI;IAChC,QAAQ,UAAU,CAAC,IAAI,CAAC,CAAE,KAAK;KAAE,MAAM;KAAQ;IAAM,CAAC;GACxD,EACF;GACA,cAAc,EAIZ,eAAe;IACb,MAAM,QAAQ,cAAc,IAAI;IAChC,sBAAsB,IAAI;IAC1B,MAAM,cAAc,KAAK,MAAM,WAAW,KAAK;IAC/C,KAAK,QAAQ,UAAU,CAAC,CAAE,KAAK;KAC7B,MAAM;KACN;KACA,OAAO;MAAE,OAAO;MAAa,KAAK,cAAc;KAAe;IACjE,CAAC;GACH,EACF;EACF;CACF,CAAC;AACH;AAMA,MAAM,+BAA+B;CACnC,GAAG;CACH,mBAAmB;AACrB;AAEA,MAAM,+BAA+B;CACnC,GAAG;CACH,mBAAmB;AACrB;AAEA,MAAM,8BAA8B;CAClC,GAAG;CACH,mBAAmB;AACrB;AAMA,MAAa,oBAAoB,eAAe;CAC9C,WAAW;EAAC;EAAgB;EAAW;EAAe;EAAiB;CAAsB;CAC7F,aAAa,CAAC,yBAAyB;CACvC,QAAQ,EACN,OACA,SACA,SAAS,CAAC,QAQV,oBAAoB;EAClB;EACA;EACA,QAAQ;GAAE,GAAG;GAA8B,GAAG;EAAO;CACvD,CAAC;AACL,CAAC;AAED,MAAa,oBAAoB,eAAe;CAC9C,WAAW;EAAC;EAAgB;EAAW;EAAe;EAAiB;CAAsB;CAC7F,aAAa,CAAC,yBAAyB;CACvC,QAAQ,EACN,OACA,SACA,SAAS,CAAC,QAQV,oBAAoB;EAClB;EACA;EACA,QAAQ;GAAE,GAAG;GAA8B,GAAG;EAAO;CACvD,CAAC;AACL,CAAC;AAED,MAAa,wBAAwB,eAAe;CAClD,WAAW;EAAC;EAAgB;EAAW;EAAe;EAAiB;CAAqB;CAC5F,aAAa,CAAC,6BAA6B;CAC3C,QAAQ,EACN,OACA,SACA,SAAS,CAAC,QAQV,oBAAoB;EAClB;EACA;EACA,QAAQ;GAAE,GAAG;GAA6B,GAAG;EAAO;CACtD,CAAC;AACL,CAAC"}
@@ -0,0 +1,77 @@
1
+ import { defineBehavior } from "../../../core/composition/create-composition.js";
2
+ import { effect } from "../../../core/signals/effect.js";
3
+ import { getMinBufferedEnd } from "../../../media/dom/mse/duration.js";
4
+ import { listen } from "@videojs/utils/dom";
5
+ //#region src/playback/behaviors/dom/recover-end-stall.ts
6
+ /**
7
+ * Recover the end-of-stream stall that Chrome exhibits on skewed A/V. After
8
+ * `endOfStream`, when the audio and video tracks end a few ms apart (e.g. a source
9
+ * with an A/V PTS skew), Chrome's audio-clock-paced playback freezes the playhead
10
+ * ~50–70ms short of the reachable buffered end and never fires `ended` — so playback
11
+ * hangs at the very end and loop never re-triggers. This behavior watches for the
12
+ * `waiting` event that fires at that freeze and, when the MediaSource is `ended` and
13
+ * the playhead sits at the reachable buffered end, nudges `currentTime` to `duration`
14
+ * to force the native `ended`.
15
+ *
16
+ * **Event-driven, no poll.** `waiting` fires at the instant the playhead stalls
17
+ * (measured ~0ms latency), so there's nothing to gain from polling — and polling would
18
+ * add its interval + a stall threshold before reacting.
19
+ *
20
+ * **Proximity to the *reachable* buffered end** is the discriminator. That end is
21
+ * `getMinBufferedEnd(mediaSource.sourceBuffers)` — the `min` of the per-track (video/audio)
22
+ * SourceBuffer ends, i.e. the furthest point playback can reach — read from the SourceBuffers
23
+ * directly rather than the `mediaElement.buffered` aggregate. Once `endOfStream` is signalled
24
+ * that's the true content end. Requiring the playhead within `endStallNudgeWindow` of it
25
+ * distinguishes the real end-of-stream freeze from a mid-stream buffer-hole stall (which sits
26
+ * far from the buffered end), so we never skip content. The window must exceed the freeze gap;
27
+ * too small would miss the stall (a permanent hang), so the default is generous relative to the
28
+ * measured gap and is config-tunable for empirical tuning.
29
+ *
30
+ * Inert where it shouldn't act: live (the MediaSource never reaches `ended` while the
31
+ * window grows; `duration` is `Infinity`) and streams that end cleanly (no `waiting`).
32
+ *
33
+ * See `internal/decisions/spf/end-of-stream-av-skew-recovery.md`.
34
+ */
35
+ /** ~2.8× the measured max freeze gap (71ms) — tight, but with headroom against a miss. */
36
+ const DEFAULT_END_STALL_NUDGE_WINDOW = .2;
37
+ /**
38
+ * Whether a `waiting` should be forced to `ended`: the MediaSource is `ended`, the
39
+ * stream is finite (not live), playback is active (not paused/seeking/already-ended),
40
+ * and the playhead sits within `nudgeWindow` of the reachable buffered end (so it's the
41
+ * true end, not a mid-stream buffer hole). Pure — the behavior supplies the live values.
42
+ */
43
+ function shouldForceEnded(input, nudgeWindow) {
44
+ const { msEnded, durationFinite, paused, seeking, ended, currentTime, bufferedEnd } = input;
45
+ if (!msEnded || !durationFinite || paused || seeking || ended || bufferedEnd === void 0) return false;
46
+ const gap = bufferedEnd - currentTime;
47
+ return gap >= 0 && gap < nudgeWindow;
48
+ }
49
+ function recoverEndStallSetup({ context, config }) {
50
+ const nudgeWindow = config?.endStallNudgeWindow ?? .2;
51
+ return effect(() => {
52
+ const mediaElement = context.mediaElement.get();
53
+ if (!mediaElement) return;
54
+ const onWaiting = () => {
55
+ const mediaSource = context.mediaSource.get();
56
+ if (shouldForceEnded({
57
+ msEnded: mediaSource?.readyState === "ended",
58
+ durationFinite: Number.isFinite(mediaElement.duration),
59
+ paused: mediaElement.paused,
60
+ seeking: mediaElement.seeking,
61
+ ended: mediaElement.ended,
62
+ currentTime: mediaElement.currentTime,
63
+ bufferedEnd: mediaSource ? getMinBufferedEnd(mediaSource.sourceBuffers) : void 0
64
+ }, nudgeWindow)) mediaElement.currentTime = mediaElement.duration;
65
+ };
66
+ return listen(mediaElement, "waiting", onWaiting);
67
+ });
68
+ }
69
+ const recoverEndStall = defineBehavior({
70
+ stateKeys: [],
71
+ contextKeys: ["mediaElement", "mediaSource"],
72
+ setup: recoverEndStallSetup
73
+ });
74
+ //#endregion
75
+ export { DEFAULT_END_STALL_NUDGE_WINDOW, recoverEndStall, shouldForceEnded };
76
+
77
+ //# sourceMappingURL=recover-end-stall.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recover-end-stall.js","names":[],"sources":["../../../../../src/playback/behaviors/dom/recover-end-stall.ts"],"sourcesContent":["/**\n * Recover the end-of-stream stall that Chrome exhibits on skewed A/V. After\n * `endOfStream`, when the audio and video tracks end a few ms apart (e.g. a source\n * with an A/V PTS skew), Chrome's audio-clock-paced playback freezes the playhead\n * ~50–70ms short of the reachable buffered end and never fires `ended` — so playback\n * hangs at the very end and loop never re-triggers. This behavior watches for the\n * `waiting` event that fires at that freeze and, when the MediaSource is `ended` and\n * the playhead sits at the reachable buffered end, nudges `currentTime` to `duration`\n * to force the native `ended`.\n *\n * **Event-driven, no poll.** `waiting` fires at the instant the playhead stalls\n * (measured ~0ms latency), so there's nothing to gain from polling — and polling would\n * add its interval + a stall threshold before reacting.\n *\n * **Proximity to the *reachable* buffered end** is the discriminator. That end is\n * `getMinBufferedEnd(mediaSource.sourceBuffers)` — the `min` of the per-track (video/audio)\n * SourceBuffer ends, i.e. the furthest point playback can reach — read from the SourceBuffers\n * directly rather than the `mediaElement.buffered` aggregate. Once `endOfStream` is signalled\n * that's the true content end. Requiring the playhead within `endStallNudgeWindow` of it\n * distinguishes the real end-of-stream freeze from a mid-stream buffer-hole stall (which sits\n * far from the buffered end), so we never skip content. The window must exceed the freeze gap;\n * too small would miss the stall (a permanent hang), so the default is generous relative to the\n * measured gap and is config-tunable for empirical tuning.\n *\n * Inert where it shouldn't act: live (the MediaSource never reaches `ended` while the\n * window grows; `duration` is `Infinity`) and streams that end cleanly (no `waiting`).\n *\n * See `internal/decisions/spf/end-of-stream-av-skew-recovery.md`.\n */\nimport { listen } from '@videojs/utils/dom';\nimport { defineBehavior } from '../../../core/composition/create-composition';\nimport { effect } from '../../../core/signals/effect';\nimport type { ReadonlySignal } from '../../../core/signals/primitives';\nimport { getMinBufferedEnd } from '../../../media/dom/mse/duration';\n\nexport interface RecoverEndStallContext {\n mediaElement?: HTMLMediaElement | undefined;\n mediaSource?: MediaSource | undefined;\n}\n\nexport interface RecoverEndStallConfig {\n /**\n * How close (seconds) the playhead must be to the reachable buffered end for a\n * `waiting` to count as the end-of-stream freeze. Must exceed the audio-clock freeze\n * gap (~50–70ms measured on Chrome); too small risks missing the stall (a permanent\n * hang), so keep a margin. Default {@link DEFAULT_END_STALL_NUDGE_WINDOW}.\n */\n endStallNudgeWindow?: number;\n}\n\n/** ~2.8× the measured max freeze gap (71ms) — tight, but with headroom against a miss. */\nexport const DEFAULT_END_STALL_NUDGE_WINDOW = 0.2;\n\n/**\n * Whether a `waiting` should be forced to `ended`: the MediaSource is `ended`, the\n * stream is finite (not live), playback is active (not paused/seeking/already-ended),\n * and the playhead sits within `nudgeWindow` of the reachable buffered end (so it's the\n * true end, not a mid-stream buffer hole). Pure — the behavior supplies the live values.\n */\nexport function shouldForceEnded(\n input: {\n msEnded: boolean;\n durationFinite: boolean;\n paused: boolean;\n seeking: boolean;\n ended: boolean;\n currentTime: number;\n bufferedEnd: number | undefined;\n },\n nudgeWindow: number\n): boolean {\n const { msEnded, durationFinite, paused, seeking, ended, currentTime, bufferedEnd } = input;\n if (!msEnded || !durationFinite || paused || seeking || ended || bufferedEnd === undefined) {\n return false;\n }\n const gap = bufferedEnd - currentTime;\n return gap >= 0 && gap < nudgeWindow;\n}\n\nfunction recoverEndStallSetup({\n context,\n config,\n}: {\n context: {\n mediaElement: ReadonlySignal<RecoverEndStallContext['mediaElement']>;\n mediaSource: ReadonlySignal<RecoverEndStallContext['mediaSource']>;\n };\n config?: RecoverEndStallConfig;\n}): () => void {\n const nudgeWindow = config?.endStallNudgeWindow ?? DEFAULT_END_STALL_NUDGE_WINDOW;\n\n return effect(() => {\n const mediaElement = context.mediaElement.get();\n if (!mediaElement) return;\n\n const onWaiting = () => {\n const mediaSource = context.mediaSource.get();\n const forceEnded = shouldForceEnded(\n {\n msEnded: mediaSource?.readyState === 'ended',\n durationFinite: Number.isFinite(mediaElement.duration),\n paused: mediaElement.paused,\n seeking: mediaElement.seeking,\n ended: mediaElement.ended,\n currentTime: mediaElement.currentTime,\n bufferedEnd: mediaSource ? getMinBufferedEnd(mediaSource.sourceBuffers) : undefined,\n },\n nudgeWindow\n );\n // Nudge to `duration` → native `ended` (the seeking/ended guards above prevent a\n // re-fire while the nudge-seek is in flight, so no latch is needed).\n if (forceEnded) mediaElement.currentTime = mediaElement.duration;\n };\n\n return listen(mediaElement, 'waiting', onWaiting);\n });\n}\n\nexport const recoverEndStall = defineBehavior({\n stateKeys: [] as const,\n contextKeys: ['mediaElement', 'mediaSource'] as const,\n setup: recoverEndStallSetup,\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmDA,MAAa,iCAAiC;;;;;;;AAQ9C,SAAgB,iBACd,OASA,aACS;CACT,MAAM,EAAE,SAAS,gBAAgB,QAAQ,SAAS,OAAO,aAAa,gBAAgB;CACtF,IAAI,CAAC,WAAW,CAAC,kBAAkB,UAAU,WAAW,SAAS,gBAAgB,KAAA,GAC/E,OAAO;CAET,MAAM,MAAM,cAAc;CAC1B,OAAO,OAAO,KAAK,MAAM;AAC3B;AAEA,SAAS,qBAAqB,EAC5B,SACA,UAOa;CACb,MAAM,cAAc,QAAQ,uBAAA;CAE5B,OAAO,aAAa;EAClB,MAAM,eAAe,QAAQ,aAAa,IAAI;EAC9C,IAAI,CAAC,cAAc;EAEnB,MAAM,kBAAkB;GACtB,MAAM,cAAc,QAAQ,YAAY,IAAI;GAe5C,IAdmB,iBACjB;IACE,SAAS,aAAa,eAAe;IACrC,gBAAgB,OAAO,SAAS,aAAa,QAAQ;IACrD,QAAQ,aAAa;IACrB,SAAS,aAAa;IACtB,OAAO,aAAa;IACpB,aAAa,aAAa;IAC1B,aAAa,cAAc,kBAAkB,YAAY,aAAa,IAAI,KAAA;GAC5E,GACA,WAIW,GAAG,aAAa,cAAc,aAAa;EAC1D;EAEA,OAAO,OAAO,cAAc,WAAW,SAAS;CAClD,CAAC;AACH;AAEA,MAAa,kBAAkB,eAAe;CAC5C,WAAW,CAAC;CACZ,aAAa,CAAC,gBAAgB,aAAa;CAC3C,OAAO;AACT,CAAC"}
@@ -2,12 +2,14 @@ import { computed } 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 { hasCodecs } from "../../../media/utils/tracks.js";
5
- import { AUDIO_TYPE_CONFIG, VIDEO_TYPE_CONFIG } from "../track-types.js";
6
- import { getSelectedTrack } from "../../../media/utils/track-selection.js";
5
+ import { AUDIO_TYPE_CONFIG, VIDEO_TYPE_CONFIG } from "../../primitives/track-types.js";
7
6
  import { buildMimeCodec, createSourceBuffer } from "../../../media/dom/mse/mediasource-setup.js";
7
+ import { getSelectedTrack } from "../../../media/utils/track-selection.js";
8
8
  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
+ import { failoverFetch } from "../../primitives/failover-fetch.js";
12
+ import { listen } from "@videojs/utils/dom";
11
13
  //#region src/playback/behaviors/dom/setup-buffer-actors.ts
12
14
  /**
13
15
  * **Per-type buffer + segment-loader actor setup.** Per available track
@@ -39,7 +41,7 @@ import { createSourceBufferActor } from "../../actors/dom/source-buffer.js";
39
41
  * with its upstream and downstream siblings:
40
42
  *
41
43
  * 1. **Upstream — default selections land in one `runPending`.**
42
- * `selectAudioTrack` (default audio) and `switchVideoQuality`
44
+ * `selectAudioTrack` (default audio) and `switchVideoTrack`
43
45
  * (default video) both subscribe to `state.presentation` flipping to
44
46
  * resolved; their effects run in the same `runPending` iteration and
45
47
  * write `selectedAudioTrackId` + `selectedVideoTrackId` within it.
@@ -71,7 +73,7 @@ import { createSourceBufferActor } from "../../actors/dom/source-buffer.js";
71
73
  * `updateMediaSourceDuration`) only read these slots.
72
74
  */
73
75
  function setupBufferActors({ state, context, config }) {
74
- const { type, selectedKey, actorKey, loaderKey, fetch, forwardBuffer, backBuffer } = config;
76
+ const { type, selectedKey, actorKey, loaderKey, fetch, forwardBuffer, backBuffer, messagePipelines } = config;
75
77
  const derivedStateSignal = computed(() => {
76
78
  if (!context.mediaSource.get()) return "preconditions-unmet";
77
79
  return hasCodecs(getSelectedTrack({
@@ -85,22 +87,32 @@ function setupBufferActors({ state, context, config }) {
85
87
  states: {
86
88
  "preconditions-unmet": {},
87
89
  "buffer-ready": { entry: () => {
88
- const bufferActor = createSourceBufferActor(createSourceBuffer(context.mediaSource.get(), buildMimeCodec(getSelectedTrack({
90
+ const mediaSource = context.mediaSource.get();
91
+ const bufferActor = createSourceBufferActor(createSourceBuffer(mediaSource, buildMimeCodec(getSelectedTrack({
89
92
  presentation: state.presentation.get(),
90
93
  [selectedKey]: state[selectedKey].get()
91
94
  }, type))));
92
95
  const segmentLoader = createSegmentLoaderActor(bufferActor, fetch, {
93
96
  forwardBuffer,
94
- backBuffer
97
+ backBuffer,
98
+ messagePipelines
99
+ }, {
100
+ state,
101
+ context,
102
+ config
95
103
  });
96
104
  context[actorKey].set(bufferActor);
97
105
  context[loaderKey].set(segmentLoader);
98
- return () => {
106
+ const disconnect = new AbortController();
107
+ const teardownActors = () => {
99
108
  segmentLoader.destroy();
100
109
  bufferActor.destroy();
101
110
  context[loaderKey].set(void 0);
102
111
  context[actorKey].set(void 0);
112
+ disconnect.abort();
103
113
  };
114
+ listen(mediaSource, "sourceclose", teardownActors, { signal: disconnect.signal });
115
+ return teardownActors;
104
116
  } }
105
117
  }
106
118
  });
@@ -131,13 +143,17 @@ const setupVideoBufferActors = defineBehavior({
131
143
  slowTotalWeight: 0,
132
144
  bytesSampled: 0
133
145
  }, (next) => state.bandwidthState.set(next));
146
+ const typeConfig = {
147
+ ...VIDEO_TYPE_CONFIG,
148
+ ...config
149
+ };
134
150
  return setupBufferActors({
135
151
  state,
136
152
  context,
137
153
  config: {
138
- ...VIDEO_TYPE_CONFIG,
139
- fetch: trackedFetch,
140
- ...config
154
+ ...typeConfig,
155
+ messagePipelines: config.videoMessagePipelines,
156
+ fetch: failoverFetch(trackedFetch, state, typeConfig)
141
157
  }
142
158
  });
143
159
  }
@@ -148,7 +164,17 @@ const setupVideoBufferActors = defineBehavior({
148
164
  * a non-sampling `fetchStream` (no audio ABR); adding audio ABR is a
149
165
  * localized change to this setup body (swap `fetchStream` for a
150
166
  * `createTrackedFetch` call + declare `bandwidthState` writable here)
151
- * without touching the shared helper.
167
+ * without touching the shared helper. See
168
+ * `internal/design/spf/features/audio-abr.md` for the design surface
169
+ * (bandwidth-state sharing, multi-writer coordination, EWMA mixed-
170
+ * source sampling).
171
+ *
172
+ * **Mid-stream audio track switching is NOT this behavior's concern.**
173
+ * Slot writes to `selectedAudioTrackId` (default selection, programmatic
174
+ * filter-driven, future ABR) are owned by `switchAudioTrack` / future
175
+ * `switchAudioQuality` in `track-switching.ts`. Flush orchestration on
176
+ * track change is dispatched from there via `audioBufferActor.send(...)`
177
+ * — keeping this setup behavior focused on per-source actor lifecycle.
152
178
  */
153
179
  const setupAudioBufferActors = defineBehavior({
154
180
  stateKeys: ["presentation", "selectedAudioTrackId"],
@@ -157,15 +183,21 @@ const setupAudioBufferActors = defineBehavior({
157
183
  "audioBufferActor",
158
184
  "audioSegmentLoaderActor"
159
185
  ],
160
- setup: ({ state, context, config = {} }) => setupBufferActors({
161
- state,
162
- context,
163
- config: {
186
+ setup: ({ state, context, config = {} }) => {
187
+ const typeConfig = {
164
188
  ...AUDIO_TYPE_CONFIG,
165
- fetch: fetchStream,
166
189
  ...config
167
- }
168
- })
190
+ };
191
+ return setupBufferActors({
192
+ state,
193
+ context,
194
+ config: {
195
+ ...typeConfig,
196
+ messagePipelines: config.audioMessagePipelines,
197
+ fetch: failoverFetch(fetchStream, state, typeConfig)
198
+ }
199
+ });
200
+ }
169
201
  });
170
202
  //#endregion
171
203
  export { setupAudioBufferActors, setupVideoBufferActors };
@@ -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 `switchVideoQuality`\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 { 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 { AUDIO_TYPE_CONFIG, VIDEO_TYPE_CONFIG } from '../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?: object;\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 return setupBufferActors({\n state,\n context,\n config: { ...VIDEO_TYPE_CONFIG, fetch: trackedFetch, ...config },\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.\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?: object;\n }) =>\n setupBufferActors({\n state,\n context,\n config: { ...AUDIO_TYPE_CONFIG, fetch: fetchStream, ...config },\n }),\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmIA,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;AACD,SAAO,kBAAkB;GACvB;GACA;GACA,QAAQ;IAAE,GAAG;IAAmB,OAAO;IAAc,GAAG;IAAQ;GACjE,CAAC;;CAEL,CAAC;;;;;;;;;AAUF,MAAa,yBAAyB,eAAe;CACnD,WAAW,CAAC,gBAAgB,uBAAuB;CACnD,aAAa;EAAC;EAAe;EAAoB;EAA0B;CAC3E,QAAQ,EACN,OACA,SACA,SAAS,EAAE,OAMX,kBAAkB;EAChB;EACA;EACA,QAAQ;GAAE,GAAG;GAAmB,OAAO;GAAa,GAAG;GAAQ;EAChE,CAAC;CACL,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