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

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 (461) hide show
  1. package/dist/{dev/core → default/core/actors}/create-machine-actor.js +3 -3
  2. package/dist/default/core/actors/create-machine-actor.js.map +1 -0
  3. package/dist/default/core/{create-transition-actor.js → actors/create-transition-actor.js} +3 -3
  4. package/dist/default/core/actors/create-transition-actor.js.map +1 -0
  5. package/dist/default/core/composition/create-composition.js +85 -0
  6. package/dist/default/core/composition/create-composition.js.map +1 -0
  7. package/dist/default/core/composition/share-signals.js +37 -0
  8. package/dist/default/core/composition/share-signals.js.map +1 -0
  9. package/dist/{dev/core → default/core/reactors}/create-machine-reactor.js +4 -4
  10. package/dist/default/core/reactors/create-machine-reactor.js.map +1 -0
  11. package/dist/default/core/signals/primitives.js +33 -11
  12. package/dist/default/core/signals/primitives.js.map +1 -1
  13. package/dist/default/core/{task.js → tasks/task.js} +2 -2
  14. package/dist/default/core/tasks/task.js.map +1 -0
  15. package/dist/default/dom.js +9 -10
  16. package/dist/default/hls.js +3 -0
  17. package/dist/default/index.js +12 -3
  18. package/dist/default/index.js.map +1 -1
  19. package/dist/default/media/abr/quality-selection.js +83 -0
  20. package/dist/default/media/abr/quality-selection.js.map +1 -0
  21. package/dist/default/{core → media}/buffer/back-buffer.js +2 -2
  22. package/dist/default/media/buffer/back-buffer.js.map +1 -0
  23. package/dist/{dev/core → default/media}/buffer/forward-buffer.js +14 -2
  24. package/dist/default/media/buffer/forward-buffer.js.map +1 -0
  25. package/dist/{dev/dom/media → default/media/dom/mse}/append-segment.js +1 -1
  26. package/dist/default/media/dom/mse/append-segment.js.map +1 -0
  27. package/dist/default/{dom/media → media/dom/mse}/buffer-flusher.js +1 -1
  28. package/dist/default/media/dom/mse/buffer-flusher.js.map +1 -0
  29. package/dist/default/media/dom/mse/duration.js +78 -0
  30. package/dist/default/media/dom/mse/duration.js.map +1 -0
  31. package/dist/default/media/dom/mse/end-of-stream.js +20 -0
  32. package/dist/default/media/dom/mse/end-of-stream.js.map +1 -0
  33. package/dist/default/{dom/media → media/dom/mse}/mediasource-setup.js +63 -18
  34. package/dist/default/media/dom/mse/mediasource-setup.js.map +1 -0
  35. package/dist/default/{dom/text/parse-vtt-segment.js → media/dom/text/resolve-vtt-segment.js} +5 -5
  36. package/dist/default/media/dom/text/resolve-vtt-segment.js.map +1 -0
  37. package/dist/default/media/dom/text/text-track-slots.js +69 -0
  38. package/dist/default/media/dom/text/text-track-slots.js.map +1 -0
  39. package/dist/{dev/core → default/media}/hls/parse-attributes.js +1 -1
  40. package/dist/default/media/hls/parse-attributes.js.map +1 -0
  41. package/dist/{dev/core → default/media}/hls/parse-media-playlist.js +1 -1
  42. package/dist/default/media/hls/parse-media-playlist.js.map +1 -0
  43. package/dist/{dev/core → default/media}/hls/parse-multivariant.js +2 -2
  44. package/dist/default/media/hls/parse-multivariant.js.map +1 -0
  45. package/dist/default/{core → media}/hls/resolve-url.js +1 -1
  46. package/dist/default/media/hls/resolve-url.js.map +1 -0
  47. package/dist/default/media/primitives/select-tracks.js +45 -0
  48. package/dist/default/media/primitives/select-tracks.js.map +1 -0
  49. package/dist/default/media/types/index.js +36 -0
  50. package/dist/default/media/types/index.js.map +1 -0
  51. package/dist/default/media/utils/preload.js +22 -0
  52. package/dist/default/media/utils/preload.js.map +1 -0
  53. package/dist/default/media/utils/track-selection.js +47 -0
  54. package/dist/default/media/utils/track-selection.js.map +1 -0
  55. package/dist/default/media/utils/tracks.js +103 -0
  56. package/dist/default/media/utils/tracks.js.map +1 -0
  57. package/dist/default/{core/abr → network}/bandwidth-estimator.js +6 -5
  58. package/dist/default/network/bandwidth-estimator.js.map +1 -0
  59. package/dist/{dev/dom → default}/network/chunked-stream-iterable.js +1 -1
  60. package/dist/default/network/chunked-stream-iterable.js.map +1 -0
  61. package/dist/{dev/core/abr → default/network}/ewma.js +1 -1
  62. package/dist/default/network/ewma.js.map +1 -0
  63. package/dist/default/network/fetch.js +109 -0
  64. package/dist/default/network/fetch.js.map +1 -0
  65. package/dist/{dev/dom/features/segment-loader-actor.js → default/playback/actors/dom/segment-loader.js} +24 -15
  66. package/dist/default/playback/actors/dom/segment-loader.js.map +1 -0
  67. package/dist/{dev/dom/media/source-buffer-actor.js → default/playback/actors/dom/source-buffer.js} +8 -8
  68. package/dist/default/playback/actors/dom/source-buffer.js.map +1 -0
  69. package/dist/{dev/dom/features/text-tracks-actor.js → default/playback/actors/dom/text-tracks.js} +7 -3
  70. package/dist/default/playback/actors/dom/text-tracks.js.map +1 -0
  71. package/dist/default/playback/actors/text-track-segment-loader.js +135 -0
  72. package/dist/default/playback/actors/text-track-segment-loader.js.map +1 -0
  73. package/dist/default/playback/behaviors/calculate-presentation-duration.js +58 -0
  74. package/dist/default/playback/behaviors/calculate-presentation-duration.js.map +1 -0
  75. package/dist/default/playback/behaviors/dom/end-of-stream.js +178 -0
  76. package/dist/default/playback/behaviors/dom/end-of-stream.js.map +1 -0
  77. package/dist/default/playback/behaviors/dom/load-segments.js +177 -0
  78. package/dist/default/playback/behaviors/dom/load-segments.js.map +1 -0
  79. package/dist/default/playback/behaviors/dom/setup-buffer-actors.js +173 -0
  80. package/dist/default/playback/behaviors/dom/setup-buffer-actors.js.map +1 -0
  81. package/dist/default/playback/behaviors/dom/setup-mediasource.js +89 -0
  82. package/dist/default/playback/behaviors/dom/setup-mediasource.js.map +1 -0
  83. package/dist/default/playback/behaviors/dom/setup-text-track-actors.js +54 -0
  84. package/dist/default/playback/behaviors/dom/setup-text-track-actors.js.map +1 -0
  85. package/dist/default/playback/behaviors/dom/sync-text-tracks.js +99 -0
  86. package/dist/default/playback/behaviors/dom/sync-text-tracks.js.map +1 -0
  87. package/dist/default/playback/behaviors/dom/track-current-time.js +46 -0
  88. package/dist/default/playback/behaviors/dom/track-current-time.js.map +1 -0
  89. package/dist/default/playback/behaviors/dom/track-load-triggers.js +94 -0
  90. package/dist/default/playback/behaviors/dom/track-load-triggers.js.map +1 -0
  91. package/dist/default/playback/behaviors/dom/track-playback-rate.js +37 -0
  92. package/dist/default/playback/behaviors/dom/track-playback-rate.js.map +1 -0
  93. package/dist/default/playback/behaviors/dom/update-mediasource-duration.js +90 -0
  94. package/dist/default/playback/behaviors/dom/update-mediasource-duration.js.map +1 -0
  95. package/dist/default/playback/behaviors/quality-switching.js +96 -0
  96. package/dist/default/playback/behaviors/quality-switching.js.map +1 -0
  97. package/dist/default/playback/behaviors/resolve-presentation.js +85 -0
  98. package/dist/default/playback/behaviors/resolve-presentation.js.map +1 -0
  99. package/dist/default/playback/behaviors/resolve-track.js +97 -0
  100. package/dist/default/playback/behaviors/resolve-track.js.map +1 -0
  101. package/dist/default/playback/behaviors/select-tracks.js +132 -0
  102. package/dist/default/playback/behaviors/select-tracks.js.map +1 -0
  103. package/dist/default/playback/behaviors/sync-preload.js +59 -0
  104. package/dist/default/playback/behaviors/sync-preload.js.map +1 -0
  105. package/dist/default/playback/behaviors/track-types.js +61 -0
  106. package/dist/default/playback/behaviors/track-types.js.map +1 -0
  107. package/dist/default/{dom/playback-engine → playback/engines/hls}/adapter.js +36 -25
  108. package/dist/default/playback/engines/hls/adapter.js.map +1 -0
  109. package/dist/default/playback/engines/hls/engine.js +102 -0
  110. package/dist/default/playback/engines/hls/engine.js.map +1 -0
  111. package/dist/dev/core/{actor.d.ts → actors/actor.d.ts} +2 -2
  112. package/dist/dev/core/actors/actor.d.ts.map +1 -0
  113. package/dist/dev/core/actors/create-machine-actor.d.ts +125 -0
  114. package/dist/dev/core/actors/create-machine-actor.d.ts.map +1 -0
  115. package/dist/{default/core → dev/core/actors}/create-machine-actor.js +3 -3
  116. package/dist/dev/core/actors/create-machine-actor.js.map +1 -0
  117. package/dist/dev/core/actors/create-transition-actor.d.ts +42 -0
  118. package/dist/dev/core/actors/create-transition-actor.d.ts.map +1 -0
  119. package/dist/dev/core/{create-transition-actor.js → actors/create-transition-actor.js} +3 -3
  120. package/dist/dev/core/actors/create-transition-actor.js.map +1 -0
  121. package/dist/dev/core/composition/create-composition.d.ts +274 -0
  122. package/dist/dev/core/composition/create-composition.d.ts.map +1 -0
  123. package/dist/dev/core/composition/create-composition.js +85 -0
  124. package/dist/dev/core/composition/create-composition.js.map +1 -0
  125. package/dist/dev/core/composition/share-signals.d.ts +45 -0
  126. package/dist/dev/core/composition/share-signals.d.ts.map +1 -0
  127. package/dist/dev/core/composition/share-signals.js +37 -0
  128. package/dist/dev/core/composition/share-signals.js.map +1 -0
  129. package/dist/dev/core/machine.d.ts +0 -1
  130. package/dist/dev/core/machine.d.ts.map +1 -1
  131. package/dist/dev/core/reactors/create-machine-reactor.d.ts +93 -0
  132. package/dist/dev/core/reactors/create-machine-reactor.d.ts.map +1 -0
  133. package/dist/{default/core → dev/core/reactors}/create-machine-reactor.js +4 -4
  134. package/dist/dev/core/reactors/create-machine-reactor.js.map +1 -0
  135. package/dist/dev/core/signals/primitives.d.ts +38 -1
  136. package/dist/dev/core/signals/primitives.d.ts.map +1 -1
  137. package/dist/dev/core/signals/primitives.js +33 -11
  138. package/dist/dev/core/signals/primitives.js.map +1 -1
  139. package/dist/dev/core/tasks/task.d.ts +115 -0
  140. package/dist/dev/core/tasks/task.d.ts.map +1 -0
  141. package/dist/dev/core/{task.js → tasks/task.js} +2 -2
  142. package/dist/dev/core/tasks/task.js.map +1 -0
  143. package/dist/dev/dom.d.ts +9 -10
  144. package/dist/dev/dom.js +9 -10
  145. package/dist/dev/hls.d.ts +3 -0
  146. package/dist/dev/hls.js +3 -0
  147. package/dist/dev/index.d.ts +15 -3
  148. package/dist/dev/index.d.ts.map +1 -1
  149. package/dist/dev/index.js +12 -3
  150. package/dist/dev/index.js.map +1 -1
  151. package/dist/dev/media/abr/quality-selection.d.ts +22 -0
  152. package/dist/dev/media/abr/quality-selection.d.ts.map +1 -0
  153. package/dist/dev/media/abr/quality-selection.js +83 -0
  154. package/dist/dev/media/abr/quality-selection.js.map +1 -0
  155. package/dist/dev/media/buffer/back-buffer.d.ts +14 -0
  156. package/dist/dev/media/buffer/back-buffer.d.ts.map +1 -0
  157. package/dist/dev/{core → media}/buffer/back-buffer.js +2 -2
  158. package/dist/dev/media/buffer/back-buffer.js.map +1 -0
  159. package/dist/dev/media/buffer/forward-buffer.d.ts +14 -0
  160. package/dist/dev/media/buffer/forward-buffer.d.ts.map +1 -0
  161. package/dist/{default/core → dev/media}/buffer/forward-buffer.js +14 -2
  162. package/dist/dev/media/buffer/forward-buffer.js.map +1 -0
  163. package/dist/dev/{dom/media → media/dom/mse}/append-segment.d.ts +1 -1
  164. package/dist/dev/media/dom/mse/append-segment.d.ts.map +1 -0
  165. package/dist/{default/dom/media → dev/media/dom/mse}/append-segment.js +1 -1
  166. package/dist/dev/media/dom/mse/append-segment.js.map +1 -0
  167. package/dist/dev/{dom/media → media/dom/mse}/buffer-flusher.d.ts +1 -1
  168. package/dist/dev/media/dom/mse/buffer-flusher.d.ts.map +1 -0
  169. package/dist/dev/{dom/media → media/dom/mse}/buffer-flusher.js +1 -1
  170. package/dist/dev/media/dom/mse/buffer-flusher.js.map +1 -0
  171. package/dist/dev/media/dom/mse/duration.js +78 -0
  172. package/dist/dev/media/dom/mse/duration.js.map +1 -0
  173. package/dist/dev/media/dom/mse/end-of-stream.js +20 -0
  174. package/dist/dev/media/dom/mse/end-of-stream.js.map +1 -0
  175. package/dist/dev/{dom/media → media/dom/mse}/mediasource-setup.js +63 -18
  176. package/dist/dev/media/dom/mse/mediasource-setup.js.map +1 -0
  177. package/dist/dev/media/dom/text/resolve-vtt-segment.d.ts +12 -0
  178. package/dist/dev/media/dom/text/resolve-vtt-segment.d.ts.map +1 -0
  179. package/dist/dev/{dom/text/parse-vtt-segment.js → media/dom/text/resolve-vtt-segment.js} +5 -5
  180. package/dist/dev/media/dom/text/resolve-vtt-segment.js.map +1 -0
  181. package/dist/dev/media/dom/text/text-track-slots.d.ts +31 -0
  182. package/dist/dev/media/dom/text/text-track-slots.d.ts.map +1 -0
  183. package/dist/dev/media/dom/text/text-track-slots.js +69 -0
  184. package/dist/dev/media/dom/text/text-track-slots.js.map +1 -0
  185. package/dist/{default/core → dev/media}/hls/parse-attributes.js +1 -1
  186. package/dist/dev/media/hls/parse-attributes.js.map +1 -0
  187. package/dist/{default/core → dev/media}/hls/parse-media-playlist.js +1 -1
  188. package/dist/dev/media/hls/parse-media-playlist.js.map +1 -0
  189. package/dist/{default/core → dev/media}/hls/parse-multivariant.js +2 -2
  190. package/dist/dev/media/hls/parse-multivariant.js.map +1 -0
  191. package/dist/dev/{core → media}/hls/resolve-url.js +1 -1
  192. package/dist/dev/media/hls/resolve-url.js.map +1 -0
  193. package/dist/dev/media/primitives/select-tracks.js +45 -0
  194. package/dist/dev/media/primitives/select-tracks.js.map +1 -0
  195. package/dist/dev/{core → media}/types/index.d.ts +30 -2
  196. package/dist/dev/media/types/index.d.ts.map +1 -0
  197. package/dist/dev/media/types/index.js +36 -0
  198. package/dist/dev/media/types/index.js.map +1 -0
  199. package/dist/dev/media/utils/preload.js +22 -0
  200. package/dist/dev/media/utils/preload.js.map +1 -0
  201. package/dist/dev/media/utils/track-selection.js +47 -0
  202. package/dist/dev/media/utils/track-selection.js.map +1 -0
  203. package/dist/dev/media/utils/tracks.js +103 -0
  204. package/dist/dev/media/utils/tracks.js.map +1 -0
  205. package/dist/dev/{core/abr → network}/bandwidth-estimator.d.ts +17 -2
  206. package/dist/dev/network/bandwidth-estimator.d.ts.map +1 -0
  207. package/dist/dev/{core/abr → network}/bandwidth-estimator.js +6 -5
  208. package/dist/dev/network/bandwidth-estimator.js.map +1 -0
  209. package/dist/{default/dom → dev}/network/chunked-stream-iterable.js +1 -1
  210. package/dist/dev/network/chunked-stream-iterable.js.map +1 -0
  211. package/dist/{default/core/abr → dev/network}/ewma.js +1 -1
  212. package/dist/dev/network/ewma.js.map +1 -0
  213. package/dist/dev/network/fetch.js +109 -0
  214. package/dist/dev/network/fetch.js.map +1 -0
  215. package/dist/dev/playback/actors/dom/segment-loader.d.ts +36 -0
  216. package/dist/dev/playback/actors/dom/segment-loader.d.ts.map +1 -0
  217. package/dist/{default/dom/features/segment-loader-actor.js → dev/playback/actors/dom/segment-loader.js} +24 -15
  218. package/dist/dev/playback/actors/dom/segment-loader.js.map +1 -0
  219. package/dist/dev/{dom/media/source-buffer-actor.d.ts → playback/actors/dom/source-buffer.d.ts} +5 -5
  220. package/dist/dev/playback/actors/dom/source-buffer.d.ts.map +1 -0
  221. package/dist/{default/dom/media/source-buffer-actor.js → dev/playback/actors/dom/source-buffer.js} +8 -8
  222. package/dist/dev/playback/actors/dom/source-buffer.js.map +1 -0
  223. package/dist/dev/playback/actors/dom/text-tracks.d.ts +1 -0
  224. package/dist/{default/dom/features/text-tracks-actor.js → dev/playback/actors/dom/text-tracks.js} +7 -3
  225. package/dist/dev/playback/actors/dom/text-tracks.js.map +1 -0
  226. package/dist/dev/playback/actors/text-track-segment-loader.d.ts +59 -0
  227. package/dist/dev/playback/actors/text-track-segment-loader.d.ts.map +1 -0
  228. package/dist/dev/playback/actors/text-track-segment-loader.js +135 -0
  229. package/dist/dev/playback/actors/text-track-segment-loader.js.map +1 -0
  230. package/dist/dev/playback/actors/text-tracks.d.ts +42 -0
  231. package/dist/dev/playback/actors/text-tracks.d.ts.map +1 -0
  232. package/dist/dev/playback/behaviors/calculate-presentation-duration.d.ts +17 -0
  233. package/dist/dev/playback/behaviors/calculate-presentation-duration.d.ts.map +1 -0
  234. package/dist/dev/playback/behaviors/calculate-presentation-duration.js +58 -0
  235. package/dist/dev/playback/behaviors/calculate-presentation-duration.js.map +1 -0
  236. package/dist/dev/playback/behaviors/dom/end-of-stream.js +178 -0
  237. package/dist/dev/playback/behaviors/dom/end-of-stream.js.map +1 -0
  238. package/dist/dev/playback/behaviors/dom/load-segments.d.ts +62 -0
  239. package/dist/dev/playback/behaviors/dom/load-segments.d.ts.map +1 -0
  240. package/dist/dev/playback/behaviors/dom/load-segments.js +177 -0
  241. package/dist/dev/playback/behaviors/dom/load-segments.js.map +1 -0
  242. package/dist/dev/playback/behaviors/dom/setup-buffer-actors.js +173 -0
  243. package/dist/dev/playback/behaviors/dom/setup-buffer-actors.js.map +1 -0
  244. package/dist/dev/playback/behaviors/dom/setup-mediasource.js +89 -0
  245. package/dist/dev/playback/behaviors/dom/setup-mediasource.js.map +1 -0
  246. package/dist/dev/playback/behaviors/dom/setup-text-track-actors.d.ts +31 -0
  247. package/dist/dev/playback/behaviors/dom/setup-text-track-actors.d.ts.map +1 -0
  248. package/dist/dev/playback/behaviors/dom/setup-text-track-actors.js +54 -0
  249. package/dist/dev/playback/behaviors/dom/setup-text-track-actors.js.map +1 -0
  250. package/dist/dev/playback/behaviors/dom/sync-text-tracks.js +99 -0
  251. package/dist/dev/playback/behaviors/dom/sync-text-tracks.js.map +1 -0
  252. package/dist/dev/playback/behaviors/dom/track-current-time.d.ts +34 -0
  253. package/dist/dev/playback/behaviors/dom/track-current-time.d.ts.map +1 -0
  254. package/dist/dev/playback/behaviors/dom/track-current-time.js +46 -0
  255. package/dist/dev/playback/behaviors/dom/track-current-time.js.map +1 -0
  256. package/dist/dev/playback/behaviors/dom/track-load-triggers.d.ts +48 -0
  257. package/dist/dev/playback/behaviors/dom/track-load-triggers.d.ts.map +1 -0
  258. package/dist/dev/playback/behaviors/dom/track-load-triggers.js +94 -0
  259. package/dist/dev/playback/behaviors/dom/track-load-triggers.js.map +1 -0
  260. package/dist/dev/playback/behaviors/dom/track-playback-rate.d.ts +34 -0
  261. package/dist/dev/playback/behaviors/dom/track-playback-rate.d.ts.map +1 -0
  262. package/dist/dev/playback/behaviors/dom/track-playback-rate.js +37 -0
  263. package/dist/dev/playback/behaviors/dom/track-playback-rate.js.map +1 -0
  264. package/dist/dev/playback/behaviors/dom/update-mediasource-duration.js +90 -0
  265. package/dist/dev/playback/behaviors/dom/update-mediasource-duration.js.map +1 -0
  266. package/dist/dev/playback/behaviors/quality-switching.js +96 -0
  267. package/dist/dev/playback/behaviors/quality-switching.js.map +1 -0
  268. package/dist/dev/playback/behaviors/resolve-presentation.d.ts +7 -0
  269. package/dist/dev/playback/behaviors/resolve-presentation.d.ts.map +1 -0
  270. package/dist/dev/playback/behaviors/resolve-presentation.js +85 -0
  271. package/dist/dev/playback/behaviors/resolve-presentation.js.map +1 -0
  272. package/dist/dev/playback/behaviors/resolve-track.js +97 -0
  273. package/dist/dev/playback/behaviors/resolve-track.js.map +1 -0
  274. package/dist/dev/playback/behaviors/select-tracks.js +132 -0
  275. package/dist/dev/playback/behaviors/select-tracks.js.map +1 -0
  276. package/dist/dev/playback/behaviors/sync-preload.js +59 -0
  277. package/dist/dev/playback/behaviors/sync-preload.js.map +1 -0
  278. package/dist/dev/playback/behaviors/track-types.js +61 -0
  279. package/dist/dev/playback/behaviors/track-types.js.map +1 -0
  280. package/dist/dev/{dom/playback-engine → playback/engines/hls}/adapter.d.ts +14 -10
  281. package/dist/dev/playback/engines/hls/adapter.d.ts.map +1 -0
  282. package/dist/dev/{dom/playback-engine → playback/engines/hls}/adapter.js +36 -25
  283. package/dist/dev/playback/engines/hls/adapter.js.map +1 -0
  284. package/dist/dev/playback/engines/hls/engine.d.ts +177 -0
  285. package/dist/dev/playback/engines/hls/engine.d.ts.map +1 -0
  286. package/dist/dev/playback/engines/hls/engine.js +102 -0
  287. package/dist/dev/playback/engines/hls/engine.js.map +1 -0
  288. package/package.json +8 -9
  289. package/dist/default/core/abr/bandwidth-estimator.js.map +0 -1
  290. package/dist/default/core/abr/ewma.js.map +0 -1
  291. package/dist/default/core/abr/quality-selection.js +0 -53
  292. package/dist/default/core/abr/quality-selection.js.map +0 -1
  293. package/dist/default/core/buffer/back-buffer.js.map +0 -1
  294. package/dist/default/core/buffer/forward-buffer.js.map +0 -1
  295. package/dist/default/core/create-machine-actor.js.map +0 -1
  296. package/dist/default/core/create-machine-reactor.js.map +0 -1
  297. package/dist/default/core/create-transition-actor.js.map +0 -1
  298. package/dist/default/core/features/calculate-presentation-duration.js +0 -53
  299. package/dist/default/core/features/calculate-presentation-duration.js.map +0 -1
  300. package/dist/default/core/features/quality-switching.js +0 -69
  301. package/dist/default/core/features/quality-switching.js.map +0 -1
  302. package/dist/default/core/features/resolve-presentation.js +0 -73
  303. package/dist/default/core/features/resolve-presentation.js.map +0 -1
  304. package/dist/default/core/features/resolve-track.js +0 -66
  305. package/dist/default/core/features/resolve-track.js.map +0 -1
  306. package/dist/default/core/features/select-tracks.js +0 -130
  307. package/dist/default/core/features/select-tracks.js.map +0 -1
  308. package/dist/default/core/features/sync-preload-attribute.js +0 -26
  309. package/dist/default/core/features/sync-preload-attribute.js.map +0 -1
  310. package/dist/default/core/hls/parse-attributes.js.map +0 -1
  311. package/dist/default/core/hls/parse-media-playlist.js.map +0 -1
  312. package/dist/default/core/hls/parse-multivariant.js.map +0 -1
  313. package/dist/default/core/hls/resolve-url.js.map +0 -1
  314. package/dist/default/core/task.js.map +0 -1
  315. package/dist/default/core/types/index.js +0 -15
  316. package/dist/default/core/types/index.js.map +0 -1
  317. package/dist/default/core/utils/generate-id.js +0 -21
  318. package/dist/default/core/utils/generate-id.js.map +0 -1
  319. package/dist/default/core/utils/track-selection.js +0 -39
  320. package/dist/default/core/utils/track-selection.js.map +0 -1
  321. package/dist/default/dom/features/end-of-stream.js +0 -145
  322. package/dist/default/dom/features/end-of-stream.js.map +0 -1
  323. package/dist/default/dom/features/load-segments.js +0 -192
  324. package/dist/default/dom/features/load-segments.js.map +0 -1
  325. package/dist/default/dom/features/load-text-track-cues.js +0 -104
  326. package/dist/default/dom/features/load-text-track-cues.js.map +0 -1
  327. package/dist/default/dom/features/segment-loader-actor.js.map +0 -1
  328. package/dist/default/dom/features/setup-mediasource.js +0 -48
  329. package/dist/default/dom/features/setup-mediasource.js.map +0 -1
  330. package/dist/default/dom/features/setup-sourcebuffer.js +0 -78
  331. package/dist/default/dom/features/setup-sourcebuffer.js.map +0 -1
  332. package/dist/default/dom/features/sync-text-tracks.js +0 -95
  333. package/dist/default/dom/features/sync-text-tracks.js.map +0 -1
  334. package/dist/default/dom/features/text-track-segment-loader-actor.js +0 -55
  335. package/dist/default/dom/features/text-track-segment-loader-actor.js.map +0 -1
  336. package/dist/default/dom/features/text-tracks-actor.js.map +0 -1
  337. package/dist/default/dom/features/track-current-time.js +0 -49
  338. package/dist/default/dom/features/track-current-time.js.map +0 -1
  339. package/dist/default/dom/features/track-playback-initiated.js +0 -66
  340. package/dist/default/dom/features/track-playback-initiated.js.map +0 -1
  341. package/dist/default/dom/features/track-playback-rate.js +0 -32
  342. package/dist/default/dom/features/track-playback-rate.js.map +0 -1
  343. package/dist/default/dom/features/update-duration.js +0 -78
  344. package/dist/default/dom/features/update-duration.js.map +0 -1
  345. package/dist/default/dom/media/append-segment.js.map +0 -1
  346. package/dist/default/dom/media/buffer-flusher.js.map +0 -1
  347. package/dist/default/dom/media/mediasource-setup.js.map +0 -1
  348. package/dist/default/dom/media/source-buffer-actor.js.map +0 -1
  349. package/dist/default/dom/network/chunked-stream-iterable.js.map +0 -1
  350. package/dist/default/dom/network/fetch.js +0 -54
  351. package/dist/default/dom/network/fetch.js.map +0 -1
  352. package/dist/default/dom/playback-engine/adapter.js.map +0 -1
  353. package/dist/default/dom/playback-engine/engine.js +0 -142
  354. package/dist/default/dom/playback-engine/engine.js.map +0 -1
  355. package/dist/default/dom/playback-engine.js +0 -4
  356. package/dist/default/dom/text/parse-vtt-segment.js.map +0 -1
  357. package/dist/dev/core/abr/bandwidth-estimator.d.ts.map +0 -1
  358. package/dist/dev/core/abr/bandwidth-estimator.js.map +0 -1
  359. package/dist/dev/core/abr/ewma.js.map +0 -1
  360. package/dist/dev/core/abr/quality-selection.js +0 -53
  361. package/dist/dev/core/abr/quality-selection.js.map +0 -1
  362. package/dist/dev/core/actor.d.ts.map +0 -1
  363. package/dist/dev/core/buffer/back-buffer.js.map +0 -1
  364. package/dist/dev/core/buffer/forward-buffer.js.map +0 -1
  365. package/dist/dev/core/create-machine-actor.d.ts +0 -12
  366. package/dist/dev/core/create-machine-actor.d.ts.map +0 -1
  367. package/dist/dev/core/create-machine-actor.js.map +0 -1
  368. package/dist/dev/core/create-machine-reactor.d.ts +0 -8
  369. package/dist/dev/core/create-machine-reactor.d.ts.map +0 -1
  370. package/dist/dev/core/create-machine-reactor.js.map +0 -1
  371. package/dist/dev/core/create-transition-actor.d.ts +0 -23
  372. package/dist/dev/core/create-transition-actor.d.ts.map +0 -1
  373. package/dist/dev/core/create-transition-actor.js.map +0 -1
  374. package/dist/dev/core/features/calculate-presentation-duration.js +0 -53
  375. package/dist/dev/core/features/calculate-presentation-duration.js.map +0 -1
  376. package/dist/dev/core/features/quality-switching.js +0 -69
  377. package/dist/dev/core/features/quality-switching.js.map +0 -1
  378. package/dist/dev/core/features/resolve-presentation.js +0 -73
  379. package/dist/dev/core/features/resolve-presentation.js.map +0 -1
  380. package/dist/dev/core/features/resolve-track.js +0 -66
  381. package/dist/dev/core/features/resolve-track.js.map +0 -1
  382. package/dist/dev/core/features/select-tracks.js +0 -130
  383. package/dist/dev/core/features/select-tracks.js.map +0 -1
  384. package/dist/dev/core/features/sync-preload-attribute.js +0 -26
  385. package/dist/dev/core/features/sync-preload-attribute.js.map +0 -1
  386. package/dist/dev/core/hls/parse-attributes.js.map +0 -1
  387. package/dist/dev/core/hls/parse-media-playlist.js.map +0 -1
  388. package/dist/dev/core/hls/parse-multivariant.js.map +0 -1
  389. package/dist/dev/core/hls/resolve-url.js.map +0 -1
  390. package/dist/dev/core/task.js.map +0 -1
  391. package/dist/dev/core/types/index.d.ts.map +0 -1
  392. package/dist/dev/core/types/index.js +0 -15
  393. package/dist/dev/core/types/index.js.map +0 -1
  394. package/dist/dev/core/utils/generate-id.js +0 -21
  395. package/dist/dev/core/utils/generate-id.js.map +0 -1
  396. package/dist/dev/core/utils/track-selection.d.ts +0 -16
  397. package/dist/dev/core/utils/track-selection.d.ts.map +0 -1
  398. package/dist/dev/core/utils/track-selection.js +0 -39
  399. package/dist/dev/core/utils/track-selection.js.map +0 -1
  400. package/dist/dev/dom/features/end-of-stream.js +0 -145
  401. package/dist/dev/dom/features/end-of-stream.js.map +0 -1
  402. package/dist/dev/dom/features/load-segments.d.ts +0 -72
  403. package/dist/dev/dom/features/load-segments.d.ts.map +0 -1
  404. package/dist/dev/dom/features/load-segments.js +0 -192
  405. package/dist/dev/dom/features/load-segments.js.map +0 -1
  406. package/dist/dev/dom/features/load-text-track-cues.d.ts +0 -85
  407. package/dist/dev/dom/features/load-text-track-cues.d.ts.map +0 -1
  408. package/dist/dev/dom/features/load-text-track-cues.js +0 -104
  409. package/dist/dev/dom/features/load-text-track-cues.js.map +0 -1
  410. package/dist/dev/dom/features/segment-loader-actor.d.ts +0 -1
  411. package/dist/dev/dom/features/segment-loader-actor.js.map +0 -1
  412. package/dist/dev/dom/features/setup-mediasource.js +0 -48
  413. package/dist/dev/dom/features/setup-mediasource.js.map +0 -1
  414. package/dist/dev/dom/features/setup-sourcebuffer.d.ts +0 -9
  415. package/dist/dev/dom/features/setup-sourcebuffer.d.ts.map +0 -1
  416. package/dist/dev/dom/features/setup-sourcebuffer.js +0 -78
  417. package/dist/dev/dom/features/setup-sourcebuffer.js.map +0 -1
  418. package/dist/dev/dom/features/sync-text-tracks.js +0 -95
  419. package/dist/dev/dom/features/sync-text-tracks.js.map +0 -1
  420. package/dist/dev/dom/features/text-track-segment-loader-actor.d.ts +0 -12
  421. package/dist/dev/dom/features/text-track-segment-loader-actor.d.ts.map +0 -1
  422. package/dist/dev/dom/features/text-track-segment-loader-actor.js +0 -55
  423. package/dist/dev/dom/features/text-track-segment-loader-actor.js.map +0 -1
  424. package/dist/dev/dom/features/text-tracks-actor.d.ts +0 -31
  425. package/dist/dev/dom/features/text-tracks-actor.d.ts.map +0 -1
  426. package/dist/dev/dom/features/text-tracks-actor.js.map +0 -1
  427. package/dist/dev/dom/features/track-current-time.d.ts +0 -40
  428. package/dist/dev/dom/features/track-current-time.d.ts.map +0 -1
  429. package/dist/dev/dom/features/track-current-time.js +0 -49
  430. package/dist/dev/dom/features/track-current-time.js.map +0 -1
  431. package/dist/dev/dom/features/track-playback-initiated.d.ts +0 -46
  432. package/dist/dev/dom/features/track-playback-initiated.d.ts.map +0 -1
  433. package/dist/dev/dom/features/track-playback-initiated.js +0 -66
  434. package/dist/dev/dom/features/track-playback-initiated.js.map +0 -1
  435. package/dist/dev/dom/features/track-playback-rate.d.ts +0 -34
  436. package/dist/dev/dom/features/track-playback-rate.d.ts.map +0 -1
  437. package/dist/dev/dom/features/track-playback-rate.js +0 -32
  438. package/dist/dev/dom/features/track-playback-rate.js.map +0 -1
  439. package/dist/dev/dom/features/update-duration.js +0 -78
  440. package/dist/dev/dom/features/update-duration.js.map +0 -1
  441. package/dist/dev/dom/media/append-segment.d.ts.map +0 -1
  442. package/dist/dev/dom/media/append-segment.js.map +0 -1
  443. package/dist/dev/dom/media/buffer-flusher.d.ts.map +0 -1
  444. package/dist/dev/dom/media/buffer-flusher.js.map +0 -1
  445. package/dist/dev/dom/media/mediasource-setup.js.map +0 -1
  446. package/dist/dev/dom/media/source-buffer-actor.d.ts.map +0 -1
  447. package/dist/dev/dom/media/source-buffer-actor.js.map +0 -1
  448. package/dist/dev/dom/network/chunked-stream-iterable.js.map +0 -1
  449. package/dist/dev/dom/network/fetch.js +0 -54
  450. package/dist/dev/dom/network/fetch.js.map +0 -1
  451. package/dist/dev/dom/playback-engine/adapter.d.ts.map +0 -1
  452. package/dist/dev/dom/playback-engine/adapter.js.map +0 -1
  453. package/dist/dev/dom/playback-engine/engine.d.ts +0 -124
  454. package/dist/dev/dom/playback-engine/engine.d.ts.map +0 -1
  455. package/dist/dev/dom/playback-engine/engine.js +0 -142
  456. package/dist/dev/dom/playback-engine/engine.js.map +0 -1
  457. package/dist/dev/dom/playback-engine.d.ts +0 -4
  458. package/dist/dev/dom/playback-engine.js +0 -4
  459. package/dist/dev/dom/text/parse-vtt-segment.d.ts +0 -12
  460. package/dist/dev/dom/text/parse-vtt-segment.d.ts.map +0 -1
  461. package/dist/dev/dom/text/parse-vtt-segment.js.map +0 -1
