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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (375) hide show
  1. package/README.md +5 -0
  2. package/dist/default/background-video.js +3 -0
  3. package/dist/default/core/actors/create-machine-actor.js.map +1 -1
  4. package/dist/default/core/actors/create-transition-actor.js.map +1 -1
  5. package/dist/default/core/composition/create-composition.js +2 -2
  6. package/dist/default/core/composition/create-composition.js.map +1 -1
  7. package/dist/default/core/composition/share-signals.js +13 -11
  8. package/dist/default/core/composition/share-signals.js.map +1 -1
  9. package/dist/default/core/machine.js.map +1 -1
  10. package/dist/default/core/reactors/create-machine-reactor.js.map +1 -1
  11. package/dist/default/core/signals/effect.js.map +1 -1
  12. package/dist/default/core/signals/primitives.js.map +1 -1
  13. package/dist/default/core/tasks/task.js.map +1 -1
  14. package/dist/default/hls.js +4 -1
  15. package/dist/default/media/abr/quality-selection.js +6 -68
  16. package/dist/default/media/abr/quality-selection.js.map +1 -1
  17. package/dist/default/media/buffer/back-buffer.js.map +1 -1
  18. package/dist/default/media/buffer/forward-buffer.js +37 -3
  19. package/dist/default/media/buffer/forward-buffer.js.map +1 -1
  20. package/dist/default/media/dom/capabilities.js +70 -0
  21. package/dist/default/media/dom/capabilities.js.map +1 -0
  22. package/dist/default/media/dom/mse/append-segment.js.map +1 -1
  23. package/dist/default/media/dom/mse/buffer-flusher.js.map +1 -1
  24. package/dist/default/media/dom/mse/duration.js +27 -10
  25. package/dist/default/media/dom/mse/duration.js.map +1 -1
  26. package/dist/default/media/dom/mse/end-of-stream.js.map +1 -1
  27. package/dist/default/media/dom/mse/mediasource-setup.js +68 -17
  28. package/dist/default/media/dom/mse/mediasource-setup.js.map +1 -1
  29. package/dist/default/media/dom/text/resolve-vtt-segment.js +1 -7
  30. package/dist/default/media/dom/text/resolve-vtt-segment.js.map +1 -1
  31. package/dist/default/media/dom/text/text-track-slots.js +3 -3
  32. package/dist/default/media/dom/text/text-track-slots.js.map +1 -1
  33. package/dist/default/media/hls/parse-attributes.js +17 -3
  34. package/dist/default/media/hls/parse-attributes.js.map +1 -1
  35. package/dist/default/media/hls/parse-media-playlist.js +29 -1
  36. package/dist/default/media/hls/parse-media-playlist.js.map +1 -1
  37. package/dist/default/media/hls/parse-multivariant.js +15 -6
  38. package/dist/default/media/hls/parse-multivariant.js.map +1 -1
  39. package/dist/default/media/hls/resolve-url.js.map +1 -1
  40. package/dist/default/media/media-tracks/media-tracks.js +101 -0
  41. package/dist/default/media/media-tracks/media-tracks.js.map +1 -0
  42. package/dist/default/media/mp4/box.js +58 -0
  43. package/dist/default/media/mp4/box.js.map +1 -0
  44. package/dist/default/media/mp4/timestamp-origin.js +107 -0
  45. package/dist/default/media/mp4/timestamp-origin.js.map +1 -0
  46. package/dist/default/media/primitives/select-tracks.js +98 -13
  47. package/dist/default/media/primitives/select-tracks.js.map +1 -1
  48. package/dist/default/media/text/parse-vtt-timestamp-map.js +42 -0
  49. package/dist/default/media/text/parse-vtt-timestamp-map.js.map +1 -0
  50. package/dist/default/media/text/resolve-vtt-metadata.js +23 -0
  51. package/dist/default/media/text/resolve-vtt-metadata.js.map +1 -0
  52. package/dist/default/media/types/index.js.map +1 -1
  53. package/dist/default/media/utils/cdn.js +56 -0
  54. package/dist/default/media/utils/cdn.js.map +1 -0
  55. package/dist/default/media/utils/preload.js.map +1 -1
  56. package/dist/default/media/utils/track-selection.js +1 -1
  57. package/dist/default/media/utils/track-selection.js.map +1 -1
  58. package/dist/default/media/utils/tracks.js +31 -4
  59. package/dist/default/media/utils/tracks.js.map +1 -1
  60. package/dist/default/media-tracks.js +2 -0
  61. package/dist/default/network/bandwidth-estimator.js.map +1 -1
  62. package/dist/default/network/chunked-stream-iterable.js.map +1 -1
  63. package/dist/default/network/ewma.js +1 -1
  64. package/dist/default/network/ewma.js.map +1 -1
  65. package/dist/default/network/fetch.js +7 -1
  66. package/dist/default/network/fetch.js.map +1 -1
  67. package/dist/default/playback/actors/dom/segment-loader.js +103 -106
  68. package/dist/default/playback/actors/dom/segment-loader.js.map +1 -1
  69. package/dist/default/playback/actors/dom/source-buffer.js +3 -1
  70. package/dist/default/playback/actors/dom/source-buffer.js.map +1 -1
  71. package/dist/default/playback/actors/dom/text-tracks.js.map +1 -1
  72. package/dist/default/playback/actors/text-track-segment-loader.js +30 -16
  73. package/dist/default/playback/actors/text-track-segment-loader.js.map +1 -1
  74. package/dist/default/playback/behaviors/calculate-presentation-duration.js +21 -43
  75. package/dist/default/playback/behaviors/calculate-presentation-duration.js.map +1 -1
  76. package/dist/default/playback/behaviors/derive-cdn-priority.js +68 -0
  77. package/dist/default/playback/behaviors/derive-cdn-priority.js.map +1 -0
  78. package/dist/default/playback/behaviors/dom/airplay.js +211 -0
  79. package/dist/default/playback/behaviors/dom/airplay.js.map +1 -0
  80. package/dist/default/playback/behaviors/dom/apply-start-position.js +93 -0
  81. package/dist/default/playback/behaviors/dom/apply-start-position.js.map +1 -0
  82. package/dist/default/playback/behaviors/dom/end-of-stream.js +25 -102
  83. package/dist/default/playback/behaviors/dom/end-of-stream.js.map +1 -1
  84. package/dist/default/playback/behaviors/dom/load-segments.js +6 -2
  85. package/dist/default/playback/behaviors/dom/load-segments.js.map +1 -1
  86. package/dist/default/playback/behaviors/dom/recover-end-stall.js +77 -0
  87. package/dist/default/playback/behaviors/dom/recover-end-stall.js.map +1 -0
  88. package/dist/default/playback/behaviors/dom/setup-buffer-actors.js +50 -18
  89. package/dist/default/playback/behaviors/dom/setup-buffer-actors.js.map +1 -1
  90. package/dist/default/playback/behaviors/dom/setup-mediasource.js +36 -11
  91. package/dist/default/playback/behaviors/dom/setup-mediasource.js.map +1 -1
  92. package/dist/default/playback/behaviors/dom/setup-text-track-actors.js +11 -25
  93. package/dist/default/playback/behaviors/dom/setup-text-track-actors.js.map +1 -1
  94. package/dist/default/playback/behaviors/dom/sync-text-tracks.js +43 -18
  95. package/dist/default/playback/behaviors/dom/sync-text-tracks.js.map +1 -1
  96. package/dist/default/playback/behaviors/dom/track-current-time.js +6 -0
  97. package/dist/default/playback/behaviors/dom/track-current-time.js.map +1 -1
  98. package/dist/default/playback/behaviors/dom/track-load-triggers.js +4 -4
  99. package/dist/default/playback/behaviors/dom/track-load-triggers.js.map +1 -1
  100. package/dist/default/playback/behaviors/dom/track-playback-rate.js.map +1 -1
  101. package/dist/default/playback/behaviors/dom/update-mediasource-duration.js +3 -2
  102. package/dist/default/playback/behaviors/dom/update-mediasource-duration.js.map +1 -1
  103. package/dist/default/playback/behaviors/establish-start-media-time.js +123 -0
  104. package/dist/default/playback/behaviors/establish-start-media-time.js.map +1 -0
  105. package/dist/default/playback/behaviors/resolve-presentation.js.map +1 -1
  106. package/dist/default/playback/behaviors/resolve-track.js +45 -22
  107. package/dist/default/playback/behaviors/resolve-track.js.map +1 -1
  108. package/dist/default/playback/behaviors/select-tracks.js +46 -77
  109. package/dist/default/playback/behaviors/select-tracks.js.map +1 -1
  110. package/dist/default/playback/behaviors/setup-failover-monitor.js +66 -0
  111. package/dist/default/playback/behaviors/setup-failover-monitor.js.map +1 -0
  112. package/dist/default/playback/behaviors/sync-preload.js.map +1 -1
  113. package/dist/default/playback/behaviors/track-switching.js +403 -0
  114. package/dist/default/playback/behaviors/track-switching.js.map +1 -0
  115. package/dist/default/playback/engines/background-video/adapter.js +158 -0
  116. package/dist/default/playback/engines/background-video/adapter.js.map +1 -0
  117. package/dist/default/playback/engines/background-video/engine.js +75 -0
  118. package/dist/default/playback/engines/background-video/engine.js.map +1 -0
  119. package/dist/default/playback/engines/hls/adapter-audio-only.js +124 -0
  120. package/dist/default/playback/engines/hls/adapter-audio-only.js.map +1 -0
  121. package/dist/default/playback/engines/hls/adapter.js +20 -11
  122. package/dist/default/playback/engines/hls/adapter.js.map +1 -1
  123. package/dist/default/playback/engines/hls/engine-audio-only.js +94 -0
  124. package/dist/default/playback/engines/hls/engine-audio-only.js.map +1 -0
  125. package/dist/default/playback/engines/hls/engine.js +38 -8
  126. package/dist/default/playback/engines/hls/engine.js.map +1 -1
  127. package/dist/default/playback/primitives/failover-fetch.js +42 -0
  128. package/dist/default/playback/primitives/failover-fetch.js.map +1 -0
  129. package/dist/default/playback/primitives/head-peek.js +44 -0
  130. package/dist/default/playback/primitives/head-peek.js.map +1 -0
  131. package/dist/default/playback/primitives/relocation-pipelines.js +203 -0
  132. package/dist/default/playback/primitives/relocation-pipelines.js.map +1 -0
  133. package/dist/default/playback/primitives/segment-load-pipeline.js +96 -0
  134. package/dist/default/playback/primitives/segment-load-pipeline.js.map +1 -0
  135. package/dist/default/playback/primitives/text-segment-load-pipeline.js +37 -0
  136. package/dist/default/playback/primitives/text-segment-load-pipeline.js.map +1 -0
  137. package/dist/{dev/playback/behaviors → default/playback/primitives}/track-types.js +1 -1
  138. package/dist/default/playback/primitives/track-types.js.map +1 -0
  139. package/dist/dev/background-video.d.ts +3 -0
  140. package/dist/dev/background-video.js +3 -0
  141. package/dist/dev/core/actors/actor.d.ts +0 -1
  142. package/dist/dev/core/actors/actor.d.ts.map +1 -1
  143. package/dist/dev/core/actors/create-machine-actor.d.ts +9 -6
  144. package/dist/dev/core/actors/create-machine-actor.d.ts.map +1 -1
  145. package/dist/dev/core/actors/create-machine-actor.js.map +1 -1
  146. package/dist/dev/core/actors/create-transition-actor.d.ts +0 -1
  147. package/dist/dev/core/actors/create-transition-actor.d.ts.map +1 -1
  148. package/dist/dev/core/actors/create-transition-actor.js.map +1 -1
  149. package/dist/dev/core/composition/create-composition.d.ts +10 -11
  150. package/dist/dev/core/composition/create-composition.d.ts.map +1 -1
  151. package/dist/dev/core/composition/create-composition.js +2 -2
  152. package/dist/dev/core/composition/create-composition.js.map +1 -1
  153. package/dist/dev/core/composition/share-signals.d.ts +11 -10
  154. package/dist/dev/core/composition/share-signals.d.ts.map +1 -1
  155. package/dist/dev/core/composition/share-signals.js +13 -11
  156. package/dist/dev/core/composition/share-signals.js.map +1 -1
  157. package/dist/dev/core/machine.d.ts +1 -0
  158. package/dist/dev/core/machine.d.ts.map +1 -1
  159. package/dist/dev/core/machine.js.map +1 -1
  160. package/dist/dev/core/reactors/create-machine-reactor.d.ts +2 -2
  161. package/dist/dev/core/reactors/create-machine-reactor.d.ts.map +1 -1
  162. package/dist/dev/core/reactors/create-machine-reactor.js.map +1 -1
  163. package/dist/dev/core/signals/effect.d.ts.map +1 -1
  164. package/dist/dev/core/signals/effect.js.map +1 -1
  165. package/dist/dev/core/signals/primitives.d.ts +1 -2
  166. package/dist/dev/core/signals/primitives.d.ts.map +1 -1
  167. package/dist/dev/core/signals/primitives.js.map +1 -1
  168. package/dist/dev/core/tasks/task.d.ts +2 -2
  169. package/dist/dev/core/tasks/task.d.ts.map +1 -1
  170. package/dist/dev/core/tasks/task.js.map +1 -1
  171. package/dist/dev/hls.d.ts +5 -1
  172. package/dist/dev/hls.js +4 -1
  173. package/dist/dev/index.d.ts +0 -1
  174. package/dist/dev/index.d.ts.map +1 -1
  175. package/dist/dev/media/abr/quality-selection.d.ts.map +1 -1
  176. package/dist/dev/media/abr/quality-selection.js +6 -68
  177. package/dist/dev/media/abr/quality-selection.js.map +1 -1
  178. package/dist/dev/media/buffer/back-buffer.d.ts.map +1 -1
  179. package/dist/dev/media/buffer/back-buffer.js.map +1 -1
  180. package/dist/dev/media/buffer/forward-buffer.d.ts.map +1 -1
  181. package/dist/dev/media/buffer/forward-buffer.js +37 -3
  182. package/dist/dev/media/buffer/forward-buffer.js.map +1 -1
  183. package/dist/dev/media/dom/capabilities.js +70 -0
  184. package/dist/dev/media/dom/capabilities.js.map +1 -0
  185. package/dist/dev/media/dom/mse/append-segment.d.ts +3 -9
  186. package/dist/dev/media/dom/mse/append-segment.d.ts.map +1 -1
  187. package/dist/dev/media/dom/mse/append-segment.js.map +1 -1
  188. package/dist/dev/media/dom/mse/buffer-flusher.d.ts.map +1 -1
  189. package/dist/dev/media/dom/mse/buffer-flusher.js.map +1 -1
  190. package/dist/dev/media/dom/mse/duration.js +27 -10
  191. package/dist/dev/media/dom/mse/duration.js.map +1 -1
  192. package/dist/dev/media/dom/mse/end-of-stream.js.map +1 -1
  193. package/dist/dev/media/dom/mse/mediasource-setup.js +68 -17
  194. package/dist/dev/media/dom/mse/mediasource-setup.js.map +1 -1
  195. package/dist/dev/media/dom/text/resolve-vtt-segment.d.ts +0 -6
  196. package/dist/dev/media/dom/text/resolve-vtt-segment.d.ts.map +1 -1
  197. package/dist/dev/media/dom/text/resolve-vtt-segment.js +1 -7
  198. package/dist/dev/media/dom/text/resolve-vtt-segment.js.map +1 -1
  199. package/dist/dev/media/dom/text/text-track-slots.d.ts +0 -1
  200. package/dist/dev/media/dom/text/text-track-slots.d.ts.map +1 -1
  201. package/dist/dev/media/dom/text/text-track-slots.js +3 -3
  202. package/dist/dev/media/dom/text/text-track-slots.js.map +1 -1
  203. package/dist/dev/media/hls/parse-attributes.js +17 -3
  204. package/dist/dev/media/hls/parse-attributes.js.map +1 -1
  205. package/dist/dev/media/hls/parse-media-playlist.js +29 -1
  206. package/dist/dev/media/hls/parse-media-playlist.js.map +1 -1
  207. package/dist/dev/media/hls/parse-multivariant.js +15 -6
  208. package/dist/dev/media/hls/parse-multivariant.js.map +1 -1
  209. package/dist/dev/media/hls/resolve-url.js.map +1 -1
  210. package/dist/dev/media/media-tracks/media-tracks.d.ts +51 -0
  211. package/dist/dev/media/media-tracks/media-tracks.d.ts.map +1 -0
  212. package/dist/dev/media/media-tracks/media-tracks.js +101 -0
  213. package/dist/dev/media/media-tracks/media-tracks.js.map +1 -0
  214. package/dist/dev/media/mp4/box.js +58 -0
  215. package/dist/dev/media/mp4/box.js.map +1 -0
  216. package/dist/dev/media/mp4/timestamp-origin.js +107 -0
  217. package/dist/dev/media/mp4/timestamp-origin.js.map +1 -0
  218. package/dist/dev/media/primitives/select-tracks.d.ts +35 -0
  219. package/dist/dev/media/primitives/select-tracks.d.ts.map +1 -0
  220. package/dist/dev/media/primitives/select-tracks.js +98 -13
  221. package/dist/dev/media/primitives/select-tracks.js.map +1 -1
  222. package/dist/dev/media/text/parse-vtt-timestamp-map.js +42 -0
  223. package/dist/dev/media/text/parse-vtt-timestamp-map.js.map +1 -0
  224. package/dist/dev/media/text/resolve-vtt-metadata.js +23 -0
  225. package/dist/dev/media/text/resolve-vtt-metadata.js.map +1 -0
  226. package/dist/dev/media/types/index.d.ts +69 -2
  227. package/dist/dev/media/types/index.d.ts.map +1 -1
  228. package/dist/dev/media/types/index.js.map +1 -1
  229. package/dist/dev/media/utils/cdn.d.ts +13 -0
  230. package/dist/dev/media/utils/cdn.d.ts.map +1 -0
  231. package/dist/dev/media/utils/cdn.js +56 -0
  232. package/dist/dev/media/utils/cdn.js.map +1 -0
  233. package/dist/dev/media/utils/preload.js.map +1 -1
  234. package/dist/dev/media/utils/track-selection.js +1 -1
  235. package/dist/dev/media/utils/track-selection.js.map +1 -1
  236. package/dist/dev/media/utils/tracks.js +31 -4
  237. package/dist/dev/media/utils/tracks.js.map +1 -1
  238. package/dist/dev/media-tracks.d.ts +3 -0
  239. package/dist/dev/media-tracks.js +2 -0
  240. package/dist/dev/network/bandwidth-estimator.d.ts.map +1 -1
  241. package/dist/dev/network/bandwidth-estimator.js.map +1 -1
  242. package/dist/dev/network/chunked-stream-iterable.js.map +1 -1
  243. package/dist/dev/network/ewma.js +1 -1
  244. package/dist/dev/network/ewma.js.map +1 -1
  245. package/dist/dev/network/fetch.js +7 -1
  246. package/dist/dev/network/fetch.js.map +1 -1
  247. package/dist/dev/playback/actors/dom/segment-loader.d.ts +16 -4
  248. package/dist/dev/playback/actors/dom/segment-loader.d.ts.map +1 -1
  249. package/dist/dev/playback/actors/dom/segment-loader.js +103 -106
  250. package/dist/dev/playback/actors/dom/segment-loader.js.map +1 -1
  251. package/dist/dev/playback/actors/dom/source-buffer.d.ts +10 -25
  252. package/dist/dev/playback/actors/dom/source-buffer.d.ts.map +1 -1
  253. package/dist/dev/playback/actors/dom/source-buffer.js +3 -1
  254. package/dist/dev/playback/actors/dom/source-buffer.js.map +1 -1
  255. package/dist/dev/playback/actors/dom/text-tracks.d.ts +3 -1
  256. package/dist/dev/playback/actors/dom/text-tracks.js.map +1 -1
  257. package/dist/dev/playback/actors/text-track-segment-loader.d.ts +7 -12
  258. package/dist/dev/playback/actors/text-track-segment-loader.d.ts.map +1 -1
  259. package/dist/dev/playback/actors/text-track-segment-loader.js +30 -16
  260. package/dist/dev/playback/actors/text-track-segment-loader.js.map +1 -1
  261. package/dist/dev/playback/actors/text-tracks.d.ts +3 -12
  262. package/dist/dev/playback/actors/text-tracks.d.ts.map +1 -1
  263. package/dist/dev/playback/behaviors/calculate-presentation-duration.d.ts +11 -2
  264. package/dist/dev/playback/behaviors/calculate-presentation-duration.d.ts.map +1 -1
  265. package/dist/dev/playback/behaviors/calculate-presentation-duration.js +21 -43
  266. package/dist/dev/playback/behaviors/calculate-presentation-duration.js.map +1 -1
  267. package/dist/dev/playback/behaviors/derive-cdn-priority.js +68 -0
  268. package/dist/dev/playback/behaviors/derive-cdn-priority.js.map +1 -0
  269. package/dist/dev/playback/behaviors/dom/airplay.js +211 -0
  270. package/dist/dev/playback/behaviors/dom/airplay.js.map +1 -0
  271. package/dist/dev/playback/behaviors/dom/apply-start-position.js +93 -0
  272. package/dist/dev/playback/behaviors/dom/apply-start-position.js.map +1 -0
  273. package/dist/dev/playback/behaviors/dom/end-of-stream.js +25 -102
  274. package/dist/dev/playback/behaviors/dom/end-of-stream.js.map +1 -1
  275. package/dist/dev/playback/behaviors/dom/load-segments.d.ts +14 -4
  276. package/dist/dev/playback/behaviors/dom/load-segments.d.ts.map +1 -1
  277. package/dist/dev/playback/behaviors/dom/load-segments.js +6 -2
  278. package/dist/dev/playback/behaviors/dom/load-segments.js.map +1 -1
  279. package/dist/dev/playback/behaviors/dom/recover-end-stall.js +77 -0
  280. package/dist/dev/playback/behaviors/dom/recover-end-stall.js.map +1 -0
  281. package/dist/dev/playback/behaviors/dom/setup-buffer-actors.js +50 -18
  282. package/dist/dev/playback/behaviors/dom/setup-buffer-actors.js.map +1 -1
  283. package/dist/dev/playback/behaviors/dom/setup-mediasource.js +36 -11
  284. package/dist/dev/playback/behaviors/dom/setup-mediasource.js.map +1 -1
  285. package/dist/dev/playback/behaviors/dom/setup-text-track-actors.d.ts +16 -19
  286. package/dist/dev/playback/behaviors/dom/setup-text-track-actors.d.ts.map +1 -1
  287. package/dist/dev/playback/behaviors/dom/setup-text-track-actors.js +11 -25
  288. package/dist/dev/playback/behaviors/dom/setup-text-track-actors.js.map +1 -1
  289. package/dist/dev/playback/behaviors/dom/sync-text-tracks.js +43 -18
  290. package/dist/dev/playback/behaviors/dom/sync-text-tracks.js.map +1 -1
  291. package/dist/dev/playback/behaviors/dom/track-current-time.d.ts +1 -2
  292. package/dist/dev/playback/behaviors/dom/track-current-time.d.ts.map +1 -1
  293. package/dist/dev/playback/behaviors/dom/track-current-time.js +6 -0
  294. package/dist/dev/playback/behaviors/dom/track-current-time.js.map +1 -1
  295. package/dist/dev/playback/behaviors/dom/track-load-triggers.d.ts +1 -2
  296. package/dist/dev/playback/behaviors/dom/track-load-triggers.d.ts.map +1 -1
  297. package/dist/dev/playback/behaviors/dom/track-load-triggers.js +4 -4
  298. package/dist/dev/playback/behaviors/dom/track-load-triggers.js.map +1 -1
  299. package/dist/dev/playback/behaviors/dom/track-playback-rate.d.ts +1 -2
  300. package/dist/dev/playback/behaviors/dom/track-playback-rate.d.ts.map +1 -1
  301. package/dist/dev/playback/behaviors/dom/track-playback-rate.js.map +1 -1
  302. package/dist/dev/playback/behaviors/dom/update-mediasource-duration.js +3 -2
  303. package/dist/dev/playback/behaviors/dom/update-mediasource-duration.js.map +1 -1
  304. package/dist/dev/playback/behaviors/establish-start-media-time.d.ts +31 -0
  305. package/dist/dev/playback/behaviors/establish-start-media-time.d.ts.map +1 -0
  306. package/dist/dev/playback/behaviors/establish-start-media-time.js +123 -0
  307. package/dist/dev/playback/behaviors/establish-start-media-time.js.map +1 -0
  308. package/dist/dev/playback/behaviors/resolve-presentation.d.ts +2 -1
  309. package/dist/dev/playback/behaviors/resolve-presentation.d.ts.map +1 -1
  310. package/dist/dev/playback/behaviors/resolve-presentation.js.map +1 -1
  311. package/dist/dev/playback/behaviors/resolve-track.js +45 -22
  312. package/dist/dev/playback/behaviors/resolve-track.js.map +1 -1
  313. package/dist/dev/playback/behaviors/select-tracks.d.ts +14 -0
  314. package/dist/dev/playback/behaviors/select-tracks.d.ts.map +1 -0
  315. package/dist/dev/playback/behaviors/select-tracks.js +46 -77
  316. package/dist/dev/playback/behaviors/select-tracks.js.map +1 -1
  317. package/dist/dev/playback/behaviors/setup-failover-monitor.d.ts +14 -0
  318. package/dist/dev/playback/behaviors/setup-failover-monitor.d.ts.map +1 -0
  319. package/dist/dev/playback/behaviors/setup-failover-monitor.js +66 -0
  320. package/dist/dev/playback/behaviors/setup-failover-monitor.js.map +1 -0
  321. package/dist/dev/playback/behaviors/sync-preload.js.map +1 -1
  322. package/dist/dev/playback/behaviors/track-switching.js +403 -0
  323. package/dist/dev/playback/behaviors/track-switching.js.map +1 -0
  324. package/dist/dev/playback/engines/background-video/adapter.d.ts +59 -0
  325. package/dist/dev/playback/engines/background-video/adapter.d.ts.map +1 -0
  326. package/dist/dev/playback/engines/background-video/adapter.js +158 -0
  327. package/dist/dev/playback/engines/background-video/adapter.js.map +1 -0
  328. package/dist/dev/playback/engines/background-video/engine.d.ts +106 -0
  329. package/dist/dev/playback/engines/background-video/engine.d.ts.map +1 -0
  330. package/dist/dev/playback/engines/background-video/engine.js +75 -0
  331. package/dist/dev/playback/engines/background-video/engine.js.map +1 -0
  332. package/dist/dev/playback/engines/hls/adapter-audio-only.d.ts +46 -0
  333. package/dist/dev/playback/engines/hls/adapter-audio-only.d.ts.map +1 -0
  334. package/dist/dev/playback/engines/hls/adapter-audio-only.js +124 -0
  335. package/dist/dev/playback/engines/hls/adapter-audio-only.js.map +1 -0
  336. package/dist/dev/playback/engines/hls/adapter.d.ts +3 -5
  337. package/dist/dev/playback/engines/hls/adapter.d.ts.map +1 -1
  338. package/dist/dev/playback/engines/hls/adapter.js +20 -11
  339. package/dist/dev/playback/engines/hls/adapter.js.map +1 -1
  340. package/dist/dev/playback/engines/hls/engine-audio-only.d.ts +156 -0
  341. package/dist/dev/playback/engines/hls/engine-audio-only.d.ts.map +1 -0
  342. package/dist/dev/playback/engines/hls/engine-audio-only.js +94 -0
  343. package/dist/dev/playback/engines/hls/engine-audio-only.js.map +1 -0
  344. package/dist/dev/playback/engines/hls/engine.d.ts +109 -4
  345. package/dist/dev/playback/engines/hls/engine.d.ts.map +1 -1
  346. package/dist/dev/playback/engines/hls/engine.js +38 -8
  347. package/dist/dev/playback/engines/hls/engine.js.map +1 -1
  348. package/dist/dev/playback/primitives/derive-start-media-time.d.ts +16 -0
  349. package/dist/dev/playback/primitives/derive-start-media-time.d.ts.map +1 -0
  350. package/dist/dev/playback/primitives/failover-fetch.js +42 -0
  351. package/dist/dev/playback/primitives/failover-fetch.js.map +1 -0
  352. package/dist/dev/playback/primitives/head-peek.js +44 -0
  353. package/dist/dev/playback/primitives/head-peek.js.map +1 -0
  354. package/dist/dev/playback/primitives/relocation-pipelines.js +203 -0
  355. package/dist/dev/playback/primitives/relocation-pipelines.js.map +1 -0
  356. package/dist/dev/playback/primitives/segment-load-pipeline.d.ts +2 -0
  357. package/dist/dev/playback/primitives/segment-load-pipeline.js +96 -0
  358. package/dist/dev/playback/primitives/segment-load-pipeline.js.map +1 -0
  359. package/dist/dev/playback/primitives/source-buffer-messages.d.ts +43 -0
  360. package/dist/dev/playback/primitives/source-buffer-messages.d.ts.map +1 -0
  361. package/dist/dev/playback/primitives/text-segment-load-pipeline.d.ts +65 -0
  362. package/dist/dev/playback/primitives/text-segment-load-pipeline.d.ts.map +1 -0
  363. package/dist/dev/playback/primitives/text-segment-load-pipeline.js +37 -0
  364. package/dist/dev/playback/primitives/text-segment-load-pipeline.js.map +1 -0
  365. package/dist/dev/playback/primitives/text-track-messages.d.ts +14 -0
  366. package/dist/dev/playback/primitives/text-track-messages.d.ts.map +1 -0
  367. package/dist/{default/playback/behaviors → dev/playback/primitives}/track-types.js +1 -1
  368. package/dist/dev/playback/primitives/track-types.js.map +1 -0
  369. package/package.json +17 -7
  370. package/dist/default/playback/behaviors/quality-switching.js +0 -96
  371. package/dist/default/playback/behaviors/quality-switching.js.map +0 -1
  372. package/dist/default/playback/behaviors/track-types.js.map +0 -1
  373. package/dist/dev/playback/behaviors/quality-switching.js +0 -96
  374. package/dist/dev/playback/behaviors/quality-switching.js.map +0 -1
  375. package/dist/dev/playback/behaviors/track-types.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"setup-text-track-actors.js","names":[],"sources":["../../../../../src/playback/behaviors/dom/setup-text-track-actors.ts"],"sourcesContent":["/**\n * **Own the TextTracks actor pair for the current `mediaElement`.** When a\n * `mediaElement` is in scope, creates the `TextTracksActor` (bound to the\n * element's `textTracks`) and the `TextTrackSegmentLoaderActor` (bound to\n * that actor + the injected cue resolver), and publishes both on\n * `context`. On element identity change or behavior destroy, destroys both\n * actors and clears the slots.\n *\n * Single-resource synchronous create/destroy driven by one signal — the\n * simple-effect form is the right shape (per `behaviors.md` → \"Where both\n * shapes are legitimate\": criterion 4b applies; sole writer of\n * `textTracksActor` / `textTrackSegmentLoaderActor`, the effect's cleanup\n * return handles destroy + slot clear structurally).\n *\n * Pairs with the `loadTextTrackSegments` behavior (a per-type variant of\n * `setupSegmentLoading` in `load-segments.ts`), which only reads\n * `textTrackSegmentLoaderActor`. The cue resolver is injected via\n * `config` so this behavior owns the DOM-bound part of the text-track\n * pipeline.\n */\nimport { defineBehavior } from '../../../core/composition/create-composition';\nimport { effect } from '../../../core/signals/effect';\nimport type { ReadonlySignal, Signal } from '../../../core/signals/primitives';\nimport { createTextTracksActor } from '../../actors/dom/text-tracks';\nimport {\n createTextTrackSegmentLoaderActor,\n type TextTrackSegmentLoaderActor,\n type TextTrackSegmentLoaderActorConfig,\n type TextTrackSegmentResolver,\n} from '../../actors/text-track-segment-loader';\nimport type { TextTracksActor } from '../../actors/text-tracks';\n\nexport interface TextTrackActorsContext {\n mediaElement?: HTMLMediaElement | undefined;\n textTracksActor?: TextTracksActor<VTTCue> | undefined;\n textTrackSegmentLoaderActor?: TextTrackSegmentLoaderActor | undefined;\n}\n\nexport interface TextTrackActorsConfig extends TextTrackSegmentLoaderActorConfig {\n resolveTextTrackSegment: TextTrackSegmentResolver<VTTCue>;\n}\n\nfunction setupTextTrackActorsSetup({\n context,\n config,\n}: {\n context: {\n mediaElement: ReadonlySignal<TextTrackActorsContext['mediaElement']>;\n textTracksActor: Signal<TextTrackActorsContext['textTracksActor']>;\n textTrackSegmentLoaderActor: Signal<TextTrackActorsContext['textTrackSegmentLoaderActor']>;\n };\n config: TextTrackActorsConfig;\n}): () => void {\n return effect(() => {\n const mediaElement = context.mediaElement.get();\n if (!mediaElement) return;\n\n const textTracksActor = createTextTracksActor(mediaElement);\n const textTrackSegmentLoaderActor = createTextTrackSegmentLoaderActor(\n textTracksActor,\n config.resolveTextTrackSegment,\n { forwardBuffer: config.forwardBuffer }\n );\n context.textTracksActor.set(textTracksActor);\n context.textTrackSegmentLoaderActor.set(textTrackSegmentLoaderActor);\n\n return () => {\n textTracksActor.destroy();\n textTrackSegmentLoaderActor.destroy();\n context.textTracksActor.set(undefined);\n context.textTrackSegmentLoaderActor.set(undefined);\n };\n });\n}\n\nexport const setupTextTrackActors = defineBehavior({\n stateKeys: [],\n contextKeys: ['mediaElement', 'textTracksActor', 'textTrackSegmentLoaderActor'],\n setup: setupTextTrackActorsSetup,\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AA0CA,SAAS,0BAA0B,EACjC,SACA,UAQa;AACb,QAAO,aAAa;EAClB,MAAM,eAAe,QAAQ,aAAa,KAAK;AAC/C,MAAI,CAAC,aAAc;EAEnB,MAAM,kBAAkB,sBAAsB,aAAa;EAC3D,MAAM,8BAA8B,kCAClC,iBACA,OAAO,yBACP,EAAE,eAAe,OAAO,eAAe,CACxC;AACD,UAAQ,gBAAgB,IAAI,gBAAgB;AAC5C,UAAQ,4BAA4B,IAAI,4BAA4B;AAEpE,eAAa;AACX,mBAAgB,SAAS;AACzB,+BAA4B,SAAS;AACrC,WAAQ,gBAAgB,IAAI,KAAA,EAAU;AACtC,WAAQ,4BAA4B,IAAI,KAAA,EAAU;;GAEpD;;AAGJ,MAAa,uBAAuB,eAAe;CACjD,WAAW,EAAE;CACb,aAAa;EAAC;EAAgB;EAAmB;EAA8B;CAC/E,OAAO;CACR,CAAC"}
