@videojs/spf 10.0.0-beta.10 → 10.0.0-beta.12

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 (236) hide show
  1. package/README.md +16 -40
  2. package/dist/default/core/abr/bandwidth-estimator.js +95 -0
  3. package/dist/default/core/abr/bandwidth-estimator.js.map +1 -0
  4. package/dist/default/core/abr/ewma.js +71 -0
  5. package/dist/default/core/abr/ewma.js.map +1 -0
  6. package/dist/default/core/abr/quality-selection.js +53 -0
  7. package/dist/default/core/abr/quality-selection.js.map +1 -0
  8. package/dist/default/core/buffer/back-buffer.js +46 -0
  9. package/dist/default/core/buffer/back-buffer.js.map +1 -0
  10. package/dist/default/core/buffer/forward-buffer.js +73 -0
  11. package/dist/default/core/buffer/forward-buffer.js.map +1 -0
  12. package/dist/default/core/features/calculate-presentation-duration.js +53 -0
  13. package/dist/default/core/features/calculate-presentation-duration.js.map +1 -0
  14. package/dist/default/core/features/quality-switching.js +69 -0
  15. package/dist/default/core/features/quality-switching.js.map +1 -0
  16. package/dist/default/core/features/resolve-presentation.js +77 -0
  17. package/dist/default/core/features/resolve-presentation.js.map +1 -0
  18. package/dist/default/core/features/resolve-track.js +66 -0
  19. package/dist/default/core/features/resolve-track.js.map +1 -0
  20. package/dist/default/core/features/select-tracks.js +130 -0
  21. package/dist/default/core/features/select-tracks.js.map +1 -0
  22. package/dist/default/core/features/sync-preload-attribute.js +26 -0
  23. package/dist/default/core/features/sync-preload-attribute.js.map +1 -0
  24. package/dist/default/core/hls/parse-attributes.js +134 -0
  25. package/dist/default/core/hls/parse-attributes.js.map +1 -0
  26. package/dist/default/core/hls/parse-media-playlist.js +82 -0
  27. package/dist/default/core/hls/parse-media-playlist.js.map +1 -0
  28. package/dist/default/core/hls/parse-multivariant.js +214 -0
  29. package/dist/default/core/hls/parse-multivariant.js.map +1 -0
  30. package/dist/default/core/hls/resolve-url.js +11 -0
  31. package/dist/default/core/hls/resolve-url.js.map +1 -0
  32. package/dist/default/core/signals/effect.js +41 -0
  33. package/dist/default/core/signals/effect.js.map +1 -0
  34. package/dist/default/core/signals/primitives.js +32 -0
  35. package/dist/default/core/signals/primitives.js.map +1 -0
  36. package/dist/default/core/task.js +122 -0
  37. package/dist/default/core/task.js.map +1 -0
  38. package/dist/default/core/types/index.js +15 -0
  39. package/dist/default/core/types/index.js.map +1 -0
  40. package/dist/default/core/utils/generate-id.js +21 -0
  41. package/dist/default/core/utils/generate-id.js.map +1 -0
  42. package/dist/default/core/utils/track-selection.js +39 -0
  43. package/dist/default/core/utils/track-selection.js.map +1 -0
  44. package/dist/default/dom/features/end-of-stream.js +145 -0
  45. package/dist/default/dom/features/end-of-stream.js.map +1 -0
  46. package/dist/default/dom/features/load-segments.js +192 -0
  47. package/dist/default/dom/features/load-segments.js.map +1 -0
  48. package/dist/default/dom/features/load-text-track-cues.js +161 -0
  49. package/dist/default/dom/features/load-text-track-cues.js.map +1 -0
  50. package/dist/default/dom/features/segment-loader-actor.js +196 -0
  51. package/dist/default/dom/features/segment-loader-actor.js.map +1 -0
  52. package/dist/default/dom/features/setup-mediasource.js +48 -0
  53. package/dist/default/dom/features/setup-mediasource.js.map +1 -0
  54. package/dist/default/dom/features/setup-sourcebuffer.js +78 -0
  55. package/dist/default/dom/features/setup-sourcebuffer.js.map +1 -0
  56. package/dist/default/dom/features/setup-text-tracks.js +67 -0
  57. package/dist/default/dom/features/setup-text-tracks.js.map +1 -0
  58. package/dist/default/dom/features/sync-selected-text-track-from-dom.js +54 -0
  59. package/dist/default/dom/features/sync-selected-text-track-from-dom.js.map +1 -0
  60. package/dist/default/dom/features/sync-text-track-modes.js +33 -0
  61. package/dist/default/dom/features/sync-text-track-modes.js.map +1 -0
  62. package/dist/default/dom/features/track-current-time.js +49 -0
  63. package/dist/default/dom/features/track-current-time.js.map +1 -0
  64. package/dist/default/dom/features/track-playback-initiated.js +53 -0
  65. package/dist/default/dom/features/track-playback-initiated.js.map +1 -0
  66. package/dist/default/dom/features/track-playback-rate.js +32 -0
  67. package/dist/default/dom/features/track-playback-rate.js.map +1 -0
  68. package/dist/default/dom/features/update-duration.js +78 -0
  69. package/dist/default/dom/features/update-duration.js.map +1 -0
  70. package/dist/default/dom/media/append-segment.js +60 -0
  71. package/dist/default/dom/media/append-segment.js.map +1 -0
  72. package/dist/default/dom/media/buffer-flusher.js +55 -0
  73. package/dist/default/dom/media/buffer-flusher.js.map +1 -0
  74. package/dist/default/dom/media/mediasource-setup.js +146 -0
  75. package/dist/default/dom/media/mediasource-setup.js.map +1 -0
  76. package/dist/default/dom/media/source-buffer-actor.js +176 -0
  77. package/dist/default/dom/media/source-buffer-actor.js.map +1 -0
  78. package/dist/default/dom/network/chunked-stream-iterable.js +49 -0
  79. package/dist/default/dom/network/chunked-stream-iterable.js.map +1 -0
  80. package/dist/default/dom/network/fetch.js +55 -0
  81. package/dist/default/dom/network/fetch.js.map +1 -0
  82. package/dist/default/dom/playback-engine/adapter.js +95 -0
  83. package/dist/default/dom/playback-engine/adapter.js.map +1 -0
  84. package/dist/default/dom/playback-engine/engine.js +151 -0
  85. package/dist/default/dom/playback-engine/engine.js.map +1 -0
  86. package/dist/default/dom/playback-engine.js +4 -0
  87. package/dist/default/dom/text/parse-vtt-segment.js +56 -0
  88. package/dist/default/dom/text/parse-vtt-segment.js.map +1 -0
  89. package/dist/default/dom.js +10 -0
  90. package/dist/{src-DRsS_H80.js → default/index.js} +3 -3
  91. package/dist/default/index.js.map +1 -0
  92. package/dist/dev/core/abr/bandwidth-estimator.d.ts +35 -0
  93. package/dist/dev/core/abr/bandwidth-estimator.d.ts.map +1 -0
  94. package/dist/dev/core/abr/bandwidth-estimator.js +95 -0
  95. package/dist/dev/core/abr/bandwidth-estimator.js.map +1 -0
  96. package/dist/dev/core/abr/ewma.js +71 -0
  97. package/dist/dev/core/abr/ewma.js.map +1 -0
  98. package/dist/dev/core/abr/quality-selection.js +53 -0
  99. package/dist/dev/core/abr/quality-selection.js.map +1 -0
  100. package/dist/dev/core/actor.d.ts +18 -0
  101. package/dist/dev/core/actor.d.ts.map +1 -0
  102. package/dist/dev/core/buffer/back-buffer.js +46 -0
  103. package/dist/dev/core/buffer/back-buffer.js.map +1 -0
  104. package/dist/dev/core/buffer/forward-buffer.js +73 -0
  105. package/dist/dev/core/buffer/forward-buffer.js.map +1 -0
  106. package/dist/dev/core/features/calculate-presentation-duration.js +53 -0
  107. package/dist/dev/core/features/calculate-presentation-duration.js.map +1 -0
  108. package/dist/dev/core/features/quality-switching.js +69 -0
  109. package/dist/dev/core/features/quality-switching.js.map +1 -0
  110. package/dist/dev/core/features/resolve-presentation.js +77 -0
  111. package/dist/dev/core/features/resolve-presentation.js.map +1 -0
  112. package/dist/dev/core/features/resolve-track.js +66 -0
  113. package/dist/dev/core/features/resolve-track.js.map +1 -0
  114. package/dist/dev/core/features/select-tracks.js +130 -0
  115. package/dist/dev/core/features/select-tracks.js.map +1 -0
  116. package/dist/dev/core/features/sync-preload-attribute.js +26 -0
  117. package/dist/dev/core/features/sync-preload-attribute.js.map +1 -0
  118. package/dist/dev/core/hls/parse-attributes.js +134 -0
  119. package/dist/dev/core/hls/parse-attributes.js.map +1 -0
  120. package/dist/dev/core/hls/parse-media-playlist.js +82 -0
  121. package/dist/dev/core/hls/parse-media-playlist.js.map +1 -0
  122. package/dist/dev/core/hls/parse-multivariant.js +214 -0
  123. package/dist/dev/core/hls/parse-multivariant.js.map +1 -0
  124. package/dist/dev/core/hls/resolve-url.js +11 -0
  125. package/dist/dev/core/hls/resolve-url.js.map +1 -0
  126. package/dist/dev/core/signals/effect.d.ts +15 -0
  127. package/dist/dev/core/signals/effect.d.ts.map +1 -0
  128. package/dist/dev/core/signals/effect.js +41 -0
  129. package/dist/dev/core/signals/effect.js.map +1 -0
  130. package/dist/dev/core/signals/primitives.d.ts +10 -0
  131. package/dist/dev/core/signals/primitives.d.ts.map +1 -0
  132. package/dist/dev/core/signals/primitives.js +32 -0
  133. package/dist/dev/core/signals/primitives.js.map +1 -0
  134. package/dist/dev/core/task.js +122 -0
  135. package/dist/dev/core/task.js.map +1 -0
  136. package/dist/dev/core/types/index.d.ts +173 -0
  137. package/dist/dev/core/types/index.d.ts.map +1 -0
  138. package/dist/dev/core/types/index.js +15 -0
  139. package/dist/dev/core/types/index.js.map +1 -0
  140. package/dist/dev/core/utils/generate-id.js +21 -0
  141. package/dist/dev/core/utils/generate-id.js.map +1 -0
  142. package/dist/dev/core/utils/track-selection.d.ts +16 -0
  143. package/dist/dev/core/utils/track-selection.d.ts.map +1 -0
  144. package/dist/dev/core/utils/track-selection.js +39 -0
  145. package/dist/dev/core/utils/track-selection.js.map +1 -0
  146. package/dist/dev/dom/features/end-of-stream.js +145 -0
  147. package/dist/dev/dom/features/end-of-stream.js.map +1 -0
  148. package/dist/dev/dom/features/load-segments.d.ts +72 -0
  149. package/dist/dev/dom/features/load-segments.d.ts.map +1 -0
  150. package/dist/dev/dom/features/load-segments.js +192 -0
  151. package/dist/dev/dom/features/load-segments.js.map +1 -0
  152. package/dist/dev/dom/features/load-text-track-cues.d.ts +61 -0
  153. package/dist/dev/dom/features/load-text-track-cues.d.ts.map +1 -0
  154. package/dist/dev/dom/features/load-text-track-cues.js +161 -0
  155. package/dist/dev/dom/features/load-text-track-cues.js.map +1 -0
  156. package/dist/dev/dom/features/segment-loader-actor.d.ts +1 -0
  157. package/dist/dev/dom/features/segment-loader-actor.js +196 -0
  158. package/dist/dev/dom/features/segment-loader-actor.js.map +1 -0
  159. package/dist/dev/dom/features/setup-mediasource.js +48 -0
  160. package/dist/dev/dom/features/setup-mediasource.js.map +1 -0
  161. package/dist/dev/dom/features/setup-sourcebuffer.d.ts +9 -0
  162. package/dist/dev/dom/features/setup-sourcebuffer.d.ts.map +1 -0
  163. package/dist/dev/dom/features/setup-sourcebuffer.js +78 -0
  164. package/dist/dev/dom/features/setup-sourcebuffer.js.map +1 -0
  165. package/dist/dev/dom/features/setup-text-tracks.js +67 -0
  166. package/dist/dev/dom/features/setup-text-tracks.js.map +1 -0
  167. package/dist/dev/dom/features/sync-selected-text-track-from-dom.js +54 -0
  168. package/dist/dev/dom/features/sync-selected-text-track-from-dom.js.map +1 -0
  169. package/dist/dev/dom/features/sync-text-track-modes.js +33 -0
  170. package/dist/dev/dom/features/sync-text-track-modes.js.map +1 -0
  171. package/dist/dev/dom/features/track-current-time.d.ts +40 -0
  172. package/dist/dev/dom/features/track-current-time.d.ts.map +1 -0
  173. package/dist/dev/dom/features/track-current-time.js +49 -0
  174. package/dist/dev/dom/features/track-current-time.js.map +1 -0
  175. package/dist/dev/dom/features/track-playback-initiated.d.ts +44 -0
  176. package/dist/dev/dom/features/track-playback-initiated.d.ts.map +1 -0
  177. package/dist/dev/dom/features/track-playback-initiated.js +53 -0
  178. package/dist/dev/dom/features/track-playback-initiated.js.map +1 -0
  179. package/dist/dev/dom/features/track-playback-rate.d.ts +34 -0
  180. package/dist/dev/dom/features/track-playback-rate.d.ts.map +1 -0
  181. package/dist/dev/dom/features/track-playback-rate.js +32 -0
  182. package/dist/dev/dom/features/track-playback-rate.js.map +1 -0
  183. package/dist/dev/dom/features/update-duration.js +78 -0
  184. package/dist/dev/dom/features/update-duration.js.map +1 -0
  185. package/dist/dev/dom/media/append-segment.d.ts +24 -0
  186. package/dist/dev/dom/media/append-segment.d.ts.map +1 -0
  187. package/dist/dev/dom/media/append-segment.js +60 -0
  188. package/dist/dev/dom/media/append-segment.js.map +1 -0
  189. package/dist/dev/dom/media/buffer-flusher.d.ts +24 -0
  190. package/dist/dev/dom/media/buffer-flusher.d.ts.map +1 -0
  191. package/dist/dev/dom/media/buffer-flusher.js +55 -0
  192. package/dist/dev/dom/media/buffer-flusher.js.map +1 -0
  193. package/dist/dev/dom/media/mediasource-setup.js +146 -0
  194. package/dist/dev/dom/media/mediasource-setup.js.map +1 -0
  195. package/dist/dev/dom/media/source-buffer-actor.d.ts +56 -0
  196. package/dist/dev/dom/media/source-buffer-actor.d.ts.map +1 -0
  197. package/dist/dev/dom/media/source-buffer-actor.js +176 -0
  198. package/dist/dev/dom/media/source-buffer-actor.js.map +1 -0
  199. package/dist/dev/dom/network/chunked-stream-iterable.js +49 -0
  200. package/dist/dev/dom/network/chunked-stream-iterable.js.map +1 -0
  201. package/dist/dev/dom/network/fetch.js +55 -0
  202. package/dist/dev/dom/network/fetch.js.map +1 -0
  203. package/dist/dev/dom/playback-engine/adapter.d.ts +41 -0
  204. package/dist/dev/dom/playback-engine/adapter.d.ts.map +1 -0
  205. package/dist/dev/dom/playback-engine/adapter.js +95 -0
  206. package/dist/dev/dom/playback-engine/adapter.js.map +1 -0
  207. package/dist/dev/dom/playback-engine/engine.d.ts +123 -0
  208. package/dist/dev/dom/playback-engine/engine.d.ts.map +1 -0
  209. package/dist/dev/dom/playback-engine/engine.js +151 -0
  210. package/dist/dev/dom/playback-engine/engine.js.map +1 -0
  211. package/dist/dev/dom/playback-engine.d.ts +4 -0
  212. package/dist/dev/dom/playback-engine.js +4 -0
  213. package/dist/dev/dom/text/parse-vtt-segment.d.ts +12 -0
  214. package/dist/dev/dom/text/parse-vtt-segment.d.ts.map +1 -0
  215. package/dist/dev/dom/text/parse-vtt-segment.js +56 -0
  216. package/dist/dev/dom/text/parse-vtt-segment.js.map +1 -0
  217. package/dist/dev/dom.d.ts +10 -0
  218. package/dist/dev/dom.js +10 -0
  219. package/dist/dev/index.d.ts.map +1 -0
  220. package/dist/dev/index.js +14 -0
  221. package/dist/dev/index.js.map +1 -0
  222. package/package.json +24 -23
  223. package/dist/adapter-25AtMN8s.d.ts +0 -719
  224. package/dist/adapter-25AtMN8s.d.ts.map +0 -1
  225. package/dist/adapter-CflgYzjF.js +0 -3556
  226. package/dist/adapter-CflgYzjF.js.map +0 -1
  227. package/dist/dom/index.d.ts +0 -221
  228. package/dist/dom/index.d.ts.map +0 -1
  229. package/dist/dom/index.js +0 -38
  230. package/dist/dom/index.js.map +0 -1
  231. package/dist/dom/playback-engine/index.d.ts +0 -2
  232. package/dist/dom/playback-engine/index.js +0 -4
  233. package/dist/index.d.ts.map +0 -1
  234. package/dist/index.js +0 -3
  235. package/dist/src-DRsS_H80.js.map +0 -1
  236. /package/dist/{index.d.ts → dev/index.d.ts} +0 -0