@@ -0,0 +1,96 @@
1
+ import { computed, peek } from "../../core/signals/primitives.js";
2
+ import { defineBehavior } from "../../core/composition/create-composition.js";
3
+ import { createMachineReactor } from "../../core/reactors/create-machine-reactor.js";
4
+ import { isResolvedPresentation } from "../../media/types/index.js";
5
+ import { getTracksByType } from "../../media/utils/tracks.js";
6
+ import { DEFAULT_BANDWIDTH_CONFIG, getBandwidthEstimate } from "../../network/bandwidth-estimator.js";
7
+ import { DEFAULT_QUALITY_CONFIG, selectLowestQuality, selectQuality } from "../../media/abr/quality-selection.js";
8
+ function setupQualitySwitching({ state, config }) {
9
+ const safetyMargin = config.quality?.safetyMargin ?? DEFAULT_QUALITY_CONFIG.safetyMargin;
10
+ const upgradeMargin = config.quality?.upgradeMargin ?? DEFAULT_QUALITY_CONFIG.upgradeMargin;
11
+ const initialBandwidth = config.initialBandwidth ?? 5e6;
12
+ const bandwidthConfig = {
13
+ ...DEFAULT_BANDWIDTH_CONFIG,
14
+ ...config.bandwidth
15
+ };
16
+ const { selectionKey, userSelectionKey, getTracks, selectOptimal } = config;
17
+ const derivedStateSignal = computed(() => isResolvedPresentation(state.presentation.get()) ? "presentation-resolved" : "presentation-unresolved");
18
+ return createMachineReactor({
19
+ initial: "presentation-unresolved",
20
+ monitor: () => derivedStateSignal.get(),
21
+ states: {
22
+ "presentation-unresolved": {},
23
+ "presentation-resolved": {
24
+ entry: () => () => state[selectionKey].set(void 0),
25
+ effects: [() => {
26
+ const presentation = peek(state.presentation);
27
+ if (!presentation) return;
28
+ const allTracks = getTracks(presentation);
29
+ const [firstAllTrack] = allTracks;
30
+ if (!firstAllTrack) return;
31
+ const userFilter = state[userSelectionKey].get();
32
+ const matching = userFilter ? allTracks.filter((track) => {
33
+ for (const key in userFilter) {
34
+ const filterValue = userFilter[key];
35
+ if (filterValue !== void 0 && track[key] !== filterValue) return false;
36
+ }
37
+ return true;
38
+ }) : allTracks;
39
+ const candidates = matching.length > 0 ? matching : allTracks;
40
+ if (!candidates.length) return;
41
+ const selectedId = state[selectionKey].get();
42
+ if (candidates.length === 1) {
43
+ if (candidates[0].id !== selectedId) state[selectionKey].set(candidates[0].id);
44
+ return;
45
+ }
46
+ const bandwidth = getBandwidthEstimate(state.bandwidthState.get(), initialBandwidth, bandwidthConfig);
47
+ if (!selectedId && config.picker) {
48
+ const id = config.picker(presentation, config);
49
+ if (id) {
50
+ state[selectionKey].set(id);
51
+ return;
52
+ }
53
+ }
54
+ const optimal = selectOptimal(candidates, bandwidth, {
55
+ safetyMargin,
56
+ upgradeMargin,
57
+ currentTrack: candidates.find((t) => t.id === selectedId)
58
+ }) ?? selectLowestQuality(candidates);
59
+ if (optimal.id !== selectedId) state[selectionKey].set(optimal.id);
60
+ }]
61
+ }
62
+ }
63
+ });
64
+ }
65
+ /**
66
+ * Manage `selectedVideoTrackId`: pick a default on src load, dynamically
67
+ * adjust based on bandwidth, clear on src unload. Honors
68
+ * `userVideoTrackSelection` as a partial-track constraint on candidates;
69
+ * short-circuits ABR when the constraint narrows to a single track.
70
+ *
71
+ * @example
72
+ * const reactor = switchVideoQuality.setup({ state });
73
+ */
74
+ const switchVideoQuality = defineBehavior({
75
+ stateKeys: [
76
+ "presentation",
77
+ "bandwidthState",
78
+ "selectedVideoTrackId",
79
+ "userVideoTrackSelection"
80
+ ],
81
+ contextKeys: [],
82
+ setup: ({ state, config }) => setupQualitySwitching({
83
+ state,
84
+ config: {
85
+ ...config,
86
+ selectionKey: "selectedVideoTrackId",
87
+ userSelectionKey: "userVideoTrackSelection",
88
+ getTracks: (presentation) => getTracksByType(presentation, "video"),
89
+ selectOptimal: selectQuality
90
+ }
91
+ })
92
+ });
93
+ //#endregion
94
+ export { switchVideoQuality };
95
+
96
+ //# sourceMappingURL=quality-switching.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quality-switching.js","names":[],"sources":["../../../../src/playback/behaviors/quality-switching.ts"],"sourcesContent":["/**\n * **Manage the per-type ABR-eligible track selection slot.** While a\n * presentation is resolved, owns the slot's lifecycle: pick a default,\n * dynamically adjust based on media metrics (today: bandwidth via\n * `selectQuality`), and clear on src unload. User intent is expressed as a\n * partial-track description in a sibling slot (e.g.\n * `userVideoTrackSelection`) which constrains the candidate set for\n * selection — when the constraint narrows candidates to exactly one, the\n * choice is fully determined and ABR is short-circuited (no bandwidth read,\n * no effect re-fire on bandwidth changes).\n *\n * Lifecycle: `'presentation-unresolved'` ↔ `'presentation-resolved'`. The\n * `'presentation-resolved'` state owns the selection slot; its\n * entry-returned cleanup clears the slot on exit (canonical\n * cleanup-binds-to-setup per `reactors.md`).\n *\n * Hysteresis: downgrades apply immediately; upgrades require the optimal\n * track's bandwidth to exceed the current track's by `upgradeMargin`. No\n * temporal state — short-term smoothing is the bandwidth estimator's job.\n *\n * Initial pick is configurable via `config.picker` — pass any `TrackPicker`\n * to override the bandwidth-aware default for the empty-slot case. ABR\n * re-evaluation (downgrade/upgrade) is unaffected. Composing\n * `switchVideoQuality` with `picker: (p) => pickFirstTrackId(p, 'video')`\n * yields \"first track at load, ABR adjusts from there\" — a non-bandwidth\n * initial pick paired with bandwidth-driven re-evaluation.\n */\n\nimport { defineBehavior } from '../../core/composition/create-composition';\nimport { createMachineReactor } from '../../core/reactors/create-machine-reactor';\nimport { computed, peek, type ReadonlySignal, type Signal } from '../../core/signals/primitives';\nimport {\n DEFAULT_QUALITY_CONFIG,\n type QualityConfig,\n selectLowestQuality,\n selectQuality,\n} from '../../media/abr/quality-selection';\nimport type { TrackPicker } from '../../media/primitives/select-tracks';\nimport {\n isResolvedPresentation,\n type MaybeResolvedPresentation,\n type PartiallyResolvedVideoTrack,\n type VideoTrack,\n} from '../../media/types';\nimport { getTracksByType } from '../../media/utils/tracks';\nimport type { BandwidthConfig, BandwidthState } from '../../network/bandwidth-estimator';\nimport { DEFAULT_BANDWIDTH_CONFIG, getBandwidthEstimate } from '../../network/bandwidth-estimator';\n\nexport interface QualitySwitchingState {\n presentation?: MaybeResolvedPresentation;\n bandwidthState?: BandwidthState;\n selectedVideoTrackId?: string;\n /**\n * Partial-track description expressing user intent. When set, narrows\n * the ABR candidate set to tracks matching every present field. The\n * common case is `{ id: 'specific-track-id' }` for a \"manual quality\"\n * pick, but other partial shapes work — e.g., `{ height: 720 }` would\n * constrain to 720p tracks; ABR continues to pick among them.\n *\n * When the narrowed candidates contain exactly one track, ABR is\n * short-circuited entirely (no bandwidth read, no effect re-fire).\n *\n * When the filter matches no tracks in the current presentation (e.g.,\n * user-picked id from a previous source doesn't exist here), falls back\n * to the unfiltered set rather than stalling playback.\n */\n userVideoTrackSelection?: Partial<VideoTrack>;\n}\n\nexport interface QualitySwitchingConfig {\n /**\n * Quality-selection tuning. `safetyMargin` is the bandwidth-headroom\n * multiplier; `upgradeMargin` is the hysteresis ratio gating upgrades.\n * Defaults: `DEFAULT_QUALITY_CONFIG` (0.85 / 1.15).\n */\n quality?: Partial<QualityConfig>;\n\n /**\n * Bandwidth-estimator tuning passed through to `getBandwidthEstimate`.\n * Merged over `DEFAULT_BANDWIDTH_CONFIG`.\n */\n bandwidth?: Partial<BandwidthConfig>;\n\n /**\n * Bandwidth estimate in bps to use before enough samples have been collected.\n * Default: 5_000_000 (5 Mbps).\n */\n initialBandwidth?: number;\n\n /**\n * Override the initial-pick algorithm. When set, the picker is called the\n * first time the slot is empty in the `'presentation-resolved'` state;\n * its returned id is set verbatim (no bandwidth-aware logic). Subsequent\n * ABR re-evaluation (downgrade/upgrade by bandwidth + hysteresis) is\n * unaffected and runs as usual.\n *\n * Default (no picker): a bandwidth-aware initial pick driven by\n * `initialBandwidth` and `quality.safetyMargin`, identical to ABR's\n * downgrade branch.\n *\n * Honors of `userVideoTrackSelection` are the picker's responsibility\n * when overridden. If the picker returns `undefined`, the bandwidth-aware\n * default pick fires (graceful fallback).\n */\n picker?: TrackPicker<QualitySwitchingConfig>;\n}\n\n/** Default initial-bandwidth value used by quality-switching variants before measurements arrive. */\nexport const DEFAULT_INITIAL_BANDWIDTH = 5_000_000;\n\n// ============================================================================\n// Specialization helper\n//\n// `setupQualitySwitching` has the same shape as a Behavior `setup` function:\n// `({ state, config }) => Reactor`. Each `switchXQuality` export below calls\n// it from inside its own `defineBehavior` setup, passing the per-type slot\n// keys, track type, and selection algorithm explicitly via three generic\n// parameters — `S` (selection slot key), `U` (user-selection slot key), `T`\n// (ABR track type). The per-type export is the single point that ties them\n// together; the helper stays slot-agnostic.\n//\n// -- Design note: why narrow `SelectionKey` / `UserSelectionKey` unions ----\n// Goal we did not reach: have callers \"fully pass in\" the slot keys, with\n// the helper enforcing zero internal knowledge of which literals are valid.\n// What blocks it: indexing a mapped-type intersection by a generic key.\n// When `S extends keyof QualitySwitchingState` (or `string`), TS conservatively\n// treats `state[selectionKey]` as the union of every possible match across\n// the intersected mapped portions — including the fixed-key signals\n// (`presentation`, `bandwidthState`) — and widens to their value-type union.\n// The sibling pattern hits the same constraint and answers it the same way:\n// `SelectedTrackKey` in `select-tracks.ts:43` is a hardcoded narrow union\n// for the same reason.\n//\n// Routes considered for \"fully passed in\", with trade-offs (left here for\n// the larger-group conversation):\n//\n// A. Derived constraint — `Exclude<keyof QualitySwitchingState, 'presentation'\n// | 'bandwidthState'>`. Removes the literal enumeration; same narrowness,\n// computed from the state shape. Still \"knows\" the fixed-key names.\n// B. Broad constraint (`S extends keyof QualitySwitchingState`) + access-site\n// casts (`state[selectionKey] as Signal<string | undefined>` at the top\n// of the helper). Caller passes any keys; the type system stops checking\n// S ≠ U or that the keys actually refer to selection slots. Once you\n// cast at the top, the body is structurally the original remap pattern\n// with extra ceremony.\n// C. Remap pattern (pre-refactor) — caller passes signals under logical\n// names (`selection`, `userSelection`); helper has no key generics.\n// Cleanest types; inconsistent with the sibling helpers.\n//\n// Current pick is the narrow-union route because it matches siblings and\n// the unions read as documentation (\"these are the slots this helper\n// manages\") rather than restriction. Extending for audio is one literal\n// per union.\n// --------------------------------------------------------------------------\n// ============================================================================\n\ntype AbrTrack = { id: string; bandwidth: number };\n\ntype SelectionKey = 'selectedVideoTrackId';\ntype UserSelectionKey = 'userVideoTrackSelection';\n\n// Each mapped value references `P` so TS keeps the per-key dependency and\n// resolves `state[selectionKey]` / `state[userSelectionKey]` to the right\n// arm. `T` (track type) deliberately stays out of the state map — pulling\n// it in detaches the user-selection mapped value from `P` and TS collapses\n// the intersection. T flows through `QualitySwitchingSetupConfig` instead;\n// the user-filter access casts at the read site (see below).\ntype QualitySwitchingStateMap<S extends SelectionKey, U extends UserSelectionKey> = {\n presentation: ReadonlySignal<QualitySwitchingState['presentation']>;\n bandwidthState: ReadonlySignal<QualitySwitchingState['bandwidthState']>;\n} & { [P in S]: Signal<QualitySwitchingState[P]> } & { [P in U]: ReadonlySignal<QualitySwitchingState[P]> };\n\ninterface QualitySwitchingSetupConfig<S extends SelectionKey, U extends UserSelectionKey, T extends AbrTrack>\n extends QualitySwitchingConfig {\n selectionKey: S;\n userSelectionKey: U;\n getTracks: (presentation: MaybeResolvedPresentation) => readonly T[];\n selectOptimal: (\n tracks: readonly T[],\n bandwidth: number,\n opts: { safetyMargin: number; upgradeMargin: number; currentTrack?: T }\n ) => T | undefined;\n}\n\nfunction setupQualitySwitching<S extends SelectionKey, U extends UserSelectionKey, T extends AbrTrack>({\n state,\n config,\n}: {\n state: QualitySwitchingStateMap<S, U>;\n config: QualitySwitchingSetupConfig<S, U, T>;\n}) {\n const safetyMargin = config.quality?.safetyMargin ?? DEFAULT_QUALITY_CONFIG.safetyMargin;\n const upgradeMargin = config.quality?.upgradeMargin ?? DEFAULT_QUALITY_CONFIG.upgradeMargin;\n const initialBandwidth = config.initialBandwidth ?? DEFAULT_INITIAL_BANDWIDTH;\n const bandwidthConfig: BandwidthConfig = { ...DEFAULT_BANDWIDTH_CONFIG, ...config.bandwidth };\n const { selectionKey, userSelectionKey, getTracks, selectOptimal } = config;\n\n const derivedStateSignal = computed(() =>\n isResolvedPresentation(state.presentation.get())\n ? ('presentation-resolved' as const)\n : ('presentation-unresolved' as const)\n );\n\n return createMachineReactor({\n initial: 'presentation-unresolved',\n monitor: () => derivedStateSignal.get(),\n states: {\n 'presentation-unresolved': {},\n 'presentation-resolved': {\n // Canonical cleanup-binds-to-setup: the selection slot's valid\n // lifespan is exactly 'presentation-resolved'. Clear fires on\n // 'presentation-resolved' exit, covering both src unload and\n // behavior destroy.\n entry: () => () => state[selectionKey].set(undefined),\n effects: [\n () => {\n const presentation = peek(state.presentation);\n if (!presentation) return;\n\n const allTracks = getTracks(presentation);\n const [firstAllTrack] = allTracks;\n if (!firstAllTrack) return;\n\n // State stores the filter as `Partial<VideoTrack>` (user-facing\n // shape — includes V-only fields like `height`); the helper\n // works against `Partial<T>` so filter and track access share\n // one index type. Filter keys absent on a partially-resolved\n // track read as `undefined` and just exclude that track.\n const userFilter = state[userSelectionKey].get() as Partial<T> | undefined;\n const matching = userFilter\n ? allTracks.filter((track) => {\n for (const key in userFilter) {\n const filterValue = userFilter[key as keyof T];\n if (filterValue !== undefined && track[key as keyof T] !== filterValue) return false;\n }\n return true;\n })\n : allTracks;\n // Fall back to all tracks when the filter excludes everything\n // (e.g., user-picked id doesn't exist in the current source).\n const candidates = matching.length > 0 ? matching : allTracks;\n if (!candidates.length) return;\n\n const selectedId = state[selectionKey].get();\n\n // Common case: user has fully constrained the choice (e.g.,\n // `{ id: 'specific-720p' }` narrows to a single track). Skip\n // the ABR path — and crucially, don't read `bandwidthState`\n // so the effect doesn't re-fire on bandwidth changes while\n // the user's selection holds.\n if (candidates.length === 1) {\n if (candidates[0]!.id !== selectedId) state[selectionKey].set(candidates[0]!.id);\n return;\n }\n\n // Read bandwidth up front to establish the signal subscription —\n // future bandwidth changes must re-fire this effect even when\n // the picker branch below takes the early-return path. (If the\n // picker bails out without ever touching `bandwidthState`, ABR\n // would otherwise be deaf to subsequent bandwidth changes.)\n //\n // Single path for pre-trust and post-trust:\n // `getBandwidthEstimate` returns `initialBandwidth` when state\n // is undefined or bytes sampled hasn't crossed `minTotalBytes`,\n // so the initial pick + early-ABR window run the same\n // `selectOptimal` path as a fully-trusted measurement.\n const bandwidth = getBandwidthEstimate(state.bandwidthState.get(), initialBandwidth, bandwidthConfig);\n\n // Picker-driven initial pick: when the slot is empty and the\n // caller supplied a `picker`, defer to it instead of the\n // bandwidth-aware default. The picker sees the full\n // presentation (not narrowed by `userVideoTrackSelection`) —\n // honoring the filter is the picker's responsibility when\n // overridden. Returning `undefined` falls through to the\n // bandwidth-aware default (graceful fallback).\n //\n // ABR re-evaluation (downgrade/upgrade by bandwidth) runs as\n // usual on subsequent effect re-runs once the slot is set.\n if (!selectedId && config.picker) {\n const id = config.picker(presentation, config);\n if (id) {\n state[selectionKey].set(id);\n return;\n }\n }\n // `selectOptimal` decides the track to apply now given current\n // selection + bandwidth + tuning. It returns:\n // - the optimal when no current track or on a downgrade\n // - the optimal when an upgrade clears `upgradeMargin`\n // - the current track itself when an upgrade doesn't clear margin\n // (caller's id-compare below no-ops in that case)\n // Outer `?? selectLowestQuality` is defensive — `selectQuality` falls\n // back to lowest internally; this catches future impls that don't.\n // `selectLowestQuality` returns `undefined` only on empty input,\n // and `candidates.length >= 2` here.\n const currentTrack = candidates.find((t) => t.id === selectedId);\n const optimal =\n selectOptimal(candidates, bandwidth, { safetyMargin, upgradeMargin, currentTrack }) ??\n selectLowestQuality(candidates)!;\n if (optimal.id !== selectedId) state[selectionKey].set(optimal.id);\n },\n ],\n },\n },\n });\n}\n\n// ============================================================================\n// Specialized exports — one per ABR-enabled track type\n// ============================================================================\n\n/**\n * Manage `selectedVideoTrackId`: pick a default on src load, dynamically\n * adjust based on bandwidth, clear on src unload. Honors\n * `userVideoTrackSelection` as a partial-track constraint on candidates;\n * short-circuits ABR when the constraint narrows to a single track.\n *\n * @example\n * const reactor = switchVideoQuality.setup({ state });\n */\nexport const switchVideoQuality = defineBehavior({\n stateKeys: ['presentation', 'bandwidthState', 'selectedVideoTrackId', 'userVideoTrackSelection'],\n contextKeys: [],\n setup: ({\n state,\n config,\n }: {\n state: QualitySwitchingStateMap<'selectedVideoTrackId', 'userVideoTrackSelection'>;\n config?: QualitySwitchingConfig;\n }) =>\n setupQualitySwitching<'selectedVideoTrackId', 'userVideoTrackSelection', PartiallyResolvedVideoTrack | VideoTrack>({\n state,\n config: {\n ...config,\n selectionKey: 'selectedVideoTrackId',\n userSelectionKey: 'userVideoTrackSelection',\n getTracks: (presentation) =>\n getTracksByType(presentation, 'video') as readonly (PartiallyResolvedVideoTrack | VideoTrack)[],\n selectOptimal: selectQuality,\n },\n }),\n});\n"],"mappings":";;;;;;;AAwLA,SAAS,sBAA8F,EACrG,OACA,UAIC;CACD,MAAM,eAAe,OAAO,SAAS,gBAAgB,uBAAuB;CAC5E,MAAM,gBAAgB,OAAO,SAAS,iBAAiB,uBAAuB;CAC9E,MAAM,mBAAmB,OAAO,oBAAA;CAChC,MAAM,kBAAmC;EAAE,GAAG;EAA0B,GAAG,OAAO;EAAW;CAC7F,MAAM,EAAE,cAAc,kBAAkB,WAAW,kBAAkB;CAErE,MAAM,qBAAqB,eACzB,uBAAuB,MAAM,aAAa,KAAK,CAAC,GAC3C,0BACA,0BACN;AAED,QAAO,qBAAqB;EAC1B,SAAS;EACT,eAAe,mBAAmB,KAAK;EACvC,QAAQ;GACN,2BAA2B,EAAE;GAC7B,yBAAyB;IAKvB,mBAAmB,MAAM,cAAc,IAAI,KAAA,EAAU;IACrD,SAAS,OACD;KACJ,MAAM,eAAe,KAAK,MAAM,aAAa;AAC7C,SAAI,CAAC,aAAc;KAEnB,MAAM,YAAY,UAAU,aAAa;KACzC,MAAM,CAAC,iBAAiB;AACxB,SAAI,CAAC,cAAe;KAOpB,MAAM,aAAa,MAAM,kBAAkB,KAAK;KAChD,MAAM,WAAW,aACb,UAAU,QAAQ,UAAU;AAC1B,WAAK,MAAM,OAAO,YAAY;OAC5B,MAAM,cAAc,WAAW;AAC/B,WAAI,gBAAgB,KAAA,KAAa,MAAM,SAAoB,YAAa,QAAO;;AAEjF,aAAO;OACP,GACF;KAGJ,MAAM,aAAa,SAAS,SAAS,IAAI,WAAW;AACpD,SAAI,CAAC,WAAW,OAAQ;KAExB,MAAM,aAAa,MAAM,cAAc,KAAK;AAO5C,SAAI,WAAW,WAAW,GAAG;AAC3B,UAAI,WAAW,GAAI,OAAO,WAAY,OAAM,cAAc,IAAI,WAAW,GAAI,GAAG;AAChF;;KAcF,MAAM,YAAY,qBAAqB,MAAM,eAAe,KAAK,EAAE,kBAAkB,gBAAgB;AAYrG,SAAI,CAAC,cAAc,OAAO,QAAQ;MAChC,MAAM,KAAK,OAAO,OAAO,cAAc,OAAO;AAC9C,UAAI,IAAI;AACN,aAAM,cAAc,IAAI,GAAG;AAC3B;;;KAcJ,MAAM,UACJ,cAAc,YAAY,WAAW;MAAE;MAAc;MAAe,cAFjD,WAAW,MAAM,MAAM,EAAE,OAAO,WAAW;MAEoB,CAAC,IACnF,oBAAoB,WAAW;AACjC,SAAI,QAAQ,OAAO,WAAY,OAAM,cAAc,IAAI,QAAQ,GAAG;MAErE;IACF;GACF;EACF,CAAC;;;;;;;;;;;AAgBJ,MAAa,qBAAqB,eAAe;CAC/C,WAAW;EAAC;EAAgB;EAAkB;EAAwB;EAA0B;CAChG,aAAa,EAAE;CACf,QAAQ,EACN,OACA,aAKA,sBAAmH;EACjH;EACA,QAAQ;GACN,GAAG;GACH,cAAc;GACd,kBAAkB;GAClB,YAAY,iBACV,gBAAgB,cAAc,QAAQ;GACxC,eAAe;GAChB;EACF,CAAC;CACL,CAAC"}
@@ -0,0 +1,85 @@
1
+ import { computed } from "../../core/signals/primitives.js";
2
+ import { defineBehavior } from "../../core/composition/create-composition.js";
3
+ import { createMachineReactor } from "../../core/reactors/create-machine-reactor.js";
4
+ import { isResolvedPresentation } from "../../media/types/index.js";
5
+ import { fetchResolvable, getResponseText } from "../../network/fetch.js";
6
+ import { isBlockingPreload } from "../../media/utils/preload.js";
7
+ //#region src/playback/behaviors/resolve-presentation.ts
8
+ /**
9
+ * **Resolve an unresolved presentation by fetching and parsing its manifest.**
10
+ *
11
+ * Reads `state.presentation`; when it holds `{ url }` (unresolved) and the
12
+ * preload / load-activation gate is met, fetches the manifest, parses it via
13
+ * the **required** `config.parsePresentation`, and writes the resolved
14
+ * `Presentation` back to the same slot. The behavior is format-neutral: the
15
+ * composing engine wires in its parser (e.g. the HLS engine supplies the
16
+ * multivariant-playlist parser).
17
+ *
18
+ * Source-identity-driven, expressed as a 4-state machine:
19
+ *
20
+ * ```
21
+ * 'preconditions-unmet' → 'idle' → 'resolving' → 'resolved'
22
+ * ```
23
+ *
24
+ * - `'preconditions-unmet'`: no presentation, or presentation has no URL.
25
+ * - `'idle'`: URL present, unresolved, gate unmet (blocking preload + no
26
+ * load-activation). Waits for the gate to open.
27
+ * - `'resolving'`: URL present, unresolved, gate met. Entry starts the fetch
28
+ * and returns the AbortController — the reactor calls `.abort()` on state
29
+ * exit, so source change / gate-close / destroy all cancel cleanly.
30
+ * - `'resolved'`: `state.presentation` holds a resolved `Presentation`.
31
+ *
32
+ * Gate semantics: `state.preload` (or `config.defaultPreload`, default
33
+ * `'metadata'`, when state.preload is unset) blocks resolution when its
34
+ * value is `'none'` (see `isBlockingPreload` in `media/utils/preload`).
35
+ * `state.loadActivated` is an override — true bypasses the preload gate
36
+ * entirely.
37
+ *
38
+ * Multi-writer with the engine adapter, which writes the initial unresolved
39
+ * `{ url }` to `state.presentation` from src input. Different domains
40
+ * (config-input vs. derived state via fetch) — legitimate multi-writer.
41
+ */
42
+ function deriveState(presentation, preload, loadActivated, defaultPreload) {
43
+ if (!presentation?.url) return "preconditions-unmet";
44
+ if (isResolvedPresentation(presentation)) return "resolved";
45
+ return !!loadActivated || !isBlockingPreload(preload, defaultPreload) ? "resolving" : "idle";
46
+ }
47
+ function resolvePresentationSetup({ state, config }) {
48
+ const { parsePresentation } = config;
49
+ const defaultPreload = config.defaultPreload ?? "metadata";
50
+ const derivedStateSignal = computed(() => deriveState(state.presentation.get(), state.preload.get(), state.loadActivated.get(), defaultPreload));
51
+ return createMachineReactor({
52
+ initial: "preconditions-unmet",
53
+ monitor: () => derivedStateSignal.get(),
54
+ states: {
55
+ "preconditions-unmet": {},
56
+ idle: {},
57
+ resolving: { entry: () => {
58
+ const presentation = state.presentation.get();
59
+ const ac = new AbortController();
60
+ fetchResolvable(presentation, { signal: ac.signal }).then((response) => getResponseText(response)).then((text) => {
61
+ const parsed = parsePresentation(text, presentation);
62
+ state.presentation.set(parsed);
63
+ }).catch((error) => {
64
+ if (error instanceof Error && error.name === "AbortError") return;
65
+ console.error("[resolvePresentation] manifest fetch/parse failed:", error);
66
+ });
67
+ return ac;
68
+ } },
69
+ resolved: {}
70
+ }
71
+ });
72
+ }
73
+ const resolvePresentation = defineBehavior({
74
+ stateKeys: [
75
+ "presentation",
76
+ "preload",
77
+ "loadActivated"
78
+ ],
79
+ contextKeys: [],
80
+ setup: resolvePresentationSetup
81
+ });
82
+ //#endregion
83
+ export { resolvePresentation };
84
+
85
+ //# sourceMappingURL=resolve-presentation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve-presentation.js","names":[],"sources":["../../../../src/playback/behaviors/resolve-presentation.ts"],"sourcesContent":["/**\n * **Resolve an unresolved presentation by fetching and parsing its manifest.**\n *\n * Reads `state.presentation`; when it holds `{ url }` (unresolved) and the\n * preload / load-activation gate is met, fetches the manifest, parses it via\n * the **required** `config.parsePresentation`, and writes the resolved\n * `Presentation` back to the same slot. The behavior is format-neutral: the\n * composing engine wires in its parser (e.g. the HLS engine supplies the\n * multivariant-playlist parser).\n *\n * Source-identity-driven, expressed as a 4-state machine:\n *\n * ```\n * 'preconditions-unmet' → 'idle' → 'resolving' → 'resolved'\n * ```\n *\n * - `'preconditions-unmet'`: no presentation, or presentation has no URL.\n * - `'idle'`: URL present, unresolved, gate unmet (blocking preload + no\n * load-activation). Waits for the gate to open.\n * - `'resolving'`: URL present, unresolved, gate met. Entry starts the fetch\n * and returns the AbortController — the reactor calls `.abort()` on state\n * exit, so source change / gate-close / destroy all cancel cleanly.\n * - `'resolved'`: `state.presentation` holds a resolved `Presentation`.\n *\n * Gate semantics: `state.preload` (or `config.defaultPreload`, default\n * `'metadata'`, when state.preload is unset) blocks resolution when its\n * value is `'none'` (see `isBlockingPreload` in `media/utils/preload`).\n * `state.loadActivated` is an override — true bypasses the preload gate\n * entirely.\n *\n * Multi-writer with the engine adapter, which writes the initial unresolved\n * `{ url }` to `state.presentation` from src input. Different domains\n * (config-input vs. derived state via fetch) — legitimate multi-writer.\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 { isResolvedPresentation, type MaybeResolvedPresentation, type Presentation } from '../../media/types';\nimport { DEFAULT_PRELOAD, isBlockingPreload, type StandardPreload } from '../../media/utils/preload';\nimport { fetchResolvable, getResponseText } from '../../network/fetch';\n\nexport interface PresentationState {\n presentation?: MaybeResolvedPresentation;\n preload?: 'auto' | 'metadata' | 'none' | undefined;\n /** True once a preload-overriding event has fired for the current source — enables resolution regardless of preload. */\n loadActivated?: boolean;\n}\n\nexport type ParsePresentation = (text: string, presentation: MaybeResolvedPresentation) => Presentation;\n\nexport interface ResolvePresentationConfig {\n /**\n * Parses a manifest body into a resolved `Presentation`. **Required** —\n * the behavior is format-neutral and the composing engine supplies its\n * own parser (HLS, DASH, etc.).\n */\n parsePresentation: ParsePresentation;\n /**\n * Fallback used when `state.preload` is unset (undefined / empty) for the\n * resolution-gate decision. Defaults to `'metadata'`, matching\n * `syncPreload`'s own `defaultPreload`.\n */\n defaultPreload?: StandardPreload;\n}\n\nexport type ResolvePresentationState = 'preconditions-unmet' | 'idle' | 'resolving' | 'resolved';\n\nfunction deriveState(\n presentation: MaybeResolvedPresentation | undefined,\n preload: PresentationState['preload'],\n loadActivated: boolean | undefined,\n defaultPreload: StandardPreload\n): ResolvePresentationState {\n if (!presentation?.url) return 'preconditions-unmet';\n if (isResolvedPresentation(presentation)) return 'resolved';\n const gateOpen = !!loadActivated || !isBlockingPreload(preload, defaultPreload);\n return gateOpen ? 'resolving' : 'idle';\n}\n\nfunction resolvePresentationSetup({\n state,\n config,\n}: {\n state: {\n presentation: Signal<PresentationState['presentation']>;\n preload: ReadonlySignal<PresentationState['preload']>;\n loadActivated: ReadonlySignal<PresentationState['loadActivated']>;\n };\n config: ResolvePresentationConfig;\n}): Reactor<ResolvePresentationState | 'destroying' | 'destroyed'> {\n const { parsePresentation } = config;\n const defaultPreload: StandardPreload = config.defaultPreload ?? DEFAULT_PRELOAD;\n\n const derivedStateSignal = computed(() =>\n deriveState(state.presentation.get(), state.preload.get(), state.loadActivated.get(), defaultPreload)\n );\n\n return createMachineReactor<ResolvePresentationState>({\n initial: 'preconditions-unmet',\n monitor: () => derivedStateSignal.get(),\n states: {\n 'preconditions-unmet': {},\n idle: {},\n resolving: {\n entry: () => {\n const presentation = state.presentation.get()!;\n const ac = new AbortController();\n\n fetchResolvable(presentation, { signal: ac.signal })\n .then((response) => getResponseText(response))\n .then((text) => {\n const parsed = parsePresentation(text, presentation);\n state.presentation.set(parsed);\n })\n .catch((error) => {\n if (error instanceof Error && error.name === 'AbortError') return;\n // TODO(error-management): route to a state-error slot once one exists.\n console.error('[resolvePresentation] manifest fetch/parse failed:', error);\n });\n\n return ac;\n },\n },\n resolved: {},\n },\n });\n}\n\nexport const resolvePresentation = defineBehavior({\n stateKeys: ['presentation', 'preload', 'loadActivated'],\n contextKeys: [],\n setup: resolvePresentationSetup,\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoEA,SAAS,YACP,cACA,SACA,eACA,gBAC0B;AAC1B,KAAI,CAAC,cAAc,IAAK,QAAO;AAC/B,KAAI,uBAAuB,aAAa,CAAE,QAAO;AAEjD,QADiB,CAAC,CAAC,iBAAiB,CAAC,kBAAkB,SAAS,eAAe,GAC7D,cAAc;;AAGlC,SAAS,yBAAyB,EAChC,OACA,UAQiE;CACjE,MAAM,EAAE,sBAAsB;CAC9B,MAAM,iBAAkC,OAAO,kBAAA;CAE/C,MAAM,qBAAqB,eACzB,YAAY,MAAM,aAAa,KAAK,EAAE,MAAM,QAAQ,KAAK,EAAE,MAAM,cAAc,KAAK,EAAE,eAAe,CACtG;AAED,QAAO,qBAA+C;EACpD,SAAS;EACT,eAAe,mBAAmB,KAAK;EACvC,QAAQ;GACN,uBAAuB,EAAE;GACzB,MAAM,EAAE;GACR,WAAW,EACT,aAAa;IACX,MAAM,eAAe,MAAM,aAAa,KAAK;IAC7C,MAAM,KAAK,IAAI,iBAAiB;AAEhC,oBAAgB,cAAc,EAAE,QAAQ,GAAG,QAAQ,CAAC,CACjD,MAAM,aAAa,gBAAgB,SAAS,CAAC,CAC7C,MAAM,SAAS;KACd,MAAM,SAAS,kBAAkB,MAAM,aAAa;AACpD,WAAM,aAAa,IAAI,OAAO;MAC9B,CACD,OAAO,UAAU;AAChB,SAAI,iBAAiB,SAAS,MAAM,SAAS,aAAc;AAE3D,aAAQ,MAAM,sDAAsD,MAAM;MAC1E;AAEJ,WAAO;MAEV;GACD,UAAU,EAAE;GACb;EACF,CAAC;;AAGJ,MAAa,sBAAsB,eAAe;CAChD,WAAW;EAAC;EAAgB;EAAW;EAAgB;CACvD,aAAa,EAAE;CACf,OAAO;CACR,CAAC"}
@@ -0,0 +1,97 @@
1
+ import { computed, peek, update } from "../../core/signals/primitives.js";
2
+ import { defineBehavior } from "../../core/composition/create-composition.js";
3
+ import { ConcurrentRunner, Task } from "../../core/tasks/task.js";
4
+ import { createMachineReactor } from "../../core/reactors/create-machine-reactor.js";
5
+ import { isResolvedPresentation, isResolvedTrack } from "../../media/types/index.js";
6
+ import { findTrack, updateTrackInPresentation } from "../../media/utils/tracks.js";
7
+ import { AUDIO_TYPE_CONFIG, TEXT_TYPE_CONFIG, VIDEO_TYPE_CONFIG } from "./track-types.js";
8
+ import { fetchResolvable, getResponseText } from "../../network/fetch.js";
9
+ import { parseMediaPlaylist } from "../../media/hls/parse-media-playlist.js";
10
+ //#region src/playback/behaviors/resolve-track.ts
11
+ function setupTrackResolution({ state, config: { selectedKey, findTrackToResolve } }) {
12
+ const runner = new ConcurrentRunner();
13
+ const derivedStateSignal = computed(() => isResolvedPresentation(state.presentation.get()) ? "presentation-resolved" : "presentation-unresolved");
14
+ return createMachineReactor({
15
+ initial: "presentation-unresolved",
16
+ monitor: () => derivedStateSignal.get(),
17
+ states: {
18
+ "presentation-unresolved": {},
19
+ "presentation-resolved": {
20
+ entry: () => () => runner.abortAll(),
21
+ effects: [() => {
22
+ const presentation = peek(state.presentation);
23
+ const trackId = state[selectedKey].get();
24
+ if (!presentation || !trackId) return;
25
+ const track = findTrackToResolve(presentation, trackId);
26
+ if (!track || isResolvedTrack(track)) return;
27
+ runner.schedule(new Task(async (signal) => {
28
+ const mediaTrack = parseMediaPlaylist(await getResponseText(await fetchResolvable(track, { signal })), track);
29
+ update(state.presentation, (current) => isResolvedPresentation(current) ? updateTrackInPresentation(current, mediaTrack) : current);
30
+ }, { id: track.id }));
31
+ }]
32
+ }
33
+ }
34
+ });
35
+ }
36
+ const VIDEO_TRACK_RESOLUTION_CONFIG = {
37
+ ...VIDEO_TYPE_CONFIG,
38
+ findTrackToResolve: (presentation, trackId) => findTrack(presentation, "video", trackId)
39
+ };
40
+ const AUDIO_TRACK_RESOLUTION_CONFIG = {
41
+ ...AUDIO_TYPE_CONFIG,
42
+ findTrackToResolve: (presentation, trackId) => findTrack(presentation, "audio", trackId)
43
+ };
44
+ const TEXT_TRACK_RESOLUTION_CONFIG = {
45
+ ...TEXT_TYPE_CONFIG,
46
+ findTrackToResolve: (presentation, trackId) => findTrack(presentation, "text", trackId)
47
+ };
48
+ /**
49
+ * Resolve unresolved video tracks. Schedules a fetch task whenever the
50
+ * selected video track is partially resolved, parses the manifest, and
51
+ * writes the resolved track back into `state.presentation`.
52
+ */
53
+ const resolveVideoTrack = defineBehavior({
54
+ stateKeys: ["presentation", "selectedVideoTrackId"],
55
+ contextKeys: [],
56
+ setup: ({ state, config = {} }) => setupTrackResolution({
57
+ state,
58
+ config: {
59
+ ...VIDEO_TRACK_RESOLUTION_CONFIG,
60
+ ...config
61
+ }
62
+ })
63
+ });
64
+ /**
65
+ * Resolve unresolved audio tracks. Same shape as `resolveVideoTrack`,
66
+ * narrowed to audio.
67
+ */
68
+ const resolveAudioTrack = defineBehavior({
69
+ stateKeys: ["presentation", "selectedAudioTrackId"],
70
+ contextKeys: [],
71
+ setup: ({ state, config = {} }) => setupTrackResolution({
72
+ state,
73
+ config: {
74
+ ...AUDIO_TRACK_RESOLUTION_CONFIG,
75
+ ...config
76
+ }
77
+ })
78
+ });
79
+ /**
80
+ * Resolve unresolved text tracks. Same shape as `resolveVideoTrack`,
81
+ * narrowed to text.
82
+ */
83
+ const resolveTextTrack = defineBehavior({
84
+ stateKeys: ["presentation", "selectedTextTrackId"],
85
+ contextKeys: [],
86
+ setup: ({ state, config = {} }) => setupTrackResolution({
87
+ state,
88
+ config: {
89
+ ...TEXT_TRACK_RESOLUTION_CONFIG,
90
+ ...config
91
+ }
92
+ })
93
+ });
94
+ //#endregion
95
+ export { resolveAudioTrack, resolveTextTrack, resolveVideoTrack };
96
+
97
+ //# sourceMappingURL=resolve-track.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve-track.js","names":[],"sources":["../../../../src/playback/behaviors/resolve-track.ts"],"sourcesContent":["import { defineBehavior } from '../../core/composition/create-composition';\nimport { createMachineReactor } from '../../core/reactors/create-machine-reactor';\nimport { computed, peek, type ReadonlySignal, type Signal, update } from '../../core/signals/primitives';\nimport { ConcurrentRunner, Task } from '../../core/tasks/task';\nimport { parseMediaPlaylist } from '../../media/hls/parse-media-playlist';\nimport type { MaybeResolvedPresentation, PartiallyResolvedTrack, ResolvedTrack } from '../../media/types';\nimport { isResolvedPresentation, isResolvedTrack } from '../../media/types';\nimport { findTrack, updateTrackInPresentation } from '../../media/utils/tracks';\nimport { fetchResolvable, getResponseText } from '../../network/fetch';\nimport { AUDIO_TYPE_CONFIG, TEXT_TYPE_CONFIG, VIDEO_TYPE_CONFIG } from './track-types';\n\n// ============================================================================\n// Specialization helper\n//\n// `setupTrackResolution` has the same shape as a Behavior `setup` function:\n// `({ state, config }) => cleanup`. Each `resolveXTrack` export below calls it\n// from inside its own `defineBehavior` setup, supplying its per-type config\n// inline. The orchestration — gate on a selection, short-circuit when the\n// track is already resolved or missing, schedule the fetch+parse, and patch\n// the resolved track back into `state.presentation` — is shared.\n// ============================================================================\n\n/**\n * State shape for track resolution. Uses `MaybeResolvedPresentation` so it\n * matches the engine's slot type; resolution narrows internally.\n */\nexport interface ResolveTrackState {\n presentation?: MaybeResolvedPresentation;\n selectedVideoTrackId?: string;\n selectedAudioTrackId?: string;\n selectedTextTrackId?: string;\n}\n\ntype SelectedTrackKey = 'selectedVideoTrackId' | 'selectedAudioTrackId' | 'selectedTextTrackId';\n\ntype ResolveTrackStateMap<K extends SelectedTrackKey> = {\n presentation: Signal<ResolveTrackState['presentation']>;\n} & { [P in K]: ReadonlySignal<ResolveTrackState[P]> };\n\ninterface TrackResolutionConfig<K extends SelectedTrackKey> {\n selectedKey: K;\n findTrackToResolve: (\n presentation: MaybeResolvedPresentation,\n trackId: string\n ) => PartiallyResolvedTrack | ResolvedTrack | undefined;\n}\n\nfunction setupTrackResolution<K extends SelectedTrackKey>({\n state,\n config: { selectedKey, findTrackToResolve },\n}: {\n state: ResolveTrackStateMap<K>;\n config: TrackResolutionConfig<K>;\n}) {\n // NOTE: This can/maybe will be pulled into a per-use case factory (e.g. something like createTaskRunner() with args TBD),\n // likely eventually passed down via config or a new \"definitions\" argument. This will allow us to decide if we want our task runner/scheduler\n // to e.g. run concurrently (like we currently are), serially with a queue, or abort the previous task and replace it with the newly scheduled one. (CJP).\n const runner = new ConcurrentRunner();\n\n // Reactor states model the FSM the previous effect-based body was\n // hand-rolling. 'presentation-resolved' is entered when the\n // presentation is fully parsed (has a Ham id + selectionSets); leaving\n // it (presentation cleared or reset to an unresolved value) aborts all\n // in-flight tasks via the entry-cleanup. Most URL changes go through\n // 'presentation-unresolved' naturally (set undefined → set new partial\n // → re-parse), so the common case is covered by state-exit alone; the\n // task body's commit-time id check covers the pathological\n // resolved→resolved-without-unresolved transition.\n const derivedStateSignal = computed(() =>\n isResolvedPresentation(state.presentation.get())\n ? ('presentation-resolved' as const)\n : ('presentation-unresolved' as const)\n );\n\n return createMachineReactor({\n initial: 'presentation-unresolved',\n monitor: () => derivedStateSignal.get(),\n states: {\n 'presentation-unresolved': {},\n 'presentation-resolved': {\n // `entry` runs on state entry; the function it returns is the\n // state-exit cleanup. Returning `() => runner.abortAll()` binds\n // abort-of-in-flight-resolutions to leaving 'presentation-resolved'\n // (presentation cleared/reset, or behavior destroyed) —\n // source-change cancellation expressed structurally through the\n // state machine.\n entry: () => () => runner.abortAll(),\n effects: [\n () => {\n // The reactor's state transitions handle relevant presentation\n // changes (presentation-resolved ↔ presentation-unresolved);\n // within 'presentation-resolved' we peek (untracked read) so\n // internal updates (segments added by sibling tasks) don't\n // re-fire the effect.\n const presentation = peek(state.presentation);\n const trackId = state[selectedKey].get();\n if (!presentation || !trackId) return;\n\n const track = findTrackToResolve(presentation, trackId);\n if (!track || isResolvedTrack(track)) return;\n\n runner.schedule(\n // NOTE: This can/maybe will be pulled into a per-use case factory (e.g. something like createResolveTrackTask(track, context, config)),\n // likely eventually passed down via config or a new \"definitions\" argument (CJP).\n new Task(\n async (signal) => {\n const response = await fetchResolvable(track, { signal });\n const text = await getResponseText(response);\n const mediaTrack = parseMediaPlaylist(text, track);\n\n // Updater handles undefined inputs by returning current\n // unchanged; isResolvedPresentation narrows for the patch.\n // State-exit on resolving→unresolved fires runner.abortAll\n // before any URL change settles, and per the Fetch spec the\n // signal abort cancels in-flight body reads — so by the\n // time we reach this point the presentation we resolved\n // against is the live one.\n update(state.presentation, (current) =>\n isResolvedPresentation(current) ? updateTrackInPresentation(current, mediaTrack) : current\n );\n },\n { id: track.id }\n )\n );\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_TRACK_RESOLUTION_CONFIG = {\n ...VIDEO_TYPE_CONFIG,\n findTrackToResolve: (presentation: MaybeResolvedPresentation, trackId: string) =>\n findTrack(presentation, 'video', trackId),\n} as const;\n\nconst AUDIO_TRACK_RESOLUTION_CONFIG = {\n ...AUDIO_TYPE_CONFIG,\n findTrackToResolve: (presentation: MaybeResolvedPresentation, trackId: string) =>\n findTrack(presentation, 'audio', trackId),\n} as const;\n\nconst TEXT_TRACK_RESOLUTION_CONFIG = {\n ...TEXT_TYPE_CONFIG,\n findTrackToResolve: (presentation: MaybeResolvedPresentation, trackId: string) =>\n findTrack(presentation, 'text', trackId),\n} as const;\n\n// ============================================================================\n// Specialized exports — one per track type\n// ============================================================================\n\n/**\n * Resolve unresolved video tracks. Schedules a fetch task whenever the\n * selected video track is partially resolved, parses the manifest, and\n * writes the resolved track back into `state.presentation`.\n */\nexport const resolveVideoTrack = defineBehavior({\n stateKeys: ['presentation', 'selectedVideoTrackId'],\n contextKeys: [],\n setup: ({ state, config = {} }: { state: ResolveTrackStateMap<'selectedVideoTrackId'>; config?: object }) =>\n setupTrackResolution({\n state,\n config: { ...VIDEO_TRACK_RESOLUTION_CONFIG, ...config },\n }),\n});\n\n/**\n * Resolve unresolved audio tracks. Same shape as `resolveVideoTrack`,\n * narrowed to audio.\n */\nexport const resolveAudioTrack = defineBehavior({\n stateKeys: ['presentation', 'selectedAudioTrackId'],\n contextKeys: [],\n setup: ({ state, config = {} }: { state: ResolveTrackStateMap<'selectedAudioTrackId'>; config?: object }) =>\n setupTrackResolution({\n state,\n config: { ...AUDIO_TRACK_RESOLUTION_CONFIG, ...config },\n }),\n});\n\n/**\n * Resolve unresolved text tracks. Same shape as `resolveVideoTrack`,\n * narrowed to text.\n */\nexport const resolveTextTrack = defineBehavior({\n stateKeys: ['presentation', 'selectedTextTrackId'],\n contextKeys: [],\n setup: ({ state, config = {} }: { state: ResolveTrackStateMap<'selectedTextTrackId'>; config?: object }) =>\n setupTrackResolution({\n state,\n config: { ...TEXT_TRACK_RESOLUTION_CONFIG, ...config },\n }),\n});\n"],"mappings":";;;;;;;;;;AA+CA,SAAS,qBAAiD,EACxD,OACA,QAAQ,EAAE,aAAa,wBAItB;CAID,MAAM,SAAS,IAAI,kBAAkB;CAWrC,MAAM,qBAAqB,eACzB,uBAAuB,MAAM,aAAa,KAAK,CAAC,GAC3C,0BACA,0BACN;AAED,QAAO,qBAAqB;EAC1B,SAAS;EACT,eAAe,mBAAmB,KAAK;EACvC,QAAQ;GACN,2BAA2B,EAAE;GAC7B,yBAAyB;IAOvB,mBAAmB,OAAO,UAAU;IACpC,SAAS,OACD;KAMJ,MAAM,eAAe,KAAK,MAAM,aAAa;KAC7C,MAAM,UAAU,MAAM,aAAa,KAAK;AACxC,SAAI,CAAC,gBAAgB,CAAC,QAAS;KAE/B,MAAM,QAAQ,mBAAmB,cAAc,QAAQ;AACvD,SAAI,CAAC,SAAS,gBAAgB,MAAM,CAAE;AAEtC,YAAO,SAGL,IAAI,KACF,OAAO,WAAW;MAGhB,MAAM,aAAa,mBADN,MAAM,gBADF,MAAM,gBAAgB,OAAO,EAAE,QAAQ,CAAC,CACb,EACA,MAAM;AASlD,aAAO,MAAM,eAAe,YAC1B,uBAAuB,QAAQ,GAAG,0BAA0B,SAAS,WAAW,GAAG,QACpF;QAEH,EAAE,IAAI,MAAM,IAAI,CACjB,CACF;MAEJ;IACF;GACF;EACF,CAAC;;AAOJ,MAAM,gCAAgC;CACpC,GAAG;CACH,qBAAqB,cAAyC,YAC5D,UAAU,cAAc,SAAS,QAAQ;CAC5C;AAED,MAAM,gCAAgC;CACpC,GAAG;CACH,qBAAqB,cAAyC,YAC5D,UAAU,cAAc,SAAS,QAAQ;CAC5C;AAED,MAAM,+BAA+B;CACnC,GAAG;CACH,qBAAqB,cAAyC,YAC5D,UAAU,cAAc,QAAQ,QAAQ;CAC3C;;;;;;AAWD,MAAa,oBAAoB,eAAe;CAC9C,WAAW,CAAC,gBAAgB,uBAAuB;CACnD,aAAa,EAAE;CACf,QAAQ,EAAE,OAAO,SAAS,EAAE,OAC1B,qBAAqB;EACnB;EACA,QAAQ;GAAE,GAAG;GAA+B,GAAG;GAAQ;EACxD,CAAC;CACL,CAAC;;;;;AAMF,MAAa,oBAAoB,eAAe;CAC9C,WAAW,CAAC,gBAAgB,uBAAuB;CACnD,aAAa,EAAE;CACf,QAAQ,EAAE,OAAO,SAAS,EAAE,OAC1B,qBAAqB;EACnB;EACA,QAAQ;GAAE,GAAG;GAA+B,GAAG;GAAQ;EACxD,CAAC;CACL,CAAC;;;;;AAMF,MAAa,mBAAmB,eAAe;CAC7C,WAAW,CAAC,gBAAgB,sBAAsB;CAClD,aAAa,EAAE;CACf,QAAQ,EAAE,OAAO,SAAS,EAAE,OAC1B,qBAAqB;EACnB;EACA,QAAQ;GAAE,GAAG;GAA8B,GAAG;GAAQ;EACvD,CAAC;CACL,CAAC"}
@@ -0,0 +1,132 @@
1
+ import { computed } from "../../core/signals/primitives.js";
2
+ import { defineBehavior } from "../../core/composition/create-composition.js";
3
+ import { createMachineReactor } from "../../core/reactors/create-machine-reactor.js";
4
+ import { isResolvedPresentation } from "../../media/types/index.js";
5
+ import { AUDIO_TYPE_CONFIG, TEXT_TYPE_CONFIG, VIDEO_TYPE_CONFIG } from "./track-types.js";
6
+ import { pickFirstTrackId, pickTextTrack } from "../../media/primitives/select-tracks.js";
7
+ //#region src/playback/behaviors/select-tracks.ts
8
+ /**
9
+ * **Default audio/text track selection on src load / unselect on src unload.**
10
+ * When a presentation is resolved, sets `selectedAudioTrackId` /
11
+ * `selectedTextTrackId` to a per-type-picker default if no selection already
12
+ * exists. When the presentation is unset/reset (transitions back to
13
+ * unresolved), clears the selection so a stale id from the previous source
14
+ * doesn't persist.
15
+ *
16
+ * Lifecycle-driven: each transition fires its work once. Does not police
17
+ * the selection between transitions; external writes (user picks) are left
18
+ * alone.
19
+ *
20
+ * Picker is config-driven: each per-type export wires a sensible default
21
+ * (`pickFirstTrackId` for audio, `pickTextTrack` for text) and the caller
22
+ * can supply their own via `config.picker` for custom selection logic
23
+ * (language preferences, default-track handling, etc.). The behavior's
24
+ * `config` is forwarded to the picker as its second argument, so options
25
+ * like `preferredAudioLanguage` / `preferredSubtitleLanguage` reach the
26
+ * picker without an intermediate wrapping layer.
27
+ *
28
+ * Compose `selectVideoTrack` for the simple "pick a default video track"
29
+ * behavior, or `switchVideoQuality` (`./quality-switching.ts`) for the
30
+ * ABR-driven variant — they're alternatives, not stackable (both write
31
+ * `selectedVideoTrackId`). The simple variant tree-shakes out the
32
+ * ABR machinery (bandwidth-estimator, quality-selection algorithms).
33
+ */
34
+ function setupTrackSelection({ state, config: { selectedKey, picker, pickerConfig } }) {
35
+ const derivedStateSignal = computed(() => isResolvedPresentation(state.presentation.get()) ? "presentation-resolved" : "presentation-unresolved");
36
+ return createMachineReactor({
37
+ initial: "presentation-unresolved",
38
+ monitor: () => derivedStateSignal.get(),
39
+ states: {
40
+ "presentation-unresolved": {},
41
+ "presentation-resolved": { entry: () => {
42
+ if (!state[selectedKey].get()) {
43
+ const id = picker(state.presentation.get(), pickerConfig);
44
+ if (id) state[selectedKey].set(id);
45
+ }
46
+ return () => state[selectedKey].set(void 0);
47
+ } }
48
+ }
49
+ });
50
+ }
51
+ /** Default video picker: first track in the video selection set. */
52
+ const defaultVideoPicker = (presentation) => pickFirstTrackId(presentation, "video");
53
+ const VIDEO_TRACK_SELECTION_CONFIG = {
54
+ ...VIDEO_TYPE_CONFIG,
55
+ picker: defaultVideoPicker
56
+ };
57
+ /** Default audio picker: first track in the audio selection set. */
58
+ const defaultAudioPicker = (presentation) => pickFirstTrackId(presentation, "audio");
59
+ const AUDIO_TRACK_SELECTION_CONFIG = {
60
+ ...AUDIO_TYPE_CONFIG,
61
+ picker: defaultAudioPicker
62
+ };
63
+ const TEXT_TRACK_SELECTION_CONFIG = {
64
+ ...TEXT_TYPE_CONFIG,
65
+ picker: pickTextTrack
66
+ };
67
+ defineBehavior({
68
+ stateKeys: ["presentation", "selectedVideoTrackId"],
69
+ contextKeys: [],
70
+ setup: ({ state, config }) => setupTrackSelection({
71
+ state,
72
+ config: {
73
+ ...VIDEO_TRACK_SELECTION_CONFIG,
74
+ picker: config?.picker ?? VIDEO_TRACK_SELECTION_CONFIG.picker,
75
+ pickerConfig: config
76
+ }
77
+ })
78
+ });
79
+ /**
80
+ * Select an audio track when a presentation loads. Clears the selection
81
+ * on src unload.
82
+ *
83
+ * @example
84
+ * const reactor = selectAudioTrack.setup({ state });
85
+ *
86
+ * @example
87
+ * // Custom picker with language preference
88
+ * const reactor = selectAudioTrack.setup({
89
+ * state,
90
+ * config: { preferredAudioLanguage: 'en', picker: myLanguageAwarePicker },
91
+ * });
92
+ */
93
+ const selectAudioTrack = defineBehavior({
94
+ stateKeys: ["presentation", "selectedAudioTrackId"],
95
+ contextKeys: [],
96
+ setup: ({ state, config }) => setupTrackSelection({
97
+ state,
98
+ config: {
99
+ ...AUDIO_TRACK_SELECTION_CONFIG,
100
+ picker: config?.picker ?? AUDIO_TRACK_SELECTION_CONFIG.picker,
101
+ pickerConfig: config
102
+ }
103
+ })
104
+ });
105
+ /**
106
+ * Select a text track based on user preferences (preferred language,
107
+ * default-track auto-select, forced-track filtering). Clears the selection
108
+ * on src unload.
109
+ *
110
+ * Unlike audio selection, the default text picker returns `undefined` when
111
+ * no preference matches, leaving the selection unset — text-track
112
+ * selection is user opt-in.
113
+ *
114
+ * @example
115
+ * const reactor = selectTextTrack.setup({ state, config: { preferredSubtitleLanguage: 'en' } });
116
+ */
117
+ const selectTextTrack = defineBehavior({
118
+ stateKeys: ["presentation", "selectedTextTrackId"],
119
+ contextKeys: [],
120
+ setup: ({ state, config }) => setupTrackSelection({
121
+ state,
122
+ config: {
123
+ ...TEXT_TRACK_SELECTION_CONFIG,
124
+ picker: config?.picker ?? TEXT_TRACK_SELECTION_CONFIG.picker,
125
+ pickerConfig: config
126
+ }
127
+ })
128
+ });
129
+ //#endregion
130
+ export { selectAudioTrack, selectTextTrack };
131
+
132
+ //# sourceMappingURL=select-tracks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"select-tracks.js","names":[],"sources":["../../../../src/playback/behaviors/select-tracks.ts"],"sourcesContent":["/**\n * **Default audio/text track selection on src load / unselect on src unload.**\n * When a presentation is resolved, sets `selectedAudioTrackId` /\n * `selectedTextTrackId` to a per-type-picker default if no selection already\n * exists. When the presentation is unset/reset (transitions back to\n * unresolved), clears the selection so a stale id from the previous source\n * doesn't persist.\n *\n * Lifecycle-driven: each transition fires its work once. Does not police\n * the selection between transitions; external writes (user picks) are left\n * alone.\n *\n * Picker is config-driven: each per-type export wires a sensible default\n * (`pickFirstTrackId` for audio, `pickTextTrack` for text) and the caller\n * can supply their own via `config.picker` for custom selection logic\n * (language preferences, default-track handling, etc.). The behavior's\n * `config` is forwarded to the picker as its second argument, so options\n * like `preferredAudioLanguage` / `preferredSubtitleLanguage` reach the\n * picker without an intermediate wrapping layer.\n *\n * Compose `selectVideoTrack` for the simple \"pick a default video track\"\n * behavior, or `switchVideoQuality` (`./quality-switching.ts`) for the\n * ABR-driven variant — they're alternatives, not stackable (both write\n * `selectedVideoTrackId`). The simple variant tree-shakes out the\n * ABR machinery (bandwidth-estimator, quality-selection algorithms).\n */\n\nimport { defineBehavior } from '../../core/composition/create-composition';\nimport { createMachineReactor } from '../../core/reactors/create-machine-reactor';\nimport { computed, type ReadonlySignal, type Signal } from '../../core/signals/primitives';\nimport {\n type AudioSelectionConfig,\n pickFirstTrackId,\n pickTextTrack,\n type TextSelectionConfig,\n type TrackPicker,\n type TrackSelectionState,\n type VideoSelectionConfig,\n} from '../../media/primitives/select-tracks';\nimport { isResolvedPresentation } from '../../media/types';\nimport { AUDIO_TYPE_CONFIG, TEXT_TYPE_CONFIG, VIDEO_TYPE_CONFIG } from './track-types';\n\n// ============================================================================\n// Specialization helper\n//\n// `setupTrackSelection` has the same shape as a Behavior `setup` function:\n// `({ state, config }) => Reactor`. Each `selectXTrack` export below calls\n// it from inside its own `defineBehavior` setup, supplying its per-type\n// `selectedKey`, default picker, and forwarded picker config. The lifecycle\n// — pick on entering 'presentation-resolved' if not already selected; clear\n// on entering 'presentation-unresolved' — is shared.\n// ============================================================================\n\ntype SelectedTrackKey = 'selectedVideoTrackId' | 'selectedAudioTrackId' | 'selectedTextTrackId';\n\ntype SelectStateMap<K extends SelectedTrackKey> = {\n presentation: ReadonlySignal<TrackSelectionState['presentation']>;\n} & { [P in K]: Signal<TrackSelectionState[P]> };\n\ninterface TrackSelectionSetupConfig<K extends SelectedTrackKey, PickerConfig> {\n selectedKey: K;\n picker: TrackPicker<PickerConfig>;\n pickerConfig?: PickerConfig;\n}\n\nfunction setupTrackSelection<K extends SelectedTrackKey, PickerConfig>({\n state,\n config: { selectedKey, picker, pickerConfig },\n}: {\n state: SelectStateMap<K>;\n config: TrackSelectionSetupConfig<K, PickerConfig>;\n}) {\n const derivedStateSignal = computed(() =>\n isResolvedPresentation(state.presentation.get())\n ? ('presentation-resolved' as const)\n : ('presentation-unresolved' as const)\n );\n\n return createMachineReactor({\n initial: 'presentation-unresolved',\n monitor: () => derivedStateSignal.get(),\n states: {\n 'presentation-unresolved': {},\n 'presentation-resolved': {\n // Entry: pick a default on entering presentation-resolved if none\n // is set. External writes (user picks, ABR) that already populated\n // the slot are left alone.\n //\n // The returned cleanup runs on state exit — which fires on src\n // unload (presentation-resolved → presentation-unresolved) AND on\n // behavior destroy (presentation-resolved → destroying →\n // destroyed). Putting the clear here rather than as\n // presentation-unresolved.entry is more cohesive (operation +\n // cleanup co-located) and correctly covers destroy (destroy\n // doesn't pass through presentation-unresolved).\n entry: () => {\n if (!state[selectedKey].get()) {\n // `state.presentation.get()` is non-null inside this entry —\n // the reactor's `'presentation-resolved'` gate is exactly\n // `isResolvedPresentation(state.presentation.get())`, which\n // requires a truthy Presentation.\n const id = picker(state.presentation.get()!, pickerConfig);\n if (id) state[selectedKey].set(id);\n }\n return () => state[selectedKey].set(undefined);\n },\n },\n },\n });\n}\n\n// ============================================================================\n// Per-helper-per-type configs — defaults that variants spread engine config over\n//\n// The engine-facing config carries `picker?` (optional override) plus\n// fields the default picker consults (preferred language, etc.). The\n// variant resolves the final picker (engine override or default) and\n// forwards the whole engine config as `pickerConfig` so rich pickers can\n// read their options.\n// ============================================================================\n\n/** Default video picker: first track in the video selection set. */\nconst defaultVideoPicker: TrackPicker = (presentation) => pickFirstTrackId(presentation, 'video');\n\nconst VIDEO_TRACK_SELECTION_CONFIG = {\n ...VIDEO_TYPE_CONFIG,\n picker: defaultVideoPicker,\n} as const;\n\n/** Default audio picker: first track in the audio selection set. */\nconst defaultAudioPicker: TrackPicker = (presentation) => pickFirstTrackId(presentation, 'audio');\n\nconst AUDIO_TRACK_SELECTION_CONFIG = {\n ...AUDIO_TYPE_CONFIG,\n picker: defaultAudioPicker,\n} as const;\n\nconst TEXT_TRACK_SELECTION_CONFIG = {\n ...TEXT_TYPE_CONFIG,\n picker: pickTextTrack,\n} as const;\n\n// ============================================================================\n// Specialized exports — one per track type\n// ============================================================================\n\n/**\n * Config for `selectVideoTrack`. Pass `picker` to fully override selection\n * logic; otherwise the default `pickFirstTrackId` is used.\n */\nexport interface SelectVideoTrackConfig extends Omit<VideoSelectionConfig, 'type'> {\n picker?: TrackPicker<SelectVideoTrackConfig>;\n}\n\n/**\n * Select a video track when a presentation loads. Clears the selection on\n * src unload.\n *\n * This is the simple, non-ABR counterpart to `switchVideoQuality` — compose\n * one or the other, not both (both write `selectedVideoTrackId`). Composing\n * `selectVideoTrack` alone tree-shakes out the ABR code path\n * (bandwidth-estimator, quality-selection); use it for sources without\n * meaningful quality variants, test setups, or players that intentionally\n * pin a quality.\n *\n * @example\n * const reactor = selectVideoTrack.setup({ state });\n */\nexport const selectVideoTrack = defineBehavior({\n stateKeys: ['presentation', 'selectedVideoTrackId'],\n contextKeys: [],\n setup: ({ state, config }: { state: SelectStateMap<'selectedVideoTrackId'>; config?: SelectVideoTrackConfig }) =>\n setupTrackSelection({\n state,\n config: {\n ...VIDEO_TRACK_SELECTION_CONFIG,\n picker: config?.picker ?? VIDEO_TRACK_SELECTION_CONFIG.picker,\n pickerConfig: config,\n },\n }),\n});\n\n/**\n * Config for `selectAudioTrack`. Pass `picker` to fully override selection\n * logic; otherwise the default `pickFirstTrackId` is used (which ignores\n * the other fields — they reach the picker as its second arg for callers\n * who supply a richer picker, like a language-aware one).\n */\nexport interface SelectAudioTrackConfig extends Omit<AudioSelectionConfig, 'type'> {\n picker?: TrackPicker<SelectAudioTrackConfig>;\n}\n\n/**\n * Select an audio track when a presentation loads. Clears the selection\n * on src unload.\n *\n * @example\n * const reactor = selectAudioTrack.setup({ state });\n *\n * @example\n * // Custom picker with language preference\n * const reactor = selectAudioTrack.setup({\n * state,\n * config: { preferredAudioLanguage: 'en', picker: myLanguageAwarePicker },\n * });\n */\nexport const selectAudioTrack = defineBehavior({\n stateKeys: ['presentation', 'selectedAudioTrackId'],\n contextKeys: [],\n setup: ({ state, config }: { state: SelectStateMap<'selectedAudioTrackId'>; config?: SelectAudioTrackConfig }) =>\n setupTrackSelection({\n state,\n config: {\n ...AUDIO_TRACK_SELECTION_CONFIG,\n picker: config?.picker ?? AUDIO_TRACK_SELECTION_CONFIG.picker,\n pickerConfig: config,\n },\n }),\n});\n\n/**\n * Config for `selectTextTrack`. Pass `picker` to fully override selection\n * logic; otherwise the default `pickTextTrack` is used, which honors the\n * other fields.\n */\nexport interface SelectTextTrackConfig extends TextSelectionConfig {\n picker?: TrackPicker<SelectTextTrackConfig>;\n}\n\n/**\n * Select a text track based on user preferences (preferred language,\n * default-track auto-select, forced-track filtering). Clears the selection\n * on src unload.\n *\n * Unlike audio selection, the default text picker returns `undefined` when\n * no preference matches, leaving the selection unset — text-track\n * selection is user opt-in.\n *\n * @example\n * const reactor = selectTextTrack.setup({ state, config: { preferredSubtitleLanguage: 'en' } });\n */\nexport const selectTextTrack = defineBehavior({\n stateKeys: ['presentation', 'selectedTextTrackId'],\n contextKeys: [],\n setup: ({ state, config }: { state: SelectStateMap<'selectedTextTrackId'>; config?: SelectTextTrackConfig }) =>\n setupTrackSelection({\n state,\n config: {\n ...TEXT_TRACK_SELECTION_CONFIG,\n picker: config?.picker ?? TEXT_TRACK_SELECTION_CONFIG.picker,\n pickerConfig: config,\n },\n }),\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiEA,SAAS,oBAA8D,EACrE,OACA,QAAQ,EAAE,aAAa,QAAQ,kBAI9B;CACD,MAAM,qBAAqB,eACzB,uBAAuB,MAAM,aAAa,KAAK,CAAC,GAC3C,0BACA,0BACN;AAED,QAAO,qBAAqB;EAC1B,SAAS;EACT,eAAe,mBAAmB,KAAK;EACvC,QAAQ;GACN,2BAA2B,EAAE;GAC7B,yBAAyB,EAYvB,aAAa;AACX,QAAI,CAAC,MAAM,aAAa,KAAK,EAAE;KAK7B,MAAM,KAAK,OAAO,MAAM,aAAa,KAAK,EAAG,aAAa;AAC1D,SAAI,GAAI,OAAM,aAAa,IAAI,GAAG;;AAEpC,iBAAa,MAAM,aAAa,IAAI,KAAA,EAAU;MAEjD;GACF;EACF,CAAC;;;AAcJ,MAAM,sBAAmC,iBAAiB,iBAAiB,cAAc,QAAQ;AAEjG,MAAM,+BAA+B;CACnC,GAAG;CACH,QAAQ;CACT;;AAGD,MAAM,sBAAmC,iBAAiB,iBAAiB,cAAc,QAAQ;AAEjG,MAAM,+BAA+B;CACnC,GAAG;CACH,QAAQ;CACT;AAED,MAAM,8BAA8B;CAClC,GAAG;CACH,QAAQ;CACT;AA4B+B,eAAe;CAC7C,WAAW,CAAC,gBAAgB,uBAAuB;CACnD,aAAa,EAAE;CACf,QAAQ,EAAE,OAAO,aACf,oBAAoB;EAClB;EACA,QAAQ;GACN,GAAG;GACH,QAAQ,QAAQ,UAAU,6BAA6B;GACvD,cAAc;GACf;EACF,CAAC;CACL,CAAC;;;;;;;;;;;;;;;AA0BF,MAAa,mBAAmB,eAAe;CAC7C,WAAW,CAAC,gBAAgB,uBAAuB;CACnD,aAAa,EAAE;CACf,QAAQ,EAAE,OAAO,aACf,oBAAoB;EAClB;EACA,QAAQ;GACN,GAAG;GACH,QAAQ,QAAQ,UAAU,6BAA6B;GACvD,cAAc;GACf;EACF,CAAC;CACL,CAAC;;;;;;;;;;;;;AAuBF,MAAa,kBAAkB,eAAe;CAC5C,WAAW,CAAC,gBAAgB,sBAAsB;CAClD,aAAa,EAAE;CACf,QAAQ,EAAE,OAAO,aACf,oBAAoB;EAClB;EACA,QAAQ;GACN,GAAG;GACH,QAAQ,QAAQ,UAAU,4BAA4B;GACtD,cAAc;GACf;EACF,CAAC;CACL,CAAC"}
@@ -0,0 +1,59 @@
1
+ import { computed, peek } from "../../core/signals/primitives.js";
2
+ import { defineBehavior } from "../../core/composition/create-composition.js";
3
+ import { effect } from "../../core/signals/effect.js";
4
+ import { isStandardPreload } from "../../media/utils/preload.js";
5
+ //#region src/playback/behaviors/sync-preload.ts
6
+ /**
7
+ * **Bidirectional sync between `state.preload` and `mediaElement.preload`.**
8
+ *
9
+ * Two effects:
10
+ * - **Read (DOM → state)** — on `context.mediaElement` swap or
11
+ * `state.presentation.url` change, copies `mediaElement.preload` into
12
+ * `state.preload` if it's a W3C value and `state.preload` isn't holding
13
+ * an extended (non-W3C) value. When the DOM has no W3C opinion and
14
+ * `state.preload` is undefined, backfills from `config.defaultPreload`
15
+ * (default-default `'metadata'`) so `state.preload` is never undefined
16
+ * in steady state.
17
+ * - **Write (state → DOM)** — on `state.preload` change or
18
+ * `context.mediaElement` swap, writes `state.preload` back to
19
+ * `mediaElement.preload` if the value is W3C.
20
+ *
21
+ * Extended values (e.g. `'canplay'`) written externally to `state.preload`
22
+ * are sticky: read won't overwrite them, write won't push them to the DOM.
23
+ * All writes are deduped to break echo loops and avoid spurious re-triggers
24
+ * downstream (notably `resolvePresentation`, which reads `state.preload`).
25
+ */
26
+ function syncPreloadSetup({ state, context, config }) {
27
+ const defaultPreload = config?.defaultPreload ?? "metadata";
28
+ const presentationUrl = computed(() => state.presentation.get()?.url);
29
+ const cleanupRead = effect(() => {
30
+ presentationUrl.get();
31
+ const mediaElement = context.mediaElement.get();
32
+ const current = peek(state.preload);
33
+ if (current !== void 0 && !isStandardPreload(current)) return;
34
+ const target = mediaElement && isStandardPreload(mediaElement.preload) ? mediaElement.preload : current === void 0 ? defaultPreload : void 0;
35
+ if (target === void 0 || target === current) return;
36
+ state.preload.set(target);
37
+ });
38
+ const cleanupWrite = effect(() => {
39
+ const next = state.preload.get();
40
+ const mediaElement = context.mediaElement.get();
41
+ if (!mediaElement) return;
42
+ if (!isStandardPreload(next)) return;
43
+ if (mediaElement.preload === next) return;
44
+ mediaElement.preload = next;
45
+ });
46
+ return () => {
47
+ cleanupRead();
48
+ cleanupWrite();
49
+ };
50
+ }
51
+ const syncPreload = defineBehavior({
52
+ stateKeys: ["preload", "presentation"],
53
+ contextKeys: ["mediaElement"],
54
+ setup: syncPreloadSetup
55
+ });
56
+ //#endregion
57
+ export { syncPreload };
58
+
59
+ //# sourceMappingURL=sync-preload.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync-preload.js","names":[],"sources":["../../../../src/playback/behaviors/sync-preload.ts"],"sourcesContent":["/**\n * **Bidirectional sync between `state.preload` and `mediaElement.preload`.**\n *\n * Two effects:\n * - **Read (DOM → state)** — on `context.mediaElement` swap or\n * `state.presentation.url` change, copies `mediaElement.preload` into\n * `state.preload` if it's a W3C value and `state.preload` isn't holding\n * an extended (non-W3C) value. When the DOM has no W3C opinion and\n * `state.preload` is undefined, backfills from `config.defaultPreload`\n * (default-default `'metadata'`) so `state.preload` is never undefined\n * in steady state.\n * - **Write (state → DOM)** — on `state.preload` change or\n * `context.mediaElement` swap, writes `state.preload` back to\n * `mediaElement.preload` if the value is W3C.\n *\n * Extended values (e.g. `'canplay'`) written externally to `state.preload`\n * are sticky: read won't overwrite them, write won't push them to the DOM.\n * All writes are deduped to break echo loops and avoid spurious re-triggers\n * downstream (notably `resolvePresentation`, which reads `state.preload`).\n */\nimport { defineBehavior } from '../../core/composition/create-composition';\nimport { effect } from '../../core/signals/effect';\nimport { computed, peek, type ReadonlySignal, type Signal } from '../../core/signals/primitives';\nimport type { MediaElementLike } from '../../media/types';\nimport { DEFAULT_PRELOAD, isStandardPreload, type StandardPreload } from '../../media/utils/preload';\nimport type { PresentationState } from './resolve-presentation';\n\nexport interface SyncPreloadConfig {\n /**\n * Backfill applied to `state.preload` when neither the host element nor\n * external code has supplied a W3C value. Defaults to `'metadata'`.\n */\n defaultPreload?: StandardPreload;\n}\n\ntype State = Pick<PresentationState, 'preload' | 'presentation'>;\n\nfunction syncPreloadSetup({\n state,\n context,\n config,\n}: {\n state: {\n preload: Signal<State['preload']>;\n presentation: ReadonlySignal<State['presentation']>;\n };\n context: { mediaElement: ReadonlySignal<MediaElementLike | undefined> };\n config?: SyncPreloadConfig;\n}): () => void {\n const defaultPreload: StandardPreload = config?.defaultPreload ?? DEFAULT_PRELOAD;\n const presentationUrl = computed(() => state.presentation.get()?.url);\n\n // Read must be registered before write: effects re-run in registration\n // order (see core/signals/effect.ts → runPending). On a shared-signal\n // change (e.g. `context.mediaElement` swap), read fires first and gets to\n // claim `state.preload`, giving \"most-recent-wins on attach\" — a freshly\n // mounted <video> with a W3C `preload` attribute overrides whatever\n // `state.preload` previously held. Swap the order or split these into\n // separate behaviors and the \"most-recent-wins on attach\" test pins the\n // regression.\n const cleanupRead = effect(() => {\n presentationUrl.get();\n const mediaElement = context.mediaElement.get();\n // peek (not get): external writes to state.preload shouldn't re-trigger\n // this effect, only mediaElement / presentation-url changes should.\n const current = peek(state.preload);\n // Extended sticky: leave non-W3C values (e.g. 'canplay') alone.\n if (current !== undefined && !isStandardPreload(current)) return;\n\n const target: StandardPreload | undefined =\n mediaElement && isStandardPreload(mediaElement.preload)\n ? mediaElement.preload\n : current === undefined\n ? defaultPreload\n : undefined;\n if (target === undefined || target === current) return;\n state.preload.set(target);\n });\n\n const cleanupWrite = effect(() => {\n const next = state.preload.get();\n const mediaElement = context.mediaElement.get();\n if (!mediaElement) return;\n if (!isStandardPreload(next)) return;\n if (mediaElement.preload === next) return;\n mediaElement.preload = next;\n });\n\n return () => {\n cleanupRead();\n cleanupWrite();\n };\n}\n\nexport const syncPreload = defineBehavior({\n stateKeys: ['preload', 'presentation'],\n contextKeys: ['mediaElement'],\n setup: syncPreloadSetup,\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AAqCA,SAAS,iBAAiB,EACxB,OACA,SACA,UAQa;CACb,MAAM,iBAAkC,QAAQ,kBAAA;CAChD,MAAM,kBAAkB,eAAe,MAAM,aAAa,KAAK,EAAE,IAAI;CAUrE,MAAM,cAAc,aAAa;AAC/B,kBAAgB,KAAK;EACrB,MAAM,eAAe,QAAQ,aAAa,KAAK;EAG/C,MAAM,UAAU,KAAK,MAAM,QAAQ;AAEnC,MAAI,YAAY,KAAA,KAAa,CAAC,kBAAkB,QAAQ,CAAE;EAE1D,MAAM,SACJ,gBAAgB,kBAAkB,aAAa,QAAQ,GACnD,aAAa,UACb,YAAY,KAAA,IACV,iBACA,KAAA;AACR,MAAI,WAAW,KAAA,KAAa,WAAW,QAAS;AAChD,QAAM,QAAQ,IAAI,OAAO;GACzB;CAEF,MAAM,eAAe,aAAa;EAChC,MAAM,OAAO,MAAM,QAAQ,KAAK;EAChC,MAAM,eAAe,QAAQ,aAAa,KAAK;AAC/C,MAAI,CAAC,aAAc;AACnB,MAAI,CAAC,kBAAkB,KAAK,CAAE;AAC9B,MAAI,aAAa,YAAY,KAAM;AACnC,eAAa,UAAU;GACvB;AAEF,cAAa;AACX,eAAa;AACb,gBAAc;;;AAIlB,MAAa,cAAc,eAAe;CACxC,WAAW,CAAC,WAAW,eAAe;CACtC,aAAa,CAAC,eAAe;CAC7B,OAAO;CACR,CAAC"}