@videojs/spf 10.0.0-beta.2 → 10.0.0-beta.21

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 (264) hide show
  1. package/README.md +16 -40
  2. package/dist/default/core/abr/bandwidth-estimator.js +95 -0
  3. package/dist/default/core/abr/bandwidth-estimator.js.map +1 -0
  4. package/dist/default/core/abr/ewma.js +71 -0
  5. package/dist/default/core/abr/ewma.js.map +1 -0
  6. package/dist/default/core/abr/quality-selection.js +53 -0
  7. package/dist/default/core/abr/quality-selection.js.map +1 -0
  8. package/dist/default/core/buffer/back-buffer.js +46 -0
  9. package/dist/default/core/buffer/back-buffer.js.map +1 -0
  10. package/dist/default/core/buffer/forward-buffer.js +73 -0
  11. package/dist/default/core/buffer/forward-buffer.js.map +1 -0
  12. package/dist/default/core/create-machine-actor.js +91 -0
  13. package/dist/default/core/create-machine-actor.js.map +1 -0
  14. package/dist/default/core/create-machine-reactor.js +83 -0
  15. package/dist/default/core/create-machine-reactor.js.map +1 -0
  16. package/dist/default/core/create-transition-actor.js +46 -0
  17. package/dist/default/core/create-transition-actor.js.map +1 -0
  18. package/dist/default/core/features/calculate-presentation-duration.js +53 -0
  19. package/dist/default/core/features/calculate-presentation-duration.js.map +1 -0
  20. package/dist/default/core/features/quality-switching.js +69 -0
  21. package/dist/default/core/features/quality-switching.js.map +1 -0
  22. package/dist/default/core/features/resolve-presentation.js +73 -0
  23. package/dist/default/core/features/resolve-presentation.js.map +1 -0
  24. package/dist/default/core/features/resolve-track.js +66 -0
  25. package/dist/default/core/features/resolve-track.js.map +1 -0
  26. package/dist/default/core/features/select-tracks.js +130 -0
  27. package/dist/default/core/features/select-tracks.js.map +1 -0
  28. package/dist/default/core/features/sync-preload-attribute.js +26 -0
  29. package/dist/default/core/features/sync-preload-attribute.js.map +1 -0
  30. package/dist/default/core/hls/parse-attributes.js +134 -0
  31. package/dist/default/core/hls/parse-attributes.js.map +1 -0
  32. package/dist/default/core/hls/parse-media-playlist.js +82 -0
  33. package/dist/default/core/hls/parse-media-playlist.js.map +1 -0
  34. package/dist/default/core/hls/parse-multivariant.js +214 -0
  35. package/dist/default/core/hls/parse-multivariant.js.map +1 -0
  36. package/dist/default/core/hls/resolve-url.js +11 -0
  37. package/dist/default/core/hls/resolve-url.js.map +1 -0
  38. package/dist/default/core/machine.js +26 -0
  39. package/dist/default/core/machine.js.map +1 -0
  40. package/dist/default/core/signals/effect.js +41 -0
  41. package/dist/default/core/signals/effect.js.map +1 -0
  42. package/dist/default/core/signals/primitives.js +32 -0
  43. package/dist/default/core/signals/primitives.js.map +1 -0
  44. package/dist/default/core/task.js +191 -0
  45. package/dist/default/core/task.js.map +1 -0
  46. package/dist/default/core/types/index.js +15 -0
  47. package/dist/default/core/types/index.js.map +1 -0
  48. package/dist/default/core/utils/generate-id.js +21 -0
  49. package/dist/default/core/utils/generate-id.js.map +1 -0
  50. package/dist/default/core/utils/track-selection.js +39 -0
  51. package/dist/default/core/utils/track-selection.js.map +1 -0
  52. package/dist/default/dom/features/end-of-stream.js +145 -0
  53. package/dist/default/dom/features/end-of-stream.js.map +1 -0
  54. package/dist/default/dom/features/load-segments.js +192 -0
  55. package/dist/default/dom/features/load-segments.js.map +1 -0
  56. package/dist/default/dom/features/load-text-track-cues.js +104 -0
  57. package/dist/default/dom/features/load-text-track-cues.js.map +1 -0
  58. package/dist/default/dom/features/segment-loader-actor.js +243 -0
  59. package/dist/default/dom/features/segment-loader-actor.js.map +1 -0
  60. package/dist/default/dom/features/setup-mediasource.js +48 -0
  61. package/dist/default/dom/features/setup-mediasource.js.map +1 -0
  62. package/dist/default/dom/features/setup-sourcebuffer.js +78 -0
  63. package/dist/default/dom/features/setup-sourcebuffer.js.map +1 -0
  64. package/dist/default/dom/features/sync-text-tracks.js +95 -0
  65. package/dist/default/dom/features/sync-text-tracks.js.map +1 -0
  66. package/dist/default/dom/features/text-track-segment-loader-actor.js +55 -0
  67. package/dist/default/dom/features/text-track-segment-loader-actor.js.map +1 -0
  68. package/dist/default/dom/features/text-tracks-actor.js +42 -0
  69. package/dist/default/dom/features/text-tracks-actor.js.map +1 -0
  70. package/dist/default/dom/features/track-current-time.js +49 -0
  71. package/dist/default/dom/features/track-current-time.js.map +1 -0
  72. package/dist/default/dom/features/track-playback-initiated.js +66 -0
  73. package/dist/default/dom/features/track-playback-initiated.js.map +1 -0
  74. package/dist/default/dom/features/track-playback-rate.js +32 -0
  75. package/dist/default/dom/features/track-playback-rate.js.map +1 -0
  76. package/dist/default/dom/features/update-duration.js +78 -0
  77. package/dist/default/dom/features/update-duration.js.map +1 -0
  78. package/dist/default/dom/media/append-segment.js +60 -0
  79. package/dist/default/dom/media/append-segment.js.map +1 -0
  80. package/dist/default/dom/media/buffer-flusher.js +55 -0
  81. package/dist/default/dom/media/buffer-flusher.js.map +1 -0
  82. package/dist/default/dom/media/mediasource-setup.js +146 -0
  83. package/dist/default/dom/media/mediasource-setup.js.map +1 -0
  84. package/dist/default/dom/media/source-buffer-actor.js +137 -0
  85. package/dist/default/dom/media/source-buffer-actor.js.map +1 -0
  86. package/dist/default/dom/network/chunked-stream-iterable.js +49 -0
  87. package/dist/default/dom/network/chunked-stream-iterable.js.map +1 -0
  88. package/dist/default/dom/network/fetch.js +54 -0
  89. package/dist/default/dom/network/fetch.js.map +1 -0
  90. package/dist/default/dom/playback-engine/adapter.js +100 -0
  91. package/dist/default/dom/playback-engine/adapter.js.map +1 -0
  92. package/dist/default/dom/playback-engine/engine.js +142 -0
  93. package/dist/default/dom/playback-engine/engine.js.map +1 -0
  94. package/dist/default/dom/playback-engine.js +4 -0
  95. package/dist/default/dom/text/parse-vtt-segment.js +56 -0
  96. package/dist/default/dom/text/parse-vtt-segment.js.map +1 -0
  97. package/dist/default/dom.js +10 -0
  98. package/dist/{src-DRsS_H80.js → default/index.js} +3 -3
  99. package/dist/default/index.js.map +1 -0
  100. package/dist/dev/core/abr/bandwidth-estimator.d.ts +35 -0
  101. package/dist/dev/core/abr/bandwidth-estimator.d.ts.map +1 -0
  102. package/dist/dev/core/abr/bandwidth-estimator.js +95 -0
  103. package/dist/dev/core/abr/bandwidth-estimator.js.map +1 -0
  104. package/dist/dev/core/abr/ewma.js +71 -0
  105. package/dist/dev/core/abr/ewma.js.map +1 -0
  106. package/dist/dev/core/abr/quality-selection.js +53 -0
  107. package/dist/dev/core/abr/quality-selection.js.map +1 -0
  108. package/dist/dev/core/actor.d.ts +27 -0
  109. package/dist/dev/core/actor.d.ts.map +1 -0
  110. package/dist/dev/core/buffer/back-buffer.js +46 -0
  111. package/dist/dev/core/buffer/back-buffer.js.map +1 -0
  112. package/dist/dev/core/buffer/forward-buffer.js +73 -0
  113. package/dist/dev/core/buffer/forward-buffer.js.map +1 -0
  114. package/dist/dev/core/create-machine-actor.d.ts +12 -0
  115. package/dist/dev/core/create-machine-actor.d.ts.map +1 -0
  116. package/dist/dev/core/create-machine-actor.js +91 -0
  117. package/dist/dev/core/create-machine-actor.js.map +1 -0
  118. package/dist/dev/core/create-machine-reactor.d.ts +8 -0
  119. package/dist/dev/core/create-machine-reactor.d.ts.map +1 -0
  120. package/dist/dev/core/create-machine-reactor.js +83 -0
  121. package/dist/dev/core/create-machine-reactor.js.map +1 -0
  122. package/dist/dev/core/create-transition-actor.d.ts +23 -0
  123. package/dist/dev/core/create-transition-actor.d.ts.map +1 -0
  124. package/dist/dev/core/create-transition-actor.js +46 -0
  125. package/dist/dev/core/create-transition-actor.js.map +1 -0
  126. package/dist/dev/core/features/calculate-presentation-duration.js +53 -0
  127. package/dist/dev/core/features/calculate-presentation-duration.js.map +1 -0
  128. package/dist/dev/core/features/quality-switching.js +69 -0
  129. package/dist/dev/core/features/quality-switching.js.map +1 -0
  130. package/dist/dev/core/features/resolve-presentation.js +73 -0
  131. package/dist/dev/core/features/resolve-presentation.js.map +1 -0
  132. package/dist/dev/core/features/resolve-track.js +66 -0
  133. package/dist/dev/core/features/resolve-track.js.map +1 -0
  134. package/dist/dev/core/features/select-tracks.js +130 -0
  135. package/dist/dev/core/features/select-tracks.js.map +1 -0
  136. package/dist/dev/core/features/sync-preload-attribute.js +26 -0
  137. package/dist/dev/core/features/sync-preload-attribute.js.map +1 -0
  138. package/dist/dev/core/hls/parse-attributes.js +134 -0
  139. package/dist/dev/core/hls/parse-attributes.js.map +1 -0
  140. package/dist/dev/core/hls/parse-media-playlist.js +82 -0
  141. package/dist/dev/core/hls/parse-media-playlist.js.map +1 -0
  142. package/dist/dev/core/hls/parse-multivariant.js +214 -0
  143. package/dist/dev/core/hls/parse-multivariant.js.map +1 -0
  144. package/dist/dev/core/hls/resolve-url.js +11 -0
  145. package/dist/dev/core/hls/resolve-url.js.map +1 -0
  146. package/dist/dev/core/machine.d.ts +21 -0
  147. package/dist/dev/core/machine.d.ts.map +1 -0
  148. package/dist/dev/core/machine.js +26 -0
  149. package/dist/dev/core/machine.js.map +1 -0
  150. package/dist/dev/core/signals/effect.d.ts +15 -0
  151. package/dist/dev/core/signals/effect.d.ts.map +1 -0
  152. package/dist/dev/core/signals/effect.js +41 -0
  153. package/dist/dev/core/signals/effect.js.map +1 -0
  154. package/dist/dev/core/signals/primitives.d.ts +10 -0
  155. package/dist/dev/core/signals/primitives.d.ts.map +1 -0
  156. package/dist/dev/core/signals/primitives.js +32 -0
  157. package/dist/dev/core/signals/primitives.js.map +1 -0
  158. package/dist/dev/core/task.js +191 -0
  159. package/dist/dev/core/task.js.map +1 -0
  160. package/dist/dev/core/types/index.d.ts +166 -0
  161. package/dist/dev/core/types/index.d.ts.map +1 -0
  162. package/dist/dev/core/types/index.js +15 -0
  163. package/dist/dev/core/types/index.js.map +1 -0
  164. package/dist/dev/core/utils/generate-id.js +21 -0
  165. package/dist/dev/core/utils/generate-id.js.map +1 -0
  166. package/dist/dev/core/utils/track-selection.d.ts +16 -0
  167. package/dist/dev/core/utils/track-selection.d.ts.map +1 -0
  168. package/dist/dev/core/utils/track-selection.js +39 -0
  169. package/dist/dev/core/utils/track-selection.js.map +1 -0
  170. package/dist/dev/dom/features/end-of-stream.js +145 -0
  171. package/dist/dev/dom/features/end-of-stream.js.map +1 -0
  172. package/dist/dev/dom/features/load-segments.d.ts +72 -0
  173. package/dist/dev/dom/features/load-segments.d.ts.map +1 -0
  174. package/dist/dev/dom/features/load-segments.js +192 -0
  175. package/dist/dev/dom/features/load-segments.js.map +1 -0
  176. package/dist/dev/dom/features/load-text-track-cues.d.ts +85 -0
  177. package/dist/dev/dom/features/load-text-track-cues.d.ts.map +1 -0
  178. package/dist/dev/dom/features/load-text-track-cues.js +104 -0
  179. package/dist/dev/dom/features/load-text-track-cues.js.map +1 -0
  180. package/dist/dev/dom/features/segment-loader-actor.d.ts +1 -0
  181. package/dist/dev/dom/features/segment-loader-actor.js +243 -0
  182. package/dist/dev/dom/features/segment-loader-actor.js.map +1 -0
  183. package/dist/dev/dom/features/setup-mediasource.js +48 -0
  184. package/dist/dev/dom/features/setup-mediasource.js.map +1 -0
  185. package/dist/dev/dom/features/setup-sourcebuffer.d.ts +9 -0
  186. package/dist/dev/dom/features/setup-sourcebuffer.d.ts.map +1 -0
  187. package/dist/dev/dom/features/setup-sourcebuffer.js +78 -0
  188. package/dist/dev/dom/features/setup-sourcebuffer.js.map +1 -0
  189. package/dist/dev/dom/features/sync-text-tracks.js +95 -0
  190. package/dist/dev/dom/features/sync-text-tracks.js.map +1 -0
  191. package/dist/dev/dom/features/text-track-segment-loader-actor.d.ts +12 -0
  192. package/dist/dev/dom/features/text-track-segment-loader-actor.d.ts.map +1 -0
  193. package/dist/dev/dom/features/text-track-segment-loader-actor.js +55 -0
  194. package/dist/dev/dom/features/text-track-segment-loader-actor.js.map +1 -0
  195. package/dist/dev/dom/features/text-tracks-actor.d.ts +31 -0
  196. package/dist/dev/dom/features/text-tracks-actor.d.ts.map +1 -0
  197. package/dist/dev/dom/features/text-tracks-actor.js +42 -0
  198. package/dist/dev/dom/features/text-tracks-actor.js.map +1 -0
  199. package/dist/dev/dom/features/track-current-time.d.ts +40 -0
  200. package/dist/dev/dom/features/track-current-time.d.ts.map +1 -0
  201. package/dist/dev/dom/features/track-current-time.js +49 -0
  202. package/dist/dev/dom/features/track-current-time.js.map +1 -0
  203. package/dist/dev/dom/features/track-playback-initiated.d.ts +46 -0
  204. package/dist/dev/dom/features/track-playback-initiated.d.ts.map +1 -0
  205. package/dist/dev/dom/features/track-playback-initiated.js +66 -0
  206. package/dist/dev/dom/features/track-playback-initiated.js.map +1 -0
  207. package/dist/dev/dom/features/track-playback-rate.d.ts +34 -0
  208. package/dist/dev/dom/features/track-playback-rate.d.ts.map +1 -0
  209. package/dist/dev/dom/features/track-playback-rate.js +32 -0
  210. package/dist/dev/dom/features/track-playback-rate.js.map +1 -0
  211. package/dist/dev/dom/features/update-duration.js +78 -0
  212. package/dist/dev/dom/features/update-duration.js.map +1 -0
  213. package/dist/dev/dom/media/append-segment.d.ts +24 -0
  214. package/dist/dev/dom/media/append-segment.d.ts.map +1 -0
  215. package/dist/dev/dom/media/append-segment.js +60 -0
  216. package/dist/dev/dom/media/append-segment.js.map +1 -0
  217. package/dist/dev/dom/media/buffer-flusher.d.ts +24 -0
  218. package/dist/dev/dom/media/buffer-flusher.d.ts.map +1 -0
  219. package/dist/dev/dom/media/buffer-flusher.js +55 -0
  220. package/dist/dev/dom/media/buffer-flusher.js.map +1 -0
  221. package/dist/dev/dom/media/mediasource-setup.js +146 -0
  222. package/dist/dev/dom/media/mediasource-setup.js.map +1 -0
  223. package/dist/dev/dom/media/source-buffer-actor.d.ts +62 -0
  224. package/dist/dev/dom/media/source-buffer-actor.d.ts.map +1 -0
  225. package/dist/dev/dom/media/source-buffer-actor.js +137 -0
  226. package/dist/dev/dom/media/source-buffer-actor.js.map +1 -0
  227. package/dist/dev/dom/network/chunked-stream-iterable.js +49 -0
  228. package/dist/dev/dom/network/chunked-stream-iterable.js.map +1 -0
  229. package/dist/dev/dom/network/fetch.js +54 -0
  230. package/dist/dev/dom/network/fetch.js.map +1 -0
  231. package/dist/dev/dom/playback-engine/adapter.d.ts +40 -0
  232. package/dist/dev/dom/playback-engine/adapter.d.ts.map +1 -0
  233. package/dist/dev/dom/playback-engine/adapter.js +100 -0
  234. package/dist/dev/dom/playback-engine/adapter.js.map +1 -0
  235. package/dist/dev/dom/playback-engine/engine.d.ts +124 -0
  236. package/dist/dev/dom/playback-engine/engine.d.ts.map +1 -0
  237. package/dist/dev/dom/playback-engine/engine.js +142 -0
  238. package/dist/dev/dom/playback-engine/engine.js.map +1 -0
  239. package/dist/dev/dom/playback-engine.d.ts +4 -0
  240. package/dist/dev/dom/playback-engine.js +4 -0
  241. package/dist/dev/dom/text/parse-vtt-segment.d.ts +12 -0
  242. package/dist/dev/dom/text/parse-vtt-segment.d.ts.map +1 -0
  243. package/dist/dev/dom/text/parse-vtt-segment.js +56 -0
  244. package/dist/dev/dom/text/parse-vtt-segment.js.map +1 -0
  245. package/dist/dev/dom.d.ts +10 -0
  246. package/dist/dev/dom.js +10 -0
  247. package/dist/dev/index.d.ts.map +1 -0
  248. package/dist/dev/index.js +14 -0
  249. package/dist/dev/index.js.map +1 -0
  250. package/package.json +26 -24
  251. package/dist/adapter-CMw-rvbk.js +0 -3512
  252. package/dist/adapter-CMw-rvbk.js.map +0 -1
  253. package/dist/adapter-l8iJN_GI.d.ts +0 -689
  254. package/dist/adapter-l8iJN_GI.d.ts.map +0 -1
  255. package/dist/dom/index.d.ts +0 -243
  256. package/dist/dom/index.d.ts.map +0 -1
  257. package/dist/dom/index.js +0 -38
  258. package/dist/dom/index.js.map +0 -1
  259. package/dist/dom/playback-engine/index.d.ts +0 -2
  260. package/dist/dom/playback-engine/index.js +0 -4
  261. package/dist/index.d.ts.map +0 -1
  262. package/dist/index.js +0 -3
  263. package/dist/src-DRsS_H80.js.map +0 -1
  264. /package/dist/{index.d.ts → dev/index.d.ts} +0 -0