1
+ {"version":3,"file":"setup-text-track-actors.js","names":[],"sources":["../../../../../src/playback/behaviors/dom/setup-text-track-actors.ts"],"sourcesContent":["/**\n * **Own the TextTracks actor pair for the current `mediaElement`.** When a\n * `mediaElement` is in scope, creates the `TextTracksActor` (bound to the\n * element's `textTracks`) and the `TextTrackSegmentLoaderActor` (bound to\n * that actor + the injected cue resolver), and publishes both on\n * `context`. On element identity change or behavior destroy, destroys both\n * actors and clears the slots.\n *\n * Single-resource synchronous create/destroy driven by one signal — the\n * simple-effect form is the right shape (per `behaviors.md` → \"Where both\n * shapes are legitimate\": criterion 4b applies; sole writer of\n * `textTracksActor` / `textTrackSegmentLoaderActor`, the effect's cleanup\n * return handles destroy + slot clear structurally).\n *\n * Pairs with the `loadTextTrackSegments` behavior (a per-type variant of\n * `setupSegmentLoading` in `load-segments.ts`), which only reads\n * `textTrackSegmentLoaderActor`. The cue resolver is injected via\n * `config` so this behavior owns the DOM-bound part of the text-track\n * pipeline.\n */\nimport type { AnySlotMap, Behavior } from '../../../core/composition/create-composition';\nimport { effect } from '../../../core/signals/effect';\nimport type { ReadonlySignal, Signal } from '../../../core/signals/primitives';\nimport { createTextTracksActor } from '../../actors/dom/text-tracks';\nimport {\n createTextTrackSegmentLoaderActor,\n type TextTrackSegmentLoaderActor,\n type TextTrackSegmentLoaderActorConfig,\n} from '../../actors/text-track-segment-loader';\nimport type { TextTracksActor } from '../../actors/text-tracks';\nimport type { TextMessagePipelines, TextTrackSegmentResolver } from '../../primitives/text-segment-load-pipeline';\n\nexport interface TextTrackActorsContext {\n mediaElement?: HTMLMediaElement | undefined;\n textTracksActor?: TextTracksActor<VTTCue> | undefined;\n textTrackSegmentLoaderActor?: TextTrackSegmentLoaderActor | undefined;\n}\n\nexport interface TextTrackActorsConfig extends Pick<TextTrackSegmentLoaderActorConfig<VTTCue>, 'forwardBuffer'> {\n resolveTextTrackSegment: TextTrackSegmentResolver<VTTCue>;\n /**\n * Ordered text step pipeline, mapped to the loader's `messagePipelines`. Named\n * with the `text` domain prefix to mirror the v/a `video`/`audioMessagePipelines`\n * composition-config slots. Defaults (in the loader) to `resolveCues → dispatchCues`.\n */\n textMessagePipelines?: TextMessagePipelines<VTTCue>;\n}\n\nfunction setupTextTrackActorsSetup({\n state,\n context,\n config,\n}: {\n // Forwarded opaquely into the loader's steps (relocation reads composition state);\n // this behavior owns no state of its own (stateKeys: []).\n state: AnySlotMap;\n context: {\n mediaElement: ReadonlySignal<TextTrackActorsContext['mediaElement']>;\n textTracksActor: Signal<TextTrackActorsContext['textTracksActor']>;\n textTrackSegmentLoaderActor: Signal<TextTrackActorsContext['textTrackSegmentLoaderActor']>;\n };\n config: TextTrackActorsConfig;\n}): () => void {\n return effect(() => {\n const mediaElement = context.mediaElement.get();\n if (!mediaElement) return;\n\n const textTracksActor = createTextTracksActor(mediaElement);\n const textTrackSegmentLoaderActor = createTextTrackSegmentLoaderActor(\n textTracksActor,\n config.resolveTextTrackSegment,\n { forwardBuffer: config.forwardBuffer, messagePipelines: config.textMessagePipelines },\n // Composition deps forwarded into each step (relocation reads the primary A/V origin).\n { state, context, config }\n );\n context.textTracksActor.set(textTracksActor);\n context.textTrackSegmentLoaderActor.set(textTrackSegmentLoaderActor);\n\n return () => {\n textTracksActor.destroy();\n textTrackSegmentLoaderActor.destroy();\n context.textTracksActor.set(undefined);\n context.textTrackSegmentLoaderActor.set(undefined);\n };\n });\n}\n\n// Manual `Behavior` literal (like `end-of-stream`): no state of its own\n// (`stateKeys: []`), but the setup forwards the composition `state` to the resolver\n// opaquely. A literal (not `defineBehavior`) so `stateKeys: []` can coexist with a\n// setup that reads `state`.\nexport const setupTextTrackActors: Behavior<\n Record<never, never>,\n {\n mediaElement: ReadonlySignal<TextTrackActorsContext['mediaElement']>;\n textTracksActor: Signal<TextTrackActorsContext['textTracksActor']>;\n textTrackSegmentLoaderActor: Signal<TextTrackActorsContext['textTrackSegmentLoaderActor']>;\n },\n TextTrackActorsConfig\n> = {\n stateKeys: [],\n contextKeys: ['mediaElement', 'textTracksActor', 'textTrackSegmentLoaderActor'],\n setup: setupTextTrackActorsSetup,\n};\n"],"mappings":";;;;AAgDA,SAAS,0BAA0B,EACjC,OACA,SACA,UAWa;CACb,OAAO,aAAa;EAClB,MAAM,eAAe,QAAQ,aAAa,IAAI;EAC9C,IAAI,CAAC,cAAc;EAEnB,MAAM,kBAAkB,sBAAsB,YAAY;EAC1D,MAAM,8BAA8B,kCAClC,iBACA,OAAO,yBACP;GAAE,eAAe,OAAO;GAAe,kBAAkB,OAAO;EAAqB,GAErF;GAAE;GAAO;GAAS;EAAO,CAC3B;EACA,QAAQ,gBAAgB,IAAI,eAAe;EAC3C,QAAQ,4BAA4B,IAAI,2BAA2B;EAEnE,aAAa;GACX,gBAAgB,QAAQ;GACxB,4BAA4B,QAAQ;GACpC,QAAQ,gBAAgB,IAAI,KAAA,CAAS;GACrC,QAAQ,4BAA4B,IAAI,KAAA,CAAS;EACnD;CACF,CAAC;AACH;AAMA,MAAa,uBAQT;CACF,WAAW,CAAC;CACZ,aAAa;EAAC;EAAgB;EAAmB;CAA6B;CAC9E,OAAO;AACT"}
@@ -11,11 +11,13 @@ import { listen } from "@videojs/utils/dom";
11
11
  * available, allocate one slot in `mediaElement.textTracks` per model text
