@videojs/spf 10.0.0-beta.30 → 10.0.0-beta.32

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 (520) hide show
  1. package/dist/default/core/actors/create-machine-actor.js +23 -25
  2. package/dist/default/core/actors/create-machine-actor.js.map +1 -1
  3. package/dist/default/core/actors/create-transition-actor.js +7 -9
  4. package/dist/default/core/actors/create-transition-actor.js.map +1 -1
  5. package/dist/default/core/composition/create-composition.js +26 -29
  6. package/dist/default/core/composition/create-composition.js.map +1 -1
  7. package/dist/default/core/composition/share-signals.js +12 -17
  8. package/dist/default/core/composition/share-signals.js.map +1 -1
  9. package/dist/default/core/machine.js +3 -4
  10. package/dist/default/core/machine.js.map +1 -1
  11. package/dist/default/core/reactors/create-machine-reactor.js +20 -19
  12. package/dist/default/core/reactors/create-machine-reactor.js.map +1 -1
  13. package/dist/default/core/signals/effect.js +24 -4
  14. package/dist/default/core/signals/effect.js.map +1 -1
  15. package/dist/default/core/signals/primitives.js +10 -14
  16. package/dist/default/core/signals/primitives.js.map +1 -1
  17. package/dist/default/core/signals/when.js +6 -8
  18. package/dist/default/core/signals/when.js.map +1 -1
  19. package/dist/default/core/tasks/delayed-reschedule.js +7 -11
  20. package/dist/default/core/tasks/delayed-reschedule.js.map +1 -1
  21. package/dist/default/core/tasks/task.js +59 -82
  22. package/dist/default/core/tasks/task.js.map +1 -1
  23. package/dist/default/hls.js +3 -1
  24. package/dist/default/index.js +2 -3
  25. package/dist/default/index.js.map +1 -1
  26. package/dist/default/media/abr/quality-selection.js +3 -7
  27. package/dist/default/media/abr/quality-selection.js.map +1 -1
  28. package/dist/default/media/buffer/back-buffer.js +15 -19
  29. package/dist/default/media/buffer/back-buffer.js.map +1 -1
  30. package/dist/default/media/buffer/forward-buffer.js +32 -41
  31. package/dist/default/media/buffer/forward-buffer.js.map +1 -1
  32. package/dist/default/media/dom/capabilities.js +26 -37
  33. package/dist/default/media/dom/capabilities.js.map +1 -1
  34. package/dist/default/media/dom/mse/append-segment.js +3 -5
  35. package/dist/default/media/dom/mse/append-segment.js.map +1 -1
  36. package/dist/default/media/dom/mse/buffer-flusher.js +5 -5
  37. package/dist/default/media/dom/mse/buffer-flusher.js.map +1 -1
  38. package/dist/default/media/dom/mse/duration.js +23 -31
  39. package/dist/default/media/dom/mse/duration.js.map +1 -1
  40. package/dist/default/media/dom/mse/end-of-stream.js +3 -6
  41. package/dist/default/media/dom/mse/end-of-stream.js.map +1 -1
  42. package/dist/default/media/dom/mse/mediasource-setup.js +65 -81
  43. package/dist/default/media/dom/mse/mediasource-setup.js.map +1 -1
  44. package/dist/default/media/dom/screen.js +37 -54
  45. package/dist/default/media/dom/screen.js.map +1 -1
  46. package/dist/default/media/dom/text/resolve-vtt-segment.js.map +1 -1
  47. package/dist/default/media/dom/text/text-track-slots.js +15 -23
  48. package/dist/default/media/dom/text/text-track-slots.js.map +1 -1
  49. package/dist/default/media/errors.js +25 -34
  50. package/dist/default/media/errors.js.map +1 -1
  51. package/dist/default/media/hls/parse-attributes.js +9 -26
  52. package/dist/default/media/hls/parse-attributes.js.map +1 -1
  53. package/dist/default/media/hls/parse-media-playlist.js +25 -33
  54. package/dist/default/media/hls/parse-media-playlist.js.map +1 -1
  55. package/dist/default/media/hls/parse-multivariant.js +3 -3
  56. package/dist/default/media/hls/parse-multivariant.js.map +1 -1
  57. package/dist/default/media/hls/reload-policy.js +19 -27
  58. package/dist/default/media/hls/reload-policy.js.map +1 -1
  59. package/dist/default/media/hls/resolve-url.js +1 -3
  60. package/dist/default/media/hls/resolve-url.js.map +1 -1
  61. package/dist/default/media/live-window.js +9 -13
  62. package/dist/default/media/live-window.js.map +1 -1
  63. package/dist/default/media/media-tracks/media-tracks.js +12 -17
  64. package/dist/default/media/media-tracks/media-tracks.js.map +1 -1
  65. package/dist/default/media/mp4/box.js +4 -5
  66. package/dist/default/media/mp4/box.js.map +1 -1
  67. package/dist/default/media/mp4/timestamp-origin.js +30 -42
  68. package/dist/default/media/mp4/timestamp-origin.js.map +1 -1
  69. package/dist/default/media/primitives/resolution.js +5 -8
  70. package/dist/default/media/primitives/resolution.js.map +1 -1
  71. package/dist/default/media/primitives/select-tracks.js +24 -34
  72. package/dist/default/media/primitives/select-tracks.js.map +1 -1
  73. package/dist/default/media/text/parse-vtt-timestamp-map.js +6 -8
  74. package/dist/default/media/text/parse-vtt-timestamp-map.js.map +1 -1
  75. package/dist/default/media/text/resolve-vtt-metadata.js +8 -9
  76. package/dist/default/media/text/resolve-vtt-metadata.js.map +1 -1
  77. package/dist/default/media/types/index.js +10 -18
  78. package/dist/default/media/types/index.js.map +1 -1
  79. package/dist/default/media/utils/cdn.js +10 -16
  80. package/dist/default/media/utils/cdn.js.map +1 -1
  81. package/dist/default/media/utils/preload.js +4 -6
  82. package/dist/default/media/utils/preload.js.map +1 -1
  83. package/dist/default/media/utils/track-selection.js +10 -14
  84. package/dist/default/media/utils/track-selection.js.map +1 -1
  85. package/dist/default/media/utils/tracks.js +56 -54
  86. package/dist/default/media/utils/tracks.js.map +1 -1
  87. package/dist/default/network/bandwidth-estimator.js +18 -19
  88. package/dist/default/network/bandwidth-estimator.js.map +1 -1
  89. package/dist/default/network/chunked-stream-iterable.js +4 -6
  90. package/dist/default/network/chunked-stream-iterable.js.map +1 -1
  91. package/dist/default/network/ewma.js +22 -25
  92. package/dist/default/network/ewma.js.map +1 -1
  93. package/dist/default/network/fetch.js +30 -37
  94. package/dist/default/network/fetch.js.map +1 -1
  95. package/dist/default/playback/actors/dom/segment-loader.js +28 -44
  96. package/dist/default/playback/actors/dom/segment-loader.js.map +1 -1
  97. package/dist/default/playback/actors/dom/source-buffer.js.map +1 -1
  98. package/dist/default/playback/actors/dom/text-tracks.js.map +1 -1
  99. package/dist/default/playback/actors/text-track-segment-loader.js +24 -35
  100. package/dist/default/playback/actors/text-track-segment-loader.js.map +1 -1
  101. package/dist/default/playback/adapters/hls-audio/adapter.js +23 -31
  102. package/dist/default/playback/adapters/hls-audio/adapter.js.map +1 -1
  103. package/dist/default/playback/adapters/hls-audio/media.js.map +1 -1
  104. package/dist/default/playback/adapters/hls-background-video/adapter.js +50 -70
  105. package/dist/default/playback/adapters/hls-background-video/adapter.js.map +1 -1
  106. package/dist/default/playback/adapters/hls-background-video/host.js +12 -16
  107. package/dist/default/playback/adapters/hls-background-video/host.js.map +1 -1
  108. package/dist/default/playback/adapters/hls-background-video/media.js +5 -8
  109. package/dist/default/playback/adapters/hls-background-video/media.js.map +1 -1
  110. package/dist/default/playback/adapters/hls-video/adapter.js +43 -60
  111. package/dist/default/playback/adapters/hls-video/adapter.js.map +1 -1
  112. package/dist/default/playback/adapters/hls-video/error-surface.js +13 -18
  113. package/dist/default/playback/adapters/hls-video/error-surface.js.map +1 -1
  114. package/dist/default/playback/adapters/hls-video/media-tracks.js +4 -5
  115. package/dist/default/playback/adapters/hls-video/media-tracks.js.map +1 -1
  116. package/dist/default/playback/adapters/hls-video/media.js.map +1 -1
  117. package/dist/default/playback/adapters/mux-audio/media.js +8 -12
  118. package/dist/default/playback/adapters/mux-audio/media.js.map +1 -1
  119. package/dist/default/playback/adapters/mux-video/adapter.js +25 -33
  120. package/dist/default/playback/adapters/mux-video/adapter.js.map +1 -1
  121. package/dist/default/playback/adapters/mux-video/media.js +7 -9
  122. package/dist/default/playback/adapters/mux-video/media.js.map +1 -1
  123. package/dist/default/playback/behaviors/calculate-presentation-duration.js +5 -8
  124. package/dist/default/playback/behaviors/calculate-presentation-duration.js.map +1 -1
  125. package/dist/default/playback/behaviors/collect-errors.js +39 -52
  126. package/dist/default/playback/behaviors/collect-errors.js.map +1 -1
  127. package/dist/default/playback/behaviors/derive-cdn-priority.js +16 -23
  128. package/dist/default/playback/behaviors/derive-cdn-priority.js.map +1 -1
  129. package/dist/default/playback/behaviors/dom/airplay.js +38 -61
  130. package/dist/default/playback/behaviors/dom/airplay.js.map +1 -1
  131. package/dist/default/playback/behaviors/dom/apply-start-position.js +23 -35
  132. package/dist/default/playback/behaviors/dom/apply-start-position.js.map +1 -1
  133. package/dist/default/playback/behaviors/dom/end-of-stream.js +11 -16
  134. package/dist/default/playback/behaviors/dom/end-of-stream.js.map +1 -1
  135. package/dist/default/playback/behaviors/dom/load-segments.js +27 -39
  136. package/dist/default/playback/behaviors/dom/load-segments.js.map +1 -1
  137. package/dist/default/playback/behaviors/dom/recover-end-stall.js +21 -26
  138. package/dist/default/playback/behaviors/dom/recover-end-stall.js.map +1 -1
  139. package/dist/default/playback/behaviors/dom/seek-to-live-edge.js +41 -60
  140. package/dist/default/playback/behaviors/dom/seek-to-live-edge.js.map +1 -1
  141. package/dist/default/playback/behaviors/dom/setup-buffer-actors.js +50 -74
  142. package/dist/default/playback/behaviors/dom/setup-buffer-actors.js.map +1 -1
  143. package/dist/default/playback/behaviors/dom/setup-mediasource.js +29 -43
  144. package/dist/default/playback/behaviors/dom/setup-mediasource.js.map +1 -1
  145. package/dist/default/playback/behaviors/dom/setup-text-track-actors.js.map +1 -1
  146. package/dist/default/playback/behaviors/dom/sync-live-seekable-range.js +3 -4
  147. package/dist/default/playback/behaviors/dom/sync-live-seekable-range.js.map +1 -1
  148. package/dist/default/playback/behaviors/dom/sync-text-tracks.js +30 -45
  149. package/dist/default/playback/behaviors/dom/sync-text-tracks.js.map +1 -1
  150. package/dist/default/playback/behaviors/dom/track-current-time.js +9 -11
  151. package/dist/default/playback/behaviors/dom/track-current-time.js.map +1 -1
  152. package/dist/default/playback/behaviors/dom/track-load-triggers.js +24 -32
  153. package/dist/default/playback/behaviors/dom/track-load-triggers.js.map +1 -1
  154. package/dist/default/playback/behaviors/dom/track-playback-rate.js +6 -8
  155. package/dist/default/playback/behaviors/dom/track-playback-rate.js.map +1 -1
  156. package/dist/default/playback/behaviors/dom/track-player-resolution.js +10 -14
  157. package/dist/default/playback/behaviors/dom/track-player-resolution.js.map +1 -1
  158. package/dist/default/playback/behaviors/dom/track-screen-resolution.js +8 -9
  159. package/dist/default/playback/behaviors/dom/track-screen-resolution.js.map +1 -1
  160. package/dist/default/playback/behaviors/dom/update-mediasource-duration.js +28 -43
  161. package/dist/default/playback/behaviors/dom/update-mediasource-duration.js.map +1 -1
  162. package/dist/default/playback/behaviors/establish-start-media-time.js +32 -42
  163. package/dist/default/playback/behaviors/establish-start-media-time.js.map +1 -1
  164. package/dist/default/playback/behaviors/resolve-presentation.js +13 -22
  165. package/dist/default/playback/behaviors/resolve-presentation.js.map +1 -1
  166. package/dist/default/playback/behaviors/resolve-track.js +5 -12
  167. package/dist/default/playback/behaviors/resolve-track.js.map +1 -1
  168. package/dist/default/playback/behaviors/select-tracks.js +77 -104
  169. package/dist/default/playback/behaviors/select-tracks.js.map +1 -1
  170. package/dist/default/playback/behaviors/setup-failover-monitor.js +11 -16
  171. package/dist/default/playback/behaviors/setup-failover-monitor.js.map +1 -1
  172. package/dist/default/playback/behaviors/sync-preload.js +9 -13
  173. package/dist/default/playback/behaviors/sync-preload.js.map +1 -1
  174. package/dist/default/playback/behaviors/track-switching.js +189 -173
  175. package/dist/default/playback/behaviors/track-switching.js.map +1 -1
  176. package/dist/default/playback/engines/hls/engine-audio-only.js +20 -24
  177. package/dist/default/playback/engines/hls/engine-audio-only.js.map +1 -1
  178. package/dist/default/playback/engines/hls/engine-background-video.js +22 -27
  179. package/dist/default/playback/engines/hls/engine-background-video.js.map +1 -1
  180. package/dist/default/playback/engines/hls/engine.js +22 -25
  181. package/dist/default/playback/engines/hls/engine.js.map +1 -1
  182. package/dist/default/playback/primitives/error-messages.js +16 -25
  183. package/dist/default/playback/primitives/error-messages.js.map +1 -1
  184. package/dist/default/playback/primitives/failover-fetch.js +9 -14
  185. package/dist/default/playback/primitives/failover-fetch.js.map +1 -1
  186. package/dist/default/playback/primitives/head-peek.js +7 -10
  187. package/dist/default/playback/primitives/head-peek.js.map +1 -1
  188. package/dist/default/playback/primitives/live-window.js +12 -15
  189. package/dist/default/playback/primitives/live-window.js.map +1 -1
  190. package/dist/default/playback/primitives/relocation-pipelines.js +34 -46
  191. package/dist/default/playback/primitives/relocation-pipelines.js.map +1 -1
  192. package/dist/default/playback/primitives/report-track-conditions.js +26 -38
  193. package/dist/default/playback/primitives/report-track-conditions.js.map +1 -1
  194. package/dist/default/playback/primitives/segment-load-pipeline.js +15 -15
  195. package/dist/default/playback/primitives/segment-load-pipeline.js.map +1 -1
  196. package/dist/default/playback/primitives/selection-rules.js +65 -36
  197. package/dist/default/playback/primitives/selection-rules.js.map +1 -1
  198. package/dist/default/playback/primitives/text-segment-load-pipeline.js +4 -5
  199. package/dist/default/playback/primitives/text-segment-load-pipeline.js.map +1 -1
  200. package/dist/default/playback/primitives/track-types.js +10 -18
  201. package/dist/default/playback/primitives/track-types.js.map +1 -1
  202. package/dist/dev/core/actors/actor.d.ts +4 -4
  203. package/dist/dev/core/actors/actor.d.ts.map +1 -1
  204. package/dist/dev/core/actors/create-machine-actor.d.ts +38 -47
  205. package/dist/dev/core/actors/create-machine-actor.d.ts.map +1 -1
  206. package/dist/dev/core/actors/create-machine-actor.js +23 -25
  207. package/dist/dev/core/actors/create-machine-actor.js.map +1 -1
  208. package/dist/dev/core/actors/create-transition-actor.d.ts +11 -15
  209. package/dist/dev/core/actors/create-transition-actor.d.ts.map +1 -1
  210. package/dist/dev/core/actors/create-transition-actor.js +7 -9
  211. package/dist/dev/core/actors/create-transition-actor.js.map +1 -1
  212. package/dist/dev/core/composition/create-composition.d.ts +70 -102
  213. package/dist/dev/core/composition/create-composition.d.ts.map +1 -1
  214. package/dist/dev/core/composition/create-composition.js +26 -29
  215. package/dist/dev/core/composition/create-composition.js.map +1 -1
  216. package/dist/dev/core/composition/share-signals.d.ts +17 -24
  217. package/dist/dev/core/composition/share-signals.d.ts.map +1 -1
  218. package/dist/dev/core/composition/share-signals.js +12 -17
  219. package/dist/dev/core/composition/share-signals.js.map +1 -1
  220. package/dist/dev/core/machine.d.ts +4 -4
  221. package/dist/dev/core/machine.js +3 -4
  222. package/dist/dev/core/machine.js.map +1 -1
  223. package/dist/dev/core/reactors/create-machine-reactor.d.ts +33 -39
  224. package/dist/dev/core/reactors/create-machine-reactor.d.ts.map +1 -1
  225. package/dist/dev/core/reactors/create-machine-reactor.js +20 -19
  226. package/dist/dev/core/reactors/create-machine-reactor.js.map +1 -1
  227. package/dist/dev/core/signals/effect.d.ts +3 -4
  228. package/dist/dev/core/signals/effect.d.ts.map +1 -1
  229. package/dist/dev/core/signals/effect.js +24 -4
  230. package/dist/dev/core/signals/effect.js.map +1 -1
  231. package/dist/dev/core/signals/primitives.d.ts +10 -12
  232. package/dist/dev/core/signals/primitives.d.ts.map +1 -1
  233. package/dist/dev/core/signals/primitives.js +10 -14
  234. package/dist/dev/core/signals/primitives.js.map +1 -1
  235. package/dist/dev/core/signals/when.js +6 -8
  236. package/dist/dev/core/signals/when.js.map +1 -1
  237. package/dist/dev/core/tasks/delayed-reschedule.js +7 -11
  238. package/dist/dev/core/tasks/delayed-reschedule.js.map +1 -1
  239. package/dist/dev/core/tasks/task.d.ts +51 -73
  240. package/dist/dev/core/tasks/task.d.ts.map +1 -1
  241. package/dist/dev/core/tasks/task.js +59 -82
  242. package/dist/dev/core/tasks/task.js.map +1 -1
  243. package/dist/dev/hls.d.ts +3 -1
  244. package/dist/dev/hls.js +3 -1
  245. package/dist/dev/index.d.ts +2 -3
  246. package/dist/dev/index.d.ts.map +1 -1
  247. package/dist/dev/index.js +2 -3
  248. package/dist/dev/index.js.map +1 -1
  249. package/dist/dev/media/abr/quality-selection.d.ts +7 -10
  250. package/dist/dev/media/abr/quality-selection.d.ts.map +1 -1
  251. package/dist/dev/media/abr/quality-selection.js +3 -7
  252. package/dist/dev/media/abr/quality-selection.js.map +1 -1
  253. package/dist/dev/media/buffer/back-buffer.d.ts +2 -7
  254. package/dist/dev/media/buffer/back-buffer.d.ts.map +1 -1
  255. package/dist/dev/media/buffer/back-buffer.js +15 -19
  256. package/dist/dev/media/buffer/back-buffer.js.map +1 -1
  257. package/dist/dev/media/buffer/forward-buffer.d.ts +2 -7
  258. package/dist/dev/media/buffer/forward-buffer.d.ts.map +1 -1
  259. package/dist/dev/media/buffer/forward-buffer.js +32 -41
  260. package/dist/dev/media/buffer/forward-buffer.js.map +1 -1
  261. package/dist/dev/media/dom/capabilities.js +26 -37
  262. package/dist/dev/media/dom/capabilities.js.map +1 -1
  263. package/dist/dev/media/dom/mse/append-segment.d.ts +3 -5
  264. package/dist/dev/media/dom/mse/append-segment.d.ts.map +1 -1
  265. package/dist/dev/media/dom/mse/append-segment.js +3 -5
  266. package/dist/dev/media/dom/mse/append-segment.js.map +1 -1
  267. package/dist/dev/media/dom/mse/buffer-flusher.d.ts +5 -5
  268. package/dist/dev/media/dom/mse/buffer-flusher.js +5 -5
  269. package/dist/dev/media/dom/mse/buffer-flusher.js.map +1 -1
  270. package/dist/dev/media/dom/mse/duration.js +23 -31
  271. package/dist/dev/media/dom/mse/duration.js.map +1 -1
  272. package/dist/dev/media/dom/mse/end-of-stream.js +3 -6
  273. package/dist/dev/media/dom/mse/end-of-stream.js.map +1 -1
  274. package/dist/dev/media/dom/mse/mediasource-setup.js +65 -81
  275. package/dist/dev/media/dom/mse/mediasource-setup.js.map +1 -1
  276. package/dist/dev/media/dom/screen.d.ts.map +1 -1
  277. package/dist/dev/media/dom/screen.js +37 -54
  278. package/dist/dev/media/dom/screen.js.map +1 -1
  279. package/dist/dev/media/dom/text/resolve-vtt-segment.d.ts.map +1 -1
  280. package/dist/dev/media/dom/text/resolve-vtt-segment.js.map +1 -1
  281. package/dist/dev/media/dom/text/text-track-slots.d.ts +10 -16
  282. package/dist/dev/media/dom/text/text-track-slots.d.ts.map +1 -1
  283. package/dist/dev/media/dom/text/text-track-slots.js +15 -23
  284. package/dist/dev/media/dom/text/text-track-slots.js.map +1 -1
  285. package/dist/dev/media/errors.d.ts +32 -46
  286. package/dist/dev/media/errors.d.ts.map +1 -1
  287. package/dist/dev/media/errors.js +25 -34
  288. package/dist/dev/media/errors.js.map +1 -1
  289. package/dist/dev/media/hls/parse-attributes.js +9 -26
  290. package/dist/dev/media/hls/parse-attributes.js.map +1 -1
  291. package/dist/dev/media/hls/parse-media-playlist.js +25 -33
  292. package/dist/dev/media/hls/parse-media-playlist.js.map +1 -1
  293. package/dist/dev/media/hls/parse-multivariant.js +3 -3
  294. package/dist/dev/media/hls/parse-multivariant.js.map +1 -1
  295. package/dist/dev/media/hls/reload-policy.js +19 -27
  296. package/dist/dev/media/hls/reload-policy.js.map +1 -1
  297. package/dist/dev/media/hls/resolve-url.js +1 -3
  298. package/dist/dev/media/hls/resolve-url.js.map +1 -1
  299. package/dist/dev/media/live-window.js +9 -13
  300. package/dist/dev/media/live-window.js.map +1 -1
  301. package/dist/dev/media/media-tracks/media-tracks.d.ts +8 -13
  302. package/dist/dev/media/media-tracks/media-tracks.d.ts.map +1 -1
  303. package/dist/dev/media/media-tracks/media-tracks.js +12 -17
  304. package/dist/dev/media/media-tracks/media-tracks.js.map +1 -1
  305. package/dist/dev/media/mp4/box.js +4 -5
  306. package/dist/dev/media/mp4/box.js.map +1 -1
  307. package/dist/dev/media/mp4/timestamp-origin.js +30 -42
  308. package/dist/dev/media/mp4/timestamp-origin.js.map +1 -1
  309. package/dist/dev/media/primitives/resolution.d.ts +6 -8
  310. package/dist/dev/media/primitives/resolution.d.ts.map +1 -1
  311. package/dist/dev/media/primitives/resolution.js +5 -8
  312. package/dist/dev/media/primitives/resolution.js.map +1 -1
  313. package/dist/dev/media/primitives/select-tracks.js +24 -34
  314. package/dist/dev/media/primitives/select-tracks.js.map +1 -1
  315. package/dist/dev/media/text/parse-vtt-timestamp-map.js +6 -8
  316. package/dist/dev/media/text/parse-vtt-timestamp-map.js.map +1 -1
  317. package/dist/dev/media/text/resolve-vtt-metadata.js +8 -9
  318. package/dist/dev/media/text/resolve-vtt-metadata.js.map +1 -1
  319. package/dist/dev/media/types/index.d.ts +106 -177
  320. package/dist/dev/media/types/index.d.ts.map +1 -1
  321. package/dist/dev/media/types/index.js +10 -18
  322. package/dist/dev/media/types/index.js.map +1 -1
  323. package/dist/dev/media/utils/cdn.d.ts +4 -6
  324. package/dist/dev/media/utils/cdn.d.ts.map +1 -1
  325. package/dist/dev/media/utils/cdn.js +10 -16
  326. package/dist/dev/media/utils/cdn.js.map +1 -1
  327. package/dist/dev/media/utils/preload.js +4 -6
  328. package/dist/dev/media/utils/preload.js.map +1 -1
  329. package/dist/dev/media/utils/track-selection.js +10 -14
  330. package/dist/dev/media/utils/track-selection.js.map +1 -1
  331. package/dist/dev/media/utils/tracks.js +56 -54
  332. package/dist/dev/media/utils/tracks.js.map +1 -1
  333. package/dist/dev/network/bandwidth-estimator.d.ts +7 -9
  334. package/dist/dev/network/bandwidth-estimator.d.ts.map +1 -1
  335. package/dist/dev/network/bandwidth-estimator.js +18 -19
  336. package/dist/dev/network/bandwidth-estimator.js.map +1 -1
  337. package/dist/dev/network/chunked-stream-iterable.js +4 -6
  338. package/dist/dev/network/chunked-stream-iterable.js.map +1 -1
  339. package/dist/dev/network/ewma.js +22 -25
  340. package/dist/dev/network/ewma.js.map +1 -1
  341. package/dist/dev/network/fetch.js +30 -37
  342. package/dist/dev/network/fetch.js.map +1 -1
  343. package/dist/dev/playback/actors/dom/segment-loader.d.ts +7 -11
  344. package/dist/dev/playback/actors/dom/segment-loader.d.ts.map +1 -1
  345. package/dist/dev/playback/actors/dom/segment-loader.js +28 -44
  346. package/dist/dev/playback/actors/dom/segment-loader.js.map +1 -1
  347. package/dist/dev/playback/actors/dom/source-buffer.d.ts +5 -7
  348. package/dist/dev/playback/actors/dom/source-buffer.d.ts.map +1 -1
  349. package/dist/dev/playback/actors/dom/source-buffer.js.map +1 -1
  350. package/dist/dev/playback/actors/dom/text-tracks.js.map +1 -1
  351. package/dist/dev/playback/actors/text-track-segment-loader.d.ts +10 -16
  352. package/dist/dev/playback/actors/text-track-segment-loader.d.ts.map +1 -1
  353. package/dist/dev/playback/actors/text-track-segment-loader.js +24 -35
  354. package/dist/dev/playback/actors/text-track-segment-loader.js.map +1 -1
  355. package/dist/dev/playback/actors/text-tracks.d.ts +5 -8
  356. package/dist/dev/playback/actors/text-tracks.d.ts.map +1 -1
  357. package/dist/dev/playback/adapters/hls-audio/adapter.d.ts +11 -14
  358. package/dist/dev/playback/adapters/hls-audio/adapter.d.ts.map +1 -1
  359. package/dist/dev/playback/adapters/hls-audio/adapter.js +23 -31
  360. package/dist/dev/playback/adapters/hls-audio/adapter.js.map +1 -1
  361. package/dist/dev/playback/adapters/hls-audio/media.d.ts.map +1 -1
  362. package/dist/dev/playback/adapters/hls-audio/media.js.map +1 -1
  363. package/dist/dev/playback/adapters/hls-background-video/adapter.d.ts +28 -39
  364. package/dist/dev/playback/adapters/hls-background-video/adapter.d.ts.map +1 -1
  365. package/dist/dev/playback/adapters/hls-background-video/adapter.js +50 -70
  366. package/dist/dev/playback/adapters/hls-background-video/adapter.js.map +1 -1
  367. package/dist/dev/playback/adapters/hls-background-video/host.d.ts +12 -16
  368. package/dist/dev/playback/adapters/hls-background-video/host.d.ts.map +1 -1
  369. package/dist/dev/playback/adapters/hls-background-video/host.js +12 -16
  370. package/dist/dev/playback/adapters/hls-background-video/host.js.map +1 -1
  371. package/dist/dev/playback/adapters/hls-background-video/media.d.ts +5 -8
  372. package/dist/dev/playback/adapters/hls-background-video/media.d.ts.map +1 -1
  373. package/dist/dev/playback/adapters/hls-background-video/media.js +5 -8
  374. package/dist/dev/playback/adapters/hls-background-video/media.js.map +1 -1
  375. package/dist/dev/playback/adapters/hls-video/adapter.d.ts +12 -14
  376. package/dist/dev/playback/adapters/hls-video/adapter.d.ts.map +1 -1
  377. package/dist/dev/playback/adapters/hls-video/adapter.js +43 -60
  378. package/dist/dev/playback/adapters/hls-video/adapter.js.map +1 -1
  379. package/dist/dev/playback/adapters/hls-video/error-surface.d.ts +4 -5
  380. package/dist/dev/playback/adapters/hls-video/error-surface.d.ts.map +1 -1
  381. package/dist/dev/playback/adapters/hls-video/error-surface.js +13 -18
  382. package/dist/dev/playback/adapters/hls-video/error-surface.js.map +1 -1
  383. package/dist/dev/playback/adapters/hls-video/media-tracks.d.ts +4 -5
  384. package/dist/dev/playback/adapters/hls-video/media-tracks.d.ts.map +1 -1
  385. package/dist/dev/playback/adapters/hls-video/media-tracks.js +4 -5
  386. package/dist/dev/playback/adapters/hls-video/media-tracks.js.map +1 -1
  387. package/dist/dev/playback/adapters/hls-video/media.d.ts.map +1 -1
  388. package/dist/dev/playback/adapters/hls-video/media.js.map +1 -1
  389. package/dist/dev/playback/adapters/mux-audio/media.d.ts +8 -12
  390. package/dist/dev/playback/adapters/mux-audio/media.d.ts.map +1 -1
  391. package/dist/dev/playback/adapters/mux-audio/media.js +8 -12
  392. package/dist/dev/playback/adapters/mux-audio/media.js.map +1 -1
  393. package/dist/dev/playback/adapters/mux-video/adapter.d.ts +9 -11
  394. package/dist/dev/playback/adapters/mux-video/adapter.d.ts.map +1 -1
  395. package/dist/dev/playback/adapters/mux-video/adapter.js +25 -33
  396. package/dist/dev/playback/adapters/mux-video/adapter.js.map +1 -1
  397. package/dist/dev/playback/adapters/mux-video/media.d.ts +7 -9
  398. package/dist/dev/playback/adapters/mux-video/media.d.ts.map +1 -1
  399. package/dist/dev/playback/adapters/mux-video/media.js +7 -9
  400. package/dist/dev/playback/adapters/mux-video/media.js.map +1 -1
  401. package/dist/dev/playback/behaviors/calculate-presentation-duration.d.ts +5 -8
  402. package/dist/dev/playback/behaviors/calculate-presentation-duration.d.ts.map +1 -1
  403. package/dist/dev/playback/behaviors/calculate-presentation-duration.js +5 -8
  404. package/dist/dev/playback/behaviors/calculate-presentation-duration.js.map +1 -1
  405. package/dist/dev/playback/behaviors/collect-errors.d.ts +2 -0
  406. package/dist/dev/playback/behaviors/collect-errors.js +39 -52
  407. package/dist/dev/playback/behaviors/collect-errors.js.map +1 -1
  408. package/dist/dev/playback/behaviors/derive-cdn-priority.js +16 -23
  409. package/dist/dev/playback/behaviors/derive-cdn-priority.js.map +1 -1
  410. package/dist/dev/playback/behaviors/dom/airplay.js +38 -61
  411. package/dist/dev/playback/behaviors/dom/airplay.js.map +1 -1
  412. package/dist/dev/playback/behaviors/dom/apply-start-position.js +23 -35
  413. package/dist/dev/playback/behaviors/dom/apply-start-position.js.map +1 -1
  414. package/dist/dev/playback/behaviors/dom/end-of-stream.js +11 -16
  415. package/dist/dev/playback/behaviors/dom/end-of-stream.js.map +1 -1
  416. package/dist/dev/playback/behaviors/dom/load-segments.d.ts +5 -8
  417. package/dist/dev/playback/behaviors/dom/load-segments.d.ts.map +1 -1
  418. package/dist/dev/playback/behaviors/dom/load-segments.js +27 -39
  419. package/dist/dev/playback/behaviors/dom/load-segments.js.map +1 -1
  420. package/dist/dev/playback/behaviors/dom/recover-end-stall.js +21 -26
  421. package/dist/dev/playback/behaviors/dom/recover-end-stall.js.map +1 -1
  422. package/dist/dev/playback/behaviors/dom/seek-to-live-edge.js +41 -60
  423. package/dist/dev/playback/behaviors/dom/seek-to-live-edge.js.map +1 -1
  424. package/dist/dev/playback/behaviors/dom/setup-buffer-actors.js +50 -74
  425. package/dist/dev/playback/behaviors/dom/setup-buffer-actors.js.map +1 -1
  426. package/dist/dev/playback/behaviors/dom/setup-mediasource.js +29 -43
  427. package/dist/dev/playback/behaviors/dom/setup-mediasource.js.map +1 -1
  428. package/dist/dev/playback/behaviors/dom/setup-text-track-actors.d.ts +3 -3
  429. package/dist/dev/playback/behaviors/dom/setup-text-track-actors.d.ts.map +1 -1
  430. package/dist/dev/playback/behaviors/dom/setup-text-track-actors.js.map +1 -1
  431. package/dist/dev/playback/behaviors/dom/sync-live-seekable-range.js +3 -4
  432. package/dist/dev/playback/behaviors/dom/sync-live-seekable-range.js.map +1 -1
  433. package/dist/dev/playback/behaviors/dom/sync-text-tracks.js +30 -45
  434. package/dist/dev/playback/behaviors/dom/sync-text-tracks.js.map +1 -1
  435. package/dist/dev/playback/behaviors/dom/track-current-time.d.ts +2 -2
  436. package/dist/dev/playback/behaviors/dom/track-current-time.d.ts.map +1 -1
  437. package/dist/dev/playback/behaviors/dom/track-current-time.js +9 -11
  438. package/dist/dev/playback/behaviors/dom/track-current-time.js.map +1 -1
  439. package/dist/dev/playback/behaviors/dom/track-load-triggers.d.ts +6 -12
  440. package/dist/dev/playback/behaviors/dom/track-load-triggers.d.ts.map +1 -1
  441. package/dist/dev/playback/behaviors/dom/track-load-triggers.js +24 -32
  442. package/dist/dev/playback/behaviors/dom/track-load-triggers.js.map +1 -1
  443. package/dist/dev/playback/behaviors/dom/track-playback-rate.d.ts +2 -2
  444. package/dist/dev/playback/behaviors/dom/track-playback-rate.d.ts.map +1 -1
  445. package/dist/dev/playback/behaviors/dom/track-playback-rate.js +6 -8
  446. package/dist/dev/playback/behaviors/dom/track-playback-rate.js.map +1 -1
  447. package/dist/dev/playback/behaviors/dom/track-player-resolution.d.ts.map +1 -1
  448. package/dist/dev/playback/behaviors/dom/track-player-resolution.js +10 -14
  449. package/dist/dev/playback/behaviors/dom/track-player-resolution.js.map +1 -1
  450. package/dist/dev/playback/behaviors/dom/track-screen-resolution.js +8 -9
  451. package/dist/dev/playback/behaviors/dom/track-screen-resolution.js.map +1 -1
  452. package/dist/dev/playback/behaviors/dom/update-mediasource-duration.js +28 -43
  453. package/dist/dev/playback/behaviors/dom/update-mediasource-duration.js.map +1 -1
  454. package/dist/dev/playback/behaviors/establish-start-media-time.d.ts +15 -19
  455. package/dist/dev/playback/behaviors/establish-start-media-time.d.ts.map +1 -1
  456. package/dist/dev/playback/behaviors/establish-start-media-time.js +32 -42
  457. package/dist/dev/playback/behaviors/establish-start-media-time.js.map +1 -1
  458. package/dist/dev/playback/behaviors/resolve-presentation.d.ts.map +1 -1
  459. package/dist/dev/playback/behaviors/resolve-presentation.js +13 -22
  460. package/dist/dev/playback/behaviors/resolve-presentation.js.map +1 -1
  461. package/dist/dev/playback/behaviors/resolve-track.js +5 -12
  462. package/dist/dev/playback/behaviors/resolve-track.js.map +1 -1
  463. package/dist/dev/playback/behaviors/select-tracks.d.ts +28 -39
  464. package/dist/dev/playback/behaviors/select-tracks.d.ts.map +1 -1
  465. package/dist/dev/playback/behaviors/select-tracks.js +77 -104
  466. package/dist/dev/playback/behaviors/select-tracks.js.map +1 -1
  467. package/dist/dev/playback/behaviors/setup-failover-monitor.d.ts +1 -4
  468. package/dist/dev/playback/behaviors/setup-failover-monitor.d.ts.map +1 -1
  469. package/dist/dev/playback/behaviors/setup-failover-monitor.js +11 -16
  470. package/dist/dev/playback/behaviors/setup-failover-monitor.js.map +1 -1
  471. package/dist/dev/playback/behaviors/sync-preload.js +9 -13
  472. package/dist/dev/playback/behaviors/sync-preload.js.map +1 -1
  473. package/dist/dev/playback/behaviors/track-switching.d.ts +110 -0
  474. package/dist/dev/playback/behaviors/track-switching.d.ts.map +1 -0
  475. package/dist/dev/playback/behaviors/track-switching.js +189 -173
  476. package/dist/dev/playback/behaviors/track-switching.js.map +1 -1
  477. package/dist/dev/playback/engines/hls/engine-audio-only.d.ts +59 -72
  478. package/dist/dev/playback/engines/hls/engine-audio-only.d.ts.map +1 -1
  479. package/dist/dev/playback/engines/hls/engine-audio-only.js +20 -24
  480. package/dist/dev/playback/engines/hls/engine-audio-only.js.map +1 -1
  481. package/dist/dev/playback/engines/hls/engine-background-video.d.ts +51 -78
  482. package/dist/dev/playback/engines/hls/engine-background-video.d.ts.map +1 -1
  483. package/dist/dev/playback/engines/hls/engine-background-video.js +22 -27
  484. package/dist/dev/playback/engines/hls/engine-background-video.js.map +1 -1
  485. package/dist/dev/playback/engines/hls/engine.d.ts +115 -155
  486. package/dist/dev/playback/engines/hls/engine.d.ts.map +1 -1
  487. package/dist/dev/playback/engines/hls/engine.js +22 -25
  488. package/dist/dev/playback/engines/hls/engine.js.map +1 -1
  489. package/dist/dev/playback/primitives/derive-start-media-time.d.ts +3 -3
  490. package/dist/dev/playback/primitives/derive-start-media-time.d.ts.map +1 -1
  491. package/dist/dev/playback/primitives/error-messages.js +16 -25
  492. package/dist/dev/playback/primitives/error-messages.js.map +1 -1
  493. package/dist/dev/playback/primitives/failover-fetch.js +9 -14
  494. package/dist/dev/playback/primitives/failover-fetch.js.map +1 -1
  495. package/dist/dev/playback/primitives/head-peek.js +7 -10
  496. package/dist/dev/playback/primitives/head-peek.js.map +1 -1
  497. package/dist/dev/playback/primitives/live-window.js +12 -15
  498. package/dist/dev/playback/primitives/live-window.js.map +1 -1
  499. package/dist/dev/playback/primitives/relocation-pipelines.js +34 -46
  500. package/dist/dev/playback/primitives/relocation-pipelines.js.map +1 -1
  501. package/dist/dev/playback/primitives/report-track-conditions.d.ts +1 -4
  502. package/dist/dev/playback/primitives/report-track-conditions.d.ts.map +1 -1
  503. package/dist/dev/playback/primitives/report-track-conditions.js +26 -38
  504. package/dist/dev/playback/primitives/report-track-conditions.js.map +1 -1
  505. package/dist/dev/playback/primitives/segment-load-pipeline.js +15 -15
  506. package/dist/dev/playback/primitives/segment-load-pipeline.js.map +1 -1
  507. package/dist/dev/playback/primitives/selection-rules.d.ts +45 -17
  508. package/dist/dev/playback/primitives/selection-rules.d.ts.map +1 -1
  509. package/dist/dev/playback/primitives/selection-rules.js +65 -36
  510. package/dist/dev/playback/primitives/selection-rules.js.map +1 -1
  511. package/dist/dev/playback/primitives/source-buffer-messages.d.ts +6 -9
  512. package/dist/dev/playback/primitives/source-buffer-messages.d.ts.map +1 -1
  513. package/dist/dev/playback/primitives/text-segment-load-pipeline.d.ts +20 -29
  514. package/dist/dev/playback/primitives/text-segment-load-pipeline.d.ts.map +1 -1
  515. package/dist/dev/playback/primitives/text-segment-load-pipeline.js +4 -5
  516. package/dist/dev/playback/primitives/text-segment-load-pipeline.js.map +1 -1
  517. package/dist/dev/playback/primitives/text-track-messages.d.ts.map +1 -1
  518. package/dist/dev/playback/primitives/track-types.js +10 -18
  519. package/dist/dev/playback/primitives/track-types.js.map +1 -1
  520. package/package.json +25 -28
