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

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 (516) hide show
  1. package/dist/default/background-video.js +3 -0
  2. package/dist/{dev/core → default/core/actors}/create-machine-actor.js +3 -3
  3. package/dist/default/core/actors/create-machine-actor.js.map +1 -0
  4. package/dist/default/core/{create-transition-actor.js → actors/create-transition-actor.js} +3 -3
  5. package/dist/default/core/actors/create-transition-actor.js.map +1 -0
  6. package/dist/default/core/composition/create-composition.js +85 -0
  7. package/dist/default/core/composition/create-composition.js.map +1 -0
  8. package/dist/default/core/composition/share-signals.js +39 -0
  9. package/dist/default/core/composition/share-signals.js.map +1 -0
  10. package/dist/{dev/core → default/core/reactors}/create-machine-reactor.js +4 -4
  11. package/dist/default/core/reactors/create-machine-reactor.js.map +1 -0
  12. package/dist/default/core/signals/primitives.js +33 -11
  13. package/dist/default/core/signals/primitives.js.map +1 -1
  14. package/dist/default/core/{task.js → tasks/task.js} +2 -2
  15. package/dist/default/core/tasks/task.js.map +1 -0
  16. package/dist/default/dom.js +9 -10
  17. package/dist/default/hls.js +5 -0
  18. package/dist/default/index.js +12 -3
  19. package/dist/default/index.js.map +1 -1
  20. package/dist/default/media/abr/quality-selection.js +21 -0
  21. package/dist/default/media/abr/quality-selection.js.map +1 -0
  22. package/dist/default/{core → media}/buffer/back-buffer.js +2 -2
  23. package/dist/default/media/buffer/back-buffer.js.map +1 -0
  24. package/dist/{dev/core → default/media}/buffer/forward-buffer.js +14 -2
  25. package/dist/default/media/buffer/forward-buffer.js.map +1 -0
  26. package/dist/default/media/dom/capabilities.js +70 -0
  27. package/dist/default/media/dom/capabilities.js.map +1 -0
  28. package/dist/{dev/dom/media → default/media/dom/mse}/append-segment.js +1 -1
  29. package/dist/default/media/dom/mse/append-segment.js.map +1 -0
  30. package/dist/default/{dom/media → media/dom/mse}/buffer-flusher.js +1 -1
  31. package/dist/default/media/dom/mse/buffer-flusher.js.map +1 -0
  32. package/dist/default/media/dom/mse/duration.js +78 -0
  33. package/dist/default/media/dom/mse/duration.js.map +1 -0
  34. package/dist/default/media/dom/mse/end-of-stream.js +20 -0
  35. package/dist/default/media/dom/mse/end-of-stream.js.map +1 -0
  36. package/dist/default/{dom/media → media/dom/mse}/mediasource-setup.js +63 -18
  37. package/dist/default/media/dom/mse/mediasource-setup.js.map +1 -0
  38. package/dist/default/{dom/text/parse-vtt-segment.js → media/dom/text/resolve-vtt-segment.js} +5 -5
  39. package/dist/default/media/dom/text/resolve-vtt-segment.js.map +1 -0
  40. package/dist/default/media/dom/text/text-track-slots.js +69 -0
  41. package/dist/default/media/dom/text/text-track-slots.js.map +1 -0
  42. package/dist/{dev/core → default/media}/hls/parse-attributes.js +17 -3
  43. package/dist/default/media/hls/parse-attributes.js.map +1 -0
  44. package/dist/{dev/core → default/media}/hls/parse-media-playlist.js +25 -2
  45. package/dist/default/media/hls/parse-media-playlist.js.map +1 -0
  46. package/dist/{dev/core → default/media}/hls/parse-multivariant.js +17 -8
  47. package/dist/default/media/hls/parse-multivariant.js.map +1 -0
  48. package/dist/default/{core → media}/hls/resolve-url.js +1 -1
  49. package/dist/default/media/hls/resolve-url.js.map +1 -0
  50. package/dist/default/media/primitives/select-tracks.js +130 -0
  51. package/dist/default/media/primitives/select-tracks.js.map +1 -0
  52. package/dist/default/media/types/index.js +36 -0
  53. package/dist/default/media/types/index.js.map +1 -0
  54. package/dist/default/media/utils/cdn.js +56 -0
  55. package/dist/default/media/utils/cdn.js.map +1 -0
  56. package/dist/default/media/utils/preload.js +22 -0
  57. package/dist/default/media/utils/preload.js.map +1 -0
  58. package/dist/default/media/utils/track-selection.js +47 -0
  59. package/dist/default/media/utils/track-selection.js.map +1 -0
  60. package/dist/default/media/utils/tracks.js +130 -0
  61. package/dist/default/media/utils/tracks.js.map +1 -0
  62. package/dist/default/{core/abr → network}/bandwidth-estimator.js +6 -5
  63. package/dist/default/network/bandwidth-estimator.js.map +1 -0
  64. package/dist/{dev/dom → default}/network/chunked-stream-iterable.js +1 -1
  65. package/dist/default/network/chunked-stream-iterable.js.map +1 -0
  66. package/dist/{dev/core/abr → default/network}/ewma.js +1 -1
  67. package/dist/default/network/ewma.js.map +1 -0
  68. package/dist/default/network/fetch.js +115 -0
  69. package/dist/default/network/fetch.js.map +1 -0
  70. package/dist/default/{dom/features/segment-loader-actor.js → playback/actors/dom/segment-loader.js} +63 -23
  71. package/dist/default/playback/actors/dom/segment-loader.js.map +1 -0
  72. package/dist/default/{dom/media/source-buffer-actor.js → playback/actors/dom/source-buffer.js} +10 -9
  73. package/dist/default/playback/actors/dom/source-buffer.js.map +1 -0
  74. package/dist/{dev/dom/features/text-tracks-actor.js → default/playback/actors/dom/text-tracks.js} +7 -3
  75. package/dist/default/playback/actors/dom/text-tracks.js.map +1 -0
  76. package/dist/default/playback/actors/text-track-segment-loader.js +135 -0
  77. package/dist/default/playback/actors/text-track-segment-loader.js.map +1 -0
  78. package/dist/default/playback/behaviors/calculate-presentation-duration.js +36 -0
  79. package/dist/default/playback/behaviors/calculate-presentation-duration.js.map +1 -0
  80. package/dist/default/playback/behaviors/derive-cdn-priority.js +68 -0
  81. package/dist/default/playback/behaviors/derive-cdn-priority.js.map +1 -0
  82. package/dist/default/playback/behaviors/dom/end-of-stream.js +90 -0
  83. package/dist/default/playback/behaviors/dom/end-of-stream.js.map +1 -0
  84. package/dist/default/playback/behaviors/dom/load-segments.js +177 -0
  85. package/dist/default/playback/behaviors/dom/load-segments.js.map +1 -0
  86. package/dist/default/playback/behaviors/dom/setup-buffer-actors.js +192 -0
  87. package/dist/default/playback/behaviors/dom/setup-buffer-actors.js.map +1 -0
  88. package/dist/default/playback/behaviors/dom/setup-mediasource.js +89 -0
  89. package/dist/default/playback/behaviors/dom/setup-mediasource.js.map +1 -0
  90. package/dist/default/playback/behaviors/dom/setup-text-track-actors.js +54 -0
  91. package/dist/default/playback/behaviors/dom/setup-text-track-actors.js.map +1 -0
  92. package/dist/default/playback/behaviors/dom/sync-text-tracks.js +124 -0
  93. package/dist/default/playback/behaviors/dom/sync-text-tracks.js.map +1 -0
  94. package/dist/default/playback/behaviors/dom/track-current-time.js +52 -0
  95. package/dist/default/playback/behaviors/dom/track-current-time.js.map +1 -0
  96. package/dist/default/playback/behaviors/dom/track-load-triggers.js +94 -0
  97. package/dist/default/playback/behaviors/dom/track-load-triggers.js.map +1 -0
  98. package/dist/default/playback/behaviors/dom/track-playback-rate.js +37 -0
  99. package/dist/default/playback/behaviors/dom/track-playback-rate.js.map +1 -0
  100. package/dist/default/playback/behaviors/dom/update-mediasource-duration.js +90 -0
  101. package/dist/default/playback/behaviors/dom/update-mediasource-duration.js.map +1 -0
  102. package/dist/default/playback/behaviors/resolve-presentation.js +85 -0
  103. package/dist/default/playback/behaviors/resolve-presentation.js.map +1 -0
  104. package/dist/default/playback/behaviors/resolve-track.js +120 -0
  105. package/dist/default/playback/behaviors/resolve-track.js.map +1 -0
  106. package/dist/default/playback/behaviors/select-tracks.js +101 -0
  107. package/dist/default/playback/behaviors/select-tracks.js.map +1 -0
  108. package/dist/default/playback/behaviors/setup-failover-monitor.js +66 -0
  109. package/dist/default/playback/behaviors/setup-failover-monitor.js.map +1 -0
  110. package/dist/default/playback/behaviors/sync-preload.js +59 -0
  111. package/dist/default/playback/behaviors/sync-preload.js.map +1 -0
  112. package/dist/default/playback/behaviors/track-switching.js +341 -0
  113. package/dist/default/playback/behaviors/track-switching.js.map +1 -0
  114. package/dist/default/playback/engines/background-video/adapter.js +158 -0
  115. package/dist/default/playback/engines/background-video/adapter.js.map +1 -0
  116. package/dist/default/playback/engines/background-video/engine.js +75 -0
  117. package/dist/default/playback/engines/background-video/engine.js.map +1 -0
  118. package/dist/default/playback/engines/hls/adapter-audio-only.js +119 -0
  119. package/dist/default/playback/engines/hls/adapter-audio-only.js.map +1 -0
  120. package/dist/default/{dom/playback-engine → playback/engines/hls}/adapter.js +40 -26
  121. package/dist/default/playback/engines/hls/adapter.js.map +1 -0
  122. package/dist/default/playback/engines/hls/engine-audio-only.js +80 -0
  123. package/dist/default/playback/engines/hls/engine-audio-only.js.map +1 -0
  124. package/dist/default/playback/engines/hls/engine.js +115 -0
  125. package/dist/default/playback/engines/hls/engine.js.map +1 -0
  126. package/dist/default/playback/primitives/failover-fetch.js +42 -0
  127. package/dist/default/playback/primitives/failover-fetch.js.map +1 -0
  128. package/dist/default/playback/primitives/track-types.js +61 -0
  129. package/dist/default/playback/primitives/track-types.js.map +1 -0
  130. package/dist/dev/background-video.d.ts +3 -0
  131. package/dist/dev/background-video.js +3 -0
  132. package/dist/dev/core/{actor.d.ts → actors/actor.d.ts} +2 -2
  133. package/dist/dev/core/actors/actor.d.ts.map +1 -0
  134. package/dist/dev/core/actors/create-machine-actor.d.ts +125 -0
  135. package/dist/dev/core/actors/create-machine-actor.d.ts.map +1 -0
  136. package/dist/{default/core → dev/core/actors}/create-machine-actor.js +3 -3
  137. package/dist/dev/core/actors/create-machine-actor.js.map +1 -0
  138. package/dist/dev/core/actors/create-transition-actor.d.ts +42 -0
  139. package/dist/dev/core/actors/create-transition-actor.d.ts.map +1 -0
  140. package/dist/dev/core/{create-transition-actor.js → actors/create-transition-actor.js} +3 -3
  141. package/dist/dev/core/actors/create-transition-actor.js.map +1 -0
  142. package/dist/dev/core/composition/create-composition.d.ts +274 -0
  143. package/dist/dev/core/composition/create-composition.d.ts.map +1 -0
  144. package/dist/dev/core/composition/create-composition.js +85 -0
  145. package/dist/dev/core/composition/create-composition.js.map +1 -0
  146. package/dist/dev/core/composition/share-signals.d.ts +47 -0
  147. package/dist/dev/core/composition/share-signals.d.ts.map +1 -0
  148. package/dist/dev/core/composition/share-signals.js +39 -0
  149. package/dist/dev/core/composition/share-signals.js.map +1 -0
  150. package/dist/dev/core/machine.d.ts +0 -1
  151. package/dist/dev/core/machine.d.ts.map +1 -1
  152. package/dist/dev/core/reactors/create-machine-reactor.d.ts +93 -0
  153. package/dist/dev/core/reactors/create-machine-reactor.d.ts.map +1 -0
  154. package/dist/{default/core → dev/core/reactors}/create-machine-reactor.js +4 -4
  155. package/dist/dev/core/reactors/create-machine-reactor.js.map +1 -0
  156. package/dist/dev/core/signals/primitives.d.ts +38 -1
  157. package/dist/dev/core/signals/primitives.d.ts.map +1 -1
  158. package/dist/dev/core/signals/primitives.js +33 -11
  159. package/dist/dev/core/signals/primitives.js.map +1 -1
  160. package/dist/dev/core/tasks/task.d.ts +115 -0
  161. package/dist/dev/core/tasks/task.d.ts.map +1 -0
  162. package/dist/dev/core/{task.js → tasks/task.js} +2 -2
  163. package/dist/dev/core/tasks/task.js.map +1 -0
  164. package/dist/dev/dom.d.ts +9 -10
  165. package/dist/dev/dom.js +9 -10
  166. package/dist/dev/hls.d.ts +5 -0
  167. package/dist/dev/hls.js +5 -0
  168. package/dist/dev/index.d.ts +15 -3
  169. package/dist/dev/index.d.ts.map +1 -1
  170. package/dist/dev/index.js +12 -3
  171. package/dist/dev/index.js.map +1 -1
  172. package/dist/dev/media/abr/quality-selection.d.ts +22 -0
  173. package/dist/dev/media/abr/quality-selection.d.ts.map +1 -0
  174. package/dist/dev/media/abr/quality-selection.js +21 -0
  175. package/dist/dev/media/abr/quality-selection.js.map +1 -0
  176. package/dist/dev/media/buffer/back-buffer.d.ts +14 -0
  177. package/dist/dev/media/buffer/back-buffer.d.ts.map +1 -0
  178. package/dist/dev/{core → media}/buffer/back-buffer.js +2 -2
  179. package/dist/dev/media/buffer/back-buffer.js.map +1 -0
  180. package/dist/dev/media/buffer/forward-buffer.d.ts +14 -0
  181. package/dist/dev/media/buffer/forward-buffer.d.ts.map +1 -0
  182. package/dist/{default/core → dev/media}/buffer/forward-buffer.js +14 -2
  183. package/dist/dev/media/buffer/forward-buffer.js.map +1 -0
  184. package/dist/dev/media/dom/capabilities.js +70 -0
  185. package/dist/dev/media/dom/capabilities.js.map +1 -0
  186. package/dist/dev/{dom/media → media/dom/mse}/append-segment.d.ts +1 -1
  187. package/dist/dev/media/dom/mse/append-segment.d.ts.map +1 -0
  188. package/dist/{default/dom/media → dev/media/dom/mse}/append-segment.js +1 -1
  189. package/dist/dev/media/dom/mse/append-segment.js.map +1 -0
  190. package/dist/dev/{dom/media → media/dom/mse}/buffer-flusher.d.ts +1 -1
  191. package/dist/dev/media/dom/mse/buffer-flusher.d.ts.map +1 -0
  192. package/dist/dev/{dom/media → media/dom/mse}/buffer-flusher.js +1 -1
  193. package/dist/dev/media/dom/mse/buffer-flusher.js.map +1 -0
  194. package/dist/dev/media/dom/mse/duration.js +78 -0
  195. package/dist/dev/media/dom/mse/duration.js.map +1 -0
  196. package/dist/dev/media/dom/mse/end-of-stream.js +20 -0
  197. package/dist/dev/media/dom/mse/end-of-stream.js.map +1 -0
  198. package/dist/dev/{dom/media → media/dom/mse}/mediasource-setup.js +63 -18
  199. package/dist/dev/media/dom/mse/mediasource-setup.js.map +1 -0
  200. package/dist/dev/media/dom/text/resolve-vtt-segment.d.ts +12 -0
  201. package/dist/dev/media/dom/text/resolve-vtt-segment.d.ts.map +1 -0
  202. package/dist/dev/{dom/text/parse-vtt-segment.js → media/dom/text/resolve-vtt-segment.js} +5 -5
  203. package/dist/dev/media/dom/text/resolve-vtt-segment.js.map +1 -0
  204. package/dist/dev/media/dom/text/text-track-slots.d.ts +31 -0
  205. package/dist/dev/media/dom/text/text-track-slots.d.ts.map +1 -0
  206. package/dist/dev/media/dom/text/text-track-slots.js +69 -0
  207. package/dist/dev/media/dom/text/text-track-slots.js.map +1 -0
  208. package/dist/{default/core → dev/media}/hls/parse-attributes.js +17 -3
  209. package/dist/dev/media/hls/parse-attributes.js.map +1 -0
  210. package/dist/{default/core → dev/media}/hls/parse-media-playlist.js +25 -2
  211. package/dist/dev/media/hls/parse-media-playlist.js.map +1 -0
  212. package/dist/{default/core → dev/media}/hls/parse-multivariant.js +17 -8
  213. package/dist/dev/media/hls/parse-multivariant.js.map +1 -0
  214. package/dist/dev/{core → media}/hls/resolve-url.js +1 -1
  215. package/dist/dev/media/hls/resolve-url.js.map +1 -0
  216. package/dist/dev/media/primitives/select-tracks.d.ts +36 -0
  217. package/dist/dev/media/primitives/select-tracks.d.ts.map +1 -0
  218. package/dist/dev/media/primitives/select-tracks.js +130 -0
  219. package/dist/dev/media/primitives/select-tracks.js.map +1 -0
  220. package/dist/dev/{core → media}/types/index.d.ts +55 -3
  221. package/dist/dev/media/types/index.d.ts.map +1 -0
  222. package/dist/dev/media/types/index.js +36 -0
  223. package/dist/dev/media/types/index.js.map +1 -0
  224. package/dist/dev/media/utils/cdn.d.ts +13 -0
  225. package/dist/dev/media/utils/cdn.d.ts.map +1 -0
  226. package/dist/dev/media/utils/cdn.js +56 -0
  227. package/dist/dev/media/utils/cdn.js.map +1 -0
  228. package/dist/dev/media/utils/preload.js +22 -0
  229. package/dist/dev/media/utils/preload.js.map +1 -0
  230. package/dist/dev/media/utils/track-selection.js +47 -0
  231. package/dist/dev/media/utils/track-selection.js.map +1 -0
  232. package/dist/dev/media/utils/tracks.js +130 -0
  233. package/dist/dev/media/utils/tracks.js.map +1 -0
  234. package/dist/dev/{core/abr → network}/bandwidth-estimator.d.ts +17 -2
  235. package/dist/dev/network/bandwidth-estimator.d.ts.map +1 -0
  236. package/dist/dev/{core/abr → network}/bandwidth-estimator.js +6 -5
  237. package/dist/dev/network/bandwidth-estimator.js.map +1 -0
  238. package/dist/{default/dom → dev}/network/chunked-stream-iterable.js +1 -1
  239. package/dist/dev/network/chunked-stream-iterable.js.map +1 -0
  240. package/dist/{default/core/abr → dev/network}/ewma.js +1 -1
  241. package/dist/dev/network/ewma.js.map +1 -0
  242. package/dist/dev/network/fetch.js +115 -0
  243. package/dist/dev/network/fetch.js.map +1 -0
  244. package/dist/dev/playback/actors/dom/segment-loader.d.ts +36 -0
  245. package/dist/dev/playback/actors/dom/segment-loader.d.ts.map +1 -0
  246. package/dist/dev/{dom/features/segment-loader-actor.js → playback/actors/dom/segment-loader.js} +63 -23
  247. package/dist/dev/playback/actors/dom/segment-loader.js.map +1 -0
  248. package/dist/dev/{dom/media/source-buffer-actor.d.ts → playback/actors/dom/source-buffer.d.ts} +20 -5
  249. package/dist/dev/playback/actors/dom/source-buffer.d.ts.map +1 -0
  250. package/dist/dev/{dom/media/source-buffer-actor.js → playback/actors/dom/source-buffer.js} +10 -9
  251. package/dist/dev/playback/actors/dom/source-buffer.js.map +1 -0
  252. package/dist/dev/playback/actors/dom/text-tracks.d.ts +1 -0
  253. package/dist/{default/dom/features/text-tracks-actor.js → dev/playback/actors/dom/text-tracks.js} +7 -3
  254. package/dist/dev/playback/actors/dom/text-tracks.js.map +1 -0
  255. package/dist/dev/playback/actors/text-track-segment-loader.d.ts +59 -0
  256. package/dist/dev/playback/actors/text-track-segment-loader.d.ts.map +1 -0
  257. package/dist/dev/playback/actors/text-track-segment-loader.js +135 -0
  258. package/dist/dev/playback/actors/text-track-segment-loader.js.map +1 -0
  259. package/dist/dev/playback/actors/text-tracks.d.ts +42 -0
  260. package/dist/dev/playback/actors/text-tracks.d.ts.map +1 -0
  261. package/dist/dev/playback/behaviors/calculate-presentation-duration.d.ts +25 -0
  262. package/dist/dev/playback/behaviors/calculate-presentation-duration.d.ts.map +1 -0
  263. package/dist/dev/playback/behaviors/calculate-presentation-duration.js +36 -0
  264. package/dist/dev/playback/behaviors/calculate-presentation-duration.js.map +1 -0
  265. package/dist/dev/playback/behaviors/derive-cdn-priority.js +68 -0
  266. package/dist/dev/playback/behaviors/derive-cdn-priority.js.map +1 -0
  267. package/dist/dev/playback/behaviors/dom/end-of-stream.js +90 -0
  268. package/dist/dev/playback/behaviors/dom/end-of-stream.js.map +1 -0
  269. package/dist/dev/playback/behaviors/dom/load-segments.d.ts +62 -0
  270. package/dist/dev/playback/behaviors/dom/load-segments.d.ts.map +1 -0
  271. package/dist/dev/playback/behaviors/dom/load-segments.js +177 -0
  272. package/dist/dev/playback/behaviors/dom/load-segments.js.map +1 -0
  273. package/dist/dev/playback/behaviors/dom/setup-buffer-actors.js +192 -0
  274. package/dist/dev/playback/behaviors/dom/setup-buffer-actors.js.map +1 -0
  275. package/dist/dev/playback/behaviors/dom/setup-mediasource.js +89 -0
  276. package/dist/dev/playback/behaviors/dom/setup-mediasource.js.map +1 -0
  277. package/dist/dev/playback/behaviors/dom/setup-text-track-actors.d.ts +31 -0
  278. package/dist/dev/playback/behaviors/dom/setup-text-track-actors.d.ts.map +1 -0
  279. package/dist/dev/playback/behaviors/dom/setup-text-track-actors.js +54 -0
  280. package/dist/dev/playback/behaviors/dom/setup-text-track-actors.js.map +1 -0
  281. package/dist/dev/playback/behaviors/dom/sync-text-tracks.js +124 -0
  282. package/dist/dev/playback/behaviors/dom/sync-text-tracks.js.map +1 -0
  283. package/dist/dev/playback/behaviors/dom/track-current-time.d.ts +34 -0
  284. package/dist/dev/playback/behaviors/dom/track-current-time.d.ts.map +1 -0
  285. package/dist/dev/playback/behaviors/dom/track-current-time.js +52 -0
  286. package/dist/dev/playback/behaviors/dom/track-current-time.js.map +1 -0
  287. package/dist/dev/playback/behaviors/dom/track-load-triggers.d.ts +48 -0
  288. package/dist/dev/playback/behaviors/dom/track-load-triggers.d.ts.map +1 -0
  289. package/dist/dev/playback/behaviors/dom/track-load-triggers.js +94 -0
  290. package/dist/dev/playback/behaviors/dom/track-load-triggers.js.map +1 -0
  291. package/dist/dev/playback/behaviors/dom/track-playback-rate.d.ts +34 -0
  292. package/dist/dev/playback/behaviors/dom/track-playback-rate.d.ts.map +1 -0
  293. package/dist/dev/playback/behaviors/dom/track-playback-rate.js +37 -0
  294. package/dist/dev/playback/behaviors/dom/track-playback-rate.js.map +1 -0
  295. package/dist/dev/playback/behaviors/dom/update-mediasource-duration.js +90 -0
  296. package/dist/dev/playback/behaviors/dom/update-mediasource-duration.js.map +1 -0
  297. package/dist/dev/playback/behaviors/resolve-presentation.d.ts +7 -0
  298. package/dist/dev/playback/behaviors/resolve-presentation.d.ts.map +1 -0
  299. package/dist/dev/playback/behaviors/resolve-presentation.js +85 -0
  300. package/dist/dev/playback/behaviors/resolve-presentation.js.map +1 -0
  301. package/dist/dev/playback/behaviors/resolve-track.js +120 -0
  302. package/dist/dev/playback/behaviors/resolve-track.js.map +1 -0
  303. package/dist/dev/playback/behaviors/select-tracks.d.ts +13 -0
  304. package/dist/dev/playback/behaviors/select-tracks.d.ts.map +1 -0
  305. package/dist/dev/playback/behaviors/select-tracks.js +101 -0
  306. package/dist/dev/playback/behaviors/select-tracks.js.map +1 -0
  307. package/dist/dev/playback/behaviors/setup-failover-monitor.d.ts +12 -0
  308. package/dist/dev/playback/behaviors/setup-failover-monitor.d.ts.map +1 -0
  309. package/dist/dev/playback/behaviors/setup-failover-monitor.js +66 -0
  310. package/dist/dev/playback/behaviors/setup-failover-monitor.js.map +1 -0
  311. package/dist/dev/playback/behaviors/sync-preload.js +59 -0
  312. package/dist/dev/playback/behaviors/sync-preload.js.map +1 -0
  313. package/dist/dev/playback/behaviors/track-switching.js +341 -0
  314. package/dist/dev/playback/behaviors/track-switching.js.map +1 -0
  315. package/dist/dev/playback/engines/background-video/adapter.d.ts +60 -0
  316. package/dist/dev/playback/engines/background-video/adapter.d.ts.map +1 -0
  317. package/dist/dev/playback/engines/background-video/adapter.js +158 -0
  318. package/dist/dev/playback/engines/background-video/adapter.js.map +1 -0
  319. package/dist/dev/playback/engines/background-video/engine.d.ts +107 -0
  320. package/dist/dev/playback/engines/background-video/engine.d.ts.map +1 -0
  321. package/dist/dev/playback/engines/background-video/engine.js +75 -0
  322. package/dist/dev/playback/engines/background-video/engine.js.map +1 -0
  323. package/dist/dev/playback/engines/hls/adapter-audio-only.d.ts +45 -0
  324. package/dist/dev/playback/engines/hls/adapter-audio-only.d.ts.map +1 -0
  325. package/dist/dev/playback/engines/hls/adapter-audio-only.js +119 -0
  326. package/dist/dev/playback/engines/hls/adapter-audio-only.js.map +1 -0
  327. package/dist/dev/{dom/playback-engine → playback/engines/hls}/adapter.d.ts +13 -12
  328. package/dist/dev/playback/engines/hls/adapter.d.ts.map +1 -0
  329. package/dist/dev/{dom/playback-engine → playback/engines/hls}/adapter.js +40 -26
  330. package/dist/dev/playback/engines/hls/adapter.js.map +1 -0
  331. package/dist/dev/playback/engines/hls/engine-audio-only.d.ts +127 -0
  332. package/dist/dev/playback/engines/hls/engine-audio-only.d.ts.map +1 -0
  333. package/dist/dev/playback/engines/hls/engine-audio-only.js +80 -0
  334. package/dist/dev/playback/engines/hls/engine-audio-only.js.map +1 -0
  335. package/dist/dev/playback/engines/hls/engine.d.ts +236 -0
  336. package/dist/dev/playback/engines/hls/engine.d.ts.map +1 -0
  337. package/dist/dev/playback/engines/hls/engine.js +115 -0
  338. package/dist/dev/playback/engines/hls/engine.js.map +1 -0
  339. package/dist/dev/playback/primitives/failover-fetch.js +42 -0
  340. package/dist/dev/playback/primitives/failover-fetch.js.map +1 -0
  341. package/dist/dev/playback/primitives/track-types.js +61 -0
  342. package/dist/dev/playback/primitives/track-types.js.map +1 -0
  343. package/package.json +12 -7
  344. package/dist/default/core/abr/bandwidth-estimator.js.map +0 -1
  345. package/dist/default/core/abr/ewma.js.map +0 -1
  346. package/dist/default/core/abr/quality-selection.js +0 -53
  347. package/dist/default/core/abr/quality-selection.js.map +0 -1
  348. package/dist/default/core/buffer/back-buffer.js.map +0 -1
  349. package/dist/default/core/buffer/forward-buffer.js.map +0 -1
  350. package/dist/default/core/create-machine-actor.js.map +0 -1
  351. package/dist/default/core/create-machine-reactor.js.map +0 -1
  352. package/dist/default/core/create-transition-actor.js.map +0 -1
  353. package/dist/default/core/features/calculate-presentation-duration.js +0 -53
  354. package/dist/default/core/features/calculate-presentation-duration.js.map +0 -1
  355. package/dist/default/core/features/quality-switching.js +0 -69
  356. package/dist/default/core/features/quality-switching.js.map +0 -1
  357. package/dist/default/core/features/resolve-presentation.js +0 -73
  358. package/dist/default/core/features/resolve-presentation.js.map +0 -1
  359. package/dist/default/core/features/resolve-track.js +0 -66
  360. package/dist/default/core/features/resolve-track.js.map +0 -1
  361. package/dist/default/core/features/select-tracks.js +0 -130
  362. package/dist/default/core/features/select-tracks.js.map +0 -1
  363. package/dist/default/core/features/sync-preload-attribute.js +0 -26
  364. package/dist/default/core/features/sync-preload-attribute.js.map +0 -1
  365. package/dist/default/core/hls/parse-attributes.js.map +0 -1
  366. package/dist/default/core/hls/parse-media-playlist.js.map +0 -1
  367. package/dist/default/core/hls/parse-multivariant.js.map +0 -1
  368. package/dist/default/core/hls/resolve-url.js.map +0 -1
  369. package/dist/default/core/task.js.map +0 -1
  370. package/dist/default/core/types/index.js +0 -15
  371. package/dist/default/core/types/index.js.map +0 -1
  372. package/dist/default/core/utils/generate-id.js +0 -21
  373. package/dist/default/core/utils/generate-id.js.map +0 -1
  374. package/dist/default/core/utils/track-selection.js +0 -39
  375. package/dist/default/core/utils/track-selection.js.map +0 -1
  376. package/dist/default/dom/features/end-of-stream.js +0 -145
  377. package/dist/default/dom/features/end-of-stream.js.map +0 -1
  378. package/dist/default/dom/features/load-segments.js +0 -192
  379. package/dist/default/dom/features/load-segments.js.map +0 -1
  380. package/dist/default/dom/features/load-text-track-cues.js +0 -104
  381. package/dist/default/dom/features/load-text-track-cues.js.map +0 -1
  382. package/dist/default/dom/features/segment-loader-actor.js.map +0 -1
  383. package/dist/default/dom/features/setup-mediasource.js +0 -48
  384. package/dist/default/dom/features/setup-mediasource.js.map +0 -1
  385. package/dist/default/dom/features/setup-sourcebuffer.js +0 -78
  386. package/dist/default/dom/features/setup-sourcebuffer.js.map +0 -1
  387. package/dist/default/dom/features/sync-text-tracks.js +0 -95
  388. package/dist/default/dom/features/sync-text-tracks.js.map +0 -1
  389. package/dist/default/dom/features/text-track-segment-loader-actor.js +0 -55
  390. package/dist/default/dom/features/text-track-segment-loader-actor.js.map +0 -1
  391. package/dist/default/dom/features/text-tracks-actor.js.map +0 -1
  392. package/dist/default/dom/features/track-current-time.js +0 -49
  393. package/dist/default/dom/features/track-current-time.js.map +0 -1
  394. package/dist/default/dom/features/track-playback-initiated.js +0 -66
  395. package/dist/default/dom/features/track-playback-initiated.js.map +0 -1
  396. package/dist/default/dom/features/track-playback-rate.js +0 -32
  397. package/dist/default/dom/features/track-playback-rate.js.map +0 -1
  398. package/dist/default/dom/features/update-duration.js +0 -78
  399. package/dist/default/dom/features/update-duration.js.map +0 -1
  400. package/dist/default/dom/media/append-segment.js.map +0 -1
  401. package/dist/default/dom/media/buffer-flusher.js.map +0 -1
  402. package/dist/default/dom/media/mediasource-setup.js.map +0 -1
  403. package/dist/default/dom/media/source-buffer-actor.js.map +0 -1
  404. package/dist/default/dom/network/chunked-stream-iterable.js.map +0 -1
  405. package/dist/default/dom/network/fetch.js +0 -54
  406. package/dist/default/dom/network/fetch.js.map +0 -1
  407. package/dist/default/dom/playback-engine/adapter.js.map +0 -1
  408. package/dist/default/dom/playback-engine/engine.js +0 -142
  409. package/dist/default/dom/playback-engine/engine.js.map +0 -1
  410. package/dist/default/dom/playback-engine.js +0 -4
  411. package/dist/default/dom/text/parse-vtt-segment.js.map +0 -1
  412. package/dist/dev/core/abr/bandwidth-estimator.d.ts.map +0 -1
  413. package/dist/dev/core/abr/bandwidth-estimator.js.map +0 -1
  414. package/dist/dev/core/abr/ewma.js.map +0 -1
  415. package/dist/dev/core/abr/quality-selection.js +0 -53
  416. package/dist/dev/core/abr/quality-selection.js.map +0 -1
  417. package/dist/dev/core/actor.d.ts.map +0 -1
  418. package/dist/dev/core/buffer/back-buffer.js.map +0 -1
  419. package/dist/dev/core/buffer/forward-buffer.js.map +0 -1
  420. package/dist/dev/core/create-machine-actor.d.ts +0 -12
  421. package/dist/dev/core/create-machine-actor.d.ts.map +0 -1
  422. package/dist/dev/core/create-machine-actor.js.map +0 -1
  423. package/dist/dev/core/create-machine-reactor.d.ts +0 -8
  424. package/dist/dev/core/create-machine-reactor.d.ts.map +0 -1
  425. package/dist/dev/core/create-machine-reactor.js.map +0 -1
  426. package/dist/dev/core/create-transition-actor.d.ts +0 -23
  427. package/dist/dev/core/create-transition-actor.d.ts.map +0 -1
  428. package/dist/dev/core/create-transition-actor.js.map +0 -1
  429. package/dist/dev/core/features/calculate-presentation-duration.js +0 -53
  430. package/dist/dev/core/features/calculate-presentation-duration.js.map +0 -1
  431. package/dist/dev/core/features/quality-switching.js +0 -69
  432. package/dist/dev/core/features/quality-switching.js.map +0 -1
  433. package/dist/dev/core/features/resolve-presentation.js +0 -73
  434. package/dist/dev/core/features/resolve-presentation.js.map +0 -1
  435. package/dist/dev/core/features/resolve-track.js +0 -66
  436. package/dist/dev/core/features/resolve-track.js.map +0 -1
  437. package/dist/dev/core/features/select-tracks.js +0 -130
  438. package/dist/dev/core/features/select-tracks.js.map +0 -1
  439. package/dist/dev/core/features/sync-preload-attribute.js +0 -26
  440. package/dist/dev/core/features/sync-preload-attribute.js.map +0 -1
  441. package/dist/dev/core/hls/parse-attributes.js.map +0 -1
  442. package/dist/dev/core/hls/parse-media-playlist.js.map +0 -1
  443. package/dist/dev/core/hls/parse-multivariant.js.map +0 -1
  444. package/dist/dev/core/hls/resolve-url.js.map +0 -1
  445. package/dist/dev/core/task.js.map +0 -1
  446. package/dist/dev/core/types/index.d.ts.map +0 -1
  447. package/dist/dev/core/types/index.js +0 -15
  448. package/dist/dev/core/types/index.js.map +0 -1
  449. package/dist/dev/core/utils/generate-id.js +0 -21
  450. package/dist/dev/core/utils/generate-id.js.map +0 -1
  451. package/dist/dev/core/utils/track-selection.d.ts +0 -16
  452. package/dist/dev/core/utils/track-selection.d.ts.map +0 -1
  453. package/dist/dev/core/utils/track-selection.js +0 -39
  454. package/dist/dev/core/utils/track-selection.js.map +0 -1
  455. package/dist/dev/dom/features/end-of-stream.js +0 -145
  456. package/dist/dev/dom/features/end-of-stream.js.map +0 -1
  457. package/dist/dev/dom/features/load-segments.d.ts +0 -72
  458. package/dist/dev/dom/features/load-segments.d.ts.map +0 -1
  459. package/dist/dev/dom/features/load-segments.js +0 -192
  460. package/dist/dev/dom/features/load-segments.js.map +0 -1
  461. package/dist/dev/dom/features/load-text-track-cues.d.ts +0 -85
  462. package/dist/dev/dom/features/load-text-track-cues.d.ts.map +0 -1
  463. package/dist/dev/dom/features/load-text-track-cues.js +0 -104
  464. package/dist/dev/dom/features/load-text-track-cues.js.map +0 -1
  465. package/dist/dev/dom/features/segment-loader-actor.d.ts +0 -1
  466. package/dist/dev/dom/features/segment-loader-actor.js.map +0 -1
  467. package/dist/dev/dom/features/setup-mediasource.js +0 -48
  468. package/dist/dev/dom/features/setup-mediasource.js.map +0 -1
  469. package/dist/dev/dom/features/setup-sourcebuffer.d.ts +0 -9
  470. package/dist/dev/dom/features/setup-sourcebuffer.d.ts.map +0 -1
  471. package/dist/dev/dom/features/setup-sourcebuffer.js +0 -78
  472. package/dist/dev/dom/features/setup-sourcebuffer.js.map +0 -1
  473. package/dist/dev/dom/features/sync-text-tracks.js +0 -95
  474. package/dist/dev/dom/features/sync-text-tracks.js.map +0 -1
  475. package/dist/dev/dom/features/text-track-segment-loader-actor.d.ts +0 -12
  476. package/dist/dev/dom/features/text-track-segment-loader-actor.d.ts.map +0 -1
  477. package/dist/dev/dom/features/text-track-segment-loader-actor.js +0 -55
  478. package/dist/dev/dom/features/text-track-segment-loader-actor.js.map +0 -1
  479. package/dist/dev/dom/features/text-tracks-actor.d.ts +0 -31
  480. package/dist/dev/dom/features/text-tracks-actor.d.ts.map +0 -1
  481. package/dist/dev/dom/features/text-tracks-actor.js.map +0 -1
  482. package/dist/dev/dom/features/track-current-time.d.ts +0 -40
  483. package/dist/dev/dom/features/track-current-time.d.ts.map +0 -1
  484. package/dist/dev/dom/features/track-current-time.js +0 -49
  485. package/dist/dev/dom/features/track-current-time.js.map +0 -1
  486. package/dist/dev/dom/features/track-playback-initiated.d.ts +0 -46
  487. package/dist/dev/dom/features/track-playback-initiated.d.ts.map +0 -1
  488. package/dist/dev/dom/features/track-playback-initiated.js +0 -66
  489. package/dist/dev/dom/features/track-playback-initiated.js.map +0 -1
  490. package/dist/dev/dom/features/track-playback-rate.d.ts +0 -34
  491. package/dist/dev/dom/features/track-playback-rate.d.ts.map +0 -1
  492. package/dist/dev/dom/features/track-playback-rate.js +0 -32
  493. package/dist/dev/dom/features/track-playback-rate.js.map +0 -1
  494. package/dist/dev/dom/features/update-duration.js +0 -78
  495. package/dist/dev/dom/features/update-duration.js.map +0 -1
  496. package/dist/dev/dom/media/append-segment.d.ts.map +0 -1
  497. package/dist/dev/dom/media/append-segment.js.map +0 -1
  498. package/dist/dev/dom/media/buffer-flusher.d.ts.map +0 -1
  499. package/dist/dev/dom/media/buffer-flusher.js.map +0 -1
  500. package/dist/dev/dom/media/mediasource-setup.js.map +0 -1
  501. package/dist/dev/dom/media/source-buffer-actor.d.ts.map +0 -1
  502. package/dist/dev/dom/media/source-buffer-actor.js.map +0 -1
  503. package/dist/dev/dom/network/chunked-stream-iterable.js.map +0 -1
  504. package/dist/dev/dom/network/fetch.js +0 -54
  505. package/dist/dev/dom/network/fetch.js.map +0 -1
  506. package/dist/dev/dom/playback-engine/adapter.d.ts.map +0 -1
  507. package/dist/dev/dom/playback-engine/adapter.js.map +0 -1
  508. package/dist/dev/dom/playback-engine/engine.d.ts +0 -124
  509. package/dist/dev/dom/playback-engine/engine.d.ts.map +0 -1
  510. package/dist/dev/dom/playback-engine/engine.js +0 -142
  511. package/dist/dev/dom/playback-engine/engine.js.map +0 -1
  512. package/dist/dev/dom/playback-engine.d.ts +0 -4
  513. package/dist/dev/dom/playback-engine.js +0 -4
  514. package/dist/dev/dom/text/parse-vtt-segment.d.ts +0 -12
  515. package/dist/dev/dom/text/parse-vtt-segment.d.ts.map +0 -1
  516. package/dist/dev/dom/text/parse-vtt-segment.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tracks.js","names":[],"sources":["../../../../src/media/utils/tracks.ts"],"sourcesContent":["import type {\n AudioTrack,\n MaybeResolvedPresentation,\n PartiallyResolvedTrack,\n Presentation,\n ResolvedTrack,\n TextTrack,\n TrackType,\n VideoTrack,\n} from '../types';\nimport { isResolvedTrack } from '../types';\n\n/**\n * Get the tracks of the given type from a presentation's first switching set.\n *\n * Returns `[]` when the presentation is unresolved, when no selection set of\n * `type` exists, or when its first switching set is empty. Returned tracks may\n * be partially resolved (URL only) or fully resolved (with segments) — callers\n * narrow as needed.\n *\n * The \"first switching set\" assumption matches the rest of the codebase\n * (HLS typically has one switching set per type); multi-group / multi-period\n * support would generalize this.\n */\nexport function getTracksByType(\n presentation: MaybeResolvedPresentation,\n type: TrackType\n): readonly (PartiallyResolvedTrack | ResolvedTrack)[] {\n return presentation.selectionSets?.find(({ type: t }) => t === type)?.switchingSets[0]?.tracks ?? [];\n}\n\n/**\n * Find a track of the given type and id within a presentation.\n *\n * Returns the matching track from the first switching set of the matching\n * selection set, or `undefined` if either is missing. The returned track may\n * be partially resolved (URL only) or fully resolved (with segments) — callers\n * narrow as needed.\n */\nexport function findTrack(\n presentation: MaybeResolvedPresentation,\n type: TrackType,\n trackId: string\n): PartiallyResolvedTrack | ResolvedTrack | undefined {\n return getTracksByType(presentation, type).find(({ id }) => id === trackId);\n}\n\n/**\n * Find a track by id across all selection sets in a presentation, without\n * knowing its type up front. Used when the caller has a track id obtained\n * from a downstream consumer (e.g. `SourceBufferActor.initTrackId`) and\n * needs to locate the corresponding track in the presentation.\n *\n * Track ids are unique within a presentation per the HLS spec; the first\n * match wins.\n */\nexport function findTrackById(\n presentation: MaybeResolvedPresentation,\n trackId: string\n): PartiallyResolvedTrack | ResolvedTrack | undefined {\n for (const selectionSet of presentation.selectionSets ?? []) {\n const track = selectionSet.switchingSets[0]?.tracks.find(({ id }) => id === trackId);\n if (track) return track;\n }\n return undefined;\n}\n\n/**\n * Find a text track of the given id within a presentation and narrow it to\n * the fully-resolved `TextTrack` shape (segments populated). Returns\n * `undefined` if no track matches the id, the matching track isn't a text\n * track, or it hasn't been resolved yet.\n *\n * The segments-non-empty check stays at the call site — a resolved track\n * with zero segments is a valid state, distinct from \"ready to load.\"\n */\nexport function findResolvedTextTrack(\n presentation: MaybeResolvedPresentation | undefined,\n trackId: string | undefined\n): TextTrack | undefined {\n if (!presentation || !trackId) return undefined;\n const track = findTrack(presentation, 'text', trackId);\n // `findTrack` returns the wide union; narrow via discriminant before\n // applying `isResolvedTrack`'s text-specific overload.\n if (!track || track.type !== 'text' || !isResolvedTrack(track)) return undefined;\n return track;\n}\n\nexport function findResolvedVideoTrack(\n presentation: MaybeResolvedPresentation | undefined,\n trackId: string | undefined\n): VideoTrack | undefined {\n if (!presentation || !trackId) return undefined;\n const track = findTrack(presentation, 'video', trackId);\n if (!track || track.type !== 'video' || !isResolvedTrack(track)) return undefined;\n return track;\n}\n\nexport function findResolvedAudioTrack(\n presentation: MaybeResolvedPresentation | undefined,\n trackId: string | undefined\n): AudioTrack | undefined {\n if (!presentation || !trackId) return undefined;\n const track = findTrack(presentation, 'audio', trackId);\n if (!track || track.type !== 'audio' || !isResolvedTrack(track)) return undefined;\n return track;\n}\n\n/**\n * Whether a track carries a non-empty `codecs` array. Both partially-\n * resolved and fully-resolved tracks may carry codecs — they come from\n * the multivariant playlist's `EXT-X-STREAM-INF` line, not from the\n * per-type media playlist — so this works at either resolution stage.\n *\n * `TextTrack` doesn't declare a `codecs` field; the `'codecs' in track`\n * check narrows it out for the false branch.\n */\nexport function hasCodecs(track: PartiallyResolvedTrack | ResolvedTrack | undefined): boolean {\n return !!track && 'codecs' in track && !!track.codecs?.length;\n}\n\n/**\n * Set `mimeType` on every track of one `type` (immutably). Used to propagate a\n * detected container across a type's renditions: an ABR ladder is the same\n * content at different bitrates, so one rendition's container holds for all of\n * them — capability probing + SourceBuffer setup then get the right MIME for the\n * whole type from a single resolved media playlist, without fetching the rest.\n *\n * Scoped to one type on purpose: propagating *across* audio/video would be wrong\n * for mixed-container sources (e.g. muxed-TS video + raw-`.aac` audio) and races\n * concurrent per-type resolution. Same-type writes are disjoint and safe.\n * Idempotent — tracks already at `mimeType` are left as-is.\n */\nexport function applyContainerMimeType(presentation: Presentation, type: TrackType, mimeType: string): Presentation {\n return {\n ...presentation,\n selectionSets: presentation.selectionSets.map((selectionSet) =>\n selectionSet.type === type\n ? {\n ...selectionSet,\n switchingSets: selectionSet.switchingSets.map((switchingSet) => ({\n ...switchingSet,\n tracks: switchingSet.tracks.map((track) =>\n track.mimeType === mimeType ? track : { ...track, mimeType }\n ),\n })),\n }\n : selectionSet\n ),\n } as Presentation;\n}\n\n/**\n * Updates a track within a presentation (immutably). Generic — works for\n * video, audio, or text tracks.\n */\nexport function updateTrackInPresentation<T extends ResolvedTrack>(\n presentation: Presentation,\n resolvedTrack: T\n): Presentation {\n const trackId = resolvedTrack.id;\n return {\n ...presentation,\n selectionSets: presentation.selectionSets.map((selectionSet) => ({\n ...selectionSet,\n switchingSets: selectionSet.switchingSets.map((switchingSet) => ({\n ...switchingSet,\n tracks: switchingSet.tracks.map((track) => (track.id === trackId ? resolvedTrack : track)),\n })),\n })),\n } as Presentation;\n}\n"],"mappings":";;;;;;;;;;;;;;AAwBA,SAAgB,gBACd,cACA,MACqD;AACrD,QAAO,aAAa,eAAe,MAAM,EAAE,MAAM,QAAQ,MAAM,KAAK,EAAE,cAAc,IAAI,UAAU,EAAE;;;;;;;;;;AAWtG,SAAgB,UACd,cACA,MACA,SACoD;AACpD,QAAO,gBAAgB,cAAc,KAAK,CAAC,MAAM,EAAE,SAAS,OAAO,QAAQ;;;;;;;;;;;AAY7E,SAAgB,cACd,cACA,SACoD;AACpD,MAAK,MAAM,gBAAgB,aAAa,iBAAiB,EAAE,EAAE;EAC3D,MAAM,QAAQ,aAAa,cAAc,IAAI,OAAO,MAAM,EAAE,SAAS,OAAO,QAAQ;AACpF,MAAI,MAAO,QAAO;;;;;;;;;;;;AActB,SAAgB,sBACd,cACA,SACuB;AACvB,KAAI,CAAC,gBAAgB,CAAC,QAAS,QAAO,KAAA;CACtC,MAAM,QAAQ,UAAU,cAAc,QAAQ,QAAQ;AAGtD,KAAI,CAAC,SAAS,MAAM,SAAS,UAAU,CAAC,gBAAgB,MAAM,CAAE,QAAO,KAAA;AACvE,QAAO;;AAGT,SAAgB,uBACd,cACA,SACwB;AACxB,KAAI,CAAC,gBAAgB,CAAC,QAAS,QAAO,KAAA;CACtC,MAAM,QAAQ,UAAU,cAAc,SAAS,QAAQ;AACvD,KAAI,CAAC,SAAS,MAAM,SAAS,WAAW,CAAC,gBAAgB,MAAM,CAAE,QAAO,KAAA;AACxE,QAAO;;AAGT,SAAgB,uBACd,cACA,SACwB;AACxB,KAAI,CAAC,gBAAgB,CAAC,QAAS,QAAO,KAAA;CACtC,MAAM,QAAQ,UAAU,cAAc,SAAS,QAAQ;AACvD,KAAI,CAAC,SAAS,MAAM,SAAS,WAAW,CAAC,gBAAgB,MAAM,CAAE,QAAO,KAAA;AACxE,QAAO;;;;;;;;;;;AAYT,SAAgB,UAAU,OAAoE;AAC5F,QAAO,CAAC,CAAC,SAAS,YAAY,SAAS,CAAC,CAAC,MAAM,QAAQ;;;;;;;;;;;;;;AAezD,SAAgB,uBAAuB,cAA4B,MAAiB,UAAgC;AAClH,QAAO;EACL,GAAG;EACH,eAAe,aAAa,cAAc,KAAK,iBAC7C,aAAa,SAAS,OAClB;GACE,GAAG;GACH,eAAe,aAAa,cAAc,KAAK,kBAAkB;IAC/D,GAAG;IACH,QAAQ,aAAa,OAAO,KAAK,UAC/B,MAAM,aAAa,WAAW,QAAQ;KAAE,GAAG;KAAO;KAAU,CAC7D;IACF,EAAE;GACJ,GACD,aACL;EACF;;;;;;AAOH,SAAgB,0BACd,cACA,eACc;CACd,MAAM,UAAU,cAAc;AAC9B,QAAO;EACL,GAAG;EACH,eAAe,aAAa,cAAc,KAAK,kBAAkB;GAC/D,GAAG;GACH,eAAe,aAAa,cAAc,KAAK,kBAAkB;IAC/D,GAAG;IACH,QAAQ,aAAa,OAAO,KAAK,UAAW,MAAM,OAAO,UAAU,gBAAgB,MAAO;IAC3F,EAAE;GACJ,EAAE;EACJ"}
@@ -1,5 +1,5 @@
1
1
  import { applyZeroFactor, calculateEwma } from "./ewma.js";
2
- //#region src/core/abr/bandwidth-estimator.ts
2
+ //#region src/network/bandwidth-estimator.ts
3
3
  /**
4
4
  * Dual EWMA Bandwidth Estimator
5
5
  *
@@ -73,9 +73,10 @@ function sampleBandwidth(state, durationMs, numBytes, config = DEFAULT_BANDWIDTH
73
73
  * - When bandwidth drops, fast EWMA reacts first and dominates (quick adaptation)
74
74
  * - When bandwidth rises, slow EWMA lags behind and dominates (slow adaptation)
75
75
  *
76
- * Uses default estimate until enough data has been sampled.
76
+ * Uses default estimate until enough data has been sampled — and when no
77
+ * estimator state exists at all (`state === undefined`).
77
78
  *
78
- * @param state - Current estimator state
79
+ * @param state - Current estimator state, or `undefined` before any samples have been collected
79
80
  * @param defaultEstimate - Fallback estimate before sufficient samples (bps)
80
81
  * @param config - Optional estimator configuration (uses defaults if not provided)
81
82
  * @returns Bandwidth estimate in bits per second
@@ -84,12 +85,12 @@ function sampleBandwidth(state, durationMs, numBytes, config = DEFAULT_BANDWIDTH
84
85
  * const estimate = getBandwidthEstimate(state, 5_000_000); // 5 Mbps default
85
86
  */
86
87
  function getBandwidthEstimate(state, defaultEstimate, config = DEFAULT_BANDWIDTH_CONFIG) {
87
- if (state.bytesSampled < config.minTotalBytes) return defaultEstimate;
88
+ if (!state || state.bytesSampled < config.minTotalBytes) return defaultEstimate;
88
89
  const fastEstimate = applyZeroFactor(state.fastEstimate, state.fastTotalWeight, config.fastHalfLife);
89
90
  const slowEstimate = applyZeroFactor(state.slowEstimate, state.slowTotalWeight, config.slowHalfLife);
90
91
  return Math.min(fastEstimate, slowEstimate);
91
92
  }
92
93
  //#endregion
93
- export { getBandwidthEstimate, sampleBandwidth };
94
+ export { DEFAULT_BANDWIDTH_CONFIG, getBandwidthEstimate, sampleBandwidth };
94
95
 
95
96
  //# sourceMappingURL=bandwidth-estimator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bandwidth-estimator.js","names":[],"sources":["../../../src/network/bandwidth-estimator.ts"],"sourcesContent":["/**\n * Dual EWMA Bandwidth Estimator\n *\n * Estimates available bandwidth using two EWMA calculations with different\n * half-lives, taking the minimum of both. This approach (from Shaka Player):\n *\n * - **Fast EWMA** (2s half-life): Reacts quickly to bandwidth drops\n * - **Slow EWMA** (5s half-life): Provides stability during fluctuations\n * - **min(fast, slow)**: Adapts down quickly, up slowly\n *\n * This naturally provides asymmetric behavior needed for good QoE:\n * avoiding stalls (quick downgrade) while preventing oscillation (slow upgrade).\n */\n\nimport { applyZeroFactor, calculateEwma } from './ewma';\n\n/**\n * Bandwidth estimator state.\n *\n * This state structure will be managed by O1 (State Container).\n * Functions in this module operate on this state immutably.\n */\nexport interface BandwidthState {\n /** Fast-moving EWMA estimate (raw, uncorrected). */\n fastEstimate: number;\n /** Total weight accumulated in fast EWMA. */\n fastTotalWeight: number;\n /** Slow-moving EWMA estimate (raw, uncorrected). */\n slowEstimate: number;\n /** Total weight accumulated in slow EWMA. */\n slowTotalWeight: number;\n /** Total bytes sampled across all valid samples. */\n bytesSampled: number;\n}\n\n/**\n * Configuration for bandwidth estimation.\n */\nexport interface BandwidthConfig {\n /** Half-life for fast EWMA in seconds. */\n fastHalfLife: number;\n /** Half-life for slow EWMA in seconds. */\n slowHalfLife: number;\n /** Minimum total bytes before trusting the estimate. */\n minTotalBytes: number;\n /** Minimum bytes per sample to count (filters TTFB-dominated samples). */\n minBytes: number;\n /** Minimum sample duration in ms (filters cached responses). */\n minDuration: number;\n}\n\n/**\n * Default bandwidth estimator configuration.\n *\n * Values match Shaka Player defaults based on experimentation.\n */\nexport const DEFAULT_BANDWIDTH_CONFIG: BandwidthConfig = {\n fastHalfLife: 2, // 2 seconds\n slowHalfLife: 5, // 5 seconds\n minTotalBytes: 128_000, // 128 KB\n minBytes: 16_000, // 16 KB\n minDuration: 5, // 5 ms\n};\n\n/**\n * Add a bandwidth sample from a segment download.\n *\n * Samples are filtered based on:\n * - Minimum bytes (filters TTFB-dominated small segments)\n * - Minimum duration (filters cached responses)\n *\n * Valid samples update both fast and slow EWMA estimates.\n *\n * @param state - Current estimator state\n * @param durationMs - Download duration in milliseconds\n * @param numBytes - Number of bytes downloaded\n * @param config - Optional estimator configuration (uses defaults if not provided)\n * @returns New estimator state with sample incorporated (or unchanged if filtered)\n *\n * @example\n * let state = { fastEstimate: 0, fastTotalWeight: 0, ... };\n * // Sample: 1MB in 1 second\n * state = sampleBandwidth(state, 1000, 1_000_000);\n */\nexport function sampleBandwidth(\n state: BandwidthState,\n durationMs: number,\n numBytes: number,\n config: BandwidthConfig = DEFAULT_BANDWIDTH_CONFIG\n): BandwidthState {\n // Always track bytes for startup phase calculation\n const updatedBytesSampled = state.bytesSampled + numBytes;\n\n // Filter: Ignore samples below minimum bytes (TTFB-dominated)\n // Small segments' download time is mostly connection setup latency,\n // which would artificially lower our bandwidth estimate\n if (numBytes < config.minBytes) {\n return { ...state, bytesSampled: updatedBytesSampled };\n }\n\n // Filter: Ignore samples faster than minimum duration (cached responses)\n // Cached responses load nearly instantly and would artificially inflate\n // our bandwidth estimate\n if (durationMs < config.minDuration) {\n return { ...state, bytesSampled: updatedBytesSampled };\n }\n\n // Calculate bandwidth in bits per second\n // Formula: (bytes * 8 bits/byte * 1000 ms/s) / ms = bits/s\n const bandwidth = (8000 * numBytes) / durationMs;\n\n // Weight by duration in seconds\n // Longer downloads are more reliable indicators of true bandwidth\n const weight = durationMs / 1000;\n\n return {\n fastEstimate: calculateEwma(state.fastEstimate, bandwidth, weight, config.fastHalfLife),\n fastTotalWeight: state.fastTotalWeight + weight,\n slowEstimate: calculateEwma(state.slowEstimate, bandwidth, weight, config.slowHalfLife),\n slowTotalWeight: state.slowTotalWeight + weight,\n bytesSampled: updatedBytesSampled,\n };\n}\n\n/**\n * Get the current bandwidth estimate.\n *\n * Returns the **minimum** of the fast and slow EWMA estimates.\n * This provides the key asymmetric behavior:\n * - When bandwidth drops, fast EWMA reacts first and dominates (quick adaptation)\n * - When bandwidth rises, slow EWMA lags behind and dominates (slow adaptation)\n *\n * Uses default estimate until enough data has been sampled — and when no\n * estimator state exists at all (`state === undefined`).\n *\n * @param state - Current estimator state, or `undefined` before any samples have been collected\n * @param defaultEstimate - Fallback estimate before sufficient samples (bps)\n * @param config - Optional estimator configuration (uses defaults if not provided)\n * @returns Bandwidth estimate in bits per second\n *\n * @example\n * const estimate = getBandwidthEstimate(state, 5_000_000); // 5 Mbps default\n */\nexport function getBandwidthEstimate(\n state: BandwidthState | undefined,\n defaultEstimate: number,\n config: BandwidthConfig = DEFAULT_BANDWIDTH_CONFIG\n): number {\n // Use default until we have enough samples to trust our estimate\n if (!state || state.bytesSampled < config.minTotalBytes) {\n return defaultEstimate;\n }\n\n // Apply zero-factor correction to both estimates\n const fastEstimate = applyZeroFactor(state.fastEstimate, state.fastTotalWeight, config.fastHalfLife);\n\n const slowEstimate = applyZeroFactor(state.slowEstimate, state.slowTotalWeight, config.slowHalfLife);\n\n // Take the minimum - this is the key insight from Shaka Player\n // It naturally provides \"down quickly, up slowly\" behavior\n return Math.min(fastEstimate, slowEstimate);\n}\n\n/**\n * Check if the estimator has enough data to provide a reliable estimate.\n *\n * Requires both:\n * - Enough total bytes sampled (minTotalBytes threshold)\n * - At least one valid EWMA sample (totalWeight > 0)\n *\n * @param state - Current estimator state\n * @param config - Optional estimator configuration (uses defaults if not provided)\n * @returns True if we've sampled enough bytes to trust the estimate\n *\n * @example\n * if (hasGoodEstimate(state)) {\n * const estimate = getBandwidthEstimate(state, 5_000_000);\n * }\n */\nexport function hasGoodEstimate(state: BandwidthState, config: BandwidthConfig = DEFAULT_BANDWIDTH_CONFIG): boolean {\n // Need enough total bytes AND at least one valid EWMA sample\n return state.bytesSampled >= config.minTotalBytes && state.fastTotalWeight > 0 && state.slowTotalWeight > 0;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAwDA,MAAa,2BAA4C;CACvD,cAAc;CACd,cAAc;CACd,eAAe;CACf,UAAU;CACV,aAAa;CACd;;;;;;;;;;;;;;;;;;;;;AAsBD,SAAgB,gBACd,OACA,YACA,UACA,SAA0B,0BACV;CAEhB,MAAM,sBAAsB,MAAM,eAAe;AAKjD,KAAI,WAAW,OAAO,SACpB,QAAO;EAAE,GAAG;EAAO,cAAc;EAAqB;AAMxD,KAAI,aAAa,OAAO,YACtB,QAAO;EAAE,GAAG;EAAO,cAAc;EAAqB;CAKxD,MAAM,YAAa,MAAO,WAAY;CAItC,MAAM,SAAS,aAAa;AAE5B,QAAO;EACL,cAAc,cAAc,MAAM,cAAc,WAAW,QAAQ,OAAO,aAAa;EACvF,iBAAiB,MAAM,kBAAkB;EACzC,cAAc,cAAc,MAAM,cAAc,WAAW,QAAQ,OAAO,aAAa;EACvF,iBAAiB,MAAM,kBAAkB;EACzC,cAAc;EACf;;;;;;;;;;;;;;;;;;;;;AAsBH,SAAgB,qBACd,OACA,iBACA,SAA0B,0BAClB;AAER,KAAI,CAAC,SAAS,MAAM,eAAe,OAAO,cACxC,QAAO;CAIT,MAAM,eAAe,gBAAgB,MAAM,cAAc,MAAM,iBAAiB,OAAO,aAAa;CAEpG,MAAM,eAAe,gBAAgB,MAAM,cAAc,MAAM,iBAAiB,OAAO,aAAa;AAIpG,QAAO,KAAK,IAAI,cAAc,aAAa"}
@@ -1,4 +1,4 @@
1
- //#region src/dom/network/chunked-stream-iterable.ts
1
+ //#region src/network/chunked-stream-iterable.ts
2
2
  const DEFAULT_MIN_CHUNK_SIZE = 2 ** 17;
3
3
  /**
4
4
  * Adapts a `ReadableStream<Uint8Array>` (e.g. `response.body`) into an
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chunked-stream-iterable.js","names":["#readableStream"],"sources":["../../../src/network/chunked-stream-iterable.ts"],"sourcesContent":["const DEFAULT_MIN_CHUNK_SIZE = 2 ** 17; // 128 KB\n\nexport interface ChunkedStreamIterableOptions {\n minChunkSize?: number;\n}\n\n/**\n * Adapts a `ReadableStream<Uint8Array>` (e.g. `response.body`) into an\n * `AsyncIterable<Uint8Array>` that yields chunks no smaller than\n * `minChunkSize` bytes. Smaller network chunks are accumulated and yielded\n * together once the threshold is met. Any remainder is flushed on stream end.\n *\n * Errors from the underlying stream propagate naturally — the reader lock is\n * always released via `finally`.\n */\nexport class ChunkedStreamIterable implements AsyncIterable<Uint8Array> {\n readonly minChunkSize: number;\n #readableStream: ReadableStream<Uint8Array>;\n\n constructor(\n readableStream: ReadableStream<Uint8Array>,\n { minChunkSize = DEFAULT_MIN_CHUNK_SIZE }: ChunkedStreamIterableOptions = {}\n ) {\n this.#readableStream = readableStream;\n this.minChunkSize = minChunkSize;\n }\n\n async *[Symbol.asyncIterator](): AsyncGenerator<Uint8Array> {\n let pending: Uint8Array | undefined;\n const reader = this.#readableStream.getReader();\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) {\n if (pending) yield pending;\n break;\n }\n\n pending = pending ? concat(pending, value) : value;\n\n if (pending.length >= this.minChunkSize) {\n yield pending;\n pending = undefined;\n }\n }\n } finally {\n reader.releaseLock();\n }\n }\n}\n\nfunction concat(a: Uint8Array, b: Uint8Array): Uint8Array {\n const result = new Uint8Array(a.length + b.length);\n result.set(a);\n result.set(b, a.length);\n return result;\n}\n"],"mappings":";AAAA,MAAM,yBAAyB,KAAK;;;;;;;;;;AAepC,IAAa,wBAAb,MAAwE;CACtE;CACA;CAEA,YACE,gBACA,EAAE,eAAe,2BAAyD,EAAE,EAC5E;AACA,QAAA,iBAAuB;AACvB,OAAK,eAAe;;CAGtB,QAAQ,OAAO,iBAA6C;EAC1D,IAAI;EACJ,MAAM,SAAS,MAAA,eAAqB,WAAW;AAC/C,MAAI;AACF,UAAO,MAAM;IACX,MAAM,EAAE,MAAM,UAAU,MAAM,OAAO,MAAM;AAC3C,QAAI,MAAM;AACR,SAAI,QAAS,OAAM;AACnB;;AAGF,cAAU,UAAU,OAAO,SAAS,MAAM,GAAG;AAE7C,QAAI,QAAQ,UAAU,KAAK,cAAc;AACvC,WAAM;AACN,eAAU,KAAA;;;YAGN;AACR,UAAO,aAAa;;;;AAK1B,SAAS,OAAO,GAAe,GAA2B;CACxD,MAAM,SAAS,IAAI,WAAW,EAAE,SAAS,EAAE,OAAO;AAClD,QAAO,IAAI,EAAE;AACb,QAAO,IAAI,GAAG,EAAE,OAAO;AACvB,QAAO"}
@@ -1,4 +1,4 @@
1
- //#region src/core/abr/ewma.ts
1
+ //#region src/network/ewma.ts
2
2
  /**
3
3
  * Exponentially Weighted Moving Average (EWMA)
4
4
  *
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ewma.js","names":[],"sources":["../../../src/network/ewma.ts"],"sourcesContent":["/**\n * Exponentially Weighted Moving Average (EWMA)\n *\n * Pure functional implementation of EWMA calculations.\n * Based on Shaka Player's EWMA algorithm.\n */\n\n/**\n * Calculate alpha (decay factor) from half-life.\n *\n * Alpha determines how quickly old data \"expires\":\n * - alpha close to 1 = slow decay (long memory)\n * - alpha close to 0 = fast decay (short memory)\n *\n * @param halfLife - The quantity of prior samples (by weight) that make up\n * half of the new estimate. Must be positive.\n * @returns Alpha value between 0 and 1\n *\n * @example\n * const alpha = calculateAlpha(2); // ≈ 0.7071 for 2-second half-life\n */\nexport function calculateAlpha(halfLife: number): number {\n return Math.exp(Math.log(0.5) / halfLife);\n}\n\n/**\n * Calculate exponentially weighted moving average.\n *\n * Updates an estimate by blending a new value with the previous estimate,\n * weighted by the sample duration. Longer samples have more influence.\n *\n * @param prevEstimate - Previous EWMA estimate\n * @param value - New sample value to incorporate\n * @param weight - Sample weight (typically duration in seconds)\n * @param halfLife - Half-life for decay (typically 2-5 seconds)\n * @returns Updated EWMA estimate\n *\n * @example\n * let estimate = 0;\n * estimate = calculateEwma(estimate, 1_000_000, 1, 2); // First sample\n * estimate = calculateEwma(estimate, 2_000_000, 1, 2); // Second sample\n */\nexport function calculateEwma(prevEstimate: number, value: number, weight: number, halfLife: number): number {\n const alpha = calculateAlpha(halfLife);\n const adjAlpha = alpha ** weight;\n return value * (1 - adjAlpha) + adjAlpha * prevEstimate;\n}\n\n/**\n * Apply zero-factor correction to EWMA estimate.\n *\n * The zero-factor correction compensates for bias when starting from zero.\n * Without this correction, early estimates would be artificially low.\n *\n * As totalWeight increases, the correction factor approaches 1, meaning\n * the estimate becomes more reliable and needs less correction.\n *\n * @param estimate - Raw EWMA estimate (uncorrected)\n * @param totalWeight - Accumulated weight from all samples\n * @param halfLife - Half-life used in EWMA calculation\n * @returns Corrected estimate, or 0 if totalWeight is 0\n *\n * @example\n * const raw = calculateEwma(0, 1_000_000, 1, 2);\n * const corrected = applyZeroFactor(raw, 1, 2); // ≈ 1_000_000\n */\nexport function applyZeroFactor(estimate: number, totalWeight: number, halfLife: number): number {\n if (totalWeight === 0) {\n return 0;\n }\n\n const alpha = calculateAlpha(halfLife);\n const zeroFactor = 1 - alpha ** totalWeight;\n return estimate / zeroFactor;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAqBA,SAAgB,eAAe,UAA0B;AACvD,QAAO,KAAK,IAAI,KAAK,IAAI,GAAI,GAAG,SAAS;;;;;;;;;;;;;;;;;;;AAoB3C,SAAgB,cAAc,cAAsB,OAAe,QAAgB,UAA0B;CAE3G,MAAM,WADQ,eAAe,SAAS,IACZ;AAC1B,QAAO,SAAS,IAAI,YAAY,WAAW;;;;;;;;;;;;;;;;;;;;AAqB7C,SAAgB,gBAAgB,UAAkB,aAAqB,UAA0B;AAC/F,KAAI,gBAAgB,EAClB,QAAO;AAKT,QAAO,YADY,IADL,eAAe,SAAS,IACN"}
@@ -0,0 +1,115 @@
1
+ import { sampleBandwidth } from "./bandwidth-estimator.js";
2
+ import { ChunkedStreamIterable } from "./chunked-stream-iterable.js";
3
+ //#region src/network/fetch.ts
4
+ /**
5
+ * HTTP Fetch Wrapper
6
+ *
7
+ * Composable building blocks:
8
+ * - fetchResolvable() — fetch a Resource (handles byte ranges); returns Response
9
+ * - getResponseText() — extract text from Response
10
+ * - fetchResolvableStream() — single-stage async generator over body chunks
11
+ * - fetchStream() — two-stage: await connection establishment, then lazily
12
+ * iterate body chunks. Use when timing the connection start independently
13
+ * of body consumption matters (e.g., observable fetch timing for ABR).
14
+ * - createTrackedFetch() — factory for a fetchStream-shape function that
15
+ * samples bandwidth (via EWMA) per chunk and notifies via callback.
16
+ */
17
+ /**
18
+ * Fetch resolvable from a Resource.
19
+ *
20
+ * Handles byte range requests if byteRange is present.
21
+ * Returns native fetch Response for composability (can extract text, stream, etc.).
22
+ *
23
+ * @param addressable - Resource to fetch (url + optional byteRange)
24
+ * @returns Promise resolving to Response
25
+ *
26
+ * @example
27
+ * const response = await fetchResolvable({ url: 'https://example.com/segment.m4s' });
28
+ * const text = await getResponseText(response);
29
+ *
30
+ * @example
31
+ * // With byte range
32
+ * const response = await fetchResolvable({
33
+ * url: 'https://example.com/file.mp4',
34
+ * byteRange: { start: 1000, end: 1999 }
35
+ * });
36
+ */
37
+ async function fetchResolvable(addressable, options) {
38
+ const headers = new Headers(options?.headers);
39
+ if (addressable.byteRange) {
40
+ const { start, end } = addressable.byteRange;
41
+ headers.set("Range", `bytes=${start}-${end}`);
42
+ }
43
+ const request = new Request(addressable.url, {
44
+ method: "GET",
45
+ headers,
46
+ ...options
47
+ });
48
+ return fetch(request);
49
+ }
50
+ /**
51
+ * Extract text from Response.
52
+ *
53
+ * Accepts minimal Response-like object (just needs text() method).
54
+ * Returns promise from response.text().
55
+ *
56
+ * @param response - Response-like object with text() method
57
+ * @returns Promise resolving to text content
58
+ *
59
+ * @example
60
+ * const response = await fetchResolvable(addressable);
61
+ * const text = await getResponseText(response);
62
+ */
63
+ function getResponseText(response) {
64
+ return response.text();
65
+ }
66
+ /** Default {@link FetchText}: fetch the resource, reject on non-OK, return text. */
67
+ const fetchResolvableText = async (addressable, options) => {
68
+ const response = await fetchResolvable(addressable, options);
69
+ if (!response.ok) throw new Error(`fetchResolvableText: ${response.status} ${response.statusText} for ${addressable.url}`);
70
+ return getResponseText(response);
71
+ };
72
+ async function fetchStream(addressable, options) {
73
+ const { minChunkSize, ...fetchOptions } = options ?? {};
74
+ const response = await fetchResolvable(addressable, fetchOptions);
75
+ if (!response.body) throw new Error("Response has no body");
76
+ return new ChunkedStreamIterable(response.body, ...minChunkSize !== void 0 ? [{ minChunkSize }] : []);
77
+ }
78
+ /**
79
+ * Returns a {@link FetchBytes} function that samples bandwidth via EWMA
80
+ * per body chunk. The factory captures the running bandwidth state
81
+ * internally; per chunk it computes the next state and notifies the
82
+ * supplied `onSample` callback.
83
+ *
84
+ * The factory's internal accumulator is seeded from `initial` and updated
85
+ * on every chunk; callers don't need to thread it back in. `onSample`
86
+ * receives the *new* state after each chunk — typical use is to bridge
87
+ * samples back into engine state for ABR consumers.
88
+ *
89
+ * @param initial - Starting `BandwidthState` (commonly zeros or the
90
+ * engine's current accumulator).
91
+ * @param onSample - Called with the new `BandwidthState` after each chunk.
92
+ */
93
+ function createTrackedFetch(initial, onSample) {
94
+ let state = initial;
95
+ return async (addressable, options) => {
96
+ const { minChunkSize, ...fetchOptions } = options ?? {};
97
+ const response = await fetchResolvable(addressable, fetchOptions);
98
+ if (!response.body) throw new Error("Response has no body");
99
+ const body = response.body;
100
+ return { [Symbol.asyncIterator]: async function* () {
101
+ let chunkStart = performance.now();
102
+ for await (const chunk of new ChunkedStreamIterable(body, ...minChunkSize !== void 0 ? [{ minChunkSize }] : [])) {
103
+ const elapsed = performance.now() - chunkStart;
104
+ state = sampleBandwidth(state, elapsed, chunk.byteLength);
105
+ onSample(state);
106
+ yield chunk;
107
+ chunkStart = performance.now();
108
+ }
109
+ } };
110
+ };
111
+ }
112
+ //#endregion
113
+ export { createTrackedFetch, fetchResolvable, fetchResolvableText, fetchStream, getResponseText };
114
+
115
+ //# sourceMappingURL=fetch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fetch.js","names":[],"sources":["../../../src/network/fetch.ts"],"sourcesContent":["/**\n * HTTP Fetch Wrapper\n *\n * Composable building blocks:\n * - fetchResolvable() — fetch a Resource (handles byte ranges); returns Response\n * - getResponseText() — extract text from Response\n * - fetchResolvableStream() — single-stage async generator over body chunks\n * - fetchStream() — two-stage: await connection establishment, then lazily\n * iterate body chunks. Use when timing the connection start independently\n * of body consumption matters (e.g., observable fetch timing for ABR).\n * - createTrackedFetch() — factory for a fetchStream-shape function that\n * samples bandwidth (via EWMA) per chunk and notifies via callback.\n */\n\nimport { type BandwidthState, sampleBandwidth } from './bandwidth-estimator';\nimport { ChunkedStreamIterable, type ChunkedStreamIterableOptions } from './chunked-stream-iterable';\n\n/**\n * Minimal Response-like interface for text extraction.\n * Allows testing without full Response object.\n */\nexport interface ResponseLike {\n text(): Promise<string>;\n}\n\n/**\n * An HTTP-addressable resource — URL plus optional byte range.\n * Media's `AddressableObject` (and anything else with the same shape)\n * is structurally compatible; kept local so this module stays\n * domain-agnostic.\n */\nexport interface Resource {\n url: string;\n byteRange?: {\n start: number;\n end: number;\n };\n}\n\n/**\n * Fetch resolvable from a Resource.\n *\n * Handles byte range requests if byteRange is present.\n * Returns native fetch Response for composability (can extract text, stream, etc.).\n *\n * @param addressable - Resource to fetch (url + optional byteRange)\n * @returns Promise resolving to Response\n *\n * @example\n * const response = await fetchResolvable({ url: 'https://example.com/segment.m4s' });\n * const text = await getResponseText(response);\n *\n * @example\n * // With byte range\n * const response = await fetchResolvable({\n * url: 'https://example.com/file.mp4',\n * byteRange: { start: 1000, end: 1999 }\n * });\n */\nexport async function fetchResolvable(addressable: Resource, options?: RequestInit): Promise<Response> {\n const headers = new Headers(options?.headers);\n\n // Add Range header for byte range requests\n if (addressable.byteRange) {\n const { start, end } = addressable.byteRange;\n headers.set('Range', `bytes=${start}-${end}`);\n }\n\n const request = new Request(addressable.url, {\n method: 'GET',\n headers,\n ...options,\n });\n\n return fetch(request);\n}\n\n/**\n * Fetch resolvable as bytes.\n *\n * Convenience wrapper around fetchResolvable that resolves the body as an\n * ArrayBuffer. Use this when you need the raw bytes (e.g. segment appends).\n * For text or streaming consumption, use fetchResolvable directly.\n */\nexport async function fetchResolvableBytes(addressable: Resource, options?: RequestInit): Promise<ArrayBuffer> {\n const response = await fetchResolvable(addressable, options);\n return response.arrayBuffer();\n}\n\n/**\n * Fetch resolvable as a stream of Uint8Array chunks.\n *\n * Convenience wrapper around fetchResolvable that yields the body as chunks\n * via ChunkedStreamIterable. Headers are awaited before the first chunk is\n * yielded (TTFB is accounted for before iteration begins).\n *\n * Throws if the response body is null (e.g. non-body HTTP status).\n * Errors from the underlying stream propagate naturally as thrown errors.\n */\nexport async function* fetchResolvableStream(\n addressable: Resource,\n options?: RequestInit & ChunkedStreamIterableOptions\n): AsyncGenerator<Uint8Array> {\n const { minChunkSize, ...fetchOptions } = options ?? {};\n const response = await fetchResolvable(addressable, fetchOptions);\n if (!response.body) throw new Error('Response has no body');\n yield* new ChunkedStreamIterable(response.body, ...(minChunkSize !== undefined ? [{ minChunkSize }] : []));\n}\n\n/**\n * Extract text from Response.\n *\n * Accepts minimal Response-like object (just needs text() method).\n * Returns promise from response.text().\n *\n * @param response - Response-like object with text() method\n * @returns Promise resolving to text content\n *\n * @example\n * const response = await fetchResolvable(addressable);\n * const text = await getResponseText(response);\n */\nexport function getResponseText(response: ResponseLike): Promise<string> {\n return response.text();\n}\n\n/**\n * Fetch a resource and resolve its text body — the text analog of\n * {@link FetchBytes}. A non-OK status rejects, so HTTP failures surface as\n * rejections that callers (and decorators like the failover tracker) handle\n * uniformly with network errors.\n */\nexport type FetchText = (addressable: Resource, options?: RequestInit) => Promise<string>;\n\n/** Default {@link FetchText}: fetch the resource, reject on non-OK, return text. */\nexport const fetchResolvableText: FetchText = async (addressable, options) => {\n const response = await fetchResolvable(addressable, options);\n if (!response.ok) {\n throw new Error(`fetchResolvableText: ${response.status} ${response.statusText} for ${addressable.url}`);\n }\n return getResponseText(response);\n};\n\n/**\n * Two-stage fetch helper: eagerly starts the HTTP request (TTFB is awaited),\n * then returns a lazy iterable over the response body. Separating connection\n * start from body iteration makes fetch timing predictable and observable\n * regardless of when downstream consumers begin pulling chunks.\n *\n * Sibling to {@link fetchResolvableStream}, which is single-stage (calls\n * `fetch` only when iteration starts). Pick `fetchStream` when \"when did the\n * fetch start\" needs to be observable separately from \"when did the body\n * begin arriving.\"\n */\nexport type FetchOptions = RequestInit & ChunkedStreamIterableOptions;\n\nexport type FetchBytes = (addressable: Resource, options?: FetchOptions) => Promise<AsyncIterable<Uint8Array>>;\n\nexport async function fetchStream(addressable: Resource, options?: FetchOptions): Promise<AsyncIterable<Uint8Array>> {\n const { minChunkSize, ...fetchOptions } = options ?? {};\n const response = await fetchResolvable(addressable, fetchOptions);\n if (!response.body) throw new Error('Response has no body');\n return new ChunkedStreamIterable(response.body, ...(minChunkSize !== undefined ? [{ minChunkSize }] : []));\n}\n\n/**\n * Returns a {@link FetchBytes} function that samples bandwidth via EWMA\n * per body chunk. The factory captures the running bandwidth state\n * internally; per chunk it computes the next state and notifies the\n * supplied `onSample` callback.\n *\n * The factory's internal accumulator is seeded from `initial` and updated\n * on every chunk; callers don't need to thread it back in. `onSample`\n * receives the *new* state after each chunk — typical use is to bridge\n * samples back into engine state for ABR consumers.\n *\n * @param initial - Starting `BandwidthState` (commonly zeros or the\n * engine's current accumulator).\n * @param onSample - Called with the new `BandwidthState` after each chunk.\n */\nexport function createTrackedFetch(initial: BandwidthState, onSample: (next: BandwidthState) => void): FetchBytes {\n let state = initial;\n return async (addressable, options) => {\n const { minChunkSize, ...fetchOptions } = options ?? {};\n const response = await fetchResolvable(addressable, fetchOptions);\n if (!response.body) throw new Error('Response has no body');\n const body = response.body;\n return {\n [Symbol.asyncIterator]: async function* () {\n let chunkStart = performance.now();\n for await (const chunk of new ChunkedStreamIterable(\n body,\n ...(minChunkSize !== undefined ? [{ minChunkSize }] : [])\n )) {\n const elapsed = performance.now() - chunkStart;\n state = sampleBandwidth(state, elapsed, chunk.byteLength);\n onSample(state);\n yield chunk;\n chunkStart = performance.now();\n }\n },\n };\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2DA,eAAsB,gBAAgB,aAAuB,SAA0C;CACrG,MAAM,UAAU,IAAI,QAAQ,SAAS,QAAQ;AAG7C,KAAI,YAAY,WAAW;EACzB,MAAM,EAAE,OAAO,QAAQ,YAAY;AACnC,UAAQ,IAAI,SAAS,SAAS,MAAM,GAAG,MAAM;;CAG/C,MAAM,UAAU,IAAI,QAAQ,YAAY,KAAK;EAC3C,QAAQ;EACR;EACA,GAAG;EACJ,CAAC;AAEF,QAAO,MAAM,QAAQ;;;;;;;;;;;;;;;AAgDvB,SAAgB,gBAAgB,UAAyC;AACvE,QAAO,SAAS,MAAM;;;AAYxB,MAAa,sBAAiC,OAAO,aAAa,YAAY;CAC5E,MAAM,WAAW,MAAM,gBAAgB,aAAa,QAAQ;AAC5D,KAAI,CAAC,SAAS,GACZ,OAAM,IAAI,MAAM,wBAAwB,SAAS,OAAO,GAAG,SAAS,WAAW,OAAO,YAAY,MAAM;AAE1G,QAAO,gBAAgB,SAAS;;AAkBlC,eAAsB,YAAY,aAAuB,SAA4D;CACnH,MAAM,EAAE,cAAc,GAAG,iBAAiB,WAAW,EAAE;CACvD,MAAM,WAAW,MAAM,gBAAgB,aAAa,aAAa;AACjE,KAAI,CAAC,SAAS,KAAM,OAAM,IAAI,MAAM,uBAAuB;AAC3D,QAAO,IAAI,sBAAsB,SAAS,MAAM,GAAI,iBAAiB,KAAA,IAAY,CAAC,EAAE,cAAc,CAAC,GAAG,EAAE,CAAE;;;;;;;;;;;;;;;;;AAkB5G,SAAgB,mBAAmB,SAAyB,UAAsD;CAChH,IAAI,QAAQ;AACZ,QAAO,OAAO,aAAa,YAAY;EACrC,MAAM,EAAE,cAAc,GAAG,iBAAiB,WAAW,EAAE;EACvD,MAAM,WAAW,MAAM,gBAAgB,aAAa,aAAa;AACjE,MAAI,CAAC,SAAS,KAAM,OAAM,IAAI,MAAM,uBAAuB;EAC3D,MAAM,OAAO,SAAS;AACtB,SAAO,EACL,CAAC,OAAO,gBAAgB,mBAAmB;GACzC,IAAI,aAAa,YAAY,KAAK;AAClC,cAAW,MAAM,SAAS,IAAI,sBAC5B,MACA,GAAI,iBAAiB,KAAA,IAAY,CAAC,EAAE,cAAc,CAAC,GAAG,EAAE,CACzD,EAAE;IACD,MAAM,UAAU,YAAY,KAAK,GAAG;AACpC,YAAQ,gBAAgB,OAAO,SAAS,MAAM,WAAW;AACzD,aAAS,MAAM;AACf,UAAM;AACN,iBAAa,YAAY,KAAK;;KAGnC"}
@@ -1,9 +1,11 @@
1
- import { calculateForwardFlushPoint, getSegmentsToLoad } from "../../core/buffer/forward-buffer.js";
2
- import { effect } from "../../core/signals/effect.js";
3
- import { calculateBackBufferFlushPoint } from "../../core/buffer/back-buffer.js";
4
- import { createMachineActor } from "../../core/create-machine-actor.js";
5
- import { SerialRunner, Task } from "../../core/task.js";
6
- //#region src/dom/features/segment-loader-actor.ts
1
+ import { peek } from "../../../core/signals/primitives.js";
2
+ import { effect } from "../../../core/signals/effect.js";
3
+ import { SerialRunner, Task } from "../../../core/tasks/task.js";
4
+ import { createMachineActor } from "../../../core/actors/create-machine-actor.js";
5
+ import { DEFAULT_FORWARD_BUFFER_CONFIG, calculateForwardFlushPoint, getSegmentsToLoad } from "../../../media/buffer/forward-buffer.js";
6
+ import "../../../media/types/index.js";
7
+ import { DEFAULT_BACK_BUFFER_CONFIG, calculateBackBufferFlushPoint } from "../../../media/buffer/back-buffer.js";
8
+ //#region src/playback/actors/dom/segment-loader.ts
7
9
  /**
8
10
  * Resolves when the SourceBufferActor snapshot reaches 'idle'.
9
11
  * Rejects if the signal is aborted or the actor is destroyed.
@@ -122,9 +124,17 @@ function makeLoadTask(op, { getContext, setContext, fetchBytes, sourceBufferActo
122
124
  * Accepts an optional `minChunkSize` in options; init segments pass `Infinity`
123
125
  * so the entire body accumulates as one chunk before appending.
124
126
  */
125
- function createSegmentLoaderActor(sourceBufferActor, fetchBytes) {
127
+ function createSegmentLoaderActor(sourceBufferActor, fetchBytes, config = {}) {
128
+ const forwardBufferConfig = {
129
+ ...DEFAULT_FORWARD_BUFFER_CONFIG,
130
+ ...config.forwardBuffer
131
+ };
132
+ const backBufferConfig = {
133
+ ...DEFAULT_BACK_BUFFER_CONFIG,
134
+ ...config.backBuffer
135
+ };
126
136
  const getBufferedSegments = (allSegments) => {
127
- const bufferedIds = new Set(sourceBufferActor.snapshot.get().context.segments.filter((s) => !s.partial).map((s) => s.id));
137
+ const bufferedIds = new Set(peek(sourceBufferActor.snapshot).context.segments.filter((s) => !s.partial).map((s) => s.id));
128
138
  return allSegments.filter((s) => bufferedIds.has(s.id));
129
139
  };
130
140
  /**
@@ -134,9 +144,19 @@ function createSegmentLoaderActor(sourceBufferActor, fetchBytes) {
134
144
  * @todo Rename alongside LoadTask (e.g. planOps).
135
145
  *
136
146
  * Case 1 — Removes: forward and back buffer flush points, segment-aligned.
137
- * No flush on track switch: appending new content overwrites existing buffer
138
- * ranges, and the actor's time-aligned deduplication keeps the segment model
139
- * accurate as new segments arrive.
147
+ * ABR-style track switches (same content, different bitrate) do not flush:
148
+ * appending new content overwrites existing buffer ranges, and the actor's
149
+ * time-aligned deduplication keeps the segment model accurate as new
150
+ * segments arrive.
151
+ *
152
+ * Cross-rendition track switches (audio language change, text language
153
+ * change) do flush: the buffered content is semantically incompatible with
154
+ * the newly-selected track, so overwrite-on-append would leave stale
155
+ * content playing until each replacement segment lands. Today's predicate:
156
+ * `actorCtx.initTrackLanguage !== track.language` — fires for language
157
+ * changes, no-ops for video / same-language audio bitrate switches.
158
+ * Future stage: pluggable predicate / strategy at actor construction time
159
+ * for codec-change (5.1 surround) and other cross-rendition shapes.
140
160
  *
141
161
  * Case 2 — Init: schedule if not yet committed for this track.
142
162
  *
@@ -144,34 +164,54 @@ function createSegmentLoaderActor(sourceBufferActor, fetchBytes) {
144
164
  */
145
165
  const planTasks = (message) => {
146
166
  const { track, range } = message;
147
- const actorCtx = sourceBufferActor.snapshot.get().context;
167
+ const actorCtx = peek(sourceBufferActor.snapshot).context;
148
168
  const bufferedSegments = getBufferedSegments(track.segments);
149
169
  const currentTime = range?.start ?? 0;
150
170
  const tasks = [];
171
+ const isCrossRenditionSwitch = actorCtx.initTrackId !== void 0 && actorCtx.initTrackId !== track.id && actorCtx.initTrackLanguage !== track.language;
172
+ const removes = [];
173
+ const staleRanges = [];
151
174
  if (range) {
152
- const forwardFlushStart = calculateForwardFlushPoint(bufferedSegments, currentTime);
153
- if (forwardFlushStart < Infinity) tasks.push({
154
- type: "remove",
175
+ if (isCrossRenditionSwitch) {
176
+ const staleStart = actorCtx.segments.find((s) => s.startTime <= currentTime && s.startTime + s.duration > currentTime)?.startTime ?? actorCtx.segments.find((s) => s.startTime > currentTime)?.startTime;
177
+ if (staleStart !== void 0) staleRanges.push({
178
+ start: staleStart,
179
+ end: Infinity
180
+ });
181
+ }
182
+ const forwardFlushStart = calculateForwardFlushPoint(bufferedSegments, currentTime, forwardBufferConfig);
183
+ if (forwardFlushStart < Infinity) removes.push({
155
184
  start: forwardFlushStart,
156
185
  end: Infinity
157
186
  });
158
- const backFlushEnd = calculateBackBufferFlushPoint(bufferedSegments, currentTime);
159
- if (backFlushEnd > 0) tasks.push({
160
- type: "remove",
187
+ const backFlushEnd = calculateBackBufferFlushPoint(bufferedSegments, currentTime, backBufferConfig);
188
+ if (backFlushEnd > 0) removes.push({
161
189
  start: 0,
162
190
  end: backFlushEnd
163
191
  });
192
+ for (const r of removes) tasks.push({
193
+ type: "remove",
194
+ start: r.start,
195
+ end: r.end
196
+ });
164
197
  }
198
+ const overlapsStale = (seg) => {
199
+ const segEnd = seg.startTime + seg.duration;
200
+ return removes.some((r) => seg.startTime < r.end && segEnd > r.start) || staleRanges.some((r) => seg.startTime < r.end && segEnd > r.start);
201
+ };
202
+ const effectiveBuffered = removes.length + staleRanges.length > 0 ? bufferedSegments.filter((s) => !overlapsStale(s)) : bufferedSegments;
165
203
  if (actorCtx.initTrackId !== track.id) tasks.push({
166
204
  type: "append-init",
167
- meta: { trackId: track.id },
205
+ meta: {
206
+ trackId: track.id,
207
+ language: track.language
208
+ },
168
209
  url: track.initialization.url,
169
210
  ...track.initialization.byteRange !== void 0 && { byteRange: track.initialization.byteRange }
170
211
  });
171
212
  if (range) {
172
- const EPSILON = 1e-4;
173
- const segmentsToLoad = getSegmentsToLoad(track.segments, bufferedSegments, currentTime).filter((seg) => {
174
- const existing = actorCtx.segments.find((s) => Math.abs(s.startTime - seg.startTime) < EPSILON);
213
+ const segmentsToLoad = getSegmentsToLoad(track.segments, effectiveBuffered, currentTime, forwardBufferConfig).filter((seg) => {
214
+ const existing = actorCtx.segments.find((s) => !overlapsStale(s) && Math.abs(s.startTime - seg.startTime) < 1e-4);
175
215
  if (existing?.partial) return true;
176
216
  if (!existing?.trackBandwidth || !track.bandwidth) return true;
177
217
  return track.bandwidth > existing.trackBandwidth;
@@ -240,4 +280,4 @@ function createSegmentLoaderActor(sourceBufferActor, fetchBytes) {
240
280
  //#endregion
241
281
  export { createSegmentLoaderActor };
242
282
 
243
- //# sourceMappingURL=segment-loader-actor.js.map
283
+ //# sourceMappingURL=segment-loader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"segment-loader.js","names":[],"sources":["../../../../../src/playback/actors/dom/segment-loader.ts"],"sourcesContent":["import { createMachineActor, type HandlerContext, type MessageActor } from '../../../core/actors/create-machine-actor';\nimport { effect } from '../../../core/signals/effect';\nimport { peek } from '../../../core/signals/primitives';\nimport { SerialRunner, Task } from '../../../core/tasks/task';\nimport {\n type BackBufferConfig,\n calculateBackBufferFlushPoint,\n DEFAULT_BACK_BUFFER_CONFIG,\n} from '../../../media/buffer/back-buffer';\nimport {\n calculateForwardFlushPoint,\n DEFAULT_FORWARD_BUFFER_CONFIG,\n type ForwardBufferConfig,\n getSegmentsToLoad,\n} from '../../../media/buffer/forward-buffer';\nimport {\n type AddressableObject,\n type AudioTrack,\n SEGMENT_TIME_EPSILON,\n type Segment,\n type VideoTrack,\n} from '../../../media/types';\nimport type { AppendInitMessage, AppendSegmentMessage, RemoveMessage, SourceBufferActor } from './source-buffer';\n\n// ============================================================================\n// BUFFER STATE TYPES\n// ============================================================================\n\n/**\n * Buffer state for a single SourceBuffer.\n * Tracks which init segment and media segments are loaded.\n */\nexport interface SourceBufferState {\n /** Track ID of the loaded init segment */\n initTrackId?: string;\n /** Loaded media segments (unordered - selectors derive ordering) */\n segments: Array<{ id: string; trackId: string }>;\n}\n\n/**\n * Buffer state for all SourceBuffers.\n */\nexport interface BufferState {\n video?: SourceBufferState;\n audio?: SourceBufferState;\n}\n\n// ============================================================================\n// MESSAGE PROTOCOL\n// ============================================================================\n\n/** Track types that have SourceBuffers (video and audio only). */\nexport type SegmentLoaderTrack = VideoTrack | AudioTrack;\n\n/**\n * Message sent to a SegmentLoaderActor.\n *\n * `range` is optional to distinguish loading modes:\n * - No range: load init segment only (metadata preload mode)\n * - With range: load init + all segments overlapping [start, end]\n *\n * `start` and `end` are raw time values — no segment snapping.\n * The actor maps them onto segment boundaries internally.\n */\nexport type SegmentLoaderMessage = {\n type: 'load';\n track: SegmentLoaderTrack;\n range?: { start: number; end: number };\n};\n\n// ============================================================================\n// LOAD TASK\n// ============================================================================\n\n/**\n * A LoadTask is the intent to perform one unit of SegmentLoader work.\n * Unlike SourceBufferMessage, fetch-based tasks carry a URL rather than\n * pre-fetched data — the runner fetches and appends them in sequence.\n *\n * Derived from SourceBufferMessage types by removing `data` and adding\n * a fetch URL via AddressableObject.\n *\n * @todo Rename — \"LoadTask\" risks confusion with the `Task` class used for\n * SourceBufferActor scheduling. These are closer to operation descriptors or\n * messages than tasks in that sense.\n */\nexport type LoadTask =\n | (Omit<AppendInitMessage, 'data'> & AddressableObject)\n | (Omit<AppendSegmentMessage, 'data'> & AddressableObject)\n | RemoveMessage;\n\n// ============================================================================\n// ACTOR INTERFACE\n// ============================================================================\n\n/** Finite states of the actor. */\nexport type SegmentLoaderActorState = 'idle' | 'loading' | 'destroyed';\n\n/** Non-finite (extended) data managed by the actor. */\nexport interface SegmentLoaderActorContext {\n /** Track ID of the init segment currently being fetched/appended, or null. */\n inFlightInitTrackId: string | null;\n /** Segment ID currently being fetched/appended, or null. */\n inFlightSegmentId: string | null;\n}\n\nexport type SegmentLoaderActor = MessageActor<SegmentLoaderActorState, SegmentLoaderActorContext, SegmentLoaderMessage>;\n\n/**\n * Configuration for `createSegmentLoaderActor`. Each sub-config is\n * spread over the corresponding `DEFAULT_*_CONFIG` so callers can\n * override individual fields.\n */\nexport interface SegmentLoaderActorConfig {\n forwardBuffer?: Partial<ForwardBufferConfig>;\n backBuffer?: Partial<BackBufferConfig>;\n}\n\n// ============================================================================\n// HELPERS\n// ============================================================================\n\ntype FetchBytes = (\n addressable: AddressableObject,\n options?: RequestInit & { minChunkSize?: number }\n) => Promise<AsyncIterable<Uint8Array>>;\n\n/**\n * Resolves when the SourceBufferActor snapshot reaches 'idle'.\n * Rejects if the signal is aborted or the actor is destroyed.\n *\n * Used to sequence SourceBufferActor operations without awaiting send()\n * directly — send() is fire-and-forget; callers observe completion via\n * state transition.\n */\nfunction waitForIdle(snapshot: SourceBufferActor['snapshot'], signal: AbortSignal): Promise<void> {\n return new Promise((resolve, reject) => {\n if (snapshot.get().value === 'idle') {\n resolve();\n return;\n }\n if (snapshot.get().value === 'destroyed') {\n reject(new DOMException('Aborted', 'AbortError'));\n return;\n }\n if (signal.aborted) {\n reject(signal.reason);\n return;\n }\n\n let stop: (() => void) | undefined;\n\n const cleanup = (fn: () => void) => {\n stop?.();\n signal.removeEventListener('abort', onAbort);\n fn();\n };\n\n const onAbort = () => cleanup(() => reject(signal.reason));\n\n stop = effect(() => {\n const value = snapshot.get().value;\n if (value === 'idle') cleanup(resolve);\n else if (value === 'destroyed') cleanup(() => reject(new DOMException('Aborted', 'AbortError')));\n });\n\n signal.addEventListener('abort', onAbort, { once: true });\n });\n}\n\n// ============================================================================\n// LOAD TASK FACTORY\n// ============================================================================\n\ninterface LoadTaskOptions {\n getContext: () => SegmentLoaderActorContext;\n setContext: (ctx: SegmentLoaderActorContext) => void;\n fetchBytes: FetchBytes;\n sourceBufferActor: SourceBufferActor;\n}\n\n/**\n * Wraps a LoadTask descriptor into a Task that fetches (if needed) and\n * forwards to SourceBufferActor. Updates in-flight context around async\n * operations so the loading handler can make accurate continue/preempt\n * decisions at any point.\n */\nfunction makeLoadTask(\n op: LoadTask,\n { getContext, setContext, fetchBytes, sourceBufferActor }: LoadTaskOptions\n): Task<void> {\n return new Task(async (taskSignal) => {\n if (taskSignal.aborted) return;\n\n if (op.type === 'remove') {\n sourceBufferActor.send(op);\n await waitForIdle(sourceBufferActor.snapshot, taskSignal);\n return;\n }\n\n if (op.type === 'append-init') {\n setContext({ ...getContext(), inFlightInitTrackId: op.meta.trackId });\n try {\n // Init segments are small and need the full body before appending.\n // minChunkSize: Infinity accumulates all chunks into one before yielding.\n const data = await fetchBytes(op, { signal: taskSignal, minChunkSize: Infinity });\n if (!taskSignal.aborted) {\n sourceBufferActor.send({ type: 'append-init', data, meta: op.meta });\n await waitForIdle(sourceBufferActor.snapshot, taskSignal);\n }\n } finally {\n setContext({ ...getContext(), inFlightInitTrackId: null });\n }\n return;\n }\n\n // append-segment: await headers eagerly (starts the HTTP connection and\n // records the fetch in observers like tests), then pass the body stream\n // directly to the actor so chunks are appended as they arrive.\n setContext({ ...getContext(), inFlightSegmentId: op.meta.id });\n try {\n const stream = await fetchBytes(op, { signal: taskSignal });\n if (!taskSignal.aborted) {\n sourceBufferActor.send({ type: 'append-segment', data: stream, meta: op.meta });\n await waitForIdle(sourceBufferActor.snapshot, taskSignal);\n }\n } finally {\n setContext({ ...getContext(), inFlightSegmentId: null });\n }\n });\n}\n\n// ============================================================================\n// IMPLEMENTATION\n// ============================================================================\n\n/**\n * Creates a SegmentLoaderActor for one track type (video or audio).\n *\n * Receives load assignments via `send()` and owns all execution: planning,\n * removes, fetches, and appends. Coordinates with the SourceBufferActor for\n * all physical SourceBuffer operations.\n *\n * Planning (Cases 1–3) happens in the `load` handler on every incoming\n * message, producing an ordered LoadTask list. The runner drains that list\n * sequentially via SerialRunner. When a new message arrives mid-run, the\n * handler replans and either continues the in-flight operation (abortPending\n * + schedule new remainder) or preempts it (abortAll + cancel SourceBuffer\n * if needed + schedule new plan).\n *\n * @param sourceBufferActor - Shared SourceBufferActor reference (not owned)\n * @param fetchBytes - Tracked fetch closure (owns throughput sampling for segments).\n * Accepts an optional `minChunkSize` in options; init segments pass `Infinity`\n * so the entire body accumulates as one chunk before appending.\n */\nexport function createSegmentLoaderActor(\n sourceBufferActor: SourceBufferActor,\n fetchBytes: FetchBytes,\n config: SegmentLoaderActorConfig = {}\n): SegmentLoaderActor {\n type UserState = Exclude<SegmentLoaderActorState, 'destroyed'>;\n type Ctx = HandlerContext<UserState, SegmentLoaderActorContext, () => SerialRunner>;\n\n const forwardBufferConfig: ForwardBufferConfig = { ...DEFAULT_FORWARD_BUFFER_CONFIG, ...config.forwardBuffer };\n const backBufferConfig: BackBufferConfig = { ...DEFAULT_BACK_BUFFER_CONFIG, ...config.backBuffer };\n\n const getBufferedSegments = (allSegments: readonly Segment[]): Segment[] => {\n // Exclude partial segments — they are still being streamed and must not be\n // treated as fully buffered for load planning or buffer window calculations.\n //\n // `peek` defensively: `load` handlers run synchronously inside `send()`,\n // which is called from inside the dispatcher reactor's `effects:` body.\n // A tracked `.snapshot.get()` here would leak the source-buffer-actor's\n // snapshot into the dispatcher's dep set, causing the dispatcher to re-\n // fire on every SourceBufferActor state change. Mirrors the fix applied\n // to the text-track loader in `b3f44efe`.\n const bufferedIds = new Set(\n peek(sourceBufferActor.snapshot)\n .context.segments.filter((s) => !s.partial)\n .map((s) => s.id)\n );\n return allSegments.filter((s) => bufferedIds.has(s.id));\n };\n\n /**\n * Translate a load message into an ordered LoadTask list based on committed\n * actor state. In-flight awareness is handled separately in the load handler.\n *\n * @todo Rename alongside LoadTask (e.g. planOps).\n *\n * Case 1 — Removes: forward and back buffer flush points, segment-aligned.\n * ABR-style track switches (same content, different bitrate) do not flush:\n * appending new content overwrites existing buffer ranges, and the actor's\n * time-aligned deduplication keeps the segment model accurate as new\n * segments arrive.\n *\n * Cross-rendition track switches (audio language change, text language\n * change) do flush: the buffered content is semantically incompatible with\n * the newly-selected track, so overwrite-on-append would leave stale\n * content playing until each replacement segment lands. Today's predicate:\n * `actorCtx.initTrackLanguage !== track.language` — fires for language\n * changes, no-ops for video / same-language audio bitrate switches.\n * Future stage: pluggable predicate / strategy at actor construction time\n * for codec-change (5.1 surround) and other cross-rendition shapes.\n *\n * Case 2 — Init: schedule if not yet committed for this track.\n *\n * Case 3 — Segments: all segments in the load window not yet committed.\n */\n const planTasks = (message: SegmentLoaderMessage): LoadTask[] => {\n const { track, range } = message;\n // `peek` for the same reason as `getBufferedSegments` above — avoid\n // leaking the SourceBufferActor snapshot into the calling dispatcher's\n // tracking scope.\n const actorCtx = peek(sourceBufferActor.snapshot).context;\n const bufferedSegments = getBufferedSegments(track.segments);\n const currentTime = range?.start ?? 0;\n const tasks: LoadTask[] = [];\n\n // Cross-rendition switch check (mid-stream language change). Fires when\n // (a) an init segment has already been committed for some track,\n // (b) the newly-selected track is a different track, and\n // (c) the languages differ. The buffered range from the current segment\n // boundary forward is treated as stale (new track's same-timestamp\n // segments will overwrite it via MSE append-at-same-timestamp); no\n // explicit `remove` task is emitted for the cross-rendition range.\n // Computed against the currently-buffered segments (stable reference;\n // new track's playlist may not be resolved yet at first load).\n const isCrossRenditionSwitch =\n actorCtx.initTrackId !== undefined &&\n actorCtx.initTrackId !== track.id &&\n actorCtx.initTrackLanguage !== track.language;\n\n // Two categories of \"buffered content that should not gate planning\":\n //\n // - `removes` — content that needs an explicit `remove` task (out-of-window\n // forward content, back-buffer content beyond the keep window). Emitted\n // as `{ type: 'remove' }` tasks.\n // - `staleRanges` — content that the new appends will overwrite at the\n // same timestamps (cross-rendition switch). No explicit `remove` —\n // MSE's overwrite-on-append handles it, which gives a much smaller\n // perceived audio gap than `remove`-then-fetch-then-append.\n //\n // Both categories affect `effectiveBuffered` so `getSegmentsToLoad`\n // re-plans new-track segments inside them.\n const removes: Array<{ start: number; end: number }> = [];\n const staleRanges: Array<{ start: number; end: number }> = [];\n if (range) {\n if (isCrossRenditionSwitch) {\n // Mark current-segment-start onward as stale. Falls back to the\n // first buffered segment after currentTime when the playhead sits\n // in a buffer gap.\n const currentSeg = actorCtx.segments.find(\n (s) => s.startTime <= currentTime && s.startTime + s.duration > currentTime\n );\n const staleStart = currentSeg?.startTime ?? actorCtx.segments.find((s) => s.startTime > currentTime)?.startTime;\n if (staleStart !== undefined) {\n staleRanges.push({ start: staleStart, end: Infinity });\n }\n }\n const forwardFlushStart = calculateForwardFlushPoint(bufferedSegments, currentTime, forwardBufferConfig);\n if (forwardFlushStart < Infinity) {\n removes.push({ start: forwardFlushStart, end: Infinity });\n }\n const backFlushEnd = calculateBackBufferFlushPoint(bufferedSegments, currentTime, backBufferConfig);\n if (backFlushEnd > 0) {\n removes.push({ start: 0, end: backFlushEnd });\n }\n for (const r of removes) tasks.push({ type: 'remove', start: r.start, end: r.end });\n }\n\n // Treat any segment overlapping a planned remove OR a stale range as\n // not-buffered. Without this, sibling renditions that share segment IDs\n // and startTimes (audio language variants) would see the new track's\n // same-startTime segments marked \"buffered\" by the pre-flush snapshot,\n // skip them in `getSegmentsToLoad`, and leave a permanent gap from\n // `currentSegmentStart` to the end of the old buffer window — stalling\n // playback.\n const overlapsStale = (seg: { startTime: number; duration: number }): boolean => {\n const segEnd = seg.startTime + seg.duration;\n return (\n removes.some((r) => seg.startTime < r.end && segEnd > r.start) ||\n staleRanges.some((r) => seg.startTime < r.end && segEnd > r.start)\n );\n };\n const effectiveBuffered =\n removes.length + staleRanges.length > 0 ? bufferedSegments.filter((s) => !overlapsStale(s)) : bufferedSegments;\n\n // Case 2: Init\n if (actorCtx.initTrackId !== track.id) {\n tasks.push({\n type: 'append-init',\n meta: { trackId: track.id, language: track.language },\n url: track.initialization.url,\n ...(track.initialization.byteRange !== undefined && { byteRange: track.initialization.byteRange }),\n });\n }\n\n // Case 3: Segments\n if (range) {\n const segmentsToLoad = getSegmentsToLoad(\n track.segments,\n effectiveBuffered,\n currentTime,\n forwardBufferConfig\n ).filter((seg) => {\n // Quality-aware filter: skip segments already covered by equal-or-higher-quality\n // content in the actor context. Preserves buffered high-quality content during\n // ABR downgrades; loads during upgrades and for uncovered positions.\n // Actor entries that overlap a planned remove OR a stale range are treated\n // as nonexistent here — they're about to be flushed or overwritten, so the\n // new-track segment must load regardless of the existing entry's bandwidth.\n const existing = actorCtx.segments.find(\n (s) => !overlapsStale(s) && Math.abs(s.startTime - seg.startTime) < SEGMENT_TIME_EPSILON\n );\n // Partial segments are still streaming — treat as not buffered so they\n // are always re-planned (avoids relying on incomplete data).\n if (existing?.partial) return true;\n if (!existing?.trackBandwidth || !track.bandwidth) return true;\n return track.bandwidth > existing.trackBandwidth;\n });\n for (const segment of segmentsToLoad) {\n tasks.push({\n type: 'append-segment',\n meta: {\n id: segment.id,\n startTime: segment.startTime,\n duration: segment.duration,\n trackId: track.id,\n trackBandwidth: track.bandwidth,\n },\n url: segment.url,\n ...(segment.byteRange !== undefined && { byteRange: segment.byteRange }),\n });\n }\n }\n\n return tasks;\n };\n\n const scheduleAll = (tasks: LoadTask[], { getContext, setContext, runner }: Ctx): void => {\n tasks.forEach((op) => {\n runner\n .schedule(makeLoadTask(op, { getContext, setContext, fetchBytes, sourceBufferActor }))\n .then(undefined, (e: unknown) => {\n if (e instanceof Error && e.name === 'AbortError') return;\n // On unexpected fetch/append errors, abort remaining tasks so a failed\n // init doesn't cause segment fetches to proceed with no init segment.\n console.error('Unexpected error in segment loader:', e);\n runner.abortPending();\n });\n });\n };\n\n return createMachineActor<UserState, SegmentLoaderActorContext, SegmentLoaderMessage, () => SerialRunner>({\n runner: () => new SerialRunner(),\n initial: 'idle',\n context: { inFlightInitTrackId: null, inFlightSegmentId: null },\n states: {\n idle: {\n on: {\n load: (msg, ctx) => {\n const allTasks = planTasks(msg);\n if (allTasks.length === 0) return;\n ctx.transition('loading');\n scheduleAll(allTasks, ctx);\n },\n },\n },\n loading: {\n onSettled: 'idle',\n on: {\n load: (msg, ctx) => {\n const { context, runner } = ctx;\n const allTasks = planTasks(msg);\n\n // Determine whether the in-flight operation is still needed.\n const inFlightStillNeeded =\n (context.inFlightSegmentId !== null &&\n allTasks.some((t) => t.type === 'append-segment' && t.meta.id === context.inFlightSegmentId)) ||\n (context.inFlightInitTrackId !== null &&\n allTasks.some((t) => t.type === 'append-init' && t.meta.trackId === context.inFlightInitTrackId));\n\n if (inFlightStillNeeded) {\n // Continue: abort only the pending queue, let the in-flight task finish.\n // Schedule everything except the in-flight item — it covers that slot.\n runner.abortPending();\n scheduleAll(\n allTasks.filter(\n (t) =>\n !(t.type === 'append-segment' && t.meta.id === context.inFlightSegmentId) &&\n !(t.type === 'append-init' && t.meta.trackId === context.inFlightInitTrackId)\n ),\n ctx\n );\n } else {\n // Preempt: abort everything and replan.\n runner.abortAll();\n // Cancel SourceBufferActor tasks when a segment is in-flight (always\n // discard) or when a track switch is happening (new track's init follows).\n // For a same-track seek with an in-flight init, skip cancel — the task's\n // signal is not aborted (abortAll was called on the runner, but the init\n // task already completed or will complete via its own signal path).\n const cancelSourceBuffer =\n context.inFlightSegmentId !== null ||\n (context.inFlightInitTrackId !== null &&\n allTasks.some((t) => t.type === 'append-init' && t.meta.trackId !== context.inFlightInitTrackId));\n if (cancelSourceBuffer) {\n sourceBufferActor.send({ type: 'cancel' });\n }\n scheduleAll(allTasks, ctx);\n }\n },\n },\n },\n },\n });\n}\n"],"mappings":"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"}
@@ -1,8 +1,9 @@
1
- import { createMachineActor } from "../../core/create-machine-actor.js";
2
- import { SerialRunner, Task } from "../../core/task.js";
3
- import { appendSegment } from "./append-segment.js";
4
- import { flushBuffer } from "./buffer-flusher.js";
5
- //#region src/dom/media/source-buffer-actor.ts
1
+ import { SerialRunner, Task } from "../../../core/tasks/task.js";
2
+ import { createMachineActor } from "../../../core/actors/create-machine-actor.js";
3
+ import { appendSegment } from "../../../media/dom/mse/append-segment.js";
4
+ import { flushBuffer } from "../../../media/dom/mse/buffer-flusher.js";
5
+ import { SEGMENT_TIME_EPSILON } from "../../../media/types/index.js";
6
+ //#region src/playback/actors/dom/source-buffer.ts
6
7
  function snapshotBuffered(buffered) {
7
8
  const ranges = [];
8
9
  for (let i = 0; i < buffered.length; i++) ranges.push({
@@ -18,7 +19,8 @@ function appendInitTask(message, { getContext, sourceBuffer }) {
18
19
  await appendSegment(sourceBuffer, message.data);
19
20
  return {
20
21
  ...ctx,
21
- initTrackId: message.meta.trackId
22
+ initTrackId: message.meta.trackId,
23
+ initTrackLanguage: message.meta.language
22
24
  };
23
25
  });
24
26
  }
@@ -27,8 +29,7 @@ function appendSegmentTask(message, { getContext, sourceBuffer, setContext }) {
27
29
  const ctx = getContext();
28
30
  if (taskSignal.aborted) return ctx;
29
31
  const { meta } = message;
30
- const EPSILON = 1e-4;
31
- const filtered = ctx.segments.filter((s) => Math.abs(s.startTime - meta.startTime) >= EPSILON);
32
+ const filtered = ctx.segments.filter((s) => Math.abs(s.startTime - meta.startTime) >= SEGMENT_TIME_EPSILON);
32
33
  if (!(message.data instanceof ArrayBuffer)) setContext({
33
34
  ...ctx,
34
35
  segments: [...filtered, {
@@ -134,4 +135,4 @@ function createSourceBufferActor(sourceBuffer, initialContext) {
134
135
  //#endregion
135
136
  export { createSourceBufferActor };
136
137
 
137
- //# sourceMappingURL=source-buffer-actor.js.map
138
+ //# sourceMappingURL=source-buffer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"source-buffer.js","names":[],"sources":["../../../../../src/playback/actors/dom/source-buffer.ts"],"sourcesContent":["import { createMachineActor, type HandlerContext, type MessageActor } from '../../../core/actors/create-machine-actor';\nimport { SerialRunner, Task } from '../../../core/tasks/task';\nimport { type AppendData, appendSegment } from '../../../media/dom/mse/append-segment';\nimport { flushBuffer } from '../../../media/dom/mse/buffer-flusher';\nimport { SEGMENT_TIME_EPSILON, type Segment, type Track } from '../../../media/types';\n\n// =============================================================================\n// Types\n// =============================================================================\n\nexport interface BufferedRange {\n start: number;\n end: number;\n}\n\nexport type AppendSegmentMeta = Pick<Segment, 'id' | 'startTime' | 'duration'> & {\n trackId: Track['id'];\n /** Declared track bandwidth in bps (from playlist BANDWIDTH attribute). */\n trackBandwidth?: number;\n};\n\nexport type { AppendData };\n\nexport type AppendInitMessage = {\n type: 'append-init';\n data: AppendData;\n /**\n * `language` is captured alongside `trackId` so downstream loaders can\n * compare the buffered track's language to the newly-selected track's\n * language and decide whether ahead-buffer flush is warranted on track\n * switch (see `segment-loader`'s `planTasks`). Undefined for video and\n * for audio without explicit `LANGUAGE` attribute.\n */\n meta: { trackId: Track['id']; language?: string };\n};\nexport type AppendSegmentMessage = { type: 'append-segment'; data: AppendData; meta: AppendSegmentMeta };\nexport type RemoveMessage = { type: 'remove'; start: number; end: number };\nexport type IndividualSourceBufferMessage = AppendInitMessage | AppendSegmentMessage | RemoveMessage;\nexport type BatchMessage = { type: 'batch'; messages: IndividualSourceBufferMessage[] };\nexport type CancelMessage = { type: 'cancel' };\n\n/** All messages accepted by a SourceBufferActor. */\nexport type SourceBufferMessage = IndividualSourceBufferMessage | BatchMessage | CancelMessage;\n\n/** Finite states of the actor. */\nexport type SourceBufferActorState = 'idle' | 'updating' | 'destroyed';\n\n/** Non-finite (extended) data managed by the actor — the XState \"context\". */\nexport interface SourceBufferActorContext {\n initTrackId?: string | undefined;\n /**\n * Language of the most recently appended init segment's track (when\n * present on the playlist). Used by the segment-loader's `planTasks`\n * to detect cross-language switches and schedule ahead-buffer flush.\n * Undefined for video and for language-less audio.\n */\n initTrackLanguage?: string | undefined;\n segments: Array<\n Pick<Segment, 'id' | 'startTime' | 'duration'> & {\n trackId: Track['id'];\n trackBandwidth?: number;\n /**\n * True while a streaming append is in progress for this segment.\n * The segment's data is partially present in the SourceBuffer.\n * Downstream code must not treat a partial segment as fully buffered.\n */\n partial?: boolean;\n }\n >;\n bufferedRanges: BufferedRange[];\n}\n\n/** SourceBuffer actor: queues operations, owns its snapshot. */\nexport type SourceBufferActor = MessageActor<SourceBufferActorState, SourceBufferActorContext, SourceBufferMessage>;\n\n// =============================================================================\n// Helpers\n// =============================================================================\n\nfunction snapshotBuffered(buffered: TimeRanges): BufferedRange[] {\n const ranges: BufferedRange[] = [];\n for (let i = 0; i < buffered.length; i++) {\n ranges.push({ start: buffered.start(i), end: buffered.end(i) });\n }\n return ranges;\n}\n\n// =============================================================================\n// Message task factories\n// =============================================================================\n\n// Context is read lazily via getContext at task execution time — not at creation\n// time — so each task always operates on the most recent context regardless of\n// when it was scheduled.\n\ninterface MessageTaskOptions {\n getContext: () => SourceBufferActorContext;\n sourceBuffer: SourceBuffer;\n setContext: (ctx: SourceBufferActorContext) => void;\n}\n\nfunction appendInitTask(\n message: AppendInitMessage,\n { getContext, sourceBuffer }: MessageTaskOptions\n): Task<SourceBufferActorContext> {\n return new Task(async (taskSignal) => {\n const ctx = getContext();\n if (taskSignal.aborted) return ctx;\n await appendSegment(sourceBuffer, message.data);\n // No abort check here: the physical SourceBuffer has been modified, so\n // the model must be updated to match regardless of signal state.\n return { ...ctx, initTrackId: message.meta.trackId, initTrackLanguage: message.meta.language };\n });\n}\n\nfunction appendSegmentTask(\n message: AppendSegmentMessage,\n { getContext, sourceBuffer, setContext }: MessageTaskOptions\n): Task<SourceBufferActorContext> {\n return new Task(async (taskSignal) => {\n const ctx = getContext();\n if (taskSignal.aborted) return ctx;\n\n const { meta } = message;\n // Remove any existing entry at the same start time (same \"slot\" in the\n // timeline), then record the new segment. Assumes time-aligned segments\n // across playlists. `SEGMENT_TIME_EPSILON` guards against floating-point\n // drift in parsed timestamps (shared with the segment-loader quality\n // filter — single source of truth).\n const filtered = ctx.segments.filter((s) => Math.abs(s.startTime - meta.startTime) >= SEGMENT_TIME_EPSILON);\n\n // For streaming data: emit partial state before the first chunk so\n // downstream code can see the in-progress segment and treat it as\n // incomplete. ArrayBuffer appends are atomic so no partial state is\n // needed — context is updated once at task completion.\n if (!(message.data instanceof ArrayBuffer)) {\n setContext({\n ...ctx,\n segments: [\n ...filtered,\n {\n id: meta.id,\n startTime: meta.startTime,\n duration: meta.duration,\n trackId: meta.trackId,\n ...(meta.trackBandwidth !== undefined && { trackBandwidth: meta.trackBandwidth }),\n partial: true,\n },\n ],\n bufferedRanges: ctx.bufferedRanges,\n });\n }\n\n await appendSegment(sourceBuffer, message.data, taskSignal);\n // No abort check here: the physical SourceBuffer has been modified, so\n // the model must be updated to match regardless of signal state.\n return {\n ...ctx,\n segments: [\n ...filtered,\n {\n id: meta.id,\n startTime: meta.startTime,\n duration: meta.duration,\n trackId: meta.trackId,\n ...(meta.trackBandwidth !== undefined && { trackBandwidth: meta.trackBandwidth }),\n },\n ],\n bufferedRanges: snapshotBuffered(sourceBuffer.buffered),\n };\n });\n}\n\nfunction removeTask(\n message: RemoveMessage,\n { getContext, sourceBuffer }: MessageTaskOptions\n): Task<SourceBufferActorContext> {\n return new Task(async (taskSignal) => {\n const ctx = getContext();\n if (taskSignal.aborted) return ctx;\n await flushBuffer(sourceBuffer, message.start, message.end);\n // No abort check here: the physical SourceBuffer has been modified, so\n // the model must be updated to match regardless of signal state.\n //\n // Use the post-flush buffered ranges as ground truth. A segment is kept\n // in the model only if its midpoint falls within a buffered range.\n // Midpoint-based membership handles flush boundaries that don't align\n // exactly with segment edges without over-removing adjacent segments.\n const bufferedRanges = snapshotBuffered(sourceBuffer.buffered);\n const filtered = ctx.segments.filter((s) => {\n const midpoint = s.startTime + s.duration / 2;\n return bufferedRanges.some((r) => midpoint >= r.start && midpoint < r.end);\n });\n return { ...ctx, segments: filtered, bufferedRanges };\n });\n}\n\ntype MessageTaskFactory<T extends IndividualSourceBufferMessage> = (\n message: T,\n options: MessageTaskOptions\n) => Task<SourceBufferActorContext>;\n\nconst messageTaskFactories = {\n 'append-init': appendInitTask,\n 'append-segment': appendSegmentTask,\n remove: removeTask,\n} satisfies {\n [K in IndividualSourceBufferMessage['type']]: MessageTaskFactory<Extract<IndividualSourceBufferMessage, { type: K }>>;\n};\n\nfunction messageToTask(\n message: IndividualSourceBufferMessage,\n options: MessageTaskOptions\n): Task<SourceBufferActorContext> {\n const factory = messageTaskFactories[message.type] as MessageTaskFactory<typeof message>;\n return factory(message, options);\n}\n\n// =============================================================================\n// Implementation\n// =============================================================================\n\nexport function createSourceBufferActor(\n sourceBuffer: SourceBuffer,\n initialContext?: Partial<SourceBufferActorContext>\n): SourceBufferActor {\n type UserState = Exclude<SourceBufferActorState, 'destroyed'>;\n\n const handleError = (e: unknown): void => {\n if (!(e instanceof Error && e.name === 'AbortError')) {\n console.error('SourceBuffer operation failed:', e);\n }\n };\n\n type Ctx = HandlerContext<UserState, SourceBufferActorContext, () => SerialRunner>;\n\n const onMessage = (msg: IndividualSourceBufferMessage, { transition, setContext, getContext, runner }: Ctx): void => {\n transition('updating');\n const task = messageToTask(msg, { getContext, sourceBuffer, setContext });\n runner.schedule(task).then(setContext, handleError);\n };\n\n return createMachineActor<UserState, SourceBufferActorContext, SourceBufferMessage, () => SerialRunner>({\n runner: () => new SerialRunner(),\n initial: 'idle',\n context: { segments: [], bufferedRanges: [], initTrackId: undefined, ...initialContext },\n states: {\n idle: {\n on: {\n 'append-init': onMessage,\n 'append-segment': onMessage,\n remove: onMessage,\n batch: (msg, { transition, setContext, getContext, runner }) => {\n const { messages } = msg;\n if (messages.length === 0) return;\n\n transition('updating');\n messages.forEach((msg) => {\n const task = messageToTask(msg, { getContext, sourceBuffer, setContext });\n runner.schedule(task).then(setContext, handleError);\n });\n },\n },\n },\n updating: {\n // Automatically return to idle once all scheduled tasks settle.\n onSettled: 'idle',\n on: {\n // Abort all in-progress and pending tasks. onSettled handles → 'idle'\n // once the aborted tasks drain.\n cancel: (_, { runner }) => {\n runner.abortAll();\n },\n },\n },\n },\n });\n}\n"],"mappings":";;;;;;AA+EA,SAAS,iBAAiB,UAAuC;CAC/D,MAAM,SAA0B,EAAE;AAClC,MAAK,IAAI,IAAI,GAAG,IAAI,SAAS,QAAQ,IACnC,QAAO,KAAK;EAAE,OAAO,SAAS,MAAM,EAAE;EAAE,KAAK,SAAS,IAAI,EAAE;EAAE,CAAC;AAEjE,QAAO;;AAiBT,SAAS,eACP,SACA,EAAE,YAAY,gBACkB;AAChC,QAAO,IAAI,KAAK,OAAO,eAAe;EACpC,MAAM,MAAM,YAAY;AACxB,MAAI,WAAW,QAAS,QAAO;AAC/B,QAAM,cAAc,cAAc,QAAQ,KAAK;AAG/C,SAAO;GAAE,GAAG;GAAK,aAAa,QAAQ,KAAK;GAAS,mBAAmB,QAAQ,KAAK;GAAU;GAC9F;;AAGJ,SAAS,kBACP,SACA,EAAE,YAAY,cAAc,cACI;AAChC,QAAO,IAAI,KAAK,OAAO,eAAe;EACpC,MAAM,MAAM,YAAY;AACxB,MAAI,WAAW,QAAS,QAAO;EAE/B,MAAM,EAAE,SAAS;EAMjB,MAAM,WAAW,IAAI,SAAS,QAAQ,MAAM,KAAK,IAAI,EAAE,YAAY,KAAK,UAAU,IAAI,qBAAqB;AAM3G,MAAI,EAAE,QAAQ,gBAAgB,aAC5B,YAAW;GACT,GAAG;GACH,UAAU,CACR,GAAG,UACH;IACE,IAAI,KAAK;IACT,WAAW,KAAK;IAChB,UAAU,KAAK;IACf,SAAS,KAAK;IACd,GAAI,KAAK,mBAAmB,KAAA,KAAa,EAAE,gBAAgB,KAAK,gBAAgB;IAChF,SAAS;IACV,CACF;GACD,gBAAgB,IAAI;GACrB,CAAC;AAGJ,QAAM,cAAc,cAAc,QAAQ,MAAM,WAAW;AAG3D,SAAO;GACL,GAAG;GACH,UAAU,CACR,GAAG,UACH;IACE,IAAI,KAAK;IACT,WAAW,KAAK;IAChB,UAAU,KAAK;IACf,SAAS,KAAK;IACd,GAAI,KAAK,mBAAmB,KAAA,KAAa,EAAE,gBAAgB,KAAK,gBAAgB;IACjF,CACF;GACD,gBAAgB,iBAAiB,aAAa,SAAS;GACxD;GACD;;AAGJ,SAAS,WACP,SACA,EAAE,YAAY,gBACkB;AAChC,QAAO,IAAI,KAAK,OAAO,eAAe;EACpC,MAAM,MAAM,YAAY;AACxB,MAAI,WAAW,QAAS,QAAO;AAC/B,QAAM,YAAY,cAAc,QAAQ,OAAO,QAAQ,IAAI;EAQ3D,MAAM,iBAAiB,iBAAiB,aAAa,SAAS;EAC9D,MAAM,WAAW,IAAI,SAAS,QAAQ,MAAM;GAC1C,MAAM,WAAW,EAAE,YAAY,EAAE,WAAW;AAC5C,UAAO,eAAe,MAAM,MAAM,YAAY,EAAE,SAAS,WAAW,EAAE,IAAI;IAC1E;AACF,SAAO;GAAE,GAAG;GAAK,UAAU;GAAU;GAAgB;GACrD;;AAQJ,MAAM,uBAAuB;CAC3B,eAAe;CACf,kBAAkB;CAClB,QAAQ;CACT;AAID,SAAS,cACP,SACA,SACgC;CAChC,MAAM,UAAU,qBAAqB,QAAQ;AAC7C,QAAO,QAAQ,SAAS,QAAQ;;AAOlC,SAAgB,wBACd,cACA,gBACmB;CAGnB,MAAM,eAAe,MAAqB;AACxC,MAAI,EAAE,aAAa,SAAS,EAAE,SAAS,cACrC,SAAQ,MAAM,kCAAkC,EAAE;;CAMtD,MAAM,aAAa,KAAoC,EAAE,YAAY,YAAY,YAAY,aAAwB;AACnH,aAAW,WAAW;EACtB,MAAM,OAAO,cAAc,KAAK;GAAE;GAAY;GAAc;GAAY,CAAC;AACzE,SAAO,SAAS,KAAK,CAAC,KAAK,YAAY,YAAY;;AAGrD,QAAO,mBAAiG;EACtG,cAAc,IAAI,cAAc;EAChC,SAAS;EACT,SAAS;GAAE,UAAU,EAAE;GAAE,gBAAgB,EAAE;GAAE,aAAa,KAAA;GAAW,GAAG;GAAgB;EACxF,QAAQ;GACN,MAAM,EACJ,IAAI;IACF,eAAe;IACf,kBAAkB;IAClB,QAAQ;IACR,QAAQ,KAAK,EAAE,YAAY,YAAY,YAAY,aAAa;KAC9D,MAAM,EAAE,aAAa;AACrB,SAAI,SAAS,WAAW,EAAG;AAE3B,gBAAW,WAAW;AACtB,cAAS,SAAS,QAAQ;MACxB,MAAM,OAAO,cAAc,KAAK;OAAE;OAAY;OAAc;OAAY,CAAC;AACzE,aAAO,SAAS,KAAK,CAAC,KAAK,YAAY,YAAY;OACnD;;IAEL,EACF;GACD,UAAU;IAER,WAAW;IACX,IAAI,EAGF,SAAS,GAAG,EAAE,aAAa;AACzB,YAAO,UAAU;OAEpB;IACF;GACF;EACF,CAAC"}
@@ -1,5 +1,5 @@
1
- import { createTransitionActor } from "../../core/create-transition-actor.js";
2
- //#region src/dom/features/text-tracks-actor.ts
1
+ import { createTransitionActor } from "../../../core/actors/create-transition-actor.js";
2
+ //#region src/playback/actors/dom/text-tracks.ts
3
3
  function isDuplicateCue(cue, existing) {
4
4
  return existing.some((r) => r.startTime === cue.startTime && r.endTime === cue.endTime && r.text === cue.text);
5
5
  }
@@ -9,6 +9,10 @@ function createTextTracksActor(mediaElement) {
9
9
  loaded: {},
10
10
  segments: {}
11
11
  }, (context, message) => {
12
+ if (message.type === "clear") return {
13
+ loaded: {},
14
+ segments: {}
15
+ };
12
16
  const { meta, cues } = message;
13
17
  const { trackId, id: segmentId, startTime, duration } = meta;
14
18
  const textTrack = Array.from(mediaElement.textTracks).find((t) => t.id === trackId);
@@ -39,4 +43,4 @@ function createTextTracksActor(mediaElement) {
39
43
  //#endregion
40
44
  export { createTextTracksActor };
41
45
 
42
- //# sourceMappingURL=text-tracks-actor.js.map
46
+ //# sourceMappingURL=text-tracks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"text-tracks.js","names":[],"sources":["../../../../../src/playback/actors/dom/text-tracks.ts"],"sourcesContent":["import { createTransitionActor } from '../../../core/actors/create-transition-actor';\nimport type { Cue } from '../../../media/types';\nimport type {\n AddCuesMessage,\n ClearMessage,\n CueSegmentMeta,\n TextTracksActor,\n TextTracksActorContext,\n TextTracksActorMessage,\n} from '../text-tracks';\n\n// Re-export the host-agnostic types so existing dom-side consumers can keep\n// importing from this module.\nexport type {\n AddCuesMessage,\n ClearMessage,\n CueSegmentMeta,\n TextTracksActor,\n TextTracksActorContext,\n TextTracksActorMessage,\n};\n\n// =============================================================================\n// Helpers\n// =============================================================================\n\nfunction isDuplicateCue(cue: VTTCue, existing: Cue[]): boolean {\n return existing.some((r) => r.startTime === cue.startTime && r.endTime === cue.endTime && r.text === cue.text);\n}\n\n// =============================================================================\n// Implementation\n// =============================================================================\n\n/** TextTrack actor: wraps all text tracks on a media element, owns cue operations. */\nexport function createTextTracksActor(mediaElement: HTMLMediaElement): TextTracksActor<VTTCue> {\n return createTransitionActor({ loaded: {}, segments: {} } as TextTracksActorContext, (context, message) => {\n if (message.type === 'clear') {\n // Reset the cue + segment cache. DOM cleanup is the caller's job —\n // by the time we get here, `syncTextTracks` has already removed\n // the `<track>` children (and their cues) via\n // `removeAllSubtitlesTracksFromMedia`.\n return { loaded: {}, segments: {} };\n }\n\n // NOTE: Currently assumes cues are applied to a non-disabled TextTrack. Discuss different approaches here, including:\n // - Making the message responsible for auto-selection of the textTrack (changes logic in sync-text-tracks)\n // - Silent gating/console warning + early bail\n // - throwing a domain-specific error\n // - accepting as is (which would result in errors, but also \"shouldn't ever happen\" unless a bug is introduced)\n // (CJP)\n const { meta, cues } = message;\n const { trackId, id: segmentId, startTime, duration } = meta;\n const textTrack = Array.from(mediaElement.textTracks).find((t) => t.id === trackId);\n if (!textTrack) return context;\n\n const existingCues = context.loaded[trackId] ?? [];\n const existingSegments = context.segments[trackId] ?? [];\n const prunedCues = cues.filter((cue) => !isDuplicateCue(cue, existingCues));\n const segmentAlreadyLoaded = existingSegments.some((s) => s.id === segmentId);\n\n if (prunedCues.length === 0 && segmentAlreadyLoaded) return context;\n\n for (const cue of prunedCues) textTrack.addCue(cue);\n return {\n ...context,\n loaded: {\n ...context.loaded,\n [trackId]: [...existingCues, ...prunedCues],\n },\n segments: segmentAlreadyLoaded\n ? context.segments\n : {\n ...context.segments,\n [trackId]: [...existingSegments, { id: segmentId, startTime, duration }],\n },\n };\n });\n}\n"],"mappings":";;AA0BA,SAAS,eAAe,KAAa,UAA0B;AAC7D,QAAO,SAAS,MAAM,MAAM,EAAE,cAAc,IAAI,aAAa,EAAE,YAAY,IAAI,WAAW,EAAE,SAAS,IAAI,KAAK;;;AAQhH,SAAgB,sBAAsB,cAAyD;AAC7F,QAAO,sBAAsB;EAAE,QAAQ,EAAE;EAAE,UAAU,EAAE;EAAE,GAA6B,SAAS,YAAY;AACzG,MAAI,QAAQ,SAAS,QAKnB,QAAO;GAAE,QAAQ,EAAE;GAAE,UAAU,EAAE;GAAE;EASrC,MAAM,EAAE,MAAM,SAAS;EACvB,MAAM,EAAE,SAAS,IAAI,WAAW,WAAW,aAAa;EACxD,MAAM,YAAY,MAAM,KAAK,aAAa,WAAW,CAAC,MAAM,MAAM,EAAE,OAAO,QAAQ;AACnF,MAAI,CAAC,UAAW,QAAO;EAEvB,MAAM,eAAe,QAAQ,OAAO,YAAY,EAAE;EAClD,MAAM,mBAAmB,QAAQ,SAAS,YAAY,EAAE;EACxD,MAAM,aAAa,KAAK,QAAQ,QAAQ,CAAC,eAAe,KAAK,aAAa,CAAC;EAC3E,MAAM,uBAAuB,iBAAiB,MAAM,MAAM,EAAE,OAAO,UAAU;AAE7E,MAAI,WAAW,WAAW,KAAK,qBAAsB,QAAO;AAE5D,OAAK,MAAM,OAAO,WAAY,WAAU,OAAO,IAAI;AACnD,SAAO;GACL,GAAG;GACH,QAAQ;IACN,GAAG,QAAQ;KACV,UAAU,CAAC,GAAG,cAAc,GAAG,WAAW;IAC5C;GACD,UAAU,uBACN,QAAQ,WACR;IACE,GAAG,QAAQ;KACV,UAAU,CAAC,GAAG,kBAAkB;KAAE,IAAI;KAAW;KAAW;KAAU,CAAC;IACzE;GACN;GACD"}