12
12
  * track — via creating `<track>` children, since that's the only spec
13
13
  * mechanism for adding *and* removing entries to `textTracks` (no
14
- * `removeTextTrack` API exists). Once slots are provisioned, mirror
15
- * `selectedTextTrackId` into their `mode`s, and propagate user-initiated
16
- * DOM `change` events back to `selectedTextTrackId` so non-SPF consumers
17
- * (host-page captions buttons, browser native UI, video.js store) remain
18
- * the canonical write path for selection.
14
+ * `removeTextTrack` API exists). Once slots are provisioned, mirror the
15
+ * resolved `selectedTextTrackId` into their `mode`s (one-way: state → DOM),
16
+ * and propagate user-initiated DOM `change` events back to
17
+ * `userTextTrackSelection` the standing *intent* (a language-based partial,
18
+ * or `'off'`) that `switchTextTrack` resolves into `selectedTextTrackId`. So
19
+ * non-SPF consumers (host-page captions buttons, browser native UI, video.js
20
+ * store) drive selection by expressing intent, not by writing the resolved id.
19
21
  *
20
22
  * Single-positive-state reactor (`'preconditions-unmet'` ↔ `'sync-active'`):
21
23
  * the entry allocates the slots, applies the initial selection, attaches
@@ -34,19 +36,37 @@ import { listen } from "@videojs/utils/dom";
34
36
  * presentation reusing a trackId would have `getSegmentsToLoad` treat
35
37
  * its segments as already-buffered and skip loading them.
36
38
  *
37
- * Multi-writer with `selectTextTrack` is intentional and orthogonal:
38
- * `selectTextTrack` owns the *default-on-load / clear-on-unload* contract
39
- * for `selectedTextTrackId`; this behavior writes only from DOM `change`
40
- * events. They reflect distinct decision-making domains (config-driven
41
- * intent vs DOM-driven user action), so this behavior never clears the
42
- * slot on source unload that's `selectTextTrack`'s contract. (It *does*
43
- * write `undefined` when a user disables all tracks via native UI, since
44
- * that's a real user action that should round-trip into SPF state.)
39
+ * Single-writer separation: `selectedTextTrackId` is the resolved *output*
40
+ * owned solely by `switchTextTrack`; this behavior only reads it (to mirror
41
+ * modes). The write path here is `userTextTrackSelection` the user-intent
42
+ * *input* so DOM action and the resolver never contend for one slot. The
43
+ * intent isn't cleared on source unload (it's a standing preference, like
44
+ * `userAudioTrackSelection`); `'off'` is written when the user disables all
45
+ * tracks via native UI.
46
+ *
47
+ * Echo guard: `selectedTextTrackId` is exactly the id this behavior last drove
48
+ * into the DOM, so a `change` event still showing it is our own echo (or a
49
+ * resolver-driven correction — e.g. the picked track's CDN failed and the
50
+ * resolver disabled it) and is ignored, never written back as a spurious user
51
+ * action. Only a showing id that *differs* from the resolved id is a real user
52
+ * pick. The settling-window guard additionally swallows Chromium's init-time
53
+ * auto-selection before the resolved selection has settled.
45
54
  */
46
55
  function deriveState(presentation, mediaElement) {
47
56
  if (!mediaElement || !presentation) return "preconditions-unmet";
48
57
  return getTracksByType(presentation, "text").length > 0 ? "sync-active" : "preconditions-unmet";
49
58
  }
