@videojs/spf 10.0.0-beta.1 → 10.0.0-beta.11

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 (242) hide show
  1. package/README.md +16 -40
  2. package/dist/default/all.js +16 -0
  3. package/dist/default/core/abr/bandwidth-estimator.js +96 -0
  4. package/dist/default/core/abr/bandwidth-estimator.js.map +1 -0
  5. package/dist/default/core/abr/ewma.js +71 -0
  6. package/dist/default/core/abr/ewma.js.map +1 -0
  7. package/dist/default/core/abr/quality-selection.js +53 -0
  8. package/dist/default/core/abr/quality-selection.js.map +1 -0
  9. package/dist/default/core/buffer/back-buffer.js +46 -0
  10. package/dist/default/core/buffer/back-buffer.js.map +1 -0
  11. package/dist/default/core/buffer/forward-buffer.js +73 -0
  12. package/dist/default/core/buffer/forward-buffer.js.map +1 -0
  13. package/dist/default/core/events/create-event-stream.js +48 -0
  14. package/dist/default/core/events/create-event-stream.js.map +1 -0
  15. package/dist/default/core/features/calculate-presentation-duration.js +51 -0
  16. package/dist/default/core/features/calculate-presentation-duration.js.map +1 -0
  17. package/dist/default/core/features/quality-switching.js +68 -0
  18. package/dist/default/core/features/quality-switching.js.map +1 -0
  19. package/dist/default/core/features/resolve-presentation.js +99 -0
  20. package/dist/default/core/features/resolve-presentation.js.map +1 -0
  21. package/dist/default/core/features/resolve-track.js +77 -0
  22. package/dist/default/core/features/resolve-track.js.map +1 -0
  23. package/dist/default/core/features/select-tracks.js +147 -0
  24. package/dist/default/core/features/select-tracks.js.map +1 -0
  25. package/dist/default/core/hls/parse-attributes.js +134 -0
  26. package/dist/default/core/hls/parse-attributes.js.map +1 -0
  27. package/dist/default/core/hls/parse-media-playlist.js +83 -0
  28. package/dist/default/core/hls/parse-media-playlist.js.map +1 -0
  29. package/dist/default/core/hls/parse-multivariant.js +215 -0
  30. package/dist/default/core/hls/parse-multivariant.js.map +1 -0
  31. package/dist/default/core/hls/resolve-url.js +11 -0
  32. package/dist/default/core/hls/resolve-url.js.map +1 -0
  33. package/dist/default/core/reactive/combine-latest.js +72 -0
  34. package/dist/default/core/reactive/combine-latest.js.map +1 -0
  35. package/dist/default/core/state/create-state.js +143 -0
  36. package/dist/default/core/state/create-state.js.map +1 -0
  37. package/dist/default/core/task.js +122 -0
  38. package/dist/default/core/task.js.map +1 -0
  39. package/dist/default/core/types/index.js +15 -0
  40. package/dist/default/core/types/index.js.map +1 -0
  41. package/dist/default/core/utils/generate-id.js +21 -0
  42. package/dist/default/core/utils/generate-id.js.map +1 -0
  43. package/dist/default/core/utils/track-selection.js +31 -0
  44. package/dist/default/core/utils/track-selection.js.map +1 -0
  45. package/dist/default/dom/features/end-of-stream.js +169 -0
  46. package/dist/default/dom/features/end-of-stream.js.map +1 -0
  47. package/dist/default/dom/features/load-segments.js +188 -0
  48. package/dist/default/dom/features/load-segments.js.map +1 -0
  49. package/dist/default/dom/features/load-text-track-cues.js +151 -0
  50. package/dist/default/dom/features/load-text-track-cues.js.map +1 -0
  51. package/dist/default/dom/features/segment-loader-actor.js +197 -0
  52. package/dist/default/dom/features/segment-loader-actor.js.map +1 -0
  53. package/dist/default/dom/features/setup-mediasource.js +55 -0
  54. package/dist/default/dom/features/setup-mediasource.js.map +1 -0
  55. package/dist/default/dom/features/setup-sourcebuffer.js +70 -0
  56. package/dist/default/dom/features/setup-sourcebuffer.js.map +1 -0
  57. package/dist/default/dom/features/setup-text-tracks.js +88 -0
  58. package/dist/default/dom/features/setup-text-tracks.js.map +1 -0
  59. package/dist/default/dom/features/sync-selected-text-track-from-dom.js +63 -0
  60. package/dist/default/dom/features/sync-selected-text-track-from-dom.js.map +1 -0
  61. package/dist/default/dom/features/sync-text-track-modes.js +38 -0
  62. package/dist/default/dom/features/sync-text-track-modes.js.map +1 -0
  63. package/dist/default/dom/features/track-current-time.js +46 -0
  64. package/dist/default/dom/features/track-current-time.js.map +1 -0
  65. package/dist/default/dom/features/track-playback-initiated.js +55 -0
  66. package/dist/default/dom/features/track-playback-initiated.js.map +1 -0
  67. package/dist/{dom/index.js → default/dom/features/track-playback-rate.js} +2 -4
  68. package/dist/default/dom/features/track-playback-rate.js.map +1 -0
  69. package/dist/default/dom/features/update-duration.js +72 -0
  70. package/dist/default/dom/features/update-duration.js.map +1 -0
  71. package/dist/default/dom/media/append-segment.js +60 -0
  72. package/dist/default/dom/media/append-segment.js.map +1 -0
  73. package/dist/default/dom/media/buffer-flusher.js +55 -0
  74. package/dist/default/dom/media/buffer-flusher.js.map +1 -0
  75. package/dist/default/dom/media/mediasource-setup.js +154 -0
  76. package/dist/default/dom/media/mediasource-setup.js.map +1 -0
  77. package/dist/default/dom/media/source-buffer-actor.js +186 -0
  78. package/dist/default/dom/media/source-buffer-actor.js.map +1 -0
  79. package/dist/default/dom/network/chunked-stream-iterable.js +49 -0
  80. package/dist/default/dom/network/chunked-stream-iterable.js.map +1 -0
  81. package/dist/default/dom/network/fetch.js +56 -0
  82. package/dist/default/dom/network/fetch.js.map +1 -0
  83. package/dist/default/dom/playback-engine/adapter.js +95 -0
  84. package/dist/default/dom/playback-engine/adapter.js.map +1 -0
  85. package/dist/default/dom/playback-engine/engine.js +176 -0
  86. package/dist/default/dom/playback-engine/engine.js.map +1 -0
  87. package/dist/default/dom/playback-engine.js +4 -0
  88. package/dist/default/dom/text/parse-vtt-segment.js +56 -0
  89. package/dist/default/dom/text/parse-vtt-segment.js.map +1 -0
  90. package/dist/default/dom.js +11 -0
  91. package/dist/{src-DRsS_H80.js → default/index.js} +2 -2
  92. package/dist/default/index.js.map +1 -0
  93. package/dist/dev/all.js +16 -0
  94. package/dist/dev/core/abr/bandwidth-estimator.d.ts +35 -0
  95. package/dist/dev/core/abr/bandwidth-estimator.d.ts.map +1 -0
  96. package/dist/dev/core/abr/bandwidth-estimator.js +96 -0
  97. package/dist/dev/core/abr/bandwidth-estimator.js.map +1 -0
  98. package/dist/dev/core/abr/ewma.js +71 -0
  99. package/dist/dev/core/abr/ewma.js.map +1 -0
  100. package/dist/dev/core/abr/quality-selection.js +53 -0
  101. package/dist/dev/core/abr/quality-selection.js.map +1 -0
  102. package/dist/dev/core/actor.d.ts +31 -0
  103. package/dist/dev/core/actor.d.ts.map +1 -0
  104. package/dist/dev/core/buffer/back-buffer.js +46 -0
  105. package/dist/dev/core/buffer/back-buffer.js.map +1 -0
  106. package/dist/dev/core/buffer/forward-buffer.js +73 -0
  107. package/dist/dev/core/buffer/forward-buffer.js.map +1 -0
  108. package/dist/dev/core/events/create-event-stream.d.ts +48 -0
  109. package/dist/dev/core/events/create-event-stream.d.ts.map +1 -0
  110. package/dist/dev/core/events/create-event-stream.js +48 -0
  111. package/dist/dev/core/events/create-event-stream.js.map +1 -0
  112. package/dist/dev/core/features/calculate-presentation-duration.js +51 -0
  113. package/dist/dev/core/features/calculate-presentation-duration.js.map +1 -0
  114. package/dist/dev/core/features/quality-switching.js +68 -0
  115. package/dist/dev/core/features/quality-switching.js.map +1 -0
  116. package/dist/dev/core/features/resolve-presentation.d.ts +16 -0
  117. package/dist/dev/core/features/resolve-presentation.d.ts.map +1 -0
  118. package/dist/dev/core/features/resolve-presentation.js +99 -0
  119. package/dist/dev/core/features/resolve-presentation.js.map +1 -0
  120. package/dist/dev/core/features/resolve-track.d.ts +13 -0
  121. package/dist/dev/core/features/resolve-track.d.ts.map +1 -0
  122. package/dist/dev/core/features/resolve-track.js +77 -0
  123. package/dist/dev/core/features/resolve-track.js.map +1 -0
  124. package/dist/dev/core/features/select-tracks.d.ts +11 -0
  125. package/dist/dev/core/features/select-tracks.d.ts.map +1 -0
  126. package/dist/dev/core/features/select-tracks.js +147 -0
  127. package/dist/dev/core/features/select-tracks.js.map +1 -0
  128. package/dist/dev/core/hls/parse-attributes.js +134 -0
  129. package/dist/dev/core/hls/parse-attributes.js.map +1 -0
  130. package/dist/dev/core/hls/parse-media-playlist.js +83 -0
  131. package/dist/dev/core/hls/parse-media-playlist.js.map +1 -0
  132. package/dist/dev/core/hls/parse-multivariant.js +215 -0
  133. package/dist/dev/core/hls/parse-multivariant.js.map +1 -0
  134. package/dist/dev/core/hls/resolve-url.js +11 -0
  135. package/dist/dev/core/hls/resolve-url.js.map +1 -0
  136. package/dist/dev/core/reactive/combine-latest.js +72 -0
  137. package/dist/dev/core/reactive/combine-latest.js.map +1 -0
  138. package/dist/dev/core/state/create-state.d.ts +104 -0
  139. package/dist/dev/core/state/create-state.d.ts.map +1 -0
  140. package/dist/dev/core/state/create-state.js +143 -0
  141. package/dist/dev/core/state/create-state.js.map +1 -0
  142. package/dist/dev/core/task.js +122 -0
  143. package/dist/dev/core/task.js.map +1 -0
  144. package/dist/dev/core/types/index.d.ts +173 -0
  145. package/dist/dev/core/types/index.d.ts.map +1 -0
  146. package/dist/dev/core/types/index.js +15 -0
  147. package/dist/dev/core/types/index.js.map +1 -0
  148. package/dist/dev/core/utils/generate-id.js +21 -0
  149. package/dist/dev/core/utils/generate-id.js.map +1 -0
  150. package/dist/dev/core/utils/track-selection.d.ts +16 -0
  151. package/dist/dev/core/utils/track-selection.d.ts.map +1 -0
  152. package/dist/dev/core/utils/track-selection.js +31 -0
  153. package/dist/dev/core/utils/track-selection.js.map +1 -0
  154. package/dist/dev/dom/features/end-of-stream.js +169 -0
  155. package/dist/dev/dom/features/end-of-stream.js.map +1 -0
  156. package/dist/dev/dom/features/load-segments.d.ts +72 -0
  157. package/dist/dev/dom/features/load-segments.d.ts.map +1 -0
  158. package/dist/dev/dom/features/load-segments.js +188 -0
  159. package/dist/dev/dom/features/load-segments.js.map +1 -0
  160. package/dist/dev/dom/features/load-text-track-cues.d.ts +61 -0
  161. package/dist/dev/dom/features/load-text-track-cues.d.ts.map +1 -0
  162. package/dist/dev/dom/features/load-text-track-cues.js +151 -0
  163. package/dist/dev/dom/features/load-text-track-cues.js.map +1 -0
  164. package/dist/dev/dom/features/segment-loader-actor.js +197 -0
  165. package/dist/dev/dom/features/segment-loader-actor.js.map +1 -0
  166. package/dist/dev/dom/features/setup-mediasource.js +55 -0
  167. package/dist/dev/dom/features/setup-mediasource.js.map +1 -0
  168. package/dist/dev/dom/features/setup-sourcebuffer.d.ts +9 -0
  169. package/dist/dev/dom/features/setup-sourcebuffer.d.ts.map +1 -0
  170. package/dist/dev/dom/features/setup-sourcebuffer.js +70 -0
  171. package/dist/dev/dom/features/setup-sourcebuffer.js.map +1 -0
  172. package/dist/dev/dom/features/setup-text-tracks.js +88 -0
  173. package/dist/dev/dom/features/setup-text-tracks.js.map +1 -0
  174. package/dist/dev/dom/features/sync-selected-text-track-from-dom.js +63 -0
  175. package/dist/dev/dom/features/sync-selected-text-track-from-dom.js.map +1 -0
  176. package/dist/dev/dom/features/sync-text-track-modes.js +38 -0
  177. package/dist/dev/dom/features/sync-text-track-modes.js.map +1 -0
  178. package/dist/dev/dom/features/track-current-time.d.ts +40 -0
  179. package/dist/dev/dom/features/track-current-time.d.ts.map +1 -0
  180. package/dist/dev/dom/features/track-current-time.js +46 -0
  181. package/dist/dev/dom/features/track-current-time.js.map +1 -0
  182. package/dist/dev/dom/features/track-playback-initiated.d.ts +53 -0
  183. package/dist/dev/dom/features/track-playback-initiated.d.ts.map +1 -0
  184. package/dist/dev/dom/features/track-playback-initiated.js +55 -0
  185. package/dist/dev/dom/features/track-playback-initiated.js.map +1 -0
  186. package/dist/dev/dom/features/track-playback-rate.d.ts +34 -0
  187. package/dist/dev/dom/features/track-playback-rate.d.ts.map +1 -0
  188. package/dist/dev/dom/features/track-playback-rate.js +36 -0
  189. package/dist/dev/dom/features/track-playback-rate.js.map +1 -0
  190. package/dist/dev/dom/features/update-duration.js +72 -0
  191. package/dist/dev/dom/features/update-duration.js.map +1 -0
  192. package/dist/dev/dom/media/append-segment.d.ts +24 -0
  193. package/dist/dev/dom/media/append-segment.d.ts.map +1 -0
  194. package/dist/dev/dom/media/append-segment.js +60 -0
  195. package/dist/dev/dom/media/append-segment.js.map +1 -0
  196. package/dist/dev/dom/media/buffer-flusher.d.ts +24 -0
  197. package/dist/dev/dom/media/buffer-flusher.d.ts.map +1 -0
  198. package/dist/dev/dom/media/buffer-flusher.js +55 -0
  199. package/dist/dev/dom/media/buffer-flusher.js.map +1 -0
  200. package/dist/dev/dom/media/mediasource-setup.js +154 -0
  201. package/dist/dev/dom/media/mediasource-setup.js.map +1 -0
  202. package/dist/dev/dom/media/source-buffer-actor.d.ts +56 -0
  203. package/dist/dev/dom/media/source-buffer-actor.d.ts.map +1 -0
  204. package/dist/dev/dom/media/source-buffer-actor.js +186 -0
  205. package/dist/dev/dom/media/source-buffer-actor.js.map +1 -0
  206. package/dist/dev/dom/network/chunked-stream-iterable.js +49 -0
  207. package/dist/dev/dom/network/chunked-stream-iterable.js.map +1 -0
  208. package/dist/dev/dom/network/fetch.js +56 -0
  209. package/dist/dev/dom/network/fetch.js.map +1 -0
  210. package/dist/dev/dom/playback-engine/adapter.d.ts +41 -0
  211. package/dist/dev/dom/playback-engine/adapter.d.ts.map +1 -0
  212. package/dist/dev/dom/playback-engine/adapter.js +95 -0
  213. package/dist/dev/dom/playback-engine/adapter.js.map +1 -0
  214. package/dist/dev/dom/playback-engine/engine.d.ts +135 -0
  215. package/dist/dev/dom/playback-engine/engine.d.ts.map +1 -0
  216. package/dist/dev/dom/playback-engine/engine.js +176 -0
  217. package/dist/dev/dom/playback-engine/engine.js.map +1 -0
  218. package/dist/dev/dom/playback-engine.d.ts +3 -0
  219. package/dist/dev/dom/playback-engine.js +4 -0
  220. package/dist/dev/dom/text/parse-vtt-segment.d.ts +12 -0
  221. package/dist/dev/dom/text/parse-vtt-segment.d.ts.map +1 -0
  222. package/dist/dev/dom/text/parse-vtt-segment.js +56 -0
  223. package/dist/dev/dom/text/parse-vtt-segment.js.map +1 -0
  224. package/dist/dev/dom.d.ts +10 -0
  225. package/dist/dev/dom.js +11 -0
  226. package/dist/dev/index.d.ts.map +1 -0
  227. package/dist/dev/index.js +14 -0
  228. package/dist/dev/index.js.map +1 -0
  229. package/package.json +22 -22
  230. package/dist/adapter-CMw-rvbk.js +0 -3512
  231. package/dist/adapter-CMw-rvbk.js.map +0 -1
  232. package/dist/adapter-l8iJN_GI.d.ts +0 -689
  233. package/dist/adapter-l8iJN_GI.d.ts.map +0 -1
  234. package/dist/dom/index.d.ts +0 -243
  235. package/dist/dom/index.d.ts.map +0 -1
  236. package/dist/dom/index.js.map +0 -1
  237. package/dist/dom/playback-engine/index.d.ts +0 -2
  238. package/dist/dom/playback-engine/index.js +0 -4
  239. package/dist/index.d.ts.map +0 -1
  240. package/dist/index.js +0 -3
  241. package/dist/src-DRsS_H80.js.map +0 -1
  242. /package/dist/{index.d.ts → dev/index.d.ts} +0 -0