@@ -4,35 +4,32 @@ import { ChunkedStreamIterable } from "./chunked-stream-iterable.js";
4
4
  /**
5
5
  * HTTP Fetch Wrapper
6
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.
7
+ * Composable building blocks: - fetchResolvable() — fetch a Resource (handles byte ranges); returns Response -
8
+ * getResponseText() — extract text from Response - fetchResolvableStream() single-stage async generator over body
9
+ * chunks - fetchStream() — two-stage: await connection establishment, then lazily iterate body chunks. Use when timing
10
+ * the connection start independently of body consumption matters (e.g., observable fetch timing for ABR). -
11
+ * createTrackedFetch() — factory for a fetchStream-shape function that samples bandwidth (via EWMA) per chunk and
12
+ * notifies via callback.
16
13
  */
17
14
  /**
18
15
  * Fetch resolvable from a Resource.
19
16
  *
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
17
+ * Handles byte range requests if byteRange is present. Returns native fetch Response for composability (can extract
18
+ * text, stream, etc.).
25
19
  *
26
20
  * @example
27
- * const response = await fetchResolvable({ url: 'https://example.com/segment.m4s' });
28
- * const text = await getResponseText(response);
21
+ * const response = await fetchResolvable({ url: 'https://example.com/segment.m4s' });
22
+ * const text = await getResponseText(response);
29
23
  *
30
24
  * @example
31
- * // With byte range
32
- * const response = await fetchResolvable({
33
- * url: 'https://example.com/file.mp4',
34
- * byteRange: { start: 1000, end: 1999 }
35
- * });
25
+ * // With byte range
26
+ * const response = await fetchResolvable({
27
+ * url: 'https://example.com/file.mp4',
28
+ * byteRange: { start: 1000, end: 1999 },
29
+ * });
30
+ *
31
+ * @param addressable - Resource to fetch (url + optional byteRange)
32
+ * @returns Promise resolving to Response
36
33
  */