@@ -0,0 +1,192 @@
1
+ import { sampleBandwidth } from "../../core/abr/bandwidth-estimator.js";
2
+ import { DEFAULT_FORWARD_BUFFER_CONFIG } from "../../core/buffer/forward-buffer.js";
3
+ import { effect } from "../../core/signals/effect.js";
4
+ import { computed, signal } from "../../core/signals/primitives.js";
5
+ import { isResolvedTrack } from "../../core/types/index.js";
6
+ import { getSelectedTrack } from "../../core/utils/track-selection.js";
7
+ import { ChunkedStreamIterable } from "../network/chunked-stream-iterable.js";
8
+ import { fetchResolvable } from "../network/fetch.js";
9
+ import { createSegmentLoaderActor } from "./segment-loader-actor.js";
10
+ //#region src/dom/features/load-segments.ts
11
+ const ActorKeyByType = {
12
+ video: "videoBufferActor",
13
+ audio: "audioBufferActor"
14
+ };
15
+ function createTrackedFetch(throughput, onSample) {
16
+ return async (addressable, options) => {
17
+ const { minChunkSize, ...fetchOptions } = options ?? {};
18
+ const response = await fetchResolvable(addressable, fetchOptions);
19
+ if (!response.body) throw new Error("Response has no body");
20
+ const body = response.body;
21
+ return { [Symbol.asyncIterator]: async function* () {
22
+ let chunkStart = performance.now();
23
+ for await (const chunk of new ChunkedStreamIterable(body, ...minChunkSize !== void 0 ? [{ minChunkSize }] : [])) {
24
+ const elapsed = performance.now() - chunkStart;
25
+ const next = sampleBandwidth(throughput.get(), elapsed, chunk.byteLength);
26
+ throughput.set(next);
27
+ onSample?.(next);
28
+ yield chunk;
29
+ chunkStart = performance.now();
30
+ }
31
+ } };
32
+ };
33
+ }
34
+ /**
35
+ * Non-tracking fetch: eagerly starts the request and returns the response body
36
+ * as a lazy chunk iterable. Used for audio tracks which don't sample bandwidth.
37
+ * Pass `minChunkSize: Infinity` to accumulate the full body as a single chunk
38
+ * (equivalent to arrayBuffer() but through the same streaming path).
39
+ */
40
+ async function fetchStream(addressable, options) {
41
+ const { minChunkSize, ...fetchOptions } = options ?? {};
42
+ const response = await fetchResolvable(addressable, fetchOptions);
43
+ if (!response.body) throw new Error("Response has no body");
44
+ return new ChunkedStreamIterable(response.body, ...minChunkSize !== void 0 ? [{ minChunkSize }] : []);
45
+ }
46
+ function selectLoadingInputs([segmentsCanLoad, state], type) {
47
+ const { playbackInitiated, preload, currentTime } = state;
48
+ return {
49
+ playbackInitiated,
50
+ preload,
51
+ currentTime,
52
+ track: getSelectedTrack(state, type),
53
+ segmentsCanLoad
54
+ };
55
+ }
56
+ /**
57
+ * Equality function encoding the condition hierarchy for relevant changes.
58
+ *
59
+ * Pre-play (!playbackInitiated):
60
+ * Only preload changes matter. currentTime and resolvedTrackId are ignored
61
+ * (track changes not supported pre-play; currentTime value is used at
62
+ * trigger time but changes don't re-trigger).
63
+ *
64
+ * playbackInitiated transition:
65
+ * Always fires (handled in the subscriber; preload='auto' suppression
66
+ * applied there since equality functions have no memory of prior values).
67
+ *
68
+ * Post-play (playbackInitiated):
69
+ * resolvedTrackId changes (track switch or previously-unresolved track
70
+ * resolving) and currentTime changes both trigger. preload is irrelevant.
71
+ */
72
+ const segmentStartFor = (currentTime, track) => {
73
+ if (currentTime == null) return void 0;
74
+ return track?.segments.find(({ startTime, duration }, i, segments) => currentTime >= startTime && (currentTime < startTime + duration || i === segments.length - 1))?.startTime;
75
+ };
76
+ /**
77
+ * Returns true when the inputs are equal (no meaningful change — don't fire).
78
+ * Returns false when the inputs differ in a way that requires a new message.
79
+ *
80
+ * This IS the shouldLoadSegments logic, expressed as an equality function.
81
+ */
82
+ function loadingInputsEq(prevState, curState) {
83
+ if (!curState.segmentsCanLoad) return true;
84
+ if (!curState.playbackInitiated) {
85
+ if (curState.preload === "none") return true;
86
+ return curState.preload === prevState.preload;
87
+ }
88
+ if (!prevState.playbackInitiated && curState.playbackInitiated) {
89
+ if (prevState.preload !== "auto") return false;
90
+ }
91
+ if (!curState.track || !isResolvedTrack(curState.track)) return true;
92
+ if (prevState.track?.id !== curState.track.id && isResolvedTrack(curState.track)) return false;
93
+ return segmentStartFor(prevState.currentTime, curState.track) === segmentStartFor(curState.currentTime, curState.track);
94
+ }
95
+ /**
96
+ * Load segments orchestration — Reactor layer.
97
+ *
98
+ * Sends typed load messages to a SegmentLoaderActor when relevant conditions
99
+ * change. Uses targeted subscriptions rather than broad combineLatest so only
100
+ * meaningful state changes trigger evaluation.
101
+ *
102
+ * Condition hierarchy (see SegmentLoadingKey for detail):
103
+ *
104
+ * !playbackInitiated
105
+ * preload==='none' (or unset) → dormant; no trigger
106
+ * preload==='metadata' → trigger on transition to 'metadata'
107
+ * preload==='auto' → trigger on transition to 'auto'
108
+ *
109
+ * !playbackInitiated → playbackInitiated
110
+ * preload !== 'auto' → trigger (message shape changes)
111
+ * preload === 'auto' → suppressed (was already full-range mode;
112
+ * let segmentStart take over post-play)
113
+ * KNOWN LIMITATION: seek-before-play with
114
+ * preload='auto' is not supported — if the
115
+ * user seeks before pressing play, the
116
+ * first re-send is delayed until the next
117
+ * segment boundary crossing post-play.
118
+ *
119
+ * playbackInitiated
120
+ * resolvedTrackId changes → trigger
121
+ * segmentStart(currentTime) changes → trigger (segment boundary only)
122
+ *
123
+ * @example
124
+ * const cleanup = loadSegments({ state, owners }, { type: 'video' });
125
+ */
126
+ function loadSegments({ state, owners }, config) {
127
+ const { type } = config;
128
+ const actorKey = ActorKeyByType[type];
129
+ const initialBandwidth = state.get().bandwidthState;
130
+ const throughput = signal(initialBandwidth ?? {
131
+ fastEstimate: 0,
132
+ fastTotalWeight: 0,
133
+ slowEstimate: 0,
134
+ slowTotalWeight: 0,
135
+ bytesSampled: 0
136
+ });
137
+ const fetchBytes = type === "video" ? createTrackedFetch(throughput, initialBandwidth !== void 0 ? (next) => {
138
+ state.set(Object.assign({}, state.get(), { bandwidthState: next }));
139
+ } : void 0) : fetchStream;
140
+ const segmentLoader = signal(void 0);
141
+ const actorSource = computed(() => owners.get()[actorKey]);
142
+ let currentLoader;
143
+ const cleanupActorLifecycle = effect(() => {
144
+ const actor = actorSource.get();
145
+ if (currentLoader) {
146
+ currentLoader.destroy();
147
+ segmentLoader.set(void 0);
148
+ currentLoader = void 0;
149
+ }
150
+ if (actor) {
151
+ const loader = createSegmentLoaderActor(actor, fetchBytes);
152
+ currentLoader = loader;
153
+ segmentLoader.set(loader);
154
+ }
155
+ });
156
+ const segmentsCanLoad = computed(() => {
157
+ const track = getSelectedTrack(state.get(), type);
158
+ return !!track && isResolvedTrack(track) && !!segmentLoader.get();
159
+ });
160
+ const loadingInputs = computed(() => selectLoadingInputs([segmentsCanLoad.get(), state.get()], type));
161
+ let prevInputs;
162
+ const cleanupLoadEffect = effect(() => {
163
+ const inputs = loadingInputs.get();
164
+ if (prevInputs !== void 0 && loadingInputsEq(prevInputs, inputs)) return;
165
+ const { preload, playbackInitiated, currentTime, track, segmentsCanLoad: canLoad } = inputs;
166
+ if (!canLoad) return;
167
+ prevInputs = inputs;
168
+ if (!(preload === "auto" || !!playbackInitiated))
169
+ /** @ts-expect-error */
170
+ segmentLoader.get()?.send({
171
+ type: "load",
172
+ track
173
+ });
174
+ else segmentLoader.get()?.send({
175
+ type: "load",
176
+ track,
177
+ range: {
178
+ start: currentTime,
179
+ end: currentTime + DEFAULT_FORWARD_BUFFER_CONFIG.bufferDuration
180
+ }
181
+ });
182
+ });
183
+ return () => {
184
+ cleanupActorLifecycle();
185
+ cleanupLoadEffect();
186
+ currentLoader?.destroy();
187
+ };
188
+ }
189
+ //#endregion
190
+ export { loadSegments };
191
+
192
+ //# sourceMappingURL=load-segments.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"load-segments.js","names":[],"sources":["../../../../src/dom/features/load-segments.ts"],"sourcesContent":["import { type BandwidthState, sampleBandwidth } from '../../core/abr/bandwidth-estimator';\nimport { DEFAULT_FORWARD_BUFFER_CONFIG } from '../../core/buffer/forward-buffer';\nimport { effect } from '../../core/signals/effect';\nimport { computed, type Signal, signal } from '../../core/signals/primitives';\nimport type { AddressableObject, Presentation, ResolvedTrack } from '../../core/types';\nimport { isResolvedTrack } from '../../core/types';\nimport { getSelectedTrack, type TrackSelectionState } from '../../core/utils/track-selection';\nimport type { SourceBufferActor } from '../media/source-buffer-actor';\nimport { ChunkedStreamIterable, type ChunkedStreamIterableOptions } from '../network/chunked-stream-iterable';\nimport { fetchResolvable } from '../network/fetch';\nimport {\n type BufferState,\n createSegmentLoaderActor,\n type SegmentLoaderActor,\n type SourceBufferState,\n} from './segment-loader-actor';\nimport type { MediaTrackType } from './setup-sourcebuffer';\n\n// Re-export buffer state types for consumers that import them from this module.\nexport type { BufferState, SourceBufferState };\n\n// Map track type to SourceBufferActor owner property key.\nconst ActorKeyByType = {\n video: 'videoBufferActor',\n audio: 'audioBufferActor',\n} as const;\n\n// ============================================================================\n// TRACKED FETCH\n// ============================================================================\n\n/**\n * Creates a fetch function that eagerly starts the HTTP request (TTFB is\n * awaited), then returns a lazy iterable over the response body that\n * transparently samples bandwidth per chunk.\n *\n * Separating connection start from body reading ensures `fetch()` is called\n * as soon as the task begins — not deferred until the actor's append loop\n * first pulls a chunk. This makes fetch timing predictable and observable\n * (e.g. in tests that record fetched URLs) regardless of downstream consumers.\n *\n * `onSample` bridges throughput state outward; see Phase 2 comment for detail.\n */\ntype FetchOptions = RequestInit & ChunkedStreamIterableOptions;\n\nfunction createTrackedFetch(\n throughput: Signal<BandwidthState>,\n onSample?: (next: BandwidthState) => void\n): (addressable: AddressableObject, options?: FetchOptions) => Promise<AsyncIterable<Uint8Array>> {\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 const next = sampleBandwidth(throughput.get(), elapsed, chunk.byteLength);\n throughput.set(next);\n onSample?.(next);\n yield chunk;\n chunkStart = performance.now();\n }\n },\n };\n };\n}\n\n/**\n * Non-tracking fetch: eagerly starts the request and returns the response body\n * as a lazy chunk iterable. Used for audio tracks which don't sample bandwidth.\n * Pass `minChunkSize: Infinity` to accumulate the full body as a single chunk\n * (equivalent to arrayBuffer() but through the same streaming path).\n */\nasync function fetchStream(addressable: AddressableObject, 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// STATE & OWNERS\n// ============================================================================\n\n/**\n * State shape for segment loading.\n */\nexport interface SegmentLoadingState extends TrackSelectionState {\n presentation?: Presentation;\n preload?: string;\n bandwidthState?: BandwidthState;\n /** Current playback position in seconds. Defaults to 0 when undefined. */\n currentTime?: number;\n /** True once the user has initiated playback. Allows segment loading regardless of preload setting. */\n playbackInitiated?: boolean;\n}\n\n/**\n * Owners shape for segment loading.\n */\nexport interface SegmentLoadingOwners {\n videoBuffer?: SourceBuffer;\n audioBuffer?: SourceBuffer;\n videoBufferActor?: SourceBufferActor;\n audioBufferActor?: SourceBufferActor;\n}\n\n// ============================================================================\n// STATE SELECTOR\n// ============================================================================\n\n/**\n * The subset of state that drives segment loading decisions.\n * A plain \"pick\" with one derived field (resolved track ID).\n */\ntype LoadingInputs = {\n playbackInitiated: boolean | undefined;\n preload: string | undefined;\n currentTime: number | undefined;\n /** @TODO cleanup type precision via inference+generics */\n track: ReturnType<typeof getSelectedTrack>;\n segmentsCanLoad: boolean;\n};\n\nfunction selectLoadingInputs(\n [segmentsCanLoad, state]: [boolean, SegmentLoadingState],\n type: MediaTrackType\n): LoadingInputs {\n const { playbackInitiated, preload, currentTime } = state;\n const track = getSelectedTrack(state, type);\n return {\n playbackInitiated,\n preload,\n currentTime,\n track,\n segmentsCanLoad,\n };\n}\n\n/**\n * Equality function encoding the condition hierarchy for relevant changes.\n *\n * Pre-play (!playbackInitiated):\n * Only preload changes matter. currentTime and resolvedTrackId are ignored\n * (track changes not supported pre-play; currentTime value is used at\n * trigger time but changes don't re-trigger).\n *\n * playbackInitiated transition:\n * Always fires (handled in the subscriber; preload='auto' suppression\n * applied there since equality functions have no memory of prior values).\n *\n * Post-play (playbackInitiated):\n * resolvedTrackId changes (track switch or previously-unresolved track\n * resolving) and currentTime changes both trigger. preload is irrelevant.\n */\nconst segmentStartFor = (currentTime: number | undefined, track: ResolvedTrack | undefined) => {\n if (currentTime == null) return undefined;\n return track?.segments.find(\n ({ startTime, duration }, i, segments) =>\n currentTime >= startTime && (currentTime < startTime + duration || i === segments.length - 1)\n )?.startTime;\n};\n\n/**\n * Returns true when the inputs are equal (no meaningful change — don't fire).\n * Returns false when the inputs differ in a way that requires a new message.\n *\n * This IS the shouldLoadSegments logic, expressed as an equality function.\n */\nfunction loadingInputsEq(prevState: LoadingInputs, curState: LoadingInputs): boolean {\n if (!curState.segmentsCanLoad) return true;\n // Haven't started playback. Only care about preload changes.\n if (!curState.playbackInitiated) {\n if (curState.preload === 'none') return true; // blocked — equal, don't fire\n return curState.preload === prevState.preload; // equal if preload unchanged\n }\n // Transition: !playbackInitiated → playbackInitiated\n if (!prevState.playbackInitiated && curState.playbackInitiated) {\n if (prevState.preload !== 'auto') return false; // fire — message shape changes\n // preload was 'auto': fall through to segment comparison (suppress if same position)\n }\n\n // Don't treat as a change if we don't (yet) have a currently resolved track.\n if (!curState.track || !isResolvedTrack(curState.track)) return true;\n\n // If we *do* have a currently resolved track, treat as a change if the track ids have changed\n if (prevState.track?.id !== curState.track.id && isResolvedTrack(curState.track)) return false;\n\n // Finally, if playback has initiated *and* we have a resolved track, check if currentTime\n // has changed \"significantly\" (based on segment time range boundaries) and treat as a change\n // if so (regardless of whether or not the track has changed)\n return (\n segmentStartFor(prevState.currentTime, curState.track as ResolvedTrack) ===\n segmentStartFor(curState.currentTime, curState.track as ResolvedTrack)\n );\n}\n\n// ============================================================================\n// REACTOR\n// ============================================================================\n\n/**\n * Load segments orchestration — Reactor layer.\n *\n * Sends typed load messages to a SegmentLoaderActor when relevant conditions\n * change. Uses targeted subscriptions rather than broad combineLatest so only\n * meaningful state changes trigger evaluation.\n *\n * Condition hierarchy (see SegmentLoadingKey for detail):\n *\n * !playbackInitiated\n * preload==='none' (or unset) → dormant; no trigger\n * preload==='metadata' → trigger on transition to 'metadata'\n * preload==='auto' → trigger on transition to 'auto'\n *\n * !playbackInitiated → playbackInitiated\n * preload !== 'auto' → trigger (message shape changes)\n * preload === 'auto' → suppressed (was already full-range mode;\n * let segmentStart take over post-play)\n * KNOWN LIMITATION: seek-before-play with\n * preload='auto' is not supported — if the\n * user seeks before pressing play, the\n * first re-send is delayed until the next\n * segment boundary crossing post-play.\n *\n * playbackInitiated\n * resolvedTrackId changes → trigger\n * segmentStart(currentTime) changes → trigger (segment boundary only)\n *\n * @example\n * const cleanup = loadSegments({ state, owners }, { type: 'video' });\n */\nexport function loadSegments<S extends SegmentLoadingState, O extends SegmentLoadingOwners>(\n { state, owners }: { state: Signal<S>; owners: Signal<O> },\n config: { type: MediaTrackType }\n): () => void {\n const { type } = config;\n const actorKey = ActorKeyByType[type];\n\n // Local throughput signal — used by createTrackedFetch to sample bandwidth\n // per chunk and propagate back to the shared state for ABR.\n const initialBandwidth = state.get().bandwidthState;\n const throughput = signal<BandwidthState>(\n initialBandwidth ?? {\n fastEstimate: 0,\n fastTotalWeight: 0,\n slowEstimate: 0,\n slowTotalWeight: 0,\n bytesSampled: 0,\n }\n );\n\n const fetchBytes =\n type === 'video'\n ? createTrackedFetch(\n throughput,\n initialBandwidth !== undefined\n ? (next) => {\n state.set(Object.assign({}, state.get(), { bandwidthState: next }) as S);\n }\n : undefined\n )\n : fetchStream;\n\n // Local segment loader — signal so segmentsCanLoad can track it reactively\n const segmentLoader = signal<SegmentLoaderActor | undefined>(undefined);\n\n // Computed: isolates the specific actor key so the lifecycle effect only\n // re-runs when that actor reference changes, not on unrelated owners updates.\n const actorSource = computed<SourceBufferActor | undefined>(() => owners.get()[actorKey]);\n\n // Actor lifecycle — destroy and recreate SegmentLoaderActor when the\n // upstream SourceBufferActor is replaced (quality switch) or removed.\n let currentLoader: SegmentLoaderActor | undefined;\n const cleanupActorLifecycle = effect(() => {\n const actor = actorSource.get();\n\n if (currentLoader) {\n currentLoader.destroy();\n segmentLoader.set(undefined);\n currentLoader = undefined;\n }\n\n if (actor) {\n const loader = createSegmentLoaderActor(actor, fetchBytes);\n currentLoader = loader;\n segmentLoader.set(loader);\n }\n });\n\n // Derived: true only when we have both a resolved track and a ready loader\n const segmentsCanLoad = computed(() => {\n const track = getSelectedTrack(state.get(), type);\n return !!track && isResolvedTrack(track) && !!segmentLoader.get();\n });\n\n // Loading inputs — selectLoadingInputs is cheap; let the effect body apply\n // loadingInputsEq rather than the computed, so the first run always fires\n // regardless of the initial state.\n const loadingInputs = computed(() => selectLoadingInputs([segmentsCanLoad.get(), state.get()], type));\n\n // Load effect — prevInputs guards against redundant messages using the same\n // equality semantics as the former combineLatest selector subscription.\n // prevInputs is updated only when we actually send a message so that the\n // transition segmentsCanLoad: false → true always fires (loadingInputsEq\n // returns true for that transition, so we must not skip it here).\n let prevInputs: LoadingInputs | undefined;\n const cleanupLoadEffect = effect(() => {\n const inputs = loadingInputs.get();\n if (prevInputs !== undefined && loadingInputsEq(prevInputs, inputs)) return;\n\n const { preload, playbackInitiated, currentTime, track, segmentsCanLoad: canLoad } = inputs;\n if (!canLoad) return;\n\n prevInputs = inputs;\n\n const fullMode = preload === 'auto' || !!playbackInitiated;\n if (!fullMode) {\n // Metadata mode: init only, no range.\n /** @ts-expect-error */\n segmentLoader.get()?.send({ type: 'load', track });\n } else {\n segmentLoader.get()?.send({\n type: 'load',\n /** @ts-expect-error */\n track,\n range: {\n start: currentTime as number,\n end: (currentTime as number) + DEFAULT_FORWARD_BUFFER_CONFIG.bufferDuration,\n },\n });\n }\n });\n\n return () => {\n cleanupActorLifecycle();\n cleanupLoadEffect();\n currentLoader?.destroy();\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,104 @@
1
+ import { computed, untrack, update } from "../../core/signals/primitives.js";
2
+ import { isResolvedTrack } from "../../core/types/index.js";
3
+ import { createMachineReactor } from "../../core/create-machine-reactor.js";
4
+ import { createTextTrackSegmentLoaderActor } from "./text-track-segment-loader-actor.js";
5
+ import { createTextTracksActor } from "./text-tracks-actor.js";
6
+ //#region src/dom/features/load-text-track-cues.ts
7
+ function getTextTracks(presentation) {
8
+ return presentation?.selectionSets?.find((s) => s.type === "text")?.switchingSets[0]?.tracks;
9
+ }
10
+ function findSelectedTrack(state) {
11
+ const track = getTextTracks(state.presentation)?.find((t) => t.id === state.selectedTextTrackId);
12
+ return track && isResolvedTrack(track) ? track : void 0;
13
+ }
14
+ /**
15
+ * Derives the correct state from current state and owners.
16
+ *
17
+ * States are mutually exclusive and exhaustive:
18
+ * - `'preconditions-unmet'`: no mediaElement, or no resolved presentation with text tracks
19
+ * - `'setting-up'`: preconditions met; actors not yet in owners
20
+ * - `'pending'`: actors alive; no selection, or selected track not yet resolved/in DOM
21
+ * - `'monitoring-for-loads'`: selected track resolved, in DOM — ready to dispatch load messages
22
+ */
23
+ function deriveState(state, owners) {
24
+ if (!owners.mediaElement || !getTextTracks(state.presentation)?.length) return "preconditions-unmet";
25
+ if (!owners.textTracksActor || !owners.segmentLoaderActor) return "setting-up";
26
+ const track = findSelectedTrack(state);
27
+ if (!track || track.segments.length === 0) return "pending";
28
+ if (!Array.from(owners.mediaElement.textTracks).some((t) => t.id === state.selectedTextTrackId)) return "pending";
29
+ return "monitoring-for-loads";
30
+ }
31
+ function teardownActors(owners) {
32
+ const { textTracksActor, segmentLoaderActor } = untrack(() => owners.get());
33
+ if (!textTracksActor && !segmentLoaderActor) return;
34
+ textTracksActor?.destroy();
35
+ segmentLoaderActor?.destroy();
36
+ update(owners, {
37
+ textTracksActor: void 0,
38
+ segmentLoaderActor: void 0
39
+ });
40
+ }
41
+ /**
42
+ * Text track cue loading orchestration.
43
+ *
44
+ * A single `always` monitor keeps the reactor in sync with conditions.
45
+ * Actor lifecycle is managed across two states:
46
+ *
47
+ * - **`'setting-up'`** — creates `TextTracksActor` and `TextTrackSegmentLoaderActor`
48
+ * and writes them to owners. Entry resets any stale actors first.
49
+ * - **`'preconditions-unmet'`** — destroys any actors in owners and resets them to
50
+ * `undefined`. Handles all paths back from active states.
51
+ * - **`'monitoring-for-loads'`** — reactive dispatch: re-runs whenever `state`
52
+ * changes (selection, currentTime, presentation) and sends a `load` message to
53
+ * the segment loader.
54
+ *
55
+ * **Destroy note:** actors written to owners are NOT auto-destroyed when
56
+ * `reactor.destroy()` is called. Callers must read them from owners and destroy
57
+ * them explicitly alongside the reactor.
58
+ *
59
+ * @example
60
+ * const reactor = loadTextTrackCues({ state, owners });
61
+ * // later:
62
+ * const { textTracksActor, segmentLoaderActor } = owners.get();
63
+ * textTracksActor?.destroy();
64
+ * segmentLoaderActor?.destroy();
65
+ * reactor.destroy();
66
+ */
67
+ function loadTextTrackCues({ state, owners }) {
68
+ const derivedStateSignal = computed(() => deriveState(state.get(), owners.get()));
69
+ const currentTimeSignal = computed(() => state.get().currentTime ?? 0);
70
+ const selectedTrackSignal = computed(() => findSelectedTrack(state.get()));
71
+ return createMachineReactor({
72
+ initial: "preconditions-unmet",
73
+ monitor: () => derivedStateSignal.get(),
74
+ states: {
75
+ "preconditions-unmet": { entry: () => {
76
+ teardownActors(owners);
77
+ } },
78
+ "setting-up": { entry: () => {
79
+ teardownActors(owners);
80
+ const mediaElement = owners.get().mediaElement;
81
+ const textTracksActor = createTextTracksActor(mediaElement);
82
+ update(owners, {
83
+ textTracksActor,
84
+ segmentLoaderActor: createTextTrackSegmentLoaderActor(textTracksActor)
85
+ });
86
+ } },
87
+ pending: {},
88
+ "monitoring-for-loads": { effects: () => {
89
+ const currentTime = currentTimeSignal.get();
90
+ const track = selectedTrackSignal.get();
91
+ const { segmentLoaderActor } = untrack(() => owners.get());
92
+ segmentLoaderActor.send({
93
+ type: "load",
94
+ track,
95
+ currentTime
96
+ });
97
+ } }
98
+ }
99
+ });
100
+ }
101
+ //#endregion
102
+ export { loadTextTrackCues };
103
+
104
+ //# sourceMappingURL=load-text-track-cues.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"load-text-track-cues.js","names":[],"sources":["../../../../src/dom/features/load-text-track-cues.ts"],"sourcesContent":["import type { Reactor } from '../../core/create-machine-reactor';\nimport { createMachineReactor } from '../../core/create-machine-reactor';\nimport { computed, type Signal, untrack, update } from '../../core/signals/primitives';\nimport type { Presentation, TextTrack } from '../../core/types';\nimport { isResolvedTrack } from '../../core/types';\nimport type { TextTrackSegmentLoaderActor } from './text-track-segment-loader-actor';\nimport { createTextTrackSegmentLoaderActor } from './text-track-segment-loader-actor';\nimport type { TextTracksActor } from './text-tracks-actor';\nimport { createTextTracksActor } from './text-tracks-actor';\n\n/**\n * FSM states for text track cue loading.\n *\n * ```\n * 'preconditions-unmet' ── mediaElement + presentation + text tracks ──→ 'setting-up'\n * ↑ |\n * │ actors created\n * │ in owners\n * │ ↓\n * ├───────────────── preconditions lost ─────────────────────── 'pending'\n * │ |\n * │ selectedTrack resolved + in DOM\n * │ ↓\n * └───────────────── preconditions lost ──────────── 'monitoring-for-loads'\n *\n * any state ──── destroy() ────→ 'destroying' ────→ 'destroyed'\n * ```\n */\nexport type LoadTextTrackCuesState = 'preconditions-unmet' | 'setting-up' | 'pending' | 'monitoring-for-loads';\n\n/**\n * State shape for text track cue loading.\n */\nexport interface TextTrackCueLoadingState {\n selectedTextTrackId?: string;\n presentation?: Presentation;\n /** Current playback position — used to gate VTT segment fetching to the forward buffer window. */\n currentTime?: number;\n}\n\n/**\n * Owners shape for text track cue loading.\n *\n * `textTracksActor` and `segmentLoaderActor` are managed by this reactor:\n * written to owners in `'setting-up'` and reset to `undefined` on entry to\n * `'preconditions-unmet'` or `'setting-up'`. Callers are responsible for\n * destroying them when the reactor itself is destroyed (actors are not\n * auto-cleaned on `destroy()` — read them from owners and destroy explicitly).\n */\nexport interface TextTrackCueLoadingOwners {\n mediaElement?: HTMLMediaElement | undefined;\n textTracksActor?: TextTracksActor | undefined;\n segmentLoaderActor?: TextTrackSegmentLoaderActor | undefined;\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nfunction getTextTracks(presentation: Presentation | undefined) {\n return presentation?.selectionSets?.find((s) => s.type === 'text')?.switchingSets[0]?.tracks;\n}\n\nfunction findSelectedTrack(state: TextTrackCueLoadingState): TextTrack | undefined {\n const track = getTextTracks(state.presentation)?.find((t) => t.id === state.selectedTextTrackId);\n return track && isResolvedTrack(track) ? track : undefined;\n}\n\n/**\n * Derives the correct state from current state and owners.\n *\n * States are mutually exclusive and exhaustive:\n * - `'preconditions-unmet'`: no mediaElement, or no resolved presentation with text tracks\n * - `'setting-up'`: preconditions met; actors not yet in owners\n * - `'pending'`: actors alive; no selection, or selected track not yet resolved/in DOM\n * - `'monitoring-for-loads'`: selected track resolved, in DOM — ready to dispatch load messages\n */\nfunction deriveState(state: TextTrackCueLoadingState, owners: TextTrackCueLoadingOwners): LoadTextTrackCuesState {\n if (!owners.mediaElement || !getTextTracks(state.presentation)?.length) {\n return 'preconditions-unmet';\n }\n if (!owners.textTracksActor || !owners.segmentLoaderActor) {\n return 'setting-up';\n }\n const track = findSelectedTrack(state);\n if (!track || track.segments.length === 0) return 'pending';\n if (!Array.from(owners.mediaElement.textTracks).some((t) => t.id === state.selectedTextTrackId)) {\n return 'pending';\n }\n return 'monitoring-for-loads';\n}\n\nfunction teardownActors(owners: Signal<TextTrackCueLoadingOwners>) {\n const { textTracksActor, segmentLoaderActor } = untrack(() => owners.get());\n // Only update owners if there are actors to clean up — avoids spurious\n // signal writes on initial startup that would re-trigger other features.\n if (!textTracksActor && !segmentLoaderActor) return;\n textTracksActor?.destroy();\n segmentLoaderActor?.destroy();\n update(owners, { textTracksActor: undefined, segmentLoaderActor: undefined });\n}\n\n// ============================================================================\n// Main export\n// ============================================================================\n\n/**\n * Text track cue loading orchestration.\n *\n * A single `always` monitor keeps the reactor in sync with conditions.\n * Actor lifecycle is managed across two states:\n *\n * - **`'setting-up'`** — creates `TextTracksActor` and `TextTrackSegmentLoaderActor`\n * and writes them to owners. Entry resets any stale actors first.\n * - **`'preconditions-unmet'`** — destroys any actors in owners and resets them to\n * `undefined`. Handles all paths back from active states.\n * - **`'monitoring-for-loads'`** — reactive dispatch: re-runs whenever `state`\n * changes (selection, currentTime, presentation) and sends a `load` message to\n * the segment loader.\n *\n * **Destroy note:** actors written to owners are NOT auto-destroyed when\n * `reactor.destroy()` is called. Callers must read them from owners and destroy\n * them explicitly alongside the reactor.\n *\n * @example\n * const reactor = loadTextTrackCues({ state, owners });\n * // later:\n * const { textTracksActor, segmentLoaderActor } = owners.get();\n * textTracksActor?.destroy();\n * segmentLoaderActor?.destroy();\n * reactor.destroy();\n */\nexport function loadTextTrackCues<S extends TextTrackCueLoadingState, O extends TextTrackCueLoadingOwners>({\n state,\n owners,\n}: {\n state: Signal<S>;\n owners: Signal<O>;\n}): Reactor<LoadTextTrackCuesState | 'destroying' | 'destroyed'> {\n const derivedStateSignal = computed(() => deriveState(state.get(), owners.get()));\n const currentTimeSignal = computed(() => state.get().currentTime ?? 0);\n const selectedTrackSignal = computed(() => findSelectedTrack(state.get()));\n\n return createMachineReactor<LoadTextTrackCuesState>({\n initial: 'preconditions-unmet',\n monitor: () => derivedStateSignal.get(),\n states: {\n 'preconditions-unmet': {\n // Entry: defensive actor reset on state entry (no-op if already undefined).\n entry: () => {\n teardownActors(owners);\n },\n },\n\n 'setting-up': {\n // Entry: reset any stale actors, then create fresh ones and write to owners.\n // The fn body is automatically untracked — no untrack() needed for mediaElement.\n entry: () => {\n teardownActors(owners);\n const mediaElement = owners.get().mediaElement as HTMLMediaElement;\n const textTracksActor = createTextTracksActor(mediaElement);\n const segmentLoaderActor = createTextTrackSegmentLoaderActor(textTracksActor);\n update(owners, { textTracksActor, segmentLoaderActor } as Partial<O>);\n },\n },\n\n pending: {},\n\n 'monitoring-for-loads': {\n // Reaction: re-runs whenever currentTime or selectedTrack changes, dispatching\n // a load message to the segment loader. owners is read with untrack() since\n // actor presence is guaranteed by deriveState when in this state.\n effects: () => {\n const currentTime = currentTimeSignal.get();\n const track = selectedTrackSignal.get()!;\n // deriveState guarantees segmentLoaderActor is in owners and findSelectedTrack\n // returns a valid resolved track when in this state. The always monitor\n // (registered before this effect) transitions us out before this re-runs\n // if either invariant ever stops holding.\n const { segmentLoaderActor } = untrack(() => owners.get());\n segmentLoaderActor!.send({ type: 'load', track, currentTime });\n },\n },\n },\n });\n}\n"],"mappings":";;;;;;AA2DA,SAAS,cAAc,cAAwC;AAC7D,QAAO,cAAc,eAAe,MAAM,MAAM,EAAE,SAAS,OAAO,EAAE,cAAc,IAAI;;AAGxF,SAAS,kBAAkB,OAAwD;CACjF,MAAM,QAAQ,cAAc,MAAM,aAAa,EAAE,MAAM,MAAM,EAAE,OAAO,MAAM,oBAAoB;AAChG,QAAO,SAAS,gBAAgB,MAAM,GAAG,QAAQ,KAAA;;;;;;;;;;;AAYnD,SAAS,YAAY,OAAiC,QAA2D;AAC/G,KAAI,CAAC,OAAO,gBAAgB,CAAC,cAAc,MAAM,aAAa,EAAE,OAC9D,QAAO;AAET,KAAI,CAAC,OAAO,mBAAmB,CAAC,OAAO,mBACrC,QAAO;CAET,MAAM,QAAQ,kBAAkB,MAAM;AACtC,KAAI,CAAC,SAAS,MAAM,SAAS,WAAW,EAAG,QAAO;AAClD,KAAI,CAAC,MAAM,KAAK,OAAO,aAAa,WAAW,CAAC,MAAM,MAAM,EAAE,OAAO,MAAM,oBAAoB,CAC7F,QAAO;AAET,QAAO;;AAGT,SAAS,eAAe,QAA2C;CACjE,MAAM,EAAE,iBAAiB,uBAAuB,cAAc,OAAO,KAAK,CAAC;AAG3E,KAAI,CAAC,mBAAmB,CAAC,mBAAoB;AAC7C,kBAAiB,SAAS;AAC1B,qBAAoB,SAAS;AAC7B,QAAO,QAAQ;EAAE,iBAAiB,KAAA;EAAW,oBAAoB,KAAA;EAAW,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiC/E,SAAgB,kBAA2F,EACzG,OACA,UAI+D;CAC/D,MAAM,qBAAqB,eAAe,YAAY,MAAM,KAAK,EAAE,OAAO,KAAK,CAAC,CAAC;CACjF,MAAM,oBAAoB,eAAe,MAAM,KAAK,CAAC,eAAe,EAAE;CACtE,MAAM,sBAAsB,eAAe,kBAAkB,MAAM,KAAK,CAAC,CAAC;AAE1E,QAAO,qBAA6C;EAClD,SAAS;EACT,eAAe,mBAAmB,KAAK;EACvC,QAAQ;GACN,uBAAuB,EAErB,aAAa;AACX,mBAAe,OAAO;MAEzB;GAED,cAAc,EAGZ,aAAa;AACX,mBAAe,OAAO;IACtB,MAAM,eAAe,OAAO,KAAK,CAAC;IAClC,MAAM,kBAAkB,sBAAsB,aAAa;AAE3D,WAAO,QAAQ;KAAE;KAAiB,oBADP,kCAAkC,gBAAgB;KACvB,CAAe;MAExE;GAED,SAAS,EAAE;GAEX,wBAAwB,EAItB,eAAe;IACb,MAAM,cAAc,kBAAkB,KAAK;IAC3C,MAAM,QAAQ,oBAAoB,KAAK;IAKvC,MAAM,EAAE,uBAAuB,cAAc,OAAO,KAAK,CAAC;AAC1D,uBAAoB,KAAK;KAAE,MAAM;KAAQ;KAAO;KAAa,CAAC;MAEjE;GACF;EACF,CAAC"}
@@ -0,0 +1,243 @@
1
+ import { calculateForwardFlushPoint, getSegmentsToLoad } from "../../core/buffer/forward-buffer.js";
2
+ import { effect } from "../../core/signals/effect.js";
3
+ import { calculateBackBufferFlushPoint } from "../../core/buffer/back-buffer.js";
4
+ import { createMachineActor } from "../../core/create-machine-actor.js";
5
+ import { SerialRunner, Task } from "../../core/task.js";
6
+ //#region src/dom/features/segment-loader-actor.ts
7
+ /**
8
+ * Resolves when the SourceBufferActor snapshot reaches 'idle'.
9
+ * Rejects if the signal is aborted or the actor is destroyed.
10
+ *
11
+ * Used to sequence SourceBufferActor operations without awaiting send()
12
+ * directly — send() is fire-and-forget; callers observe completion via
13
+ * state transition.
14
+ */
15
+ function waitForIdle(snapshot, signal) {
16
+ return new Promise((resolve, reject) => {
17
+ if (snapshot.get().value === "idle") {
18
+ resolve();
19
+ return;
20
+ }
21
+ if (snapshot.get().value === "destroyed") {
22
+ reject(new DOMException("Aborted", "AbortError"));
23
+ return;
24
+ }
25
+ if (signal.aborted) {
26
+ reject(signal.reason);
27
+ return;
28
+ }
29
+ let stop;
30
+ const cleanup = (fn) => {
31
+ stop?.();
32
+ signal.removeEventListener("abort", onAbort);
33
+ fn();
34
+ };
35
+ const onAbort = () => cleanup(() => reject(signal.reason));
36
+ stop = effect(() => {
37
+ const value = snapshot.get().value;
38
+ if (value === "idle") cleanup(resolve);
39
+ else if (value === "destroyed") cleanup(() => reject(new DOMException("Aborted", "AbortError")));
40
+ });
41
+ signal.addEventListener("abort", onAbort, { once: true });
42
+ });
43
+ }
44
+ /**
45
+ * Wraps a LoadTask descriptor into a Task that fetches (if needed) and
46
+ * forwards to SourceBufferActor. Updates in-flight context around async
47
+ * operations so the loading handler can make accurate continue/preempt
48
+ * decisions at any point.
49
+ */
50
+ function makeLoadTask(op, { getContext, setContext, fetchBytes, sourceBufferActor }) {
51
+ return new Task(async (taskSignal) => {
52
+ if (taskSignal.aborted) return;
53
+ if (op.type === "remove") {
54
+ sourceBufferActor.send(op);
55
+ await waitForIdle(sourceBufferActor.snapshot, taskSignal);
56
+ return;
57
+ }
58
+ if (op.type === "append-init") {
59
+ setContext({
60
+ ...getContext(),
61
+ inFlightInitTrackId: op.meta.trackId
62
+ });
63
+ try {
64
+ const data = await fetchBytes(op, {
65
+ signal: taskSignal,
66
+ minChunkSize: Infinity
67
+ });
68
+ if (!taskSignal.aborted) {
69
+ sourceBufferActor.send({
70
+ type: "append-init",
71
+ data,
72
+ meta: op.meta
73
+ });
74
+ await waitForIdle(sourceBufferActor.snapshot, taskSignal);
75
+ }
76
+ } finally {
77
+ setContext({
78
+ ...getContext(),
79
+ inFlightInitTrackId: null
80
+ });
81
+ }
82
+ return;
83
+ }
84
+ setContext({
85
+ ...getContext(),
86
+ inFlightSegmentId: op.meta.id
87
+ });
88
+ try {
89
+ const stream = await fetchBytes(op, { signal: taskSignal });
90
+ if (!taskSignal.aborted) {
91
+ sourceBufferActor.send({
92
+ type: "append-segment",
93
+ data: stream,
94
+ meta: op.meta
95
+ });
96
+ await waitForIdle(sourceBufferActor.snapshot, taskSignal);
97
+ }
98
+ } finally {
99
+ setContext({
100
+ ...getContext(),
101
+ inFlightSegmentId: null
102
+ });
103
+ }
104
+ });
105
+ }
106
+ /**
107
+ * Creates a SegmentLoaderActor for one track type (video or audio).
108
+ *
109
+ * Receives load assignments via `send()` and owns all execution: planning,
110
+ * removes, fetches, and appends. Coordinates with the SourceBufferActor for
111
+ * all physical SourceBuffer operations.
112
+ *
113
+ * Planning (Cases 1–3) happens in the `load` handler on every incoming
114
+ * message, producing an ordered LoadTask list. The runner drains that list
115
+ * sequentially via SerialRunner. When a new message arrives mid-run, the
116
+ * handler replans and either continues the in-flight operation (abortPending
117
+ * + schedule new remainder) or preempts it (abortAll + cancel SourceBuffer
118
+ * if needed + schedule new plan).
119
+ *
120
+ * @param sourceBufferActor - Shared SourceBufferActor reference (not owned)
121
+ * @param fetchBytes - Tracked fetch closure (owns throughput sampling for segments).
122
+ * Accepts an optional `minChunkSize` in options; init segments pass `Infinity`
123
+ * so the entire body accumulates as one chunk before appending.
124
+ */
125
+ function createSegmentLoaderActor(sourceBufferActor, fetchBytes) {
126
+ const getBufferedSegments = (allSegments) => {
127
+ const bufferedIds = new Set(sourceBufferActor.snapshot.get().context.segments.filter((s) => !s.partial).map((s) => s.id));
128
+ return allSegments.filter((s) => bufferedIds.has(s.id));
129
+ };
130
+ /**
131
+ * Translate a load message into an ordered LoadTask list based on committed
132
+ * actor state. In-flight awareness is handled separately in the load handler.
133
+ *
134
+ * @todo Rename alongside LoadTask (e.g. planOps).
135
+ *
136
+ * Case 1 — Removes: forward and back buffer flush points, segment-aligned.
137
+ * No flush on track switch: appending new content overwrites existing buffer
138
+ * ranges, and the actor's time-aligned deduplication keeps the segment model
139
+ * accurate as new segments arrive.
140
+ *
141
+ * Case 2 — Init: schedule if not yet committed for this track.
142
+ *
143
+ * Case 3 — Segments: all segments in the load window not yet committed.
144
+ */
145
+ const planTasks = (message) => {
146
+ const { track, range } = message;
147
+ const actorCtx = sourceBufferActor.snapshot.get().context;
148
+ const bufferedSegments = getBufferedSegments(track.segments);
149
+ const currentTime = range?.start ?? 0;
150
+ const tasks = [];
151
+ if (range) {
152
+ const forwardFlushStart = calculateForwardFlushPoint(bufferedSegments, currentTime);
153
+ if (forwardFlushStart < Infinity) tasks.push({
154
+ type: "remove",
155
+ start: forwardFlushStart,
156
+ end: Infinity
157
+ });
158
+ const backFlushEnd = calculateBackBufferFlushPoint(bufferedSegments, currentTime);
159
+ if (backFlushEnd > 0) tasks.push({
160
+ type: "remove",
161
+ start: 0,
162
+ end: backFlushEnd
163
+ });
164
+ }
165
+ if (actorCtx.initTrackId !== track.id) tasks.push({
166
+ type: "append-init",
167
+ meta: { trackId: track.id },
168
+ url: track.initialization.url,
169
+ ...track.initialization.byteRange !== void 0 && { byteRange: track.initialization.byteRange }
170
+ });
171
+ if (range) {
172
+ const EPSILON = 1e-4;
173
+ const segmentsToLoad = getSegmentsToLoad(track.segments, bufferedSegments, currentTime).filter((seg) => {
174
+ const existing = actorCtx.segments.find((s) => Math.abs(s.startTime - seg.startTime) < EPSILON);
175
+ if (existing?.partial) return true;
176
+ if (!existing?.trackBandwidth || !track.bandwidth) return true;
177
+ return track.bandwidth > existing.trackBandwidth;
178
+ });
179
+ for (const segment of segmentsToLoad) tasks.push({
180
+ type: "append-segment",
181
+ meta: {
182
+ id: segment.id,
183
+ startTime: segment.startTime,
184
+ duration: segment.duration,
185
+ trackId: track.id,
186
+ trackBandwidth: track.bandwidth
187
+ },
188
+ url: segment.url,
189
+ ...segment.byteRange !== void 0 && { byteRange: segment.byteRange }
190
+ });
191
+ }
192
+ return tasks;
193
+ };
194
+ const scheduleAll = (tasks, { getContext, setContext, runner }) => {
195
+ tasks.forEach((op) => {
196
+ runner.schedule(makeLoadTask(op, {
197
+ getContext,
198
+ setContext,
199
+ fetchBytes,
200
+ sourceBufferActor
201
+ })).then(void 0, (e) => {
202
+ if (e instanceof Error && e.name === "AbortError") return;
203
+ console.error("Unexpected error in segment loader:", e);
204
+ runner.abortPending();
205
+ });
206
+ });
207
+ };
208
+ return createMachineActor({
209
+ runner: () => new SerialRunner(),
210
+ initial: "idle",
211
+ context: {
212
+ inFlightInitTrackId: null,
213
+ inFlightSegmentId: null
214
+ },
215
+ states: {
216
+ idle: { on: { load: (msg, ctx) => {
217
+ const allTasks = planTasks(msg);
218
+ if (allTasks.length === 0) return;
219
+ ctx.transition("loading");
220
+ scheduleAll(allTasks, ctx);
221
+ } } },
222
+ loading: {
223
+ onSettled: "idle",
224
+ on: { load: (msg, ctx) => {
225
+ const { context, runner } = ctx;
226
+ const allTasks = planTasks(msg);
227
+ if (context.inFlightSegmentId !== null && allTasks.some((t) => t.type === "append-segment" && t.meta.id === context.inFlightSegmentId) || context.inFlightInitTrackId !== null && allTasks.some((t) => t.type === "append-init" && t.meta.trackId === context.inFlightInitTrackId)) {
228
+ runner.abortPending();
229
+ scheduleAll(allTasks.filter((t) => !(t.type === "append-segment" && t.meta.id === context.inFlightSegmentId) && !(t.type === "append-init" && t.meta.trackId === context.inFlightInitTrackId)), ctx);
230
+ } else {
231
+ runner.abortAll();
232
+ if (context.inFlightSegmentId !== null || context.inFlightInitTrackId !== null && allTasks.some((t) => t.type === "append-init" && t.meta.trackId !== context.inFlightInitTrackId)) sourceBufferActor.send({ type: "cancel" });
233
+ scheduleAll(allTasks, ctx);
234
+ }
235
+ } }
236
+ }
237
+ }
238
+ });
239
+ }
240
+ //#endregion
241
+ export { createSegmentLoaderActor };
242
+
243
+ //# sourceMappingURL=segment-loader-actor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"segment-loader-actor.js","names":[],"sources":["../../../../src/dom/features/segment-loader-actor.ts"],"sourcesContent":["import { calculateBackBufferFlushPoint } from '../../core/buffer/back-buffer';\nimport { calculateForwardFlushPoint, getSegmentsToLoad } from '../../core/buffer/forward-buffer';\nimport { createMachineActor, type HandlerContext, type MessageActor } from '../../core/create-machine-actor';\nimport { effect } from '../../core/signals/effect';\nimport { SerialRunner, Task } from '../../core/task';\nimport type { AddressableObject, AudioTrack, Segment, VideoTrack } from '../../core/types';\nimport type {\n AppendInitMessage,\n AppendSegmentMessage,\n RemoveMessage,\n SourceBufferActor,\n} from '../media/source-buffer-actor';\n\n// ============================================================================\n// BUFFER STATE TYPES\n// ============================================================================\n\n/**\n * Buffer state for a single SourceBuffer.\n * Tracks which init segment and media segments are loaded.\n */\nexport interface SourceBufferState {\n /** Track ID of the loaded init segment */\n initTrackId?: string;\n /** Loaded media segments (unordered - selectors derive ordering) */\n segments: Array<{ id: string; trackId: string }>;\n}\n\n/**\n * Buffer state for all SourceBuffers.\n */\nexport interface BufferState {\n video?: SourceBufferState;\n audio?: SourceBufferState;\n}\n\n// ============================================================================\n// MESSAGE PROTOCOL\n// ============================================================================\n\n/** Track types that have SourceBuffers (video and audio only). */\nexport type SegmentLoaderTrack = VideoTrack | AudioTrack;\n\n/**\n * Message sent to a SegmentLoaderActor.\n *\n * `range` is optional to distinguish loading modes:\n * - No range: load init segment only (metadata preload mode)\n * - With range: load init + all segments overlapping [start, end]\n *\n * `start` and `end` are raw time values — no segment snapping.\n * The actor maps them onto segment boundaries internally.\n */\nexport type SegmentLoaderMessage = {\n type: 'load';\n track: SegmentLoaderTrack;\n range?: { start: number; end: number };\n};\n\n// ============================================================================\n// LOAD TASK\n// ============================================================================\n\n/**\n * A LoadTask is the intent to perform one unit of SegmentLoader work.\n * Unlike SourceBufferMessage, fetch-based tasks carry a URL rather than\n * pre-fetched data — the runner fetches and appends them in sequence.\n *\n * Derived from SourceBufferMessage types by removing `data` and adding\n * a fetch URL via AddressableObject.\n *\n * @todo Rename — \"LoadTask\" risks confusion with the `Task` class used for\n * SourceBufferActor scheduling. These are closer to operation descriptors or\n * messages than tasks in that sense.\n */\nexport type LoadTask =\n | (Omit<AppendInitMessage, 'data'> & AddressableObject)\n | (Omit<AppendSegmentMessage, 'data'> & AddressableObject)\n | RemoveMessage;\n\n// ============================================================================\n// ACTOR INTERFACE\n// ============================================================================\n\n/** Finite states of the actor. */\nexport type SegmentLoaderActorState = 'idle' | 'loading' | 'destroyed';\n\n/** Non-finite (extended) data managed by the actor. */\nexport interface SegmentLoaderActorContext {\n /** Track ID of the init segment currently being fetched/appended, or null. */\n inFlightInitTrackId: string | null;\n /** Segment ID currently being fetched/appended, or null. */\n inFlightSegmentId: string | null;\n}\n\nexport type SegmentLoaderActor = MessageActor<SegmentLoaderActorState, SegmentLoaderActorContext, SegmentLoaderMessage>;\n\n// ============================================================================\n// HELPERS\n// ============================================================================\n\ntype FetchBytes = (\n addressable: AddressableObject,\n options?: RequestInit & { minChunkSize?: number }\n) => Promise<AsyncIterable<Uint8Array>>;\n\n/**\n * Resolves when the SourceBufferActor snapshot reaches 'idle'.\n * Rejects if the signal is aborted or the actor is destroyed.\n *\n * Used to sequence SourceBufferActor operations without awaiting send()\n * directly — send() is fire-and-forget; callers observe completion via\n * state transition.\n */\nfunction waitForIdle(snapshot: SourceBufferActor['snapshot'], signal: AbortSignal): Promise<void> {\n return new Promise((resolve, reject) => {\n if (snapshot.get().value === 'idle') {\n resolve();\n return;\n }\n if (snapshot.get().value === 'destroyed') {\n reject(new DOMException('Aborted', 'AbortError'));\n return;\n }\n if (signal.aborted) {\n reject(signal.reason);\n return;\n }\n\n let stop: (() => void) | undefined;\n\n const cleanup = (fn: () => void) => {\n stop?.();\n signal.removeEventListener('abort', onAbort);\n fn();\n };\n\n const onAbort = () => cleanup(() => reject(signal.reason));\n\n stop = effect(() => {\n const value = snapshot.get().value;\n if (value === 'idle') cleanup(resolve);\n else if (value === 'destroyed') cleanup(() => reject(new DOMException('Aborted', 'AbortError')));\n });\n\n signal.addEventListener('abort', onAbort, { once: true });\n });\n}\n\n// ============================================================================\n// LOAD TASK FACTORY\n// ============================================================================\n\ninterface LoadTaskOptions {\n getContext: () => SegmentLoaderActorContext;\n setContext: (ctx: SegmentLoaderActorContext) => void;\n fetchBytes: FetchBytes;\n sourceBufferActor: SourceBufferActor;\n}\n\n/**\n * Wraps a LoadTask descriptor into a Task that fetches (if needed) and\n * forwards to SourceBufferActor. Updates in-flight context around async\n * operations so the loading handler can make accurate continue/preempt\n * decisions at any point.\n */\nfunction makeLoadTask(\n op: LoadTask,\n { getContext, setContext, fetchBytes, sourceBufferActor }: LoadTaskOptions\n): Task<void> {\n return new Task(async (taskSignal) => {\n if (taskSignal.aborted) return;\n\n if (op.type === 'remove') {\n sourceBufferActor.send(op);\n await waitForIdle(sourceBufferActor.snapshot, taskSignal);\n return;\n }\n\n if (op.type === 'append-init') {\n setContext({ ...getContext(), inFlightInitTrackId: op.meta.trackId });\n try {\n // Init segments are small and need the full body before appending.\n // minChunkSize: Infinity accumulates all chunks into one before yielding.\n const data = await fetchBytes(op, { signal: taskSignal, minChunkSize: Infinity });\n if (!taskSignal.aborted) {\n sourceBufferActor.send({ type: 'append-init', data, meta: op.meta });\n await waitForIdle(sourceBufferActor.snapshot, taskSignal);\n }\n } finally {\n setContext({ ...getContext(), inFlightInitTrackId: null });\n }\n return;\n }\n\n // append-segment: await headers eagerly (starts the HTTP connection and\n // records the fetch in observers like tests), then pass the body stream\n // directly to the actor so chunks are appended as they arrive.\n setContext({ ...getContext(), inFlightSegmentId: op.meta.id });\n try {\n const stream = await fetchBytes(op, { signal: taskSignal });\n if (!taskSignal.aborted) {\n sourceBufferActor.send({ type: 'append-segment', data: stream, meta: op.meta });\n await waitForIdle(sourceBufferActor.snapshot, taskSignal);\n }\n } finally {\n setContext({ ...getContext(), inFlightSegmentId: null });\n }\n });\n}\n\n// ============================================================================\n// IMPLEMENTATION\n// ============================================================================\n\n/**\n * Creates a SegmentLoaderActor for one track type (video or audio).\n *\n * Receives load assignments via `send()` and owns all execution: planning,\n * removes, fetches, and appends. Coordinates with the SourceBufferActor for\n * all physical SourceBuffer operations.\n *\n * Planning (Cases 1–3) happens in the `load` handler on every incoming\n * message, producing an ordered LoadTask list. The runner drains that list\n * sequentially via SerialRunner. When a new message arrives mid-run, the\n * handler replans and either continues the in-flight operation (abortPending\n * + schedule new remainder) or preempts it (abortAll + cancel SourceBuffer\n * if needed + schedule new plan).\n *\n * @param sourceBufferActor - Shared SourceBufferActor reference (not owned)\n * @param fetchBytes - Tracked fetch closure (owns throughput sampling for segments).\n * Accepts an optional `minChunkSize` in options; init segments pass `Infinity`\n * so the entire body accumulates as one chunk before appending.\n */\nexport function createSegmentLoaderActor(\n sourceBufferActor: SourceBufferActor,\n fetchBytes: FetchBytes\n): SegmentLoaderActor {\n type UserState = Exclude<SegmentLoaderActorState, 'destroyed'>;\n type Ctx = HandlerContext<UserState, SegmentLoaderActorContext, () => SerialRunner>;\n\n const getBufferedSegments = (allSegments: readonly Segment[]): Segment[] => {\n // Exclude partial segments — they are still being streamed and must not be\n // treated as fully buffered for load planning or buffer window calculations.\n const bufferedIds = new Set(\n sourceBufferActor.snapshot\n .get()\n .context.segments.filter((s) => !s.partial)\n .map((s) => s.id)\n );\n return allSegments.filter((s) => bufferedIds.has(s.id));\n };\n\n /**\n * Translate a load message into an ordered LoadTask list based on committed\n * actor state. In-flight awareness is handled separately in the load handler.\n *\n * @todo Rename alongside LoadTask (e.g. planOps).\n *\n * Case 1 — Removes: forward and back buffer flush points, segment-aligned.\n * No flush on track switch: appending new content overwrites existing buffer\n * ranges, and the actor's time-aligned deduplication keeps the segment model\n * accurate as new segments arrive.\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 const actorCtx = sourceBufferActor.snapshot.get().context;\n const bufferedSegments = getBufferedSegments(track.segments);\n const currentTime = range?.start ?? 0;\n const tasks: LoadTask[] = [];\n\n // Case 1: Removes\n if (range) {\n const forwardFlushStart = calculateForwardFlushPoint(bufferedSegments, currentTime);\n if (forwardFlushStart < Infinity) {\n tasks.push({ type: 'remove', start: forwardFlushStart, end: Infinity });\n }\n const backFlushEnd = calculateBackBufferFlushPoint(bufferedSegments, currentTime);\n if (backFlushEnd > 0) {\n tasks.push({ type: 'remove', start: 0, end: backFlushEnd });\n }\n }\n\n // Case 2: Init\n if (actorCtx.initTrackId !== track.id) {\n tasks.push({\n type: 'append-init',\n meta: { trackId: track.id },\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 EPSILON = 0.0001;\n const segmentsToLoad = getSegmentsToLoad(track.segments, bufferedSegments, currentTime).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 const existing = actorCtx.segments.find((s) => Math.abs(s.startTime - seg.startTime) < EPSILON);\n // Partial segments are still streaming — treat as not buffered so they\n // are always re-planned (avoids relying on incomplete data).\n if (existing?.partial) return true;\n if (!existing?.trackBandwidth || !track.bandwidth) return true;\n return track.bandwidth > existing.trackBandwidth;\n });\n for (const segment of segmentsToLoad) {\n tasks.push({\n type: 'append-segment',\n meta: {\n id: segment.id,\n startTime: segment.startTime,\n duration: segment.duration,\n trackId: track.id,\n trackBandwidth: track.bandwidth,\n },\n url: segment.url,\n ...(segment.byteRange !== undefined && { byteRange: segment.byteRange }),\n });\n }\n }\n\n return tasks;\n };\n\n const scheduleAll = (tasks: LoadTask[], { getContext, setContext, runner }: Ctx): void => {\n tasks.forEach((op) => {\n runner\n .schedule(makeLoadTask(op, { getContext, setContext, fetchBytes, sourceBufferActor }))\n .then(undefined, (e: unknown) => {\n if (e instanceof Error && e.name === 'AbortError') return;\n // On unexpected fetch/append errors, abort remaining tasks so a failed\n // init doesn't cause segment fetches to proceed with no init segment.\n console.error('Unexpected error in segment loader:', e);\n runner.abortPending();\n });\n });\n };\n\n return createMachineActor<UserState, SegmentLoaderActorContext, SegmentLoaderMessage, () => SerialRunner>({\n runner: () => new SerialRunner(),\n initial: 'idle',\n context: { inFlightInitTrackId: null, inFlightSegmentId: null },\n states: {\n idle: {\n on: {\n load: (msg, ctx) => {\n const allTasks = planTasks(msg);\n if (allTasks.length === 0) return;\n ctx.transition('loading');\n scheduleAll(allTasks, ctx);\n },\n },\n },\n loading: {\n onSettled: 'idle',\n on: {\n load: (msg, ctx) => {\n const { context, runner } = ctx;\n const allTasks = planTasks(msg);\n\n // Determine whether the in-flight operation is still needed.\n const inFlightStillNeeded =\n (context.inFlightSegmentId !== null &&\n allTasks.some((t) => t.type === 'append-segment' && t.meta.id === context.inFlightSegmentId)) ||\n (context.inFlightInitTrackId !== null &&\n allTasks.some((t) => t.type === 'append-init' && t.meta.trackId === context.inFlightInitTrackId));\n\n if (inFlightStillNeeded) {\n // Continue: abort only the pending queue, let the in-flight task finish.\n // Schedule everything except the in-flight item — it covers that slot.\n runner.abortPending();\n scheduleAll(\n allTasks.filter(\n (t) =>\n !(t.type === 'append-segment' && t.meta.id === context.inFlightSegmentId) &&\n !(t.type === 'append-init' && t.meta.trackId === context.inFlightInitTrackId)\n ),\n ctx\n );\n } else {\n // Preempt: abort everything and replan.\n runner.abortAll();\n // Cancel SourceBufferActor tasks when a segment is in-flight (always\n // discard) or when a track switch is happening (new track's init follows).\n // For a same-track seek with an in-flight init, skip cancel — the task's\n // signal is not aborted (abortAll was called on the runner, but the init\n // task already completed or will complete via its own signal path).\n const cancelSourceBuffer =\n context.inFlightSegmentId !== null ||\n (context.inFlightInitTrackId !== null &&\n allTasks.some((t) => t.type === 'append-init' && t.meta.trackId !== context.inFlightInitTrackId));\n if (cancelSourceBuffer) {\n sourceBufferActor.send({ type: 'cancel' });\n }\n scheduleAll(allTasks, ctx);\n }\n },\n },\n },\n },\n });\n}\n"],"mappings":"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"}