@@ -0,0 +1,176 @@
1
+ import { createState } from "../../core/state/create-state.js";
2
+ import { createEventStream } from "../../core/events/create-event-stream.js";
3
+ import { resolvePresentation, syncPreloadAttribute } from "../../core/features/resolve-presentation.js";
4
+ import { switchQuality } from "../../core/features/quality-switching.js";
5
+ import { loadSegments } from "../features/load-segments.js";
6
+ import { destroyVttParser } from "../text/parse-vtt-segment.js";
7
+ import { loadTextTrackCues } from "../features/load-text-track-cues.js";
8
+ import { trackCurrentTime } from "../features/track-current-time.js";
9
+ import { trackPlaybackInitiated } from "../features/track-playback-initiated.js";
10
+ import { calculatePresentationDuration } from "../../core/features/calculate-presentation-duration.js";
11
+ import { resolveTrack } from "../../core/features/resolve-track.js";
12
+ import { selectAudioTrack, selectTextTrack, selectVideoTrack } from "../../core/features/select-tracks.js";
13
+ import { endOfStream } from "../features/end-of-stream.js";
14
+ import { setupMediaSource } from "../features/setup-mediasource.js";
15
+ import { setupSourceBuffers } from "../features/setup-sourcebuffer.js";
16
+ import { setupTextTracks } from "../features/setup-text-tracks.js";
17
+ import { syncSelectedTextTrackFromDom } from "../features/sync-selected-text-track-from-dom.js";
18
+ import { syncTextTrackModes } from "../features/sync-text-track-modes.js";
19
+ import { updateDuration } from "../features/update-duration.js";
20
+
21
+ //#region src/dom/playback-engine/engine.ts
22
+ /**
23
+ * Create a POC playback engine.
24
+ *
25
+ * Wires together all orchestrations to create a reactive playback pipeline:
26
+ * 1. Resolve presentation (multivariant playlist)
27
+ * 2. Select initial video and audio tracks
28
+ * 3. Resolve selected tracks (media playlists)
29
+ * 4. Setup MediaSource
30
+ * 5. Setup SourceBuffers for video and audio
31
+ *
32
+ * Note: This is a POC - does not yet load/append segments.
33
+ *
34
+ * @param config - Playback engine configuration
35
+ * @returns Playback engine instance with state, owners, and destroy function
36
+ *
37
+ * @example
38
+ * const engine = createPlaybackEngine({
39
+ * initialBandwidth: 2_000_000,
40
+ * preferredAudioLanguage: 'en',
41
+ * });
42
+ *
43
+ * // Initialize by patching state and owners
44
+ * engine.owners.patch({ mediaElement: document.querySelector('video') });
45
+ * engine.state.patch({
46
+ * presentation: { url: 'https://example.com/playlist.m3u8' },
47
+ * preload: 'auto',
48
+ * });
49
+ *
50
+ * // Inspect state
51
+ * console.log(engine.state.current);
52
+ *
53
+ * // Cleanup
54
+ * engine.destroy();
55
+ */
56
+ function createPlaybackEngine(config = {}) {
57
+ const state = createState({ bandwidthState: {
58
+ fastEstimate: 0,
59
+ fastTotalWeight: 0,
60
+ slowEstimate: 0,
61
+ slowTotalWeight: 0,
62
+ bytesSampled: 0
63
+ } });
64
+ const owners = createState({});
65
+ const events = createEventStream();
66
+ const cleanups = [
67
+ syncPreloadAttribute(state, owners),
68
+ trackPlaybackInitiated({
69
+ state,
70
+ owners,
71
+ events
72
+ }),
73
+ resolvePresentation({
74
+ state,
75
+ events
76
+ }),
77
+ selectVideoTrack({
78
+ state,
79
+ owners,
80
+ events
81
+ }, {
82
+ type: "video",
83
+ ...config.initialBandwidth !== void 0 && { initialBandwidth: config.initialBandwidth }
84
+ }),
85
+ selectAudioTrack({
86
+ state,
87
+ owners,
88
+ events
89
+ }, {
90
+ type: "audio",
91
+ ...config.preferredAudioLanguage !== void 0 && { preferredAudioLanguage: config.preferredAudioLanguage }
92
+ }),
93
+ selectTextTrack({
94
+ state,
95
+ owners,
96
+ events
97
+ }, {
98
+ type: "text",
99
+ ...config.preferredSubtitleLanguage !== void 0 && { preferredSubtitleLanguage: config.preferredSubtitleLanguage },
100
+ ...config.includeForcedTracks !== void 0 && { includeForcedTracks: config.includeForcedTracks },
101
+ ...config.enableDefaultTrack !== void 0 && { enableDefaultTrack: config.enableDefaultTrack }
102
+ }),
103
+ resolveTrack({
104
+ state,
105
+ events
106
+ }, { type: "video" }),
107
+ resolveTrack({
108
+ state,
109
+ events
110
+ }, { type: "audio" }),
111
+ resolveTrack({
112
+ state,
113
+ events
114
+ }, { type: "text" }),
115
+ calculatePresentationDuration({ state }),
116
+ setupMediaSource({
117
+ state,
118
+ owners
119
+ }),
120
+ updateDuration({
121
+ state,
122
+ owners
123
+ }),
124
+ setupSourceBuffers({
125
+ state,
126
+ owners
127
+ }),
128
+ trackCurrentTime({
129
+ state,
130
+ owners
131
+ }),
132
+ switchQuality({ state }),
133
+ loadSegments({
134
+ state,
135
+ owners
136
+ }, { type: "video" }),
137
+ loadSegments({
138
+ state,
139
+ owners
140
+ }, { type: "audio" }),
141
+ endOfStream({
142
+ state,
143
+ owners
144
+ }),
145
+ setupTextTracks({
146
+ state,
147
+ owners
148
+ }),
149
+ syncTextTrackModes({
150
+ state,
151
+ owners
152
+ }),
153
+ syncSelectedTextTrackFromDom({
154
+ state,
155
+ owners
156
+ }),
157
+ loadTextTrackCues({
158
+ state,
159
+ owners
160
+ })
161
+ ];
162
+ events.dispatch({ type: "@@INITIALIZE@@" });
163
+ return {
164
+ state,
165
+ owners,
166
+ events,
167
+ destroy: () => {
168
+ cleanups.forEach((cleanup) => cleanup());
169
+ destroyVttParser();
170
+ }
171
+ };
172
+ }
173
+
174
+ //#endregion
175
+ export { createPlaybackEngine };
176
+ //# sourceMappingURL=engine.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine.js","names":[],"sources":["../../../../src/dom/playback-engine/engine.ts"],"sourcesContent":["import type { BandwidthState } from '../../core/abr/bandwidth-estimator';\nimport { createEventStream } from '../../core/events/create-event-stream';\nimport { calculatePresentationDuration } from '../../core/features/calculate-presentation-duration';\nimport { switchQuality } from '../../core/features/quality-switching';\nimport {\n type PresentationAction,\n resolvePresentation,\n syncPreloadAttribute,\n} from '../../core/features/resolve-presentation';\nimport { resolveTrack, type TrackResolutionAction } from '../../core/features/resolve-track';\nimport {\n selectAudioTrack,\n selectTextTrack,\n selectVideoTrack,\n type TrackSelectionAction,\n} from '../../core/features/select-tracks';\nimport { createState } from '../../core/state/create-state';\nimport { endOfStream } from '../features/end-of-stream';\nimport { loadSegments } from '../features/load-segments';\nimport type { TextTrackBufferState } from '../features/load-text-track-cues';\nimport { loadTextTrackCues } from '../features/load-text-track-cues';\nimport { setupMediaSource } from '../features/setup-mediasource';\nimport { setupSourceBuffers } from '../features/setup-sourcebuffer';\nimport { setupTextTracks } from '../features/setup-text-tracks';\nimport { syncSelectedTextTrackFromDom } from '../features/sync-selected-text-track-from-dom';\nimport { syncTextTrackModes } from '../features/sync-text-track-modes';\nimport { trackCurrentTime } from '../features/track-current-time';\nimport { trackPlaybackInitiated } from '../features/track-playback-initiated';\nimport { updateDuration } from '../features/update-duration';\nimport type { SourceBufferActor } from '../media/source-buffer-actor';\nimport { destroyVttParser } from '../text/parse-vtt-segment';\n\n/**\n * Union of all action types used by playback engine orchestrations.\n * Includes synthetic @@INITIALIZE@@ event for combineLatest bootstrapping.\n */\nexport type PlaybackEngineAction =\n | PresentationAction\n | TrackResolutionAction\n | TrackSelectionAction\n | { type: '@@INITIALIZE@@' };\n\n/**\n * Configuration for the playback engine.\n */\nexport interface PlaybackEngineConfig {\n /**\n * Initial bandwidth estimate for cold start (bits per second).\n * Default: 1 Mbps (conservative).\n */\n initialBandwidth?: number;\n\n /**\n * Preferred audio language (ISO 639 code, e.g., \"en\", \"es\").\n * If not specified, selects first audio track.\n */\n preferredAudioLanguage?: string;\n\n /**\n * Preferred subtitle language (ISO 639 code, e.g., \"en\", \"es\").\n * If specified, selects matching text track if available.\n */\n preferredSubtitleLanguage?: string;\n\n /**\n * Include FORCED subtitle tracks in selection.\n * Default: false (follows hls.js/http-streaming pattern)\n */\n includeForcedTracks?: boolean;\n\n /**\n * Auto-select DEFAULT track (requires DEFAULT=YES + AUTOSELECT=YES in HLS).\n * Default: false (user opt-in, matches hls.js/http-streaming)\n */\n enableDefaultTrack?: boolean;\n}\n\n/**\n * Combined state shape for the playback engine.\n * Includes all state needed by orchestrations.\n */\nexport interface PlaybackEngineState {\n // Presentation state\n presentation?: any;\n preload?: string;\n\n // Track selection state\n selectedVideoTrackId?: string;\n selectedAudioTrackId?: string;\n // NOTE: Text Tracks (subtitles/ccs) can be unselected\n selectedTextTrackId?: string;\n\n // Bandwidth estimation state\n bandwidthState?: BandwidthState;\n\n // ABR control — set to true to suppress automatic quality switching (manual selection mode).\n // TODO: replace with separate manualVideoTrackId / abrVideoTrackId fields so the two\n // concerns don't share a field; see quality-switching.ts for the full design note.\n abrDisabled?: boolean;\n\n // Text track buffer state (tracks loaded VTT segments per text track ID)\n textBufferState?: TextTrackBufferState;\n\n // Current playback position (mirrored from mediaElement via trackCurrentTime)\n currentTime?: number;\n\n // True once the user has initiated playback (enables segment loading regardless of preload)\n playbackInitiated?: boolean;\n}\n\n/**\n * Combined owners shape for the playback engine.\n * Includes all mutable platform objects.\n */\nexport interface PlaybackEngineOwners {\n // Media element\n mediaElement?: HTMLMediaElement | undefined;\n\n // MediaSource\n mediaSource?: MediaSource;\n\n // SourceBuffers and their actors (created together by setupSourceBuffer)\n videoBuffer?: SourceBuffer;\n audioBuffer?: SourceBuffer;\n videoBufferActor?: SourceBufferActor;\n audioBufferActor?: SourceBufferActor;\n\n // Text tracks (track elements by ID)\n textTracks?: Map<string, HTMLTrackElement>;\n}\n\n/**\n * Playback engine instance.\n */\nexport interface PlaybackEngine {\n /**\n * Reactive state (for inspection/testing).\n */\n state: ReturnType<typeof createState<PlaybackEngineState>>;\n\n /**\n * Mutable owners (for inspection/testing).\n */\n owners: ReturnType<typeof createState<PlaybackEngineOwners>>;\n\n /**\n * Shared event stream (for inspection/testing/triggering events).\n */\n events: ReturnType<typeof createEventStream<PlaybackEngineAction>>;\n\n /**\n * Cleanup function to destroy all orchestrations.\n */\n destroy: () => void;\n}\n\n/**\n * Create a POC playback engine.\n *\n * Wires together all orchestrations to create a reactive playback pipeline:\n * 1. Resolve presentation (multivariant playlist)\n * 2. Select initial video and audio tracks\n * 3. Resolve selected tracks (media playlists)\n * 4. Setup MediaSource\n * 5. Setup SourceBuffers for video and audio\n *\n * Note: This is a POC - does not yet load/append segments.\n *\n * @param config - Playback engine configuration\n * @returns Playback engine instance with state, owners, and destroy function\n *\n * @example\n * const engine = createPlaybackEngine({\n * initialBandwidth: 2_000_000,\n * preferredAudioLanguage: 'en',\n * });\n *\n * // Initialize by patching state and owners\n * engine.owners.patch({ mediaElement: document.querySelector('video') });\n * engine.state.patch({\n * presentation: { url: 'https://example.com/playlist.m3u8' },\n * preload: 'auto',\n * });\n *\n * // Inspect state\n * console.log(engine.state.current);\n *\n * // Cleanup\n * engine.destroy();\n */\nexport function createPlaybackEngine(config: PlaybackEngineConfig = {}): PlaybackEngine {\n // Create reactive state and owners (initially empty)\n const state = createState<PlaybackEngineState>({\n bandwidthState: {\n fastEstimate: 0,\n fastTotalWeight: 0,\n slowEstimate: 0,\n slowTotalWeight: 0,\n bytesSampled: 0,\n },\n });\n const owners = createState<PlaybackEngineOwners>({});\n\n // Create single shared event stream for all orchestrations\n const events = createEventStream<PlaybackEngineAction>();\n\n // Wire up orchestrations (all share single event stream)\n // Note: @ts-expect-error needed due to EventStream invariance - each orchestration expects\n // specific event types, but shared stream has union of all types. Proper fix would\n // require making EventStream covariant or refactoring event system.\n const cleanups = [\n // 0a. Sync preload attribute from mediaElement → state.preload\n // Only re-reads when the mediaElement reference changes (lastMediaElement guard).\n // Normalises '' (absent attribute) to 'auto' (browser default).\n // @ts-expect-error - WritableState type variance\n syncPreloadAttribute(state, owners),\n\n // 0b. Bridge media element play event → state.playbackInitiated + event stream\n // Enables preload=\"none\"/\"metadata\" resolution via native controls / element.play()\n // @ts-expect-error - EventStream type variance\n trackPlaybackInitiated({ state, owners, events }),\n\n // 1. Resolve presentation (URL already in state)\n // @ts-expect-error - EventStream type variance\n resolvePresentation({ state, events }),\n\n // 2. Select initial tracks (when presentation loads)\n selectVideoTrack(\n // @ts-expect-error - Owners and EventStream type compatibility\n { state, owners, events },\n {\n type: 'video',\n ...(config.initialBandwidth !== undefined && { initialBandwidth: config.initialBandwidth }),\n }\n ),\n selectAudioTrack(\n // @ts-expect-error - Owners and EventStream type compatibility\n { state, owners, events },\n {\n type: 'audio',\n ...(config.preferredAudioLanguage !== undefined && { preferredAudioLanguage: config.preferredAudioLanguage }),\n }\n ),\n selectTextTrack(\n // @ts-expect-error - Owners and EventStream type compatibility\n { state, owners, events },\n {\n type: 'text',\n ...(config.preferredSubtitleLanguage !== undefined && {\n preferredSubtitleLanguage: config.preferredSubtitleLanguage,\n }),\n ...(config.includeForcedTracks !== undefined && { includeForcedTracks: config.includeForcedTracks }),\n ...(config.enableDefaultTrack !== undefined && { enableDefaultTrack: config.enableDefaultTrack }),\n }\n ),\n\n // 3. Resolve selected tracks (fetch media playlists)\n // @ts-expect-error - EventStream type variance\n resolveTrack({ state, events }, { type: 'video' as const }),\n // @ts-expect-error - EventStream type variance\n resolveTrack({ state, events }, { type: 'audio' as const }),\n // @ts-expect-error - EventStream type variance\n resolveTrack({ state, events }, { type: 'text' as const }),\n\n // 3.5. Calculate presentation duration from resolved tracks\n calculatePresentationDuration({ state }),\n\n // 4. Setup MediaSource (when presentation loaded)\n setupMediaSource({ state, owners }),\n\n // 4.5. Update MediaSource duration (when presentation duration available)\n updateDuration({ state, owners }),\n\n // 5. Setup SourceBuffers (when MediaSource ready and all selected tracks resolved)\n // Both SourceBuffers are created in a single synchronous operation to guarantee\n // neither is visible to loadSegments before the other exists — preventing the\n // Firefox bug where appending video data before audio SB is created causes\n // mozHasAudio to be permanently false.\n setupSourceBuffers({ state, owners }),\n\n // 5.5. Track currentTime from mediaElement (feeds forward buffer management)\n //\n // NOTE: SourceBufferActor wiring is intentionally absent here in Phase 1.\n //\n // Attempting to wire actors into the engine at this layer revealed a\n // brittleness in the current architecture: every patch to `owners` —\n // regardless of which field changed — wakes up ALL combineLatest subscribers\n // (loadSegments, endOfStream, etc.). Storing actor references in owners\n // caused loadSegments to re-evaluate mid-task, store a spurious pending\n // state, and run a second loading cycle on completion.\n //\n // This means any future feature that needs to introduce new reactive state\n // alongside SourceBuffers faces the same hazard. The right fix is for\n // loadSegments (and other orchestrations) to route their MSE operations\n // through the actor directly, at which point the actor lifecycle is\n // co-located with its consumer rather than managed centrally here.\n //\n // Actor wiring will be introduced in Phase 2 as part of the loadSegments\n // refactor. See .claude/plans/spf/buffer-state-shadow-actual-model.md.\n trackCurrentTime({ state, owners }),\n\n // 5.75. ABR quality switching (reacts to bandwidth samples from loadSegments)\n switchQuality({ state }),\n\n // 6. Load segments (when SourceBuffer ready and track resolved)\n loadSegments({ state, owners }, { type: 'video' }),\n loadSegments({ state, owners }, { type: 'audio' }),\n\n // 6.5. Signal end of stream when all segments loaded\n endOfStream({ state, owners }),\n\n // 7. Setup text tracks (when mediaElement and presentation ready)\n setupTextTracks({ state, owners }),\n\n // 8. Sync text track modes (when track selected and track elements created)\n syncTextTrackModes({ state, owners }),\n\n // 8.5. Bridge DOM text track mode changes → selectedTextTrackId\n // Detects when external code (e.g. captions button via toggleSubtitles())\n // sets a subtitle/caption track to 'showing' and reflects that into SPF\n // state, which in turn drives loadTextTrackCues.\n syncSelectedTextTrackFromDom({ state, owners }),\n\n // 9. Load text track cues (when track resolved and mode set)\n loadTextTrackCues({ state, owners }),\n ];\n\n // Dispatch synthetic initialize event to satisfy combineLatest\n // (combineLatest waits for all sources to emit before triggering listeners)\n events.dispatch({ type: '@@INITIALIZE@@' });\n\n // Return engine instance\n return {\n state,\n owners,\n events,\n destroy: () => {\n cleanups.forEach((cleanup) => cleanup());\n destroyVttParser();\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8LA,SAAgB,qBAAqB,SAA+B,EAAE,EAAkB;CAEtF,MAAM,QAAQ,YAAiC,EAC7C,gBAAgB;EACd,cAAc;EACd,iBAAiB;EACjB,cAAc;EACd,iBAAiB;EACjB,cAAc;EACf,EACF,CAAC;CACF,MAAM,SAAS,YAAkC,EAAE,CAAC;CAGpD,MAAM,SAAS,mBAAyC;CAMxD,MAAM,WAAW;EAKf,qBAAqB,OAAO,OAAO;EAKnC,uBAAuB;GAAE;GAAO;GAAQ;GAAQ,CAAC;EAIjD,oBAAoB;GAAE;GAAO;GAAQ,CAAC;EAGtC,iBAEE;GAAE;GAAO;GAAQ;GAAQ,EACzB;GACE,MAAM;GACN,GAAI,OAAO,qBAAqB,UAAa,EAAE,kBAAkB,OAAO,kBAAkB;GAC3F,CACF;EACD,iBAEE;GAAE;GAAO;GAAQ;GAAQ,EACzB;GACE,MAAM;GACN,GAAI,OAAO,2BAA2B,UAAa,EAAE,wBAAwB,OAAO,wBAAwB;GAC7G,CACF;EACD,gBAEE;GAAE;GAAO;GAAQ;GAAQ,EACzB;GACE,MAAM;GACN,GAAI,OAAO,8BAA8B,UAAa,EACpD,2BAA2B,OAAO,2BACnC;GACD,GAAI,OAAO,wBAAwB,UAAa,EAAE,qBAAqB,OAAO,qBAAqB;GACnG,GAAI,OAAO,uBAAuB,UAAa,EAAE,oBAAoB,OAAO,oBAAoB;GACjG,CACF;EAID,aAAa;GAAE;GAAO;GAAQ,EAAE,EAAE,MAAM,SAAkB,CAAC;EAE3D,aAAa;GAAE;GAAO;GAAQ,EAAE,EAAE,MAAM,SAAkB,CAAC;EAE3D,aAAa;GAAE;GAAO;GAAQ,EAAE,EAAE,MAAM,QAAiB,CAAC;EAG1D,8BAA8B,EAAE,OAAO,CAAC;EAGxC,iBAAiB;GAAE;GAAO;GAAQ,CAAC;EAGnC,eAAe;GAAE;GAAO;GAAQ,CAAC;EAOjC,mBAAmB;GAAE;GAAO;GAAQ,CAAC;EAqBrC,iBAAiB;GAAE;GAAO;GAAQ,CAAC;EAGnC,cAAc,EAAE,OAAO,CAAC;EAGxB,aAAa;GAAE;GAAO;GAAQ,EAAE,EAAE,MAAM,SAAS,CAAC;EAClD,aAAa;GAAE;GAAO;GAAQ,EAAE,EAAE,MAAM,SAAS,CAAC;EAGlD,YAAY;GAAE;GAAO;GAAQ,CAAC;EAG9B,gBAAgB;GAAE;GAAO;GAAQ,CAAC;EAGlC,mBAAmB;GAAE;GAAO;GAAQ,CAAC;EAMrC,6BAA6B;GAAE;GAAO;GAAQ,CAAC;EAG/C,kBAAkB;GAAE;GAAO;GAAQ,CAAC;EACrC;AAID,QAAO,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAG3C,QAAO;EACL;EACA;EACA;EACA,eAAe;AACb,YAAS,SAAS,YAAY,SAAS,CAAC;AACxC,qBAAkB;;EAErB"}
@@ -0,0 +1,4 @@
1
+ import { createPlaybackEngine } from "./playback-engine/engine.js";
2
+ import { SpfMedia } from "./playback-engine/adapter.js";
3
+
4
+ export { SpfMedia, createPlaybackEngine };
@@ -0,0 +1,56 @@
1
+ //#region src/dom/text/parse-vtt-segment.ts
2
+ /**
3
+ * Parse a VTT segment using browser's native parser.
4
+ *
5
+ * Creates a dummy video element with a track element to leverage
6
+ * the browser's optimized VTT parsing. Returns parsed VTTCue objects.
7
+ */
8
+ let dummyVideo = null;
9
+ function ensureDummyVideo() {
10
+ if (!dummyVideo) {
11
+ dummyVideo = document.createElement("video");
12
+ dummyVideo.muted = true;
13
+ dummyVideo.preload = "none";
14
+ dummyVideo.style.display = "none";
15
+ dummyVideo.crossOrigin = "anonymous";
16
+ }
17
+ return dummyVideo;
18
+ }
19
+ function parseVttSegment(url) {
20
+ const video = ensureDummyVideo();
21
+ const track = document.createElement("track");
22
+ track.kind = "subtitles";
23
+ track.default = true;
24
+ return new Promise((resolve, reject) => {
25
+ const onLoad = () => {
26
+ const cues = [];
27
+ const textTrack = track.track;
28
+ if (textTrack.cues) for (let i = 0; i < textTrack.cues.length; i++) {
29
+ const cue = textTrack.cues[i];
30
+ if (cue) cues.push(cue);
31
+ }
32
+ cleanup();
33
+ resolve(cues);
34
+ };
35
+ const onError = () => {
36
+ cleanup();
37
+ reject(/* @__PURE__ */ new Error(`Failed to load VTT segment: ${url}`));
38
+ };
39
+ const cleanup = () => {
40
+ track.removeEventListener("load", onLoad);
41
+ track.removeEventListener("error", onError);
42
+ video.removeChild(track);
43
+ };
44
+ track.addEventListener("load", onLoad);
45
+ track.addEventListener("error", onError);
46
+ video.appendChild(track);
47
+ track.src = url;
48
+ });
49
+ }
50
+ function destroyVttParser() {
51
+ dummyVideo = null;
52
+ }
53
+
54
+ //#endregion
55
+ export { destroyVttParser, parseVttSegment };
56
+ //# sourceMappingURL=parse-vtt-segment.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse-vtt-segment.js","names":[],"sources":["../../../../src/dom/text/parse-vtt-segment.ts"],"sourcesContent":["/**\n * Parse a VTT segment using browser's native parser.\n *\n * Creates a dummy video element with a track element to leverage\n * the browser's optimized VTT parsing. Returns parsed VTTCue objects.\n */\n\n// Singleton dummy video (reused across all parsing)\nlet dummyVideo: HTMLVideoElement | null = null;\n\nfunction ensureDummyVideo(): HTMLVideoElement {\n if (!dummyVideo) {\n dummyVideo = document.createElement('video');\n dummyVideo.muted = true;\n dummyVideo.preload = 'none';\n dummyVideo.style.display = 'none';\n dummyVideo.crossOrigin = 'anonymous';\n }\n return dummyVideo;\n}\n\nexport function parseVttSegment(url: string): Promise<VTTCue[]> {\n const video = ensureDummyVideo();\n const track = document.createElement('track');\n track.kind = 'subtitles';\n track.default = true;\n\n return new Promise((resolve, reject) => {\n const onLoad = (): void => {\n const cues: VTTCue[] = [];\n const textTrack = track.track;\n\n if (textTrack.cues) {\n for (let i = 0; i < textTrack.cues.length; i++) {\n const cue = textTrack.cues[i];\n if (cue) {\n cues.push(cue as VTTCue);\n }\n }\n }\n\n cleanup();\n resolve(cues);\n };\n\n const onError = (): void => {\n cleanup();\n reject(new Error(`Failed to load VTT segment: ${url}`));\n };\n\n const cleanup = (): void => {\n track.removeEventListener('load', onLoad);\n track.removeEventListener('error', onError);\n video.removeChild(track);\n };\n\n track.addEventListener('load', onLoad);\n track.addEventListener('error', onError);\n video.appendChild(track);\n track.src = url;\n });\n}\n\nexport function destroyVttParser(): void {\n dummyVideo = null;\n}\n"],"mappings":";;;;;;;AAQA,IAAI,aAAsC;AAE1C,SAAS,mBAAqC;AAC5C,KAAI,CAAC,YAAY;AACf,eAAa,SAAS,cAAc,QAAQ;AAC5C,aAAW,QAAQ;AACnB,aAAW,UAAU;AACrB,aAAW,MAAM,UAAU;AAC3B,aAAW,cAAc;;AAE3B,QAAO;;AAGT,SAAgB,gBAAgB,KAAgC;CAC9D,MAAM,QAAQ,kBAAkB;CAChC,MAAM,QAAQ,SAAS,cAAc,QAAQ;AAC7C,OAAM,OAAO;AACb,OAAM,UAAU;AAEhB,QAAO,IAAI,SAAS,SAAS,WAAW;EACtC,MAAM,eAAqB;GACzB,MAAM,OAAiB,EAAE;GACzB,MAAM,YAAY,MAAM;AAExB,OAAI,UAAU,KACZ,MAAK,IAAI,IAAI,GAAG,IAAI,UAAU,KAAK,QAAQ,KAAK;IAC9C,MAAM,MAAM,UAAU,KAAK;AAC3B,QAAI,IACF,MAAK,KAAK,IAAc;;AAK9B,YAAS;AACT,WAAQ,KAAK;;EAGf,MAAM,gBAAsB;AAC1B,YAAS;AACT,0BAAO,IAAI,MAAM,+BAA+B,MAAM,CAAC;;EAGzD,MAAM,gBAAsB;AAC1B,SAAM,oBAAoB,QAAQ,OAAO;AACzC,SAAM,oBAAoB,SAAS,QAAQ;AAC3C,SAAM,YAAY,MAAM;;AAG1B,QAAM,iBAAiB,QAAQ,OAAO;AACtC,QAAM,iBAAiB,SAAS,QAAQ;AACxC,QAAM,YAAY,MAAM;AACxB,QAAM,MAAM;GACZ;;AAGJ,SAAgB,mBAAyB;AACvC,cAAa"}
@@ -0,0 +1,11 @@
1
+ import { loadSegments } from "./dom/features/load-segments.js";
2
+ import { destroyVttParser, parseVttSegment } from "./dom/text/parse-vtt-segment.js";
3
+ import { loadTextTrackCues } from "./dom/features/load-text-track-cues.js";
4
+ import { trackCurrentTime } from "./dom/features/track-current-time.js";
5
+ import { trackPlaybackInitiated } from "./dom/features/track-playback-initiated.js";
6
+ import { trackPlaybackRate } from "./dom/features/track-playback-rate.js";
7
+ import { appendSegment } from "./dom/media/append-segment.js";
8
+ import { flushBuffer } from "./dom/media/buffer-flusher.js";
9
+ import { SpfMedia } from "./dom/playback-engine/adapter.js";
10
+
11
+ export { SpfMedia, appendSegment, destroyVttParser, flushBuffer, loadSegments, loadTextTrackCues, parseVttSegment, trackCurrentTime, trackPlaybackInitiated, trackPlaybackRate };
@@ -10,5 +10,5 @@
10
10
  const VERSION = "0.1.0";