37
34
  async function fetchResolvable(addressable, options) {
38
35
  const headers = new Headers(options?.headers);
@@ -50,15 +47,14 @@ async function fetchResolvable(addressable, options) {
50
47
  /**
51
48
  * Extract text from Response.
52
49
  *
53
- * Accepts minimal Response-like object (just needs text() method).
54
- * Returns promise from response.text().
50
+ * Accepts minimal Response-like object (just needs text() method). Returns promise from response.text().
51
+ *
52
+ * @example
53
+ * const response = await fetchResolvable(addressable);
54
+ * const text = await getResponseText(response);
55
55
  *
56
56
  * @param response - Response-like object with text() method
57
57
  * @returns Promise resolving to text content
58
- *
59
- * @example
60
- * const response = await fetchResolvable(addressable);
61
- * const text = await getResponseText(response);
62
58
  */
63
59
  function getResponseText(response) {
64
60
  return response.text();
@@ -76,18 +72,15 @@ async function fetchStream(addressable, options) {
76
72
  return new ChunkedStreamIterable(response.body, ...minChunkSize !== void 0 ? [{ minChunkSize }] : []);
77
73
  }
78
74
  /**
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.
75
+ * Returns a {@link FetchBytes} function that samples bandwidth via EWMA per body chunk. The factory captures the
76
+ * running bandwidth state internally; per chunk it computes the next state and notifies the supplied `onSample`
77
+ * callback.
83
78
  *
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.
79
+ * The factory's internal accumulator is seeded from `initial` and updated on every chunk; callers don't need to thread
80
+ * it back in. `onSample` receives the _new_ state after each chunk typical use is to bridge samples back into engine
81
+ * state for ABR consumers.
88
82
  *
89
- * @param initial - Starting `BandwidthState` (commonly zeros or the
90
- * engine's current accumulator).
83
+ * @param initial - Starting `BandwidthState` (commonly zeros or the engine's current accumulator).
91
84
  * @param onSample - Called with the new `BandwidthState` after each chunk.
92
85
  */
93
86
  function createTrackedFetch(initial, onSample) {
@@ -1 +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,OAAO;CAG5C,IAAI,YAAY,WAAW;EACzB,MAAM,EAAE,OAAO,QAAQ,YAAY;EACnC,QAAQ,IAAI,SAAS,SAAS,MAAM,GAAG,KAAK;CAC9C;CAEA,MAAM,UAAU,IAAI,QAAQ,YAAY,KAAK;EAC3C,QAAQ;EACR;EACA,GAAG;CACL,CAAC;CAED,OAAO,MAAM,OAAO;AACtB;;;;;;;;;;;;;;AA+CA,SAAgB,gBAAgB,UAAyC;CACvE,OAAO,SAAS,KAAK;AACvB;;AAWA,MAAa,sBAAiC,OAAO,aAAa,YAAY;CAC5E,MAAM,WAAW,MAAM,gBAAgB,aAAa,OAAO;CAC3D,IAAI,CAAC,SAAS,IACZ,MAAM,IAAI,MAAM,wBAAwB,SAAS,OAAO,GAAG,SAAS,WAAW,OAAO,YAAY,KAAK;CAEzG,OAAO,gBAAgB,QAAQ;AACjC;AAiBA,eAAsB,YAAY,aAAuB,SAA4D;CACnH,MAAM,EAAE,cAAc,GAAG,iBAAiB,WAAW,CAAC;CACtD,MAAM,WAAW,MAAM,gBAAgB,aAAa,YAAY;CAChE,IAAI,CAAC,SAAS,MAAM,MAAM,IAAI,MAAM,sBAAsB;CAC1D,OAAO,IAAI,sBAAsB,SAAS,MAAM,GAAI,iBAAiB,KAAA,IAAY,CAAC,EAAE,aAAa,CAAC,IAAI,CAAC,CAAE;AAC3G;;;;;;;;;;;;;;;;AAiBA,SAAgB,mBAAmB,SAAyB,UAAsD;CAChH,IAAI,QAAQ;CACZ,OAAO,OAAO,aAAa,YAAY;EACrC,MAAM,EAAE,cAAc,GAAG,iBAAiB,WAAW,CAAC;EACtD,MAAM,WAAW,MAAM,gBAAgB,aAAa,YAAY;EAChE,IAAI,CAAC,SAAS,MAAM,MAAM,IAAI,MAAM,sBAAsB;EAC1D,MAAM,OAAO,SAAS;EACtB,OAAO,EACL,CAAC,OAAO,gBAAgB,mBAAmB;GACzC,IAAI,aAAa,YAAY,IAAI;GACjC,WAAW,MAAM,SAAS,IAAI,sBAC5B,MACA,GAAI,iBAAiB,KAAA,IAAY,CAAC,EAAE,aAAa,CAAC,IAAI,CAAC,CACzD,GAAG;IACD,MAAM,UAAU,YAAY,IAAI,IAAI;IACpC,QAAQ,gBAAgB,OAAO,SAAS,MAAM,UAAU;IACxD,SAAS,KAAK;IACd,MAAM;IACN,aAAa,YAAY,IAAI;GAC/B;EACF,EACF;CACF;AACF"}
1
+ {"version":3,"file":"fetch.js","names":[],"sources":["../../../src/network/fetch.ts"],"sourcesContent":["/**\n * HTTP Fetch Wrapper\n *\n * Composable building blocks: - fetchResolvable() — fetch a Resource (handles byte ranges); returns Response -\n * getResponseText() — extract text from Response - fetchResolvableStream() — single-stage async generator over body\n * chunks - fetchStream() — two-stage: await connection establishment, then lazily iterate body chunks. Use when timing\n * the connection start independently of body consumption matters (e.g., observable fetch timing for ABR). -\n * createTrackedFetch() — factory for a fetchStream-shape function that samples bandwidth (via EWMA) per chunk and\n * notifies via callback.\n */\n\nimport { type BandwidthState, sampleBandwidth } from './bandwidth-estimator';\nimport { ChunkedStreamIterable, type ChunkedStreamIterableOptions } from './chunked-stream-iterable';\n\n/** Minimal Response-like interface for text extraction. Allows testing without full Response object. */\nexport interface ResponseLike {\n text(): Promise<string>;\n}\n\n/**\n * An HTTP-addressable resource — URL plus optional byte range. Media's `AddressableObject` (and anything else with the\n * same shape) is structurally compatible; kept local so this module stays 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. Returns native fetch Response for composability (can extract\n * text, stream, etc.).\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 *\n * @param addressable - Resource to fetch (url + optional byteRange)\n * @returns Promise resolving to Response\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\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 ArrayBuffer. Use this when you need the raw\n * bytes (e.g. segment appends). 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\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 via ChunkedStreamIterable. Headers are\n * awaited before the first chunk is yielded (TTFB is accounted for before iteration begins).\n *\n * Throws if the response body is null (e.g. non-body HTTP status). Errors from the underlying stream propagate\n * 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\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). Returns promise from response.text().\n *\n * @example\n * const response = await fetchResolvable(addressable);\n * const text = await getResponseText(response);\n *\n * @param response - Response-like object with text() method\n * @returns Promise resolving to text content\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 {@link FetchBytes}. A non-OK status rejects, so HTTP\n * failures surface as rejections that callers (and decorators like the failover tracker) handle uniformly with network\n * 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\n if (!response.ok) {\n throw new Error(`fetchResolvableText: ${response.status} ${response.statusText} for ${addressable.url}`);\n }\n\n return getResponseText(response);\n};\n\n/**\n * Two-stage fetch helper: eagerly starts the HTTP request (TTFB is awaited), then returns a lazy iterable over the\n * response body. Separating connection 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 `fetch` only when iteration starts). Pick\n * `fetchStream` when \"when did the fetch start\" needs to be observable separately from \"when did the body begin\n * 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\n return new ChunkedStreamIterable(response.body, ...(minChunkSize !== undefined ? [{ minChunkSize }] : []));\n}\n\n/**\n * Returns a {@link FetchBytes} function that samples bandwidth via EWMA per body chunk. The factory captures the\n * running bandwidth state internally; per chunk it computes the next state and notifies the supplied `onSample`\n * callback.\n *\n * The factory's internal accumulator is seeded from `initial` and updated on every chunk; callers don't need to thread\n * it back in. `onSample` receives the _new_ state after each chunk — typical use is to bridge samples back into engine\n * state for ABR consumers.\n *\n * @param initial - Starting `BandwidthState` (commonly zeros or the 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\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\n const body = response.body;\n\n return {\n [Symbol.asyncIterator]: async function* () {\n let chunkStart = performance.now();\n\n for await (const chunk of new ChunkedStreamIterable(\n body,\n ...(minChunkSize !== undefined ? [{ minChunkSize }] : [])\n )) {\n const elapsed = performance.now() - chunkStart;\n\n state = sampleBandwidth(state, elapsed, chunk.byteLength);\n onSample(state);\n yield chunk;\n chunkStart = performance.now();\n }\n },\n };\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmDA,eAAsB,gBAAgB,aAAuB,SAA0C;CACrG,MAAM,UAAU,IAAI,QAAQ,SAAS,OAAO;CAG5C,IAAI,YAAY,WAAW;EACzB,MAAM,EAAE,OAAO,QAAQ,YAAY;EAEnC,QAAQ,IAAI,SAAS,SAAS,MAAM,GAAG,KAAK;CAC9C;CAEA,MAAM,UAAU,IAAI,QAAQ,YAAY,KAAK;EAC3C,QAAQ;EACR;EACA,GAAG;CACL,CAAC;CAED,OAAO,MAAM,OAAO;AACtB;;;;;;;;;;;;;AA8CA,SAAgB,gBAAgB,UAAyC;CACvE,OAAO,SAAS,KAAK;AACvB;;AAUA,MAAa,sBAAiC,OAAO,aAAa,YAAY;CAC5E,MAAM,WAAW,MAAM,gBAAgB,aAAa,OAAO;CAE3D,IAAI,CAAC,SAAS,IACZ,MAAM,IAAI,MAAM,wBAAwB,SAAS,OAAO,GAAG,SAAS,WAAW,OAAO,YAAY,KAAK;CAGzG,OAAO,gBAAgB,QAAQ;AACjC;AAeA,eAAsB,YAAY,aAAuB,SAA4D;CACnH,MAAM,EAAE,cAAc,GAAG,iBAAiB,WAAW,CAAC;CACtD,MAAM,WAAW,MAAM,gBAAgB,aAAa,YAAY;CAChE,IAAI,CAAC,SAAS,MAAM,MAAM,IAAI,MAAM,sBAAsB;CAE1D,OAAO,IAAI,sBAAsB,SAAS,MAAM,GAAI,iBAAiB,KAAA,IAAY,CAAC,EAAE,aAAa,CAAC,IAAI,CAAC,CAAE;AAC3G;;;;;;;;;;;;;AAcA,SAAgB,mBAAmB,SAAyB,UAAsD;CAChH,IAAI,QAAQ;CAEZ,OAAO,OAAO,aAAa,YAAY;EACrC,MAAM,EAAE,cAAc,GAAG,iBAAiB,WAAW,CAAC;EACtD,MAAM,WAAW,MAAM,gBAAgB,aAAa,YAAY;EAChE,IAAI,CAAC,SAAS,MAAM,MAAM,IAAI,MAAM,sBAAsB;EAE1D,MAAM,OAAO,SAAS;EAEtB,OAAO,EACL,CAAC,OAAO,gBAAgB,mBAAmB;GACzC,IAAI,aAAa,YAAY,IAAI;GAEjC,WAAW,MAAM,SAAS,IAAI,sBAC5B,MACA,GAAI,iBAAiB,KAAA,IAAY,CAAC,EAAE,aAAa,CAAC,IAAI,CAAC,CACzD,GAAG;IACD,MAAM,UAAU,YAAY,IAAI,IAAI;IAEpC,QAAQ,gBAAgB,OAAO,SAAS,MAAM,UAAU;IACxD,SAAS,KAAK;IACd,MAAM;IACN,aAAa,YAAY,IAAI;GAC/B;EACF,EACF;CACF;AACF"}
@@ -7,11 +7,9 @@ import { DEFAULT_BACK_BUFFER_CONFIG, calculateBackBufferFlushPoint } from "../..
7
7
  import { DEFAULT_MESSAGE_PIPELINES } from "../../primitives/segment-load-pipeline.js";
8
8
  //#region src/playback/actors/dom/segment-loader.ts
9
9
  /**
10
- * Wraps a LoadTask descriptor into a Task that runs the op's message pipeline
11
- * (fetch/discover/stamp/dispatch, per the composition's `messagePipelines`).
12
- * Updates in-flight context around the async region so the loading handler can
13
- * make accurate continue/preempt decisions at any point, and checks the abort
14
- * signal before each step.
10
+ * Wraps a LoadTask descriptor into a Task that runs the op's message pipeline (fetch/discover/stamp/dispatch, per the
11
+ * composition's `messagePipelines`). Updates in-flight context around the async region so the loading handler can make
12
+ * accurate continue/preempt decisions at any point, and checks the abort signal before each step.
15
13
  */
16
14
  function makeLoadTask(op, { getContext, setContext, pipelines, deps }) {
17
15
  return new Task(async (taskSignal) => {
@@ -51,25 +49,20 @@ function makeLoadTask(op, { getContext, setContext, pipelines, deps }) {
51
49
  /**
52
50
  * Creates a SegmentLoaderActor for one track type (video or audio).
53
51
  *
54
- * Receives load assignments via `send()` and owns all execution: planning,
55
- * removes, fetches, and appends. Coordinates with the SourceBufferActor for
56
- * all physical SourceBuffer operations.
52
+ * Receives load assignments via `send()` and owns all execution: planning, removes, fetches, and appends. Coordinates
53
+ * with the SourceBufferActor for all physical SourceBuffer operations.
57
54
  *
58
- * Planning (Cases 1–3) happens in the `load` handler on every incoming
59
- * message, producing an ordered LoadTask list. The runner drains that list
60
- * sequentially via SerialRunner. When a new message arrives mid-run, the
61
- * handler replans and either continues the in-flight operation (abortPending
62
- * + schedule new remainder) or preempts it (abortAll + cancel SourceBuffer
63
- * if needed + schedule new plan).
55
+ * Planning (Cases 1–3) happens in the `load` handler on every incoming message, producing an ordered LoadTask list. The
56
+ * runner drains that list sequentially via SerialRunner. When a new message arrives mid-run, the handler replans and
57
+ * either continues the in-flight operation (abortPending + schedule new remainder) or preempts it (abortAll + cancel
58
+ * SourceBuffer if needed + schedule new plan).
64
59
  *
65
60
  * @param sourceBufferActor - Shared SourceBufferActor reference (not owned)
66
- * @param fetchBytes - Tracked fetch closure (owns throughput sampling for segments).
67
- * Accepts an optional `minChunkSize` in options; init segments pass `Infinity`
68
- * so the entire body accumulates as one chunk before appending.
69
- * @param compositionDeps - The composition's `state`/`context`/`config`, threaded
70
- * opaquely into each step's {@link StepDeps} (the loader never reads them). Lets
71
- * injected steps (relocation) read composition signals at call time. Defaults to
72
- * empty for standalone / base-pipeline use.
61
+ * @param fetchBytes - Tracked fetch closure (owns throughput sampling for segments). Accepts an optional `minChunkSize`
62
+ * in options; init segments pass `Infinity` so the entire body accumulates as one chunk before appending.
63
+ * @param compositionDeps - The composition's `state`/`context`/`config`, threaded opaquely into each step's
64
+ * {@link StepDeps} (the loader never reads them). Lets injected steps (relocation) read composition signals at call
65
+ * time. Defaults to empty for standalone / base-pipeline use.
73
66
  */
74
67
  function createSegmentLoaderActor(sourceBufferActor, fetchBytes, config = {}, compositionDeps = {
75
68
  state: {},
@@ -95,36 +88,27 @@ function createSegmentLoaderActor(sourceBufferActor, fetchBytes, config = {}, co
95
88
  };
96
89
  const pipelines = (config.messagePipelines ?? DEFAULT_MESSAGE_PIPELINES)();
97
90
  const getBufferedSegments = (allSegments) => {
98
- const merged = mergeTimeRanges(peek(sourceBufferActor.snapshot).context.segments.filter((s) => !s.partial).map((s) => ({
91
+ const appended = peek(sourceBufferActor.snapshot).context.segments.filter((s) => !s.partial);
92
+ const merged = mergeTimeRanges(appended.map((s) => ({
99
93
  start: s.startTime,
100
94
  end: s.startTime + s.duration
101
95
  })));
102
96
  return allSegments.filter((s) => isTimeRangeCovered(s.startTime, s.startTime + s.duration, merged));
103
97
  };
104
98
  /**
105
- * Translate a load message into an ordered LoadTask list based on committed
106
- * actor state. In-flight awareness is handled separately in the load handler.
99
+ * Translate a load message into an ordered LoadTask list based on committed actor state. In-flight awareness is
100
+ * handled separately in the load handler.
107
101
  *
108
- * @todo Rename alongside LoadTask (e.g. planOps).
109
- *
110
- * Case 1 Removes: forward and back buffer flush points, segment-aligned.
111
- * ABR-style track switches (same content, different bitrate) do not flush:
112
- * appending new content overwrites existing buffer ranges, and the actor's
113
- * time-aligned deduplication keeps the segment model accurate as new
114
- * segments arrive.
115
- *
116
- * Cross-rendition track switches (audio language change, text language
117
- * change) do flush: the buffered content is semantically incompatible with
118
- * the newly-selected track, so overwrite-on-append would leave stale
119
- * content playing until each replacement segment lands. Today's predicate:
120
- * `actorCtx.initTrackLanguage !== track.language` — fires for language
121
- * changes, no-ops for video / same-language audio bitrate switches.
122
- * Future stage: pluggable predicate / strategy at actor construction time
123
- * for codec-change (5.1 surround) and other cross-rendition shapes.
124
- *
125
- * Case 2 — Init: schedule if not yet committed for this track.
126
- *
127
- * Case 3 — Segments: all segments in the load window not yet committed.
102
+ * @todo Rename alongside LoadTask (e.g. planOps). Case 1 — Removes: forward and back buffer flush points,
103
+ * segment-aligned. ABR-style track switches (same content, different bitrate) do not flush: appending new content
104
+ * overwrites existing buffer ranges, and the actor's time-aligned deduplication keeps the segment model accurate as
105
+ * new segments arrive. Cross-rendition track switches (audio language change, text language change) do flush: the
106
+ * buffered content is semantically incompatible with the newly-selected track, so overwrite-on-append would leave
107
+ * stale content playing until each replacement segment lands. Today's predicate: `actorCtx.initTrackLanguage !==
108
+ * track.language` — fires for language changes, no-ops for video / same-language audio bitrate switches. Future
109
+ * stage: pluggable predicate / strategy at actor construction time for codec-change (5.1 surround) and other
110
+ * cross-rendition shapes. Case 2 — Init: schedule if not yet committed for this track. Case 3 Segments: all
111
+ * segments in the load window not yet committed.
128
112
  */
129
113
  const planTasks = (message) => {
130
114
  const { track, range } = message;
@@ -1 +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 { 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 isTimeRangeCovered,\n mergeTimeRanges,\n} from '../../../media/buffer/forward-buffer';\nimport { type AudioTrack, SEGMENT_TIME_EPSILON, type Segment, type VideoTrack } from '../../../media/types';\nimport {\n DEFAULT_MESSAGE_PIPELINES,\n type FetchBytes,\n type Frame,\n type LoadStep,\n type LoadTask,\n type MessagePipelines,\n type StepDeps,\n} from '../../primitives/segment-load-pipeline';\nimport type { 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// 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 /**\n * Identity of the segment currently being fetched/appended, or null. Tracks\n * the `trackId` alongside the positional `id` because renditions number\n * segments independently (`segment-N`): an in-flight LOW `segment-0` must not\n * be mistaken for a needed HIGH `segment-0` on a switch (see the loading\n * handler's continue/preempt decision). Mirrors the text-track loader, which\n * already matches its in-flight segment on `(inFlightTrackId, inFlightSegmentId)`.\n */\n inFlightSegment: { id: string; trackId: 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 /** Per-message-type step pipelines. Defaults to {@link DEFAULT_MESSAGE_PIPELINES} (`fetch → dispatch`). */\n messagePipelines?: MessagePipelines;\n}\n\n// ============================================================================\n// LOAD TASK FACTORY\n// ============================================================================\n\ninterface LoadTaskOptions {\n getContext: () => SegmentLoaderActorContext;\n setContext: (ctx: SegmentLoaderActorContext) => void;\n pipelines: Record<LoadTask['type'], LoadStep[]>;\n deps: StepDeps;\n}\n\n/**\n * Wraps a LoadTask descriptor into a Task that runs the op's message pipeline\n * (fetch/discover/stamp/dispatch, per the composition's `messagePipelines`).\n * Updates in-flight context around the async region so the loading handler can\n * make accurate continue/preempt decisions at any point, and checks the abort\n * signal before each step.\n */\nfunction makeLoadTask(op: LoadTask, { getContext, setContext, pipelines, deps }: LoadTaskOptions): Task<void> {\n return new Task(async (taskSignal) => {\n if (taskSignal.aborted) return;\n\n const frame: Frame = op.type === 'append-segment' ? { op, meta: op.meta } : { op };\n\n // In-flight bookkeeping brackets the async region; the `finally` resets it\n // even if a step aborts or throws mid-pipeline. Only append ops track it.\n try {\n if (op.type === 'append-init') setContext({ ...getContext(), inFlightInitTrackId: op.meta.trackId });\n else if (op.type === 'append-segment')\n setContext({ ...getContext(), inFlightSegment: { id: op.meta.id, trackId: op.meta.trackId } });\n\n for (const step of pipelines[op.type]) {\n if (taskSignal.aborted) return;\n await step(frame, taskSignal, deps);\n }\n } finally {\n if (op.type === 'append-init') setContext({ ...getContext(), inFlightInitTrackId: null });\n else if (op.type === 'append-segment') setContext({ ...getContext(), inFlightSegment: 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 * @param compositionDeps - The composition's `state`/`context`/`config`, threaded\n * opaquely into each step's {@link StepDeps} (the loader never reads them). Lets\n * injected steps (relocation) read composition signals at call time. Defaults to\n * empty for standalone / base-pipeline use.\n */\nexport function createSegmentLoaderActor(\n sourceBufferActor: SourceBufferActor,\n fetchBytes: FetchBytes,\n config: SegmentLoaderActorConfig = {},\n compositionDeps: StepDeps = { state: {}, context: {}, config: {} }\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 // Fold the loader's own wiring into the passthrough `config` (see `stepWiring`) so\n // base steps read it from the uniform `{state,context,config}` — present in both\n // composition and standalone use.\n const deps: StepDeps = {\n state: compositionDeps.state,\n context: compositionDeps.context,\n config: { ...compositionDeps.config, sourceBufferActor, fetch: fetchBytes },\n };\n // Built once per actor (fresh stateful steps per source); default is `fetch → dispatch`.\n const pipelines = (config.messagePipelines ?? DEFAULT_MESSAGE_PIPELINES)();\n\n const getBufferedSegments = (allSegments: readonly Segment[]): Segment[] => {\n // A candidate segment counts as \"already buffered\" only when its whole time\n // span is covered by appended content — a TIME question, not a positional-id\n // one. Renditions are numbered independently per playlist (`segment-N`), so\n // matching by id assumes every rung shares the same segment grid. That fails\n // for renditions whose boundaries don't align (e.g. a 30fps rung cuts its\n // first GOP at 7.13s, a 60fps rung at 7.98s): the switched-to rung's segment\n // reuses a buffered id but spans a *later*-ending range, so an id match would\n // skip it and leave its uncovered tail as a gap. Covering by time refetches\n // exactly that straddling segment (MSE overwrites the overlap on append).\n //\n // Exclude partial segments — they are still streaming and must not count as\n // 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 appended = peek(sourceBufferActor.snapshot).context.segments.filter((s) => !s.partial);\n const merged = mergeTimeRanges(appended.map((s) => ({ start: s.startTime, end: s.startTime + s.duration })));\n return allSegments.filter((s) => isTimeRangeCovered(s.startTime, s.startTime + s.duration, merged));\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.schedule(makeLoadTask(op, { getContext, setContext, pipelines, deps })).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, inFlightSegment: 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. The\n // segment match requires BOTH the positional id AND the trackId:\n // renditions number segments independently, so an in-flight LOW\n // `segment-0` (spanning [0, 7.13]) must NOT be treated as covering a\n // switched-to HIGH `segment-0` (spanning [0, 7.98]). Matching by id\n // alone kept the LOW fetch, skipped HIGH's leading segment, and left\n // its tail as a gap — intermittently, only when the switch beat the\n // in-flight append. A track switch now always preempts (correct: the\n // new rendition's same-slot segment must be (re)fetched).\n const inFlight = context.inFlightSegment;\n const segmentInFlightStillNeeded = (t: LoadTask): boolean =>\n t.type === 'append-segment' &&\n inFlight !== null &&\n t.meta.id === inFlight.id &&\n t.meta.trackId === inFlight.trackId;\n const inFlightStillNeeded =\n (inFlight !== null && allTasks.some(segmentInFlightStillNeeded)) ||\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 !segmentInFlightStillNeeded(t) &&\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.inFlightSegment !== 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
+ {"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 { 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 isTimeRangeCovered,\n mergeTimeRanges,\n} from '../../../media/buffer/forward-buffer';\nimport { type AudioTrack, SEGMENT_TIME_EPSILON, type Segment, type VideoTrack } from '../../../media/types';\nimport {\n DEFAULT_MESSAGE_PIPELINES,\n type FetchBytes,\n type Frame,\n type LoadStep,\n type LoadTask,\n type MessagePipelines,\n type StepDeps,\n} from '../../primitives/segment-load-pipeline';\nimport type { SourceBufferActor } from './source-buffer';\n\n// ============================================================================\n// BUFFER STATE TYPES\n// ============================================================================\n\n/** Buffer state for a single SourceBuffer. Tracks which init segment and media segments are loaded. */\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/** Buffer state for all SourceBuffers. */\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: - No range: load init segment only (metadata preload mode) - With\n * range: load init + all segments overlapping [start, end]\n *\n * `start` and `end` are raw time values — no segment snapping. 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// 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 /**\n * Identity of the segment currently being fetched/appended, or null. Tracks the `trackId` alongside the positional\n * `id` because renditions number segments independently (`segment-N`): an in-flight LOW `segment-0` must not be\n * mistaken for a needed HIGH `segment-0` on a switch (see the loading handler's continue/preempt decision). Mirrors\n * the text-track loader, which already matches its in-flight segment on `(inFlightTrackId, inFlightSegmentId)`.\n */\n inFlightSegment: { id: string; trackId: string } | null;\n}\n\nexport type SegmentLoaderActor = MessageActor<SegmentLoaderActorState, SegmentLoaderActorContext, SegmentLoaderMessage>;\n\n/**\n * Configuration for `createSegmentLoaderActor`. Each sub-config is spread over the corresponding `DEFAULT_*_CONFIG` so\n * callers can override individual fields.\n */\nexport interface SegmentLoaderActorConfig {\n forwardBuffer?: Partial<ForwardBufferConfig>;\n backBuffer?: Partial<BackBufferConfig>;\n /** Per-message-type step pipelines. Defaults to {@link DEFAULT_MESSAGE_PIPELINES} (`fetch → dispatch`). */\n messagePipelines?: MessagePipelines;\n}\n\n// ============================================================================\n// LOAD TASK FACTORY\n// ============================================================================\n\ninterface LoadTaskOptions {\n getContext: () => SegmentLoaderActorContext;\n setContext: (ctx: SegmentLoaderActorContext) => void;\n pipelines: Record<LoadTask['type'], LoadStep[]>;\n deps: StepDeps;\n}\n\n/**\n * Wraps a LoadTask descriptor into a Task that runs the op's message pipeline (fetch/discover/stamp/dispatch, per the\n * composition's `messagePipelines`). Updates in-flight context around the async region so the loading handler can make\n * accurate continue/preempt decisions at any point, and checks the abort signal before each step.\n */\nfunction makeLoadTask(op: LoadTask, { getContext, setContext, pipelines, deps }: LoadTaskOptions): Task<void> {\n return new Task(async (taskSignal) => {\n if (taskSignal.aborted) return;\n\n const frame: Frame = op.type === 'append-segment' ? { op, meta: op.meta } : { op };\n\n // In-flight bookkeeping brackets the async region; the `finally` resets it\n // even if a step aborts or throws mid-pipeline. Only append ops track it.\n try {\n if (op.type === 'append-init') setContext({ ...getContext(), inFlightInitTrackId: op.meta.trackId });\n else if (op.type === 'append-segment')\n setContext({ ...getContext(), inFlightSegment: { id: op.meta.id, trackId: op.meta.trackId } });\n\n for (const step of pipelines[op.type]) {\n if (taskSignal.aborted) return;\n\n await step(frame, taskSignal, deps);\n }\n } finally {\n if (op.type === 'append-init') setContext({ ...getContext(), inFlightInitTrackId: null });\n else if (op.type === 'append-segment') setContext({ ...getContext(), inFlightSegment: 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, removes, fetches, and appends. Coordinates\n * with the SourceBufferActor for all physical SourceBuffer operations.\n *\n * Planning (Cases 1–3) happens in the `load` handler on every incoming message, producing an ordered LoadTask list. The\n * runner drains that list sequentially via SerialRunner. When a new message arrives mid-run, the handler replans and\n * either continues the in-flight operation (abortPending + schedule new remainder) or preempts it (abortAll + cancel\n * SourceBuffer 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). Accepts an optional `minChunkSize`\n * in options; init segments pass `Infinity` so the entire body accumulates as one chunk before appending.\n * @param compositionDeps - The composition's `state`/`context`/`config`, threaded opaquely into each step's\n * {@link StepDeps} (the loader never reads them). Lets injected steps (relocation) read composition signals at call\n * time. Defaults to empty for standalone / base-pipeline use.\n */\nexport function createSegmentLoaderActor(\n sourceBufferActor: SourceBufferActor,\n fetchBytes: FetchBytes,\n config: SegmentLoaderActorConfig = {},\n compositionDeps: StepDeps = { state: {}, context: {}, config: {} }\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 // Fold the loader's own wiring into the passthrough `config` (see `stepWiring`) so\n // base steps read it from the uniform `{state,context,config}` — present in both\n // composition and standalone use.\n const deps: StepDeps = {\n state: compositionDeps.state,\n context: compositionDeps.context,\n config: { ...compositionDeps.config, sourceBufferActor, fetch: fetchBytes },\n };\n // Built once per actor (fresh stateful steps per source); default is `fetch → dispatch`.\n const pipelines = (config.messagePipelines ?? DEFAULT_MESSAGE_PIPELINES)();\n\n const getBufferedSegments = (allSegments: readonly Segment[]): Segment[] => {\n // A candidate segment counts as \"already buffered\" only when its whole time\n // span is covered by appended content — a TIME question, not a positional-id\n // one. Renditions are numbered independently per playlist (`segment-N`), so\n // matching by id assumes every rung shares the same segment grid. That fails\n // for renditions whose boundaries don't align (e.g. a 30fps rung cuts its\n // first GOP at 7.13s, a 60fps rung at 7.98s): the switched-to rung's segment\n // reuses a buffered id but spans a *later*-ending range, so an id match would\n // skip it and leave its uncovered tail as a gap. Covering by time refetches\n // exactly that straddling segment (MSE overwrites the overlap on append).\n //\n // Exclude partial segments — they are still streaming and must not count as\n // 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 appended = peek(sourceBufferActor.snapshot).context.segments.filter((s) => !s.partial);\n const merged = mergeTimeRanges(appended.map((s) => ({ start: s.startTime, end: s.startTime + s.duration })));\n\n return allSegments.filter((s) => isTimeRangeCovered(s.startTime, s.startTime + s.duration, merged));\n };\n\n /**\n * Translate a load message into an ordered LoadTask list based on committed actor state. In-flight awareness is\n * handled separately in the load handler.\n *\n * @todo Rename alongside LoadTask (e.g. planOps). Case 1 — Removes: forward and back buffer flush points,\n * segment-aligned. ABR-style track switches (same content, different bitrate) do not flush: appending new content\n * overwrites existing buffer ranges, and the actor's time-aligned deduplication keeps the segment model accurate as\n * new segments arrive. Cross-rendition track switches (audio language change, text language change) do flush: the\n * buffered content is semantically incompatible with the newly-selected track, so overwrite-on-append would leave\n * stale content playing until each replacement segment lands. Today's predicate: `actorCtx.initTrackLanguage !==\n * track.language` — fires for language changes, no-ops for video / same-language audio bitrate switches. Future\n * stage: pluggable predicate / strategy at actor construction time for codec-change (5.1 surround) and other\n * cross-rendition shapes. Case 2 — Init: schedule if not yet committed for this track. Case 3 — Segments: all\n * 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\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\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\n if (staleStart !== undefined) {\n staleRanges.push({ start: staleStart, end: Infinity });\n }\n }\n\n const forwardFlushStart = calculateForwardFlushPoint(bufferedSegments, currentTime, forwardBufferConfig);\n\n if (forwardFlushStart < Infinity) {\n removes.push({ start: forwardFlushStart, end: Infinity });\n }\n\n const backFlushEnd = calculateBackBufferFlushPoint(bufferedSegments, currentTime, backBufferConfig);\n\n if (backFlushEnd > 0) {\n removes.push({ start: 0, end: backFlushEnd });\n }\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\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\n if (!existing?.trackBandwidth || !track.bandwidth) return true;\n\n return track.bandwidth > existing.trackBandwidth;\n });\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.schedule(makeLoadTask(op, { getContext, setContext, pipelines, deps })).then(undefined, (e: unknown) => {\n if (e instanceof Error && e.name === 'AbortError') return;\n\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, inFlightSegment: null },\n states: {\n idle: {\n on: {\n load: (msg, ctx) => {\n const allTasks = planTasks(msg);\n if (allTasks.length === 0) return;\n\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. The\n // segment match requires BOTH the positional id AND the trackId:\n // renditions number segments independently, so an in-flight LOW\n // `segment-0` (spanning [0, 7.13]) must NOT be treated as covering a\n // switched-to HIGH `segment-0` (spanning [0, 7.98]). Matching by id\n // alone kept the LOW fetch, skipped HIGH's leading segment, and left\n // its tail as a gap — intermittently, only when the switch beat the\n // in-flight append. A track switch now always preempts (correct: the\n // new rendition's same-slot segment must be (re)fetched).\n const inFlight = context.inFlightSegment;\n const segmentInFlightStillNeeded = (t: LoadTask): boolean =>\n t.type === 'append-segment' &&\n inFlight !== null &&\n t.meta.id === inFlight.id &&\n t.meta.trackId === inFlight.trackId;\n const inFlightStillNeeded =\n (inFlight !== null && allTasks.some(segmentInFlightStillNeeded)) ||\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 !segmentInFlightStillNeeded(t) &&\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.inFlightSegment !== null ||\n (context.inFlightInitTrackId !== null &&\n allTasks.some((t) => t.type === 'append-init' && t.meta.trackId !== context.inFlightInitTrackId));\n\n if (cancelSourceBuffer) {\n sourceBufferActor.send({ type: 'cancel' });\n }\n\n scheduleAll(allTasks, ctx);\n }\n },\n },\n },\n },\n });\n}\n"],"mappings":"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"}
@@ -1 +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';\nimport type {\n AppendInitMessage,\n AppendSegmentMessage,\n IndividualSourceBufferMessage,\n RemoveMessage,\n} from '../../primitives/source-buffer-messages';\n\n// =============================================================================\n// Types\n// =============================================================================\n\nexport interface BufferedRange {\n start: number;\n end: number;\n}\n\nexport type { AppendData };\n\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 //\n // Misaligned renditions (e.g. 30fps vs 60fps rungs) break the same-slot\n // assumption: the switched-to segment's differing startTime is kept as a\n // separate overlapping entry rather than truncating the older one to the\n // portion MSE didn't overwrite. Cosmetic today (see #1865 for the\n // most-recent-append-wins model); coverage/flush stay correct.\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 // Relocation: set the offset before the coded frames are appended. The\n // SerialRunner guarantees the buffer is idle here, so the assignment is safe.\n // Guarded so re-stamping the (constant) offset on later appends is a no-op.\n if (meta.timestampOffset != null && sourceBuffer.timestampOffset !== meta.timestampOffset) {\n sourceBuffer.timestampOffset = meta.timestampOffset;\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":"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"}
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';\nimport type {\n AppendInitMessage,\n AppendSegmentMessage,\n IndividualSourceBufferMessage,\n RemoveMessage,\n} from '../../primitives/source-buffer-messages';\n\n// =============================================================================\n// Types\n// =============================================================================\n\nexport interface BufferedRange {\n start: number;\n end: number;\n}\n\nexport type { AppendData };\n\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 present on the playlist). Used by the\n * segment-loader's `planTasks` to detect cross-language switches and schedule ahead-buffer flush. Undefined for video\n * 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. The segment's data is partially present in the\n * SourceBuffer. 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\n for (let i = 0; i < buffered.length; i++) {\n ranges.push({ start: buffered.start(i), end: buffered.end(i) });\n }\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\n if (taskSignal.aborted) return ctx;\n\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\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 //\n // Misaligned renditions (e.g. 30fps vs 60fps rungs) break the same-slot\n // assumption: the switched-to segment's differing startTime is kept as a\n // separate overlapping entry rather than truncating the older one to the\n // portion MSE didn't overwrite. Cosmetic today (see #1865 for the\n // most-recent-append-wins model); coverage/flush stay correct.\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 // Relocation: set the offset before the coded frames are appended. The\n // SerialRunner guarantees the buffer is idle here, so the assignment is safe.\n // Guarded so re-stamping the (constant) offset on later appends is a no-op.\n if (meta.timestampOffset != null && sourceBuffer.timestampOffset !== meta.timestampOffset) {\n sourceBuffer.timestampOffset = meta.timestampOffset;\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\n if (taskSignal.aborted) return ctx;\n\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\n return bufferedRanges.some((r) => midpoint >= r.start && midpoint < r.end);\n });\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\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\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\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":"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"}
@@ -1 +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 { AddCuesMessage, CueSegmentMeta } from '../../primitives/text-track-messages';\nimport type { ClearMessage, TextTracksActor, TextTracksActorContext, TextTracksActorMessage } 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":";;AAoBA,SAAS,eAAe,KAAa,UAA0B;CAC7D,OAAO,SAAS,MAAM,MAAM,EAAE,cAAc,IAAI,aAAa,EAAE,YAAY,IAAI,WAAW,EAAE,SAAS,IAAI,IAAI;AAC/G;;AAOA,SAAgB,sBAAsB,cAAyD;CAC7F,OAAO,sBAAsB;EAAE,QAAQ,CAAC;EAAG,UAAU,CAAC;CAAE,IAA8B,SAAS,YAAY;EACzG,IAAI,QAAQ,SAAS,SAKnB,OAAO;GAAE,QAAQ,CAAC;GAAG,UAAU,CAAC;EAAE;EASpC,MAAM,EAAE,MAAM,SAAS;EACvB,MAAM,EAAE,SAAS,IAAI,WAAW,WAAW,aAAa;EACxD,MAAM,YAAY,MAAM,KAAK,aAAa,UAAU,CAAC,CAAC,MAAM,MAAM,EAAE,OAAO,OAAO;EAClF,IAAI,CAAC,WAAW,OAAO;EAEvB,MAAM,eAAe,QAAQ,OAAO,YAAY,CAAC;EACjD,MAAM,mBAAmB,QAAQ,SAAS,YAAY,CAAC;EACvD,MAAM,aAAa,KAAK,QAAQ,QAAQ,CAAC,eAAe,KAAK,YAAY,CAAC;EAC1E,MAAM,uBAAuB,iBAAiB,MAAM,MAAM,EAAE,OAAO,SAAS;EAE5E,IAAI,WAAW,WAAW,KAAK,sBAAsB,OAAO;EAE5D,KAAK,MAAM,OAAO,YAAY,UAAU,OAAO,GAAG;EAClD,OAAO;GACL,GAAG;GACH,QAAQ;IACN,GAAG,QAAQ;KACV,UAAU,CAAC,GAAG,cAAc,GAAG,UAAU;GAC5C;GACA,UAAU,uBACN,QAAQ,WACR;IACE,GAAG,QAAQ;KACV,UAAU,CAAC,GAAG,kBAAkB;KAAE,IAAI;KAAW;KAAW;IAAS,CAAC;GACzE;EACN;CACF,CAAC;AACH"}
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 { AddCuesMessage, CueSegmentMeta } from '../../primitives/text-track-messages';\nimport type { ClearMessage, TextTracksActor, TextTracksActorContext, TextTracksActorMessage } 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 if (prunedCues.length === 0 && segmentAlreadyLoaded) return context;\n\n for (const cue of prunedCues) textTrack.addCue(cue);\n\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":";;AAoBA,SAAS,eAAe,KAAa,UAA0B;CAC7D,OAAO,SAAS,MAAM,MAAM,EAAE,cAAc,IAAI,aAAa,EAAE,YAAY,IAAI,WAAW,EAAE,SAAS,IAAI,IAAI;AAC/G;;AAOA,SAAgB,sBAAsB,cAAyD;CAC7F,OAAO,sBAAsB;EAAE,QAAQ,CAAC;EAAG,UAAU,CAAC;CAAE,IAA8B,SAAS,YAAY;EACzG,IAAI,QAAQ,SAAS,SAKnB,OAAO;GAAE,QAAQ,CAAC;GAAG,UAAU,CAAC;EAAE;EASpC,MAAM,EAAE,MAAM,SAAS;EACvB,MAAM,EAAE,SAAS,IAAI,WAAW,WAAW,aAAa;EACxD,MAAM,YAAY,MAAM,KAAK,aAAa,UAAU,CAAC,CAAC,MAAM,MAAM,EAAE,OAAO,OAAO;EAClF,IAAI,CAAC,WAAW,OAAO;EAEvB,MAAM,eAAe,QAAQ,OAAO,YAAY,CAAC;EACjD,MAAM,mBAAmB,QAAQ,SAAS,YAAY,CAAC;EACvD,MAAM,aAAa,KAAK,QAAQ,QAAQ,CAAC,eAAe,KAAK,YAAY,CAAC;EAC1E,MAAM,uBAAuB,iBAAiB,MAAM,MAAM,EAAE,OAAO,SAAS;EAC5E,IAAI,WAAW,WAAW,KAAK,sBAAsB,OAAO;EAE5D,KAAK,MAAM,OAAO,YAAY,UAAU,OAAO,GAAG;EAElD,OAAO;GACL,GAAG;GACH,QAAQ;IACN,GAAG,QAAQ;KACV,UAAU,CAAC,GAAG,cAAc,GAAG,UAAU;GAC5C;GACA,UAAU,uBACN,QAAQ,WACR;IACE,GAAG,QAAQ;KACV,UAAU,CAAC,GAAG,kBAAkB;KAAE,IAAI;KAAW;KAAW;IAAS,CAAC;GACzE;EACN;CACF,CAAC;AACH"}
@@ -5,31 +5,25 @@ import { DEFAULT_FORWARD_BUFFER_CONFIG, getSegmentsToLoad } from "../../media/bu
5
5
  import { DEFAULT_TEXT_MESSAGE_PIPELINES } from "../primitives/text-segment-load-pipeline.js";
6
6
  //#region src/playback/actors/text-track-segment-loader.ts
7
7
  /**
8
- * Loads text-track segments for a track and delegates cue management
9
- * to a TextTracksActor. Mirrors the v/a `SegmentLoaderActor` shape (FSM
10
- * with `idle` / `loading` and `inFlight*` context for continue-vs-preempt),
11
- * adapted to text:
8
+ * Loads text-track segments for a track and delegates cue management to a TextTracksActor. Mirrors the v/a
9
+ * `SegmentLoaderActor` shape (FSM with `idle` / `loading` and `inFlight*` context for continue-vs-preempt), adapted to
10
+ * text:
12
11
  *
13
- * - No init segment, no flush ops (text cues don't need eviction — they're
14
- * small and the playhead-relative window is enforced by the runtime).
15
- * - Single in-flight identity (`inFlightSegmentId`) — text has only the
16
- * media-segment path, no init-segment path.
12
+ * - No init segment, no flush ops (text cues don't need eviction — they're small and the playhead-relative window is
13
+ * enforced by the runtime).
14
+ * - Single in-flight identity (`inFlightSegmentId`) — text has only the media-segment path, no init-segment path.
17
15
  *
18
- * Planning is done in the load handler on every incoming message:
19
- * `getSegmentsToLoad` filters to the forward window, then the segments
20
- * not already in `TextTracksActor`'s context are scheduled. When a new
21
- * `load` arrives mid-run, the handler replans and either:
16
+ * Planning is done in the load handler on every incoming message: `getSegmentsToLoad` filters to the forward window,
17
+ * then the segments not already in `TextTracksActor`'s context are scheduled. When a new `load` arrives mid-run, the
18
+ * handler replans and either:
22
19
  *
23
- * - **Continues**: the in-flight segment is still in the new plan →
24
- * `abortPending` only, schedule the rest of the plan (minus the
25
- * in-flight item, which covers its slot).
26
- * - **Preempts**: in-flight segment is no longer wanted (track switch,
27
- * large seek out of window) → `abortAll`, schedule the new plan
28
- * from scratch.
20
+ * - **Continues**: the in-flight segment is still in the new plan → `abortPending` only, schedule the rest of the plan
21
+ * (minus the in-flight item, which covers its slot).
22
+ * - **Preempts**: in-flight segment is no longer wanted (track switch, large seek out of window) → `abortAll`, schedule
23
+ * the new plan from scratch.
29
24
  *
30
- * The cue parser is injected so this factory is host-agnostic. A DOM
31
- * host supplies a VTT parser backed by `<track>`/`TextTrack` APIs; a
32
- * non-DOM host (worker, test fake, alternate runtime) supplies its own.
25
+ * The cue parser is injected so this factory is host-agnostic. A DOM host supplies a VTT parser backed by
26
+ * `<track>`/`TextTrack` APIs; a non-DOM host (worker, test fake, alternate runtime) supplies its own.
33
27
  */
34
28
  function createTextTrackSegmentLoaderActor(textTracksActor, resolveSegment, config = {}, compositionDeps = {
35
29
  state: {},
@@ -51,13 +45,11 @@ function createTextTrackSegmentLoaderActor(textTracksActor, resolveSegment, conf
51
45
  };
52
46
  const pipeline = (config.messagePipelines ?? DEFAULT_TEXT_MESSAGE_PIPELINES)();
53
47
  /**
54
- * Translate a load message into an ordered TextLoadTask list based on
55
- * committed actor state. In-flight awareness is handled separately in
56
- * the `loading` state's load handler.
48
+ * Translate a load message into an ordered TextLoadTask list based on committed actor state. In-flight awareness is
49
+ * handled separately in the `loading` state's load handler.
57
50
  *
58
- * Metadata mode (no `range`) is a no-op for text — text tracks have
59
- * no init-segment concept, so there's nothing to load until a range
60
- * arrives via `'full-range'` dispatch.
51
+ * Metadata mode (no `range`) is a no-op for text — text tracks have no init-segment concept, so there's nothing to
52
+ * load until a range arrives via `'full-range'` dispatch.
61
53
  */
62
54
  const planTasks = (message) => {
63
55
  const { track, range } = message;
@@ -70,15 +62,12 @@ function createTextTrackSegmentLoaderActor(textTracksActor, resolveSegment, conf
70
62
  }));
71
63
  };
72
64
  /**
73
- * Wraps a TextLoadTask into a Task that runs the op's step pipeline
74
- * (resolve/relocate/dispatch, per the composition's `messagePipelines`).
75
- * Updates `inFlightSegmentId` around the async region so the load handler can
76
- * make accurate continue/preempt decisions, and checks the abort signal before
77
- * each step.
65
+ * Wraps a TextLoadTask into a Task that runs the op's step pipeline (resolve/relocate/dispatch, per the composition's
66
+ * `messagePipelines`). Updates `inFlightSegmentId` around the async region so the load handler can make accurate
67
+ * continue/preempt decisions, and checks the abort signal before each step.
78
68
  *
79
- * Text degrades gracefully: a step throwing (e.g. a failed segment fetch) is
80
- * logged and swallowed so the runner continues to the next segment — unlike the
81
- * v/a loader, where a failed init must abort the remaining tasks.
69
+ * Text degrades gracefully: a step throwing (e.g. a failed segment fetch) is logged and swallowed so the runner
70
+ * continues to the next segment — unlike the v/a loader, where a failed init must abort the remaining tasks.
82
71
  */
83
72
  const makeLoadTask = (op, { getContext, setContext }) => {
84
73
  return new Task(async (signal) => {