59
+ /**
60
+ * Map the DOM-showing track back to standing user intent. No showing track is an
61
+ * explicit `'off'`. Otherwise prefer a language-based partial (so the pick
62
+ * persists across source changes); fall back to `{ id }` for a track without a
63
+ * language (precise within a source, just not portable).
64
+ */
65
+ function deriveTextTrackIntent(showingId, modelTextTracks) {
66
+ if (!showingId) return "off";
67
+ const language = modelTextTracks.find((track) => track.id === showingId)?.language;
68
+ return language ? { language } : { id: showingId };
69
+ }
50
70
  function syncTextTracksSetup({ state, context, config }) {
51
71
  const { addSubtitlesTracksToMedia, getShowingSubtitlesTrackFromMedia, removeAllSubtitlesTracksFromMedia } = config;
52
72
  const derivedStateSignal = computed(() => deriveState(state.presentation.get(), context.mediaElement.get()));
@@ -58,7 +78,8 @@ function syncTextTracksSetup({ state, context, config }) {
58
78
  "sync-active": {
59
79
  entry: () => {
60
80
  const mediaElement = context.mediaElement.get();
61
- addSubtitlesTracksToMedia(mediaElement, getTracksByType(state.presentation.get(), "text"));
81
+ const modelTextTracks = getTracksByType(state.presentation.get(), "text");
82
+ addSubtitlesTracksToMedia(mediaElement, modelTextTracks);
62
83
  syncTextTrackModes(mediaElement.textTracks, state.selectedTextTrackId.get());
63
84
  let inSettlingWindow = true;
64
85
  const settlingTimeout = setTimeout(() => {
@@ -69,9 +90,9 @@ function syncTextTracksSetup({ state, context, config }) {
69
90
  syncTextTrackModes(mediaElement.textTracks, state.selectedTextTrackId.get());
70
91
  return;
71
92
  }
72
- const nextId = getShowingSubtitlesTrackFromMedia(mediaElement)?.id || void 0;
73
- if (nextId === state.selectedTextTrackId.get()) return;
74
- state.selectedTextTrackId.set(nextId);
93
+ const showingId = getShowingSubtitlesTrackFromMedia(mediaElement)?.id || void 0;
94
+ if (showingId === state.selectedTextTrackId.get()) return;
95
+ state.userTextTrackSelection.set(deriveTextTrackIntent(showingId, modelTextTracks));
75
96
  };
76
97
  const unlisten = listen(mediaElement.textTracks, "change", onChange);
77
98
  return () => {
@@ -89,7 +110,11 @@ function syncTextTracksSetup({ state, context, config }) {
89
110
  });
90
111
  }
91
112
  const syncTextTracks = defineBehavior({
92
- stateKeys: ["presentation", "selectedTextTrackId"],
113
+ stateKeys: [
114
+ "presentation",
115
+ "selectedTextTrackId",
116
+ "userTextTrackSelection"
117
+ ],
93
118
  contextKeys: ["mediaElement", "textTracksActor"],
94
119
  setup: syncTextTracksSetup
95
120
  });
@@ -1 +1 @@
1
- {"version":3,"file":"sync-text-tracks.js","names":[],"sources":["../../../../../src/playback/behaviors/dom/sync-text-tracks.ts"],"sourcesContent":["/**\n * **Own the text-track slots on the host media element, mirroring the SPF\n * model.** When a presentation is resolved and a media element is\n * available, allocate one slot in `mediaElement.textTracks` per model text\n * track — via creating `<track>` children, since that's the only spec\n * mechanism for adding *and* removing entries to `textTracks` (no\n * `removeTextTrack` API exists). Once slots are provisioned, mirror\n * `selectedTextTrackId` into their `mode`s, and propagate user-initiated\n * DOM `change` events back to `selectedTextTrackId` so non-SPF consumers\n * (host-page captions buttons, browser native UI, video.js store) remain\n * the canonical write path for selection.\n *\n * Single-positive-state reactor (`'preconditions-unmet'` ↔ `'sync-active'`):\n * the entry allocates the slots, applies the initial selection, attaches\n * the `change` listener, and opens a brief Chromium settling-window guard —\n * all transition-driven, fire-once on state entry, with paired cleanup on\n * state exit. A single `effects:` mirrors subsequent\n * `selectedTextTrackId` changes into `mode`s; that's the only\n * continuous-reactivity concern.\n *\n * State-exit cleanup also sends a `'clear'` message to the\n * `TextTracksActor` so its cue+segment cache (keyed by trackId) is\n * dropped alongside the DOM `<track>` slots. The actor itself is owned\n * by `setupTextTrackActors` and bound to mediaElement, not presentation,\n * so it survives source resets; clearing its context here keeps the\n * cache consistent with the DOM. Without this, a subsequent\n * presentation reusing a trackId would have `getSegmentsToLoad` treat\n * its segments as already-buffered and skip loading them.\n *\n * Multi-writer with `selectTextTrack` is intentional and orthogonal:\n * `selectTextTrack` owns the *default-on-load / clear-on-unload* contract\n * for `selectedTextTrackId`; this behavior writes only from DOM `change`\n * events. They reflect distinct decision-making domains (config-driven\n * intent vs DOM-driven user action), so this behavior never clears the\n * slot on source unload — that's `selectTextTrack`'s contract. (It *does*\n * write `undefined` when a user disables all tracks via native UI, since\n * that's a real user action that should round-trip into SPF state.)\n */\n\nimport { listen } from '@videojs/utils/dom';\nimport { defineBehavior } from '../../../core/composition/create-composition';\nimport type { Reactor } from '../../../core/reactors/create-machine-reactor';\nimport { createMachineReactor } from '../../../core/reactors/create-machine-reactor';\nimport { computed, peek, type ReadonlySignal, type Signal } from '../../../core/signals/primitives';\nimport { syncTextTrackModes } from '../../../media/dom/text/text-track-slots';\nimport type { MaybeResolvedPresentation, PartiallyResolvedTextTrack, TextTrack } from '../../../media/types';\nimport { getTracksByType } from '../../../media/utils/tracks';\nimport type { TextTracksActor } from '../../actors/text-tracks';\n\ntype SyncTextTracksFsmState = 'preconditions-unmet' | 'sync-active';\n\nexport interface SyncTextTracksConfig {\n /**\n * Create and append SPF-owned `<track>` slots on `mediaElement`, one per\n * model text track. Implementation tags each element so the read/remove\n * helpers can scope to SPF-owned slots. **Required** — the behavior is\n * DOM-binding-neutral and the composing engine supplies the integration.\n */\n addSubtitlesTracksToMedia: (\n mediaElement: HTMLMediaElement,\n modelTextTracks: readonly (PartiallyResolvedTextTrack | TextTrack)[]\n ) => void;\n /**\n * Return the SPF-owned subtitle/caption `TextTrack` currently in `'showing'`\n * mode, or `undefined` if none. Used by the DOM `change` bridge to mirror\n * native-UI selection back into `selectedTextTrackId`.\n */\n getShowingSubtitlesTrackFromMedia: (mediaElement: HTMLMediaElement) => globalThis.TextTrack | undefined;\n /**\n * Remove every SPF-owned `<track>` child from `mediaElement`. Called on\n * state exit (source unload, behavior destroy) to evict slots.\n */\n removeAllSubtitlesTracksFromMedia: (mediaElement: HTMLMediaElement) => void;\n}\n\nfunction deriveState(\n presentation: MaybeResolvedPresentation | undefined,\n mediaElement: HTMLMediaElement | undefined\n): SyncTextTracksFsmState {\n if (!mediaElement || !presentation) return 'preconditions-unmet';\n return getTracksByType(presentation, 'text').length > 0 ? 'sync-active' : 'preconditions-unmet';\n}\n\nfunction syncTextTracksSetup({\n state,\n context,\n config,\n}: {\n state: {\n presentation: ReadonlySignal<MaybeResolvedPresentation | undefined>;\n selectedTextTrackId: Signal<string | undefined>;\n };\n context: {\n mediaElement: ReadonlySignal<HTMLMediaElement | undefined>;\n textTracksActor: ReadonlySignal<TextTracksActor<VTTCue> | undefined>;\n };\n config: SyncTextTracksConfig;\n}): Reactor<SyncTextTracksFsmState | 'destroying' | 'destroyed'> {\n const { addSubtitlesTracksToMedia, getShowingSubtitlesTrackFromMedia, removeAllSubtitlesTracksFromMedia } = config;\n\n const derivedStateSignal = computed(() => deriveState(state.presentation.get(), context.mediaElement.get()));\n\n return createMachineReactor<SyncTextTracksFsmState>({\n initial: 'preconditions-unmet',\n monitor: () => derivedStateSignal.get(),\n states: {\n 'preconditions-unmet': {},\n\n 'sync-active': {\n // Allocate slots, apply the initial selection, attach the change\n // listener, and open the settling window — all transition-driven\n // (fire once per state entry). Entry is auto-untracked, so `.get()`\n // here doesn't subscribe; the returned cleanup detaches and evicts\n // on state exit (src reset through 'preconditions-unmet') and on\n // destroy.\n entry: () => {\n const mediaElement = context.mediaElement.get()!;\n // `getTracksByType('text', ...)` returns text tracks only — the\n // selection-set filter inside the helper ensures that — but its\n // declared return is the wide track union. Mirror the cast\n // pattern used by `quality-switching` for the video branch.\n const modelTextTracks = getTracksByType(state.presentation.get()!, 'text') as readonly (\n | PartiallyResolvedTextTrack\n | TextTrack\n )[];\n\n addSubtitlesTracksToMedia(mediaElement, modelTextTracks);\n // Apply our selection synchronously so the change-event tasks\n // these mode writes queue land in the macrotask queue *before*\n // the settling-close `setTimeout(0)` queued below. This keeps\n // the settling window open long enough to swallow Chromium's\n // own auto-selection task (also scheduled in the next tick after\n // `<track>` insertion); if the order flipped, our handler would\n // treat Chromium's auto-pick as a user action. The `effects:`\n // block below handles subsequent `selectedTextTrackId` changes.\n syncTextTrackModes(mediaElement.textTracks, state.selectedTextTrackId.get());\n\n // Chromium re-applies its own selection across the next task tick\n // after `<track>` insertion (default-track auto-pick, language\n // preference). During this window, the `change` event may fire\n // with browser-chosen modes that don't reflect a real user action;\n // re-apply our modes silently rather than writing them back.\n let inSettlingWindow = true;\n const settlingTimeout = setTimeout(() => {\n inSettlingWindow = false;\n }, 0);\n\n const onChange = (): void => {\n if (inSettlingWindow) {\n syncTextTrackModes(mediaElement.textTracks, state.selectedTextTrackId.get());\n return;\n }\n const showingTrack = getShowingSubtitlesTrackFromMedia(mediaElement);\n // `showingTrack.id` matches the SPF id we set when the slot was\n // allocated. Empty-string ids fall through to `undefined`.\n const nextId = showingTrack?.id || undefined;\n if (nextId === state.selectedTextTrackId.get()) return;\n state.selectedTextTrackId.set(nextId);\n };\n\n const unlisten = listen(mediaElement.textTracks, 'change', onChange);\n\n return () => {\n unlisten();\n clearTimeout(settlingTimeout);\n removeAllSubtitlesTracksFromMedia(mediaElement);\n // Clear the TextTracksActor's cue+segment cache, which is\n // keyed by trackId. If we don't, a subsequent presentation\n // reusing a trackId would have `getSegmentsToLoad` treat its\n // segments as already-buffered. The DOM cleanup above\n // already evicted the live cues; this drops the cache that\n // tracked them. The actor itself is owned by\n // `setupTextTrackActors` (mediaElement-bound lifecycle), so\n // we send rather than destroy.\n peek(context.textTracksActor)?.send({ type: 'clear' });\n };\n },\n\n // Mirror selection changes into mode. The state machine handles\n // mediaElement/presentation changes via 'preconditions-unmet'\n // round-trips, so we peek mediaElement here.\n effects: () => {\n const mediaElement = peek(context.mediaElement)!;\n syncTextTrackModes(mediaElement.textTracks, state.selectedTextTrackId.get());\n },\n },\n },\n });\n}\n\nexport const syncTextTracks = defineBehavior({\n stateKeys: ['presentation', 'selectedTextTrackId'],\n contextKeys: ['mediaElement', 'textTracksActor'],\n setup: syncTextTracksSetup,\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2EA,SAAS,YACP,cACA,cACwB;AACxB,KAAI,CAAC,gBAAgB,CAAC,aAAc,QAAO;AAC3C,QAAO,gBAAgB,cAAc,OAAO,CAAC,SAAS,IAAI,gBAAgB;;AAG5E,SAAS,oBAAoB,EAC3B,OACA,SACA,UAW+D;CAC/D,MAAM,EAAE,2BAA2B,mCAAmC,sCAAsC;CAE5G,MAAM,qBAAqB,eAAe,YAAY,MAAM,aAAa,KAAK,EAAE,QAAQ,aAAa,KAAK,CAAC,CAAC;AAE5G,QAAO,qBAA6C;EAClD,SAAS;EACT,eAAe,mBAAmB,KAAK;EACvC,QAAQ;GACN,uBAAuB,EAAE;GAEzB,eAAe;IAOb,aAAa;KACX,MAAM,eAAe,QAAQ,aAAa,KAAK;AAU/C,+BAA0B,cALF,gBAAgB,MAAM,aAAa,KAAK,EAAG,OAAO,CAKlB;AASxD,wBAAmB,aAAa,YAAY,MAAM,oBAAoB,KAAK,CAAC;KAO5E,IAAI,mBAAmB;KACvB,MAAM,kBAAkB,iBAAiB;AACvC,yBAAmB;QAClB,EAAE;KAEL,MAAM,iBAAuB;AAC3B,UAAI,kBAAkB;AACpB,0BAAmB,aAAa,YAAY,MAAM,oBAAoB,KAAK,CAAC;AAC5E;;MAKF,MAAM,SAHe,kCAAkC,aAAa,EAGvC,MAAM,KAAA;AACnC,UAAI,WAAW,MAAM,oBAAoB,KAAK,CAAE;AAChD,YAAM,oBAAoB,IAAI,OAAO;;KAGvC,MAAM,WAAW,OAAO,aAAa,YAAY,UAAU,SAAS;AAEpE,kBAAa;AACX,gBAAU;AACV,mBAAa,gBAAgB;AAC7B,wCAAkC,aAAa;AAS/C,WAAK,QAAQ,gBAAgB,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;;;IAO1D,eAAe;AAEb,wBADqB,KAAK,QAAQ,aAAa,CACf,YAAY,MAAM,oBAAoB,KAAK,CAAC;;IAE/E;GACF;EACF,CAAC;;AAGJ,MAAa,iBAAiB,eAAe;CAC3C,WAAW,CAAC,gBAAgB,sBAAsB;CAClD,aAAa,CAAC,gBAAgB,kBAAkB;CAChD,OAAO;CACR,CAAC"}
1
+ {"version":3,"file":"sync-text-tracks.js","names":[],"sources":["../../../../../src/playback/behaviors/dom/sync-text-tracks.ts"],"sourcesContent":["/**\n * **Own the text-track slots on the host media element, mirroring the SPF\n * model.** When a presentation is resolved and a media element is\n * available, allocate one slot in `mediaElement.textTracks` per model text\n * track — via creating `<track>` children, since that's the only spec\n * mechanism for adding *and* removing entries to `textTracks` (no\n * `removeTextTrack` API exists). Once slots are provisioned, mirror the\n * resolved `selectedTextTrackId` into their `mode`s (one-way: state → DOM),\n * and propagate user-initiated DOM `change` events back to\n * `userTextTrackSelection` — the standing *intent* (a language-based partial,\n * or `'off'`) that `switchTextTrack` resolves into `selectedTextTrackId`. So\n * non-SPF consumers (host-page captions buttons, browser native UI, video.js\n * store) drive selection by expressing intent, not by writing the resolved id.\n *\n * Single-positive-state reactor (`'preconditions-unmet'` ↔ `'sync-active'`):\n * the entry allocates the slots, applies the initial selection, attaches\n * the `change` listener, and opens a brief Chromium settling-window guard —\n * all transition-driven, fire-once on state entry, with paired cleanup on\n * state exit. A single `effects:` mirrors subsequent\n * `selectedTextTrackId` changes into `mode`s; that's the only\n * continuous-reactivity concern.\n *\n * State-exit cleanup also sends a `'clear'` message to the\n * `TextTracksActor` so its cue+segment cache (keyed by trackId) is\n * dropped alongside the DOM `<track>` slots. The actor itself is owned\n * by `setupTextTrackActors` and bound to mediaElement, not presentation,\n * so it survives source resets; clearing its context here keeps the\n * cache consistent with the DOM. Without this, a subsequent\n * presentation reusing a trackId would have `getSegmentsToLoad` treat\n * its segments as already-buffered and skip loading them.\n *\n * Single-writer separation: `selectedTextTrackId` is the resolved *output*\n * owned solely by `switchTextTrack`; this behavior only reads it (to mirror\n * modes). The write path here is `userTextTrackSelection` — the user-intent\n * *input* — so DOM action and the resolver never contend for one slot. The\n * intent isn't cleared on source unload (it's a standing preference, like\n * `userAudioTrackSelection`); `'off'` is written when the user disables all\n * tracks via native UI.\n *\n * Echo guard: `selectedTextTrackId` is exactly the id this behavior last drove\n * into the DOM, so a `change` event still showing it is our own echo (or a\n * resolver-driven correction — e.g. the picked track's CDN failed and the\n * resolver disabled it) and is ignored, never written back as a spurious user\n * action. Only a showing id that *differs* from the resolved id is a real user\n * pick. The settling-window guard additionally swallows Chromium's init-time\n * auto-selection before the resolved selection has settled.\n */\n\nimport { listen } from '@videojs/utils/dom';\nimport { defineBehavior } from '../../../core/composition/create-composition';\nimport type { Reactor } from '../../../core/reactors/create-machine-reactor';\nimport { createMachineReactor } from '../../../core/reactors/create-machine-reactor';\nimport { computed, peek, type ReadonlySignal, type Signal } from '../../../core/signals/primitives';\nimport { syncTextTrackModes } from '../../../media/dom/text/text-track-slots';\nimport type { MaybeResolvedPresentation, PartiallyResolvedTextTrack, TextTrack } from '../../../media/types';\nimport { getTracksByType } from '../../../media/utils/tracks';\nimport type { TextTracksActor } from '../../actors/text-tracks';\n\ntype SyncTextTracksFsmState = 'preconditions-unmet' | 'sync-active';\n\nexport interface SyncTextTracksConfig {\n /**\n * Create and append SPF-owned `<track>` slots on `mediaElement`, one per\n * model text track. Implementation tags each element so the read/remove\n * helpers can scope to SPF-owned slots. **Required** — the behavior is\n * DOM-binding-neutral and the composing engine supplies the integration.\n */\n addSubtitlesTracksToMedia: (\n mediaElement: HTMLMediaElement,\n modelTextTracks: readonly (PartiallyResolvedTextTrack | TextTrack)[]\n ) => void;\n /**\n * Return the SPF-owned subtitle/caption `TextTrack` currently in `'showing'`\n * mode, or `undefined` if none. Used by the DOM `change` bridge to mirror\n * native-UI selection back into `selectedTextTrackId`.\n */\n getShowingSubtitlesTrackFromMedia: (mediaElement: HTMLMediaElement) => globalThis.TextTrack | undefined;\n /**\n * Remove every SPF-owned `<track>` child from `mediaElement`. Called on\n * state exit (source unload, behavior destroy) to evict slots.\n */\n removeAllSubtitlesTracksFromMedia: (mediaElement: HTMLMediaElement) => void;\n}\n\nfunction deriveState(\n presentation: MaybeResolvedPresentation | undefined,\n mediaElement: HTMLMediaElement | undefined\n): SyncTextTracksFsmState {\n if (!mediaElement || !presentation) return 'preconditions-unmet';\n return getTracksByType(presentation, 'text').length > 0 ? 'sync-active' : 'preconditions-unmet';\n}\n\n/**\n * Map the DOM-showing track back to standing user intent. No showing track is an\n * explicit `'off'`. Otherwise prefer a language-based partial (so the pick\n * persists across source changes); fall back to `{ id }` for a track without a\n * language (precise within a source, just not portable).\n */\nfunction deriveTextTrackIntent(\n showingId: string | undefined,\n modelTextTracks: readonly (PartiallyResolvedTextTrack | TextTrack)[]\n): Partial<TextTrack> | 'off' {\n if (!showingId) return 'off';\n const language = modelTextTracks.find((track) => track.id === showingId)?.language;\n return language ? { language } : { id: showingId };\n}\n\nfunction syncTextTracksSetup({\n state,\n context,\n config,\n}: {\n state: {\n presentation: ReadonlySignal<MaybeResolvedPresentation | undefined>;\n // Read-only here: the resolved output owned by `switchTextTrack`, mirrored\n // into DOM modes and used as the echo-guard reference.\n selectedTextTrackId: ReadonlySignal<string | undefined>;\n // The write path: standing user intent the DOM bridge feeds.\n userTextTrackSelection: Signal<Partial<TextTrack> | 'off' | undefined>;\n };\n context: {\n mediaElement: ReadonlySignal<HTMLMediaElement | undefined>;\n textTracksActor: ReadonlySignal<TextTracksActor<VTTCue> | undefined>;\n };\n config: SyncTextTracksConfig;\n}): Reactor<SyncTextTracksFsmState | 'destroying' | 'destroyed'> {\n const { addSubtitlesTracksToMedia, getShowingSubtitlesTrackFromMedia, removeAllSubtitlesTracksFromMedia } = config;\n\n const derivedStateSignal = computed(() => deriveState(state.presentation.get(), context.mediaElement.get()));\n\n return createMachineReactor<SyncTextTracksFsmState>({\n initial: 'preconditions-unmet',\n monitor: () => derivedStateSignal.get(),\n states: {\n 'preconditions-unmet': {},\n\n 'sync-active': {\n // Allocate slots, apply the initial selection, attach the change\n // listener, and open the settling window — all transition-driven\n // (fire once per state entry). Entry is auto-untracked, so `.get()`\n // here doesn't subscribe; the returned cleanup detaches and evicts\n // on state exit (src reset through 'preconditions-unmet') and on\n // destroy.\n entry: () => {\n const mediaElement = context.mediaElement.get()!;\n // `getTracksByType('text', ...)` returns text tracks only — the\n // selection-set filter inside the helper ensures that — but its\n // declared return is the wide track union. Mirror the cast\n // pattern used by `track-switching` for the video branch.\n const modelTextTracks = getTracksByType(state.presentation.get()!, 'text') as readonly (\n | PartiallyResolvedTextTrack\n | TextTrack\n )[];\n\n addSubtitlesTracksToMedia(mediaElement, modelTextTracks);\n // Apply our selection synchronously so the change-event tasks\n // these mode writes queue land in the macrotask queue *before*\n // the settling-close `setTimeout(0)` queued below. This keeps\n // the settling window open long enough to swallow Chromium's\n // own auto-selection task (also scheduled in the next tick after\n // `<track>` insertion); if the order flipped, our handler would\n // treat Chromium's auto-pick as a user action. The `effects:`\n // block below handles subsequent `selectedTextTrackId` changes.\n syncTextTrackModes(mediaElement.textTracks, state.selectedTextTrackId.get());\n\n // Chromium re-applies its own selection across the next task tick\n // after `<track>` insertion (default-track auto-pick, language\n // preference). During this window, the `change` event may fire\n // with browser-chosen modes that don't reflect a real user action;\n // re-apply our modes silently rather than writing them back.\n let inSettlingWindow = true;\n const settlingTimeout = setTimeout(() => {\n inSettlingWindow = false;\n }, 0);\n\n const onChange = (): void => {\n if (inSettlingWindow) {\n syncTextTrackModes(mediaElement.textTracks, state.selectedTextTrackId.get());\n return;\n }\n const showingTrack = getShowingSubtitlesTrackFromMedia(mediaElement);\n // `showingTrack.id` matches the SPF id we set when the slot was\n // allocated. Empty-string ids fall through to `undefined`.\n const showingId = showingTrack?.id || undefined;\n // Echo guard: selectedTextTrackId is the id we last drove into the\n // DOM (mirror / resolver correction). A change still showing it is our\n // own echo — ignore it rather than write spurious intent.\n if (showingId === state.selectedTextTrackId.get()) return;\n // Genuine user action → write intent (resolved into selectedTextTrackId\n // by switchTextTrack), not the resolved id.\n state.userTextTrackSelection.set(deriveTextTrackIntent(showingId, modelTextTracks));\n };\n\n const unlisten = listen(mediaElement.textTracks, 'change', onChange);\n\n return () => {\n unlisten();\n clearTimeout(settlingTimeout);\n removeAllSubtitlesTracksFromMedia(mediaElement);\n // Clear the TextTracksActor's cue+segment cache, which is\n // keyed by trackId. If we don't, a subsequent presentation\n // reusing a trackId would have `getSegmentsToLoad` treat its\n // segments as already-buffered. The DOM cleanup above\n // already evicted the live cues; this drops the cache that\n // tracked them. The actor itself is owned by\n // `setupTextTrackActors` (mediaElement-bound lifecycle), so\n // we send rather than destroy.\n peek(context.textTracksActor)?.send({ type: 'clear' });\n };\n },\n\n // Mirror selection changes into mode. The state machine handles\n // mediaElement/presentation changes via 'preconditions-unmet'\n // round-trips, so we peek mediaElement here.\n effects: () => {\n const mediaElement = peek(context.mediaElement)!;\n syncTextTrackModes(mediaElement.textTracks, state.selectedTextTrackId.get());\n },\n },\n },\n });\n}\n\nexport const syncTextTracks = defineBehavior({\n stateKeys: ['presentation', 'selectedTextTrackId', 'userTextTrackSelection'],\n contextKeys: ['mediaElement', 'textTracksActor'],\n setup: syncTextTracksSetup,\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoFA,SAAS,YACP,cACA,cACwB;CACxB,IAAI,CAAC,gBAAgB,CAAC,cAAc,OAAO;CAC3C,OAAO,gBAAgB,cAAc,MAAM,CAAC,CAAC,SAAS,IAAI,gBAAgB;AAC5E;;;;;;;AAQA,SAAS,sBACP,WACA,iBAC4B;CAC5B,IAAI,CAAC,WAAW,OAAO;CACvB,MAAM,WAAW,gBAAgB,MAAM,UAAU,MAAM,OAAO,SAAS,CAAC,EAAE;CAC1E,OAAO,WAAW,EAAE,SAAS,IAAI,EAAE,IAAI,UAAU;AACnD;AAEA,SAAS,oBAAoB,EAC3B,OACA,SACA,UAe+D;CAC/D,MAAM,EAAE,2BAA2B,mCAAmC,sCAAsC;CAE5G,MAAM,qBAAqB,eAAe,YAAY,MAAM,aAAa,IAAI,GAAG,QAAQ,aAAa,IAAI,CAAC,CAAC;CAE3G,OAAO,qBAA6C;EAClD,SAAS;EACT,eAAe,mBAAmB,IAAI;EACtC,QAAQ;GACN,uBAAuB,CAAC;GAExB,eAAe;IAOb,aAAa;KACX,MAAM,eAAe,QAAQ,aAAa,IAAI;KAK9C,MAAM,kBAAkB,gBAAgB,MAAM,aAAa,IAAI,GAAI,MAAM;KAKzE,0BAA0B,cAAc,eAAe;KASvD,mBAAmB,aAAa,YAAY,MAAM,oBAAoB,IAAI,CAAC;KAO3E,IAAI,mBAAmB;KACvB,MAAM,kBAAkB,iBAAiB;MACvC,mBAAmB;KACrB,GAAG,CAAC;KAEJ,MAAM,iBAAuB;MAC3B,IAAI,kBAAkB;OACpB,mBAAmB,aAAa,YAAY,MAAM,oBAAoB,IAAI,CAAC;OAC3E;MACF;MAIA,MAAM,YAHe,kCAAkC,YAG1B,CAAC,EAAE,MAAM,KAAA;MAItC,IAAI,cAAc,MAAM,oBAAoB,IAAI,GAAG;MAGnD,MAAM,uBAAuB,IAAI,sBAAsB,WAAW,eAAe,CAAC;KACpF;KAEA,MAAM,WAAW,OAAO,aAAa,YAAY,UAAU,QAAQ;KAEnE,aAAa;MACX,SAAS;MACT,aAAa,eAAe;MAC5B,kCAAkC,YAAY;MAS9C,KAAK,QAAQ,eAAe,CAAC,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;KACvD;IACF;IAKA,eAAe;KAEb,mBADqB,KAAK,QAAQ,YACJ,CAAC,CAAC,YAAY,MAAM,oBAAoB,IAAI,CAAC;IAC7E;GACF;EACF;CACF,CAAC;AACH;AAEA,MAAa,iBAAiB,eAAe;CAC3C,WAAW;EAAC;EAAgB;EAAuB;CAAwB;CAC3E,aAAa,CAAC,gBAAgB,iBAAiB;CAC/C,OAAO;AACT,CAAC"}
@@ -9,6 +9,10 @@ import { listen } from "@videojs/utils/dom";
9
9
  * at the new position when this event dispatches, so buffer management can
10
10
  * react immediately rather than waiting for `timeupdate`, which does not
11
11
  * fire while paused.
12
+ * - `emptied` — fires when the resource selection algorithm tears down the
13
+ * current media (e.g. a new `src` is set on the same element). Re-syncs so
14
+ * downstream state doesn't retain the stale playback position from the
15
+ * previous source when the engine is reused across src changes.
12
16
  *
13
17
  * Also syncs immediately when a media element becomes available.
14
18
  *
@@ -27,9 +31,11 @@ function trackCurrentTimeSetup({ state, context, config }) {
27
31
  }
28
32
  const sync = () => state.currentTime.set(mediaElement.currentTime);
29
33
  sync();
34
+ const removeEmptied = listen(mediaElement, "emptied", sync);
30
35
  const removeTimeupdate = listen(mediaElement, "timeupdate", sync);
31
36
  const removeSeeking = listen(mediaElement, "seeking", sync);
32
37
  return () => {
38
+ removeEmptied();
33
39
  removeTimeupdate();
34
40
  removeSeeking();
35
41
  };
@@ -1 +1 @@
1
- {"version":3,"file":"track-current-time.js","names":[],"sources":["../../../../../src/playback/behaviors/dom/track-current-time.ts"],"sourcesContent":["/**\n * Mirror `mediaElement.currentTime` into reactive state. Listens for:\n * - `timeupdate` — fires during playback (~4 Hz)\n * - `seeking` — fires when a seek begins; per spec, `currentTime` is already\n * at the new position when this event dispatches, so buffer management can\n * react immediately rather than waiting for `timeupdate`, which does not\n * fire while paused.\n *\n * Also syncs immediately when a media element becomes available.\n *\n * When no media element is attached, writes `config.defaultCurrentTime`\n * (default-default `0`, matching the HTMLMediaElement spec) so consumers\n * always see a defined position. Read-only mirror; does not push\n * `state.currentTime` back to the element.\n */\nimport { listen } from '@videojs/utils/dom';\nimport { defineBehavior } from '../../../core/composition/create-composition';\nimport { effect } from '../../../core/signals/effect';\nimport type { ReadonlySignal, Signal } from '../../../core/signals/primitives';\n\nexport interface CurrentTimeState {\n currentTime?: number;\n}\n\nexport interface CurrentTimeContext {\n mediaElement?: HTMLMediaElement | undefined;\n}\n\nexport interface TrackCurrentTimeConfig {\n /**\n * Value written to `state.currentTime` when no media element is attached.\n * Defaults to `0` — the HTMLMediaElement spec default.\n */\n defaultCurrentTime?: number;\n}\n\nfunction trackCurrentTimeSetup({\n state,\n context,\n config,\n}: {\n state: { currentTime: Signal<CurrentTimeState['currentTime']> };\n context: { mediaElement: ReadonlySignal<CurrentTimeContext['mediaElement']> };\n config?: TrackCurrentTimeConfig;\n}): () => void {\n const defaultCurrentTime = config?.defaultCurrentTime ?? 0;\n\n return effect(() => {\n const mediaElement = context.mediaElement.get();\n if (!mediaElement) {\n state.currentTime.set(defaultCurrentTime);\n return;\n }\n\n const sync = () => state.currentTime.set(mediaElement.currentTime);\n sync();\n const removeTimeupdate = listen(mediaElement, 'timeupdate', sync);\n const removeSeeking = listen(mediaElement, 'seeking', sync);\n return () => {\n removeTimeupdate();\n removeSeeking();\n };\n });\n}\n\nexport const trackCurrentTime = defineBehavior({\n stateKeys: ['currentTime'],\n contextKeys: ['mediaElement'],\n setup: trackCurrentTimeSetup,\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAoCA,SAAS,sBAAsB,EAC7B,OACA,SACA,UAKa;CACb,MAAM,qBAAqB,QAAQ,sBAAsB;AAEzD,QAAO,aAAa;EAClB,MAAM,eAAe,QAAQ,aAAa,KAAK;AAC/C,MAAI,CAAC,cAAc;AACjB,SAAM,YAAY,IAAI,mBAAmB;AACzC;;EAGF,MAAM,aAAa,MAAM,YAAY,IAAI,aAAa,YAAY;AAClE,QAAM;EACN,MAAM,mBAAmB,OAAO,cAAc,cAAc,KAAK;EACjE,MAAM,gBAAgB,OAAO,cAAc,WAAW,KAAK;AAC3D,eAAa;AACX,qBAAkB;AAClB,kBAAe;;GAEjB;;AAGJ,MAAa,mBAAmB,eAAe;CAC7C,WAAW,CAAC,cAAc;CAC1B,aAAa,CAAC,eAAe;CAC7B,OAAO;CACR,CAAC"}
1
+ {"version":3,"file":"track-current-time.js","names":[],"sources":["../../../../../src/playback/behaviors/dom/track-current-time.ts"],"sourcesContent":["/**\n * Mirror `mediaElement.currentTime` into reactive state. Listens for:\n * - `timeupdate` — fires during playback (~4 Hz)\n * - `seeking` — fires when a seek begins; per spec, `currentTime` is already\n * at the new position when this event dispatches, so buffer management can\n * react immediately rather than waiting for `timeupdate`, which does not\n * fire while paused.\n * - `emptied` — fires when the resource selection algorithm tears down the\n * current media (e.g. a new `src` is set on the same element). Re-syncs so\n * downstream state doesn't retain the stale playback position from the\n * previous source when the engine is reused across src changes.\n *\n * Also syncs immediately when a media element becomes available.\n *\n * When no media element is attached, writes `config.defaultCurrentTime`\n * (default-default `0`, matching the HTMLMediaElement spec) so consumers\n * always see a defined position. Read-only mirror; does not push\n * `state.currentTime` back to the element.\n */\nimport { listen } from '@videojs/utils/dom';\nimport { defineBehavior } from '../../../core/composition/create-composition';\nimport { effect } from '../../../core/signals/effect';\nimport type { ReadonlySignal, Signal } from '../../../core/signals/primitives';\n\nexport interface CurrentTimeState {\n currentTime?: number;\n}\n\nexport interface CurrentTimeContext {\n mediaElement?: HTMLMediaElement | undefined;\n}\n\nexport interface TrackCurrentTimeConfig {\n /**\n * Value written to `state.currentTime` when no media element is attached.\n * Defaults to `0` — the HTMLMediaElement spec default.\n */\n defaultCurrentTime?: number;\n}\n\nfunction trackCurrentTimeSetup({\n state,\n context,\n config,\n}: {\n state: { currentTime: Signal<CurrentTimeState['currentTime']> };\n context: { mediaElement: ReadonlySignal<CurrentTimeContext['mediaElement']> };\n config?: TrackCurrentTimeConfig;\n}): () => void {\n const defaultCurrentTime = config?.defaultCurrentTime ?? 0;\n\n return effect(() => {\n const mediaElement = context.mediaElement.get();\n if (!mediaElement) {\n state.currentTime.set(defaultCurrentTime);\n return;\n }\n\n const sync = () => state.currentTime.set(mediaElement.currentTime);\n sync();\n const removeEmptied = listen(mediaElement, 'emptied', sync);\n const removeTimeupdate = listen(mediaElement, 'timeupdate', sync);\n const removeSeeking = listen(mediaElement, 'seeking', sync);\n return () => {\n removeEmptied();\n removeTimeupdate();\n removeSeeking();\n };\n });\n}\n\nexport const trackCurrentTime = defineBehavior({\n stateKeys: ['currentTime'],\n contextKeys: ['mediaElement'],\n setup: trackCurrentTimeSetup,\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAwCA,SAAS,sBAAsB,EAC7B,OACA,SACA,UAKa;CACb,MAAM,qBAAqB,QAAQ,sBAAsB;CAEzD,OAAO,aAAa;EAClB,MAAM,eAAe,QAAQ,aAAa,IAAI;EAC9C,IAAI,CAAC,cAAc;GACjB,MAAM,YAAY,IAAI,kBAAkB;GACxC;EACF;EAEA,MAAM,aAAa,MAAM,YAAY,IAAI,aAAa,WAAW;EACjE,KAAK;EACL,MAAM,gBAAgB,OAAO,cAAc,WAAW,IAAI;EAC1D,MAAM,mBAAmB,OAAO,cAAc,cAAc,IAAI;EAChE,MAAM,gBAAgB,OAAO,cAAc,WAAW,IAAI;EAC1D,aAAa;GACX,cAAc;GACd,iBAAiB;GACjB,cAAc;EAChB;CACF,CAAC;AACH;AAEA,MAAa,mBAAmB,eAAe;CAC7C,WAAW,CAAC,aAAa;CACzB,aAAa,CAAC,cAAc;CAC5B,OAAO;AACT,CAAC"}
@@ -32,9 +32,9 @@ function deriveState(presentation, mediaElement, loadActivated) {
32
32
  *
33
33
  * Writes `state.loadActivated = true` the first time a `play` or `seeking`
34
34
  * event fires on the attached media element for the current source — or
35
- * immediately on entry if the element is already in such a state
36
- * (`!el.paused` or `el.seeking`), mirroring autoplay / native-controls /
37
- * direct-DOM-`play()` scenarios.
35
+ * immediately on entry if the element is already committed to loading
36
+ * (`el.autoplay`, `!el.paused`, or `el.seeking`), covering autoplay,
37
+ * native-controls, and direct-DOM-`play()` scenarios.
38
38
  *
39
39
  * Sticky-true *within a source identity*: subsequent play/pause/seek
40
40
  * cycles don't flip back. Source identity = (mediaElement, presentation
@@ -64,7 +64,7 @@ function trackLoadTriggersSetup({ state, context }) {
64
64
  monitoring: { effects: () => {
65
65
  const el = context.mediaElement.get();
66
66
  const setLoadActivated = () => state.loadActivated.set(true);
67
- if (!el.paused || el.seeking) {
67
+ if (el.autoplay || !el.paused || el.seeking) {
68
68
  setLoadActivated();
69
69
  return;
70
70
  }
@@ -1 +1 @@
1
- {"version":3,"file":"track-load-triggers.js","names":[],"sources":["../../../../../src/playback/behaviors/dom/track-load-triggers.ts"],"sourcesContent":["import { listen } from '@videojs/utils/dom';\nimport { defineBehavior } from '../../../core/composition/create-composition';\nimport type { Reactor } from '../../../core/reactors/create-machine-reactor';\nimport { createMachineReactor } from '../../../core/reactors/create-machine-reactor';\nimport { computed, type ReadonlySignal, type Signal } from '../../../core/signals/primitives';\nimport type { MaybeResolvedPresentation } from '../../../media/types';\n\n/**\n * State shape for load-trigger tracking.\n */\nexport interface LoadTriggersState {\n /**\n * Sticky-true per source. Flips to `true` the first time an event occurs\n * that should switch loading behavior from preload-based to standard load\n * (currently DOM `play` / `seeking`). Reset to `false` on source change.\n *\n * Read by `resolvePresentation` and `loadVideoSegments` / `loadAudioSegments`\n * to mirror native `HTMLMediaElement` preload semantics — only meaningful\n * when `preload !== 'auto'`, which already loads fully regardless.\n */\n loadActivated?: boolean;\n /** Current presentation — URL is used to detect source changes. */\n presentation?: MaybeResolvedPresentation;\n}\n\n/**\n * Context shape for load-trigger tracking.\n */\nexport interface LoadTriggersContext {\n mediaElement?: HTMLMediaElement | undefined;\n}\n\ntype LoadTriggersFsmState = 'preconditions-unmet' | 'monitoring' | 'load-active';\n\n/**\n * Slot-driven FSM. The slot `loadActivated` is the canonical externally-\n * observable state; `deriveState` reads it (and the preconditions) to\n * derive the local FSM state. The slot is *both* the state and the data —\n * no separate internal bookkeeping.\n *\n * ```\n * 'preconditions-unmet' ⟷ 'monitoring' ⟷ 'load-active'\n *\n * preconditions-unmet → monitoring element + URL appear\n * monitoring → load-active slot flips true (listener fires\n * or external write)\n * load-active → monitoring within-state cleanup resets slot\n * (URL or element identity change)\n * any → preconditions-unmet element or URL → undefined\n *\n * any state → destroying → destroyed on destroy()\n * ```\n */\nfunction deriveState(\n presentation: MaybeResolvedPresentation | undefined,\n mediaElement: HTMLMediaElement | undefined,\n loadActivated: boolean | undefined\n): LoadTriggersFsmState {\n if (!mediaElement || !presentation?.url) return 'preconditions-unmet';\n if (loadActivated) return 'load-active';\n return 'monitoring';\n}\n\n/**\n * Track preload-overriding events per source.\n *\n * Writes `state.loadActivated = true` the first time a `play` or `seeking`\n * event fires on the attached media element for the current source — or\n * immediately on entry if the element is already in such a state\n * (`!el.paused` or `el.seeking`), mirroring autoplay / native-controls /\n * direct-DOM-`play()` scenarios.\n *\n * Sticky-true *within a source identity*: subsequent play/pause/seek\n * cycles don't flip back. Source identity = (mediaElement, presentation\n * URL). Either changing — including direct in-place swap with no\n * `undefined` intermediate — resets the slot to `false`.\n *\n * Multi-writer with `hls/adapter.ts:play()` (which writes `true` directly\n * on programmatic play) is intentional — different domains. The adapter\n * records programmatic intent; this behavior is the DOM-side observer.\n * Pre-existing `true` writes are honored because `deriveState` reads the\n * slot — a `true` value routes directly to `'load-active'` without\n * entering `'monitoring'`.\n *\n * @example\n * const reactor = trackLoadTriggers.setup({ state, context });\n * // later:\n * reactor.destroy();\n */\nfunction trackLoadTriggersSetup({\n state,\n context,\n}: {\n state: {\n loadActivated: Signal<LoadTriggersState['loadActivated']>;\n presentation: ReadonlySignal<LoadTriggersState['presentation']>;\n };\n context: { mediaElement: ReadonlySignal<LoadTriggersContext['mediaElement']> };\n}): Reactor<LoadTriggersFsmState | 'destroying' | 'destroyed'> {\n const derivedStateSignal = computed(() =>\n deriveState(state.presentation.get(), context.mediaElement.get(), state.loadActivated.get())\n );\n // Sparse-firing source-URL signal. Emits whenever `presentation?.url`\n // resolves to a different value — covering both URL string changes\n // (url_a → url_b) and transitions where the URL becomes defined or\n // undefined (presentation set, cleared, or replaced without a URL\n // field). Does NOT emit when `state.presentation` is rewritten with\n // the same URL: manifest parsing, duration calc, track resolution,\n // and other enrichment paths re-write the presentation object\n // frequently — tracking raw `state.presentation` here would spuriously\n // reset the 'load-active' slot.\n const urlSignal = computed(() => state.presentation.get()?.url);\n\n return createMachineReactor<LoadTriggersFsmState>({\n initial: 'preconditions-unmet',\n monitor: () => derivedStateSignal.get(),\n states: {\n 'preconditions-unmet': {},\n\n monitoring: {\n // effects (not entry) so element-identity change within this state\n // re-fires and re-attaches listeners to the new element. The monitor\n // guarantees mediaElement is truthy while we're in this state.\n effects: () => {\n const el = context.mediaElement.get()!;\n\n const setLoadActivated = () => state.loadActivated.set(true);\n if (!el.paused || el.seeking) {\n setLoadActivated();\n return;\n }\n const cleanupPlay = listen(el, 'play', setLoadActivated);\n const cleanupSeeking = listen(el, 'seeking', setLoadActivated);\n return () => {\n cleanupPlay();\n cleanupSeeking();\n };\n },\n },\n\n 'load-active': {\n // effects-based cleanup (not entry) because the cleanup must fire on\n // BOTH state exit AND within-state identity change (url_a → url_b,\n // element_a → element_b — both truthy, no state transition).\n // Re-firing tracked reads → cleanup writes loadActivated=false →\n // deriveState (which reads loadActivated) returns 'monitoring' →\n // state transitions → fresh listeners attached for the new source.\n effects: () => {\n context.mediaElement.get();\n urlSignal.get();\n return () => state.loadActivated.set(false);\n },\n },\n },\n });\n}\n\nexport const trackLoadTriggers = defineBehavior({\n stateKeys: ['loadActivated', 'presentation'],\n contextKeys: ['mediaElement'],\n setup: trackLoadTriggersSetup,\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAqDA,SAAS,YACP,cACA,cACA,eACsB;AACtB,KAAI,CAAC,gBAAgB,CAAC,cAAc,IAAK,QAAO;AAChD,KAAI,cAAe,QAAO;AAC1B,QAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BT,SAAS,uBAAuB,EAC9B,OACA,WAO6D;CAC7D,MAAM,qBAAqB,eACzB,YAAY,MAAM,aAAa,KAAK,EAAE,QAAQ,aAAa,KAAK,EAAE,MAAM,cAAc,KAAK,CAAC,CAC7F;CAUD,MAAM,YAAY,eAAe,MAAM,aAAa,KAAK,EAAE,IAAI;AAE/D,QAAO,qBAA2C;EAChD,SAAS;EACT,eAAe,mBAAmB,KAAK;EACvC,QAAQ;GACN,uBAAuB,EAAE;GAEzB,YAAY,EAIV,eAAe;IACb,MAAM,KAAK,QAAQ,aAAa,KAAK;IAErC,MAAM,yBAAyB,MAAM,cAAc,IAAI,KAAK;AAC5D,QAAI,CAAC,GAAG,UAAU,GAAG,SAAS;AAC5B,uBAAkB;AAClB;;IAEF,MAAM,cAAc,OAAO,IAAI,QAAQ,iBAAiB;IACxD,MAAM,iBAAiB,OAAO,IAAI,WAAW,iBAAiB;AAC9D,iBAAa;AACX,kBAAa;AACb,qBAAgB;;MAGrB;GAED,eAAe,EAOb,eAAe;AACb,YAAQ,aAAa,KAAK;AAC1B,cAAU,KAAK;AACf,iBAAa,MAAM,cAAc,IAAI,MAAM;MAE9C;GACF;EACF,CAAC;;AAGJ,MAAa,oBAAoB,eAAe;CAC9C,WAAW,CAAC,iBAAiB,eAAe;CAC5C,aAAa,CAAC,eAAe;CAC7B,OAAO;CACR,CAAC"}
1
+ {"version":3,"file":"track-load-triggers.js","names":[],"sources":["../../../../../src/playback/behaviors/dom/track-load-triggers.ts"],"sourcesContent":["import { listen } from '@videojs/utils/dom';\nimport { defineBehavior } from '../../../core/composition/create-composition';\nimport type { Reactor } from '../../../core/reactors/create-machine-reactor';\nimport { createMachineReactor } from '../../../core/reactors/create-machine-reactor';\nimport { computed, type ReadonlySignal, type Signal } from '../../../core/signals/primitives';\nimport type { MaybeResolvedPresentation } from '../../../media/types';\n\n/**\n * State shape for load-trigger tracking.\n */\nexport interface LoadTriggersState {\n /**\n * Sticky-true per source. Flips to `true` the first time an event occurs\n * that should switch loading behavior from preload-based to standard load\n * (currently DOM `play` / `seeking`). Reset to `false` on source change.\n *\n * Read by `resolvePresentation` and `loadVideoSegments` / `loadAudioSegments`\n * to mirror native `HTMLMediaElement` preload semantics — only meaningful\n * when `preload !== 'auto'`, which already loads fully regardless.\n */\n loadActivated?: boolean;\n /** Current presentation — URL is used to detect source changes. */\n presentation?: MaybeResolvedPresentation;\n}\n\n/**\n * Context shape for load-trigger tracking.\n */\nexport interface LoadTriggersContext {\n mediaElement?: HTMLMediaElement | undefined;\n}\n\ntype LoadTriggersFsmState = 'preconditions-unmet' | 'monitoring' | 'load-active';\n\n/**\n * Slot-driven FSM. The slot `loadActivated` is the canonical externally-\n * observable state; `deriveState` reads it (and the preconditions) to\n * derive the local FSM state. The slot is *both* the state and the data —\n * no separate internal bookkeeping.\n *\n * ```\n * 'preconditions-unmet' ⟷ 'monitoring' ⟷ 'load-active'\n *\n * preconditions-unmet → monitoring element + URL appear\n * monitoring → load-active slot flips true (listener fires\n * or external write)\n * load-active → monitoring within-state cleanup resets slot\n * (URL or element identity change)\n * any → preconditions-unmet element or URL → undefined\n *\n * any state → destroying → destroyed on destroy()\n * ```\n */\nfunction deriveState(\n presentation: MaybeResolvedPresentation | undefined,\n mediaElement: HTMLMediaElement | undefined,\n loadActivated: boolean | undefined\n): LoadTriggersFsmState {\n if (!mediaElement || !presentation?.url) return 'preconditions-unmet';\n if (loadActivated) return 'load-active';\n return 'monitoring';\n}\n\n/**\n * Track preload-overriding events per source.\n *\n * Writes `state.loadActivated = true` the first time a `play` or `seeking`\n * event fires on the attached media element for the current source — or\n * immediately on entry if the element is already committed to loading\n * (`el.autoplay`, `!el.paused`, or `el.seeking`), covering autoplay,\n * native-controls, and direct-DOM-`play()` scenarios.\n *\n * Sticky-true *within a source identity*: subsequent play/pause/seek\n * cycles don't flip back. Source identity = (mediaElement, presentation\n * URL). Either changing — including direct in-place swap with no\n * `undefined` intermediate — resets the slot to `false`.\n *\n * Multi-writer with `hls/adapter.ts:play()` (which writes `true` directly\n * on programmatic play) is intentional — different domains. The adapter\n * records programmatic intent; this behavior is the DOM-side observer.\n * Pre-existing `true` writes are honored because `deriveState` reads the\n * slot — a `true` value routes directly to `'load-active'` without\n * entering `'monitoring'`.\n *\n * @example\n * const reactor = trackLoadTriggers.setup({ state, context });\n * // later:\n * reactor.destroy();\n */\nfunction trackLoadTriggersSetup({\n state,\n context,\n}: {\n state: {\n loadActivated: Signal<LoadTriggersState['loadActivated']>;\n presentation: ReadonlySignal<LoadTriggersState['presentation']>;\n };\n context: { mediaElement: ReadonlySignal<LoadTriggersContext['mediaElement']> };\n}): Reactor<LoadTriggersFsmState | 'destroying' | 'destroyed'> {\n const derivedStateSignal = computed(() =>\n deriveState(state.presentation.get(), context.mediaElement.get(), state.loadActivated.get())\n );\n // Sparse-firing source-URL signal. Emits whenever `presentation?.url`\n // resolves to a different value — covering both URL string changes\n // (url_a → url_b) and transitions where the URL becomes defined or\n // undefined (presentation set, cleared, or replaced without a URL\n // field). Does NOT emit when `state.presentation` is rewritten with\n // the same URL: manifest parsing, duration calc, track resolution,\n // and other enrichment paths re-write the presentation object\n // frequently — tracking raw `state.presentation` here would spuriously\n // reset the 'load-active' slot.\n const urlSignal = computed(() => state.presentation.get()?.url);\n\n return createMachineReactor<LoadTriggersFsmState>({\n initial: 'preconditions-unmet',\n monitor: () => derivedStateSignal.get(),\n states: {\n 'preconditions-unmet': {},\n\n monitoring: {\n // effects (not entry) so element-identity change within this state\n // re-fires and re-attaches listeners to the new element. The monitor\n // guarantees mediaElement is truthy while we're in this state.\n effects: () => {\n const el = context.mediaElement.get()!;\n\n const setLoadActivated = () => state.loadActivated.set(true);\n if (el.autoplay || !el.paused || el.seeking) {\n setLoadActivated();\n return;\n }\n const cleanupPlay = listen(el, 'play', setLoadActivated);\n const cleanupSeeking = listen(el, 'seeking', setLoadActivated);\n return () => {\n cleanupPlay();\n cleanupSeeking();\n };\n },\n },\n\n 'load-active': {\n // effects-based cleanup (not entry) because the cleanup must fire on\n // BOTH state exit AND within-state identity change (url_a → url_b,\n // element_a → element_b — both truthy, no state transition).\n // Re-firing tracked reads → cleanup writes loadActivated=false →\n // deriveState (which reads loadActivated) returns 'monitoring' →\n // state transitions → fresh listeners attached for the new source.\n effects: () => {\n context.mediaElement.get();\n urlSignal.get();\n return () => state.loadActivated.set(false);\n },\n },\n },\n });\n}\n\nexport const trackLoadTriggers = defineBehavior({\n stateKeys: ['loadActivated', 'presentation'],\n contextKeys: ['mediaElement'],\n setup: trackLoadTriggersSetup,\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAqDA,SAAS,YACP,cACA,cACA,eACsB;CACtB,IAAI,CAAC,gBAAgB,CAAC,cAAc,KAAK,OAAO;CAChD,IAAI,eAAe,OAAO;CAC1B,OAAO;AACT;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BA,SAAS,uBAAuB,EAC9B,OACA,WAO6D;CAC7D,MAAM,qBAAqB,eACzB,YAAY,MAAM,aAAa,IAAI,GAAG,QAAQ,aAAa,IAAI,GAAG,MAAM,cAAc,IAAI,CAAC,CAC7F;CAUA,MAAM,YAAY,eAAe,MAAM,aAAa,IAAI,CAAC,EAAE,GAAG;CAE9D,OAAO,qBAA2C;EAChD,SAAS;EACT,eAAe,mBAAmB,IAAI;EACtC,QAAQ;GACN,uBAAuB,CAAC;GAExB,YAAY,EAIV,eAAe;IACb,MAAM,KAAK,QAAQ,aAAa,IAAI;IAEpC,MAAM,yBAAyB,MAAM,cAAc,IAAI,IAAI;IAC3D,IAAI,GAAG,YAAY,CAAC,GAAG,UAAU,GAAG,SAAS;KAC3C,iBAAiB;KACjB;IACF;IACA,MAAM,cAAc,OAAO,IAAI,QAAQ,gBAAgB;IACvD,MAAM,iBAAiB,OAAO,IAAI,WAAW,gBAAgB;IAC7D,aAAa;KACX,YAAY;KACZ,eAAe;IACjB;GACF,EACF;GAEA,eAAe,EAOb,eAAe;IACb,QAAQ,aAAa,IAAI;IACzB,UAAU,IAAI;IACd,aAAa,MAAM,cAAc,IAAI,KAAK;GAC5C,EACF;EACF;CACF,CAAC;AACH;AAEA,MAAa,oBAAoB,eAAe;CAC9C,WAAW,CAAC,iBAAiB,cAAc;CAC3C,aAAa,CAAC,cAAc;CAC5B,OAAO;AACT,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"track-playback-rate.js","names":[],"sources":["../../../../../src/playback/behaviors/dom/track-playback-rate.ts"],"sourcesContent":["/**\n * Mirror `mediaElement.playbackRate` into reactive state. On each `ratechange`\n * event, write the new value to `state.playbackRate`. Also syncs immediately\n * when a media element becomes available so consumers don't wait for the first\n * event.\n *\n * When no media element is attached, writes `config.defaultPlaybackRate`\n * (default-default `1`, matching the HTMLMediaElement spec) so consumers\n * always see the rate a freshly attached element would have. Read-only mirror;\n * does not push `state.playbackRate` back to the element.\n */\nimport { listen } from '@videojs/utils/dom';\nimport { defineBehavior } from '../../../core/composition/create-composition';\nimport { effect } from '../../../core/signals/effect';\nimport type { ReadonlySignal, Signal } from '../../../core/signals/primitives';\n\nexport interface PlaybackRateState {\n playbackRate?: number;\n}\n\nexport interface PlaybackRateContext {\n mediaElement?: HTMLMediaElement | undefined;\n}\n\nexport interface TrackPlaybackRateConfig {\n /**\n * Value written to `state.playbackRate` when no media element is attached.\n * Defaults to `1` — the HTMLMediaElement spec default.\n */\n defaultPlaybackRate?: number;\n}\n\nfunction trackPlaybackRateSetup({\n state,\n context,\n config,\n}: {\n state: { playbackRate: Signal<PlaybackRateState['playbackRate']> };\n context: { mediaElement: ReadonlySignal<PlaybackRateContext['mediaElement']> };\n config?: TrackPlaybackRateConfig;\n}): () => void {\n const defaultPlaybackRate = config?.defaultPlaybackRate ?? 1;\n\n return effect(() => {\n const mediaElement = context.mediaElement.get();\n if (!mediaElement) {\n state.playbackRate.set(defaultPlaybackRate);\n return;\n }\n\n const sync = () => state.playbackRate.set(mediaElement.playbackRate);\n sync();\n return listen(mediaElement, 'ratechange', sync);\n });\n}\n\nexport const trackPlaybackRate = defineBehavior({\n stateKeys: ['playbackRate'],\n contextKeys: ['mediaElement'],\n setup: trackPlaybackRateSetup,\n});\n"],"mappings":";;;;;;;;;;;;;;;AAgCA,SAAS,uBAAuB,EAC9B,OACA,SACA,UAKa;CACb,MAAM,sBAAsB,QAAQ,uBAAuB;AAE3D,QAAO,aAAa;EAClB,MAAM,eAAe,QAAQ,aAAa,KAAK;AAC/C,MAAI,CAAC,cAAc;AACjB,SAAM,aAAa,IAAI,oBAAoB;AAC3C;;EAGF,MAAM,aAAa,MAAM,aAAa,IAAI,aAAa,aAAa;AACpE,QAAM;AACN,SAAO,OAAO,cAAc,cAAc,KAAK;GAC/C;;AAGJ,MAAa,oBAAoB,eAAe;CAC9C,WAAW,CAAC,eAAe;CAC3B,aAAa,CAAC,eAAe;CAC7B,OAAO;CACR,CAAC"}
1
+ {"version":3,"file":"track-playback-rate.js","names":[],"sources":["../../../../../src/playback/behaviors/dom/track-playback-rate.ts"],"sourcesContent":["/**\n * Mirror `mediaElement.playbackRate` into reactive state. On each `ratechange`\n * event, write the new value to `state.playbackRate`. Also syncs immediately\n * when a media element becomes available so consumers don't wait for the first\n * event.\n *\n * When no media element is attached, writes `config.defaultPlaybackRate`\n * (default-default `1`, matching the HTMLMediaElement spec) so consumers\n * always see the rate a freshly attached element would have. Read-only mirror;\n * does not push `state.playbackRate` back to the element.\n */\nimport { listen } from '@videojs/utils/dom';\nimport { defineBehavior } from '../../../core/composition/create-composition';\nimport { effect } from '../../../core/signals/effect';\nimport type { ReadonlySignal, Signal } from '../../../core/signals/primitives';\n\nexport interface PlaybackRateState {\n playbackRate?: number;\n}\n\nexport interface PlaybackRateContext {\n mediaElement?: HTMLMediaElement | undefined;\n}\n\nexport interface TrackPlaybackRateConfig {\n /**\n * Value written to `state.playbackRate` when no media element is attached.\n * Defaults to `1` — the HTMLMediaElement spec default.\n */\n defaultPlaybackRate?: number;\n}\n\nfunction trackPlaybackRateSetup({\n state,\n context,\n config,\n}: {\n state: { playbackRate: Signal<PlaybackRateState['playbackRate']> };\n context: { mediaElement: ReadonlySignal<PlaybackRateContext['mediaElement']> };\n config?: TrackPlaybackRateConfig;\n}): () => void {\n const defaultPlaybackRate = config?.defaultPlaybackRate ?? 1;\n\n return effect(() => {\n const mediaElement = context.mediaElement.get();\n if (!mediaElement) {\n state.playbackRate.set(defaultPlaybackRate);\n return;\n }\n\n const sync = () => state.playbackRate.set(mediaElement.playbackRate);\n sync();\n return listen(mediaElement, 'ratechange', sync);\n });\n}\n\nexport const trackPlaybackRate = defineBehavior({\n stateKeys: ['playbackRate'],\n contextKeys: ['mediaElement'],\n setup: trackPlaybackRateSetup,\n});\n"],"mappings":";;;;;;;;;;;;;;;AAgCA,SAAS,uBAAuB,EAC9B,OACA,SACA,UAKa;CACb,MAAM,sBAAsB,QAAQ,uBAAuB;CAE3D,OAAO,aAAa;EAClB,MAAM,eAAe,QAAQ,aAAa,IAAI;EAC9C,IAAI,CAAC,cAAc;GACjB,MAAM,aAAa,IAAI,mBAAmB;GAC1C;EACF;EAEA,MAAM,aAAa,MAAM,aAAa,IAAI,aAAa,YAAY;EACnE,KAAK;EACL,OAAO,OAAO,cAAc,cAAc,IAAI;CAChD,CAAC;AACH;AAEA,MAAa,oBAAoB,eAAe;CAC9C,WAAW,CAAC,cAAc;CAC1B,aAAa,CAAC,cAAc;CAC5B,OAAO;AACT,CAAC"}
@@ -1,8 +1,8 @@
1
1
  import { computed } from "../../../core/signals/primitives.js";
2
2
  import { defineBehavior } from "../../../core/composition/create-composition.js";
3
3
  import { createMachineReactor } from "../../../core/reactors/create-machine-reactor.js";
4
- import { getMaxBufferedEnd, shouldUpdateDuration, waitForSourceBuffersReady } from "../../../media/dom/mse/duration.js";
5
4
  import { waitForMediaSourceOpen } from "../../../media/dom/mse/mediasource-setup.js";
5
+ import { getMaxBufferedEnd, shouldUpdateDuration, waitForSourceBuffersReady } from "../../../media/dom/mse/duration.js";
6
6
  //#region src/playback/behaviors/dom/update-mediasource-duration.ts
7
7
  /**
8
8
  * **Propagate `presentation.duration` to `mediaSource.duration` — exactly
@@ -71,7 +71,8 @@ function updateMediaSourceDurationSetup({ state, context }) {
71
71
  if (controller.signal.aborted) return;
72
72
  if (mediaSource.readyState !== "open") return;
73
73
  const maxBufferedEnd = getMaxBufferedEnd(mediaSource.sourceBuffers);
74
- mediaSource.duration = maxBufferedEnd > presentation.duration ? maxBufferedEnd : presentation.duration;
74
+ const duration = maxBufferedEnd > presentation.duration ? maxBufferedEnd : presentation.duration;
75
+ mediaSource.duration = duration;
75
76
  };
76
77
  writeWhenReady();
77
78
  return () => controller.abort();
@@ -1 +1 @@
1
- {"version":3,"file":"update-mediasource-duration.js","names":[],"sources":["../../../../../src/playback/behaviors/dom/update-mediasource-duration.ts"],"sourcesContent":["/**\n * **Propagate `presentation.duration` to `mediaSource.duration` — exactly\n * once per MediaSource.**\n *\n * When the presentation has a valid positive duration (including `Infinity`\n * for live) and a MediaSource is in context, writes the value through to\n * `mediaSource.duration` on initial setup — once, while `mediaSource.duration`\n * is still `NaN`. Once any non-NaN value is present (set by us, or by\n * `endOfStream` from the buffered end), the behavior leaves the property\n * alone; re-syncing a drift against `presentation.duration` would race with\n * concurrent `appendBuffer()` calls.\n *\n * The entry resolves three async preconditions in order before writing:\n *\n * 1. **MediaSource open** — `waitForMediaSourceOpen` defers until the first\n * `sourceopen` event (or any readyState transition, since `'ended'` /\n * `'closed'` mean we've missed the window and should bail).\n * 2. **All SourceBuffers idle** — `waitForSourceBuffersReady` defers per\n * the MSE-spec rule that `duration` cannot be set while any buffer has\n * `updating === true`.\n * 3. **Buffered-range clamp** — `getMaxBufferedEnd` ensures the written\n * duration is at least the highest buffered end (MSE spec disallows\n * a smaller `duration` than any buffered range).\n *\n * The buffer-set helpers operate across `mediaSource.sourceBuffers` (the\n * canonical aggregate), so the behavior composes uniformly across\n * audio-only, video-only, and mixed configurations.\n *\n * Single-positive-state reactor (`'preconditions-unmet'` ↔ `'duration-writable'`):\n * state derivation is purely signal-driven (presentation validity +\n * mediaSource existence). MediaSource lifecycle state (`readyState`) and\n * the `duration` itself are non-signal DOM properties resolved inside the\n * entry's async sequence. The state-exit cleanup aborts the in-flight\n * wait, so source resets and behavior destroy structurally cancel the\n * pending write. A post-await re-check of `mediaSource.readyState ===\n * 'open'` covers the narrow race where `endOfStream()` synchronously\n * transitions readyState to `'ended'` between our `waitForMediaSourceOpen`\n * resolution and the `mediaSource.duration` write.\n *\n * Downstream of `calculatePresentationDuration` (which writes\n * `presentation.duration`); concurrent with `endOfStream` (which may later\n * write `mediaSource.duration` — the \"exactly once\" contract keeps us out\n * of that path).\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 } from '../../../core/signals/primitives';\nimport { getMaxBufferedEnd, shouldUpdateDuration, waitForSourceBuffersReady } from '../../../media/dom/mse/duration';\nimport { waitForMediaSourceOpen } from '../../../media/dom/mse/mediasource-setup';\nimport type { MaybeResolvedPresentation } from '../../../media/types';\n\ntype DurationFsmState = 'preconditions-unmet' | 'duration-writable';\n\nfunction deriveState(\n presentation: MaybeResolvedPresentation | undefined,\n mediaSource: MediaSource | undefined\n): DurationFsmState {\n return shouldUpdateDuration(presentation, mediaSource) ? 'duration-writable' : 'preconditions-unmet';\n}\n\nfunction updateMediaSourceDurationSetup({\n state,\n context,\n}: {\n state: { presentation: ReadonlySignal<MaybeResolvedPresentation | undefined> };\n context: { mediaSource: ReadonlySignal<MediaSource | undefined> };\n}): Reactor<DurationFsmState | 'destroying' | 'destroyed'> {\n // Purely signal-driven derivation: only `presentation` validity and\n // `mediaSource` existence. MediaSource lifecycle (`readyState`),\n // its attached `sourceBuffers`, and `mediaSource.duration` itself are\n // non-signal DOM properties consumed inside the entry — none of them\n // sneak into `computed(...)`.\n const derivedStateSignal = computed(() => deriveState(state.presentation.get(), context.mediaSource.get()));\n\n return createMachineReactor<DurationFsmState>({\n initial: 'preconditions-unmet',\n monitor: () => derivedStateSignal.get(),\n states: {\n 'preconditions-unmet': {},\n\n 'duration-writable': {\n // entry body is auto-untracked. The state machine handles source\n // resets via deriveState; this entry resolves the MediaSource\n // lifecycle preconditions in sequence and binds its cleanup\n // (abort) to state exit + destroy.\n entry: () => {\n const presentation = state.presentation.get()!;\n const mediaSource = context.mediaSource.get()!;\n\n // Idempotency: someone (us on a prior entry, or concurrent\n // `endOfStream`) has already written. No-op and leave the state\n // alone — the next source-reset transition will produce a fresh\n // entry against the new MediaSource (whose `duration` starts at\n // `NaN` again).\n if (!Number.isNaN(mediaSource.duration)) return;\n\n const controller = new AbortController();\n\n const writeWhenReady = async () => {\n await waitForMediaSourceOpen(mediaSource, controller.signal);\n if (controller.signal.aborted) return;\n // `waitForMediaSourceOpen` resolves on any readyState transition\n // out of 'closed'; if we landed in 'ended' / 'closed' instead of\n // 'open', the write window is gone.\n if (mediaSource.readyState !== 'open') return;\n\n await waitForSourceBuffersReady(mediaSource.sourceBuffers, controller.signal);\n if (controller.signal.aborted) return;\n\n // Narrow race: `endOfStream()` synchronously transitions\n // readyState to 'ended' AND writes `mediaSource.duration`\n // between our `waitForMediaSourceOpen` resolution and here.\n // Re-read the DOM property to catch this window.\n if (mediaSource.readyState !== 'open') return;\n\n // MSE spec: duration cannot be less than any buffered range.\n // Re-read sourceBuffers after the await so any buffer added\n // during the wait is included in the clamp.\n const maxBufferedEnd = getMaxBufferedEnd(mediaSource.sourceBuffers);\n const duration = maxBufferedEnd > presentation.duration! ? maxBufferedEnd : presentation.duration!;\n mediaSource.duration = duration;\n };\n\n writeWhenReady();\n\n return () => controller.abort();\n },\n },\n },\n });\n}\n\nexport const updateMediaSourceDuration = defineBehavior({\n stateKeys: ['presentation'],\n contextKeys: ['mediaSource'],\n setup: updateMediaSourceDurationSetup,\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsDA,SAAS,YACP,cACA,aACkB;AAClB,QAAO,qBAAqB,cAAc,YAAY,GAAG,sBAAsB;;AAGjF,SAAS,+BAA+B,EACtC,OACA,WAIyD;CAMzD,MAAM,qBAAqB,eAAe,YAAY,MAAM,aAAa,KAAK,EAAE,QAAQ,YAAY,KAAK,CAAC,CAAC;AAE3G,QAAO,qBAAuC;EAC5C,SAAS;EACT,eAAe,mBAAmB,KAAK;EACvC,QAAQ;GACN,uBAAuB,EAAE;GAEzB,qBAAqB,EAKnB,aAAa;IACX,MAAM,eAAe,MAAM,aAAa,KAAK;IAC7C,MAAM,cAAc,QAAQ,YAAY,KAAK;AAO7C,QAAI,CAAC,OAAO,MAAM,YAAY,SAAS,CAAE;IAEzC,MAAM,aAAa,IAAI,iBAAiB;IAExC,MAAM,iBAAiB,YAAY;AACjC,WAAM,uBAAuB,aAAa,WAAW,OAAO;AAC5D,SAAI,WAAW,OAAO,QAAS;AAI/B,SAAI,YAAY,eAAe,OAAQ;AAEvC,WAAM,0BAA0B,YAAY,eAAe,WAAW,OAAO;AAC7E,SAAI,WAAW,OAAO,QAAS;AAM/B,SAAI,YAAY,eAAe,OAAQ;KAKvC,MAAM,iBAAiB,kBAAkB,YAAY,cAAc;AAEnE,iBAAY,WADK,iBAAiB,aAAa,WAAY,iBAAiB,aAAa;;AAI3F,oBAAgB;AAEhB,iBAAa,WAAW,OAAO;MAElC;GACF;EACF,CAAC;;AAGJ,MAAa,4BAA4B,eAAe;CACtD,WAAW,CAAC,eAAe;CAC3B,aAAa,CAAC,cAAc;CAC5B,OAAO;CACR,CAAC"}
1
+ {"version":3,"file":"update-mediasource-duration.js","names":[],"sources":["../../../../../src/playback/behaviors/dom/update-mediasource-duration.ts"],"sourcesContent":["/**\n * **Propagate `presentation.duration` to `mediaSource.duration` — exactly\n * once per MediaSource.**\n *\n * When the presentation has a valid positive duration (including `Infinity`\n * for live) and a MediaSource is in context, writes the value through to\n * `mediaSource.duration` on initial setup — once, while `mediaSource.duration`\n * is still `NaN`. Once any non-NaN value is present (set by us, or by\n * `endOfStream` from the buffered end), the behavior leaves the property\n * alone; re-syncing a drift against `presentation.duration` would race with\n * concurrent `appendBuffer()` calls.\n *\n * The entry resolves three async preconditions in order before writing:\n *\n * 1. **MediaSource open** — `waitForMediaSourceOpen` defers until the first\n * `sourceopen` event (or any readyState transition, since `'ended'` /\n * `'closed'` mean we've missed the window and should bail).\n * 2. **All SourceBuffers idle** — `waitForSourceBuffersReady` defers per\n * the MSE-spec rule that `duration` cannot be set while any buffer has\n * `updating === true`.\n * 3. **Buffered-range clamp** — `getMaxBufferedEnd` ensures the written\n * duration is at least the highest buffered end (MSE spec disallows\n * a smaller `duration` than any buffered range).\n *\n * The buffer-set helpers operate across `mediaSource.sourceBuffers` (the\n * canonical aggregate), so the behavior composes uniformly across\n * audio-only, video-only, and mixed configurations.\n *\n * Single-positive-state reactor (`'preconditions-unmet'` ↔ `'duration-writable'`):\n * state derivation is purely signal-driven (presentation validity +\n * mediaSource existence). MediaSource lifecycle state (`readyState`) and\n * the `duration` itself are non-signal DOM properties resolved inside the\n * entry's async sequence. The state-exit cleanup aborts the in-flight\n * wait, so source resets and behavior destroy structurally cancel the\n * pending write. A post-await re-check of `mediaSource.readyState ===\n * 'open'` covers the narrow race where `endOfStream()` synchronously\n * transitions readyState to `'ended'` between our `waitForMediaSourceOpen`\n * resolution and the `mediaSource.duration` write.\n *\n * Downstream of `calculatePresentationDuration` (which writes\n * `presentation.duration`); concurrent with `endOfStream` (which may later\n * write `mediaSource.duration` — the \"exactly once\" contract keeps us out\n * of that path).\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 } from '../../../core/signals/primitives';\nimport { getMaxBufferedEnd, shouldUpdateDuration, waitForSourceBuffersReady } from '../../../media/dom/mse/duration';\nimport { waitForMediaSourceOpen } from '../../../media/dom/mse/mediasource-setup';\nimport type { MaybeResolvedPresentation } from '../../../media/types';\n\ntype DurationFsmState = 'preconditions-unmet' | 'duration-writable';\n\nfunction deriveState(\n presentation: MaybeResolvedPresentation | undefined,\n mediaSource: MediaSource | undefined\n): DurationFsmState {\n return shouldUpdateDuration(presentation, mediaSource) ? 'duration-writable' : 'preconditions-unmet';\n}\n\nfunction updateMediaSourceDurationSetup({\n state,\n context,\n}: {\n state: { presentation: ReadonlySignal<MaybeResolvedPresentation | undefined> };\n context: { mediaSource: ReadonlySignal<MediaSource | undefined> };\n}): Reactor<DurationFsmState | 'destroying' | 'destroyed'> {\n // Purely signal-driven derivation: only `presentation` validity and\n // `mediaSource` existence. MediaSource lifecycle (`readyState`),\n // its attached `sourceBuffers`, and `mediaSource.duration` itself are\n // non-signal DOM properties consumed inside the entry — none of them\n // sneak into `computed(...)`.\n const derivedStateSignal = computed(() => deriveState(state.presentation.get(), context.mediaSource.get()));\n\n return createMachineReactor<DurationFsmState>({\n initial: 'preconditions-unmet',\n monitor: () => derivedStateSignal.get(),\n states: {\n 'preconditions-unmet': {},\n\n 'duration-writable': {\n // entry body is auto-untracked. The state machine handles source\n // resets via deriveState; this entry resolves the MediaSource\n // lifecycle preconditions in sequence and binds its cleanup\n // (abort) to state exit + destroy.\n entry: () => {\n const presentation = state.presentation.get()!;\n const mediaSource = context.mediaSource.get()!;\n\n // Idempotency: someone (us on a prior entry, or concurrent\n // `endOfStream`) has already written. No-op and leave the state\n // alone — the next source-reset transition will produce a fresh\n // entry against the new MediaSource (whose `duration` starts at\n // `NaN` again).\n if (!Number.isNaN(mediaSource.duration)) return;\n\n const controller = new AbortController();\n\n const writeWhenReady = async () => {\n await waitForMediaSourceOpen(mediaSource, controller.signal);\n if (controller.signal.aborted) return;\n // `waitForMediaSourceOpen` resolves on any readyState transition\n // out of 'closed'; if we landed in 'ended' / 'closed' instead of\n // 'open', the write window is gone.\n if (mediaSource.readyState !== 'open') return;\n\n await waitForSourceBuffersReady(mediaSource.sourceBuffers, controller.signal);\n if (controller.signal.aborted) return;\n\n // Narrow race: `endOfStream()` synchronously transitions\n // readyState to 'ended' AND writes `mediaSource.duration`\n // between our `waitForMediaSourceOpen` resolution and here.\n // Re-read the DOM property to catch this window.\n if (mediaSource.readyState !== 'open') return;\n\n // MSE spec: duration cannot be less than any buffered range.\n // Re-read sourceBuffers after the await so any buffer added\n // during the wait is included in the clamp.\n const maxBufferedEnd = getMaxBufferedEnd(mediaSource.sourceBuffers);\n const duration = maxBufferedEnd > presentation.duration! ? maxBufferedEnd : presentation.duration!;\n mediaSource.duration = duration;\n };\n\n writeWhenReady();\n\n return () => controller.abort();\n },\n },\n },\n });\n}\n\nexport const updateMediaSourceDuration = defineBehavior({\n stateKeys: ['presentation'],\n contextKeys: ['mediaSource'],\n setup: updateMediaSourceDurationSetup,\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsDA,SAAS,YACP,cACA,aACkB;CAClB,OAAO,qBAAqB,cAAc,WAAW,IAAI,sBAAsB;AACjF;AAEA,SAAS,+BAA+B,EACtC,OACA,WAIyD;CAMzD,MAAM,qBAAqB,eAAe,YAAY,MAAM,aAAa,IAAI,GAAG,QAAQ,YAAY,IAAI,CAAC,CAAC;CAE1G,OAAO,qBAAuC;EAC5C,SAAS;EACT,eAAe,mBAAmB,IAAI;EACtC,QAAQ;GACN,uBAAuB,CAAC;GAExB,qBAAqB,EAKnB,aAAa;IACX,MAAM,eAAe,MAAM,aAAa,IAAI;IAC5C,MAAM,cAAc,QAAQ,YAAY,IAAI;IAO5C,IAAI,CAAC,OAAO,MAAM,YAAY,QAAQ,GAAG;IAEzC,MAAM,aAAa,IAAI,gBAAgB;IAEvC,MAAM,iBAAiB,YAAY;KACjC,MAAM,uBAAuB,aAAa,WAAW,MAAM;KAC3D,IAAI,WAAW,OAAO,SAAS;KAI/B,IAAI,YAAY,eAAe,QAAQ;KAEvC,MAAM,0BAA0B,YAAY,eAAe,WAAW,MAAM;KAC5E,IAAI,WAAW,OAAO,SAAS;KAM/B,IAAI,YAAY,eAAe,QAAQ;KAKvC,MAAM,iBAAiB,kBAAkB,YAAY,aAAa;KAClE,MAAM,WAAW,iBAAiB,aAAa,WAAY,iBAAiB,aAAa;KACzF,YAAY,WAAW;IACzB;IAEA,eAAe;IAEf,aAAa,WAAW,MAAM;GAChC,EACF;EACF;CACF,CAAC;AACH;AAEA,MAAa,4BAA4B,eAAe;CACtD,WAAW,CAAC,cAAc;CAC1B,aAAa,CAAC,aAAa;CAC3B,OAAO;AACT,CAAC"}
@@ -0,0 +1,123 @@
1
+ import { update } from "../../core/signals/primitives.js";
2
+ import { createMachineReactor } from "../../core/reactors/create-machine-reactor.js";
3
+ import { isResolvedPresentation } from "../../media/types/index.js";
4
+ import { findTrackById } from "../../media/utils/tracks.js";
5
+ //#region src/playback/behaviors/establish-start-media-time.ts
6
+ /**
7
+ * A single type's own media-timeline origin:
8
+ * `baseMediaDecodeTime/timescale − segmentStartTime` (the `segmentStartTime` term
9
+ * makes it the stream origin even when the first loaded segment isn't the 0th).
10
+ * `undefined` until timescale + baseMediaDecodeTime + segmentStartTime are all present.
11
+ */
12
+ function ownOrigin(data) {
13
+ const { timescale, baseMediaDecodeTime, segmentStartTime } = data ?? {};
14
+ return timescale != null && baseMediaDecodeTime != null && segmentStartTime != null ? baseMediaDecodeTime / timescale - segmentStartTime : void 0;
15
+ }
16
+ /** Snap a below-threshold (incl. negative) origin to `0` so it isn't relocated. */
17
+ function thresholdOrigin(origin) {
18
+ return origin < 1 ? 0 : origin;
19
+ }
20
+ /**
21
+ * The **default** — relocate the whole presentation by one shared origin: the `min`
22
+ * across the *selected* A/V tracks' own origins, denormalized onto every type. This
23
+ * single reduce subsumes the "per-type" and "shared" tiers:
24
+ * - **aligned A/V** — `min` equals each origin (they're equal), so it matches per-type;
25
+ * - **skewed A/V** (e.g. Apple's 44ms audio-lead) — `min` keeps every track's earliest
26
+ * DTS ≥ 0 (relocating by ≤ each own origin never drives one negative) *and* preserves
27
+ * the real skew (per-type would flatten it, desyncing A/V);
28
+ * - **single type / muxed** — `min` of the one origin is that origin.
29
+ *
30
+ * Returns `undefined` for every type until all *selected* types have a complete origin
31
+ * (the shared-`min` barrier). Which types must contribute is read from `ctx` (the
32
+ * selected v/a ids); with no selection context it coordinates across whatever types
33
+ * have data. A shared origin below {@link NEAR_ZERO_ORIGIN_THRESHOLD} is returned as `0`
34
+ * (native — ordinary ~0-PTS VOD isn't relocated).
35
+ */
36
+ const deriveSharedMinStartMediaTime = (containerData, ctx) => {
37
+ const contributingTypes = [];
38
+ if (ctx.selectedVideoTrackId != null) contributingTypes.push("video");
39
+ if (ctx.selectedAudioTrackId != null) contributingTypes.push("audio");
40
+ const origins = (contributingTypes.length > 0 ? contributingTypes : Object.keys(containerData)).map((type) => ownOrigin(containerData[type]));
41
+ if (origins.length === 0 || origins.some((origin) => origin === void 0)) return {};
42
+ const shared = thresholdOrigin(Math.min(...origins));
43
+ const out = {};
44
+ for (const type of Object.keys(containerData)) out[type] = shared;
45
+ return out;
46
+ };
47
+ /**
48
+ * Coordination-axis *off* — each type relocates by its own origin, independently. Not
49
+ * the default: it flattens real A/V skew (see {@link deriveSharedMinStartMediaTime}).
50
+ * Kept as an opt-in for compositions that know their A/V is aligned and want to skip
51
+ * the shared-`min` barrier (each type stamps as soon as its own origin is discovered).
52
+ */
53
+ const derivePerTypeStartMediaTime = (containerData) => {
54
+ const out = {};
55
+ for (const [type, data] of Object.entries(containerData)) {
56
+ const origin = ownOrigin(data);
57
+ out[type] = origin === void 0 ? void 0 : thresholdOrigin(origin);
58
+ }
59
+ return out;
60
+ };
61
+ /** Stamp the derived per-track `startMediaTime` onto the model (idempotent — same reference when nothing moved). */
62
+ function stampTracks(presentation, startMediaTimes) {
63
+ let changed = false;
64
+ const selectionSets = presentation.selectionSets.map((selectionSet) => ({
65
+ ...selectionSet,
66
+ switchingSets: selectionSet.switchingSets.map((switchingSet) => ({
67
+ ...switchingSet,
68
+ tracks: switchingSet.tracks.map((track) => {
69
+ const startMediaTime = startMediaTimes[track.type];
70
+ if (startMediaTime === void 0 || track.startMediaTime === startMediaTime) return track;
71
+ changed = true;
72
+ return {
73
+ ...track,
74
+ startMediaTime
75
+ };
76
+ })
77
+ }))
78
+ }));
79
+ return changed ? {
80
+ ...presentation,
81
+ selectionSets
82
+ } : presentation;
83
+ }
84
+ function establishStartMediaTimeSetup({ state, config = {} }) {
85
+ const derive = config.deriveStartMediaTime ?? deriveSharedMinStartMediaTime;
86
+ const selectionContext = () => ({
87
+ selectedVideoTrackId: state.selectedVideoTrackId?.get(),
88
+ selectedAudioTrackId: state.selectedAudioTrackId?.get()
89
+ });
90
+ /** Established once the selected A/V tracks (whichever exist) carry `startMediaTime`. */
91
+ const established = () => {
92
+ const presentation = state.presentation.get();
93
+ if (!isResolvedPresentation(presentation)) return false;
94
+ const ids = [state.selectedVideoTrackId?.get(), state.selectedAudioTrackId?.get()].filter((id) => id !== void 0);
95
+ return ids.length > 0 && ids.every((id) => findTrackById(presentation, id)?.startMediaTime !== void 0);
96
+ };
97
+ return createMachineReactor({
98
+ initial: "inactive",
99
+ monitor: () => {
100
+ if (!isResolvedPresentation(state.presentation.get())) return "inactive";
101
+ return established() ? "established" : "monitoring";
102
+ },
103
+ states: {
104
+ inactive: { entry: () => state.mediaContainerData.set(void 0) },
105
+ monitoring: { effects: () => {
106
+ const containerData = state.mediaContainerData.get();
107
+ if (!containerData) return;
108
+ const startMediaTimes = derive(containerData, selectionContext());
109
+ update(state.presentation, (current) => stampTracks(current, startMediaTimes));
110
+ } },
111
+ established: {}
112
+ }
113
+ });
114
+ }
115
+ const establishStartMediaTime = {
116
+ stateKeys: ["presentation", "mediaContainerData"],
117
+ contextKeys: [],
118
+ setup: establishStartMediaTimeSetup
119
+ };
120
+ //#endregion
121
+ export { derivePerTypeStartMediaTime, deriveSharedMinStartMediaTime, establishStartMediaTime };
122
+
123
+ //# sourceMappingURL=establish-start-media-time.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"establish-start-media-time.js","names":[],"sources":["../../../../src/playback/behaviors/establish-start-media-time.ts"],"sourcesContent":["/**\n * Establish each track's `startMediaTime` (its media-timeline origin) once per\n * source, so a non-zero-PTS source relocates onto a 0-based presentation timeline.\n * The VOD sibling of `anchor-presentation-timeline`: both are per-source\n * establishment units that write a coordinate base value onto every track (the\n * anchor writes `startDate`/`startTime` from wall clock; this writes\n * `startMediaTime` from the container's decode-time origin).\n *\n * This is the **DOM-free reactor half**: it owns the transient `mediaContainerData`\n * slot lifecycle (`inactive` clears it per source), runs the injected\n * {@link DeriveStartMediaTime} seam over it in `monitoring`, and stamps the settled\n * `startMediaTime` onto the model — the coordinate *consume* — until `established`.\n * The byte-level discover/stamp steps that fill the slot are a separate,\n * config `messagePipelines` array (`primitives/relocation-pipelines`);\n * the two coordinate only through the shared `state.mediaContainerData` slot, never\n * by import. See `internal/design/spf/presentation-timeline-model.md`.\n */\nimport type { Behavior } from '../../core/composition/create-composition';\nimport { createMachineReactor, type Reactor } from '../../core/reactors/create-machine-reactor';\nimport { type ReadonlySignal, type Signal, update } from '../../core/signals/primitives';\nimport {\n isResolvedPresentation,\n type MaybeResolvedPresentation,\n type MediaContainerData,\n type Presentation,\n} from '../../media/types';\nimport { findTrackById } from '../../media/utils/tracks';\nimport type { DeriveStartMediaTime, DeriveStartMediaTimeContext } from '../primitives/derive-start-media-time';\n\nexport type { DeriveStartMediaTime, DeriveStartMediaTimeContext };\n\n// ============================================================================\n// STATE / CONFIG\n// ============================================================================\n\nexport interface EstablishStartMediaTimeState {\n presentation?: MaybeResolvedPresentation;\n /**\n * Transient origin-establishment data, keyed by **track type** (`'video'` /\n * `'audio'`) — per spec one init+media pair per type suffices, and ABR rungs of\n * a type share the origin. Filled by the discover steps across appends, reset\n * per source. Never the model — the churn stays here; only the settled\n * `startMediaTime` reaches `Track`.\n */\n mediaContainerData?: Record<string, MediaContainerData>;\n selectedVideoTrackId?: string;\n selectedAudioTrackId?: string;\n}\n\n/**\n * A single type's own media-timeline origin:\n * `baseMediaDecodeTime/timescale − segmentStartTime` (the `segmentStartTime` term\n * makes it the stream origin even when the first loaded segment isn't the 0th).\n * `undefined` until timescale + baseMediaDecodeTime + segmentStartTime are all present.\n */\nfunction ownOrigin(data: MediaContainerData | undefined): number | undefined {\n const { timescale, baseMediaDecodeTime, segmentStartTime } = data ?? {};\n return timescale != null && baseMediaDecodeTime != null && segmentStartTime != null\n ? baseMediaDecodeTime / timescale - segmentStartTime\n : undefined;\n}\n\n/**\n * Origins below this magnitude (seconds) are treated as `0` — the derive returns `0`, so\n * the presentation is left on its native (~0-based) timeline and no `timestampOffset` is\n * set (the loader stamp no-ops on a derived `0`). Ordinary VOD carries a small nonzero\n * encode origin (audio priming, first-frame CTS, edit lists); relocating by a sub-second\n * amount is pointless, and setting `timestampOffset` at all can ripple edge segments.\n * Relocation targets streams with an intentional large origin (instant clips, bipbop @10s).\n *\n * Absolute basis, not proportional: the DTS-below-zero / ripple risk scales with the\n * origin's absolute size, not with the presentation's duration. Also snaps negatives to\n * `0` — a negative origin would relocate *forward*, which is never the intent.\n */\nexport const NEAR_ZERO_ORIGIN_THRESHOLD = 1;\n\n/** Snap a below-threshold (incl. negative) origin to `0` so it isn't relocated. */\nfunction thresholdOrigin(origin: number): number {\n return origin < NEAR_ZERO_ORIGIN_THRESHOLD ? 0 : origin;\n}\n\n/**\n * The **default** — relocate the whole presentation by one shared origin: the `min`\n * across the *selected* A/V tracks' own origins, denormalized onto every type. This\n * single reduce subsumes the \"per-type\" and \"shared\" tiers:\n * - **aligned A/V** — `min` equals each origin (they're equal), so it matches per-type;\n * - **skewed A/V** (e.g. Apple's 44ms audio-lead) — `min` keeps every track's earliest\n * DTS ≥ 0 (relocating by ≤ each own origin never drives one negative) *and* preserves\n * the real skew (per-type would flatten it, desyncing A/V);\n * - **single type / muxed** — `min` of the one origin is that origin.\n *\n * Returns `undefined` for every type until all *selected* types have a complete origin\n * (the shared-`min` barrier). Which types must contribute is read from `ctx` (the\n * selected v/a ids); with no selection context it coordinates across whatever types\n * have data. A shared origin below {@link NEAR_ZERO_ORIGIN_THRESHOLD} is returned as `0`\n * (native — ordinary ~0-PTS VOD isn't relocated).\n */\nexport const deriveSharedMinStartMediaTime: DeriveStartMediaTime = (containerData, ctx) => {\n const contributingTypes: string[] = [];\n if (ctx.selectedVideoTrackId != null) contributingTypes.push('video');\n if (ctx.selectedAudioTrackId != null) contributingTypes.push('audio');\n const types = contributingTypes.length > 0 ? contributingTypes : Object.keys(containerData);\n\n const origins = types.map((type) => ownOrigin(containerData[type]));\n // Barrier: not ready until every contributing type has a complete origin.\n if (origins.length === 0 || origins.some((origin) => origin === undefined)) return {};\n\n const shared = thresholdOrigin(Math.min(...(origins as number[])));\n const out: Record<string, number | undefined> = {};\n for (const type of Object.keys(containerData)) out[type] = shared;\n return out;\n};\n\n/**\n * Coordination-axis *off* — each type relocates by its own origin, independently. Not\n * the default: it flattens real A/V skew (see {@link deriveSharedMinStartMediaTime}).\n * Kept as an opt-in for compositions that know their A/V is aligned and want to skip\n * the shared-`min` barrier (each type stamps as soon as its own origin is discovered).\n */\nexport const derivePerTypeStartMediaTime: DeriveStartMediaTime = (containerData) => {\n const out: Record<string, number | undefined> = {};\n for (const [type, data] of Object.entries(containerData)) {\n const origin = ownOrigin(data);\n out[type] = origin === undefined ? undefined : thresholdOrigin(origin);\n }\n return out;\n};\n\nexport interface EstablishStartMediaTimeConfig {\n /** The reduce seam (coordination knob). Defaults to {@link deriveSharedMinStartMediaTime}. */\n deriveStartMediaTime?: DeriveStartMediaTime;\n}\n\n// ============================================================================\n// REACTOR (owns the transient slot; derives + consumes onto the model)\n// ============================================================================\n\n/** Stamp the derived per-track `startMediaTime` onto the model (idempotent — same reference when nothing moved). */\nfunction stampTracks(presentation: Presentation, startMediaTimes: Record<string, number | undefined>): Presentation {\n let changed = false;\n const selectionSets = presentation.selectionSets.map((selectionSet) => ({\n ...selectionSet,\n switchingSets: selectionSet.switchingSets.map((switchingSet) => ({\n ...switchingSet,\n tracks: switchingSet.tracks.map((track) => {\n const startMediaTime = startMediaTimes[track.type];\n if (startMediaTime === undefined || track.startMediaTime === startMediaTime) return track;\n changed = true;\n return { ...track, startMediaTime };\n }),\n })),\n }));\n return changed ? ({ ...presentation, selectionSets } as Presentation) : presentation;\n}\n\ntype EstablishFsmState = 'inactive' | 'monitoring' | 'established';\n\ninterface EstablishStartMediaTimeDeps {\n state: {\n presentation: Signal<EstablishStartMediaTimeState['presentation']>;\n mediaContainerData: Signal<EstablishStartMediaTimeState['mediaContainerData']>;\n // Optional so the one behavior composes across video-only / audio-only / both,\n // like other cross-track-type behaviors (present at runtime iff a sibling owns it).\n selectedVideoTrackId?: ReadonlySignal<EstablishStartMediaTimeState['selectedVideoTrackId']>;\n selectedAudioTrackId?: ReadonlySignal<EstablishStartMediaTimeState['selectedAudioTrackId']>;\n };\n config?: EstablishStartMediaTimeConfig;\n}\n\nfunction establishStartMediaTimeSetup({\n state,\n config = {},\n}: EstablishStartMediaTimeDeps): Reactor<EstablishFsmState | 'destroying' | 'destroyed'> {\n const derive = config.deriveStartMediaTime ?? deriveSharedMinStartMediaTime;\n\n const selectionContext = (): DeriveStartMediaTimeContext => ({\n selectedVideoTrackId: state.selectedVideoTrackId?.get(),\n selectedAudioTrackId: state.selectedAudioTrackId?.get(),\n });\n\n /** Established once the selected A/V tracks (whichever exist) carry `startMediaTime`. */\n const established = (): boolean => {\n const presentation = state.presentation.get();\n if (!isResolvedPresentation(presentation)) return false;\n const ids = [state.selectedVideoTrackId?.get(), state.selectedAudioTrackId?.get()].filter(\n (id): id is string => id !== undefined\n );\n return ids.length > 0 && ids.every((id) => findTrackById(presentation, id)?.startMediaTime !== undefined);\n };\n\n return createMachineReactor<EstablishFsmState>({\n initial: 'inactive',\n monitor: () => {\n if (!isResolvedPresentation(state.presentation.get())) return 'inactive';\n return established() ? 'established' : 'monitoring';\n },\n states: {\n // Fresh per source: clear the transient slot so a new source re-discovers.\n inactive: { entry: () => state.mediaContainerData.set(undefined) },\n // Derive: reduce the accumulating container data into per-track startMediaTime\n // and stamp it onto the model. Re-runs as discover fills the slot; the update\n // reads presentation untracked, so no self-loop. Disabled on entry to\n // `established` (establish-once, sticky per source).\n monitoring: {\n effects: () => {\n const containerData = state.mediaContainerData.get();\n if (!containerData) return;\n const startMediaTimes = derive(containerData, selectionContext());\n // `current` is always resolved here — the monitor gates `monitoring` on a\n // resolved presentation and transitions before effects re-run — so we cast\n // to `Presentation` rather than re-narrowing.\n update(state.presentation as Signal<MaybeResolvedPresentation>, (current) =>\n stampTracks(current as Presentation, startMediaTimes)\n );\n },\n },\n established: {},\n },\n });\n}\n\nexport const establishStartMediaTime: Behavior<\n {\n presentation: Signal<EstablishStartMediaTimeState['presentation']>;\n mediaContainerData: Signal<EstablishStartMediaTimeState['mediaContainerData']>;\n },\n Record<never, never>,\n EstablishStartMediaTimeConfig\n> = {\n stateKeys: ['presentation', 'mediaContainerData'],\n contextKeys: [],\n setup: establishStartMediaTimeSetup,\n};\n"],"mappings":";;;;;;;;;;;AAuDA,SAAS,UAAU,MAA0D;CAC3E,MAAM,EAAE,WAAW,qBAAqB,qBAAqB,QAAQ,CAAC;CACtE,OAAO,aAAa,QAAQ,uBAAuB,QAAQ,oBAAoB,OAC3E,sBAAsB,YAAY,mBAClC,KAAA;AACN;;AAiBA,SAAS,gBAAgB,QAAwB;CAC/C,OAAO,SAAA,IAAsC,IAAI;AACnD;;;;;;;;;;;;;;;;;AAkBA,MAAa,iCAAuD,eAAe,QAAQ;CACzF,MAAM,oBAA8B,CAAC;CACrC,IAAI,IAAI,wBAAwB,MAAM,kBAAkB,KAAK,OAAO;CACpE,IAAI,IAAI,wBAAwB,MAAM,kBAAkB,KAAK,OAAO;CAGpE,MAAM,WAFQ,kBAAkB,SAAS,IAAI,oBAAoB,OAAO,KAAK,aAAa,EAAA,CAEpE,KAAK,SAAS,UAAU,cAAc,KAAK,CAAC;CAElE,IAAI,QAAQ,WAAW,KAAK,QAAQ,MAAM,WAAW,WAAW,KAAA,CAAS,GAAG,OAAO,CAAC;CAEpF,MAAM,SAAS,gBAAgB,KAAK,IAAI,GAAI,OAAoB,CAAC;CACjE,MAAM,MAA0C,CAAC;CACjD,KAAK,MAAM,QAAQ,OAAO,KAAK,aAAa,GAAG,IAAI,QAAQ;CAC3D,OAAO;AACT;;;;;;;AAQA,MAAa,+BAAqD,kBAAkB;CAClF,MAAM,MAA0C,CAAC;CACjD,KAAK,MAAM,CAAC,MAAM,SAAS,OAAO,QAAQ,aAAa,GAAG;EACxD,MAAM,SAAS,UAAU,IAAI;EAC7B,IAAI,QAAQ,WAAW,KAAA,IAAY,KAAA,IAAY,gBAAgB,MAAM;CACvE;CACA,OAAO;AACT;;AAYA,SAAS,YAAY,cAA4B,iBAAmE;CAClH,IAAI,UAAU;CACd,MAAM,gBAAgB,aAAa,cAAc,KAAK,kBAAkB;EACtE,GAAG;EACH,eAAe,aAAa,cAAc,KAAK,kBAAkB;GAC/D,GAAG;GACH,QAAQ,aAAa,OAAO,KAAK,UAAU;IACzC,MAAM,iBAAiB,gBAAgB,MAAM;IAC7C,IAAI,mBAAmB,KAAA,KAAa,MAAM,mBAAmB,gBAAgB,OAAO;IACpF,UAAU;IACV,OAAO;KAAE,GAAG;KAAO;IAAe;GACpC,CAAC;EACH,EAAE;CACJ,EAAE;CACF,OAAO,UAAW;EAAE,GAAG;EAAc;CAAc,IAAqB;AAC1E;AAgBA,SAAS,6BAA6B,EACpC,OACA,SAAS,CAAC,KAC6E;CACvF,MAAM,SAAS,OAAO,wBAAwB;CAE9C,MAAM,0BAAuD;EAC3D,sBAAsB,MAAM,sBAAsB,IAAI;EACtD,sBAAsB,MAAM,sBAAsB,IAAI;CACxD;;CAGA,MAAM,oBAA6B;EACjC,MAAM,eAAe,MAAM,aAAa,IAAI;EAC5C,IAAI,CAAC,uBAAuB,YAAY,GAAG,OAAO;EAClD,MAAM,MAAM,CAAC,MAAM,sBAAsB,IAAI,GAAG,MAAM,sBAAsB,IAAI,CAAC,CAAC,CAAC,QAChF,OAAqB,OAAO,KAAA,CAC/B;EACA,OAAO,IAAI,SAAS,KAAK,IAAI,OAAO,OAAO,cAAc,cAAc,EAAE,CAAC,EAAE,mBAAmB,KAAA,CAAS;CAC1G;CAEA,OAAO,qBAAwC;EAC7C,SAAS;EACT,eAAe;GACb,IAAI,CAAC,uBAAuB,MAAM,aAAa,IAAI,CAAC,GAAG,OAAO;GAC9D,OAAO,YAAY,IAAI,gBAAgB;EACzC;EACA,QAAQ;GAEN,UAAU,EAAE,aAAa,MAAM,mBAAmB,IAAI,KAAA,CAAS,EAAE;GAKjE,YAAY,EACV,eAAe;IACb,MAAM,gBAAgB,MAAM,mBAAmB,IAAI;IACnD,IAAI,CAAC,eAAe;IACpB,MAAM,kBAAkB,OAAO,eAAe,iBAAiB,CAAC;IAIhE,OAAO,MAAM,eAAoD,YAC/D,YAAY,SAAyB,eAAe,CACtD;GACF,EACF;GACA,aAAa,CAAC;EAChB;CACF,CAAC;AACH;AAEA,MAAa,0BAOT;CACF,WAAW,CAAC,gBAAgB,oBAAoB;CAChD,aAAa,CAAC;CACd,OAAO;AACT"}
@@ -1 +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"}
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;CAC1B,IAAI,CAAC,cAAc,KAAK,OAAO;CAC/B,IAAI,uBAAuB,YAAY,GAAG,OAAO;CAEjD,OADiB,CAAC,CAAC,iBAAiB,CAAC,kBAAkB,SAAS,cAAc,IAC5D,cAAc;AAClC;AAEA,SAAS,yBAAyB,EAChC,OACA,UAQiE;CACjE,MAAM,EAAE,sBAAsB;CAC9B,MAAM,iBAAkC,OAAO,kBAAA;CAE/C,MAAM,qBAAqB,eACzB,YAAY,MAAM,aAAa,IAAI,GAAG,MAAM,QAAQ,IAAI,GAAG,MAAM,cAAc,IAAI,GAAG,cAAc,CACtG;CAEA,OAAO,qBAA+C;EACpD,SAAS;EACT,eAAe,mBAAmB,IAAI;EACtC,QAAQ;GACN,uBAAuB,CAAC;GACxB,MAAM,CAAC;GACP,WAAW,EACT,aAAa;IACX,MAAM,eAAe,MAAM,aAAa,IAAI;IAC5C,MAAM,KAAK,IAAI,gBAAgB;IAE/B,gBAAgB,cAAc,EAAE,QAAQ,GAAG,OAAO,CAAC,CAAC,CACjD,MAAM,aAAa,gBAAgB,QAAQ,CAAC,CAAC,CAC7C,MAAM,SAAS;KACd,MAAM,SAAS,kBAAkB,MAAM,YAAY;KACnD,MAAM,aAAa,IAAI,MAAM;IAC/B,CAAC,CAAC,CACD,OAAO,UAAU;KAChB,IAAI,iBAAiB,SAAS,MAAM,SAAS,cAAc;KAE3D,QAAQ,MAAM,sDAAsD,KAAK;IAC3E,CAAC;IAEH,OAAO;GACT,EACF;GACA,UAAU,CAAC;EACb;CACF,CAAC;AACH;AAEA,MAAa,sBAAsB,eAAe;CAChD,WAAW;EAAC;EAAgB;EAAW;CAAe;CACtD,aAAa,CAAC;CACd,OAAO;AACT,CAAC"}