11
11
 
12
12
  //#endregion
13
- export { VERSION as t };
14
- //# sourceMappingURL=src-DRsS_H80.js.map
13
+ export { VERSION };
14
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/index.ts"],"sourcesContent":["/**\n * Stream Processing Framework (SPF) for Video.js 10\n *\n * Bundle size target: < 20KB (minified + gzipped)\n * Primary use case: CMAF HLS playback with Video.js v10\n *\n * @packageDocumentation\n */\n\n// Public API exports will be added here as features are implemented\n\nexport const VERSION = '0.1.0';\n"],"mappings":";;;;;;;;;AAWA,MAAa,UAAU"}
@@ -0,0 +1,16 @@
1
+ import { VERSION } from "./index.js";
2
+ import { createState } from "./core/state/create-state.js";
3
+ import { resolveUrl } from "./core/hls/resolve-url.js";
4
+ import { parseMediaPlaylist } from "./core/hls/parse-media-playlist.js";
5
+ import { parseMultivariantPlaylist } from "./core/hls/parse-multivariant.js";
6
+ import { DEFAULT_BANDWIDTH_CONFIG, getBandwidthEstimate, sampleBandwidth } from "./core/abr/bandwidth-estimator.js";
7
+ import { DEFAULT_QUALITY_CONFIG, selectQuality } from "./core/abr/quality-selection.js";
8
+ import { DEFAULT_BACK_BUFFER_CONFIG, calculateBackBufferFlushPoint } from "./core/buffer/back-buffer.js";
9
+ import { DEFAULT_FORWARD_BUFFER_CONFIG, getSegmentsToLoad } from "./core/buffer/forward-buffer.js";
10
+ import { hasPresentationDuration, isResolvedTrack } from "./core/types/index.js";
11
+ import { attachMediaSource, createMediaSource, createSourceBuffer, isCodecSupported, supportsManagedMediaSource, supportsMediaSource, waitForSourceOpen } from "./dom/media/mediasource-setup.js";
12
+ import { fetchResolvable, getResponseText } from "./dom/network/fetch.js";
13
+ import { createEventStream } from "./core/events/create-event-stream.js";
14
+ import { combineLatest } from "./core/reactive/combine-latest.js";
15
+ import { canResolve, isUnresolved, resolvePresentation, shouldResolve, syncPreloadAttribute } from "./core/features/resolve-presentation.js";
16
+ import { DEFAULT_SWITCHING_CONFIG, switchQuality } from "./core/features/quality-switching.js";
@@ -0,0 +1,35 @@
1
+ //#region src/core/abr/bandwidth-estimator.d.ts
2
+ /**
3
+ * Dual EWMA Bandwidth Estimator
4
+ *
5
+ * Estimates available bandwidth using two EWMA calculations with different
6
+ * half-lives, taking the minimum of both. This approach (from Shaka Player):
7
+ *
8
+ * - **Fast EWMA** (2s half-life): Reacts quickly to bandwidth drops
9
+ * - **Slow EWMA** (5s half-life): Provides stability during fluctuations
10
+ * - **min(fast, slow)**: Adapts down quickly, up slowly
11
+ *
12
+ * This naturally provides asymmetric behavior needed for good QoE:
13
+ * avoiding stalls (quick downgrade) while preventing oscillation (slow upgrade).
14
+ */
15
+ /**
16
+ * Bandwidth estimator state.
17
+ *
18
+ * This state structure will be managed by O1 (State Container).
19
+ * Functions in this module operate on this state immutably.
20
+ */
21
+ interface BandwidthState {
22
+ /** Fast-moving EWMA estimate (raw, uncorrected). */
23
+ fastEstimate: number;
24
+ /** Total weight accumulated in fast EWMA. */
25
+ fastTotalWeight: number;
26
+ /** Slow-moving EWMA estimate (raw, uncorrected). */
27
+ slowEstimate: number;
28
+ /** Total weight accumulated in slow EWMA. */
29
+ slowTotalWeight: number;
30
+ /** Total bytes sampled across all valid samples. */
31
+ bytesSampled: number;
32
+ }
33
+ //#endregion
34
+ export { BandwidthState };
35
+ //# sourceMappingURL=bandwidth-estimator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bandwidth-estimator.d.ts","names":[],"sources":["../../../../src/core/abr/bandwidth-estimator.ts"],"mappings":";;AAsBA;;;;;;;;;;;;;;;;;;UAAiB,cAAA;;EAEf,YAAA;;EAEA,eAAA;;EAEA,YAAA;;EAEA,eAAA;;EAEA,YAAA;AAAA"}
@@ -0,0 +1,96 @@
1
+ import { applyZeroFactor, calculateEwma } from "./ewma.js";
2
+
3
+ //#region src/core/abr/bandwidth-estimator.ts
4
+ /**
5
+ * Dual EWMA Bandwidth Estimator
6
+ *
7
+ * Estimates available bandwidth using two EWMA calculations with different
8
+ * half-lives, taking the minimum of both. This approach (from Shaka Player):
9
+ *
10
+ * - **Fast EWMA** (2s half-life): Reacts quickly to bandwidth drops
11
+ * - **Slow EWMA** (5s half-life): Provides stability during fluctuations
12
+ * - **min(fast, slow)**: Adapts down quickly, up slowly
13
+ *
14
+ * This naturally provides asymmetric behavior needed for good QoE:
15
+ * avoiding stalls (quick downgrade) while preventing oscillation (slow upgrade).
16
+ */
17
+ /**
18
+ * Default bandwidth estimator configuration.
19
+ *
20
+ * Values match Shaka Player defaults based on experimentation.
21
+ */
22
+ const DEFAULT_BANDWIDTH_CONFIG = {
23
+ fastHalfLife: 2,
24
+ slowHalfLife: 5,
25
+ minTotalBytes: 128e3,
26
+ minBytes: 16e3,
27
+ minDuration: 5
28
+ };
29
+ /**
30
+ * Add a bandwidth sample from a segment download.
31
+ *
32
+ * Samples are filtered based on:
33
+ * - Minimum bytes (filters TTFB-dominated small segments)
34
+ * - Minimum duration (filters cached responses)
35
+ *
36
+ * Valid samples update both fast and slow EWMA estimates.
37
+ *
38
+ * @param state - Current estimator state
39
+ * @param durationMs - Download duration in milliseconds
40
+ * @param numBytes - Number of bytes downloaded
41
+ * @param config - Optional estimator configuration (uses defaults if not provided)
42
+ * @returns New estimator state with sample incorporated (or unchanged if filtered)
43
+ *
44
+ * @example
45
+ * let state = { fastEstimate: 0, fastTotalWeight: 0, ... };
46
+ * // Sample: 1MB in 1 second
47
+ * state = sampleBandwidth(state, 1000, 1_000_000);
48
+ */
49
+ function sampleBandwidth(state, durationMs, numBytes, config = DEFAULT_BANDWIDTH_CONFIG) {
50
+ const updatedBytesSampled = state.bytesSampled + numBytes;
51
+ if (numBytes < config.minBytes) return {
52
+ ...state,
53
+ bytesSampled: updatedBytesSampled
54
+ };
55
+ if (durationMs < config.minDuration) return {
56
+ ...state,
57
+ bytesSampled: updatedBytesSampled
58
+ };
59
+ const bandwidth = 8e3 * numBytes / durationMs;
60
+ const weight = durationMs / 1e3;
61
+ return {
62
+ fastEstimate: calculateEwma(state.fastEstimate, bandwidth, weight, config.fastHalfLife),
63
+ fastTotalWeight: state.fastTotalWeight + weight,
64
+ slowEstimate: calculateEwma(state.slowEstimate, bandwidth, weight, config.slowHalfLife),
65
+ slowTotalWeight: state.slowTotalWeight + weight,
66
+ bytesSampled: updatedBytesSampled
67
+ };
68
+ }
69
+ /**
70
+ * Get the current bandwidth estimate.
71
+ *
72
+ * Returns the **minimum** of the fast and slow EWMA estimates.
73
+ * This provides the key asymmetric behavior:
74
+ * - When bandwidth drops, fast EWMA reacts first and dominates (quick adaptation)
75
+ * - When bandwidth rises, slow EWMA lags behind and dominates (slow adaptation)
76
+ *
77
+ * Uses default estimate until enough data has been sampled.
78
+ *
79
+ * @param state - Current estimator state
80
+ * @param defaultEstimate - Fallback estimate before sufficient samples (bps)
81
+ * @param config - Optional estimator configuration (uses defaults if not provided)
82
+ * @returns Bandwidth estimate in bits per second
83
+ *
84
+ * @example
85
+ * const estimate = getBandwidthEstimate(state, 5_000_000); // 5 Mbps default
86
+ */
87
+ function getBandwidthEstimate(state, defaultEstimate, config = DEFAULT_BANDWIDTH_CONFIG) {
88
+ if (state.bytesSampled < config.minTotalBytes) return defaultEstimate;
89
+ const fastEstimate = applyZeroFactor(state.fastEstimate, state.fastTotalWeight, config.fastHalfLife);
90
+ const slowEstimate = applyZeroFactor(state.slowEstimate, state.slowTotalWeight, config.slowHalfLife);
91
+ return Math.min(fastEstimate, slowEstimate);
92
+ }
93
+
94
+ //#endregion
95
+ export { DEFAULT_BANDWIDTH_CONFIG, getBandwidthEstimate, sampleBandwidth };
96
+ //# sourceMappingURL=bandwidth-estimator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bandwidth-estimator.js","names":[],"sources":["../../../../src/core/abr/bandwidth-estimator.ts"],"sourcesContent":["/**\n * Dual EWMA Bandwidth Estimator\n *\n * Estimates available bandwidth using two EWMA calculations with different\n * half-lives, taking the minimum of both. This approach (from Shaka Player):\n *\n * - **Fast EWMA** (2s half-life): Reacts quickly to bandwidth drops\n * - **Slow EWMA** (5s half-life): Provides stability during fluctuations\n * - **min(fast, slow)**: Adapts down quickly, up slowly\n *\n * This naturally provides asymmetric behavior needed for good QoE:\n * avoiding stalls (quick downgrade) while preventing oscillation (slow upgrade).\n */\n\nimport { applyZeroFactor, calculateEwma } from './ewma';\n\n/**\n * Bandwidth estimator state.\n *\n * This state structure will be managed by O1 (State Container).\n * Functions in this module operate on this state immutably.\n */\nexport interface BandwidthState {\n /** Fast-moving EWMA estimate (raw, uncorrected). */\n fastEstimate: number;\n /** Total weight accumulated in fast EWMA. */\n fastTotalWeight: number;\n /** Slow-moving EWMA estimate (raw, uncorrected). */\n slowEstimate: number;\n /** Total weight accumulated in slow EWMA. */\n slowTotalWeight: number;\n /** Total bytes sampled across all valid samples. */\n bytesSampled: number;\n}\n\n/**\n * Configuration for bandwidth estimation.\n */\nexport interface BandwidthConfig {\n /** Half-life for fast EWMA in seconds. */\n fastHalfLife: number;\n /** Half-life for slow EWMA in seconds. */\n slowHalfLife: number;\n /** Minimum total bytes before trusting the estimate. */\n minTotalBytes: number;\n /** Minimum bytes per sample to count (filters TTFB-dominated samples). */\n minBytes: number;\n /** Minimum sample duration in ms (filters cached responses). */\n minDuration: number;\n}\n\n/**\n * Default bandwidth estimator configuration.\n *\n * Values match Shaka Player defaults based on experimentation.\n */\nexport const DEFAULT_BANDWIDTH_CONFIG: BandwidthConfig = {\n fastHalfLife: 2, // 2 seconds\n slowHalfLife: 5, // 5 seconds\n minTotalBytes: 128_000, // 128 KB\n minBytes: 16_000, // 16 KB\n minDuration: 5, // 5 ms\n};\n\n/**\n * Add a bandwidth sample from a segment download.\n *\n * Samples are filtered based on:\n * - Minimum bytes (filters TTFB-dominated small segments)\n * - Minimum duration (filters cached responses)\n *\n * Valid samples update both fast and slow EWMA estimates.\n *\n * @param state - Current estimator state\n * @param durationMs - Download duration in milliseconds\n * @param numBytes - Number of bytes downloaded\n * @param config - Optional estimator configuration (uses defaults if not provided)\n * @returns New estimator state with sample incorporated (or unchanged if filtered)\n *\n * @example\n * let state = { fastEstimate: 0, fastTotalWeight: 0, ... };\n * // Sample: 1MB in 1 second\n * state = sampleBandwidth(state, 1000, 1_000_000);\n */\nexport function sampleBandwidth(\n state: BandwidthState,\n durationMs: number,\n numBytes: number,\n config: BandwidthConfig = DEFAULT_BANDWIDTH_CONFIG\n): BandwidthState {\n // Always track bytes for startup phase calculation\n const updatedBytesSampled = state.bytesSampled + numBytes;\n\n // Filter: Ignore samples below minimum bytes (TTFB-dominated)\n // Small segments' download time is mostly connection setup latency,\n // which would artificially lower our bandwidth estimate\n if (numBytes < config.minBytes) {\n return { ...state, bytesSampled: updatedBytesSampled };\n }\n\n // Filter: Ignore samples faster than minimum duration (cached responses)\n // Cached responses load nearly instantly and would artificially inflate\n // our bandwidth estimate\n if (durationMs < config.minDuration) {\n return { ...state, bytesSampled: updatedBytesSampled };\n }\n\n // Calculate bandwidth in bits per second\n // Formula: (bytes * 8 bits/byte * 1000 ms/s) / ms = bits/s\n const bandwidth = (8000 * numBytes) / durationMs;\n\n // Weight by duration in seconds\n // Longer downloads are more reliable indicators of true bandwidth\n const weight = durationMs / 1000;\n\n return {\n fastEstimate: calculateEwma(state.fastEstimate, bandwidth, weight, config.fastHalfLife),\n fastTotalWeight: state.fastTotalWeight + weight,\n slowEstimate: calculateEwma(state.slowEstimate, bandwidth, weight, config.slowHalfLife),\n slowTotalWeight: state.slowTotalWeight + weight,\n bytesSampled: updatedBytesSampled,\n };\n}\n\n/**\n * Get the current bandwidth estimate.\n *\n * Returns the **minimum** of the fast and slow EWMA estimates.\n * This provides the key asymmetric behavior:\n * - When bandwidth drops, fast EWMA reacts first and dominates (quick adaptation)\n * - When bandwidth rises, slow EWMA lags behind and dominates (slow adaptation)\n *\n * Uses default estimate until enough data has been sampled.\n *\n * @param state - Current estimator state\n * @param defaultEstimate - Fallback estimate before sufficient samples (bps)\n * @param config - Optional estimator configuration (uses defaults if not provided)\n * @returns Bandwidth estimate in bits per second\n *\n * @example\n * const estimate = getBandwidthEstimate(state, 5_000_000); // 5 Mbps default\n */\nexport function getBandwidthEstimate(\n state: BandwidthState,\n defaultEstimate: number,\n config: BandwidthConfig = DEFAULT_BANDWIDTH_CONFIG\n): number {\n // Use default until we have enough samples to trust our estimate\n if (state.bytesSampled < config.minTotalBytes) {\n return defaultEstimate;\n }\n\n // Apply zero-factor correction to both estimates\n const fastEstimate = applyZeroFactor(state.fastEstimate, state.fastTotalWeight, config.fastHalfLife);\n\n const slowEstimate = applyZeroFactor(state.slowEstimate, state.slowTotalWeight, config.slowHalfLife);\n\n // Take the minimum - this is the key insight from Shaka Player\n // It naturally provides \"down quickly, up slowly\" behavior\n return Math.min(fastEstimate, slowEstimate);\n}\n\n/**\n * Check if the estimator has enough data to provide a reliable estimate.\n *\n * Requires both:\n * - Enough total bytes sampled (minTotalBytes threshold)\n * - At least one valid EWMA sample (totalWeight > 0)\n *\n * @param state - Current estimator state\n * @param config - Optional estimator configuration (uses defaults if not provided)\n * @returns True if we've sampled enough bytes to trust the estimate\n *\n * @example\n * if (hasGoodEstimate(state)) {\n * const estimate = getBandwidthEstimate(state, 5_000_000);\n * }\n */\nexport function hasGoodEstimate(state: BandwidthState, config: BandwidthConfig = DEFAULT_BANDWIDTH_CONFIG): boolean {\n // Need enough total bytes AND at least one valid EWMA sample\n return state.bytesSampled >= config.minTotalBytes && state.fastTotalWeight > 0 && state.slowTotalWeight > 0;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAwDA,MAAa,2BAA4C;CACvD,cAAc;CACd,cAAc;CACd,eAAe;CACf,UAAU;CACV,aAAa;CACd;;;;;;;;;;;;;;;;;;;;;AAsBD,SAAgB,gBACd,OACA,YACA,UACA,SAA0B,0BACV;CAEhB,MAAM,sBAAsB,MAAM,eAAe;AAKjD,KAAI,WAAW,OAAO,SACpB,QAAO;EAAE,GAAG;EAAO,cAAc;EAAqB;AAMxD,KAAI,aAAa,OAAO,YACtB,QAAO;EAAE,GAAG;EAAO,cAAc;EAAqB;CAKxD,MAAM,YAAa,MAAO,WAAY;CAItC,MAAM,SAAS,aAAa;AAE5B,QAAO;EACL,cAAc,cAAc,MAAM,cAAc,WAAW,QAAQ,OAAO,aAAa;EACvF,iBAAiB,MAAM,kBAAkB;EACzC,cAAc,cAAc,MAAM,cAAc,WAAW,QAAQ,OAAO,aAAa;EACvF,iBAAiB,MAAM,kBAAkB;EACzC,cAAc;EACf;;;;;;;;;;;;;;;;;;;;AAqBH,SAAgB,qBACd,OACA,iBACA,SAA0B,0BAClB;AAER,KAAI,MAAM,eAAe,OAAO,cAC9B,QAAO;CAIT,MAAM,eAAe,gBAAgB,MAAM,cAAc,MAAM,iBAAiB,OAAO,aAAa;CAEpG,MAAM,eAAe,gBAAgB,MAAM,cAAc,MAAM,iBAAiB,OAAO,aAAa;AAIpG,QAAO,KAAK,IAAI,cAAc,aAAa"}
@@ -0,0 +1,71 @@
1
+ //#region src/core/abr/ewma.ts
2
+ /**
3
+ * Exponentially Weighted Moving Average (EWMA)
4
+ *
5
+ * Pure functional implementation of EWMA calculations.
6
+ * Based on Shaka Player's EWMA algorithm.
7
+ */
8
+ /**
9
+ * Calculate alpha (decay factor) from half-life.
10
+ *
11
+ * Alpha determines how quickly old data "expires":
12
+ * - alpha close to 1 = slow decay (long memory)
13
+ * - alpha close to 0 = fast decay (short memory)
14
+ *
15
+ * @param halfLife - The quantity of prior samples (by weight) that make up
16
+ * half of the new estimate. Must be positive.
17
+ * @returns Alpha value between 0 and 1
18
+ *
19
+ * @example
20
+ * const alpha = calculateAlpha(2); // ≈ 0.7071 for 2-second half-life
21
+ */
22
+ function calculateAlpha(halfLife) {
23
+ return Math.exp(Math.log(.5) / halfLife);
24
+ }
25
+ /**
26
+ * Calculate exponentially weighted moving average.
27
+ *
28
+ * Updates an estimate by blending a new value with the previous estimate,
29
+ * weighted by the sample duration. Longer samples have more influence.
30
+ *
31
+ * @param prevEstimate - Previous EWMA estimate
32
+ * @param value - New sample value to incorporate
33
+ * @param weight - Sample weight (typically duration in seconds)
34
+ * @param halfLife - Half-life for decay (typically 2-5 seconds)
35
+ * @returns Updated EWMA estimate
36
+ *
37
+ * @example
38
+ * let estimate = 0;
39
+ * estimate = calculateEwma(estimate, 1_000_000, 1, 2); // First sample
40
+ * estimate = calculateEwma(estimate, 2_000_000, 1, 2); // Second sample
41
+ */
42
+ function calculateEwma(prevEstimate, value, weight, halfLife) {
43
+ const adjAlpha = calculateAlpha(halfLife) ** weight;
44
+ return value * (1 - adjAlpha) + adjAlpha * prevEstimate;
45
+ }
46
+ /**
47
+ * Apply zero-factor correction to EWMA estimate.
48
+ *
49
+ * The zero-factor correction compensates for bias when starting from zero.
50
+ * Without this correction, early estimates would be artificially low.
51
+ *
52
+ * As totalWeight increases, the correction factor approaches 1, meaning
53
+ * the estimate becomes more reliable and needs less correction.
54
+ *
55
+ * @param estimate - Raw EWMA estimate (uncorrected)
56
+ * @param totalWeight - Accumulated weight from all samples
57
+ * @param halfLife - Half-life used in EWMA calculation
58
+ * @returns Corrected estimate, or 0 if totalWeight is 0
59
+ *
60
+ * @example
61
+ * const raw = calculateEwma(0, 1_000_000, 1, 2);
62
+ * const corrected = applyZeroFactor(raw, 1, 2); // ≈ 1_000_000
63
+ */
64
+ function applyZeroFactor(estimate, totalWeight, halfLife) {
65
+ if (totalWeight === 0) return 0;
66
+ return estimate / (1 - calculateAlpha(halfLife) ** totalWeight);
67
+ }
68
+
69
+ //#endregion
70
+ export { applyZeroFactor, calculateEwma };
71
+ //# sourceMappingURL=ewma.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ewma.js","names":[],"sources":["../../../../src/core/abr/ewma.ts"],"sourcesContent":["/**\n * Exponentially Weighted Moving Average (EWMA)\n *\n * Pure functional implementation of EWMA calculations.\n * Based on Shaka Player's EWMA algorithm.\n */\n\n/**\n * Calculate alpha (decay factor) from half-life.\n *\n * Alpha determines how quickly old data \"expires\":\n * - alpha close to 1 = slow decay (long memory)\n * - alpha close to 0 = fast decay (short memory)\n *\n * @param halfLife - The quantity of prior samples (by weight) that make up\n * half of the new estimate. Must be positive.\n * @returns Alpha value between 0 and 1\n *\n * @example\n * const alpha = calculateAlpha(2); // ≈ 0.7071 for 2-second half-life\n */\nexport function calculateAlpha(halfLife: number): number {\n return Math.exp(Math.log(0.5) / halfLife);\n}\n\n/**\n * Calculate exponentially weighted moving average.\n *\n * Updates an estimate by blending a new value with the previous estimate,\n * weighted by the sample duration. Longer samples have more influence.\n *\n * @param prevEstimate - Previous EWMA estimate\n * @param value - New sample value to incorporate\n * @param weight - Sample weight (typically duration in seconds)\n * @param halfLife - Half-life for decay (typically 2-5 seconds)\n * @returns Updated EWMA estimate\n *\n * @example\n * let estimate = 0;\n * estimate = calculateEwma(estimate, 1_000_000, 1, 2); // First sample\n * estimate = calculateEwma(estimate, 2_000_000, 1, 2); // Second sample\n */\nexport function calculateEwma(prevEstimate: number, value: number, weight: number, halfLife: number): number {\n const alpha = calculateAlpha(halfLife);\n const adjAlpha = alpha ** weight;\n return value * (1 - adjAlpha) + adjAlpha * prevEstimate;\n}\n\n/**\n * Apply zero-factor correction to EWMA estimate.\n *\n * The zero-factor correction compensates for bias when starting from zero.\n * Without this correction, early estimates would be artificially low.\n *\n * As totalWeight increases, the correction factor approaches 1, meaning\n * the estimate becomes more reliable and needs less correction.\n *\n * @param estimate - Raw EWMA estimate (uncorrected)\n * @param totalWeight - Accumulated weight from all samples\n * @param halfLife - Half-life used in EWMA calculation\n * @returns Corrected estimate, or 0 if totalWeight is 0\n *\n * @example\n * const raw = calculateEwma(0, 1_000_000, 1, 2);\n * const corrected = applyZeroFactor(raw, 1, 2); // ≈ 1_000_000\n */\nexport function applyZeroFactor(estimate: number, totalWeight: number, halfLife: number): number {\n if (totalWeight === 0) {\n return 0;\n }\n\n const alpha = calculateAlpha(halfLife);\n const zeroFactor = 1 - alpha ** totalWeight;\n return estimate / zeroFactor;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAqBA,SAAgB,eAAe,UAA0B;AACvD,QAAO,KAAK,IAAI,KAAK,IAAI,GAAI,GAAG,SAAS;;;;;;;;;;;;;;;;;;;AAoB3C,SAAgB,cAAc,cAAsB,OAAe,QAAgB,UAA0B;CAE3G,MAAM,WADQ,eAAe,SAAS,IACZ;AAC1B,QAAO,SAAS,IAAI,YAAY,WAAW;;;;;;;;;;;;;;;;;;;;AAqB7C,SAAgB,gBAAgB,UAAkB,aAAqB,UAA0B;AAC/F,KAAI,gBAAgB,EAClB,QAAO;AAKT,QAAO,YADY,IADL,eAAe,SAAS,IACN"}
@@ -0,0 +1,53 @@
1
+ //#region src/core/abr/quality-selection.ts
2
+ /**
3
+ * Default quality selection configuration.
4
+ * Values match Shaka Player upgrade threshold (0.85 = 15% headroom).
5
+ */
6
+ const DEFAULT_QUALITY_CONFIG = { safetyMargin: .85 };
7
+ /**
8
+ * Select the best video track based on current bandwidth estimate.
9
+ *
10
+ * Selects the highest quality track where bandwidth is sufficient with safety margin:
11
+ * - currentBandwidth >= track.bandwidth / safetyMargin
12
+ * - Default safetyMargin 0.85 means track uses ≤85% of bandwidth (15% headroom)
13
+ * - At same bandwidth, prefers higher resolution
14
+ *
15
+ * @param tracks - Available video tracks (can be unsorted)
16
+ * @param currentBandwidth - Current bandwidth estimate in bits per second
17
+ * @param config - Optional quality selection configuration
18
+ * @returns Selected track, or undefined if no tracks available
19
+ *
20
+ * @example
21
+ * const tracks = [
22
+ * { id: '360p', bandwidth: 500_000, ... },
23
+ * { id: '720p', bandwidth: 2_000_000, ... },
24
+ * { id: '1080p', bandwidth: 4_000_000, ... },
25
+ * ];
26
+ *
27
+ * // With 2.5 Mbps, selects 720p (1080p needs 4M/0.85 = 4.7 Mbps)
28
+ * const selected = selectQuality(tracks, 2_500_000);
29
+ */
30
+ function selectQuality(tracks, currentBandwidth, config = DEFAULT_QUALITY_CONFIG) {
31
+ if (tracks.length === 0) return;
32
+ const sortedTracks = tracks.slice().sort((a, b) => a.bandwidth - b.bandwidth);
33
+ let chosen;
34
+ for (const track of sortedTracks) if (currentBandwidth >= track.bandwidth / config.safetyMargin) {
35
+ if (!chosen || track.bandwidth > chosen.bandwidth || track.bandwidth === chosen.bandwidth && hasHigherResolution(track, chosen)) chosen = track;
36
+ }
37
+ return chosen ?? sortedTracks[0];
38
+ }
39
+ /**
40
+ * Check if track A has higher resolution than track B.
41
+ * Compares by total pixel count (width × height).
42
+ *
43
+ * @param trackA - First track to compare
44
+ * @param trackB - Second track to compare
45
+ * @returns True if trackA has more pixels than trackB
46
+ */
47
+ function hasHigherResolution(trackA, trackB) {
48
+ return (trackA.width ?? 0) * (trackA.height ?? 0) > (trackB.width ?? 0) * (trackB.height ?? 0);
49
+ }
50
+
51
+ //#endregion
52
+ export { DEFAULT_QUALITY_CONFIG, selectQuality };
53
+ //# sourceMappingURL=quality-selection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quality-selection.js","names":[],"sources":["../../../../src/core/abr/quality-selection.ts"],"sourcesContent":["/**\n * Quality Selection Algorithm\n *\n * Selects optimal video track based on bandwidth estimate with safety margin.\n * Stateless selection - picks highest quality that fits bandwidth.\n *\n * Key concepts:\n * - **Safety margin** (0.85): Pick track where bandwidth >= track.bandwidth / 0.85\n * - This ensures 15% headroom to avoid buffering\n * - At same bandwidth, prefer higher resolution\n */\n\nimport type { PartiallyResolvedVideoTrack } from '../types';\n\n/**\n * Quality selection configuration.\n */\nexport interface QualityConfig {\n /**\n * Safety margin (0-1).\n * To select a track, need: currentBandwidth >= track.bandwidth / safetyMargin.\n * Default 0.85 means track must use ≤85% of available bandwidth (15% headroom).\n */\n safetyMargin: number;\n}\n\n/**\n * Default quality selection configuration.\n * Values match Shaka Player upgrade threshold (0.85 = 15% headroom).\n */\nexport const DEFAULT_QUALITY_CONFIG: QualityConfig = {\n safetyMargin: 0.85,\n};\n\n/**\n * Select the best video track based on current bandwidth estimate.\n *\n * Selects the highest quality track where bandwidth is sufficient with safety margin:\n * - currentBandwidth >= track.bandwidth / safetyMargin\n * - Default safetyMargin 0.85 means track uses ≤85% of bandwidth (15% headroom)\n * - At same bandwidth, prefers higher resolution\n *\n * @param tracks - Available video tracks (can be unsorted)\n * @param currentBandwidth - Current bandwidth estimate in bits per second\n * @param config - Optional quality selection configuration\n * @returns Selected track, or undefined if no tracks available\n *\n * @example\n * const tracks = [\n * { id: '360p', bandwidth: 500_000, ... },\n * { id: '720p', bandwidth: 2_000_000, ... },\n * { id: '1080p', bandwidth: 4_000_000, ... },\n * ];\n *\n * // With 2.5 Mbps, selects 720p (1080p needs 4M/0.85 = 4.7 Mbps)\n * const selected = selectQuality(tracks, 2_500_000);\n */\nexport function selectQuality(\n tracks: PartiallyResolvedVideoTrack[],\n currentBandwidth: number,\n config: QualityConfig = DEFAULT_QUALITY_CONFIG\n): PartiallyResolvedVideoTrack | undefined {\n if (tracks.length === 0) {\n return undefined;\n }\n\n // Sort tracks by bandwidth (lowest first)\n const sortedTracks = tracks.slice().sort((a, b) => a.bandwidth - b.bandwidth);\n\n // Start with no selection\n let chosen: PartiallyResolvedVideoTrack | undefined;\n\n for (const track of sortedTracks) {\n // Check if we have enough bandwidth for this track with safety margin\n // Required bandwidth = track.bandwidth / safetyMargin\n const requiredBandwidth = track.bandwidth / config.safetyMargin;\n\n if (currentBandwidth >= requiredBandwidth) {\n // We can support this track - prefer it if better than current choice\n if (\n !chosen ||\n track.bandwidth > chosen.bandwidth ||\n (track.bandwidth === chosen.bandwidth && hasHigherResolution(track, chosen))\n ) {\n chosen = track;\n }\n }\n }\n\n // If no track fits with safety margin, fall back to lowest quality\n return chosen ?? sortedTracks[0];\n}\n\n/**\n * Check if track A has higher resolution than track B.\n * Compares by total pixel count (width × height).\n *\n * @param trackA - First track to compare\n * @param trackB - Second track to compare\n * @returns True if trackA has more pixels than trackB\n */\nfunction hasHigherResolution(trackA: PartiallyResolvedVideoTrack, trackB: PartiallyResolvedVideoTrack): boolean {\n const pixelsA = (trackA.width ?? 0) * (trackA.height ?? 0);\n const pixelsB = (trackB.width ?? 0) * (trackB.height ?? 0);\n return pixelsA > pixelsB;\n}\n"],"mappings":";;;;;AA8BA,MAAa,yBAAwC,EACnD,cAAc,KACf;;;;;;;;;;;;;;;;;;;;;;;;AAyBD,SAAgB,cACd,QACA,kBACA,SAAwB,wBACiB;AACzC,KAAI,OAAO,WAAW,EACpB;CAIF,MAAM,eAAe,OAAO,OAAO,CAAC,MAAM,GAAG,MAAM,EAAE,YAAY,EAAE,UAAU;CAG7E,IAAI;AAEJ,MAAK,MAAM,SAAS,aAKlB,KAAI,oBAFsB,MAAM,YAAY,OAAO,cAIjD;MACE,CAAC,UACD,MAAM,YAAY,OAAO,aACxB,MAAM,cAAc,OAAO,aAAa,oBAAoB,OAAO,OAAO,CAE3E,UAAS;;AAMf,QAAO,UAAU,aAAa;;;;;;;;;;AAWhC,SAAS,oBAAoB,QAAqC,QAA8C;AAG9G,SAFiB,OAAO,SAAS,MAAM,OAAO,UAAU,MACvC,OAAO,SAAS,MAAM,OAAO,UAAU"}