@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,83 @@
1
+ import { effect } from "./signals/effect.js";
2
+ import { untrack } from "./signals/primitives.js";
3
+ import { createMachineCore } from "./machine.js";
4
+ //#region src/core/create-machine-reactor.ts
5
+ const toArray = (x) => x === void 0 ? [] : Array.isArray(x) ? x : [x];
6
+ /**
7
+ * Creates a reactive Reactor from a declarative definition.
8
+ *
9
+ * A Reactor is driven by subscriptions to external signals rather than
10
+ * imperative messages. Each state holds an array of effect functions —
11
+ * every element becomes one independent `effect()` call gated on that state,
12
+ * with its own dependency tracking and cleanup lifecycle.
13
+ *
14
+ * `'destroying'` and `'destroyed'` are always implicit terminal states.
15
+ * `destroy()` transitions through both in sequence: `'destroying'` first (for
16
+ * potential async teardown in a future extension), then immediately `'destroyed'`
17
+ * for the synchronous base case. Active effect cleanups fire via disposal.
18
+ *
19
+ * @example
20
+ * const reactor = createMachineReactor({
21
+ * initial: 'waiting',
22
+ * monitor: () => srcSignal.get() ? 'active' : 'waiting',
23
+ * states: {
24
+ * active: {
25
+ * // entry: runs once on state entry; fn body is automatically untracked.
26
+ * entry: () => listen(el, 'play', handler),
27
+ * // effects: re-runs whenever tracked signals change.
28
+ * effects: () => { currentTimeSignal.get(); return cleanup; },
29
+ * },
30
+ * waiting: {},
31
+ * }
32
+ * });
33
+ */
34
+ function createMachineReactor(def) {
35
+ const { snapshotSignal, getState, transition } = createMachineCore({ value: def.initial });
36
+ const effectDisposals = [];
37
+ const wrapResult = (result) => {
38
+ if (!result) return void 0;
39
+ if (typeof result === "function") return result;
40
+ return () => result.abort();
41
+ };
42
+ const untracked = (baseCall) => () => untrack(baseCall);
43
+ const isTerminal = (snapshot) => snapshot.value === "destroying" || snapshot.value === "destroyed";
44
+ const descriptors = [...toArray(def.monitor).map((fn) => ({
45
+ fn: () => {
46
+ const target = fn();
47
+ if (target !== getState()) transition(target);
48
+ },
49
+ shouldSkip: isTerminal
50
+ })), ...Object.entries(def.states).flatMap(([state, stateDef]) => {
51
+ const isNotState = (snapshot) => snapshot.value !== state;
52
+ return [...toArray(stateDef.entry).map((fn) => ({
53
+ fn,
54
+ shouldSkip: isNotState,
55
+ toFnCall: untracked
56
+ })), ...toArray(stateDef.effects).map((fn) => ({
57
+ fn,
58
+ shouldSkip: isNotState
59
+ }))];
60
+ })];
61
+ const toEffect = ({ fn, shouldSkip, toFnCall = (baseCall) => baseCall }) => effect(() => {
62
+ if (shouldSkip(snapshotSignal.get())) return;
63
+ const baseCall = () => fn();
64
+ return wrapResult(toFnCall(baseCall)());
65
+ });
66
+ effectDisposals.push(...descriptors.map(toEffect));
67
+ return {
68
+ get snapshot() {
69
+ return snapshotSignal;
70
+ },
71
+ destroy() {
72
+ const state = getState();
73
+ if (state === "destroying" || state === "destroyed") return;
74
+ transition("destroying");
75
+ transition("destroyed");
76
+ for (const dispose of effectDisposals) dispose();
77
+ }
78
+ };
79
+ }
80
+ //#endregion
81
+ export { createMachineReactor };
82
+
83
+ //# sourceMappingURL=create-machine-reactor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-machine-reactor.js","names":[],"sources":["../../../src/core/create-machine-reactor.ts"],"sourcesContent":["import type { Machine, MachineSnapshot } from './machine';\nimport { createMachineCore } from './machine';\nimport { effect } from './signals/effect';\nimport { untrack } from './signals/primitives';\n\n// =============================================================================\n// Definition types\n// =============================================================================\n\n/**\n * A reactive state-deriving function used in the `monitor` field.\n *\n * Returns the target state the reactor should be in. Any signals read inside\n * the fn body create reactive dependencies — the framework re-evaluates it when\n * those signals change and automatically calls `transition()` when the returned\n * state differs from the current one.\n */\nexport type ReactorDeriveFn<State extends string> = () => State;\n\n/**\n * An effect function used in reactor `entry` and `effects` blocks.\n *\n * May return a cleanup function that runs before each re-evaluation and on\n * state exit (including destroy).\n */\nexport type ReactorEffectFn = () => (() => void) | { abort(): void } | void;\n\n/**\n * Per-state effect grouping for a single reactor state.\n *\n * - `entry` effects run once on state entry. The fn body is automatically\n * untracked — no `untrack()` calls are needed inside. Use this for\n * one-time setup: reading current values, attaching event listeners, etc.\n * - `effects` run on state entry and re-run whenever a signal read inside\n * the fn body changes. Use `untrack()` for reads you do not want to track.\n * Use this for work that must stay in sync with reactive state.\n *\n * Both are optional; pass `{}` for states with no effects.\n */\nexport type ReactorStateDefinition = {\n entry?: ReactorEffectFn | ReactorEffectFn[];\n effects?: ReactorEffectFn | ReactorEffectFn[];\n};\n\n/**\n * Full reactor definition passed to `createMachineReactor`.\n *\n * `State` is the set of domain-meaningful states. `'destroying'` and\n * `'destroyed'` are always added by the framework as implicit terminal states —\n * do not include them here.\n */\nexport type ReactorDefinition<State extends string> = {\n /** Initial state. */\n initial: State;\n /**\n * Reactive state derivation. Registered before per-state effects — the\n * ordering guarantee ensures transitions fired here take effect before\n * per-state effects re-evaluate in the same flush.\n */\n monitor?: ReactorDeriveFn<State> | ReactorDeriveFn<State>[];\n /**\n * Per-state effect groupings. Every valid state must be declared — pass `{}`\n * for states with no effects. `entry` and `effects` each become independent\n * `effect()` calls gated on that state, with their own cleanup lifecycles.\n */\n states: Record<State, ReactorStateDefinition>;\n};\n\n// =============================================================================\n// Live reactor interface\n// =============================================================================\n\n/** Live reactor instance returned by `createMachineReactor`. */\nexport type Reactor<State extends string> = Machine<MachineSnapshot<State>>;\n\n// =============================================================================\n// Implementation helpers\n// =============================================================================\n\nconst toArray = <T>(x: T | T[] | undefined): T[] => (x === undefined ? [] : Array.isArray(x) ? x : [x]);\n\n// =============================================================================\n// Implementation\n// =============================================================================\n\n/**\n * Creates a reactive Reactor from a declarative definition.\n *\n * A Reactor is driven by subscriptions to external signals rather than\n * imperative messages. Each state holds an array of effect functions —\n * every element becomes one independent `effect()` call gated on that state,\n * with its own dependency tracking and cleanup lifecycle.\n *\n * `'destroying'` and `'destroyed'` are always implicit terminal states.\n * `destroy()` transitions through both in sequence: `'destroying'` first (for\n * potential async teardown in a future extension), then immediately `'destroyed'`\n * for the synchronous base case. Active effect cleanups fire via disposal.\n *\n * @example\n * const reactor = createMachineReactor({\n * initial: 'waiting',\n * monitor: () => srcSignal.get() ? 'active' : 'waiting',\n * states: {\n * active: {\n * // entry: runs once on state entry; fn body is automatically untracked.\n * entry: () => listen(el, 'play', handler),\n * // effects: re-runs whenever tracked signals change.\n * effects: () => { currentTimeSignal.get(); return cleanup; },\n * },\n * waiting: {},\n * }\n * });\n */\nexport function createMachineReactor<State extends string>(\n def: ReactorDefinition<State>\n): Reactor<State | 'destroying' | 'destroyed'> {\n type FullState = State | 'destroying' | 'destroyed';\n\n const { snapshotSignal, getState, transition } = createMachineCore<FullState, MachineSnapshot<FullState>>({\n value: def.initial as FullState,\n });\n\n const effectDisposals: Array<() => void> = [];\n\n const wrapResult = (result: ReturnType<ReactorEffectFn>) => {\n if (!result) return undefined;\n if (typeof result === 'function') return result;\n return () => result.abort();\n };\n\n type EffectCall = () => ReturnType<ReactorEffectFn>;\n\n type EffectDescriptor = {\n fn: ReactorEffectFn;\n shouldSkip: (snapshot: { value: FullState }) => boolean;\n toFnCall?: (baseCall: EffectCall) => EffectCall;\n };\n\n const untracked: EffectDescriptor['toFnCall'] = (baseCall) => () => untrack(baseCall);\n\n const isTerminal = (snapshot: { value: FullState }) =>\n snapshot.value === 'destroying' || snapshot.value === 'destroyed';\n\n // `monitor` descriptors are built first — the ordering guarantee ensures\n // transitions they trigger take effect before per-state effects re-evaluate\n // in the same flush. See the comment on effect registration order in the\n // previous implementation for full details.\n const descriptors: EffectDescriptor[] = [\n ...toArray(def.monitor).map((fn) => ({\n fn: () => {\n const target = fn();\n if (target !== (getState() as State)) transition(target as FullState);\n },\n shouldSkip: isTerminal,\n })),\n ...(Object.entries(def.states) as Array<[State, ReactorStateDefinition]>).flatMap(([state, stateDef]) => {\n const isNotState = (snapshot: { value: FullState }) => snapshot.value !== state;\n return [\n ...toArray(stateDef.entry).map((fn) => ({ fn, shouldSkip: isNotState, toFnCall: untracked })),\n ...toArray(stateDef.effects).map((fn) => ({ fn, shouldSkip: isNotState })),\n ];\n }),\n ];\n\n const toEffect = ({ fn, shouldSkip, toFnCall = (baseCall) => baseCall }: EffectDescriptor) =>\n effect(() => {\n const snapshot = snapshotSignal.get();\n if (shouldSkip(snapshot)) return;\n const baseCall = () => fn();\n return wrapResult(toFnCall(baseCall)());\n });\n\n effectDisposals.push(...descriptors.map(toEffect));\n\n return {\n get snapshot() {\n return snapshotSignal;\n },\n\n destroy(): void {\n const state = getState();\n if (state === 'destroying' || state === 'destroyed') return;\n // Two-step teardown: transition through 'destroying' first to leave room\n // for async teardown in a future extension, then immediately 'destroyed'\n // for the synchronous base case. Active effect cleanups fire via disposal.\n transition('destroying');\n transition('destroyed');\n for (const dispose of effectDisposals) dispose();\n },\n };\n}\n"],"mappings":";;;;AA+EA,MAAM,WAAc,MAAiC,MAAM,KAAA,IAAY,EAAE,GAAG,MAAM,QAAQ,EAAE,GAAG,IAAI,CAAC,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCtG,SAAgB,qBACd,KAC6C;CAG7C,MAAM,EAAE,gBAAgB,UAAU,eAAe,kBAAyD,EACxG,OAAO,IAAI,SACZ,CAAC;CAEF,MAAM,kBAAqC,EAAE;CAE7C,MAAM,cAAc,WAAwC;AAC1D,MAAI,CAAC,OAAQ,QAAO,KAAA;AACpB,MAAI,OAAO,WAAW,WAAY,QAAO;AACzC,eAAa,OAAO,OAAO;;CAW7B,MAAM,aAA2C,mBAAmB,QAAQ,SAAS;CAErF,MAAM,cAAc,aAClB,SAAS,UAAU,gBAAgB,SAAS,UAAU;CAMxD,MAAM,cAAkC,CACtC,GAAG,QAAQ,IAAI,QAAQ,CAAC,KAAK,QAAQ;EACnC,UAAU;GACR,MAAM,SAAS,IAAI;AACnB,OAAI,WAAY,UAAU,CAAY,YAAW,OAAoB;;EAEvE,YAAY;EACb,EAAE,EACH,GAAI,OAAO,QAAQ,IAAI,OAAO,CAA4C,SAAS,CAAC,OAAO,cAAc;EACvG,MAAM,cAAc,aAAmC,SAAS,UAAU;AAC1E,SAAO,CACL,GAAG,QAAQ,SAAS,MAAM,CAAC,KAAK,QAAQ;GAAE;GAAI,YAAY;GAAY,UAAU;GAAW,EAAE,EAC7F,GAAG,QAAQ,SAAS,QAAQ,CAAC,KAAK,QAAQ;GAAE;GAAI,YAAY;GAAY,EAAE,CAC3E;GACD,CACH;CAED,MAAM,YAAY,EAAE,IAAI,YAAY,YAAY,aAAa,eAC3D,aAAa;AAEX,MAAI,WADa,eAAe,KAAK,CACb,CAAE;EAC1B,MAAM,iBAAiB,IAAI;AAC3B,SAAO,WAAW,SAAS,SAAS,EAAE,CAAC;GACvC;AAEJ,iBAAgB,KAAK,GAAG,YAAY,IAAI,SAAS,CAAC;AAElD,QAAO;EACL,IAAI,WAAW;AACb,UAAO;;EAGT,UAAgB;GACd,MAAM,QAAQ,UAAU;AACxB,OAAI,UAAU,gBAAgB,UAAU,YAAa;AAIrD,cAAW,aAAa;AACxB,cAAW,YAAY;AACvB,QAAK,MAAM,WAAW,gBAAiB,UAAS;;EAEnD"}
@@ -0,0 +1,46 @@
1
+ import { untrack, update } from "./signals/primitives.js";
2
+ import { createMachineCore } from "./machine.js";
3
+ //#region src/core/create-transition-actor.ts
4
+ /**
5
+ * Creates a reducer-shaped actor from an initial context and a reducer function.
6
+ *
7
+ * The reducer receives the current context and a message and returns the next
8
+ * context. Returning the same reference (by identity) skips the signal update —
9
+ * so early-returning `context` unchanged is both the no-op and the optimization.
10
+ *
11
+ * Side effects (e.g. DOM mutations) may be performed inside the reducer.
12
+ * They run synchronously before the signal is updated.
13
+ *
14
+ * @example
15
+ * const actor = createTransitionActor(
16
+ * { count: 0 },
17
+ * (context, message: { type: 'increment' }) => ({ count: context.count + 1 })
18
+ * );
19
+ */
20
+ function createTransitionActor(initialContext, reducer) {
21
+ const { snapshotSignal, getState, transition } = createMachineCore({
22
+ value: "active",
23
+ context: initialContext
24
+ });
25
+ const getContext = () => untrack(() => snapshotSignal.get().context);
26
+ const setContext = (context) => update(snapshotSignal, { context });
27
+ return {
28
+ get snapshot() {
29
+ return snapshotSignal;
30
+ },
31
+ send(message) {
32
+ if (getState() === "destroyed") return;
33
+ const context = getContext();
34
+ const newContext = reducer(context, message);
35
+ if (newContext !== context) setContext(newContext);
36
+ },
37
+ destroy() {
38
+ if (getState() === "destroyed") return;
39
+ transition("destroyed");
40
+ }
41
+ };
42
+ }
43
+ //#endregion
44
+ export { createTransitionActor };
45
+
46
+ //# sourceMappingURL=create-transition-actor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-transition-actor.js","names":[],"sources":["../../../src/core/create-transition-actor.ts"],"sourcesContent":["import type { ActorSnapshot } from './actor';\nimport type { Machine } from './machine';\nimport { createMachineCore } from './machine';\nimport { untrack, update } from './signals/primitives';\n\n// =============================================================================\n// Definition types\n// =============================================================================\n\n/**\n * A reducer-shaped actor: `(context, message) => context`.\n *\n * No finite states — the snapshot carries `value: 'active' | 'destroyed'`\n * as a universal lifecycle marker rather than domain state. The interesting\n * state is entirely in the context, which is reactive via `snapshot`.\n *\n * Use this when the actor has context that needs to be reactive but no\n * meaningful state machine (e.g., a message-driven model with DOM side\n * effects). For actors that need per-state behavior, use `createMachineActor`.\n */\nexport interface TransitionActor<Context extends object, Message extends { type: string }>\n extends Machine<ActorSnapshot<'active' | 'destroyed', Context>> {\n send(message: Message): void;\n}\n\n// =============================================================================\n// Implementation\n// =============================================================================\n\n/**\n * Creates a reducer-shaped actor from an initial context and a reducer function.\n *\n * The reducer receives the current context and a message and returns the next\n * context. Returning the same reference (by identity) skips the signal update —\n * so early-returning `context` unchanged is both the no-op and the optimization.\n *\n * Side effects (e.g. DOM mutations) may be performed inside the reducer.\n * They run synchronously before the signal is updated.\n *\n * @example\n * const actor = createTransitionActor(\n * { count: 0 },\n * (context, message: { type: 'increment' }) => ({ count: context.count + 1 })\n * );\n */\nexport function createTransitionActor<Context extends object, Message extends { type: string }>(\n initialContext: Context,\n reducer: (context: Context, message: Message) => Context\n): TransitionActor<Context, Message> {\n const { snapshotSignal, getState, transition } = createMachineCore<\n 'active' | 'destroyed',\n ActorSnapshot<'active' | 'destroyed', Context>\n >({ value: 'active', context: initialContext });\n\n const getContext = (): Context => untrack(() => snapshotSignal.get().context);\n const setContext = (context: Context): void => update(snapshotSignal, { context });\n\n return {\n get snapshot() {\n return snapshotSignal;\n },\n\n send(message: Message): void {\n if (getState() === 'destroyed') return;\n const context = getContext();\n const newContext = reducer(context, message);\n if (newContext !== context) setContext(newContext);\n },\n\n destroy(): void {\n if (getState() === 'destroyed') return;\n transition('destroyed');\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AA6CA,SAAgB,sBACd,gBACA,SACmC;CACnC,MAAM,EAAE,gBAAgB,UAAU,eAAe,kBAG/C;EAAE,OAAO;EAAU,SAAS;EAAgB,CAAC;CAE/C,MAAM,mBAA4B,cAAc,eAAe,KAAK,CAAC,QAAQ;CAC7E,MAAM,cAAc,YAA2B,OAAO,gBAAgB,EAAE,SAAS,CAAC;AAElF,QAAO;EACL,IAAI,WAAW;AACb,UAAO;;EAGT,KAAK,SAAwB;AAC3B,OAAI,UAAU,KAAK,YAAa;GAChC,MAAM,UAAU,YAAY;GAC5B,MAAM,aAAa,QAAQ,SAAS,QAAQ;AAC5C,OAAI,eAAe,QAAS,YAAW,WAAW;;EAGpD,UAAgB;AACd,OAAI,UAAU,KAAK,YAAa;AAChC,cAAW,YAAY;;EAE1B"}
@@ -0,0 +1,53 @@
1
+ import { effect } from "../signals/effect.js";
2
+ import { isResolvedTrack } from "../types/index.js";
3
+ import { getSelectedTrack } from "../utils/track-selection.js";
4
+ //#region src/core/features/calculate-presentation-duration.ts
5
+ /**
6
+ * Check if we can calculate presentation duration (have required data).
7
+ */
8
+ function canCalculateDuration(state) {
9
+ if (!state.presentation) return false;
10
+ return !!(state.selectedVideoTrackId || state.selectedAudioTrackId);
11
+ }
12
+ /**
13
+ * Check if we should calculate presentation duration (conditions met).
14
+ */
15
+ function shouldCalculateDuration(state) {
16
+ if (!canCalculateDuration(state)) return false;
17
+ const { presentation } = state;
18
+ if (presentation.duration !== void 0) return false;
19
+ const videoTrack = state.selectedVideoTrackId ? getSelectedTrack(state, "video") : void 0;
20
+ const audioTrack = state.selectedAudioTrackId ? getSelectedTrack(state, "audio") : void 0;
21
+ return !!(videoTrack && isResolvedTrack(videoTrack) || audioTrack && isResolvedTrack(audioTrack));
22
+ }
23
+ /**
24
+ * Get duration from the first resolved track (prefer video, fallback to audio).
25
+ */
26
+ function getDurationFromResolvedTracks(state) {
27
+ const videoTrack = state.selectedVideoTrackId ? getSelectedTrack(state, "video") : void 0;
28
+ if (videoTrack && isResolvedTrack(videoTrack)) return videoTrack.duration;
29
+ const audioTrack = state.selectedAudioTrackId ? getSelectedTrack(state, "audio") : void 0;
30
+ if (audioTrack && isResolvedTrack(audioTrack)) return audioTrack.duration;
31
+ }
32
+ /**
33
+ * Calculate and set presentation duration from resolved tracks.
34
+ */
35
+ function calculatePresentationDuration({ state }) {
36
+ return effect(() => {
37
+ const currentState = state.get();
38
+ if (!shouldCalculateDuration(currentState)) return;
39
+ const duration = getDurationFromResolvedTracks(currentState);
40
+ if (duration === void 0 || !Number.isFinite(duration)) return;
41
+ state.set({
42
+ ...currentState,
43
+ presentation: {
44
+ ...currentState.presentation,
45
+ duration
46
+ }
47
+ });
48
+ });
49
+ }
50
+ //#endregion
51
+ export { calculatePresentationDuration };
52
+
53
+ //# sourceMappingURL=calculate-presentation-duration.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"calculate-presentation-duration.js","names":[],"sources":["../../../../src/core/features/calculate-presentation-duration.ts"],"sourcesContent":["import { effect } from '../signals/effect';\nimport type { Signal } from '../signals/primitives';\nimport type { AudioTrack, Presentation, VideoTrack } from '../types';\nimport { isResolvedTrack } from '../types';\nimport { getSelectedTrack } from '../utils/track-selection';\n\nexport interface PresentationDurationState {\n presentation?: Presentation;\n selectedVideoTrackId?: string;\n selectedAudioTrackId?: string;\n}\n\n/**\n * Check if we can calculate presentation duration (have required data).\n */\nexport function canCalculateDuration(state: PresentationDurationState): boolean {\n if (!state.presentation) return false;\n // Need at least one selected track\n return !!(state.selectedVideoTrackId || state.selectedAudioTrackId);\n}\n\n/**\n * Check if we should calculate presentation duration (conditions met).\n */\nexport function shouldCalculateDuration(state: PresentationDurationState): boolean {\n if (!canCalculateDuration(state)) return false;\n\n const { presentation } = state;\n\n // Don't recalculate if already set\n if (presentation!.duration !== undefined) return false;\n\n // Check if any selected track is resolved\n const videoTrack = state.selectedVideoTrackId ? getSelectedTrack(state, 'video') : undefined;\n const audioTrack = state.selectedAudioTrackId ? getSelectedTrack(state, 'audio') : undefined;\n\n // At least one track must be resolved (has segments and duration)\n return !!((videoTrack && isResolvedTrack(videoTrack)) || (audioTrack && isResolvedTrack(audioTrack)));\n}\n\n/**\n * Get duration from the first resolved track (prefer video, fallback to audio).\n */\nexport function getDurationFromResolvedTracks(state: PresentationDurationState): number | undefined {\n // Try video track first\n const videoTrack = state.selectedVideoTrackId\n ? (getSelectedTrack(state, 'video') as VideoTrack | undefined)\n : undefined;\n if (videoTrack && isResolvedTrack(videoTrack)) {\n return videoTrack.duration;\n }\n\n // Fallback to audio track\n const audioTrack = state.selectedAudioTrackId\n ? (getSelectedTrack(state, 'audio') as AudioTrack | undefined)\n : undefined;\n if (audioTrack && isResolvedTrack(audioTrack)) {\n return audioTrack.duration;\n }\n\n return undefined;\n}\n\n/**\n * Calculate and set presentation duration from resolved tracks.\n */\nexport function calculatePresentationDuration<S extends PresentationDurationState>({\n state,\n}: {\n state: Signal<S>;\n}): () => void {\n return effect(() => {\n const currentState = state.get();\n if (!shouldCalculateDuration(currentState)) return;\n\n const duration = getDurationFromResolvedTracks(currentState);\n if (duration === undefined || !Number.isFinite(duration)) return;\n\n state.set({\n ...currentState,\n presentation: {\n ...currentState.presentation!,\n duration,\n },\n } as S);\n });\n}\n"],"mappings":";;;;;;;AAeA,SAAgB,qBAAqB,OAA2C;AAC9E,KAAI,CAAC,MAAM,aAAc,QAAO;AAEhC,QAAO,CAAC,EAAE,MAAM,wBAAwB,MAAM;;;;;AAMhD,SAAgB,wBAAwB,OAA2C;AACjF,KAAI,CAAC,qBAAqB,MAAM,CAAE,QAAO;CAEzC,MAAM,EAAE,iBAAiB;AAGzB,KAAI,aAAc,aAAa,KAAA,EAAW,QAAO;CAGjD,MAAM,aAAa,MAAM,uBAAuB,iBAAiB,OAAO,QAAQ,GAAG,KAAA;CACnF,MAAM,aAAa,MAAM,uBAAuB,iBAAiB,OAAO,QAAQ,GAAG,KAAA;AAGnF,QAAO,CAAC,EAAG,cAAc,gBAAgB,WAAW,IAAM,cAAc,gBAAgB,WAAW;;;;;AAMrG,SAAgB,8BAA8B,OAAsD;CAElG,MAAM,aAAa,MAAM,uBACpB,iBAAiB,OAAO,QAAQ,GACjC,KAAA;AACJ,KAAI,cAAc,gBAAgB,WAAW,CAC3C,QAAO,WAAW;CAIpB,MAAM,aAAa,MAAM,uBACpB,iBAAiB,OAAO,QAAQ,GACjC,KAAA;AACJ,KAAI,cAAc,gBAAgB,WAAW,CAC3C,QAAO,WAAW;;;;;AAStB,SAAgB,8BAAmE,EACjF,SAGa;AACb,QAAO,aAAa;EAClB,MAAM,eAAe,MAAM,KAAK;AAChC,MAAI,CAAC,wBAAwB,aAAa,CAAE;EAE5C,MAAM,WAAW,8BAA8B,aAAa;AAC5D,MAAI,aAAa,KAAA,KAAa,CAAC,OAAO,SAAS,SAAS,CAAE;AAE1D,QAAM,IAAI;GACR,GAAG;GACH,cAAc;IACZ,GAAG,aAAa;IAChB;IACD;GACF,CAAM;GACP"}
@@ -0,0 +1,69 @@
1
+ import { getBandwidthEstimate } from "../abr/bandwidth-estimator.js";
2
+ import { effect } from "../signals/effect.js";
3
+ import { update } from "../signals/primitives.js";
4
+ import { selectQuality } from "../abr/quality-selection.js";
5
+ //#region src/core/features/quality-switching.ts
6
+ /**
7
+ * Default quality switching configuration.
8
+ */
9
+ const DEFAULT_SWITCHING_CONFIG = {
10
+ safetyMargin: .85,
11
+ minUpgradeInterval: 8e3,
12
+ defaultBandwidth: 5e6
13
+ };
14
+ /**
15
+ * Get all video tracks from a presentation's first switching set.
16
+ * Returns [] when the presentation is still unresolved (no selectionSets yet).
17
+ */
18
+ function getVideoTracks(presentation) {
19
+ return (presentation.selectionSets?.find((s) => s.type === "video"))?.switchingSets[0]?.tracks ?? [];
20
+ }
21
+ /**
22
+ * Quality switching orchestration (F9).
23
+ *
24
+ * Reacts to bandwidth estimate changes and updates `selectedVideoTrackId`
25
+ * when a different quality is optimal:
26
+ *
27
+ * - **Downgrades** happen immediately to avoid buffering stalls.
28
+ * - **Upgrades** are gated by `minUpgradeInterval` to prevent oscillation.
29
+ * - The first switch (from any track, or no track) is always immediate.
30
+ *
31
+ * Smooth switching is handled downstream: when `selectedVideoTrackId` changes,
32
+ * `resolveTrack` fetches the new playlist and `loadSegments` reloads the init
33
+ * segment, then appends media segments from the current position in the new
34
+ * quality. The browser's SourceBuffer replaces the overlapping buffered range.
35
+ *
36
+ * @example
37
+ * const cleanup = switchQuality({ state });
38
+ * // Later, when done:
39
+ * cleanup();
40
+ */
41
+ function switchQuality({ state }, config = {}) {
42
+ const safetyMargin = config.safetyMargin ?? DEFAULT_SWITCHING_CONFIG.safetyMargin;
43
+ const minUpgradeInterval = config.minUpgradeInterval ?? DEFAULT_SWITCHING_CONFIG.minUpgradeInterval;
44
+ const defaultBandwidth = config.defaultBandwidth ?? DEFAULT_SWITCHING_CONFIG.defaultBandwidth;
45
+ let lastUpgradeTime = Date.now();
46
+ let firstMeaningfulFire = true;
47
+ return effect(() => {
48
+ const { presentation, bandwidthState, selectedVideoTrackId, abrDisabled } = state.get();
49
+ if (abrDisabled === true) return;
50
+ if (!presentation || !bandwidthState) return;
51
+ const videoTracks = getVideoTracks(presentation);
52
+ if (videoTracks.length === 0) return;
53
+ const isFirst = firstMeaningfulFire;
54
+ firstMeaningfulFire = false;
55
+ const optimal = selectQuality(videoTracks, getBandwidthEstimate(bandwidthState, defaultBandwidth), { safetyMargin });
56
+ if (!optimal || optimal.id === selectedVideoTrackId) return;
57
+ const currentTrack = videoTracks.find((t) => t.id === selectedVideoTrackId);
58
+ if (!currentTrack || optimal.bandwidth > currentTrack.bandwidth) {
59
+ const now = Date.now();
60
+ if (!isFirst && now - lastUpgradeTime < minUpgradeInterval) return;
61
+ lastUpgradeTime = now;
62
+ }
63
+ update(state, { selectedVideoTrackId: optimal.id });
64
+ });
65
+ }
66
+ //#endregion
67
+ export { switchQuality };
68
+
69
+ //# sourceMappingURL=quality-switching.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quality-switching.js","names":[],"sources":["../../../../src/core/features/quality-switching.ts"],"sourcesContent":["import type { BandwidthState } from '../abr/bandwidth-estimator';\nimport { getBandwidthEstimate } from '../abr/bandwidth-estimator';\nimport { selectQuality } from '../abr/quality-selection';\nimport { effect } from '../signals/effect';\nimport { type Signal, update } from '../signals/primitives';\nimport type { Presentation, VideoSelectionSet } from '../types';\n\n/**\n * State shape for quality switching.\n */\nexport interface QualitySwitchingState {\n presentation?: Presentation;\n bandwidthState?: BandwidthState;\n selectedVideoTrackId?: string;\n // TODO: abrDisabled is a blunt instrument — it requires callers to know they're\n // competing with ABR and explicitly opt out. A better long-term design would\n // separate ABR selection (abrVideoTrackId) from manual selection (manualVideoTrackId)\n // and derive the effective selectedVideoTrackId as manualVideoTrackId ?? abrVideoTrackId.\n // That way the two concerns never write to the same field and consumers are unchanged.\n /** When true, ABR quality switching is suppressed. Use for manual quality selection. */\n abrDisabled?: boolean;\n}\n\n/**\n * Configuration for quality switching behavior.\n */\nexport interface QualitySwitchingConfig {\n /**\n * Safety margin for quality selection (0–1).\n * Track is selected only when bandwidth >= track.bandwidth / safetyMargin.\n * Default: 0.85 (15% headroom).\n */\n safetyMargin?: number;\n\n /**\n * Minimum milliseconds between upgrades.\n * Prevents oscillation when bandwidth fluctuates around a quality threshold.\n * Downgrades are always immediate regardless of this setting.\n * Default: 8000 (8 seconds).\n */\n minUpgradeInterval?: number;\n\n /**\n * Bandwidth estimate in bps to use before enough samples have been collected.\n * Default: 5_000_000 (5 Mbps).\n */\n defaultBandwidth?: number;\n}\n\n/**\n * Default quality switching configuration.\n */\nexport const DEFAULT_SWITCHING_CONFIG: Required<QualitySwitchingConfig> = {\n safetyMargin: 0.85,\n minUpgradeInterval: 8000,\n defaultBandwidth: 5_000_000,\n};\n\n/**\n * Get all video tracks from a presentation's first switching set.\n * Returns [] when the presentation is still unresolved (no selectionSets yet).\n */\nfunction getVideoTracks(presentation: Presentation) {\n const videoSet = presentation.selectionSets?.find((s) => s.type === 'video') as VideoSelectionSet | undefined;\n return videoSet?.switchingSets[0]?.tracks ?? [];\n}\n\n/**\n * Quality switching orchestration (F9).\n *\n * Reacts to bandwidth estimate changes and updates `selectedVideoTrackId`\n * when a different quality is optimal:\n *\n * - **Downgrades** happen immediately to avoid buffering stalls.\n * - **Upgrades** are gated by `minUpgradeInterval` to prevent oscillation.\n * - The first switch (from any track, or no track) is always immediate.\n *\n * Smooth switching is handled downstream: when `selectedVideoTrackId` changes,\n * `resolveTrack` fetches the new playlist and `loadSegments` reloads the init\n * segment, then appends media segments from the current position in the new\n * quality. The browser's SourceBuffer replaces the overlapping buffered range.\n *\n * @example\n * const cleanup = switchQuality({ state });\n * // Later, when done:\n * cleanup();\n */\nexport function switchQuality<S extends QualitySwitchingState>(\n { state }: { state: Signal<S> },\n config: QualitySwitchingConfig = {}\n): () => void {\n const safetyMargin = config.safetyMargin ?? DEFAULT_SWITCHING_CONFIG.safetyMargin;\n const minUpgradeInterval = config.minUpgradeInterval ?? DEFAULT_SWITCHING_CONFIG.minUpgradeInterval;\n const defaultBandwidth = config.defaultBandwidth ?? DEFAULT_SWITCHING_CONFIG.defaultBandwidth;\n\n // Initialize to creation time so the interval starts counting immediately.\n // The first time we have enough data to make a meaningful quality decision\n // (presentation resolved + bandwidth available), the upgrade gate is skipped\n // so the initial ABR correction does not wait for minUpgradeInterval.\n let lastUpgradeTime = Date.now();\n let firstMeaningfulFire = true;\n\n return effect(() => {\n const currentState = state.get();\n const { presentation, bandwidthState, selectedVideoTrackId, abrDisabled } = currentState;\n\n if (abrDisabled === true) return;\n if (!presentation || !bandwidthState) return;\n\n const videoTracks = getVideoTracks(presentation);\n if (videoTracks.length === 0) return;\n\n // Consume the first-meaningful-fire flag now that we have all required data.\n const isFirst = firstMeaningfulFire;\n firstMeaningfulFire = false;\n\n const bandwidth = getBandwidthEstimate(bandwidthState, defaultBandwidth);\n const optimal = selectQuality(videoTracks as any, bandwidth, { safetyMargin });\n if (!optimal || optimal.id === selectedVideoTrackId) return;\n\n // Determine whether this is an upgrade or downgrade.\n const currentTrack = videoTracks.find((t) => t.id === selectedVideoTrackId);\n const isUpgrade = !currentTrack || optimal.bandwidth > currentTrack.bandwidth;\n\n if (isUpgrade) {\n const now = Date.now();\n // Gate upgrades with minUpgradeInterval to prevent oscillation.\n // Downgrades are always immediate; the first meaningful evaluation is always allowed.\n if (!isFirst && now - lastUpgradeTime < minUpgradeInterval) return;\n lastUpgradeTime = now;\n }\n\n const patch: Partial<QualitySwitchingState> = { selectedVideoTrackId: optimal.id };\n update(state, patch);\n });\n}\n"],"mappings":";;;;;;;;AAoDA,MAAa,2BAA6D;CACxE,cAAc;CACd,oBAAoB;CACpB,kBAAkB;CACnB;;;;;AAMD,SAAS,eAAe,cAA4B;AAElD,SADiB,aAAa,eAAe,MAAM,MAAM,EAAE,SAAS,QAAQ,GAC3D,cAAc,IAAI,UAAU,EAAE;;;;;;;;;;;;;;;;;;;;;;AAuBjD,SAAgB,cACd,EAAE,SACF,SAAiC,EAAE,EACvB;CACZ,MAAM,eAAe,OAAO,gBAAgB,yBAAyB;CACrE,MAAM,qBAAqB,OAAO,sBAAsB,yBAAyB;CACjF,MAAM,mBAAmB,OAAO,oBAAoB,yBAAyB;CAM7E,IAAI,kBAAkB,KAAK,KAAK;CAChC,IAAI,sBAAsB;AAE1B,QAAO,aAAa;EAElB,MAAM,EAAE,cAAc,gBAAgB,sBAAsB,gBADvC,MAAM,KAAK;AAGhC,MAAI,gBAAgB,KAAM;AAC1B,MAAI,CAAC,gBAAgB,CAAC,eAAgB;EAEtC,MAAM,cAAc,eAAe,aAAa;AAChD,MAAI,YAAY,WAAW,EAAG;EAG9B,MAAM,UAAU;AAChB,wBAAsB;EAGtB,MAAM,UAAU,cAAc,aADZ,qBAAqB,gBAAgB,iBAAiB,EACX,EAAE,cAAc,CAAC;AAC9E,MAAI,CAAC,WAAW,QAAQ,OAAO,qBAAsB;EAGrD,MAAM,eAAe,YAAY,MAAM,MAAM,EAAE,OAAO,qBAAqB;AAG3E,MAFkB,CAAC,gBAAgB,QAAQ,YAAY,aAAa,WAErD;GACb,MAAM,MAAM,KAAK,KAAK;AAGtB,OAAI,CAAC,WAAW,MAAM,kBAAkB,mBAAoB;AAC5D,qBAAkB;;AAIpB,SAAO,OADuC,EAAE,sBAAsB,QAAQ,IAAI,CAC9D;GACpB"}
@@ -0,0 +1,73 @@
1
+ import { computed, update } from "../signals/primitives.js";
2
+ import { fetchResolvable, getResponseText } from "../../dom/network/fetch.js";
3
+ import { createMachineReactor } from "../create-machine-reactor.js";
4
+ import { parseMultivariantPlaylist } from "../hls/parse-multivariant.js";
5
+ //#region src/core/features/resolve-presentation.ts
6
+ /**
7
+ * Determines if resolution conditions are met based on preload policy and playback state.
8
+ *
9
+ * Resolution conditions:
10
+ * - State-driven: preload is 'auto' or 'metadata'
11
+ * - Playback-driven: playbackInitiated is true
12
+ *
13
+ * @param state - Current presentation state
14
+ * @returns true if resolution conditions are met
15
+ */
16
+ function shouldResolve(state) {
17
+ const { preload, playbackInitiated } = state;
18
+ return ["auto", "metadata"].includes(preload) || !!playbackInitiated;
19
+ }
20
+ /**
21
+ * Derives the correct state from current state conditions.
22
+ *
23
+ * States are mutually exclusive and exhaustive:
24
+ * - `'preconditions-unmet'`: no presentation, or presentation has no URL
25
+ * - `'idle'`: URL present, unresolved (no id), shouldResolve not met
26
+ * - `'resolving'`: URL present, unresolved (no id), shouldResolve met
27
+ * - `'resolved'`: URL present, resolved (has id)
28
+ */
29
+ function deriveState(state) {
30
+ const { presentation } = state;
31
+ if (!presentation || !("url" in presentation)) return "preconditions-unmet";
32
+ if ("id" in presentation) return "resolved";
33
+ return shouldResolve(state) ? "resolving" : "idle";
34
+ }
35
+ /**
36
+ * Resolves unresolved presentations using reactive composition.
37
+ *
38
+ * FSM driven by `deriveState` — a single `always` monitor keeps the state in
39
+ * sync with conditions at all times. `'resolving'` additionally runs the fetch
40
+ * task and returns an AbortController so the framework aborts it on state exit.
41
+ *
42
+ * @example
43
+ * const reactor = resolvePresentation({ state });
44
+ * // later:
45
+ * reactor.destroy();
46
+ */
47
+ function resolvePresentation({ state }) {
48
+ const derivedStateSignal = computed(() => deriveState(state.get()));
49
+ return createMachineReactor({
50
+ initial: "preconditions-unmet",
51
+ monitor: () => derivedStateSignal.get(),
52
+ states: {
53
+ "preconditions-unmet": {},
54
+ idle: {},
55
+ resolving: { entry: () => {
56
+ const presentation = state.get().presentation;
57
+ const ac = new AbortController();
58
+ fetchResolvable(presentation, { signal: ac.signal }).then((response) => getResponseText(response)).then((text) => {
59
+ update(state, { presentation: parseMultivariantPlaylist(text, presentation) });
60
+ }).catch((error) => {
61
+ if (error instanceof Error && error.name === "AbortError") return;
62
+ throw error;
63
+ });
64
+ return ac;
65
+ } },
66
+ resolved: {}
67
+ }
68
+ });
69
+ }
70
+ //#endregion
71
+ export { resolvePresentation };
72
+
73
+ //# sourceMappingURL=resolve-presentation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve-presentation.js","names":[],"sources":["../../../../src/core/features/resolve-presentation.ts"],"sourcesContent":["import { fetchResolvable, getResponseText } from '../../dom/network/fetch';\nimport type { Reactor } from '../create-machine-reactor';\nimport { createMachineReactor } from '../create-machine-reactor';\nimport { parseMultivariantPlaylist } from '../hls/parse-multivariant';\nimport { computed, type Signal, update } from '../signals/primitives';\nimport type { AddressableObject, Presentation } from '../types';\n\n/**\n * Unresolved presentation - has a URL but no data yet.\n * Identical to AddressableObject per user requirement.\n */\nexport type UnresolvedPresentation = AddressableObject;\n\n/**\n * State shape for presentation resolution.\n */\nexport interface PresentationState {\n presentation?: UnresolvedPresentation | Presentation | undefined;\n preload?: 'auto' | 'metadata' | 'none' | undefined;\n /** True once the user has initiated playback — enables resolution regardless of preload. */\n playbackInitiated?: boolean;\n}\n\n/**\n * Type guard to check if presentation is unresolved.\n */\nexport function isUnresolved(\n presentation: UnresolvedPresentation | Presentation | undefined\n): presentation is UnresolvedPresentation {\n return presentation !== undefined && 'url' in presentation && !('id' in presentation);\n}\n\nexport function canResolve(\n state: PresentationState\n): state is PresentationState & { presentation: UnresolvedPresentation } {\n return isUnresolved(state.presentation);\n}\n\n/**\n * Determines if resolution conditions are met based on preload policy and playback state.\n *\n * Resolution conditions:\n * - State-driven: preload is 'auto' or 'metadata'\n * - Playback-driven: playbackInitiated is true\n *\n * @param state - Current presentation state\n * @returns true if resolution conditions are met\n */\nexport function shouldResolve(state: PresentationState): boolean {\n const { preload, playbackInitiated } = state;\n return (\n // State-driven: preload allows (auto/metadata)\n ['auto', 'metadata'].includes(preload as any) ||\n // Playback-driven: user has initiated playback\n !!playbackInitiated\n );\n}\n\nexport type ResolvePresentationState = 'preconditions-unmet' | 'idle' | 'resolving' | 'resolved';\n\n/**\n * Derives the correct state from current state conditions.\n *\n * States are mutually exclusive and exhaustive:\n * - `'preconditions-unmet'`: no presentation, or presentation has no URL\n * - `'idle'`: URL present, unresolved (no id), shouldResolve not met\n * - `'resolving'`: URL present, unresolved (no id), shouldResolve met\n * - `'resolved'`: URL present, resolved (has id)\n */\nfunction deriveState(state: PresentationState): ResolvePresentationState {\n const { presentation } = state;\n if (!presentation || !('url' in presentation)) return 'preconditions-unmet';\n if ('id' in presentation) return 'resolved';\n return shouldResolve(state) ? 'resolving' : 'idle';\n}\n\n/**\n * Resolves unresolved presentations using reactive composition.\n *\n * FSM driven by `deriveState` — a single `always` monitor keeps the state in\n * sync with conditions at all times. `'resolving'` additionally runs the fetch\n * task and returns an AbortController so the framework aborts it on state exit.\n *\n * @example\n * const reactor = resolvePresentation({ state });\n * // later:\n * reactor.destroy();\n */\nexport function resolvePresentation<S extends PresentationState>({\n state,\n}: {\n state: Signal<S>;\n}): Reactor<ResolvePresentationState | 'destroying' | 'destroyed'> {\n const derivedStateSignal = computed(() => deriveState(state.get()));\n\n return createMachineReactor<ResolvePresentationState>({\n initial: 'preconditions-unmet',\n monitor: () => derivedStateSignal.get(),\n states: {\n 'preconditions-unmet': {},\n idle: {},\n resolving: {\n // Entry: start fetch on state entry; return AbortController so the\n // framework aborts the in-flight request on state exit.\n entry: () => {\n const presentation = state.get().presentation as UnresolvedPresentation;\n const ac = new AbortController();\n\n fetchResolvable(presentation, { signal: ac.signal })\n .then((response) => getResponseText(response))\n .then((text) => {\n const parsed = parseMultivariantPlaylist(text, presentation);\n update(state, { presentation: parsed } as Partial<S>);\n })\n .catch((error) => {\n if (error instanceof Error && error.name === 'AbortError') return;\n throw error;\n });\n\n return ac;\n },\n },\n resolved: {},\n },\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;AAgDA,SAAgB,cAAc,OAAmC;CAC/D,MAAM,EAAE,SAAS,sBAAsB;AACvC,QAEE,CAAC,QAAQ,WAAW,CAAC,SAAS,QAAe,IAE7C,CAAC,CAAC;;;;;;;;;;;AAeN,SAAS,YAAY,OAAoD;CACvE,MAAM,EAAE,iBAAiB;AACzB,KAAI,CAAC,gBAAgB,EAAE,SAAS,cAAe,QAAO;AACtD,KAAI,QAAQ,aAAc,QAAO;AACjC,QAAO,cAAc,MAAM,GAAG,cAAc;;;;;;;;;;;;;;AAe9C,SAAgB,oBAAiD,EAC/D,SAGiE;CACjE,MAAM,qBAAqB,eAAe,YAAY,MAAM,KAAK,CAAC,CAAC;AAEnE,QAAO,qBAA+C;EACpD,SAAS;EACT,eAAe,mBAAmB,KAAK;EACvC,QAAQ;GACN,uBAAuB,EAAE;GACzB,MAAM,EAAE;GACR,WAAW,EAGT,aAAa;IACX,MAAM,eAAe,MAAM,KAAK,CAAC;IACjC,MAAM,KAAK,IAAI,iBAAiB;AAEhC,oBAAgB,cAAc,EAAE,QAAQ,GAAG,QAAQ,CAAC,CACjD,MAAM,aAAa,gBAAgB,SAAS,CAAC,CAC7C,MAAM,SAAS;AAEd,YAAO,OAAO,EAAE,cADD,0BAA0B,MAAM,aAAa,EACtB,CAAe;MACrD,CACD,OAAO,UAAU;AAChB,SAAI,iBAAiB,SAAS,MAAM,SAAS,aAAc;AAC3D,WAAM;MACN;AAEJ,WAAO;MAEV;GACD,UAAU,EAAE;GACb;EACF,CAAC"}
@@ -0,0 +1,66 @@
1
+ import { effect } from "../signals/effect.js";
2
+ import { isResolvedTrack } from "../types/index.js";
3
+ import { getSelectedTrack } from "../utils/track-selection.js";
4
+ import { fetchResolvable, getResponseText } from "../../dom/network/fetch.js";
5
+ import { ConcurrentRunner, Task } from "../task.js";
6
+ import { parseMediaPlaylist } from "../hls/parse-media-playlist.js";
7
+ //#region src/core/features/resolve-track.ts
8
+ function canResolve(state, config) {
9
+ const track = getSelectedTrack(state, config.type);
10
+ if (!track) return false;
11
+ return !isResolvedTrack(track);
12
+ }
13
+ /**
14
+ * Updates a track within a presentation (immutably).
15
+ * Generic - works for video, audio, or text tracks.
16
+ */
17
+ function updateTrackInPresentation(presentation, resolvedTrack) {
18
+ const trackId = resolvedTrack.id;
19
+ return {
20
+ ...presentation,
21
+ selectionSets: presentation.selectionSets.map((selectionSet) => ({
22
+ ...selectionSet,
23
+ switchingSets: selectionSet.switchingSets.map((switchingSet) => ({
24
+ ...switchingSet,
25
+ tracks: switchingSet.tracks.map((track) => track.id === trackId ? resolvedTrack : track)
26
+ }))
27
+ }))
28
+ };
29
+ }
30
+ /**
31
+ * Resolves unresolved tracks using reactive composition.
32
+ *
33
+ * Reacts to state changes and schedules fetch tasks via ConcurrentRunner when
34
+ * a selected track is unresolved. The ConcurrentRunner handles deduplication,
35
+ * parallel execution, and cleanup.
36
+ *
37
+ * Generic version that works for video, audio, or text tracks based on config.
38
+ * Type parameter T is inferred from config.type (use 'as const' for inference).
39
+ */
40
+ function resolveTrack({ state }, config) {
41
+ const runner = new ConcurrentRunner();
42
+ const cleanup = effect(() => {
43
+ const currentState = state.get();
44
+ if (!canResolve(currentState, config)) return;
45
+ const track = getSelectedTrack(currentState, config.type);
46
+ if (!track) return;
47
+ const resolvedTrack = track;
48
+ runner.schedule(new Task(async (signal) => {
49
+ const mediaTrack = parseMediaPlaylist(await getResponseText(await fetchResolvable(resolvedTrack, { signal })), resolvedTrack);
50
+ const latest = state.get();
51
+ const updatedPresentation = updateTrackInPresentation(latest.presentation, mediaTrack);
52
+ state.set({
53
+ ...latest,
54
+ presentation: updatedPresentation
55
+ });
56
+ }, { id: track.id }));
57
+ });
58
+ return () => {
59
+ runner.abortAll();
60
+ cleanup();
61
+ };
62
+ }
63
+ //#endregion
64
+ export { resolveTrack };
65
+
66
+ //# sourceMappingURL=resolve-track.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve-track.js","names":[],"sources":["../../../../src/core/features/resolve-track.ts"],"sourcesContent":["import { fetchResolvable, getResponseText } from '../../dom/network/fetch';\nimport { parseMediaPlaylist } from '../hls/parse-media-playlist';\nimport { effect } from '../signals/effect';\nimport type { Signal } from '../signals/primitives';\nimport { ConcurrentRunner, Task } from '../task';\nimport type { Presentation, ResolvedTrack, TrackType } from '../types';\nimport { isResolvedTrack } from '../types';\nimport { getSelectedTrack, type TrackSelectionState } from '../utils/track-selection';\n\n/**\n * State shape for track resolution.\n */\nexport interface TrackResolutionState extends TrackSelectionState {\n presentation?: Presentation | undefined;\n selectedVideoTrackId?: string | undefined;\n selectedAudioTrackId?: string | undefined;\n selectedTextTrackId?: string | undefined;\n}\n\nexport function canResolve<T extends TrackType>(\n state: TrackResolutionState,\n config: TrackResolutionConfig<T>\n): boolean {\n const track = getSelectedTrack(state, config.type);\n if (!track) return false;\n\n return !isResolvedTrack(track);\n}\n\n// ============================================================================\n// Public API\n// ============================================================================\n\n/**\n * Updates a track within a presentation (immutably).\n * Generic - works for video, audio, or text tracks.\n */\nexport function updateTrackInPresentation<T extends ResolvedTrack>(\n presentation: Presentation,\n resolvedTrack: T\n): Presentation {\n const trackId = resolvedTrack.id;\n return {\n ...presentation,\n selectionSets: presentation.selectionSets.map((selectionSet) => ({\n ...selectionSet,\n switchingSets: selectionSet.switchingSets.map((switchingSet) => ({\n ...switchingSet,\n tracks: switchingSet.tracks.map((track) => (track.id === trackId ? resolvedTrack : track)),\n })),\n })),\n } as Presentation;\n}\n\n/**\n * Configuration for track resolution.\n */\nexport interface TrackResolutionConfig<T extends TrackType = TrackType> {\n type: T;\n}\n\n/**\n * Resolves unresolved tracks using reactive composition.\n *\n * Reacts to state changes and schedules fetch tasks via ConcurrentRunner when\n * a selected track is unresolved. The ConcurrentRunner handles deduplication,\n * parallel execution, and cleanup.\n *\n * Generic version that works for video, audio, or text tracks based on config.\n * Type parameter T is inferred from config.type (use 'as const' for inference).\n */\nexport function resolveTrack<S extends TrackResolutionState, T extends TrackType>(\n { state }: { state: Signal<S> },\n config: TrackResolutionConfig<T>\n): () => void {\n // NOTE: This can/maybe will be pulled into a per-use case factory (e.g. something like createTaskRunner() with args TBD),\n // likely eventually passed down via config or a new \"definitions\" argument. This will allow us to decide if we want our task runner/scheduler\n // to e.g. run concurrently (like we currently are), serially with a queue, or abort the previous task and replace it with the newly scheduled one. (CJP).\n const runner = new ConcurrentRunner();\n\n const cleanup = effect(() => {\n const currentState = state.get();\n if (!canResolve(currentState, config)) return;\n\n const track = getSelectedTrack(currentState, config.type);\n if (!track) return;\n\n const resolvedTrack = track;\n\n runner.schedule(\n // NOTE: This can/maybe will be pulled into a per-use case factory (e.g. something like createResolveTrackTask(track, context, config)),\n // likely eventually passed down via config or a new \"definitions\" argument (CJP).\n new Task(\n async (signal) => {\n const response = await fetchResolvable(resolvedTrack, { signal });\n const text = await getResponseText(response);\n const mediaTrack = parseMediaPlaylist(text, resolvedTrack);\n\n // IMPORTANT: Read state.get().presentation at write time, not from the\n // captured currentState snapshot. Multiple Tasks may be running concurrently\n // (one per track being resolved), so the snapshot is likely already stale by\n // the time a task completes — a sibling task may have already written the\n // presentation with its own resolved track. Reading live state ensures each\n // task builds on top of whatever has been committed so far.\n const latest = state.get();\n const updatedPresentation = updateTrackInPresentation(latest.presentation!, mediaTrack);\n state.set({ ...latest, presentation: updatedPresentation } as S);\n },\n { id: track.id }\n )\n );\n });\n\n return () => {\n runner.abortAll();\n cleanup();\n };\n}\n"],"mappings":";;;;;;;AAmBA,SAAgB,WACd,OACA,QACS;CACT,MAAM,QAAQ,iBAAiB,OAAO,OAAO,KAAK;AAClD,KAAI,CAAC,MAAO,QAAO;AAEnB,QAAO,CAAC,gBAAgB,MAAM;;;;;;AAWhC,SAAgB,0BACd,cACA,eACc;CACd,MAAM,UAAU,cAAc;AAC9B,QAAO;EACL,GAAG;EACH,eAAe,aAAa,cAAc,KAAK,kBAAkB;GAC/D,GAAG;GACH,eAAe,aAAa,cAAc,KAAK,kBAAkB;IAC/D,GAAG;IACH,QAAQ,aAAa,OAAO,KAAK,UAAW,MAAM,OAAO,UAAU,gBAAgB,MAAO;IAC3F,EAAE;GACJ,EAAE;EACJ;;;;;;;;;;;;AAoBH,SAAgB,aACd,EAAE,SACF,QACY;CAIZ,MAAM,SAAS,IAAI,kBAAkB;CAErC,MAAM,UAAU,aAAa;EAC3B,MAAM,eAAe,MAAM,KAAK;AAChC,MAAI,CAAC,WAAW,cAAc,OAAO,CAAE;EAEvC,MAAM,QAAQ,iBAAiB,cAAc,OAAO,KAAK;AACzD,MAAI,CAAC,MAAO;EAEZ,MAAM,gBAAgB;AAEtB,SAAO,SAGL,IAAI,KACF,OAAO,WAAW;GAGhB,MAAM,aAAa,mBADN,MAAM,gBADF,MAAM,gBAAgB,eAAe,EAAE,QAAQ,CAAC,CACrB,EACA,cAAc;GAQ1D,MAAM,SAAS,MAAM,KAAK;GAC1B,MAAM,sBAAsB,0BAA0B,OAAO,cAAe,WAAW;AACvF,SAAM,IAAI;IAAE,GAAG;IAAQ,cAAc;IAAqB,CAAM;KAElE,EAAE,IAAI,MAAM,IAAI,CACjB,CACF;GACD;AAEF,cAAa;AACX,SAAO,UAAU;AACjB,WAAS"}
@@ -0,0 +1,130 @@
1
+ import { effect } from "../signals/effect.js";
2
+ import { update } from "../signals/primitives.js";
3
+ import { SelectedTrackIdKeyByType } from "../utils/track-selection.js";
4
+ //#region src/core/features/select-tracks.ts
5
+ /**
6
+ * Pick text track to activate.
7
+ *
8
+ * Selection priority (if enabled):
9
+ * 1. User preference (preferredSubtitleLanguage)
10
+ * 2. DEFAULT track (if enableDefaultTrack is true and track has DEFAULT=YES + AUTOSELECT=YES)
11
+ * 3. No auto-selection (user opt-in)
12
+ *
13
+ * By default, FORCED tracks are excluded per Apple's HLS spec.
14
+ *
15
+ * @param presentation - Presentation with text tracks
16
+ * @param config - Selection configuration
17
+ * @returns Track ID or undefined (no auto-selection)
18
+ */
19
+ function pickTextTrack(presentation, config) {
20
+ const textSet = presentation.selectionSets.find((set) => set.type === "text");
21
+ if (!textSet?.switchingSets?.[0]?.tracks.length) return void 0;
22
+ const tracks = textSet.switchingSets[0].tracks;
23
+ const availableTracks = config.includeForcedTracks ? tracks : tracks.filter((track) => !track.forced);
24
+ if (availableTracks.length === 0) return void 0;
25
+ const { preferredSubtitleLanguage, enableDefaultTrack = false } = config;
26
+ if (preferredSubtitleLanguage) {
27
+ const languageMatch = availableTracks.find((track) => track.language === preferredSubtitleLanguage);
28
+ if (languageMatch) return languageMatch.id;
29
+ }
30
+ if (enableDefaultTrack) {
31
+ const defaultTrack = availableTracks.find((track) => track.default === true);
32
+ if (defaultTrack) return defaultTrack.id;
33
+ }
34
+ }
35
+ /**
36
+ * Check if we can select a track of the given type.
37
+ *
38
+ * Returns true when:
39
+ * - Presentation exists
40
+ * - Has tracks of the specified type
41
+ *
42
+ * Generic over track type - works for video, audio, or text.
43
+ */
44
+ function canSelectTrack(state, config) {
45
+ return !!state?.presentation?.selectionSets?.find(({ type }) => type === config.type)?.switchingSets?.[0]?.tracks.length;
46
+ }
47
+ /**
48
+ * Check if we should select a track of the given type.
49
+ *
50
+ * Returns true when:
51
+ * - Track of this type is not already selected
52
+ *
53
+ * Generic over track type - works for video, audio, or text.
54
+ *
55
+ * @TODO figure out reactive model for ABR cases - right now we're only selecting
56
+ * if we have nothing selected (CJP)
57
+ */
58
+ function shouldSelectTrack(state, config) {
59
+ return !state[SelectedTrackIdKeyByType[config.type]];
60
+ }
61
+ /**
62
+ * Select video track orchestration.
63
+ *
64
+ * Selects video track when:
65
+ * - Presentation exists
66
+ * - No video track is selected yet
67
+ *
68
+ * Uses bandwidth-based quality selection algorithm.
69
+ *
70
+ * @example
71
+ * const cleanup = selectVideoTrack(
72
+ * { state, owners, events },
73
+ * { initialBandwidth: 2_000_000 }
74
+ * );
75
+ */
76
+ function selectVideoTrack({ state }, config = { type: "video" }) {
77
+ return effect(() => {
78
+ const currentState = state.get();
79
+ if (!canSelectTrack(currentState, config) || !shouldSelectTrack(currentState, config)) return;
80
+ const selectedTrackId = currentState.presentation?.selectionSets.find(({ type }) => type === config.type)?.switchingSets[0]?.tracks[0]?.id;
81
+ if (selectedTrackId) update(state, { [SelectedTrackIdKeyByType[config.type]]: selectedTrackId });
82
+ });
83
+ }
84
+ /**
85
+ * Select audio track orchestration.
86
+ *
87
+ * Selects audio track when:
88
+ * - Presentation exists
89
+ * - No audio track is selected yet
90
+ *
91
+ * Uses language and preference-based selection.
92
+ *
93
+ * @example
94
+ * const cleanup = selectAudioTrack(
95
+ * { state, owners, events },
96
+ * { preferredAudioLanguage: 'en' }
97
+ * );
98
+ */
99
+ function selectAudioTrack({ state }, config = { type: "audio" }) {
100
+ return effect(() => {
101
+ const currentState = state.get();
102
+ if (!canSelectTrack(currentState, config) || !shouldSelectTrack(currentState, config)) return;
103
+ const selectedTrackId = currentState.presentation?.selectionSets.find(({ type }) => type === "audio")?.switchingSets[0]?.tracks[0]?.id;
104
+ if (selectedTrackId) update(state, { selectedAudioTrackId: selectedTrackId });
105
+ });
106
+ }
107
+ /**
108
+ * Select text track orchestration.
109
+ *
110
+ * Selects text track when:
111
+ * - Presentation exists
112
+ * - No text track is selected yet
113
+ *
114
+ * Note: Currently does not auto-select (user opt-in).
115
+ *
116
+ * @example
117
+ * const cleanup = selectTextTrack({ state, owners, events }, {});
118
+ */
119
+ function selectTextTrack({ state }, config = { type: "text" }) {
120
+ return effect(() => {
121
+ const currentState = state.get();
122
+ if (!canSelectTrack(currentState, config) || !shouldSelectTrack(currentState, config)) return;
123
+ const selectedTextTrackId = pickTextTrack(currentState.presentation, config);
124
+ if (selectedTextTrackId) update(state, { selectedTextTrackId });
125
+ });
126
+ }
127
+ //#endregion
128
+ export { selectAudioTrack, selectTextTrack, selectVideoTrack };
129
+
130
+ //# sourceMappingURL=select-tracks.js.map