@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,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"}
@@ -0,0 +1,214 @@
1
+ import { generateId } from "../utils/generate-id.js";
2
+ import { matchTag, parseCodecs } from "./parse-attributes.js";
3
+ import { resolveUrl } from "./resolve-url.js";
4
+ //#region src/core/hls/parse-multivariant.ts
5
+ /**
6
+ * Parse HLS multivariant playlist into a Presentation.
7
+ *
8
+ * Returns Presentation with partially resolved tracks (no segment information).
9
+ * Tracks contain metadata from multivariant playlist (bandwidth, resolution, codecs)
10
+ * but segment information is added when media playlists are fetched.
11
+ *
12
+ * @param text - Raw playlist text content
13
+ * @param unresolved - Unresolved presentation (contains URL for base URL resolution)
14
+ * @returns Presentation with partially resolved tracks (duration is undefined)
15
+ */
16
+ function parseMultivariantPlaylist(text, unresolved) {
17
+ const baseUrl = unresolved.url;
18
+ const lines = text.split(/\r?\n/);
19
+ const streams = [];
20
+ const audioRenditions = [];
21
+ const subtitleRenditions = [];
22
+ let pendingStreamInfo = null;
23
+ for (const line of lines) {
24
+ const trimmed = line.trim();
25
+ if (!trimmed || trimmed.startsWith("#") && !trimmed.startsWith("#EXT")) continue;
26
+ if (trimmed === "#EXTM3U" || trimmed.startsWith("#EXT-X-VERSION:") || trimmed.startsWith("#EXT-X-INDEPENDENT-SEGMENTS")) continue;
27
+ const mediaAttrs = matchTag(trimmed, "EXT-X-MEDIA");
28
+ if (mediaAttrs) {
29
+ const type = mediaAttrs.get("TYPE");
30
+ const groupId = mediaAttrs.get("GROUP-ID");
31
+ const name = mediaAttrs.get("NAME");
32
+ if (type === "AUDIO" && groupId && name) {
33
+ const uri = mediaAttrs.get("URI");
34
+ audioRenditions.push({
35
+ groupId,
36
+ name,
37
+ language: mediaAttrs.get("LANGUAGE"),
38
+ uri: uri ? resolveUrl(uri, baseUrl) : void 0,
39
+ default: mediaAttrs.getBool("DEFAULT"),
40
+ autoselect: mediaAttrs.getBool("AUTOSELECT")
41
+ });
42
+ }
43
+ if (type === "SUBTITLES" && groupId && name) {
44
+ const uri = mediaAttrs.get("URI");
45
+ if (uri) subtitleRenditions.push({
46
+ groupId,
47
+ name,
48
+ language: mediaAttrs.get("LANGUAGE"),
49
+ uri: resolveUrl(uri, baseUrl),
50
+ default: mediaAttrs.getBool("DEFAULT"),
51
+ autoselect: mediaAttrs.getBool("AUTOSELECT"),
52
+ forced: mediaAttrs.getBool("FORCED")
53
+ });
54
+ }
55
+ continue;
56
+ }
57
+ const streamInfAttrs = matchTag(trimmed, "EXT-X-STREAM-INF");
58
+ if (streamInfAttrs) {
59
+ pendingStreamInfo = {
60
+ bandwidth: streamInfAttrs.getInt("BANDWIDTH", 0),
61
+ resolution: streamInfAttrs.getResolution("RESOLUTION"),
62
+ codecs: streamInfAttrs.get("CODECS"),
63
+ frameRate: streamInfAttrs.getFrameRate("FRAME-RATE"),
64
+ audioGroupId: streamInfAttrs.get("AUDIO")
65
+ };
66
+ continue;
67
+ }
68
+ if (!trimmed.startsWith("#") && pendingStreamInfo) {
69
+ streams.push({
70
+ ...pendingStreamInfo,
71
+ uri: resolveUrl(trimmed, baseUrl)
72
+ });
73
+ pendingStreamInfo = null;
74
+ }
75
+ }
76
+ const videoStreams = [];
77
+ const audioOnlyStreams = [];
78
+ for (const stream of streams) {
79
+ if (!stream.codecs) {
80
+ videoStreams.push(stream);
81
+ continue;
82
+ }
83
+ const parsedCodecs = parseCodecs(stream.codecs);
84
+ if (stream.codecs.split(",").length === 1) if (parsedCodecs.audio && !parsedCodecs.video) audioOnlyStreams.push(stream);
85
+ else videoStreams.push(stream);
86
+ else videoStreams.push(stream);
87
+ }
88
+ const videoTracks = videoStreams.map((stream) => {
89
+ const codecs = stream.codecs ? parseCodecs(stream.codecs) : void 0;
90
+ const track = {
91
+ type: "video",
92
+ id: generateId(),
93
+ url: stream.uri,
94
+ bandwidth: stream.bandwidth,
95
+ mimeType: "video/mp4",
96
+ codecs: []
97
+ };
98
+ if (stream.resolution?.width !== void 0) track.width = stream.resolution.width;
99
+ if (stream.resolution?.height !== void 0) track.height = stream.resolution.height;
100
+ if (codecs?.video) track.codecs = [codecs.video];
101
+ if (stream.frameRate) track.frameRate = stream.frameRate;
102
+ if (stream.audioGroupId) track.audioGroupId = stream.audioGroupId;
103
+ return track;
104
+ });
105
+ const audioOnlyTracks = audioOnlyStreams.map((stream) => {
106
+ const codecs = stream.codecs ? parseCodecs(stream.codecs) : void 0;
107
+ return {
108
+ type: "audio",
109
+ id: generateId(),
110
+ url: stream.uri,
111
+ bandwidth: stream.bandwidth,
112
+ mimeType: "audio/mp4",
113
+ codecs: codecs?.audio ? [codecs.audio] : [],
114
+ groupId: stream.audioGroupId || "default",
115
+ name: "Default",
116
+ sampleRate: 48e3,
117
+ channels: 2
118
+ };
119
+ });
120
+ const audioTracks = [...audioRenditions.map((rendition) => {
121
+ let audioCodecs;
122
+ for (const stream of streams) if (stream.audioGroupId === rendition.groupId && stream.codecs) {
123
+ const codecs = parseCodecs(stream.codecs);
124
+ if (codecs.audio) {
125
+ audioCodecs = [codecs.audio];
126
+ break;
127
+ }
128
+ }
129
+ const track = {
130
+ type: "audio",
131
+ id: generateId(),
132
+ url: rendition.uri ?? "",
133
+ groupId: rendition.groupId,
134
+ name: rendition.name,
135
+ mimeType: "audio/mp4",
136
+ bandwidth: 0,
137
+ sampleRate: 48e3,
138
+ channels: 2,
139
+ codecs: []
140
+ };
141
+ if (rendition.language) track.language = rendition.language;
142
+ if (audioCodecs) track.codecs = audioCodecs;
143
+ if (rendition.default) track.default = rendition.default;
144
+ if (rendition.autoselect) track.autoselect = rendition.autoselect;
145
+ return track;
146
+ }), ...audioOnlyTracks];
147
+ const textTracks = subtitleRenditions.map((rendition) => {
148
+ const track = {
149
+ type: "text",
150
+ id: generateId(),
151
+ url: rendition.uri,
152
+ groupId: rendition.groupId,
153
+ label: rendition.name,
154
+ kind: "subtitles",
155
+ mimeType: "text/vtt",
156
+ bandwidth: 0
157
+ };
158
+ if (rendition.language) track.language = rendition.language;
159
+ if (rendition.default && rendition.autoselect) track.default = true;
160
+ if (rendition.autoselect) track.autoselect = rendition.autoselect;
161
+ if (rendition.forced) track.forced = rendition.forced;
162
+ return track;
163
+ });
164
+ const selectionSets = [];
165
+ if (videoTracks.length > 0) {
166
+ const videoSwitchingSet = {
167
+ id: generateId(),
168
+ type: "video",
169
+ tracks: videoTracks
170
+ };
171
+ const videoSelectionSet = {
172
+ id: generateId(),
173
+ type: "video",
174
+ switchingSets: [videoSwitchingSet]
175
+ };
176
+ selectionSets.push(videoSelectionSet);
177
+ }
178
+ if (audioTracks.length > 0) {
179
+ const audioSwitchingSet = {
180
+ id: generateId(),
181
+ type: "audio",
182
+ tracks: audioTracks
183
+ };
184
+ const audioSelectionSet = {
185
+ id: generateId(),
186
+ type: "audio",
187
+ switchingSets: [audioSwitchingSet]
188
+ };
189
+ selectionSets.push(audioSelectionSet);
190
+ }
191
+ if (textTracks.length > 0) {
192
+ const textSwitchingSet = {
193
+ id: generateId(),
194
+ type: "text",
195
+ tracks: textTracks
196
+ };
197
+ const textSelectionSet = {
198
+ id: generateId(),
199
+ type: "text",
200
+ switchingSets: [textSwitchingSet]
201
+ };
202
+ selectionSets.push(textSelectionSet);
203
+ }
204
+ return {
205
+ id: generateId(),
206
+ url: unresolved.url,
207
+ startTime: 0,
208
+ selectionSets
209
+ };
210
+ }
211
+ //#endregion
212
+ export { parseMultivariantPlaylist };
213
+
214
+ //# sourceMappingURL=parse-multivariant.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse-multivariant.js","names":[],"sources":["../../../../src/core/hls/parse-multivariant.ts"],"sourcesContent":["import type {\n AddressableObject,\n AudioSelectionSet,\n AudioSwitchingSet,\n FrameRate,\n PartiallyResolvedAudioTrack,\n PartiallyResolvedTextTrack,\n PartiallyResolvedVideoTrack,\n Presentation,\n SelectionSet,\n TextSelectionSet,\n TextSwitchingSet,\n VideoSelectionSet,\n VideoSwitchingSet,\n} from '../types';\nimport { generateId } from '../utils/generate-id';\nimport { matchTag, parseCodecs } from './parse-attributes';\nimport { resolveUrl } from './resolve-url';\n\n/**\n * Parse HLS multivariant playlist into a Presentation.\n *\n * Returns Presentation with partially resolved tracks (no segment information).\n * Tracks contain metadata from multivariant playlist (bandwidth, resolution, codecs)\n * but segment information is added when media playlists are fetched.\n *\n * @param text - Raw playlist text content\n * @param unresolved - Unresolved presentation (contains URL for base URL resolution)\n * @returns Presentation with partially resolved tracks (duration is undefined)\n */\nexport function parseMultivariantPlaylist(text: string, unresolved: AddressableObject): Presentation {\n const baseUrl = unresolved.url;\n const lines = text.split(/\\r?\\n/);\n\n // Intermediate parsing structures\n interface StreamInfo {\n uri: string;\n bandwidth: number;\n resolution?: { width: number; height: number } | undefined;\n codecs?: string | undefined;\n frameRate?: FrameRate | undefined;\n audioGroupId?: string | undefined;\n }\n\n interface AudioRenditionInfo {\n groupId: string;\n name: string;\n language?: string | undefined;\n uri?: string | undefined;\n default?: boolean | undefined;\n autoselect?: boolean | undefined;\n }\n\n interface SubtitleRenditionInfo {\n groupId: string;\n name: string;\n language?: string | undefined;\n uri: string;\n default?: boolean | undefined;\n autoselect?: boolean | undefined;\n forced?: boolean | undefined;\n }\n\n const streams: StreamInfo[] = [];\n const audioRenditions: AudioRenditionInfo[] = [];\n const subtitleRenditions: SubtitleRenditionInfo[] = [];\n\n // State for STREAM-INF parsing (URI follows on next line)\n let pendingStreamInfo: Omit<StreamInfo, 'uri'> | null = null;\n\n for (const line of lines) {\n const trimmed = line.trim();\n\n // Skip empty lines and comments\n if (!trimmed || (trimmed.startsWith('#') && !trimmed.startsWith('#EXT'))) {\n continue;\n }\n\n // Skip tags not used in Presentation model\n if (\n trimmed === '#EXTM3U' ||\n trimmed.startsWith('#EXT-X-VERSION:') ||\n trimmed.startsWith('#EXT-X-INDEPENDENT-SEGMENTS')\n ) {\n continue;\n }\n\n // #EXT-X-MEDIA:TYPE=AUDIO/SUBTITLES\n const mediaAttrs = matchTag(trimmed, 'EXT-X-MEDIA');\n if (mediaAttrs) {\n const type = mediaAttrs.get('TYPE');\n const groupId = mediaAttrs.get('GROUP-ID');\n const name = mediaAttrs.get('NAME');\n\n if (type === 'AUDIO' && groupId && name) {\n const uri = mediaAttrs.get('URI');\n audioRenditions.push({\n groupId,\n name,\n language: mediaAttrs.get('LANGUAGE'),\n uri: uri ? resolveUrl(uri, baseUrl) : undefined,\n default: mediaAttrs.getBool('DEFAULT'),\n autoselect: mediaAttrs.getBool('AUTOSELECT'),\n });\n }\n\n if (type === 'SUBTITLES' && groupId && name) {\n const uri = mediaAttrs.get('URI');\n // URI is required for subtitle tracks\n if (uri) {\n subtitleRenditions.push({\n groupId,\n name,\n language: mediaAttrs.get('LANGUAGE'),\n uri: resolveUrl(uri, baseUrl),\n default: mediaAttrs.getBool('DEFAULT'),\n autoselect: mediaAttrs.getBool('AUTOSELECT'),\n forced: mediaAttrs.getBool('FORCED'),\n });\n }\n }\n continue;\n }\n\n // #EXT-X-STREAM-INF:BANDWIDTH=...\n const streamInfAttrs = matchTag(trimmed, 'EXT-X-STREAM-INF');\n if (streamInfAttrs) {\n pendingStreamInfo = {\n bandwidth: streamInfAttrs.getInt('BANDWIDTH', 0)!,\n resolution: streamInfAttrs.getResolution('RESOLUTION'),\n codecs: streamInfAttrs.get('CODECS'),\n frameRate: streamInfAttrs.getFrameRate('FRAME-RATE'),\n audioGroupId: streamInfAttrs.get('AUDIO'),\n };\n continue;\n }\n\n // URI line following STREAM-INF\n if (!trimmed.startsWith('#') && pendingStreamInfo) {\n streams.push({\n ...pendingStreamInfo,\n uri: resolveUrl(trimmed, baseUrl),\n });\n pendingStreamInfo = null;\n }\n }\n\n // Separate streams into video and audio based on codecs\n // If CODECS has single codec, use parseCodecs to determine type\n const videoStreams: typeof streams = [];\n const audioOnlyStreams: typeof streams = [];\n\n for (const stream of streams) {\n if (!stream.codecs) {\n // No codecs - assume video (default behavior)\n videoStreams.push(stream);\n continue;\n }\n\n const parsedCodecs = parseCodecs(stream.codecs);\n const codecCount = stream.codecs.split(',').length;\n\n // Single codec - determine type from parseCodecs result\n if (codecCount === 1) {\n if (parsedCodecs.audio && !parsedCodecs.video) {\n // Audio-only stream\n audioOnlyStreams.push(stream);\n } else {\n // Video stream (or unknown - default to video)\n videoStreams.push(stream);\n }\n } else {\n // Multiple codecs - video stream with muxed audio\n videoStreams.push(stream);\n }\n }\n\n // Build PartiallyResolvedVideoTracks from video streams\n const videoTracks: PartiallyResolvedVideoTrack[] = videoStreams.map((stream) => {\n const codecs = stream.codecs ? parseCodecs(stream.codecs) : undefined;\n\n const track: PartiallyResolvedVideoTrack = {\n type: 'video' as const,\n id: generateId(),\n url: stream.uri,\n bandwidth: stream.bandwidth,\n // Type-specific defaults (CMAF video)\n mimeType: 'video/mp4',\n codecs: [],\n };\n\n if (stream.resolution?.width !== undefined) {\n track.width = stream.resolution.width;\n }\n if (stream.resolution?.height !== undefined) {\n track.height = stream.resolution.height;\n }\n if (codecs?.video) {\n track.codecs = [codecs.video];\n }\n if (stream.frameRate) {\n track.frameRate = stream.frameRate;\n }\n if (stream.audioGroupId) {\n track.audioGroupId = stream.audioGroupId;\n }\n\n return track;\n });\n\n // Build PartiallyResolvedAudioTracks from audio-only streams\n const audioOnlyTracks: PartiallyResolvedAudioTrack[] = audioOnlyStreams.map((stream) => {\n const codecs = stream.codecs ? parseCodecs(stream.codecs) : undefined;\n\n const track: PartiallyResolvedAudioTrack = {\n type: 'audio' as const,\n id: generateId(),\n url: stream.uri,\n bandwidth: stream.bandwidth,\n mimeType: 'audio/mp4',\n codecs: codecs?.audio ? [codecs.audio] : [],\n groupId: stream.audioGroupId || 'default',\n name: 'Default',\n sampleRate: 48000, // Default - will be in media playlist if available\n channels: 2, // Default - will be in media playlist if available\n };\n\n return track;\n });\n\n // Build PartiallyResolvedAudioTracks from audio renditions (EXT-X-MEDIA)\n // Extract audio codecs from referencing streams\n const audioRenditionTracks: PartiallyResolvedAudioTrack[] = audioRenditions.map((rendition) => {\n let audioCodecs: string[] | undefined;\n for (const stream of streams) {\n if (stream.audioGroupId === rendition.groupId && stream.codecs) {\n const codecs = parseCodecs(stream.codecs);\n if (codecs.audio) {\n audioCodecs = [codecs.audio];\n break;\n }\n }\n }\n\n const track: PartiallyResolvedAudioTrack = {\n type: 'audio' as const,\n id: generateId(),\n url: rendition.uri ?? '',\n groupId: rendition.groupId,\n name: rendition.name,\n // Type-specific defaults (CMAF audio)\n mimeType: 'audio/mp4',\n bandwidth: 0, // Not available in multivariant for demuxed audio\n sampleRate: 48000, // CMAF default\n channels: 2, // Stereo default\n codecs: [],\n };\n\n if (rendition.language) {\n track.language = rendition.language;\n }\n if (audioCodecs) {\n track.codecs = audioCodecs;\n }\n if (rendition.default) {\n track.default = rendition.default;\n }\n if (rendition.autoselect) {\n track.autoselect = rendition.autoselect;\n }\n\n return track;\n });\n\n // Combine audio tracks from both EXT-X-MEDIA renditions and audio-only STREAM-INF\n const audioTracks = [...audioRenditionTracks, ...audioOnlyTracks];\n\n // Build PartiallyResolvedTextTracks from subtitle renditions\n const textTracks: PartiallyResolvedTextTrack[] = subtitleRenditions.map((rendition) => {\n const track: PartiallyResolvedTextTrack = {\n type: 'text' as const,\n id: generateId(),\n url: rendition.uri,\n groupId: rendition.groupId,\n label: rendition.name,\n kind: 'subtitles' as const,\n // Type-specific defaults (VTT)\n mimeType: 'text/vtt',\n bandwidth: 0, // Text tracks don't consume bandwidth\n };\n\n if (rendition.language) {\n track.language = rendition.language;\n }\n // Match hls.js/http-streaming: only set default=true when BOTH DEFAULT=YES AND AUTOSELECT=YES\n if (rendition.default && rendition.autoselect) {\n track.default = true;\n }\n if (rendition.autoselect) {\n track.autoselect = rendition.autoselect;\n }\n if (rendition.forced) {\n track.forced = rendition.forced;\n }\n\n return track;\n });\n\n // Build selection sets\n const selectionSets: SelectionSet[] = [];\n\n if (videoTracks.length > 0) {\n const videoSwitchingSet: VideoSwitchingSet = {\n id: generateId(),\n type: 'video',\n tracks: videoTracks,\n };\n\n const videoSelectionSet: VideoSelectionSet = {\n id: generateId(),\n type: 'video',\n switchingSets: [videoSwitchingSet],\n };\n\n selectionSets.push(videoSelectionSet);\n }\n\n if (audioTracks.length > 0) {\n const audioSwitchingSet: AudioSwitchingSet = {\n id: generateId(),\n type: 'audio',\n tracks: audioTracks,\n };\n\n const audioSelectionSet: AudioSelectionSet = {\n id: generateId(),\n type: 'audio',\n switchingSets: [audioSwitchingSet],\n };\n\n selectionSets.push(audioSelectionSet);\n }\n\n if (textTracks.length > 0) {\n const textSwitchingSet: TextSwitchingSet = {\n id: generateId(),\n type: 'text',\n tracks: textTracks,\n };\n\n const textSelectionSet: TextSelectionSet = {\n id: generateId(),\n type: 'text',\n switchingSets: [textSwitchingSet],\n };\n\n selectionSets.push(textSelectionSet);\n }\n\n // Build presentation (duration is undefined until tracks are resolved)\n return {\n id: generateId(),\n url: unresolved.url,\n startTime: 0,\n // duration: undefined, // Won't be known until after at least one media playlist is fetched + parsed\n selectionSets,\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ //#region src/core/hls/resolve-url.ts
2
+ /**
3
+ * Resolve a potentially relative URL against a base URL using native URL API.
4
+ */
5
+ function resolveUrl(url, baseUrl) {
6
+ return new URL(url, baseUrl).href;
7
+ }
8
+ //#endregion
9
+ export { resolveUrl };
10
+
11
+ //# sourceMappingURL=resolve-url.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve-url.js","names":[],"sources":["../../../../src/core/hls/resolve-url.ts"],"sourcesContent":["/**\n * Resolve a potentially relative URL against a base URL using native URL API.\n */\nexport function resolveUrl(url: string, baseUrl: string): string {\n return new URL(url, baseUrl).href;\n}\n"],"mappings":";;;;AAGA,SAAgB,WAAW,KAAa,SAAyB;AAC/D,QAAO,IAAI,IAAI,KAAK,QAAQ,CAAC"}
@@ -0,0 +1,15 @@
1
+ //#region src/core/signals/effect.d.ts
2
+ /**
3
+ * Run a side effect whenever its signal dependencies change.
4
+ *
5
+ * Executes immediately (synchronous initial run), then re-runs on the next
6
+ * microtask after any dependency changes. If the callback returns a function,
7
+ * it is called before each re-run and when the effect is stopped — the same
8
+ * cleanup contract as Preact Signals, Maverick Signals, and Svelte 5 $effect.
9
+ *
10
+ * Returns a cleanup function that stops the effect.
11
+ */
12
+ declare function effect(fn: () => (() => void) | void): () => void;
13
+ //#endregion
14
+ export { effect };
15
+ //# sourceMappingURL=effect.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"effect.d.ts","names":[],"sources":["../../../../src/core/signals/effect.ts"],"mappings":";;AA8BA;;;;;;;;;iBAAgB,MAAA,CAAO,EAAA"}
@@ -0,0 +1,41 @@
1
+ import { Signal } from "signal-polyfill";
2
+ //#region src/core/signals/effect.ts
3
+ const pending = /* @__PURE__ */ new Set();
4
+ const watcher = new Signal.subtle.Watcher(() => {
5
+ queueMicrotask(runPending);
6
+ });
7
+ function runPending() {
8
+ for (const c of watcher.getPending()) pending.add(c);
9
+ watcher.watch();
10
+ for (const c of pending) {
11
+ pending.delete(c);
12
+ c.get();
13
+ }
14
+ }
15
+ /**
16
+ * Run a side effect whenever its signal dependencies change.
17
+ *
18
+ * Executes immediately (synchronous initial run), then re-runs on the next
19
+ * microtask after any dependency changes. If the callback returns a function,
20
+ * it is called before each re-run and when the effect is stopped — the same
21
+ * cleanup contract as Preact Signals, Maverick Signals, and Svelte 5 $effect.
22
+ *
23
+ * Returns a cleanup function that stops the effect.
24
+ */
25
+ function effect(fn) {
26
+ let cleanup;
27
+ const c = new Signal.Computed(() => {
28
+ if (typeof cleanup === "function") cleanup();
29
+ cleanup = fn();
30
+ });
31
+ watcher.watch(c);
32
+ c.get();
33
+ return () => {
34
+ watcher.unwatch(c);
35
+ if (typeof cleanup === "function") cleanup();
36
+ };
37
+ }
38
+ //#endregion
39
+ export { effect };
40
+
41
+ //# sourceMappingURL=effect.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"effect.js","names":[],"sources":["../../../../src/core/signals/effect.ts"],"sourcesContent":["import { Signal } from 'signal-polyfill';\n\n// Computeds waiting to re-run after their dependencies changed.\nconst pending = new Set<Signal.Computed<void>>();\n\nconst watcher = new Signal.subtle.Watcher(() => {\n queueMicrotask(runPending);\n});\n\nfunction runPending() {\n for (const c of watcher.getPending()) {\n pending.add(c as Signal.Computed<void>);\n }\n watcher.watch(); // re-arm before running effects, in case they write signals\n for (const c of pending) {\n pending.delete(c);\n c.get(); // re-run the effect body\n }\n}\n\n/**\n * Run a side effect whenever its signal dependencies change.\n *\n * Executes immediately (synchronous initial run), then re-runs on the next\n * microtask after any dependency changes. If the callback returns a function,\n * it is called before each re-run and when the effect is stopped — the same\n * cleanup contract as Preact Signals, Maverick Signals, and Svelte 5 $effect.\n *\n * Returns a cleanup function that stops the effect.\n */\nexport function effect(fn: () => (() => void) | void): () => void {\n let cleanup: (() => void) | void;\n const c = new Signal.Computed(() => {\n if (typeof cleanup === 'function') cleanup();\n cleanup = fn();\n });\n watcher.watch(c);\n c.get(); // initial run\n return () => {\n watcher.unwatch(c);\n if (typeof cleanup === 'function') cleanup();\n };\n}\n"],"mappings":";;AAGA,MAAM,0BAAU,IAAI,KAA4B;AAEhD,MAAM,UAAU,IAAI,OAAO,OAAO,cAAc;AAC9C,gBAAe,WAAW;EAC1B;AAEF,SAAS,aAAa;AACpB,MAAK,MAAM,KAAK,QAAQ,YAAY,CAClC,SAAQ,IAAI,EAA2B;AAEzC,SAAQ,OAAO;AACf,MAAK,MAAM,KAAK,SAAS;AACvB,UAAQ,OAAO,EAAE;AACjB,IAAE,KAAK;;;;;;;;;;;;;AAcX,SAAgB,OAAO,IAA2C;CAChE,IAAI;CACJ,MAAM,IAAI,IAAI,OAAO,eAAe;AAClC,MAAI,OAAO,YAAY,WAAY,UAAS;AAC5C,YAAU,IAAI;GACd;AACF,SAAQ,MAAM,EAAE;AAChB,GAAE,KAAK;AACP,cAAa;AACX,UAAQ,QAAQ,EAAE;AAClB,MAAI,OAAO,YAAY,WAAY,UAAS"}
@@ -0,0 +1,10 @@
1
+ import { Signal } from "signal-polyfill";
2
+
3
+ //#region src/core/signals/primitives.d.ts
4
+ /** A writable reactive value (read + write). */
5
+ type Signal$1<T> = Signal.State<T>;
6
+ /** A read-only view of a reactive value. */
7
+ type ReadonlySignal<T> = Omit<Signal.State<T>, 'set'>;
8
+ //#endregion
9
+ export { ReadonlySignal, Signal$1 as Signal };
10
+ //# sourceMappingURL=primitives.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"primitives.d.ts","names":[],"sources":["../../../../src/core/signals/primitives.ts"],"mappings":";;;AAMA;AAAA,KAAY,QAAA,MAAY,MAAA,CAAS,KAAA,CAAM,CAAA;;KAM3B,cAAA,MAAoB,IAAA,CAAK,MAAA,CAAS,KAAA,CAAM,CAAA"}
@@ -0,0 +1,32 @@
1
+ import { Signal } from "signal-polyfill";
2
+ //#region src/core/signals/primitives.ts
3
+ /** Read a signal value without tracking it as a dependency. */
4
+ const untrack = Signal.subtle.untrack;
5
+ /** Create a writable reactive value. */
6
+ function signal(initialValue, options) {
7
+ return new Signal.State(initialValue, options);
8
+ }
9
+ /** Create a computed reactive value. */
10
+ function computed(fn, options) {
11
+ return new Signal.Computed(fn, options);
12
+ }
13
+ /**
14
+ * Update a writable signal. Accepts either a partial object to merge into the
15
+ * current state, or an updater function that receives the current state and
16
+ * returns the next state.
17
+ *
18
+ * @example
19
+ * update(state, { playbackRate: 2 });
20
+ * update(state, (s) => ({ ...s, count: s.count + 1 }));
21
+ */
22
+ function update(signal, updater) {
23
+ const current = untrack(() => signal.get());
24
+ signal.set(typeof updater === "function" ? updater(current) : {
25
+ ...current,
26
+ ...updater
27
+ });
28
+ }
29
+ //#endregion
30
+ export { computed, signal, update };
31
+
32
+ //# sourceMappingURL=primitives.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"primitives.js","names":["SignalNS"],"sources":["../../../../src/core/signals/primitives.ts"],"sourcesContent":["import { Signal as SignalNS } from 'signal-polyfill';\n\n/** Read a signal value without tracking it as a dependency. */\nexport const untrack: <T>(fn: () => T) => T = SignalNS.subtle.untrack;\n\n/** A writable reactive value (read + write). */\nexport type Signal<T> = SignalNS.State<T>;\n\n/** A derived reactive value that re-evaluates when its dependencies change (read-only). */\nexport type Computed<T> = SignalNS.Computed<T>;\n\n/** A read-only view of a reactive value. */\nexport type ReadonlySignal<T> = Omit<SignalNS.State<T>, 'set'>;\n\nexport interface SignalOptions<T> {\n equals?: (t: T, t2: T) => boolean;\n}\n\n/** Create a writable reactive value. */\nexport function signal<T>(initialValue: T, options?: SignalOptions<T>): Signal<T> {\n return new SignalNS.State(initialValue, options as SignalNS.Options<T>);\n}\n\n/** Create a computed reactive value. */\nexport function computed<T>(fn: () => T, options?: SignalOptions<T>): Computed<T> {\n return new SignalNS.Computed(fn, options as SignalNS.Options<T>);\n}\n\n/**\n * Update a writable signal. Accepts either a partial object to merge into the\n * current state, or an updater function that receives the current state and\n * returns the next state.\n *\n * @example\n * update(state, { playbackRate: 2 });\n * update(state, (s) => ({ ...s, count: s.count + 1 }));\n */\nexport function update<T extends object>(signal: Signal<T>, updater: Partial<T> | ((current: T) => T)): void {\n const current = untrack(() => signal.get());\n signal.set(typeof updater === 'function' ? updater(current) : { ...current, ...updater });\n}\n"],"mappings":";;;AAGA,MAAa,UAAiCA,OAAS,OAAO;;AAgB9D,SAAgB,OAAU,cAAiB,SAAuC;AAChF,QAAO,IAAIA,OAAS,MAAM,cAAc,QAA+B;;;AAIzE,SAAgB,SAAY,IAAa,SAAyC;AAChF,QAAO,IAAIA,OAAS,SAAS,IAAI,QAA+B;;;;;;;;;;;AAYlE,SAAgB,OAAyB,QAAmB,SAAiD;CAC3G,MAAM,UAAU,cAAc,OAAO,KAAK,CAAC;AAC3C,QAAO,IAAI,OAAO,YAAY,aAAa,QAAQ,QAAQ,GAAG;EAAE,GAAG;EAAS,GAAG;EAAS,CAAC"}
@@ -0,0 +1,122 @@
1
+ import { generateId } from "./utils/generate-id.js";
2
+ import { anyAbortSignal } from "@videojs/utils/events";
3
+ //#region src/core/task.ts
4
+ /**
5
+ * Generic reusable task that wraps an async run function.
6
+ *
7
+ * Owns its own AbortController so it can always be aborted independently.
8
+ * Optionally composes an external AbortSignal so that a parent's cancellation
9
+ * propagates into the task's work without requiring the caller to track the
10
+ * task separately.
11
+ *
12
+ * Ordering guarantee: `value` is written before `status` transitions to `'done'`;
13
+ * `error` is written before `status` transitions to `'error'`. Any reader
14
+ * observing `status === 'done'` is guaranteed `value` is already present.
15
+ */
16
+ var Task = class {
17
+ id;
18
+ #runFn;
19
+ #abortController = new AbortController();
20
+ #signal;
21
+ #status = "pending";
22
+ #value = void 0;
23
+ #error = void 0;
24
+ constructor(runFn, config) {
25
+ this.#runFn = runFn;
26
+ const rawId = config?.id;
27
+ this.id = typeof rawId === "function" ? rawId() : rawId ?? generateId();
28
+ this.#signal = config?.signal ? anyAbortSignal([this.#abortController.signal, config.signal]) : this.#abortController.signal;
29
+ }
30
+ get status() {
31
+ return this.#status;
32
+ }
33
+ get value() {
34
+ return this.#value;
35
+ }
36
+ get error() {
37
+ return this.#error;
38
+ }
39
+ async run() {
40
+ this.#status = "running";
41
+ try {
42
+ const result = await this.#runFn(this.#signal);
43
+ this.#value = result;
44
+ this.#status = "done";
45
+ return result;
46
+ } catch (e) {
47
+ this.#error = e;
48
+ this.#status = "error";
49
+ throw e;
50
+ }
51
+ }
52
+ abort() {
53
+ this.#abortController.abort();
54
+ }
55
+ };
56
+ /**
57
+ * Runs tasks concurrently, deduplicated by task id.
58
+ *
59
+ * If a task with a given id is already in flight, subsequent schedule() calls
60
+ * for that id are silently ignored until the first completes. Tasks are stored
61
+ * so abortAll() can cancel any in-flight work (e.g. on engine cleanup).
62
+ */
63
+ var ConcurrentRunner = class {
64
+ #pending = /* @__PURE__ */ new Map();
65
+ schedule(task) {
66
+ if (this.#pending.has(task.id)) return;
67
+ this.#pending.set(task.id, task);
68
+ task.run().catch((error) => {
69
+ if (!(error instanceof Error && error.name === "AbortError")) throw error;
70
+ }).finally(() => {
71
+ this.#pending.delete(task.id);
72
+ });
73
+ }
74
+ abortAll() {
75
+ for (const task of this.#pending.values()) task.abort();
76
+ this.#pending.clear();
77
+ }
78
+ };
79
+ /**
80
+ * Runs tasks one at a time in submission order.
81
+ *
82
+ * Each schedule() call returns a Promise that resolves or rejects with the
83
+ * task's result when it is eventually executed. Tasks wait in queue until the
84
+ * prior task completes.
85
+ *
86
+ * Serialization is achieved by chaining each task's run() onto the tail of a
87
+ * shared promise chain — no explicit queue or drain loop needed.
88
+ *
89
+ * abortAll() aborts all pending (not yet started) tasks and the currently
90
+ * in-flight task. Pending tasks still run briefly but receive an aborted
91
+ * signal and are expected to exit early.
92
+ */
93
+ var SerialRunner = class {
94
+ #chain = Promise.resolve();
95
+ #pending = /* @__PURE__ */ new Set();
96
+ #current = null;
97
+ schedule(task) {
98
+ const t = task;
99
+ this.#pending.add(t);
100
+ const result = this.#chain.then(() => {
101
+ this.#pending.delete(t);
102
+ this.#current = t;
103
+ return task.run();
104
+ }).finally(() => {
105
+ this.#current = null;
106
+ });
107
+ this.#chain = result.then(() => {}, () => {});
108
+ return result;
109
+ }
110
+ abortAll() {
111
+ for (const task of this.#pending) task.abort();
112
+ this.#pending.clear();
113
+ this.#current?.abort();
114
+ }
115
+ destroy() {
116
+ this.abortAll();
117
+ }
118
+ };
119
+ //#endregion
120
+ export { ConcurrentRunner, SerialRunner, Task };
121
+
122
+ //# sourceMappingURL=task.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"task.js","names":["#runFn","#abortController","#signal","#status","#value","#error","#pending","#chain","#current"],"sources":["../../../src/core/task.ts"],"sourcesContent":["import { anyAbortSignal } from '@videojs/utils/events';\nimport { generateId } from './utils/generate-id';\n\n// =============================================================================\n// DeepReadonly\n// =============================================================================\n\n/** Recursively marks all properties as readonly. */\nexport type DeepReadonly<T> = T extends (infer U)[]\n ? ReadonlyArray<DeepReadonly<U>>\n : T extends object\n ? { readonly [K in keyof T]: DeepReadonly<T[K]> }\n : T;\n\n// =============================================================================\n// Task\n// =============================================================================\n\nexport type TaskStatus = 'pending' | 'running' | 'done' | 'error';\n\n/**\n * Configuration for a Task.\n */\nexport interface TaskConfig {\n /**\n * Identifier for this task.\n * - string: used as-is\n * - () => string: called once at construction time\n * - undefined: a unique ID is generated via generateId()\n */\n id?: string | (() => string);\n\n /**\n * Optional external AbortSignal to compose with the task's internal one.\n * The task's work is aborted when either the internal controller (via abort())\n * or this external signal fires — whichever comes first.\n */\n signal?: AbortSignal;\n}\n\n/**\n * Minimal contract for a schedulable unit of async work.\n */\nexport interface TaskLike<TValue = void, TError = unknown> {\n readonly id: string;\n readonly status: TaskStatus;\n readonly value: DeepReadonly<TValue> | undefined;\n readonly error: DeepReadonly<TError> | undefined;\n run(): Promise<TValue>;\n abort(): void;\n}\n\n/**\n * Generic reusable task that wraps an async run function.\n *\n * Owns its own AbortController so it can always be aborted independently.\n * Optionally composes an external AbortSignal so that a parent's cancellation\n * propagates into the task's work without requiring the caller to track the\n * task separately.\n *\n * Ordering guarantee: `value` is written before `status` transitions to `'done'`;\n * `error` is written before `status` transitions to `'error'`. Any reader\n * observing `status === 'done'` is guaranteed `value` is already present.\n */\nexport class Task<TValue = void, TError = unknown> implements TaskLike<TValue, TError> {\n readonly id: string;\n readonly #runFn: (signal: AbortSignal) => Promise<TValue>;\n readonly #abortController = new AbortController();\n readonly #signal: AbortSignal;\n\n #status: TaskStatus = 'pending';\n #value: TValue | undefined = undefined;\n #error: TError | undefined = undefined;\n\n constructor(runFn: (signal: AbortSignal) => Promise<TValue>, config?: TaskConfig) {\n this.#runFn = runFn;\n const rawId = config?.id;\n this.id = typeof rawId === 'function' ? rawId() : (rawId ?? generateId());\n this.#signal = config?.signal\n ? anyAbortSignal([this.#abortController.signal, config.signal])\n : this.#abortController.signal;\n }\n\n get status(): TaskStatus {\n return this.#status;\n }\n\n get value(): DeepReadonly<TValue> | undefined {\n return this.#value as DeepReadonly<TValue> | undefined;\n }\n\n get error(): DeepReadonly<TError> | undefined {\n return this.#error as DeepReadonly<TError> | undefined;\n }\n\n async run(): Promise<TValue> {\n this.#status = 'running';\n try {\n const result = await this.#runFn(this.#signal);\n this.#value = result; // value before status — ordering guarantee\n this.#status = 'done';\n return result;\n } catch (e) {\n this.#error = e as TError; // error before status — ordering guarantee\n this.#status = 'error';\n throw e;\n }\n }\n\n abort(): void {\n this.#abortController.abort();\n }\n}\n\n// =============================================================================\n// ConcurrentRunner\n// =============================================================================\n\n/**\n * Runs tasks concurrently, deduplicated by task id.\n *\n * If a task with a given id is already in flight, subsequent schedule() calls\n * for that id are silently ignored until the first completes. Tasks are stored\n * so abortAll() can cancel any in-flight work (e.g. on engine cleanup).\n */\nexport class ConcurrentRunner {\n readonly #pending = new Map<string, TaskLike<unknown, unknown>>();\n\n schedule<TValue = void, TError = unknown>(task: TaskLike<TValue, TError>): void {\n if (this.#pending.has(task.id)) return;\n\n this.#pending.set(task.id, task as TaskLike<unknown, unknown>);\n task\n .run()\n .catch((error) => {\n if (!(error instanceof Error && error.name === 'AbortError')) throw error;\n })\n .finally(() => {\n this.#pending.delete(task.id);\n });\n }\n\n abortAll(): void {\n for (const task of this.#pending.values()) task.abort();\n this.#pending.clear();\n }\n}\n\n// =============================================================================\n// SerialRunner\n// =============================================================================\n\n/**\n * Runs tasks one at a time in submission order.\n *\n * Each schedule() call returns a Promise that resolves or rejects with the\n * task's result when it is eventually executed. Tasks wait in queue until the\n * prior task completes.\n *\n * Serialization is achieved by chaining each task's run() onto the tail of a\n * shared promise chain — no explicit queue or drain loop needed.\n *\n * abortAll() aborts all pending (not yet started) tasks and the currently\n * in-flight task. Pending tasks still run briefly but receive an aborted\n * signal and are expected to exit early.\n */\nexport class SerialRunner {\n #chain: Promise<unknown> = Promise.resolve();\n readonly #pending = new Set<TaskLike<unknown, unknown>>();\n #current: TaskLike<unknown, unknown> | null = null;\n\n schedule<TValue = void, TError = unknown>(task: TaskLike<TValue, TError>): Promise<TValue> {\n const t = task as TaskLike<unknown, unknown>;\n this.#pending.add(t);\n\n const result = this.#chain\n .then(() => {\n this.#pending.delete(t);\n this.#current = t;\n return task.run();\n })\n .finally(() => {\n this.#current = null;\n });\n\n // Advance the chain regardless of whether this task succeeds or fails.\n this.#chain = result.then(\n () => {},\n () => {}\n );\n\n return result as Promise<TValue>;\n }\n\n abortAll(): void {\n for (const task of this.#pending) task.abort();\n this.#pending.clear();\n this.#current?.abort();\n }\n\n destroy(): void {\n this.abortAll();\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;AAgEA,IAAa,OAAb,MAAuF;CACrF;CACA;CACA,mBAA4B,IAAI,iBAAiB;CACjD;CAEA,UAAsB;CACtB,SAA6B,KAAA;CAC7B,SAA6B,KAAA;CAE7B,YAAY,OAAiD,QAAqB;AAChF,QAAA,QAAc;EACd,MAAM,QAAQ,QAAQ;AACtB,OAAK,KAAK,OAAO,UAAU,aAAa,OAAO,GAAI,SAAS,YAAY;AACxE,QAAA,SAAe,QAAQ,SACnB,eAAe,CAAC,MAAA,gBAAsB,QAAQ,OAAO,OAAO,CAAC,GAC7D,MAAA,gBAAsB;;CAG5B,IAAI,SAAqB;AACvB,SAAO,MAAA;;CAGT,IAAI,QAA0C;AAC5C,SAAO,MAAA;;CAGT,IAAI,QAA0C;AAC5C,SAAO,MAAA;;CAGT,MAAM,MAAuB;AAC3B,QAAA,SAAe;AACf,MAAI;GACF,MAAM,SAAS,MAAM,MAAA,MAAY,MAAA,OAAa;AAC9C,SAAA,QAAc;AACd,SAAA,SAAe;AACf,UAAO;WACA,GAAG;AACV,SAAA,QAAc;AACd,SAAA,SAAe;AACf,SAAM;;;CAIV,QAAc;AACZ,QAAA,gBAAsB,OAAO;;;;;;;;;;AAejC,IAAa,mBAAb,MAA8B;CAC5B,2BAAoB,IAAI,KAAyC;CAEjE,SAA0C,MAAsC;AAC9E,MAAI,MAAA,QAAc,IAAI,KAAK,GAAG,CAAE;AAEhC,QAAA,QAAc,IAAI,KAAK,IAAI,KAAmC;AAC9D,OACG,KAAK,CACL,OAAO,UAAU;AAChB,OAAI,EAAE,iBAAiB,SAAS,MAAM,SAAS,cAAe,OAAM;IACpE,CACD,cAAc;AACb,SAAA,QAAc,OAAO,KAAK,GAAG;IAC7B;;CAGN,WAAiB;AACf,OAAK,MAAM,QAAQ,MAAA,QAAc,QAAQ,CAAE,MAAK,OAAO;AACvD,QAAA,QAAc,OAAO;;;;;;;;;;;;;;;;;AAsBzB,IAAa,eAAb,MAA0B;CACxB,SAA2B,QAAQ,SAAS;CAC5C,2BAAoB,IAAI,KAAiC;CACzD,WAA8C;CAE9C,SAA0C,MAAiD;EACzF,MAAM,IAAI;AACV,QAAA,QAAc,IAAI,EAAE;EAEpB,MAAM,SAAS,MAAA,MACZ,WAAW;AACV,SAAA,QAAc,OAAO,EAAE;AACvB,SAAA,UAAgB;AAChB,UAAO,KAAK,KAAK;IACjB,CACD,cAAc;AACb,SAAA,UAAgB;IAChB;AAGJ,QAAA,QAAc,OAAO,WACb,UACA,GACP;AAED,SAAO;;CAGT,WAAiB;AACf,OAAK,MAAM,QAAQ,MAAA,QAAe,MAAK,OAAO;AAC9C,QAAA,QAAc,OAAO;AACrB,QAAA,SAAe,OAAO;;CAGxB,UAAgB;AACd,OAAK,UAAU"}