@videojs/spf 10.0.0-beta.10 → 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-25AtMN8s.d.ts +0 -719
  231. package/dist/adapter-25AtMN8s.d.ts.map +0 -1
  232. package/dist/adapter-CflgYzjF.js +0 -3556
  233. package/dist/adapter-CflgYzjF.js.map +0 -1
  234. package/dist/dom/index.d.ts +0 -221
  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,95 @@
1
+ import { createPlaybackEngine } from "./engine.js";
2
+
3
+ //#region src/dom/playback-engine/adapter.ts
4
+ /**
5
+ * HTMLMediaElement-compatible adapter for the SPF playback engine.
6
+ *
7
+ * Implements the src/play() contract per the WHATWG HTML spec so that SPF can
8
+ * be used anywhere a media element API is expected.
9
+ *
10
+ * A new engine is created on every src assignment — this fully tears down all
11
+ * state, SourceBuffers, and in-flight requests from the previous source before
12
+ * the next one begins. The media element reference is preserved across src
13
+ * changes and re-applied to the new engine automatically.
14
+ *
15
+ * @example
16
+ * const media = new SpfMedia({ preferredAudioLanguage: 'en' });
17
+ * media.attach(document.querySelector('video'));
18
+ * media.src = 'https://stream.mux.com/abc123.m3u8';
19
+ *
20
+ * // Change source — old engine is destroyed, new one starts clean:
21
+ * media.src = 'https://stream.mux.com/xyz456.m3u8';
22
+ *
23
+ * // Explicit teardown:
24
+ * media.destroy();
25
+ */
26
+ var SpfMedia = class {
27
+ #engine;
28
+ #config;
29
+ #preload = "";
30
+ /** Pending loadstart listener from a deferred play() retry, if any. */
31
+ #loadstartListener = null;
32
+ constructor(config = {}) {
33
+ this.#config = config;
34
+ this.#engine = createPlaybackEngine(config);
35
+ }
36
+ get engine() {
37
+ return this.#engine;
38
+ }
39
+ attach(mediaElement) {
40
+ this.#engine.owners.patch({ mediaElement });
41
+ }
42
+ detach() {
43
+ this.#cancelPendingPlay();
44
+ this.#engine.owners.patch({ mediaElement: void 0 });
45
+ }
46
+ destroy() {
47
+ this.#cancelPendingPlay();
48
+ this.#engine.destroy();
49
+ }
50
+ get preload() {
51
+ return this.#preload;
52
+ }
53
+ set preload(value) {
54
+ this.#preload = value;
55
+ if (value) this.#engine.state.patch({ preload: value });
56
+ }
57
+ get src() {
58
+ return this.#engine.state.current.presentation?.url ?? "";
59
+ }
60
+ set src(value) {
61
+ const prevMediaElement = this.#engine.owners.current.mediaElement;
62
+ this.#cancelPendingPlay();
63
+ this.#engine.destroy();
64
+ this.#engine = createPlaybackEngine(this.#config);
65
+ if (this.#preload) this.#engine.state.patch({ preload: this.#preload });
66
+ if (prevMediaElement) this.#engine.owners.patch({ mediaElement: prevMediaElement });
67
+ if (value) this.#engine.state.patch({ presentation: { url: value } });
68
+ }
69
+ play() {
70
+ const { mediaElement } = this.#engine.owners.current;
71
+ if (!mediaElement) return Promise.reject(/* @__PURE__ */ new Error("SpfMedia: no media element attached"));
72
+ this.#engine.state.patch({ playbackInitiated: true });
73
+ return mediaElement.play().catch((err) => {
74
+ if (this.src) return new Promise((resolve, reject) => {
75
+ const listener = () => {
76
+ this.#loadstartListener = null;
77
+ mediaElement.play().then(resolve, reject);
78
+ };
79
+ this.#loadstartListener = listener;
80
+ mediaElement.addEventListener("loadstart", listener, { once: true });
81
+ });
82
+ throw err;
83
+ });
84
+ }
85
+ #cancelPendingPlay() {
86
+ if (!this.#loadstartListener) return;
87
+ const { mediaElement } = this.#engine.owners.current;
88
+ mediaElement?.removeEventListener("loadstart", this.#loadstartListener);
89
+ this.#loadstartListener = null;
90
+ }
91
+ };
92
+
93
+ //#endregion
94
+ export { SpfMedia };
95
+ //# sourceMappingURL=adapter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter.js","names":["#config","#engine","#cancelPendingPlay","#preload","#loadstartListener"],"sources":["../../../../src/dom/playback-engine/adapter.ts"],"sourcesContent":["import type { PlaybackEngineConfig } from './engine';\nimport { createPlaybackEngine, type PlaybackEngine } from './engine';\n\n/**\n * HTMLMediaElement-compatible adapter for the SPF playback engine.\n *\n * Implements the src/play() contract per the WHATWG HTML spec so that SPF can\n * be used anywhere a media element API is expected.\n *\n * A new engine is created on every src assignment — this fully tears down all\n * state, SourceBuffers, and in-flight requests from the previous source before\n * the next one begins. The media element reference is preserved across src\n * changes and re-applied to the new engine automatically.\n *\n * @example\n * const media = new SpfMedia({ preferredAudioLanguage: 'en' });\n * media.attach(document.querySelector('video'));\n * media.src = 'https://stream.mux.com/abc123.m3u8';\n *\n * // Change source — old engine is destroyed, new one starts clean:\n * media.src = 'https://stream.mux.com/xyz456.m3u8';\n *\n * // Explicit teardown:\n * media.destroy();\n */\nexport class SpfMedia {\n #engine: PlaybackEngine;\n #config: PlaybackEngineConfig;\n #preload: '' | 'none' | 'metadata' | 'auto' = '';\n\n /** Pending loadstart listener from a deferred play() retry, if any. */\n #loadstartListener: (() => void) | null = null;\n\n constructor(config: PlaybackEngineConfig = {}) {\n this.#config = config;\n this.#engine = createPlaybackEngine(config);\n }\n\n get engine(): PlaybackEngine {\n return this.#engine;\n }\n\n // ---------------------------------------------------------------------------\n // Media element lifecycle\n // ---------------------------------------------------------------------------\n\n attach(mediaElement: HTMLMediaElement): void {\n this.#engine.owners.patch({ mediaElement });\n }\n\n detach(): void {\n this.#cancelPendingPlay();\n this.#engine.owners.patch({ mediaElement: undefined });\n }\n\n destroy(): void {\n this.#cancelPendingPlay();\n this.#engine.destroy();\n }\n\n // ---------------------------------------------------------------------------\n // preload — synchronous IDL attribute (WHATWG §4.8.11.2)\n // ---------------------------------------------------------------------------\n\n get preload(): '' | 'none' | 'metadata' | 'auto' {\n return this.#preload;\n }\n\n set preload(value: '' | 'none' | 'metadata' | 'auto') {\n this.#preload = value;\n if (value) {\n this.#engine.state.patch({ preload: value });\n }\n // value = '' clears #preload (so the next engine recreation won't re-apply\n // an explicit value) but does not patch current state — the existing preload\n // stays in effect until the next src change creates a fresh engine.\n }\n\n // ---------------------------------------------------------------------------\n // src — synchronous IDL attribute (WHATWG §4.8.11.2)\n // Each assignment destroys the current engine and starts a fresh one, exactly\n // as the browser's load algorithm resets all media element state on src change.\n // ---------------------------------------------------------------------------\n\n get src(): string {\n return this.#engine.state.current.presentation?.url ?? '';\n }\n\n set src(value: string) {\n const prevMediaElement = this.#engine.owners.current.mediaElement;\n\n this.#cancelPendingPlay();\n this.#engine.destroy();\n this.#engine = createPlaybackEngine(this.#config);\n\n // Apply explicit preload before owners.patch so syncPreloadAttribute skips\n // element inference and the explicit value is preserved across src changes.\n if (this.#preload) {\n this.#engine.state.patch({ preload: this.#preload });\n }\n\n if (prevMediaElement) {\n this.#engine.owners.patch({ mediaElement: prevMediaElement });\n }\n\n if (value) {\n this.#engine.state.patch({ presentation: { url: value } });\n }\n }\n\n // ---------------------------------------------------------------------------\n // play() — WHATWG §4.8.11.8\n // Delegates to the attached media element's native play().\n // ---------------------------------------------------------------------------\n\n play(): Promise<void> {\n const { mediaElement } = this.#engine.owners.current;\n if (!mediaElement) {\n return Promise.reject(new Error('SpfMedia: no media element attached'));\n }\n\n // Signal play intent — enables loading even with preload=\"none\"\n this.#engine.state.patch({ playbackInitiated: true });\n\n return mediaElement.play().catch((err: unknown) => {\n // If we have a pending HLS source, the rejection may be because MSE\n // hasn't attached a blob URL yet. Wait for loadstart (src assigned\n // by MSE setup) and retry once.\n if (this.src) {\n return new Promise<void>((resolve, reject) => {\n const listener = () => {\n this.#loadstartListener = null;\n mediaElement.play().then(resolve, reject);\n };\n this.#loadstartListener = listener;\n mediaElement.addEventListener('loadstart', listener, { once: true });\n });\n }\n throw err;\n });\n }\n\n // ---------------------------------------------------------------------------\n // Private\n // ---------------------------------------------------------------------------\n\n #cancelPendingPlay(): void {\n if (!this.#loadstartListener) return;\n const { mediaElement } = this.#engine.owners.current;\n mediaElement?.removeEventListener('loadstart', this.#loadstartListener);\n this.#loadstartListener = null;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AAyBA,IAAa,WAAb,MAAsB;CACpB;CACA;CACA,WAA8C;;CAG9C,qBAA0C;CAE1C,YAAY,SAA+B,EAAE,EAAE;AAC7C,QAAKA,SAAU;AACf,QAAKC,SAAU,qBAAqB,OAAO;;CAG7C,IAAI,SAAyB;AAC3B,SAAO,MAAKA;;CAOd,OAAO,cAAsC;AAC3C,QAAKA,OAAQ,OAAO,MAAM,EAAE,cAAc,CAAC;;CAG7C,SAAe;AACb,QAAKC,mBAAoB;AACzB,QAAKD,OAAQ,OAAO,MAAM,EAAE,cAAc,QAAW,CAAC;;CAGxD,UAAgB;AACd,QAAKC,mBAAoB;AACzB,QAAKD,OAAQ,SAAS;;CAOxB,IAAI,UAA6C;AAC/C,SAAO,MAAKE;;CAGd,IAAI,QAAQ,OAA0C;AACpD,QAAKA,UAAW;AAChB,MAAI,MACF,OAAKF,OAAQ,MAAM,MAAM,EAAE,SAAS,OAAO,CAAC;;CAahD,IAAI,MAAc;AAChB,SAAO,MAAKA,OAAQ,MAAM,QAAQ,cAAc,OAAO;;CAGzD,IAAI,IAAI,OAAe;EACrB,MAAM,mBAAmB,MAAKA,OAAQ,OAAO,QAAQ;AAErD,QAAKC,mBAAoB;AACzB,QAAKD,OAAQ,SAAS;AACtB,QAAKA,SAAU,qBAAqB,MAAKD,OAAQ;AAIjD,MAAI,MAAKG,QACP,OAAKF,OAAQ,MAAM,MAAM,EAAE,SAAS,MAAKE,SAAU,CAAC;AAGtD,MAAI,iBACF,OAAKF,OAAQ,OAAO,MAAM,EAAE,cAAc,kBAAkB,CAAC;AAG/D,MAAI,MACF,OAAKA,OAAQ,MAAM,MAAM,EAAE,cAAc,EAAE,KAAK,OAAO,EAAE,CAAC;;CAS9D,OAAsB;EACpB,MAAM,EAAE,iBAAiB,MAAKA,OAAQ,OAAO;AAC7C,MAAI,CAAC,aACH,QAAO,QAAQ,uBAAO,IAAI,MAAM,sCAAsC,CAAC;AAIzE,QAAKA,OAAQ,MAAM,MAAM,EAAE,mBAAmB,MAAM,CAAC;AAErD,SAAO,aAAa,MAAM,CAAC,OAAO,QAAiB;AAIjD,OAAI,KAAK,IACP,QAAO,IAAI,SAAe,SAAS,WAAW;IAC5C,MAAM,iBAAiB;AACrB,WAAKG,oBAAqB;AAC1B,kBAAa,MAAM,CAAC,KAAK,SAAS,OAAO;;AAE3C,UAAKA,oBAAqB;AAC1B,iBAAa,iBAAiB,aAAa,UAAU,EAAE,MAAM,MAAM,CAAC;KACpE;AAEJ,SAAM;IACN;;CAOJ,qBAA2B;AACzB,MAAI,CAAC,MAAKA,kBAAoB;EAC9B,MAAM,EAAE,iBAAiB,MAAKH,OAAQ,OAAO;AAC7C,gBAAc,oBAAoB,aAAa,MAAKG,kBAAmB;AACvE,QAAKA,oBAAqB"}
@@ -0,0 +1,135 @@
1
+ import { BandwidthState } from "../../core/abr/bandwidth-estimator.js";
2
+ import { createState } from "../../core/state/create-state.js";
3
+ import { SourceBufferActor } from "../media/source-buffer-actor.js";
4
+ import { TextTrackBufferState } from "../features/load-text-track-cues.js";
5
+ import { createEventStream } from "../../core/events/create-event-stream.js";
6
+ import { PresentationAction } from "../../core/features/resolve-presentation.js";
7
+ import { TrackResolutionAction } from "../../core/features/resolve-track.js";
8
+ import { TrackSelectionAction } from "../../core/features/select-tracks.js";
9
+
10
+ //#region src/dom/playback-engine/engine.d.ts
11
+ /**
12
+ * Union of all action types used by playback engine orchestrations.
13
+ * Includes synthetic @@INITIALIZE@@ event for combineLatest bootstrapping.
14
+ */
15
+ type PlaybackEngineAction = PresentationAction | TrackResolutionAction | TrackSelectionAction | {
16
+ type: '@@INITIALIZE@@';
17
+ };
18
+ /**
19
+ * Configuration for the playback engine.
20
+ */
21
+ interface PlaybackEngineConfig {
22
+ /**
23
+ * Initial bandwidth estimate for cold start (bits per second).
24
+ * Default: 1 Mbps (conservative).
25
+ */
26
+ initialBandwidth?: number;
27
+ /**
28
+ * Preferred audio language (ISO 639 code, e.g., "en", "es").
29
+ * If not specified, selects first audio track.
30
+ */
31
+ preferredAudioLanguage?: string;
32
+ /**
33
+ * Preferred subtitle language (ISO 639 code, e.g., "en", "es").
34
+ * If specified, selects matching text track if available.
35
+ */
36
+ preferredSubtitleLanguage?: string;
37
+ /**
38
+ * Include FORCED subtitle tracks in selection.
39
+ * Default: false (follows hls.js/http-streaming pattern)
40
+ */
41
+ includeForcedTracks?: boolean;
42
+ /**
43
+ * Auto-select DEFAULT track (requires DEFAULT=YES + AUTOSELECT=YES in HLS).
44
+ * Default: false (user opt-in, matches hls.js/http-streaming)
45
+ */
46
+ enableDefaultTrack?: boolean;
47
+ }
48
+ /**
49
+ * Combined state shape for the playback engine.
50
+ * Includes all state needed by orchestrations.
51
+ */
52
+ interface PlaybackEngineState {
53
+ presentation?: any;
54
+ preload?: string;
55
+ selectedVideoTrackId?: string;
56
+ selectedAudioTrackId?: string;
57
+ selectedTextTrackId?: string;
58
+ bandwidthState?: BandwidthState;
59
+ abrDisabled?: boolean;
60
+ textBufferState?: TextTrackBufferState;
61
+ currentTime?: number;
62
+ playbackInitiated?: boolean;
63
+ }
64
+ /**
65
+ * Combined owners shape for the playback engine.
66
+ * Includes all mutable platform objects.
67
+ */
68
+ interface PlaybackEngineOwners {
69
+ mediaElement?: HTMLMediaElement | undefined;
70
+ mediaSource?: MediaSource;
71
+ videoBuffer?: SourceBuffer;
72
+ audioBuffer?: SourceBuffer;
73
+ videoBufferActor?: SourceBufferActor;
74
+ audioBufferActor?: SourceBufferActor;
75
+ textTracks?: Map<string, HTMLTrackElement>;
76
+ }
77
+ /**
78
+ * Playback engine instance.
79
+ */
80
+ interface PlaybackEngine {
81
+ /**
82
+ * Reactive state (for inspection/testing).
83
+ */
84
+ state: ReturnType<typeof createState<PlaybackEngineState>>;
85
+ /**
86
+ * Mutable owners (for inspection/testing).
87
+ */
88
+ owners: ReturnType<typeof createState<PlaybackEngineOwners>>;
89
+ /**
90
+ * Shared event stream (for inspection/testing/triggering events).
91
+ */
92
+ events: ReturnType<typeof createEventStream<PlaybackEngineAction>>;
93
+ /**
94
+ * Cleanup function to destroy all orchestrations.
95
+ */
96
+ destroy: () => void;
97
+ }
98
+ /**
99
+ * Create a POC playback engine.
100
+ *
101
+ * Wires together all orchestrations to create a reactive playback pipeline:
102
+ * 1. Resolve presentation (multivariant playlist)
103
+ * 2. Select initial video and audio tracks
104
+ * 3. Resolve selected tracks (media playlists)
105
+ * 4. Setup MediaSource
106
+ * 5. Setup SourceBuffers for video and audio
107
+ *
108
+ * Note: This is a POC - does not yet load/append segments.
109
+ *
110
+ * @param config - Playback engine configuration
111
+ * @returns Playback engine instance with state, owners, and destroy function
112
+ *
113
+ * @example
114
+ * const engine = createPlaybackEngine({
115
+ * initialBandwidth: 2_000_000,
116
+ * preferredAudioLanguage: 'en',
117
+ * });
118
+ *
119
+ * // Initialize by patching state and owners
120
+ * engine.owners.patch({ mediaElement: document.querySelector('video') });
121
+ * engine.state.patch({
122
+ * presentation: { url: 'https://example.com/playlist.m3u8' },
123
+ * preload: 'auto',
124
+ * });
125
+ *
126
+ * // Inspect state
127
+ * console.log(engine.state.current);
128
+ *
129
+ * // Cleanup
130
+ * engine.destroy();
131
+ */
132
+ declare function createPlaybackEngine(config?: PlaybackEngineConfig): PlaybackEngine;
133
+ //#endregion
134
+ export { PlaybackEngine, PlaybackEngineConfig, createPlaybackEngine };
135
+ //# sourceMappingURL=engine.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine.d.ts","names":[],"sources":["../../../../src/dom/playback-engine/engine.ts"],"mappings":";;;;;;;;;;;;AAoCA;;KAAY,oBAAA,GACR,kBAAA,GACA,qBAAA,GACA,oBAAA;EACE,IAAA;AAAA;;;;UAKW,oBAAA;EAPb;;;;EAYF,gBAAA;EALe;;;;EAWf,sBAAA;EAAA;;;;EAMA,yBAAA;EAYkB;AAOpB;;;EAbE,mBAAA;EAeA;;;;EATA,kBAAA;AAAA;;;;;UAOe,mBAAA;EAEf,YAAA;EACA,OAAA;EAGA,oBAAA;EACA,oBAAA;EAEA,mBAAA;EAGA,cAAA,GAAiB,cAAA;EAKjB,WAAA;EAGA,eAAA,GAAkB,oBAAA;EAGlB,WAAA;EAGA,iBAAA;AAAA;;;;;UAOe,oBAAA;EAEf,YAAA,GAAe,gBAAA;EAGf,WAAA,GAAc,WAAA;EAGd,WAAA,GAAc,YAAA;EACd,WAAA,GAAc,YAAA;EACd,gBAAA,GAAmB,iBAAA;EACnB,gBAAA,GAAmB,iBAAA;EAGnB,UAAA,GAAa,GAAA,SAAY,gBAAA;AAAA;;;;UAMV,cAAA;EANf;;;EAUA,KAAA,EAAO,UAAA,QAAkB,WAAA,CAAY,mBAAA;EAVI;AAM3C;;EASE,MAAA,EAAQ,UAAA,QAAkB,WAAA,CAAY,oBAAA;EALD;;;EAUrC,MAAA,EAAQ,UAAA,QAAkB,iBAAA,CAAkB,oBAAA;EALlB;;;EAU1B,OAAA;AAAA;;;;;;;;;;;;;;;;;;AAqCF;;;;;;;;;;;;;;;;;iBAAgB,oBAAA,CAAqB,MAAA,GAAQ,oBAAA,GAA4B,cAAA"}
@@ -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,3 @@
1
+ import { PlaybackEngine, PlaybackEngineConfig, createPlaybackEngine } from "./playback-engine/engine.js";
2
+ import { SpfMedia } from "./playback-engine/adapter.js";
3
+ export { type PlaybackEngine, type PlaybackEngineConfig, SpfMedia, createPlaybackEngine };
@@ -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,12 @@
1
+ //#region src/dom/text/parse-vtt-segment.d.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
+ declare function parseVttSegment(url: string): Promise<VTTCue[]>;
9
+ declare function destroyVttParser(): void;
10
+ //#endregion
11
+ export { destroyVttParser, parseVttSegment };
12
+ //# sourceMappingURL=parse-vtt-segment.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse-vtt-segment.d.ts","names":[],"sources":["../../../../src/dom/text/parse-vtt-segment.ts"],"mappings":";;AAqBA;;;;;iBAAgB,eAAA,CAAgB,GAAA,WAAc,OAAA,CAAQ,MAAA;AAAA,iBA0CtC,gBAAA,CAAA"}
@@ -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,10 @@
1
+ import { appendSegment } from "./dom/media/append-segment.js";
2
+ import { loadSegments } from "./dom/features/load-segments.js";
3
+ import { loadTextTrackCues } from "./dom/features/load-text-track-cues.js";
4
+ import { CurrentTimeOwners, CurrentTimeState, trackCurrentTime } from "./dom/features/track-current-time.js";
5
+ import { PlaybackInitiatedOwners, PlaybackInitiatedState, trackPlaybackInitiated } from "./dom/features/track-playback-initiated.js";
6
+ import { PlaybackRateOwners, PlaybackRateState, trackPlaybackRate } from "./dom/features/track-playback-rate.js";
7
+ import { flushBuffer } from "./dom/media/buffer-flusher.js";
8
+ import { SpfMedia } from "./dom/playback-engine/adapter.js";
9
+ import { destroyVttParser, parseVttSegment } from "./dom/text/parse-vtt-segment.js";
10
+ export { type CurrentTimeOwners, type CurrentTimeState, type PlaybackInitiatedOwners, type PlaybackInitiatedState, type PlaybackRateOwners, type PlaybackRateState, SpfMedia, appendSegment, destroyVttParser, flushBuffer, loadSegments, loadTextTrackCues, parseVttSegment, trackCurrentTime, trackPlaybackInitiated, trackPlaybackRate };
@@ -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 };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/index.ts"],"mappings":";;AAWA;;;;;;;cAAa,OAAA"}
@@ -0,0 +1,14 @@
1
+ //#region src/index.ts
2
+ /**
3
+ * Stream Processing Framework (SPF) for Video.js 10
4
+ *
5
+ * Bundle size target: < 20KB (minified + gzipped)
6
+ * Primary use case: CMAF HLS playback with Video.js v10
7
+ *
8
+ * @packageDocumentation
9
+ */
10
+ const VERSION = "0.1.0";
11
+
12
+ //#endregion
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"}