@@ -0,0 +1,77 @@
1
+ import { effect } from "../signals/effect.js";
2
+ import { computed, update } from "../signals/primitives.js";
3
+ import { fetchResolvable, getResponseText } from "../../dom/network/fetch.js";
4
+ import { parseMultivariantPlaylist } from "../hls/parse-multivariant.js";
5
+ //#region src/core/features/resolve-presentation.ts
6
+ /**
7
+ * Type guard to check if presentation is unresolved.
8
+ */
9
+ function isUnresolved(presentation) {
10
+ return presentation !== void 0 && "url" in presentation && !("id" in presentation);
11
+ }
12
+ function canResolve(state) {
13
+ return isUnresolved(state.presentation);
14
+ }
15
+ /**
16
+ * Determines if resolution conditions are met based on preload policy and playback state.
17
+ *
18
+ * Resolution conditions:
19
+ * - State-driven: preload is 'auto' or 'metadata'
20
+ * - Playback-driven: playbackInitiated is true
21
+ *
22
+ * @param state - Current presentation state
23
+ * @returns true if resolution conditions are met
24
+ */
25
+ function shouldResolve(state) {
26
+ const { preload, playbackInitiated } = state;
27
+ return ["auto", "metadata"].includes(preload) || !!playbackInitiated;
28
+ }
29
+ /**
30
+ * Resolves unresolved presentations using reactive composition.
31
+ *
32
+ * Triggers resolution when:
33
+ * - State-driven: Unresolved presentation + preload allows (auto/metadata)
34
+ * - Playback-driven: playbackInitiated is true
35
+ *
36
+ * @example
37
+ * ```ts
38
+ * const state = signal({ presentation: undefined, preload: 'auto', playbackInitiated: false });
39
+ *
40
+ * const cleanup = resolvePresentation({ state });
41
+ *
42
+ * // State-driven: resolves immediately when preload allows
43
+ * state.set({ ...state.get(), presentation: { url: 'http://example.com/playlist.m3u8' } });
44
+ *
45
+ * // Playback-driven: resolves when playbackInitiated is set
46
+ * state.set({ ...state.get(), preload: 'none', presentation: { url: '...' }, playbackInitiated: true });
47
+ * ```
48
+ */
49
+ function resolvePresentation({ state }) {
50
+ const canResolveSignal = computed(() => canResolve(state.get()));
51
+ const shouldResolveSignal = computed(() => shouldResolve(state.get()));
52
+ let resolving = false;
53
+ let abortController = null;
54
+ const cleanupEffect = effect(() => {
55
+ if (!canResolveSignal.get() || !shouldResolveSignal.get() || resolving) return;
56
+ const presentation = state.get().presentation;
57
+ resolving = true;
58
+ abortController = new AbortController();
59
+ fetchResolvable(presentation, { signal: abortController.signal }).then((response) => getResponseText(response)).then((text) => {
60
+ update(state, { presentation: parseMultivariantPlaylist(text, presentation) });
61
+ }).catch((error) => {
62
+ if (error instanceof Error && error.name === "AbortError") return;
63
+ throw error;
64
+ }).finally(() => {
65
+ resolving = false;
66
+ abortController = null;
67
+ });
68
+ });
69
+ return () => {
70
+ abortController?.abort();
71
+ cleanupEffect();
72
+ };
73
+ }
74
+ //#endregion
75
+ export { resolvePresentation };
76
+
77
+ //# sourceMappingURL=resolve-presentation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve-presentation.js","names":[],"sources":["../../../../src/core/features/resolve-presentation.ts"],"sourcesContent":["import { fetchResolvable, getResponseText } from '../../dom/network/fetch';\nimport { parseMultivariantPlaylist } from '../hls/parse-multivariant';\nimport { effect } from '../signals/effect';\nimport { computed, type Signal, update } from '../signals/primitives';\nimport type { AddressableObject, Presentation } from '../types';\n\n/**\n * Unresolved presentation - has a URL but no data yet.\n * Identical to AddressableObject per user requirement.\n */\nexport type UnresolvedPresentation = AddressableObject;\n\n/**\n * State shape for presentation resolution.\n */\nexport interface PresentationState {\n presentation?: UnresolvedPresentation | Presentation | undefined;\n preload?: 'auto' | 'metadata' | 'none' | undefined;\n /** True once the user has initiated playback — enables resolution regardless of preload. */\n playbackInitiated?: boolean;\n}\n\n/**\n * Type guard to check if presentation is unresolved.\n */\nexport function isUnresolved(\n presentation: UnresolvedPresentation | Presentation | undefined\n): presentation is UnresolvedPresentation {\n return presentation !== undefined && 'url' in presentation && !('id' in presentation);\n}\n\nexport function canResolve(\n state: PresentationState\n): state is PresentationState & { presentation: UnresolvedPresentation } {\n return isUnresolved(state.presentation);\n}\n\n/**\n * Determines if resolution conditions are met based on preload policy and playback state.\n *\n * Resolution conditions:\n * - State-driven: preload is 'auto' or 'metadata'\n * - Playback-driven: playbackInitiated is true\n *\n * @param state - Current presentation state\n * @returns true if resolution conditions are met\n */\nexport function shouldResolve(state: PresentationState): boolean {\n const { preload, playbackInitiated } = state;\n return (\n // State-driven: preload allows (auto/metadata)\n ['auto', 'metadata'].includes(preload as any) ||\n // Playback-driven: user has initiated playback\n !!playbackInitiated\n );\n}\n\n/**\n * Resolves unresolved presentations using reactive composition.\n *\n * Triggers resolution when:\n * - State-driven: Unresolved presentation + preload allows (auto/metadata)\n * - Playback-driven: playbackInitiated is true\n *\n * @example\n * ```ts\n * const state = signal({ presentation: undefined, preload: 'auto', playbackInitiated: false });\n *\n * const cleanup = resolvePresentation({ state });\n *\n * // State-driven: resolves immediately when preload allows\n * state.set({ ...state.get(), presentation: { url: 'http://example.com/playlist.m3u8' } });\n *\n * // Playback-driven: resolves when playbackInitiated is set\n * state.set({ ...state.get(), preload: 'none', presentation: { url: '...' }, playbackInitiated: true });\n * ```\n */\nexport function resolvePresentation<S extends PresentationState>({ state }: { state: Signal<S> }): () => void {\n const canResolveSignal = computed(() => canResolve(state.get()));\n const shouldResolveSignal = computed(() => shouldResolve(state.get()));\n\n let resolving = false;\n let abortController: AbortController | null = null;\n\n const cleanupEffect = effect(() => {\n if (!canResolveSignal.get() || !shouldResolveSignal.get() || resolving) return;\n\n const presentation = state.get().presentation as UnresolvedPresentation;\n resolving = true;\n abortController = new AbortController();\n\n fetchResolvable(presentation, { signal: abortController.signal })\n .then((response) => getResponseText(response))\n .then((text) => {\n const parsed = parseMultivariantPlaylist(text, presentation);\n const patch: Partial<PresentationState> = { presentation: parsed };\n update(state, patch);\n })\n .catch((error) => {\n if (error instanceof Error && error.name === 'AbortError') return;\n throw error;\n })\n .finally(() => {\n resolving = false;\n abortController = null;\n });\n });\n\n return () => {\n abortController?.abort();\n cleanupEffect();\n };\n}\n"],"mappings":";;;;;;;;AAyBA,SAAgB,aACd,cACwC;AACxC,QAAO,iBAAiB,KAAA,KAAa,SAAS,gBAAgB,EAAE,QAAQ;;AAG1E,SAAgB,WACd,OACuE;AACvE,QAAO,aAAa,MAAM,aAAa;;;;;;;;;;;;AAazC,SAAgB,cAAc,OAAmC;CAC/D,MAAM,EAAE,SAAS,sBAAsB;AACvC,QAEE,CAAC,QAAQ,WAAW,CAAC,SAAS,QAAe,IAE7C,CAAC,CAAC;;;;;;;;;;;;;;;;;;;;;;AAwBN,SAAgB,oBAAiD,EAAE,SAA2C;CAC5G,MAAM,mBAAmB,eAAe,WAAW,MAAM,KAAK,CAAC,CAAC;CAChE,MAAM,sBAAsB,eAAe,cAAc,MAAM,KAAK,CAAC,CAAC;CAEtE,IAAI,YAAY;CAChB,IAAI,kBAA0C;CAE9C,MAAM,gBAAgB,aAAa;AACjC,MAAI,CAAC,iBAAiB,KAAK,IAAI,CAAC,oBAAoB,KAAK,IAAI,UAAW;EAExE,MAAM,eAAe,MAAM,KAAK,CAAC;AACjC,cAAY;AACZ,oBAAkB,IAAI,iBAAiB;AAEvC,kBAAgB,cAAc,EAAE,QAAQ,gBAAgB,QAAQ,CAAC,CAC9D,MAAM,aAAa,gBAAgB,SAAS,CAAC,CAC7C,MAAM,SAAS;AAGd,UAAO,OADmC,EAAE,cAD7B,0BAA0B,MAAM,aAAa,EACM,CAC9C;IACpB,CACD,OAAO,UAAU;AAChB,OAAI,iBAAiB,SAAS,MAAM,SAAS,aAAc;AAC3D,SAAM;IACN,CACD,cAAc;AACb,eAAY;AACZ,qBAAkB;IAClB;GACJ;AAEF,cAAa;AACX,mBAAiB,OAAO;AACxB,iBAAe"}
@@ -0,0 +1,66 @@
1
+ import { effect } from "../signals/effect.js";
2
+ import { isResolvedTrack } from "../types/index.js";
3
+ import { getSelectedTrack } from "../utils/track-selection.js";
4
+ import { fetchResolvable, getResponseText } from "../../dom/network/fetch.js";
5
+ import { parseMediaPlaylist } from "../hls/parse-media-playlist.js";
6
+ import { ConcurrentRunner, Task } from "../task.js";
7
+ //#region src/core/features/resolve-track.ts
8
+ function canResolve(state, config) {
9
+ const track = getSelectedTrack(state, config.type);
10
+ if (!track) return false;
11
+ return !isResolvedTrack(track);
12
+ }
13
+ /**
14
+ * Updates a track within a presentation (immutably).
15
+ * Generic - works for video, audio, or text tracks.
16
+ */
17
+ function updateTrackInPresentation(presentation, resolvedTrack) {
18
+ const trackId = resolvedTrack.id;
19
+ return {
20
+ ...presentation,
21
+ selectionSets: presentation.selectionSets.map((selectionSet) => ({
22
+ ...selectionSet,
23
+ switchingSets: selectionSet.switchingSets.map((switchingSet) => ({
24
+ ...switchingSet,
25
+ tracks: switchingSet.tracks.map((track) => track.id === trackId ? resolvedTrack : track)
26
+ }))
27
+ }))
28
+ };
29
+ }
30
+ /**
31
+ * Resolves unresolved tracks using reactive composition.
32
+ *
33
+ * Reacts to state changes and schedules fetch tasks via ConcurrentRunner when
34
+ * a selected track is unresolved. The ConcurrentRunner handles deduplication,
35
+ * parallel execution, and cleanup.
36
+ *
37
+ * Generic version that works for video, audio, or text tracks based on config.
38
+ * Type parameter T is inferred from config.type (use 'as const' for inference).
39
+ */
40
+ function resolveTrack({ state }, config) {
41
+ const runner = new ConcurrentRunner();
42
+ const cleanup = effect(() => {
43
+ const currentState = state.get();
44
+ if (!canResolve(currentState, config)) return;
45
+ const track = getSelectedTrack(currentState, config.type);
46
+ if (!track) return;
47
+ const resolvedTrack = track;
48
+ runner.schedule(new Task(async (signal) => {
49
+ const mediaTrack = parseMediaPlaylist(await getResponseText(await fetchResolvable(resolvedTrack, { signal })), resolvedTrack);
50
+ const latest = state.get();
51
+ const updatedPresentation = updateTrackInPresentation(latest.presentation, mediaTrack);
52
+ state.set({
53
+ ...latest,
54
+ presentation: updatedPresentation
55
+ });
56
+ }, { id: track.id }));
57
+ });
58
+ return () => {
59
+ runner.abortAll();
60
+ cleanup();
61
+ };
62
+ }
63
+ //#endregion
64
+ export { resolveTrack };
65
+
66
+ //# sourceMappingURL=resolve-track.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve-track.js","names":[],"sources":["../../../../src/core/features/resolve-track.ts"],"sourcesContent":["import { fetchResolvable, getResponseText } from '../../dom/network/fetch';\nimport { parseMediaPlaylist } from '../hls/parse-media-playlist';\nimport { effect } from '../signals/effect';\nimport type { Signal } from '../signals/primitives';\nimport { ConcurrentRunner, Task } from '../task';\nimport type { Presentation, ResolvedTrack, TrackType } from '../types';\nimport { isResolvedTrack } from '../types';\nimport { getSelectedTrack, type TrackSelectionState } from '../utils/track-selection';\n\n/**\n * State shape for track resolution.\n */\nexport interface TrackResolutionState extends TrackSelectionState {\n presentation?: Presentation | undefined;\n selectedVideoTrackId?: string | undefined;\n selectedAudioTrackId?: string | undefined;\n selectedTextTrackId?: string | undefined;\n}\n\nexport function canResolve<T extends TrackType>(\n state: TrackResolutionState,\n config: TrackResolutionConfig<T>\n): boolean {\n const track = getSelectedTrack(state, config.type);\n if (!track) return false;\n\n return !isResolvedTrack(track);\n}\n\n// ============================================================================\n// Public API\n// ============================================================================\n\n/**\n * Updates a track within a presentation (immutably).\n * Generic - works for video, audio, or text tracks.\n */\nexport function updateTrackInPresentation<T extends ResolvedTrack>(\n presentation: Presentation,\n resolvedTrack: T\n): Presentation {\n const trackId = resolvedTrack.id;\n return {\n ...presentation,\n selectionSets: presentation.selectionSets.map((selectionSet) => ({\n ...selectionSet,\n switchingSets: selectionSet.switchingSets.map((switchingSet) => ({\n ...switchingSet,\n tracks: switchingSet.tracks.map((track) => (track.id === trackId ? resolvedTrack : track)),\n })),\n })),\n } as Presentation;\n}\n\n/**\n * Configuration for track resolution.\n */\nexport interface TrackResolutionConfig<T extends TrackType = TrackType> {\n type: T;\n}\n\n/**\n * Resolves unresolved tracks using reactive composition.\n *\n * Reacts to state changes and schedules fetch tasks via ConcurrentRunner when\n * a selected track is unresolved. The ConcurrentRunner handles deduplication,\n * parallel execution, and cleanup.\n *\n * Generic version that works for video, audio, or text tracks based on config.\n * Type parameter T is inferred from config.type (use 'as const' for inference).\n */\nexport function resolveTrack<S extends TrackResolutionState, T extends TrackType>(\n { state }: { state: Signal<S> },\n config: TrackResolutionConfig<T>\n): () => void {\n // NOTE: This can/maybe will be pulled into a per-use case factory (e.g. something like createTaskRunner() with args TBD),\n // likely eventually passed down via config or a new \"definitions\" argument. This will allow us to decide if we want our task runner/scheduler\n // to e.g. run concurrently (like we currently are), serially with a queue, or abort the previous task and replace it with the newly scheduled one. (CJP).\n const runner = new ConcurrentRunner();\n\n const cleanup = effect(() => {\n const currentState = state.get();\n if (!canResolve(currentState, config)) return;\n\n const track = getSelectedTrack(currentState, config.type);\n if (!track) return;\n\n const resolvedTrack = track;\n\n runner.schedule(\n // NOTE: This can/maybe will be pulled into a per-use case factory (e.g. something like createResolveTrackTask(track, context, config)),\n // likely eventually passed down via config or a new \"definitions\" argument (CJP).\n new Task(\n async (signal) => {\n const response = await fetchResolvable(resolvedTrack, { signal });\n const text = await getResponseText(response);\n const mediaTrack = parseMediaPlaylist(text, resolvedTrack);\n\n // IMPORTANT: Read state.get().presentation at write time, not from the\n // captured currentState snapshot. Multiple Tasks may be running concurrently\n // (one per track being resolved), so the snapshot is likely already stale by\n // the time a task completes — a sibling task may have already written the\n // presentation with its own resolved track. Reading live state ensures each\n // task builds on top of whatever has been committed so far.\n const latest = state.get();\n const updatedPresentation = updateTrackInPresentation(latest.presentation!, mediaTrack);\n state.set({ ...latest, presentation: updatedPresentation } as S);\n },\n { id: track.id }\n )\n );\n });\n\n return () => {\n runner.abortAll();\n cleanup();\n };\n}\n"],"mappings":";;;;;;;AAmBA,SAAgB,WACd,OACA,QACS;CACT,MAAM,QAAQ,iBAAiB,OAAO,OAAO,KAAK;AAClD,KAAI,CAAC,MAAO,QAAO;AAEnB,QAAO,CAAC,gBAAgB,MAAM;;;;;;AAWhC,SAAgB,0BACd,cACA,eACc;CACd,MAAM,UAAU,cAAc;AAC9B,QAAO;EACL,GAAG;EACH,eAAe,aAAa,cAAc,KAAK,kBAAkB;GAC/D,GAAG;GACH,eAAe,aAAa,cAAc,KAAK,kBAAkB;IAC/D,GAAG;IACH,QAAQ,aAAa,OAAO,KAAK,UAAW,MAAM,OAAO,UAAU,gBAAgB,MAAO;IAC3F,EAAE;GACJ,EAAE;EACJ;;;;;;;;;;;;AAoBH,SAAgB,aACd,EAAE,SACF,QACY;CAIZ,MAAM,SAAS,IAAI,kBAAkB;CAErC,MAAM,UAAU,aAAa;EAC3B,MAAM,eAAe,MAAM,KAAK;AAChC,MAAI,CAAC,WAAW,cAAc,OAAO,CAAE;EAEvC,MAAM,QAAQ,iBAAiB,cAAc,OAAO,KAAK;AACzD,MAAI,CAAC,MAAO;EAEZ,MAAM,gBAAgB;AAEtB,SAAO,SAGL,IAAI,KACF,OAAO,WAAW;GAGhB,MAAM,aAAa,mBADN,MAAM,gBADF,MAAM,gBAAgB,eAAe,EAAE,QAAQ,CAAC,CACrB,EACA,cAAc;GAQ1D,MAAM,SAAS,MAAM,KAAK;GAC1B,MAAM,sBAAsB,0BAA0B,OAAO,cAAe,WAAW;AACvF,SAAM,IAAI;IAAE,GAAG;IAAQ,cAAc;IAAqB,CAAM;KAElE,EAAE,IAAI,MAAM,IAAI,CACjB,CACF;GACD;AAEF,cAAa;AACX,SAAO,UAAU;AACjB,WAAS"}
@@ -0,0 +1,130 @@
1
+ import { effect } from "../signals/effect.js";
2
+ import { update } from "../signals/primitives.js";
3
+ import { SelectedTrackIdKeyByType } from "../utils/track-selection.js";
4
+ //#region src/core/features/select-tracks.ts
5
+ /**
6
+ * Pick text track to activate.
7
+ *
8
+ * Selection priority (if enabled):
9
+ * 1. User preference (preferredSubtitleLanguage)
10
+ * 2. DEFAULT track (if enableDefaultTrack is true and track has DEFAULT=YES + AUTOSELECT=YES)
11
+ * 3. No auto-selection (user opt-in)
12
+ *
13
+ * By default, FORCED tracks are excluded per Apple's HLS spec.
14
+ *
15
+ * @param presentation - Presentation with text tracks
16
+ * @param config - Selection configuration
17
+ * @returns Track ID or undefined (no auto-selection)
18
+ */
19
+ function pickTextTrack(presentation, config) {
20
+ const textSet = presentation.selectionSets.find((set) => set.type === "text");
21
+ if (!textSet?.switchingSets?.[0]?.tracks.length) return void 0;
22
+ const tracks = textSet.switchingSets[0].tracks;
23
+ const availableTracks = config.includeForcedTracks ? tracks : tracks.filter((track) => !track.forced);
24
+ if (availableTracks.length === 0) return void 0;
25
+ const { preferredSubtitleLanguage, enableDefaultTrack = false } = config;
26
+ if (preferredSubtitleLanguage) {
27
+ const languageMatch = availableTracks.find((track) => track.language === preferredSubtitleLanguage);
28
+ if (languageMatch) return languageMatch.id;
29
+ }
30
+ if (enableDefaultTrack) {
31
+ const defaultTrack = availableTracks.find((track) => track.default === true);
32
+ if (defaultTrack) return defaultTrack.id;
33
+ }
34
+ }
35
+ /**
36
+ * Check if we can select a track of the given type.
37
+ *
38
+ * Returns true when:
39
+ * - Presentation exists
40
+ * - Has tracks of the specified type
41
+ *
42
+ * Generic over track type - works for video, audio, or text.
43
+ */
44
+ function canSelectTrack(state, config) {
45
+ return !!state?.presentation?.selectionSets?.find(({ type }) => type === config.type)?.switchingSets?.[0]?.tracks.length;
46
+ }
47
+ /**
48
+ * Check if we should select a track of the given type.
49
+ *
50
+ * Returns true when:
51
+ * - Track of this type is not already selected
52
+ *
53
+ * Generic over track type - works for video, audio, or text.
54
+ *
55
+ * @TODO figure out reactive model for ABR cases - right now we're only selecting
56
+ * if we have nothing selected (CJP)
57
+ */
58
+ function shouldSelectTrack(state, config) {
59
+ return !state[SelectedTrackIdKeyByType[config.type]];
60
+ }
61
+ /**
62
+ * Select video track orchestration.
63
+ *
64
+ * Selects video track when:
65
+ * - Presentation exists
66
+ * - No video track is selected yet
67
+ *
68
+ * Uses bandwidth-based quality selection algorithm.
69
+ *
70
+ * @example
71
+ * const cleanup = selectVideoTrack(
72
+ * { state, owners, events },
73
+ * { initialBandwidth: 2_000_000 }
74
+ * );
75
+ */
76
+ function selectVideoTrack({ state }, config = { type: "video" }) {
77
+ return effect(() => {
78
+ const currentState = state.get();
79
+ if (!canSelectTrack(currentState, config) || !shouldSelectTrack(currentState, config)) return;
80
+ const selectedTrackId = currentState.presentation?.selectionSets.find(({ type }) => type === config.type)?.switchingSets[0]?.tracks[0]?.id;
81
+ if (selectedTrackId) update(state, { [SelectedTrackIdKeyByType[config.type]]: selectedTrackId });
82
+ });
83
+ }
84
+ /**
85
+ * Select audio track orchestration.
86
+ *
87
+ * Selects audio track when:
88
+ * - Presentation exists
89
+ * - No audio track is selected yet
90
+ *
91
+ * Uses language and preference-based selection.
92
+ *
93
+ * @example
94
+ * const cleanup = selectAudioTrack(
95
+ * { state, owners, events },
96
+ * { preferredAudioLanguage: 'en' }
97
+ * );
98
+ */
99
+ function selectAudioTrack({ state }, config = { type: "audio" }) {
100
+ return effect(() => {
101
+ const currentState = state.get();
102
+ if (!canSelectTrack(currentState, config) || !shouldSelectTrack(currentState, config)) return;
103
+ const selectedTrackId = currentState.presentation?.selectionSets.find(({ type }) => type === "audio")?.switchingSets[0]?.tracks[0]?.id;
104
+ if (selectedTrackId) update(state, { selectedAudioTrackId: selectedTrackId });
105
+ });
106
+ }
107
+ /**
108
+ * Select text track orchestration.
109
+ *
110
+ * Selects text track when:
111
+ * - Presentation exists
112
+ * - No text track is selected yet
113
+ *
114
+ * Note: Currently does not auto-select (user opt-in).
115
+ *
116
+ * @example
117
+ * const cleanup = selectTextTrack({ state, owners, events }, {});
118
+ */
119
+ function selectTextTrack({ state }, config = { type: "text" }) {
120
+ return effect(() => {
121
+ const currentState = state.get();
122
+ if (!canSelectTrack(currentState, config) || !shouldSelectTrack(currentState, config)) return;
123
+ const selectedTextTrackId = pickTextTrack(currentState.presentation, config);
124
+ if (selectedTextTrackId) update(state, { selectedTextTrackId });
125
+ });
126
+ }
127
+ //#endregion
128
+ export { selectAudioTrack, selectTextTrack, selectVideoTrack };
129
+
130
+ //# sourceMappingURL=select-tracks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"select-tracks.js","names":[],"sources":["../../../../src/core/features/select-tracks.ts"],"sourcesContent":["import { DEFAULT_QUALITY_CONFIG, selectQuality } from '../abr/quality-selection';\nimport { effect } from '../signals/effect';\nimport { type Signal, update } from '../signals/primitives';\nimport type { AudioSelectionSet, Presentation, TrackType, VideoSelectionSet } from '../types';\nimport { SelectedTrackIdKeyByType } from '../utils/track-selection';\n\n/**\n * Default initial bandwidth estimate for cold start (bits per second).\n * Conservative 1 Mbps to avoid over-selecting on slow connections.\n */\nexport const DEFAULT_INITIAL_BANDWIDTH = 1_000_000;\n\n/**\n * State shape for track selection.\n */\nexport interface TrackSelectionState {\n presentation?: Presentation;\n selectedVideoTrackId?: string;\n selectedAudioTrackId?: string;\n selectedTextTrackId?: string;\n}\n\n/**\n * Owners shape for track selection.\n * Currently empty - reserved for future use (e.g., bandwidth estimator).\n */\nexport type TrackSelectionOwners = Record<string, never>;\n\n/**\n * Action types for track selection.\n * Reserved for future event-driven selection triggers.\n */\nexport type TrackSelectionAction = { type: 'presentation-loaded' };\n\n/**\n * Base configuration for track selection.\n * Generic over track type with discriminant `type` field.\n */\nexport interface TrackSelectionConfig<T extends TrackType = TrackType> {\n type: T;\n}\n\n/**\n * Configuration for video track selection.\n * Generic with default to 'video' for convenience.\n */\nexport interface VideoSelectionConfig<T extends TrackType = 'video'> extends TrackSelectionConfig<T> {\n /**\n * Initial bandwidth estimate for cold start (bits per second).\n * Used to select video quality before we have real measurements.\n * Default: 1 Mbps (conservative).\n */\n initialBandwidth?: number;\n\n /**\n * Safety margin for quality selection (0-1).\n * Default: 0.85 (15% headroom).\n */\n safetyMargin?: number;\n}\n\n/**\n * Configuration for audio track selection.\n * Generic with default to 'audio' for convenience.\n */\nexport interface AudioSelectionConfig<T extends TrackType = 'audio'> extends TrackSelectionConfig<T> {\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/**\n * Configuration for text track selection.\n * Generic with default to 'text' for convenience.\n */\nexport interface TextSelectionConfig<T extends TrackType = 'text'> extends TrackSelectionConfig<T> {\n /**\n * Preferred subtitle language (ISO 639 code, e.g., \"en\", \"es\").\n * If specified, selects matching 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 * Note: Per Apple's HLS spec, if content has forced and regular subtitles\n * in the same language, the regular track MUST contain both forced and\n * regular content. Therefore, forced-only tracks are redundant and excluded\n * by default.\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 * When enabled, tracks marked with both DEFAULT=YES and AUTOSELECT=YES\n * will be automatically selected if no user preference matches.\n */\n enableDefaultTrack?: boolean;\n}\n\n// =============================================================================\n// Helper Functions (Pure Selection Logic)\n// =============================================================================\n\n/**\n * Pick video track using quality selection algorithm.\n *\n * Uses bandwidth-based selection with safety margin to pick\n * the highest quality track that fits available bandwidth.\n *\n * @param presentation - Presentation with video tracks\n * @param config - Selection configuration (bandwidth, safety margin)\n * @returns Selected video track ID, or undefined if no video tracks\n */\nexport function pickVideoTrack(presentation: Presentation, config: VideoSelectionConfig): string | undefined {\n const videoSet = presentation.selectionSets.find((set) => set.type === 'video') as VideoSelectionSet | undefined;\n\n if (!videoSet || videoSet.switchingSets.length === 0) {\n return undefined;\n }\n\n // Get first switching set's tracks (HLS typically has one switching set per type)\n const switchingSet = videoSet.switchingSets[0];\n if (!switchingSet || switchingSet.tracks.length === 0) {\n return undefined;\n }\n\n const initialBandwidth = config.initialBandwidth ?? DEFAULT_INITIAL_BANDWIDTH;\n const safetyMargin = config.safetyMargin ?? DEFAULT_QUALITY_CONFIG.safetyMargin;\n\n // selectQuality works with both partially resolved and resolved tracks\n const selected = selectQuality(switchingSet.tracks as any, initialBandwidth, { safetyMargin });\n\n return selected?.id;\n}\n\n/**\n * Pick audio track.\n *\n * Selection priority:\n * 1. First track matching preferred language (if specified)\n * 2. First default track\n * 3. First audio track\n *\n * @param presentation - Presentation with audio tracks\n * @param config - Selection configuration (preferred language)\n * @returns Selected audio track ID, or undefined if no audio tracks\n */\nexport function pickAudioTrack(presentation: Presentation, config: AudioSelectionConfig): string | undefined {\n const audioSet = presentation.selectionSets.find((set) => set.type === 'audio') as AudioSelectionSet | undefined;\n\n if (!audioSet || audioSet.switchingSets.length === 0) {\n return undefined;\n }\n\n // Get first switching set's tracks\n const switchingSet = audioSet.switchingSets[0];\n if (!switchingSet || switchingSet.tracks.length === 0) {\n return undefined;\n }\n\n const tracks = switchingSet.tracks;\n\n // Try preferred language first\n if (config.preferredAudioLanguage) {\n const languageMatch = tracks.find((track) => track.language === config.preferredAudioLanguage);\n if (languageMatch) {\n return languageMatch.id;\n }\n }\n\n // Try default track\n const defaultTrack = tracks.find((track) => track.default === true);\n if (defaultTrack) {\n return defaultTrack.id;\n }\n\n // Fall back to first track\n return tracks[0]?.id;\n}\n\n/**\n * Pick text track to activate.\n *\n * Selection priority (if enabled):\n * 1. User preference (preferredSubtitleLanguage)\n * 2. DEFAULT track (if enableDefaultTrack is true and track has DEFAULT=YES + AUTOSELECT=YES)\n * 3. No auto-selection (user opt-in)\n *\n * By default, FORCED tracks are excluded per Apple's HLS spec.\n *\n * @param presentation - Presentation with text tracks\n * @param config - Selection configuration\n * @returns Track ID or undefined (no auto-selection)\n */\nexport function pickTextTrack(presentation: Presentation, config: TextSelectionConfig): string | undefined {\n const textSet = presentation.selectionSets.find((set) => set.type === 'text');\n if (!textSet?.switchingSets?.[0]?.tracks.length) return undefined;\n\n const tracks = textSet.switchingSets[0].tracks;\n\n // Filter out FORCED tracks by default (following hls.js/http-streaming pattern)\n // Per Apple spec: regular tracks MUST contain forced content when both exist\n const availableTracks = config.includeForcedTracks ? tracks : tracks.filter((track) => !track.forced);\n\n if (availableTracks.length === 0) return undefined;\n\n const { preferredSubtitleLanguage, enableDefaultTrack = false } = config;\n\n // 1. Preferred language match (if specified)\n if (preferredSubtitleLanguage) {\n const languageMatch = availableTracks.find((track) => track.language === preferredSubtitleLanguage);\n if (languageMatch) return languageMatch.id;\n }\n\n // 2. DEFAULT track (if enabled AND track has both DEFAULT=YES + AUTOSELECT=YES)\n // Note: Parser only sets default=true when BOTH attributes present\n if (enableDefaultTrack) {\n const defaultTrack = availableTracks.find((track) => track.default === true);\n if (defaultTrack) return defaultTrack.id;\n }\n\n // 3. User opt-in (no auto-selection)\n return undefined;\n}\n\n/**\n * Check if we can select a track of the given type.\n *\n * Returns true when:\n * - Presentation exists\n * - Has tracks of the specified type\n *\n * Generic over track type - works for video, audio, or text.\n */\nexport function canSelectTrack<T extends TrackType>(\n state: TrackSelectionState,\n config: TrackSelectionConfig<T>\n): boolean {\n return !!state?.presentation?.selectionSets?.find(({ type }) => type === config.type)?.switchingSets?.[0]?.tracks\n .length;\n}\n\n/**\n * Check if we should select a track of the given type.\n *\n * Returns true when:\n * - Track of this type is not already selected\n *\n * Generic over track type - works for video, audio, or text.\n *\n * @TODO figure out reactive model for ABR cases - right now we're only selecting\n * if we have nothing selected (CJP)\n */\nexport function shouldSelectTrack<T extends TrackType>(\n state: TrackSelectionState,\n config: TrackSelectionConfig<T>\n): boolean {\n return !state[SelectedTrackIdKeyByType[config.type]];\n}\n\n// =============================================================================\n// Orchestrations\n// =============================================================================\n\n/**\n * Select video track orchestration.\n *\n * Selects video track when:\n * - Presentation exists\n * - No video track is selected yet\n *\n * Uses bandwidth-based quality selection algorithm.\n *\n * @example\n * const cleanup = selectVideoTrack(\n * { state, owners, events },\n * { initialBandwidth: 2_000_000 }\n * );\n */\nexport function selectVideoTrack<S extends TrackSelectionState>(\n { state }: { state: Signal<S> },\n config: VideoSelectionConfig = { type: 'video' }\n): () => void {\n return effect(() => {\n const currentState = state.get();\n if (!canSelectTrack(currentState, config) || !shouldSelectTrack(currentState, config)) return;\n\n // Just to have basic functionality/POC, simply selecting the first track\n const selectedTrackId = currentState.presentation?.selectionSets.find(({ type }) => type === config.type)\n ?.switchingSets[0]?.tracks[0]?.id;\n\n if (selectedTrackId) {\n const selectedTrackKey = SelectedTrackIdKeyByType[config.type];\n const patch: Partial<TrackSelectionState> = { [selectedTrackKey]: selectedTrackId };\n update(state, patch);\n }\n });\n}\n\n/**\n * Select audio track orchestration.\n *\n * Selects audio track when:\n * - Presentation exists\n * - No audio track is selected yet\n *\n * Uses language and preference-based selection.\n *\n * @example\n * const cleanup = selectAudioTrack(\n * { state, owners, events },\n * { preferredAudioLanguage: 'en' }\n * );\n */\nexport function selectAudioTrack<S extends TrackSelectionState>(\n { state }: { state: Signal<S> },\n config: AudioSelectionConfig = { type: 'audio' }\n): () => void {\n return effect(() => {\n const currentState = state.get();\n if (!canSelectTrack(currentState, config) || !shouldSelectTrack(currentState, config)) return;\n\n // Just to have basic functionality/POC, simply selecting the first track\n const selectedTrackId = currentState.presentation?.selectionSets.find(({ type }) => type === 'audio')\n ?.switchingSets[0]?.tracks[0]?.id;\n\n if (selectedTrackId) {\n const patch: Partial<TrackSelectionState> = { selectedAudioTrackId: selectedTrackId };\n update(state, patch);\n }\n });\n}\n\n/**\n * Select text track orchestration.\n *\n * Selects text track when:\n * - Presentation exists\n * - No text track is selected yet\n *\n * Note: Currently does not auto-select (user opt-in).\n *\n * @example\n * const cleanup = selectTextTrack({ state, owners, events }, {});\n */\nexport function selectTextTrack<S extends TrackSelectionState>(\n { state }: { state: Signal<S> },\n config: TextSelectionConfig = { type: 'text' }\n): () => void {\n return effect(() => {\n const currentState = state.get();\n if (!canSelectTrack(currentState, config) || !shouldSelectTrack(currentState, config)) return;\n\n // Text tracks are user opt-in - don't auto-select\n const selectedTextTrackId = pickTextTrack(currentState.presentation!, config);\n\n if (selectedTextTrackId) {\n const patch: Partial<TrackSelectionState> = { selectedTextTrackId };\n update(state, patch);\n }\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAwMA,SAAgB,cAAc,cAA4B,QAAiD;CACzG,MAAM,UAAU,aAAa,cAAc,MAAM,QAAQ,IAAI,SAAS,OAAO;AAC7E,KAAI,CAAC,SAAS,gBAAgB,IAAI,OAAO,OAAQ,QAAO,KAAA;CAExD,MAAM,SAAS,QAAQ,cAAc,GAAG;CAIxC,MAAM,kBAAkB,OAAO,sBAAsB,SAAS,OAAO,QAAQ,UAAU,CAAC,MAAM,OAAO;AAErG,KAAI,gBAAgB,WAAW,EAAG,QAAO,KAAA;CAEzC,MAAM,EAAE,2BAA2B,qBAAqB,UAAU;AAGlE,KAAI,2BAA2B;EAC7B,MAAM,gBAAgB,gBAAgB,MAAM,UAAU,MAAM,aAAa,0BAA0B;AACnG,MAAI,cAAe,QAAO,cAAc;;AAK1C,KAAI,oBAAoB;EACtB,MAAM,eAAe,gBAAgB,MAAM,UAAU,MAAM,YAAY,KAAK;AAC5E,MAAI,aAAc,QAAO,aAAa;;;;;;;;;;;;AAgB1C,SAAgB,eACd,OACA,QACS;AACT,QAAO,CAAC,CAAC,OAAO,cAAc,eAAe,MAAM,EAAE,WAAW,SAAS,OAAO,KAAK,EAAE,gBAAgB,IAAI,OACxG;;;;;;;;;;;;;AAcL,SAAgB,kBACd,OACA,QACS;AACT,QAAO,CAAC,MAAM,yBAAyB,OAAO;;;;;;;;;;;;;;;;;AAsBhD,SAAgB,iBACd,EAAE,SACF,SAA+B,EAAE,MAAM,SAAS,EACpC;AACZ,QAAO,aAAa;EAClB,MAAM,eAAe,MAAM,KAAK;AAChC,MAAI,CAAC,eAAe,cAAc,OAAO,IAAI,CAAC,kBAAkB,cAAc,OAAO,CAAE;EAGvF,MAAM,kBAAkB,aAAa,cAAc,cAAc,MAAM,EAAE,WAAW,SAAS,OAAO,KAAK,EACrG,cAAc,IAAI,OAAO,IAAI;AAEjC,MAAI,gBAGF,QAAO,OADqC,GADnB,yBAAyB,OAAO,QACS,iBAAiB,CAC/D;GAEtB;;;;;;;;;;;;;;;;;AAkBJ,SAAgB,iBACd,EAAE,SACF,SAA+B,EAAE,MAAM,SAAS,EACpC;AACZ,QAAO,aAAa;EAClB,MAAM,eAAe,MAAM,KAAK;AAChC,MAAI,CAAC,eAAe,cAAc,OAAO,IAAI,CAAC,kBAAkB,cAAc,OAAO,CAAE;EAGvF,MAAM,kBAAkB,aAAa,cAAc,cAAc,MAAM,EAAE,WAAW,SAAS,QAAQ,EACjG,cAAc,IAAI,OAAO,IAAI;AAEjC,MAAI,gBAEF,QAAO,OADqC,EAAE,sBAAsB,iBAAiB,CACjE;GAEtB;;;;;;;;;;;;;;AAeJ,SAAgB,gBACd,EAAE,SACF,SAA8B,EAAE,MAAM,QAAQ,EAClC;AACZ,QAAO,aAAa;EAClB,MAAM,eAAe,MAAM,KAAK;AAChC,MAAI,CAAC,eAAe,cAAc,OAAO,IAAI,CAAC,kBAAkB,cAAc,OAAO,CAAE;EAGvF,MAAM,sBAAsB,cAAc,aAAa,cAAe,OAAO;AAE7E,MAAI,oBAEF,QAAO,OADqC,EAAE,qBAAqB,CAC/C;GAEtB"}
@@ -0,0 +1,26 @@
1
+ import { effect } from "../signals/effect.js";
2
+ import { computed, update } from "../signals/primitives.js";
3
+ //#region src/core/features/sync-preload-attribute.ts
4
+ /**
5
+ * Syncs preload attribute from mediaElement to state.
6
+ *
7
+ * Watches the owners signal for mediaElement changes and copies the
8
+ * preload attribute to state when no explicit value has been set.
9
+ * An explicit value (set via SpfMedia.preload) always wins.
10
+ *
11
+ * @example
12
+ * const cleanup = syncPreloadAttribute({ state, owners });
13
+ */
14
+ function syncPreloadAttribute({ state, owners }) {
15
+ const mediaElement = computed(() => owners.get().mediaElement);
16
+ return effect(() => {
17
+ if (state.get().preload !== void 0) return;
18
+ const preload = mediaElement.get()?.preload || void 0;
19
+ if (preload === void 0) return;
20
+ update(state, { preload });
21
+ });
22
+ }
23
+ //#endregion
24
+ export { syncPreloadAttribute };
25
+
26
+ //# sourceMappingURL=sync-preload-attribute.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync-preload-attribute.js","names":[],"sources":["../../../../src/core/features/sync-preload-attribute.ts"],"sourcesContent":["import { effect } from '../signals/effect';\nimport { computed, type Signal, update } from '../signals/primitives';\nimport type { MediaElementLike } from '../types';\nimport type { PresentationState } from './resolve-presentation';\n\n/**\n * Owners shape for preload attribute sync.\n */\nexport interface PlatformOwners {\n mediaElement?: MediaElementLike | undefined;\n}\n\n/**\n * Syncs preload attribute from mediaElement to state.\n *\n * Watches the owners signal for mediaElement changes and copies the\n * preload attribute to state when no explicit value has been set.\n * An explicit value (set via SpfMedia.preload) always wins.\n *\n * @example\n * const cleanup = syncPreloadAttribute({ state, owners });\n */\nexport function syncPreloadAttribute<S extends PresentationState, O extends PlatformOwners>({\n state,\n owners,\n}: {\n state: Signal<S>;\n owners: Signal<O>;\n}): () => void {\n const mediaElement = computed(() => owners.get().mediaElement);\n return effect(() => {\n if (state.get().preload !== undefined) return;\n const preload = mediaElement.get()?.preload || undefined;\n if (preload === undefined) return;\n const patch: Partial<PresentationState> = { preload: preload as 'auto' | 'metadata' | 'none' };\n update(state, patch);\n });\n}\n"],"mappings":";;;;;;;;;;;;;AAsBA,SAAgB,qBAA4E,EAC1F,OACA,UAIa;CACb,MAAM,eAAe,eAAe,OAAO,KAAK,CAAC,aAAa;AAC9D,QAAO,aAAa;AAClB,MAAI,MAAM,KAAK,CAAC,YAAY,KAAA,EAAW;EACvC,MAAM,UAAU,aAAa,KAAK,EAAE,WAAW,KAAA;AAC/C,MAAI,YAAY,KAAA,EAAW;AAE3B,SAAO,OADmC,EAAW,SAAyC,CAC1E;GACpB"}
@@ -0,0 +1,134 @@
1
+ //#region src/core/hls/parse-attributes.ts
2
+ /**
3
+ * Parse HLS attribute list from a tag line.
4
+ * Handles both quoted and unquoted values.
5
+ */
6
+ function parseAttributeList(line) {
7
+ const attributes = /* @__PURE__ */ new Map();
8
+ for (const match of line.matchAll(/([A-Z0-9-]+)=(?:"([^"]*)"|([^,]*))/g)) {
9
+ const key = match[1];
10
+ const value = match[2] ?? match[3] ?? "";
11
+ if (key) attributes.set(key, value);
12
+ }
13
+ return attributes;
14
+ }
15
+ /**
16
+ * Parse RESOLUTION attribute value (WIDTHxHEIGHT).
17
+ */
18
+ function parseResolution(value) {
19
+ const match = /^(\d+)x(\d+)$/.exec(value);
20
+ if (!match) return null;
21
+ return {
22
+ width: Number.parseInt(match[1], 10),
23
+ height: Number.parseInt(match[2], 10)
24
+ };
25
+ }
26
+ /**
27
+ * Parse FRAME-RATE attribute to rational frame rate.
28
+ */
29
+ function parseFrameRate(value) {
30
+ const fps = Number.parseFloat(value);
31
+ if (Number.isNaN(fps) || fps <= 0) return void 0;
32
+ if (Math.abs(fps - 23.976) < .01) return {
33
+ frameRateNumerator: 24e3,
34
+ frameRateDenominator: 1001
35
+ };
36
+ if (Math.abs(fps - 29.97) < .01) return {
37
+ frameRateNumerator: 3e4,
38
+ frameRateDenominator: 1001
39
+ };
40
+ if (Math.abs(fps - 59.94) < .01) return {
41
+ frameRateNumerator: 6e4,
42
+ frameRateDenominator: 1001
43
+ };
44
+ if (fps % 1 === 0) return { frameRateNumerator: Math.round(fps) };
45
+ return { frameRateNumerator: Math.round(fps) };
46
+ }
47
+ /**
48
+ * Parse CODECS attribute into separate video and audio codecs.
49
+ */
50
+ function parseCodecs(codecs) {
51
+ const parts = codecs.split(",").map((s) => s.trim());
52
+ const result = {};
53
+ for (const codec of parts) if (codec.startsWith("avc1.") || codec.startsWith("hvc1.") || codec.startsWith("hev1.")) result.video = codec;
54
+ else if (codec.startsWith("mp4a.")) result.audio = codec;
55
+ return result;
56
+ }
57
+ /**
58
+ * Parse #EXTINF duration value.
59
+ */
60
+ function parseExtInfDuration(value) {
61
+ const durationPart = value.split(",")[0] ?? value;
62
+ const duration = Number.parseFloat(durationPart);
63
+ return Number.isNaN(duration) ? 0 : duration;
64
+ }
65
+ /**
66
+ * Parse BYTERANGE attribute value.
67
+ * Format: "length[@offset]"
68
+ * If offset is omitted, it continues from the previous byte range end.
69
+ */
70
+ function parseByteRange(value, previousEnd) {
71
+ const match = /^(\d+)(?:@(\d+))?$/.exec(value);
72
+ if (!match) return null;
73
+ const length = Number.parseInt(match[1], 10);
74
+ if (Number.isNaN(length)) return null;
75
+ let start;
76
+ if (match[2] !== void 0) {
77
+ start = Number.parseInt(match[2], 10);
78
+ if (Number.isNaN(start)) return null;
79
+ } else if (previousEnd !== void 0) start = previousEnd;
80
+ else return null;
81
+ return {
82
+ start,
83
+ end: start + length - 1
84
+ };
85
+ }
86
+ /**
87
+ * Create AttributeList from raw attribute string.
88
+ */
89
+ function createAttributeList(line) {
90
+ const map = parseAttributeList(line);
91
+ return {
92
+ get(key) {
93
+ return map.get(key);
94
+ },
95
+ getInt(key, defaultValue) {
96
+ const value = map.get(key);
97
+ if (value === void 0) return defaultValue;
98
+ const parsed = Number.parseInt(value, 10);
99
+ return Number.isNaN(parsed) ? defaultValue : parsed;
100
+ },
101
+ getFloat(key, defaultValue) {
102
+ const value = map.get(key);
103
+ if (value === void 0) return defaultValue;
104
+ const parsed = Number.parseFloat(value);
105
+ return Number.isNaN(parsed) ? defaultValue : parsed;
106
+ },
107
+ getBool(key) {
108
+ return map.get(key) === "YES";
109
+ },
110
+ getResolution(key) {
111
+ const value = map.get(key);
112
+ if (!value) return void 0;
113
+ return parseResolution(value) ?? void 0;
114
+ },
115
+ getFrameRate(key) {
116
+ const value = map.get(key);
117
+ if (!value) return void 0;
118
+ return parseFrameRate(value);
119
+ }
120
+ };
121
+ }
122
+ /**
123
+ * Match a tag and extract its attributes.
124
+ * Returns null if the line doesn't match the tag.
125
+ */
126
+ function matchTag(line, tag) {
127
+ const prefix = `#${tag}:`;
128
+ if (!line.startsWith(prefix)) return null;
129
+ return createAttributeList(line.slice(prefix.length));
130
+ }
131
+ //#endregion
132
+ export { matchTag, parseByteRange, parseCodecs, parseExtInfDuration };
133
+
134
+ //# sourceMappingURL=parse-attributes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse-attributes.js","names":[],"sources":["../../../../src/core/hls/parse-attributes.ts"],"sourcesContent":["import type { FrameRate } from '../types';\n\n/**\n * Parse HLS attribute list from a tag line.\n * Handles both quoted and unquoted values.\n */\nexport function parseAttributeList(line: string): Map<string, string> {\n const attributes = new Map<string, string>();\n const regex = /([A-Z0-9-]+)=(?:\"([^\"]*)\"|([^,]*))/g;\n\n for (const match of line.matchAll(regex)) {\n const key = match[1];\n const value = match[2] ?? match[3] ?? '';\n if (key) {\n attributes.set(key, value);\n }\n }\n\n return attributes;\n}\n\n/**\n * Parse RESOLUTION attribute value (WIDTHxHEIGHT).\n */\nexport function parseResolution(value: string): { width: number; height: number } | null {\n const match = /^(\\d+)x(\\d+)$/.exec(value);\n if (!match) return null;\n\n const width = Number.parseInt(match[1]!, 10);\n const height = Number.parseInt(match[2]!, 10);\n\n return { width, height };\n}\n\n/**\n * Parse FRAME-RATE attribute to rational frame rate.\n */\nexport function parseFrameRate(value: string): FrameRate | undefined {\n const fps = Number.parseFloat(value);\n if (Number.isNaN(fps) || fps <= 0) return undefined;\n\n // Common frame rates with tolerance for floating point precision\n if (Math.abs(fps - 23.976) < 0.01) {\n return { frameRateNumerator: 24000, frameRateDenominator: 1001 };\n }\n if (Math.abs(fps - 29.97) < 0.01) {\n return { frameRateNumerator: 30000, frameRateDenominator: 1001 };\n }\n if (Math.abs(fps - 59.94) < 0.01) {\n return { frameRateNumerator: 60000, frameRateDenominator: 1001 };\n }\n\n // Integer frame rates\n if (fps % 1 === 0) {\n return { frameRateNumerator: Math.round(fps) };\n }\n\n // Default: use rounded value\n return { frameRateNumerator: Math.round(fps) };\n}\n\n/**\n * Parse CODECS attribute into separate video and audio codecs.\n */\nexport function parseCodecs(codecs: string): { video?: string; audio?: string } {\n const parts = codecs.split(',').map((s) => s.trim());\n const result: { video?: string; audio?: string } = {};\n\n for (const codec of parts) {\n if (codec.startsWith('avc1.') || codec.startsWith('hvc1.') || codec.startsWith('hev1.')) {\n result.video = codec;\n } else if (codec.startsWith('mp4a.')) {\n result.audio = codec;\n }\n }\n\n return result;\n}\n\n/**\n * Parse #EXTINF duration value.\n */\nexport function parseExtInfDuration(value: string): number {\n const durationPart = value.split(',')[0] ?? value;\n const duration = Number.parseFloat(durationPart);\n return Number.isNaN(duration) ? 0 : duration;\n}\n\n/**\n * Parse BYTERANGE attribute value.\n * Format: \"length[@offset]\"\n * If offset is omitted, it continues from the previous byte range end.\n */\nexport function parseByteRange(value: string, previousEnd?: number): { start: number; end: number } | null {\n const match = /^(\\d+)(?:@(\\d+))?$/.exec(value);\n if (!match) return null;\n\n const length = Number.parseInt(match[1]!, 10);\n if (Number.isNaN(length)) return null;\n\n let start: number;\n if (match[2] !== undefined) {\n start = Number.parseInt(match[2], 10);\n if (Number.isNaN(start)) return null;\n } else if (previousEnd !== undefined) {\n start = previousEnd;\n } else {\n return null;\n }\n\n return { start, end: start + length - 1 };\n}\n\n/**\n * AttributeList - Typed attribute access wrapper.\n */\nexport interface AttributeList {\n get: (key: string) => string | undefined;\n getInt: (key: string, defaultValue?: number) => number | undefined;\n getFloat: (key: string, defaultValue?: number) => number | undefined;\n getBool: (key: string) => boolean;\n getResolution: (key: string) => { width: number; height: number } | undefined;\n getFrameRate: (key: string) => FrameRate | undefined;\n}\n\n/**\n * Create AttributeList from raw attribute string.\n */\nexport function createAttributeList(line: string): AttributeList {\n const map = parseAttributeList(line);\n\n return {\n get(key: string): string | undefined {\n return map.get(key);\n },\n\n getInt(key: string, defaultValue?: number): number | undefined {\n const value = map.get(key);\n if (value === undefined) return defaultValue;\n const parsed = Number.parseInt(value, 10);\n return Number.isNaN(parsed) ? defaultValue : parsed;\n },\n\n getFloat(key: string, defaultValue?: number): number | undefined {\n const value = map.get(key);\n if (value === undefined) return defaultValue;\n const parsed = Number.parseFloat(value);\n return Number.isNaN(parsed) ? defaultValue : parsed;\n },\n\n getBool(key: string): boolean {\n return map.get(key) === 'YES';\n },\n\n getResolution(key: string): { width: number; height: number } | undefined {\n const value = map.get(key);\n if (!value) return undefined;\n return parseResolution(value) ?? undefined;\n },\n\n getFrameRate(key: string): FrameRate | undefined {\n const value = map.get(key);\n if (!value) return undefined;\n return parseFrameRate(value);\n },\n };\n}\n\n/**\n * Match a tag and extract its attributes.\n * Returns null if the line doesn't match the tag.\n */\nexport function matchTag(line: string, tag: string): AttributeList | null {\n const prefix = `#${tag}:`;\n if (!line.startsWith(prefix)) return null;\n return createAttributeList(line.slice(prefix.length));\n}\n"],"mappings":";;;;;AAMA,SAAgB,mBAAmB,MAAmC;CACpE,MAAM,6BAAa,IAAI,KAAqB;AAG5C,MAAK,MAAM,SAAS,KAAK,SAFX,sCAE0B,EAAE;EACxC,MAAM,MAAM,MAAM;EAClB,MAAM,QAAQ,MAAM,MAAM,MAAM,MAAM;AACtC,MAAI,IACF,YAAW,IAAI,KAAK,MAAM;;AAI9B,QAAO;;;;;AAMT,SAAgB,gBAAgB,OAAyD;CACvF,MAAM,QAAQ,gBAAgB,KAAK,MAAM;AACzC,KAAI,CAAC,MAAO,QAAO;AAKnB,QAAO;EAAE,OAHK,OAAO,SAAS,MAAM,IAAK,GAAG;EAG5B,QAFD,OAAO,SAAS,MAAM,IAAK,GAAG;EAErB;;;;;AAM1B,SAAgB,eAAe,OAAsC;CACnE,MAAM,MAAM,OAAO,WAAW,MAAM;AACpC,KAAI,OAAO,MAAM,IAAI,IAAI,OAAO,EAAG,QAAO,KAAA;AAG1C,KAAI,KAAK,IAAI,MAAM,OAAO,GAAG,IAC3B,QAAO;EAAE,oBAAoB;EAAO,sBAAsB;EAAM;AAElE,KAAI,KAAK,IAAI,MAAM,MAAM,GAAG,IAC1B,QAAO;EAAE,oBAAoB;EAAO,sBAAsB;EAAM;AAElE,KAAI,KAAK,IAAI,MAAM,MAAM,GAAG,IAC1B,QAAO;EAAE,oBAAoB;EAAO,sBAAsB;EAAM;AAIlE,KAAI,MAAM,MAAM,EACd,QAAO,EAAE,oBAAoB,KAAK,MAAM,IAAI,EAAE;AAIhD,QAAO,EAAE,oBAAoB,KAAK,MAAM,IAAI,EAAE;;;;;AAMhD,SAAgB,YAAY,QAAoD;CAC9E,MAAM,QAAQ,OAAO,MAAM,IAAI,CAAC,KAAK,MAAM,EAAE,MAAM,CAAC;CACpD,MAAM,SAA6C,EAAE;AAErD,MAAK,MAAM,SAAS,MAClB,KAAI,MAAM,WAAW,QAAQ,IAAI,MAAM,WAAW,QAAQ,IAAI,MAAM,WAAW,QAAQ,CACrF,QAAO,QAAQ;UACN,MAAM,WAAW,QAAQ,CAClC,QAAO,QAAQ;AAInB,QAAO;;;;;AAMT,SAAgB,oBAAoB,OAAuB;CACzD,MAAM,eAAe,MAAM,MAAM,IAAI,CAAC,MAAM;CAC5C,MAAM,WAAW,OAAO,WAAW,aAAa;AAChD,QAAO,OAAO,MAAM,SAAS,GAAG,IAAI;;;;;;;AAQtC,SAAgB,eAAe,OAAe,aAA6D;CACzG,MAAM,QAAQ,qBAAqB,KAAK,MAAM;AAC9C,KAAI,CAAC,MAAO,QAAO;CAEnB,MAAM,SAAS,OAAO,SAAS,MAAM,IAAK,GAAG;AAC7C,KAAI,OAAO,MAAM,OAAO,CAAE,QAAO;CAEjC,IAAI;AACJ,KAAI,MAAM,OAAO,KAAA,GAAW;AAC1B,UAAQ,OAAO,SAAS,MAAM,IAAI,GAAG;AACrC,MAAI,OAAO,MAAM,MAAM,CAAE,QAAO;YACvB,gBAAgB,KAAA,EACzB,SAAQ;KAER,QAAO;AAGT,QAAO;EAAE;EAAO,KAAK,QAAQ,SAAS;EAAG;;;;;AAkB3C,SAAgB,oBAAoB,MAA6B;CAC/D,MAAM,MAAM,mBAAmB,KAAK;AAEpC,QAAO;EACL,IAAI,KAAiC;AACnC,UAAO,IAAI,IAAI,IAAI;;EAGrB,OAAO,KAAa,cAA2C;GAC7D,MAAM,QAAQ,IAAI,IAAI,IAAI;AAC1B,OAAI,UAAU,KAAA,EAAW,QAAO;GAChC,MAAM,SAAS,OAAO,SAAS,OAAO,GAAG;AACzC,UAAO,OAAO,MAAM,OAAO,GAAG,eAAe;;EAG/C,SAAS,KAAa,cAA2C;GAC/D,MAAM,QAAQ,IAAI,IAAI,IAAI;AAC1B,OAAI,UAAU,KAAA,EAAW,QAAO;GAChC,MAAM,SAAS,OAAO,WAAW,MAAM;AACvC,UAAO,OAAO,MAAM,OAAO,GAAG,eAAe;;EAG/C,QAAQ,KAAsB;AAC5B,UAAO,IAAI,IAAI,IAAI,KAAK;;EAG1B,cAAc,KAA4D;GACxE,MAAM,QAAQ,IAAI,IAAI,IAAI;AAC1B,OAAI,CAAC,MAAO,QAAO,KAAA;AACnB,UAAO,gBAAgB,MAAM,IAAI,KAAA;;EAGnC,aAAa,KAAoC;GAC/C,MAAM,QAAQ,IAAI,IAAI,IAAI;AAC1B,OAAI,CAAC,MAAO,QAAO,KAAA;AACnB,UAAO,eAAe,MAAM;;EAE/B;;;;;;AAOH,SAAgB,SAAS,MAAc,KAAmC;CACxE,MAAM,SAAS,IAAI,IAAI;AACvB,KAAI,CAAC,KAAK,WAAW,OAAO,CAAE,QAAO;AACrC,QAAO,oBAAoB,KAAK,MAAM,OAAO,OAAO,CAAC"}
@@ -0,0 +1,82 @@
1
+ import { matchTag, parseByteRange, parseExtInfDuration } from "./parse-attributes.js";
2
+ import { resolveUrl } from "./resolve-url.js";
3
+ //#region src/core/hls/parse-media-playlist.ts
4
+ /**
5
+ * Parse HLS media playlist and resolve track with segments.
6
+ *
7
+ * Takes an unresolved track (from multivariant playlist) and media playlist text,
8
+ * returns a HAM-compliant resolved track with segments.
9
+ *
10
+ * @param text - Media playlist text content
11
+ * @param unresolved - Unresolved track from parseMultivariantPlaylist
12
+ * @returns Resolved track with segments (type inferred from input)
13
+ */
14
+ function parseMediaPlaylist(text, unresolved) {
15
+ const lines = text.split(/\r?\n/);
16
+ const baseUrl = unresolved.url;
17
+ const segments = [];
18
+ let initSegmentUrl;
19
+ let initSegmentByteRange;
20
+ let currentDuration = 0;
21
+ let currentByteRange;
22
+ let currentTime = 0;
23
+ let segmentIndex = 0;
24
+ let previousByteRangeEnd;
25
+ for (const line of lines) {
26
+ const trimmed = line.trim();
27
+ if (!trimmed || trimmed.startsWith("#") && !trimmed.startsWith("#EXT")) continue;
28
+ if (trimmed === "#EXTM3U" || trimmed.startsWith("#EXT-X-VERSION:") || trimmed.startsWith("#EXT-X-TARGETDURATION:") || trimmed.startsWith("#EXT-X-PLAYLIST-TYPE:") || trimmed.startsWith("#EXT-X-INDEPENDENT-SEGMENTS")) continue;
29
+ const mapAttrs = matchTag(trimmed, "EXT-X-MAP");
30
+ if (mapAttrs) {
31
+ const uri = mapAttrs.get("URI");
32
+ if (uri) {
33
+ initSegmentUrl = resolveUrl(uri, baseUrl);
34
+ const byteRangeStr = mapAttrs.get("BYTERANGE");
35
+ if (byteRangeStr) initSegmentByteRange = parseByteRange(byteRangeStr, 0) ?? void 0;
36
+ }
37
+ continue;
38
+ }
39
+ if (trimmed.startsWith("#EXTINF:")) {
40
+ currentDuration = parseExtInfDuration(trimmed.slice(8));
41
+ continue;
42
+ }
43
+ if (trimmed.startsWith("#EXT-X-BYTERANGE:")) {
44
+ currentByteRange = parseByteRange(trimmed.slice(17), previousByteRangeEnd) ?? void 0;
45
+ continue;
46
+ }
47
+ if (trimmed === "#EXT-X-ENDLIST") continue;
48
+ if (!trimmed.startsWith("#") && currentDuration > 0) {
49
+ const segment = {
50
+ id: `segment-${segmentIndex}`,
51
+ url: resolveUrl(trimmed, baseUrl),
52
+ duration: currentDuration,
53
+ startTime: currentTime
54
+ };
55
+ if (currentByteRange) {
56
+ segment.byteRange = currentByteRange;
57
+ previousByteRangeEnd = currentByteRange.end + 1;
58
+ } else previousByteRangeEnd = void 0;
59
+ segments.push(segment);
60
+ currentTime += currentDuration;
61
+ segmentIndex++;
62
+ currentDuration = 0;
63
+ currentByteRange = void 0;
64
+ }
65
+ }
66
+ const totalDuration = currentTime;
67
+ const initialization = unresolved.type === "text" && !initSegmentUrl ? void 0 : initSegmentUrl ? {
68
+ url: initSegmentUrl,
69
+ ...initSegmentByteRange ? { byteRange: initSegmentByteRange } : {}
70
+ } : { url: "" };
71
+ return {
72
+ ...unresolved,
73
+ startTime: 0,
74
+ duration: totalDuration,
75
+ segments,
76
+ initialization
77
+ };
78
+ }
79
+ //#endregion
80
+ export { parseMediaPlaylist };
81
+
82
+ //# sourceMappingURL=parse-media-playlist.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse-media-playlist.js","names":[],"sources":["../../../../src/core/hls/parse-media-playlist.ts"],"sourcesContent":["import type {\n AudioTrack,\n PartiallyResolvedAudioTrack,\n PartiallyResolvedTextTrack,\n PartiallyResolvedTrack,\n PartiallyResolvedVideoTrack,\n Segment,\n TextTrack,\n VideoTrack,\n} from '../types';\nimport { matchTag, parseByteRange, parseExtInfDuration } from './parse-attributes';\nimport { resolveUrl } from './resolve-url';\n\n/**\n * Resolve unresolved track type to its resolved equivalent.\n */\ntype ResolveTrack<T> = T extends PartiallyResolvedVideoTrack\n ? VideoTrack\n : T extends PartiallyResolvedAudioTrack\n ? AudioTrack\n : T extends PartiallyResolvedTextTrack\n ? TextTrack\n : never;\n\n/**\n * Parse HLS media playlist and resolve track with segments.\n *\n * Takes an unresolved track (from multivariant playlist) and media playlist text,\n * returns a HAM-compliant resolved track with segments.\n *\n * @param text - Media playlist text content\n * @param unresolved - Unresolved track from parseMultivariantPlaylist\n * @returns Resolved track with segments (type inferred from input)\n */\nexport function parseMediaPlaylist<T extends PartiallyResolvedTrack>(\n text: string,\n unresolved: T | ResolveTrack<T>\n): ResolveTrack<T> {\n const lines = text.split(/\\r?\\n/);\n\n // Segments and resources resolve relative to media playlist URL (per HLS spec)\n const baseUrl = unresolved.url;\n\n // Parse playlist\n const segments: Segment[] = [];\n let initSegmentUrl: string | undefined;\n let initSegmentByteRange: { start: number; end: number } | undefined;\n\n let currentDuration = 0;\n let currentByteRange: { start: number; end: number } | undefined;\n let currentTime = 0;\n let segmentIndex = 0;\n let previousByteRangeEnd: number | undefined;\n\n for (const line of lines) {\n const trimmed = line.trim();\n\n if (!trimmed || (trimmed.startsWith('#') && !trimmed.startsWith('#EXT'))) {\n continue;\n }\n\n if (\n trimmed === '#EXTM3U' ||\n trimmed.startsWith('#EXT-X-VERSION:') ||\n trimmed.startsWith('#EXT-X-TARGETDURATION:') ||\n trimmed.startsWith('#EXT-X-PLAYLIST-TYPE:') ||\n trimmed.startsWith('#EXT-X-INDEPENDENT-SEGMENTS')\n ) {\n continue;\n }\n\n // #EXT-X-MAP - Init segment\n const mapAttrs = matchTag(trimmed, 'EXT-X-MAP');\n if (mapAttrs) {\n const uri = mapAttrs.get('URI');\n if (uri) {\n initSegmentUrl = resolveUrl(uri, baseUrl);\n const byteRangeStr = mapAttrs.get('BYTERANGE');\n if (byteRangeStr) {\n initSegmentByteRange = parseByteRange(byteRangeStr, 0) ?? undefined;\n }\n }\n continue;\n }\n\n // #EXTINF - Segment duration\n if (trimmed.startsWith('#EXTINF:')) {\n currentDuration = parseExtInfDuration(trimmed.slice(8));\n continue;\n }\n\n // #EXT-X-BYTERANGE - Segment byte range\n if (trimmed.startsWith('#EXT-X-BYTERANGE:')) {\n currentByteRange = parseByteRange(trimmed.slice(17), previousByteRangeEnd) ?? undefined;\n continue;\n }\n\n if (trimmed === '#EXT-X-ENDLIST') {\n continue;\n }\n\n // Segment URI\n if (!trimmed.startsWith('#') && currentDuration > 0) {\n const segment: Segment = {\n id: `segment-${segmentIndex}`,\n url: resolveUrl(trimmed, baseUrl),\n duration: currentDuration,\n startTime: currentTime,\n };\n\n if (currentByteRange) {\n segment.byteRange = currentByteRange;\n previousByteRangeEnd = currentByteRange.end + 1;\n } else {\n previousByteRangeEnd = undefined;\n }\n\n segments.push(segment);\n currentTime += currentDuration;\n segmentIndex++;\n\n currentDuration = 0;\n currentByteRange = undefined;\n }\n }\n\n const totalDuration = currentTime;\n\n // Build initialization (VTT may not have init segment)\n const initialization =\n unresolved.type === 'text' && !initSegmentUrl\n ? undefined\n : initSegmentUrl\n ? { url: initSegmentUrl, ...(initSegmentByteRange ? { byteRange: initSegmentByteRange } : {}) }\n : { url: '' };\n\n // Generic resolution: All type-specific fields already on unresolved track from P1\n // Just add parsed properties (startTime, duration, segments, initialization)\n return {\n ...unresolved,\n startTime: 0,\n duration: totalDuration,\n segments,\n initialization,\n } as unknown as ResolveTrack<T>;\n}\n"],"mappings":";;;;;;;;;;;;;AAkCA,SAAgB,mBACd,MACA,YACiB;CACjB,MAAM,QAAQ,KAAK,MAAM,QAAQ;CAGjC,MAAM,UAAU,WAAW;CAG3B,MAAM,WAAsB,EAAE;CAC9B,IAAI;CACJ,IAAI;CAEJ,IAAI,kBAAkB;CACtB,IAAI;CACJ,IAAI,cAAc;CAClB,IAAI,eAAe;CACnB,IAAI;AAEJ,MAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,UAAU,KAAK,MAAM;AAE3B,MAAI,CAAC,WAAY,QAAQ,WAAW,IAAI,IAAI,CAAC,QAAQ,WAAW,OAAO,CACrE;AAGF,MACE,YAAY,aACZ,QAAQ,WAAW,kBAAkB,IACrC,QAAQ,WAAW,yBAAyB,IAC5C,QAAQ,WAAW,wBAAwB,IAC3C,QAAQ,WAAW,8BAA8B,CAEjD;EAIF,MAAM,WAAW,SAAS,SAAS,YAAY;AAC/C,MAAI,UAAU;GACZ,MAAM,MAAM,SAAS,IAAI,MAAM;AAC/B,OAAI,KAAK;AACP,qBAAiB,WAAW,KAAK,QAAQ;IACzC,MAAM,eAAe,SAAS,IAAI,YAAY;AAC9C,QAAI,aACF,wBAAuB,eAAe,cAAc,EAAE,IAAI,KAAA;;AAG9D;;AAIF,MAAI,QAAQ,WAAW,WAAW,EAAE;AAClC,qBAAkB,oBAAoB,QAAQ,MAAM,EAAE,CAAC;AACvD;;AAIF,MAAI,QAAQ,WAAW,oBAAoB,EAAE;AAC3C,sBAAmB,eAAe,QAAQ,MAAM,GAAG,EAAE,qBAAqB,IAAI,KAAA;AAC9E;;AAGF,MAAI,YAAY,iBACd;AAIF,MAAI,CAAC,QAAQ,WAAW,IAAI,IAAI,kBAAkB,GAAG;GACnD,MAAM,UAAmB;IACvB,IAAI,WAAW;IACf,KAAK,WAAW,SAAS,QAAQ;IACjC,UAAU;IACV,WAAW;IACZ;AAED,OAAI,kBAAkB;AACpB,YAAQ,YAAY;AACpB,2BAAuB,iBAAiB,MAAM;SAE9C,wBAAuB,KAAA;AAGzB,YAAS,KAAK,QAAQ;AACtB,kBAAe;AACf;AAEA,qBAAkB;AAClB,sBAAmB,KAAA;;;CAIvB,MAAM,gBAAgB;CAGtB,MAAM,iBACJ,WAAW,SAAS,UAAU,CAAC,iBAC3B,KAAA,IACA,iBACE;EAAE,KAAK;EAAgB,GAAI,uBAAuB,EAAE,WAAW,sBAAsB,GAAG,EAAE;EAAG,GAC7F,EAAE,KAAK,IAAI;AAInB,QAAO;EACL,GAAG;EACH,WAAW;EACX,UAAU;EACV;EACA;EACD"}