@videojs/spf 10.0.0-beta.2 → 10.0.0-beta.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (264) hide show
  1. package/README.md +16 -40
  2. package/dist/default/core/abr/bandwidth-estimator.js +95 -0
  3. package/dist/default/core/abr/bandwidth-estimator.js.map +1 -0
  4. package/dist/default/core/abr/ewma.js +71 -0
  5. package/dist/default/core/abr/ewma.js.map +1 -0
  6. package/dist/default/core/abr/quality-selection.js +53 -0
  7. package/dist/default/core/abr/quality-selection.js.map +1 -0
  8. package/dist/default/core/buffer/back-buffer.js +46 -0
  9. package/dist/default/core/buffer/back-buffer.js.map +1 -0
  10. package/dist/default/core/buffer/forward-buffer.js +73 -0
  11. package/dist/default/core/buffer/forward-buffer.js.map +1 -0
  12. package/dist/default/core/create-machine-actor.js +91 -0
  13. package/dist/default/core/create-machine-actor.js.map +1 -0
  14. package/dist/default/core/create-machine-reactor.js +83 -0
  15. package/dist/default/core/create-machine-reactor.js.map +1 -0
  16. package/dist/default/core/create-transition-actor.js +46 -0
  17. package/dist/default/core/create-transition-actor.js.map +1 -0
  18. package/dist/default/core/features/calculate-presentation-duration.js +53 -0
  19. package/dist/default/core/features/calculate-presentation-duration.js.map +1 -0
  20. package/dist/default/core/features/quality-switching.js +69 -0
  21. package/dist/default/core/features/quality-switching.js.map +1 -0
  22. package/dist/default/core/features/resolve-presentation.js +73 -0
  23. package/dist/default/core/features/resolve-presentation.js.map +1 -0
  24. package/dist/default/core/features/resolve-track.js +66 -0
  25. package/dist/default/core/features/resolve-track.js.map +1 -0
  26. package/dist/default/core/features/select-tracks.js +130 -0
  27. package/dist/default/core/features/select-tracks.js.map +1 -0
  28. package/dist/default/core/features/sync-preload-attribute.js +26 -0
  29. package/dist/default/core/features/sync-preload-attribute.js.map +1 -0
  30. package/dist/default/core/hls/parse-attributes.js +134 -0
  31. package/dist/default/core/hls/parse-attributes.js.map +1 -0
  32. package/dist/default/core/hls/parse-media-playlist.js +82 -0
  33. package/dist/default/core/hls/parse-media-playlist.js.map +1 -0
  34. package/dist/default/core/hls/parse-multivariant.js +214 -0
  35. package/dist/default/core/hls/parse-multivariant.js.map +1 -0
  36. package/dist/default/core/hls/resolve-url.js +11 -0
  37. package/dist/default/core/hls/resolve-url.js.map +1 -0
  38. package/dist/default/core/machine.js +26 -0
  39. package/dist/default/core/machine.js.map +1 -0
  40. package/dist/default/core/signals/effect.js +41 -0
  41. package/dist/default/core/signals/effect.js.map +1 -0
  42. package/dist/default/core/signals/primitives.js +32 -0
  43. package/dist/default/core/signals/primitives.js.map +1 -0
  44. package/dist/default/core/task.js +191 -0
  45. package/dist/default/core/task.js.map +1 -0
  46. package/dist/default/core/types/index.js +15 -0
  47. package/dist/default/core/types/index.js.map +1 -0
  48. package/dist/default/core/utils/generate-id.js +21 -0
  49. package/dist/default/core/utils/generate-id.js.map +1 -0
  50. package/dist/default/core/utils/track-selection.js +39 -0
  51. package/dist/default/core/utils/track-selection.js.map +1 -0
  52. package/dist/default/dom/features/end-of-stream.js +145 -0
  53. package/dist/default/dom/features/end-of-stream.js.map +1 -0
  54. package/dist/default/dom/features/load-segments.js +192 -0
  55. package/dist/default/dom/features/load-segments.js.map +1 -0
  56. package/dist/default/dom/features/load-text-track-cues.js +104 -0
  57. package/dist/default/dom/features/load-text-track-cues.js.map +1 -0
  58. package/dist/default/dom/features/segment-loader-actor.js +243 -0
  59. package/dist/default/dom/features/segment-loader-actor.js.map +1 -0
  60. package/dist/default/dom/features/setup-mediasource.js +48 -0
  61. package/dist/default/dom/features/setup-mediasource.js.map +1 -0
  62. package/dist/default/dom/features/setup-sourcebuffer.js +78 -0
  63. package/dist/default/dom/features/setup-sourcebuffer.js.map +1 -0
  64. package/dist/default/dom/features/sync-text-tracks.js +95 -0
  65. package/dist/default/dom/features/sync-text-tracks.js.map +1 -0
  66. package/dist/default/dom/features/text-track-segment-loader-actor.js +55 -0
  67. package/dist/default/dom/features/text-track-segment-loader-actor.js.map +1 -0
  68. package/dist/default/dom/features/text-tracks-actor.js +42 -0
  69. package/dist/default/dom/features/text-tracks-actor.js.map +1 -0
  70. package/dist/default/dom/features/track-current-time.js +49 -0
  71. package/dist/default/dom/features/track-current-time.js.map +1 -0
  72. package/dist/default/dom/features/track-playback-initiated.js +66 -0
  73. package/dist/default/dom/features/track-playback-initiated.js.map +1 -0
  74. package/dist/default/dom/features/track-playback-rate.js +32 -0
  75. package/dist/default/dom/features/track-playback-rate.js.map +1 -0
  76. package/dist/default/dom/features/update-duration.js +78 -0
  77. package/dist/default/dom/features/update-duration.js.map +1 -0
  78. package/dist/default/dom/media/append-segment.js +60 -0
  79. package/dist/default/dom/media/append-segment.js.map +1 -0
  80. package/dist/default/dom/media/buffer-flusher.js +55 -0
  81. package/dist/default/dom/media/buffer-flusher.js.map +1 -0
  82. package/dist/default/dom/media/mediasource-setup.js +146 -0
  83. package/dist/default/dom/media/mediasource-setup.js.map +1 -0
  84. package/dist/default/dom/media/source-buffer-actor.js +137 -0
  85. package/dist/default/dom/media/source-buffer-actor.js.map +1 -0
  86. package/dist/default/dom/network/chunked-stream-iterable.js +49 -0
  87. package/dist/default/dom/network/chunked-stream-iterable.js.map +1 -0
  88. package/dist/default/dom/network/fetch.js +54 -0
  89. package/dist/default/dom/network/fetch.js.map +1 -0
  90. package/dist/default/dom/playback-engine/adapter.js +100 -0
  91. package/dist/default/dom/playback-engine/adapter.js.map +1 -0
  92. package/dist/default/dom/playback-engine/engine.js +142 -0
  93. package/dist/default/dom/playback-engine/engine.js.map +1 -0
  94. package/dist/default/dom/playback-engine.js +4 -0
  95. package/dist/default/dom/text/parse-vtt-segment.js +56 -0
  96. package/dist/default/dom/text/parse-vtt-segment.js.map +1 -0
  97. package/dist/default/dom.js +10 -0
  98. package/dist/{src-DRsS_H80.js → default/index.js} +3 -3
  99. package/dist/default/index.js.map +1 -0
  100. package/dist/dev/core/abr/bandwidth-estimator.d.ts +35 -0
  101. package/dist/dev/core/abr/bandwidth-estimator.d.ts.map +1 -0
  102. package/dist/dev/core/abr/bandwidth-estimator.js +95 -0
  103. package/dist/dev/core/abr/bandwidth-estimator.js.map +1 -0
  104. package/dist/dev/core/abr/ewma.js +71 -0
  105. package/dist/dev/core/abr/ewma.js.map +1 -0
  106. package/dist/dev/core/abr/quality-selection.js +53 -0
  107. package/dist/dev/core/abr/quality-selection.js.map +1 -0
  108. package/dist/dev/core/actor.d.ts +27 -0
  109. package/dist/dev/core/actor.d.ts.map +1 -0
  110. package/dist/dev/core/buffer/back-buffer.js +46 -0
  111. package/dist/dev/core/buffer/back-buffer.js.map +1 -0
  112. package/dist/dev/core/buffer/forward-buffer.js +73 -0
  113. package/dist/dev/core/buffer/forward-buffer.js.map +1 -0
  114. package/dist/dev/core/create-machine-actor.d.ts +12 -0
  115. package/dist/dev/core/create-machine-actor.d.ts.map +1 -0
  116. package/dist/dev/core/create-machine-actor.js +91 -0
  117. package/dist/dev/core/create-machine-actor.js.map +1 -0
  118. package/dist/dev/core/create-machine-reactor.d.ts +8 -0
  119. package/dist/dev/core/create-machine-reactor.d.ts.map +1 -0
  120. package/dist/dev/core/create-machine-reactor.js +83 -0
  121. package/dist/dev/core/create-machine-reactor.js.map +1 -0
  122. package/dist/dev/core/create-transition-actor.d.ts +23 -0
  123. package/dist/dev/core/create-transition-actor.d.ts.map +1 -0
  124. package/dist/dev/core/create-transition-actor.js +46 -0
  125. package/dist/dev/core/create-transition-actor.js.map +1 -0
  126. package/dist/dev/core/features/calculate-presentation-duration.js +53 -0
  127. package/dist/dev/core/features/calculate-presentation-duration.js.map +1 -0
  128. package/dist/dev/core/features/quality-switching.js +69 -0
  129. package/dist/dev/core/features/quality-switching.js.map +1 -0
  130. package/dist/dev/core/features/resolve-presentation.js +73 -0
  131. package/dist/dev/core/features/resolve-presentation.js.map +1 -0
  132. package/dist/dev/core/features/resolve-track.js +66 -0
  133. package/dist/dev/core/features/resolve-track.js.map +1 -0
  134. package/dist/dev/core/features/select-tracks.js +130 -0
  135. package/dist/dev/core/features/select-tracks.js.map +1 -0
  136. package/dist/dev/core/features/sync-preload-attribute.js +26 -0
  137. package/dist/dev/core/features/sync-preload-attribute.js.map +1 -0
  138. package/dist/dev/core/hls/parse-attributes.js +134 -0
  139. package/dist/dev/core/hls/parse-attributes.js.map +1 -0
  140. package/dist/dev/core/hls/parse-media-playlist.js +82 -0
  141. package/dist/dev/core/hls/parse-media-playlist.js.map +1 -0
  142. package/dist/dev/core/hls/parse-multivariant.js +214 -0
  143. package/dist/dev/core/hls/parse-multivariant.js.map +1 -0
  144. package/dist/dev/core/hls/resolve-url.js +11 -0
  145. package/dist/dev/core/hls/resolve-url.js.map +1 -0
  146. package/dist/dev/core/machine.d.ts +21 -0
  147. package/dist/dev/core/machine.d.ts.map +1 -0
  148. package/dist/dev/core/machine.js +26 -0
  149. package/dist/dev/core/machine.js.map +1 -0
  150. package/dist/dev/core/signals/effect.d.ts +15 -0
  151. package/dist/dev/core/signals/effect.d.ts.map +1 -0
  152. package/dist/dev/core/signals/effect.js +41 -0
  153. package/dist/dev/core/signals/effect.js.map +1 -0
  154. package/dist/dev/core/signals/primitives.d.ts +10 -0
  155. package/dist/dev/core/signals/primitives.d.ts.map +1 -0
  156. package/dist/dev/core/signals/primitives.js +32 -0
  157. package/dist/dev/core/signals/primitives.js.map +1 -0
  158. package/dist/dev/core/task.js +191 -0
  159. package/dist/dev/core/task.js.map +1 -0
  160. package/dist/dev/core/types/index.d.ts +166 -0
  161. package/dist/dev/core/types/index.d.ts.map +1 -0
  162. package/dist/dev/core/types/index.js +15 -0
  163. package/dist/dev/core/types/index.js.map +1 -0
  164. package/dist/dev/core/utils/generate-id.js +21 -0
  165. package/dist/dev/core/utils/generate-id.js.map +1 -0
  166. package/dist/dev/core/utils/track-selection.d.ts +16 -0
  167. package/dist/dev/core/utils/track-selection.d.ts.map +1 -0
  168. package/dist/dev/core/utils/track-selection.js +39 -0
  169. package/dist/dev/core/utils/track-selection.js.map +1 -0
  170. package/dist/dev/dom/features/end-of-stream.js +145 -0
  171. package/dist/dev/dom/features/end-of-stream.js.map +1 -0
  172. package/dist/dev/dom/features/load-segments.d.ts +72 -0
  173. package/dist/dev/dom/features/load-segments.d.ts.map +1 -0
  174. package/dist/dev/dom/features/load-segments.js +192 -0
  175. package/dist/dev/dom/features/load-segments.js.map +1 -0
  176. package/dist/dev/dom/features/load-text-track-cues.d.ts +85 -0
  177. package/dist/dev/dom/features/load-text-track-cues.d.ts.map +1 -0
  178. package/dist/dev/dom/features/load-text-track-cues.js +104 -0
  179. package/dist/dev/dom/features/load-text-track-cues.js.map +1 -0
  180. package/dist/dev/dom/features/segment-loader-actor.d.ts +1 -0
  181. package/dist/dev/dom/features/segment-loader-actor.js +243 -0
  182. package/dist/dev/dom/features/segment-loader-actor.js.map +1 -0
  183. package/dist/dev/dom/features/setup-mediasource.js +48 -0
  184. package/dist/dev/dom/features/setup-mediasource.js.map +1 -0
  185. package/dist/dev/dom/features/setup-sourcebuffer.d.ts +9 -0
  186. package/dist/dev/dom/features/setup-sourcebuffer.d.ts.map +1 -0
  187. package/dist/dev/dom/features/setup-sourcebuffer.js +78 -0
  188. package/dist/dev/dom/features/setup-sourcebuffer.js.map +1 -0
  189. package/dist/dev/dom/features/sync-text-tracks.js +95 -0
  190. package/dist/dev/dom/features/sync-text-tracks.js.map +1 -0
  191. package/dist/dev/dom/features/text-track-segment-loader-actor.d.ts +12 -0
  192. package/dist/dev/dom/features/text-track-segment-loader-actor.d.ts.map +1 -0
  193. package/dist/dev/dom/features/text-track-segment-loader-actor.js +55 -0
  194. package/dist/dev/dom/features/text-track-segment-loader-actor.js.map +1 -0
  195. package/dist/dev/dom/features/text-tracks-actor.d.ts +31 -0
  196. package/dist/dev/dom/features/text-tracks-actor.d.ts.map +1 -0
  197. package/dist/dev/dom/features/text-tracks-actor.js +42 -0
  198. package/dist/dev/dom/features/text-tracks-actor.js.map +1 -0
  199. package/dist/dev/dom/features/track-current-time.d.ts +40 -0
  200. package/dist/dev/dom/features/track-current-time.d.ts.map +1 -0
  201. package/dist/dev/dom/features/track-current-time.js +49 -0
  202. package/dist/dev/dom/features/track-current-time.js.map +1 -0
  203. package/dist/dev/dom/features/track-playback-initiated.d.ts +46 -0
  204. package/dist/dev/dom/features/track-playback-initiated.d.ts.map +1 -0
  205. package/dist/dev/dom/features/track-playback-initiated.js +66 -0
  206. package/dist/dev/dom/features/track-playback-initiated.js.map +1 -0
  207. package/dist/dev/dom/features/track-playback-rate.d.ts +34 -0
  208. package/dist/dev/dom/features/track-playback-rate.d.ts.map +1 -0
  209. package/dist/dev/dom/features/track-playback-rate.js +32 -0
  210. package/dist/dev/dom/features/track-playback-rate.js.map +1 -0
  211. package/dist/dev/dom/features/update-duration.js +78 -0
  212. package/dist/dev/dom/features/update-duration.js.map +1 -0
  213. package/dist/dev/dom/media/append-segment.d.ts +24 -0
  214. package/dist/dev/dom/media/append-segment.d.ts.map +1 -0
  215. package/dist/dev/dom/media/append-segment.js +60 -0
  216. package/dist/dev/dom/media/append-segment.js.map +1 -0
  217. package/dist/dev/dom/media/buffer-flusher.d.ts +24 -0
  218. package/dist/dev/dom/media/buffer-flusher.d.ts.map +1 -0
  219. package/dist/dev/dom/media/buffer-flusher.js +55 -0
  220. package/dist/dev/dom/media/buffer-flusher.js.map +1 -0
  221. package/dist/dev/dom/media/mediasource-setup.js +146 -0
  222. package/dist/dev/dom/media/mediasource-setup.js.map +1 -0
  223. package/dist/dev/dom/media/source-buffer-actor.d.ts +62 -0
  224. package/dist/dev/dom/media/source-buffer-actor.d.ts.map +1 -0
  225. package/dist/dev/dom/media/source-buffer-actor.js +137 -0
  226. package/dist/dev/dom/media/source-buffer-actor.js.map +1 -0
  227. package/dist/dev/dom/network/chunked-stream-iterable.js +49 -0
  228. package/dist/dev/dom/network/chunked-stream-iterable.js.map +1 -0
  229. package/dist/dev/dom/network/fetch.js +54 -0
  230. package/dist/dev/dom/network/fetch.js.map +1 -0
  231. package/dist/dev/dom/playback-engine/adapter.d.ts +40 -0
  232. package/dist/dev/dom/playback-engine/adapter.d.ts.map +1 -0
  233. package/dist/dev/dom/playback-engine/adapter.js +100 -0
  234. package/dist/dev/dom/playback-engine/adapter.js.map +1 -0
  235. package/dist/dev/dom/playback-engine/engine.d.ts +124 -0
  236. package/dist/dev/dom/playback-engine/engine.d.ts.map +1 -0
  237. package/dist/dev/dom/playback-engine/engine.js +142 -0
  238. package/dist/dev/dom/playback-engine/engine.js.map +1 -0
  239. package/dist/dev/dom/playback-engine.d.ts +4 -0
  240. package/dist/dev/dom/playback-engine.js +4 -0
  241. package/dist/dev/dom/text/parse-vtt-segment.d.ts +12 -0
  242. package/dist/dev/dom/text/parse-vtt-segment.d.ts.map +1 -0
  243. package/dist/dev/dom/text/parse-vtt-segment.js +56 -0
  244. package/dist/dev/dom/text/parse-vtt-segment.js.map +1 -0
  245. package/dist/dev/dom.d.ts +10 -0
  246. package/dist/dev/dom.js +10 -0
  247. package/dist/dev/index.d.ts.map +1 -0
  248. package/dist/dev/index.js +14 -0
  249. package/dist/dev/index.js.map +1 -0
  250. package/package.json +26 -24
  251. package/dist/adapter-CMw-rvbk.js +0 -3512
  252. package/dist/adapter-CMw-rvbk.js.map +0 -1
  253. package/dist/adapter-l8iJN_GI.d.ts +0 -689
  254. package/dist/adapter-l8iJN_GI.d.ts.map +0 -1
  255. package/dist/dom/index.d.ts +0 -243
  256. package/dist/dom/index.d.ts.map +0 -1
  257. package/dist/dom/index.js +0 -38
  258. package/dist/dom/index.js.map +0 -1
  259. package/dist/dom/playback-engine/index.d.ts +0 -2
  260. package/dist/dom/playback-engine/index.js +0 -4
  261. package/dist/index.d.ts.map +0 -1
  262. package/dist/index.js +0 -3
  263. package/dist/src-DRsS_H80.js.map +0 -1
  264. /package/dist/{index.d.ts → dev/index.d.ts} +0 -0
@@ -1 +0,0 @@
1
- {"version":3,"file":"adapter-CMw-rvbk.js","names":["#current","#equalityFn","#pending","#scheduleFlush","listener","#listeners","entry: SelectorEntry<T, unknown>","#selectorListeners","#pendingFlush","result: { video?: string; audio?: string }","start: number","segments: Segment[]","initSegmentUrl: string | undefined","initSegmentByteRange: { start: number; end: number } | undefined","currentByteRange: { start: number; end: number } | undefined","previousByteRangeEnd: number | undefined","segment: Segment","streams: StreamInfo[]","audioRenditions: AudioRenditionInfo[]","subtitleRenditions: SubtitleRenditionInfo[]","pendingStreamInfo: Omit<StreamInfo, 'uri'> | null","videoStreams: typeof streams","audioOnlyStreams: typeof streams","videoTracks: PartiallyResolvedVideoTrack[]","track: PartiallyResolvedVideoTrack","audioOnlyTracks: PartiallyResolvedAudioTrack[]","audioCodecs: string[] | undefined","track: PartiallyResolvedAudioTrack","textTracks: PartiallyResolvedTextTrack[]","track: PartiallyResolvedTextTrack","selectionSets: SelectionSet[]","videoSwitchingSet: VideoSwitchingSet","videoSelectionSet: VideoSelectionSet","audioSwitchingSet: AudioSwitchingSet","audioSelectionSet: AudioSelectionSet","textSwitchingSet: TextSwitchingSet","textSelectionSet: TextSelectionSet","DEFAULT_BANDWIDTH_CONFIG: BandwidthConfig","DEFAULT_QUALITY_CONFIG: QualityConfig","chosen: PartiallyResolvedVideoTrack | undefined","DEFAULT_BACK_BUFFER_CONFIG: BackBufferConfig","DEFAULT_FORWARD_BUFFER_CONFIG: ForwardBufferConfig","detach","latest: unknown[]","hasValue: boolean[]","unsubscribers: Array<() => void>","prevSelected: R | undefined","canResolve","shouldResolve","abortController: AbortController | null","DEFAULT_SWITCHING_CONFIG: Required<QualitySwitchingConfig>","pendingTasks: LoadTask[] | null","inFlightInitTrackId: string | null","inFlightSegmentId: string | null","abortController: AbortController | null","tasks: LoadTask[]","ActorKeyByType","segmentsCanLoad","state","dummyVideo: HTMLVideoElement | null","cues: VTTCue[]","currentTask: Promise<void> | null","abortController: AbortController | null","lastTrackId: string | undefined","lastMediaElement: HTMLMediaElement | undefined","removeListeners: (() => void) | null","lastMediaElement: HTMLMediaElement | undefined","removeListener: (() => void) | null","lastPresentationUrl: string | undefined","#runFn","#abortController","#signal","#status","#value","#error","#pending","#chain","#current","waitForSourceBuffersReady","getMaxBufferedEnd","activeActorUnsubs: Array<() => void>","abortController: AbortController | null","ranges: BufferedRange[]","currentTask: Promise<void> | null","createdTracks: HTMLTrackElement[]","lastMediaElement: HTMLMediaElement | undefined","removeListener: (() => void) | null","#config","#engine","#cancelPendingPlay","#loadstartListener"],"sources":["../src/core/state/create-state.ts","../src/core/hls/parse-attributes.ts","../src/core/hls/resolve-url.ts","../src/core/hls/parse-media-playlist.ts","../src/core/utils/generate-id.ts","../src/core/hls/parse-multivariant.ts","../src/core/abr/ewma.ts","../src/core/abr/bandwidth-estimator.ts","../src/core/abr/quality-selection.ts","../src/core/buffer/back-buffer.ts","../src/core/buffer/forward-buffer.ts","../src/core/types/index.ts","../src/dom/media/mediasource-setup.ts","../src/dom/network/fetch.ts","../src/core/events/create-event-stream.ts","../src/core/reactive/combine-latest.ts","../src/core/features/resolve-presentation.ts","../src/core/features/quality-switching.ts","../src/core/utils/track-selection.ts","../src/dom/features/segment-loader-actor.ts","../src/dom/features/load-segments.ts","../src/dom/text/parse-vtt-segment.ts","../src/dom/features/load-text-track-cues.ts","../src/dom/features/track-current-time.ts","../src/dom/features/track-playback-initiated.ts","../src/dom/media/append-segment.ts","../src/dom/media/buffer-flusher.ts","../src/core/features/calculate-presentation-duration.ts","../src/core/task.ts","../src/core/features/resolve-track.ts","../src/core/features/select-tracks.ts","../src/dom/features/end-of-stream.ts","../src/dom/features/setup-mediasource.ts","../src/dom/media/source-buffer-actor.ts","../src/dom/features/setup-sourcebuffer.ts","../src/dom/features/setup-text-tracks.ts","../src/dom/features/sync-selected-text-track-from-dom.ts","../src/dom/features/sync-text-track-modes.ts","../src/dom/features/update-duration.ts","../src/dom/playback-engine/engine.ts","../src/dom/playback-engine/adapter.ts"],"sourcesContent":["/**\n * Reactive state container with selectors, custom equality, and batched updates.\n *\n * Manages both immutable state values and mutable object references (e.g., HTMLMediaElement).\n */\n\n// =============================================================================\n// Symbol for type identification\n// =============================================================================\n\nconst STATE_SYMBOL = Symbol('@videojs/spf/state');\n\n// =============================================================================\n// Types\n// =============================================================================\n\n/**\n * State change listener for full-state subscriptions.\n */\nexport type StateListener<T> = (current: T) => void;\n\n/**\n * Selector function to derive a value from state.\n */\nexport type StateSelector<T, R> = (state: T) => R;\n\n/**\n * Listener for selector-based subscriptions.\n */\nexport type SelectorListener<R> = (current: R) => void;\n\n/**\n * Options for selector subscriptions.\n */\nexport interface SelectorOptions<R> {\n /**\n * Custom equality function for selected value.\n * Defaults to Object.is\n */\n equalityFn?: (a: R, b: R) => boolean;\n}\n\n/**\n * Configuration for creating a state container.\n */\nexport interface StateConfig<T> {\n /**\n * Custom equality function for change detection.\n * Defaults to Object.is for top-level comparison.\n */\n equalityFn?: (a: T, b: T) => boolean;\n}\n\n/**\n * Read-only state interface.\n */\nexport interface State<T> {\n /** Symbol for type identification */\n readonly [STATE_SYMBOL]: true;\n\n /** Current state snapshot */\n readonly current: T;\n\n /** Subscribe to state changes */\n subscribe(listener: StateListener<T>): () => void;\n}\n\n/**\n * Writable state interface with patch capability.\n */\nexport interface WritableState<T> extends State<T> {\n /** Update state with partial object (for object state) or full value (for primitive state) */\n patch(partial: T extends object ? Partial<T> : T): void;\n\n /** Subscribe to full-state changes */\n subscribe(listener: StateListener<T>): () => void;\n\n /** Subscribe to selected value changes */\n subscribe<R>(selector: StateSelector<T, R>, listener: SelectorListener<R>, options?: SelectorOptions<R>): () => void;\n\n /** Manually flush pending updates */\n flush(): void;\n}\n\n/**\n * Entry for selector subscriptions.\n */\ninterface SelectorEntry<T, R> {\n selector: StateSelector<T, R>;\n listener: SelectorListener<R>;\n options: SelectorOptions<R>;\n}\n\n// =============================================================================\n// Default equality function\n// =============================================================================\n\n/**\n * Default equality function using Object.is.\n */\nfunction defaultEquality<T>(a: T, b: T): boolean {\n return Object.is(a, b);\n}\n\n// =============================================================================\n// Implementation\n// =============================================================================\n\n/**\n * State container implementation.\n */\nclass StateContainer<T> implements WritableState<T> {\n [STATE_SYMBOL] = true as const;\n\n #current: T;\n #pending: T | null = null;\n #pendingFlush = false;\n #equalityFn: (a: T, b: T) => boolean;\n\n // Full-state listeners\n #listeners = new Set<StateListener<T>>();\n\n // Selector-based listeners\n #selectorListeners = new Set<SelectorEntry<T, unknown>>();\n\n constructor(initial: T, config?: StateConfig<T>) {\n this.#current = typeof initial === 'object' && initial !== null ? ({ ...initial } as T) : initial;\n this.#equalityFn = config?.equalityFn ?? defaultEquality;\n }\n\n get current(): T {\n // Return pending state if available (for chained patches)\n return this.#pending ?? this.#current;\n }\n\n patch(partial: T extends object ? Partial<T> : T): void {\n const base = this.#pending ?? this.#current;\n\n if (typeof base !== 'object' || base === null) {\n // Primitive state: replace the whole value\n const value = partial as T;\n if (!Object.is(base, value)) {\n this.#pending = value;\n this.#scheduleFlush();\n }\n return;\n }\n\n const next = { ...base } as T;\n let changed = false;\n\n // Apply partial updates with change detection\n for (const key in partial) {\n if (!Object.hasOwn(partial as any, key)) continue;\n\n const value = (partial as Partial<T>)[key as keyof T];\n\n if (!Object.is((base as Partial<T>)[key as keyof T], value)) {\n (next as Partial<T>)[key as keyof T] = value;\n changed = true;\n }\n }\n\n if (changed) {\n this.#pending = next;\n this.#scheduleFlush();\n }\n }\n\n subscribe(listener: StateListener<T>): () => void;\n subscribe<R>(selector: StateSelector<T, R>, listener: SelectorListener<R>, options?: SelectorOptions<R>): () => void;\n subscribe<R>(\n selectorOrListener: StateListener<T> | StateSelector<T, R>,\n maybeListener?: SelectorListener<R>,\n options?: SelectorOptions<R>\n ): () => void {\n // Overload 1: Full-state subscription\n if (maybeListener === undefined) {\n const listener = selectorOrListener as StateListener<T>;\n this.#listeners.add(listener);\n\n // Fire immediately with current state\n listener(this.current);\n\n return () => {\n this.#listeners.delete(listener);\n };\n }\n\n // Overload 2: Selector subscription\n const selector = selectorOrListener as StateSelector<T, R>;\n const listener = maybeListener;\n const opts = options ?? {};\n\n const entry: SelectorEntry<T, unknown> = {\n selector: selector as StateSelector<T, unknown>,\n listener: listener as SelectorListener<unknown>,\n options: opts as SelectorOptions<unknown>,\n };\n\n this.#selectorListeners.add(entry);\n\n // Fire immediately with current selected value\n const selected = selector(this.current);\n listener(selected);\n\n return () => {\n this.#selectorListeners.delete(entry);\n };\n }\n\n flush(): void {\n if (this.#pending === null) return;\n\n const prev = this.#current;\n const next = this.#pending;\n this.#pending = null;\n this.#pendingFlush = false;\n\n // Check if state actually changed using custom equality\n if (this.#equalityFn(prev, next)) return;\n\n this.#current = next;\n\n // Notify full-state listeners\n for (const listener of this.#listeners) {\n listener(this.#current);\n }\n\n // Notify selector listeners\n for (const entry of this.#selectorListeners) {\n const prevSelected = entry.selector(prev);\n const nextSelected = entry.selector(this.#current);\n\n const equalityFn = entry.options.equalityFn ?? Object.is;\n\n if (!equalityFn(prevSelected, nextSelected)) {\n entry.listener(nextSelected);\n }\n }\n }\n\n #scheduleFlush(): void {\n if (this.#pendingFlush) return;\n this.#pendingFlush = true;\n queueMicrotask(() => this.flush());\n }\n}\n\n// =============================================================================\n// Factory function\n// =============================================================================\n\n/**\n * Create a reactive state container.\n *\n * @example\n * ```typescript\n * const state = createState({ count: 0 });\n *\n * // Subscribe to changes\n * state.subscribe((current, prev) => {\n * console.log('Changed:', prev, '->', current);\n * });\n *\n * // Updates are batched\n * state.patch({ count: 1 });\n * state.patch({ count: 2 });\n * // Only one notification fires (with count: 2)\n * ```\n *\n * @example Selector subscriptions\n * ```typescript\n * const state = createState({ count: 0, name: 'test' });\n *\n * // Only notified when count changes\n * state.subscribe(\n * s => s.count,\n * (current, prev) => console.log(current, prev)\n * );\n * ```\n *\n * @example Custom equality\n * ```typescript\n * const state = createState(\n * { count: 0, name: 'test' },\n * { equalityFn: (a, b) => a.count === b.count }\n * );\n * ```\n */\nexport function createState<T>(initial: T, config?: StateConfig<T>): WritableState<T> {\n return new StateContainer(initial, config);\n}\n\n// =============================================================================\n// Type guard\n// =============================================================================\n\n/**\n * Check if a value is a State container.\n */\nexport function isState(value: unknown): value is State<object> {\n return typeof value === 'object' && value !== null && STATE_SYMBOL in value;\n}\n","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","/**\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","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","/**\n * Generate unique ID for HAM objects.\n *\n * Uses timestamp + random number for sufficient uniqueness.\n * IDs are strings without decimals.\n *\n * @returns Unique string ID in format: timestamp-random\n *\n * @example\n * ```ts\n * const id = generateId(); // \"1738423156789-542891\"\n * ```\n */\nexport function generateId(): string {\n return `${Date.now()}-${Math.floor(Math.random() * 1000000)}`;\n}\n","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","/**\n * Exponentially Weighted Moving Average (EWMA)\n *\n * Pure functional implementation of EWMA calculations.\n * Based on Shaka Player's EWMA algorithm.\n */\n\n/**\n * Calculate alpha (decay factor) from half-life.\n *\n * Alpha determines how quickly old data \"expires\":\n * - alpha close to 1 = slow decay (long memory)\n * - alpha close to 0 = fast decay (short memory)\n *\n * @param halfLife - The quantity of prior samples (by weight) that make up\n * half of the new estimate. Must be positive.\n * @returns Alpha value between 0 and 1\n *\n * @example\n * const alpha = calculateAlpha(2); // ≈ 0.7071 for 2-second half-life\n */\nexport function calculateAlpha(halfLife: number): number {\n return Math.exp(Math.log(0.5) / halfLife);\n}\n\n/**\n * Calculate exponentially weighted moving average.\n *\n * Updates an estimate by blending a new value with the previous estimate,\n * weighted by the sample duration. Longer samples have more influence.\n *\n * @param prevEstimate - Previous EWMA estimate\n * @param value - New sample value to incorporate\n * @param weight - Sample weight (typically duration in seconds)\n * @param halfLife - Half-life for decay (typically 2-5 seconds)\n * @returns Updated EWMA estimate\n *\n * @example\n * let estimate = 0;\n * estimate = calculateEwma(estimate, 1_000_000, 1, 2); // First sample\n * estimate = calculateEwma(estimate, 2_000_000, 1, 2); // Second sample\n */\nexport function calculateEwma(prevEstimate: number, value: number, weight: number, halfLife: number): number {\n const alpha = calculateAlpha(halfLife);\n const adjAlpha = alpha ** weight;\n return value * (1 - adjAlpha) + adjAlpha * prevEstimate;\n}\n\n/**\n * Apply zero-factor correction to EWMA estimate.\n *\n * The zero-factor correction compensates for bias when starting from zero.\n * Without this correction, early estimates would be artificially low.\n *\n * As totalWeight increases, the correction factor approaches 1, meaning\n * the estimate becomes more reliable and needs less correction.\n *\n * @param estimate - Raw EWMA estimate (uncorrected)\n * @param totalWeight - Accumulated weight from all samples\n * @param halfLife - Half-life used in EWMA calculation\n * @returns Corrected estimate, or 0 if totalWeight is 0\n *\n * @example\n * const raw = calculateEwma(0, 1_000_000, 1, 2);\n * const corrected = applyZeroFactor(raw, 1, 2); // ≈ 1_000_000\n */\nexport function applyZeroFactor(estimate: number, totalWeight: number, halfLife: number): number {\n if (totalWeight === 0) {\n return 0;\n }\n\n const alpha = calculateAlpha(halfLife);\n const zeroFactor = 1 - alpha ** totalWeight;\n return estimate / zeroFactor;\n}\n","/**\n * Dual EWMA Bandwidth Estimator\n *\n * Estimates available bandwidth using two EWMA calculations with different\n * half-lives, taking the minimum of both. This approach (from Shaka Player):\n *\n * - **Fast EWMA** (2s half-life): Reacts quickly to bandwidth drops\n * - **Slow EWMA** (5s half-life): Provides stability during fluctuations\n * - **min(fast, slow)**: Adapts down quickly, up slowly\n *\n * This naturally provides asymmetric behavior needed for good QoE:\n * avoiding stalls (quick downgrade) while preventing oscillation (slow upgrade).\n */\n\nimport { applyZeroFactor, calculateEwma } from './ewma';\n\n/**\n * Bandwidth estimator state.\n *\n * This state structure will be managed by O1 (State Container).\n * Functions in this module operate on this state immutably.\n */\nexport interface BandwidthState {\n /** Fast-moving EWMA estimate (raw, uncorrected). */\n fastEstimate: number;\n /** Total weight accumulated in fast EWMA. */\n fastTotalWeight: number;\n /** Slow-moving EWMA estimate (raw, uncorrected). */\n slowEstimate: number;\n /** Total weight accumulated in slow EWMA. */\n slowTotalWeight: number;\n /** Total bytes sampled across all valid samples. */\n bytesSampled: number;\n}\n\n/**\n * Configuration for bandwidth estimation.\n */\nexport interface BandwidthConfig {\n /** Half-life for fast EWMA in seconds. */\n fastHalfLife: number;\n /** Half-life for slow EWMA in seconds. */\n slowHalfLife: number;\n /** Minimum total bytes before trusting the estimate. */\n minTotalBytes: number;\n /** Minimum bytes per sample to count (filters TTFB-dominated samples). */\n minBytes: number;\n /** Minimum sample duration in ms (filters cached responses). */\n minDuration: number;\n}\n\n/**\n * Default bandwidth estimator configuration.\n *\n * Values match Shaka Player defaults based on experimentation.\n */\nexport const DEFAULT_BANDWIDTH_CONFIG: BandwidthConfig = {\n fastHalfLife: 2, // 2 seconds\n slowHalfLife: 5, // 5 seconds\n minTotalBytes: 128_000, // 128 KB\n minBytes: 16_000, // 16 KB\n minDuration: 5, // 5 ms\n};\n\n/**\n * Add a bandwidth sample from a segment download.\n *\n * Samples are filtered based on:\n * - Minimum bytes (filters TTFB-dominated small segments)\n * - Minimum duration (filters cached responses)\n *\n * Valid samples update both fast and slow EWMA estimates.\n *\n * @param state - Current estimator state\n * @param durationMs - Download duration in milliseconds\n * @param numBytes - Number of bytes downloaded\n * @param config - Optional estimator configuration (uses defaults if not provided)\n * @returns New estimator state with sample incorporated (or unchanged if filtered)\n *\n * @example\n * let state = { fastEstimate: 0, fastTotalWeight: 0, ... };\n * // Sample: 1MB in 1 second\n * state = sampleBandwidth(state, 1000, 1_000_000);\n */\nexport function sampleBandwidth(\n state: BandwidthState,\n durationMs: number,\n numBytes: number,\n config: BandwidthConfig = DEFAULT_BANDWIDTH_CONFIG\n): BandwidthState {\n // Always track bytes for startup phase calculation\n const updatedBytesSampled = state.bytesSampled + numBytes;\n\n // Filter: Ignore samples below minimum bytes (TTFB-dominated)\n // Small segments' download time is mostly connection setup latency,\n // which would artificially lower our bandwidth estimate\n if (numBytes < config.minBytes) {\n return { ...state, bytesSampled: updatedBytesSampled };\n }\n\n // Filter: Ignore samples faster than minimum duration (cached responses)\n // Cached responses load nearly instantly and would artificially inflate\n // our bandwidth estimate\n if (durationMs < config.minDuration) {\n return { ...state, bytesSampled: updatedBytesSampled };\n }\n\n // Calculate bandwidth in bits per second\n // Formula: (bytes * 8 bits/byte * 1000 ms/s) / ms = bits/s\n const bandwidth = (8000 * numBytes) / durationMs;\n\n // Weight by duration in seconds\n // Longer downloads are more reliable indicators of true bandwidth\n const weight = durationMs / 1000;\n\n return {\n fastEstimate: calculateEwma(state.fastEstimate, bandwidth, weight, config.fastHalfLife),\n fastTotalWeight: state.fastTotalWeight + weight,\n slowEstimate: calculateEwma(state.slowEstimate, bandwidth, weight, config.slowHalfLife),\n slowTotalWeight: state.slowTotalWeight + weight,\n bytesSampled: updatedBytesSampled,\n };\n}\n\n/**\n * Get the current bandwidth estimate.\n *\n * Returns the **minimum** of the fast and slow EWMA estimates.\n * This provides the key asymmetric behavior:\n * - When bandwidth drops, fast EWMA reacts first and dominates (quick adaptation)\n * - When bandwidth rises, slow EWMA lags behind and dominates (slow adaptation)\n *\n * Uses default estimate until enough data has been sampled.\n *\n * @param state - Current estimator state\n * @param defaultEstimate - Fallback estimate before sufficient samples (bps)\n * @param config - Optional estimator configuration (uses defaults if not provided)\n * @returns Bandwidth estimate in bits per second\n *\n * @example\n * const estimate = getBandwidthEstimate(state, 5_000_000); // 5 Mbps default\n */\nexport function getBandwidthEstimate(\n state: BandwidthState,\n defaultEstimate: number,\n config: BandwidthConfig = DEFAULT_BANDWIDTH_CONFIG\n): number {\n // Use default until we have enough samples to trust our estimate\n if (state.bytesSampled < config.minTotalBytes) {\n return defaultEstimate;\n }\n\n // Apply zero-factor correction to both estimates\n const fastEstimate = applyZeroFactor(state.fastEstimate, state.fastTotalWeight, config.fastHalfLife);\n\n const slowEstimate = applyZeroFactor(state.slowEstimate, state.slowTotalWeight, config.slowHalfLife);\n\n // Take the minimum - this is the key insight from Shaka Player\n // It naturally provides \"down quickly, up slowly\" behavior\n return Math.min(fastEstimate, slowEstimate);\n}\n\n/**\n * Check if the estimator has enough data to provide a reliable estimate.\n *\n * Requires both:\n * - Enough total bytes sampled (minTotalBytes threshold)\n * - At least one valid EWMA sample (totalWeight > 0)\n *\n * @param state - Current estimator state\n * @param config - Optional estimator configuration (uses defaults if not provided)\n * @returns True if we've sampled enough bytes to trust the estimate\n *\n * @example\n * if (hasGoodEstimate(state)) {\n * const estimate = getBandwidthEstimate(state, 5_000_000);\n * }\n */\nexport function hasGoodEstimate(state: BandwidthState, config: BandwidthConfig = DEFAULT_BANDWIDTH_CONFIG): boolean {\n // Need enough total bytes AND at least one valid EWMA sample\n return state.bytesSampled >= config.minTotalBytes && state.fastTotalWeight > 0 && state.slowTotalWeight > 0;\n}\n","/**\n * Quality Selection Algorithm\n *\n * Selects optimal video track based on bandwidth estimate with safety margin.\n * Stateless selection - picks highest quality that fits bandwidth.\n *\n * Key concepts:\n * - **Safety margin** (0.85): Pick track where bandwidth >= track.bandwidth / 0.85\n * - This ensures 15% headroom to avoid buffering\n * - At same bandwidth, prefer higher resolution\n */\n\nimport type { PartiallyResolvedVideoTrack } from '../types';\n\n/**\n * Quality selection configuration.\n */\nexport interface QualityConfig {\n /**\n * Safety margin (0-1).\n * To select a track, need: currentBandwidth >= track.bandwidth / safetyMargin.\n * Default 0.85 means track must use ≤85% of available bandwidth (15% headroom).\n */\n safetyMargin: number;\n}\n\n/**\n * Default quality selection configuration.\n * Values match Shaka Player upgrade threshold (0.85 = 15% headroom).\n */\nexport const DEFAULT_QUALITY_CONFIG: QualityConfig = {\n safetyMargin: 0.85,\n};\n\n/**\n * Select the best video track based on current bandwidth estimate.\n *\n * Selects the highest quality track where bandwidth is sufficient with safety margin:\n * - currentBandwidth >= track.bandwidth / safetyMargin\n * - Default safetyMargin 0.85 means track uses ≤85% of bandwidth (15% headroom)\n * - At same bandwidth, prefers higher resolution\n *\n * @param tracks - Available video tracks (can be unsorted)\n * @param currentBandwidth - Current bandwidth estimate in bits per second\n * @param config - Optional quality selection configuration\n * @returns Selected track, or undefined if no tracks available\n *\n * @example\n * const tracks = [\n * { id: '360p', bandwidth: 500_000, ... },\n * { id: '720p', bandwidth: 2_000_000, ... },\n * { id: '1080p', bandwidth: 4_000_000, ... },\n * ];\n *\n * // With 2.5 Mbps, selects 720p (1080p needs 4M/0.85 = 4.7 Mbps)\n * const selected = selectQuality(tracks, 2_500_000);\n */\nexport function selectQuality(\n tracks: PartiallyResolvedVideoTrack[],\n currentBandwidth: number,\n config: QualityConfig = DEFAULT_QUALITY_CONFIG\n): PartiallyResolvedVideoTrack | undefined {\n if (tracks.length === 0) {\n return undefined;\n }\n\n // Sort tracks by bandwidth (lowest first)\n const sortedTracks = tracks.slice().sort((a, b) => a.bandwidth - b.bandwidth);\n\n // Start with no selection\n let chosen: PartiallyResolvedVideoTrack | undefined;\n\n for (const track of sortedTracks) {\n // Check if we have enough bandwidth for this track with safety margin\n // Required bandwidth = track.bandwidth / safetyMargin\n const requiredBandwidth = track.bandwidth / config.safetyMargin;\n\n if (currentBandwidth >= requiredBandwidth) {\n // We can support this track - prefer it if better than current choice\n if (\n !chosen ||\n track.bandwidth > chosen.bandwidth ||\n (track.bandwidth === chosen.bandwidth && hasHigherResolution(track, chosen))\n ) {\n chosen = track;\n }\n }\n }\n\n // If no track fits with safety margin, fall back to lowest quality\n return chosen ?? sortedTracks[0];\n}\n\n/**\n * Check if track A has higher resolution than track B.\n * Compares by total pixel count (width × height).\n *\n * @param trackA - First track to compare\n * @param trackB - Second track to compare\n * @returns True if trackA has more pixels than trackB\n */\nfunction hasHigherResolution(trackA: PartiallyResolvedVideoTrack, trackB: PartiallyResolvedVideoTrack): boolean {\n const pixelsA = (trackA.width ?? 0) * (trackA.height ?? 0);\n const pixelsB = (trackB.width ?? 0) * (trackB.height ?? 0);\n return pixelsA > pixelsB;\n}\n","/**\n * Back Buffer Strategy (Simple)\n *\n * Calculates flush points for back buffer management.\n * V1 uses simple \"keep N segments\" strategy.\n */\n\nimport type { Segment } from '../types';\n\n/**\n * Back buffer configuration.\n */\nexport interface BackBufferConfig {\n /**\n * Number of segments to keep behind current playback position.\n * Default: 2 segments.\n */\n keepSegments: number;\n}\n\n/**\n * Default back buffer configuration.\n */\nexport const DEFAULT_BACK_BUFFER_CONFIG: BackBufferConfig = {\n keepSegments: 2,\n};\n\n/**\n * Calculate back buffer flush point.\n *\n * Determines where to flush old segments from the back buffer.\n * Keeps a fixed number of segments behind the current playback position.\n *\n * Algorithm:\n * 1. Find segments before currentTime\n * 2. Count back N segments (keepSegments)\n * 3. Return startTime of segment N+1 back (flush everything before this)\n *\n * @param segments - Available segments (should be sorted by startTime)\n * @param currentTime - Current playback position in seconds\n * @param config - Optional back buffer configuration\n * @returns Time in seconds to flush up to (flush range: [0, flushEnd))\n *\n * @example\n * const segments = [\n * { startTime: 0, duration: 6, ... },\n * { startTime: 6, duration: 6, ... },\n * { startTime: 12, duration: 6, ... },\n * { startTime: 18, duration: 6, ... },\n * ];\n *\n * // Playing at 18s, keep 2 segments\n * const flushEnd = calculateBackBufferFlushPoint(segments, 18);\n * // Returns 6 (flush [0, 6), keep [6-18))\n */\nexport function calculateBackBufferFlushPoint(\n segments: Segment[],\n currentTime: number,\n config: BackBufferConfig = DEFAULT_BACK_BUFFER_CONFIG\n): number {\n if (segments.length === 0) {\n return 0;\n }\n\n // Find all segments before current time (not including current segment)\n const segmentsBefore = segments.filter((seg) => seg.startTime < currentTime);\n\n // If no segments before current time, nothing to flush\n if (segmentsBefore.length === 0) {\n return 0;\n }\n\n // Calculate how many segments to flush\n // Keep last N segments, flush the rest\n const segmentsToFlush = segmentsBefore.length - config.keepSegments;\n\n // If we don't have enough segments to flush, keep everything\n if (segmentsToFlush <= 0) {\n return 0;\n }\n\n // If we want to flush all segments, return currentTime\n // (flush everything before current playback position)\n if (segmentsToFlush >= segmentsBefore.length) {\n return currentTime;\n }\n\n // Return the startTime of the first segment we want to keep\n // Everything before this will be flushed\n return segmentsBefore[segmentsToFlush]!.startTime;\n}\n","/**\n * Forward Buffer Strategy (Simple)\n *\n * Determines which segments to load for forward buffer management.\n * V1 uses simple fixed-duration strategy (buffer N seconds ahead).\n */\n\nimport type { Segment } from '../types';\n\n/**\n * Forward buffer configuration.\n */\nexport interface ForwardBufferConfig {\n /**\n * Duration in seconds to buffer ahead of current playback position.\n * Default: 30 seconds.\n */\n bufferDuration: number;\n}\n\n/**\n * Default forward buffer configuration.\n */\nexport const DEFAULT_FORWARD_BUFFER_CONFIG: ForwardBufferConfig = {\n bufferDuration: 30,\n};\n\n/**\n * Get segments that need to be loaded for forward buffer.\n *\n * Determines which segments to load to maintain target buffer duration.\n * Handles discontiguous buffering (gaps after seeks).\n *\n * Algorithm:\n * 1. Calculate target time: currentTime + bufferDuration\n * 2. Find all segments in range [currentTime, targetTime)\n * 3. Filter out segments already buffered at that time position\n * 4. Return segments to load (fills gaps + extends to target)\n *\n * @param segments - All available segments from playlist\n * @param bufferedSegments - Segments already buffered (ordered by startTime)\n * @param currentTime - Current playback position in seconds\n * @param config - Optional forward buffer configuration\n * @returns Array of segments to load (empty if buffer is sufficient)\n *\n * @example\n * // After seek: buffered [0-12, 18-30], playing at 7s\n * const toLoad = getSegmentsToLoad(segments, buffered, 7, { bufferDuration: 24 });\n * // Returns [seg-12, seg-30] (fills gap, extends to target 31s)\n */\n/**\n * Calculate the start time from which to flush forward buffer content.\n *\n * Content that starts at or beyond `currentTime + bufferDuration` is no\n * longer needed for the current playback position and should be removed\n * from the SourceBuffer. This prevents unbounded accumulation of scattered\n * SourceBuffer content after seeks, which can cause QuotaExceededError on\n * long-form content.\n *\n * Returns `Infinity` when nothing needs flushing (no buffered segments\n * exist beyond the threshold).\n *\n * @param bufferedSegments - Segments currently tracked in the buffer model\n * @param currentTime - Current playback position in seconds\n * @param config - Optional forward buffer configuration\n * @returns Start time to flush from (flush range: [flushStart, Infinity)),\n * or Infinity if no flush is needed\n *\n * @example\n * // Playing at 0s, buffered [0,6,12,18,24,30,36], bufferDuration=30\n * const flushStart = calculateForwardFlushPoint(segments, 0);\n * // Returns 30 — flush [30, Infinity), keep [0, 30)\n */\nexport function calculateForwardFlushPoint(\n bufferedSegments: readonly Segment[],\n currentTime: number,\n config: ForwardBufferConfig = DEFAULT_FORWARD_BUFFER_CONFIG\n): number {\n if (bufferedSegments.length === 0) return Infinity;\n\n const threshold = currentTime + config.bufferDuration;\n\n // Find segments that start at or beyond the threshold\n const beyond = bufferedSegments.filter((seg) => seg.startTime >= threshold);\n\n if (beyond.length === 0) return Infinity;\n\n // Flush from the earliest such segment onward\n return Math.min(...beyond.map((seg) => seg.startTime));\n}\n\nexport function getSegmentsToLoad(\n segments: readonly Segment[],\n bufferedSegments: readonly Segment[],\n currentTime: number,\n config: ForwardBufferConfig = DEFAULT_FORWARD_BUFFER_CONFIG\n): Segment[] {\n if (segments.length === 0) {\n return [];\n }\n\n // Calculate target buffer end time\n const targetTime = currentTime + config.bufferDuration;\n\n // Create set of buffered segment start times for fast lookup\n // V1 simple: if ANY segment is buffered at a given time, don't load for that time\n // V2 (future): would compare by startTime + bitrate/track for quality switching\n const bufferedStartTimes = new Set(bufferedSegments.map((seg) => seg.startTime));\n\n // Find segments to load:\n // - Overlaps buffer window [currentTime, targetTime)\n // - Not already buffered at that time position\n const toLoad = segments.filter((seg) => {\n // Segment must overlap the buffer window\n const segmentEnd = seg.startTime + seg.duration;\n const isInRange = seg.startTime < targetTime && segmentEnd > currentTime;\n\n // Must not have a segment buffered at this time position\n const isNotBuffered = !bufferedStartTimes.has(seg.startTime);\n\n return isInRange && isNotBuffered;\n });\n\n return toLoad;\n}\n","/**\n * Core SPF Types\n *\n * Based on CMAF-HAM (Common Media Application Format - Hypothetical Application Model)\n * Protocol-agnostic representation of streaming media content.\n *\n * @see https://github.com/AcademySoftwareFoundation/common-media-library\n */\n\n// =============================================================================\n// Base Types\n// =============================================================================\n\n/**\n * Base identifier type for all HAM objects.\n */\nexport interface Ham {\n id: string;\n}\n\n/**\n * Addressable resource with optional byte range.\n */\nexport interface AddressableObject {\n url: string;\n byteRange?: {\n start: number;\n end: number;\n };\n}\n\n// =============================================================================\n// Platform-agnostic Media Element\n// =============================================================================\n\n/**\n * Platform-agnostic media element interface.\n * Captures minimal shape needed for orchestration without DOM dependencies.\n * HTMLMediaElement satisfies this interface.\n */\nexport interface MediaElementLike {\n preload: string;\n}\n\n// =============================================================================\n// Time and Duration\n// =============================================================================\n\n/**\n * Time span with start time and duration.\n * Used for segments and other timed ranges.\n */\nexport interface TimeSpan {\n startTime: number;\n duration: number;\n}\n\n// =============================================================================\n// Enums\n// =============================================================================\n\n/**\n * Track content type.\n */\nexport type TrackType = 'video' | 'audio' | 'text';\n\n// =============================================================================\n// Frame Rate\n// =============================================================================\n\n/**\n * Video frame rate expressed as numerator/denominator.\n *\n * Examples:\n * - 30 fps: { frameRateNumerator: 30 }\n * - 29.97 fps: { frameRateNumerator: 30000, frameRateDenominator: 1001 }\n */\nexport interface FrameRate {\n frameRateNumerator: number;\n frameRateDenominator?: number;\n}\n\n// =============================================================================\n// Partially Resolved Tracks (before media playlist is fetched)\n// =============================================================================\n\n/**\n * Generic type for partially resolved tracks.\n * Removes fields that come from media playlist parsing.\n *\n * @param T - Track type to make partially resolved (must extend Track)\n */\nexport type PartiallyResolved<T extends Track = Track> = Omit<T, 'segments' | 'initialization' | keyof TimeSpan> & {\n segments?: never;\n duration?: never;\n startTime?: never;\n initialization?: never;\n};\n\n/**\n * Partially resolved video track from multivariant playlist.\n * Has metadata but no segments or initialization yet (media playlist not fetched).\n */\nexport type PartiallyResolvedVideoTrack = PartiallyResolved<VideoTrack>;\n\n/**\n * Partially resolved audio track from multivariant playlist.\n * Has metadata but no segments or initialization yet (media playlist not fetched).\n */\nexport type PartiallyResolvedAudioTrack = PartiallyResolved<AudioTrack>;\n\n// =============================================================================\n// Resolved Track Types (with segments from media playlist)\n// =============================================================================\n\n/**\n * Base track type containing common properties for all resolved tracks.\n * A resolved track has segments, duration, and initialization data.\n * All URLs are fully qualified (parsers resolve relative URLs).\n */\n/**\n * Track startTime is always 0 (for future multi-period support).\n */\nexport type Track = Ham &\n AddressableObject &\n TimeSpan & {\n type: TrackType;\n codecs?: string[]; // Optional per HLS spec\n mimeType: string;\n language?: string | undefined;\n bandwidth: number;\n initialization?: AddressableObject;\n segments: Segment[];\n };\n\n/**\n * Resolved video track with segments.\n */\nexport type VideoTrack = Track &\n Required<Pick<Track, 'initialization' | 'codecs'>> & {\n type: 'video';\n\n // Optional metadata from multivariant (per HLS spec)\n width?: number;\n height?: number;\n frameRate?: FrameRate;\n audioGroupId?: string;\n };\n\n/**\n * Resolved audio track with segments.\n */\nexport type AudioTrack = Track &\n Required<Pick<Track, 'initialization' | 'codecs'>> & {\n type: 'audio';\n groupId: string;\n name: string;\n sampleRate: number;\n channels: number;\n default?: boolean;\n autoselect?: boolean;\n };\n\n/**\n * Resolved text track with segments.\n */\nexport type TextTrack = Track & {\n type: 'text';\n groupId: string;\n label: string;\n kind: 'subtitles' | 'captions';\n default?: boolean;\n autoselect?: boolean;\n forced?: boolean;\n};\n\n/**\n * Partially resolved text track from multivariant playlist.\n * Has metadata but no segments or initialization yet (media playlist not fetched).\n */\nexport type PartiallyResolvedTextTrack = PartiallyResolved<TextTrack>;\n\n/**\n * Unresolved text track (alias for backwards compatibility).\n\n/**\n * Union of all resolved track types.\n */\nexport type ResolvedTrack = VideoTrack | AudioTrack | TextTrack;\n\n/**\n * Union of all partially resolved track types.\n */\nexport type PartiallyResolvedTrack =\n | PartiallyResolvedVideoTrack\n | PartiallyResolvedAudioTrack\n | PartiallyResolvedTextTrack;\n\n/**\n * Union of all unresolved track types (alias for backwards compatibility).\n\n// =============================================================================\n// Switching and Selection Sets\n// =============================================================================\n\n/**\n * Generic switching set type.\n * A group of tracks that can be switched between seamlessly.\n *\n * @param T - Track type (VideoTrack, AudioTrack, or TextTrack)\n */\nexport type SwitchingSetOf<T extends Track = Track> = Ham & {\n type: T['type'];\n tracks: (PartiallyResolved<T> | T)[];\n};\n\n/**\n * Video switching set - contains only video tracks (partially resolved or fully resolved).\n */\nexport type VideoSwitchingSet = SwitchingSetOf<VideoTrack>;\n\n/**\n * Audio switching set - contains only audio tracks (partially resolved or fully resolved).\n */\nexport type AudioSwitchingSet = SwitchingSetOf<AudioTrack>;\n\n/**\n * Text switching set - contains only text tracks (partially resolved or fully resolved).\n */\nexport type TextSwitchingSet = SwitchingSetOf<TextTrack>;\n\n/**\n * Switching set - a group of tracks that can be switched between seamlessly.\n * Discriminated by track type.\n */\nexport type SwitchingSet = VideoSwitchingSet | AudioSwitchingSet | TextSwitchingSet;\n\n/**\n * Generic selection set type.\n * Groups switching sets by track type.\n *\n * @param T - Track type (VideoTrack, AudioTrack, or TextTrack)\n */\nexport type SelectionSetOf<T extends Track = Track> = Ham & {\n type: T['type'];\n switchingSets: SwitchingSetOf<T>[];\n};\n\n/**\n * Video selection set - contains only video switching sets.\n */\nexport type VideoSelectionSet = SelectionSetOf<VideoTrack>;\n\n/**\n * Audio selection set - contains only audio switching sets.\n */\nexport type AudioSelectionSet = SelectionSetOf<AudioTrack>;\n\n/**\n * Text selection set - contains only text switching sets.\n */\nexport type TextSelectionSet = SelectionSetOf<TextTrack>;\n\n/**\n * Selection set - groups switching sets by track type.\n * Discriminated union ensures type-safe track access.\n */\nexport type SelectionSet = VideoSelectionSet | AudioSelectionSet | TextSelectionSet;\n\n// =============================================================================\n// Segment\n// =============================================================================\n\n/**\n * Media segment with timing information.\n * Follows CMAF-HAM composition pattern.\n */\nexport type Segment = Ham & AddressableObject & TimeSpan;\n\n// =============================================================================\n// Media Playlist Info\n// =============================================================================\n\n/**\n * Intermediate representation of a parsed media playlist.\n * Used internally before assembling into full Track structure.\n */\nexport interface MediaPlaylistInfo {\n version: number;\n targetDuration: number;\n playlistType: 'VOD' | 'EVENT' | undefined;\n initSegment: AddressableObject | null;\n segments: Segment[];\n duration: number;\n endList: boolean;\n}\n\n// =============================================================================\n// Presentation\n// =============================================================================\n\n/**\n * Presentation - a single playable period of content.\n * Uses TimeSpan fields (startTime always 0, duration optional until track resolved).\n *\n * Extends AddressableObject so `url` contains the original manifest URL.\n * All URLs are fully qualified (parsers resolve relative URLs).\n */\nexport type Presentation = Ham &\n AddressableObject &\n Partial<TimeSpan> & {\n selectionSets: SelectionSet[];\n };\n\n// =============================================================================\n// Type Guards\n// =============================================================================\n\n/**\n * Check if a track is resolved (has segments).\n * Works for all track types with overloaded signatures for type narrowing.\n */\nexport function isResolvedTrack(track: PartiallyResolvedVideoTrack | VideoTrack): track is VideoTrack;\nexport function isResolvedTrack(track: PartiallyResolvedAudioTrack | AudioTrack): track is AudioTrack;\nexport function isResolvedTrack(track: PartiallyResolvedTextTrack | TextTrack): track is TextTrack;\nexport function isResolvedTrack(track: PartiallyResolvedTrack | ResolvedTrack): track is ResolvedTrack;\nexport function isResolvedTrack(track: PartiallyResolvedTrack | ResolvedTrack): track is ResolvedTrack {\n return 'segments' in track;\n}\n\n/**\n * Check if a presentation has duration (at least one track resolved).\n * Narrows type to include required duration.\n */\nexport function hasPresentationDuration(\n presentation: Presentation\n): presentation is Presentation & { duration: number } {\n return presentation.duration !== undefined;\n}\n","/**\n * MediaSource Setup\n *\n * Utilities for creating and configuring MediaSource/ManagedMediaSource\n * for MSE (Media Source Extensions) playback.\n *\n * Global ManagedMediaSource types are defined in ./mediasource.d.ts\n */\n\n/**\n * Check if MediaSource API is supported.\n */\nexport function supportsMediaSource(): boolean {\n return typeof MediaSource !== 'undefined';\n}\n\n/**\n * Check if ManagedMediaSource API is supported.\n * ManagedMediaSource is a newer Safari API with better lifecycle management.\n */\nexport function supportsManagedMediaSource(): boolean {\n return typeof ManagedMediaSource !== 'undefined';\n}\n\n/**\n * Options for creating a MediaSource.\n */\nexport interface CreateMediaSourceOptions {\n /** Prefer ManagedMediaSource when available (default: false for broader compatibility). */\n preferManaged?: boolean;\n}\n\n/**\n * Create a MediaSource or ManagedMediaSource instance.\n *\n * @param options - Creation options\n * @returns A MediaSource or ManagedMediaSource instance\n * @throws Error if no MediaSource API is available\n *\n * @example\n * const mediaSource = createMediaSource();\n * const mediaElement = document.querySelector('video');\n * attachMediaSource(mediaSource, mediaElement);\n */\nexport function createMediaSource(options: CreateMediaSourceOptions = {}): MediaSource {\n const { preferManaged = false } = options;\n\n if (preferManaged && supportsManagedMediaSource()) {\n return new ManagedMediaSource!();\n }\n\n if (supportsMediaSource()) {\n return new MediaSource();\n }\n\n throw new Error('MediaSource API is not supported');\n}\n\n/**\n * Result of attaching a MediaSource to a media element.\n */\nexport interface AttachMediaSourceResult {\n /** The object URL created for the MediaSource (empty string for ManagedMediaSource). */\n url: string;\n /** Detach the MediaSource and clean up resources. */\n detach: () => void;\n}\n\n/**\n * Attach a MediaSource to an HTMLMediaElement.\n *\n * Uses srcObject for ManagedMediaSource (Safari), or createObjectURL for regular MediaSource.\n *\n * @param mediaSource - The MediaSource to attach\n * @param mediaElement - The media element to attach to\n * @returns Object with URL and detach function\n *\n * @example\n * const mediaSource = createMediaSource();\n * const { detach } = attachMediaSource(mediaSource, videoElement);\n * await waitForSourceOpen(mediaSource);\n * // Use mediaSource...\n * // Later, to clean up:\n * detach();\n */\nexport function attachMediaSource(mediaSource: MediaSource, mediaElement: HTMLMediaElement): AttachMediaSourceResult {\n // ManagedMediaSource requires srcObject instead of createObjectURL\n const isManagedMediaSource = supportsManagedMediaSource() && mediaSource instanceof ManagedMediaSource!;\n\n if (isManagedMediaSource) {\n // ManagedMediaSource requires disableRemotePlayback — without it Safari\n // will not fire sourceopen.\n (mediaElement as HTMLMediaElement & { disableRemotePlayback: boolean }).disableRemotePlayback = true;\n\n // Use srcObject for ManagedMediaSource\n (mediaElement as HTMLMediaElement & { srcObject: MediaSource | null }).srcObject = mediaSource;\n\n const detach = (): void => {\n (mediaElement as HTMLMediaElement & { srcObject: MediaSource | null }).srcObject = null;\n mediaElement.load(); // Reset the element\n };\n\n return { url: '', detach };\n }\n\n // Use createObjectURL for regular MediaSource\n const url = URL.createObjectURL(mediaSource);\n mediaElement.src = url;\n\n const detach = (): void => {\n mediaElement.removeAttribute('src');\n mediaElement.load(); // Reset the element\n URL.revokeObjectURL(url);\n };\n\n return { url, detach };\n}\n\n/**\n * Wait for a MediaSource to reach the 'open' state.\n * Resolves immediately if already open.\n *\n * @param mediaSource - The MediaSource to wait for\n * @param signal - Optional AbortSignal for cancellation\n * @returns Promise that resolves when the MediaSource is open\n *\n * @example\n * const mediaSource = createMediaSource();\n * attachMediaSource(mediaSource, videoElement);\n * await waitForSourceOpen(mediaSource);\n * // MediaSource is now ready for SourceBuffer creation\n */\nexport function waitForSourceOpen(mediaSource: MediaSource, signal?: AbortSignal): Promise<void> {\n return new Promise((resolve, reject) => {\n if (mediaSource.readyState === 'open') {\n resolve();\n return;\n }\n\n // Check if already aborted\n if (signal?.aborted) {\n reject(new DOMException('Aborted', 'AbortError'));\n return;\n }\n\n // Use internal AbortController for cleanup coordination\n const controller = new AbortController();\n const options = { signal: controller.signal };\n\n mediaSource.addEventListener(\n 'sourceopen',\n () => {\n controller.abort();\n resolve();\n },\n options\n );\n\n signal?.addEventListener(\n 'abort',\n () => {\n controller.abort();\n reject(new DOMException('Aborted', 'AbortError'));\n },\n options\n );\n });\n}\n\n/**\n * Create a SourceBuffer on a MediaSource.\n *\n * @param mediaSource - The MediaSource (must be in 'open' state)\n * @param mimeCodec - MIME type with codecs (e.g., 'video/mp4; codecs=\"avc1.42E01E\"')\n * @returns The created SourceBuffer\n * @throws Error if MediaSource is not open or codec is unsupported\n *\n * @example\n * await waitForSourceOpen(mediaSource);\n * const buffer = createSourceBuffer(mediaSource, 'video/mp4; codecs=\"avc1.42E01E\"');\n */\nexport function createSourceBuffer(mediaSource: MediaSource, mimeCodec: string): SourceBuffer {\n if (mediaSource.readyState !== 'open') {\n throw new Error('MediaSource is not open');\n }\n\n if (!isCodecSupported(mimeCodec)) {\n throw new Error(`Codec not supported: ${mimeCodec}`);\n }\n\n return mediaSource.addSourceBuffer(mimeCodec);\n}\n\n/**\n * Check if a codec is supported.\n *\n * @param mimeCodec - MIME type with codecs string\n * @returns True if the codec is supported\n *\n * @example\n * if (isCodecSupported('video/mp4; codecs=\"avc1.42E01E\"')) {\n * // Create source buffer\n * }\n */\nexport function isCodecSupported(mimeCodec: string): boolean {\n if (!supportsMediaSource()) {\n return false;\n }\n\n return MediaSource.isTypeSupported(mimeCodec);\n}\n","/**\n * HTTP Fetch Wrapper\n *\n * Two-function approach for composability:\n * 1. fetchResolvable() - Fetch AddressableObject (handles byte ranges)\n * 2. getResponseText() - Extract text from Response\n */\n\nimport type { AddressableObject } from '../../core/types';\n\n/**\n * Minimal Response-like interface for text extraction.\n * Allows testing without full Response object.\n */\nexport interface ResponseLike {\n text(): Promise<string>;\n}\n\n/**\n * Fetch resolvable from AddressableObject.\n *\n * Handles byte range requests if byteRange is present.\n * Returns native fetch Response for composability (can extract text, stream, etc.).\n *\n * @param addressable - Resource to fetch (url + optional byteRange)\n * @returns Promise resolving to Response\n *\n * @example\n * const response = await fetchResolvable({ url: 'https://example.com/segment.m4s' });\n * const text = await getResponseText(response);\n *\n * @example\n * // With byte range\n * const response = await fetchResolvable({\n * url: 'https://example.com/file.mp4',\n * byteRange: { start: 1000, end: 1999 }\n * });\n */\nexport async function fetchResolvable(addressable: AddressableObject, options?: RequestInit): Promise<Response> {\n const headers = new Headers(options?.headers);\n\n // Add Range header for byte range requests\n if (addressable.byteRange) {\n const { start, end } = addressable.byteRange;\n headers.set('Range', `bytes=${start}-${end}`);\n }\n\n const request = new Request(addressable.url, {\n method: 'GET',\n headers,\n ...options,\n });\n\n return fetch(request);\n}\n\n/**\n * Fetch resolvable as bytes.\n *\n * Convenience wrapper around fetchResolvable that resolves the body as an\n * ArrayBuffer. Use this when you need the raw bytes (e.g. segment appends).\n * For text or streaming consumption, use fetchResolvable directly.\n */\nexport async function fetchResolvableBytes(\n addressable: AddressableObject,\n options?: RequestInit\n): Promise<ArrayBuffer> {\n const response = await fetchResolvable(addressable, options);\n return response.arrayBuffer();\n}\n\n/**\n * Extract text from Response.\n *\n * Accepts minimal Response-like object (just needs text() method).\n * Returns promise from response.text().\n *\n * @param response - Response-like object with text() method\n * @returns Promise resolving to text content\n *\n * @example\n * const response = await fetchResolvable(addressable);\n * const text = await getResponseText(response);\n */\nexport function getResponseText(response: ResponseLike): Promise<string> {\n return response.text();\n}\n","/**\n * Minimal event stream with Observable-like shape.\n *\n * Simple Subject/Observable-like implementation for dispatching discrete events.\n * Events are dispatched synchronously to all subscribers.\n */\n\nconst EVENT_STREAM_SYMBOL = Symbol('@videojs/event-stream');\n\n/**\n * Event listener function.\n */\nexport type EventListener<T extends Pick<Event, 'type'>> = (event: T) => void;\n\n/**\n * Event stream interface.\n */\nexport interface EventStream<T extends Pick<Event, 'type'> = { type: string }> {\n [EVENT_STREAM_SYMBOL]: true;\n dispatch(event: T): void;\n subscribe(listener: EventListener<T>): () => void;\n}\n\n/**\n * Creates a minimal event stream for dispatching discrete events.\n *\n * Events are dispatched synchronously to all subscribers.\n * Conforms to Observable-like shape for future compatibility.\n *\n * Events must have a 'type' property for discriminated union type narrowing.\n *\n * @example\n * ```ts\n * type Action = { type: 'PLAY' } | { type: 'PAUSE' };\n * const events = createEventStream<Action>();\n *\n * events.subscribe((action) => {\n * if (action.type === 'PLAY') {\n * // Type narrowed to { type: 'PLAY' }\n * }\n * });\n *\n * events.dispatch({ type: 'PLAY' });\n * ```\n */\nexport function createEventStream<T extends Pick<Event, 'type'>>(): EventStream<T> {\n const subscribers = new Set<EventListener<T>>();\n\n return {\n [EVENT_STREAM_SYMBOL]: true,\n\n dispatch(event: T): void {\n // Snapshot subscribers to avoid notifying those added during dispatch\n const current = Array.from(subscribers);\n for (const listener of current) {\n listener(event);\n }\n },\n\n subscribe(listener: EventListener<T>): () => void {\n subscribers.add(listener);\n return () => subscribers.delete(listener);\n },\n };\n}\n\n/**\n * Type guard to check if value is an EventStream.\n */\nexport function isEventStream(value: unknown): value is EventStream {\n return typeof value === 'object' && value !== null && EVENT_STREAM_SYMBOL in value;\n}\n","/**\n * Minimal combineLatest operator for reactive composition.\n *\n * Combines multiple Observable-like sources and emits an array of their latest values\n * whenever any source emits.\n */\n\n/**\n * Observable-like interface.\n * Both State and EventStream conform to this shape.\n */\nexport interface Observable<T> {\n subscribe(listener: (value: T) => void): () => void;\n}\n\n/**\n * Infer value types from array of Observables.\n */\nexport type InferObservableValues<T extends readonly Observable<unknown>[]> = {\n [K in keyof T]: T[K] extends Observable<infer V> ? V : never;\n};\n\n/**\n * Options for selector-based subscriptions on a combined observable.\n */\nexport interface SelectorOptions<R> {\n equalityFn?: (a: R, b: R) => boolean;\n}\n\n/**\n * Observable returned by combineLatest — supports both plain and\n * selector-based subscriptions.\n */\nexport interface CombinedObservable<T> extends Observable<T> {\n subscribe(listener: (value: T) => void): () => void;\n subscribe<R>(selector: (value: T) => R, listener: (value: R) => void, options?: SelectorOptions<R>): () => void;\n}\n\n/**\n * Combines multiple Observable sources into a single Observable.\n *\n * Emits an array of latest values whenever any source emits.\n * Only emits after all sources have emitted at least once.\n *\n * Supports selector-based subscriptions (fires only when the selected value\n * changes, per the optional equalityFn) mirroring the createState API.\n *\n * @param sources - Array of Observable sources\n * @returns Combined Observable\n *\n * @example\n * ```ts\n * const state = createState({ count: 0 });\n * const events = createEventStream<Action>();\n *\n * combineLatest([state, events]).subscribe(([state, event]) => {\n * if (event.type === 'PLAY' && state.count > 0) {\n * // React to event + state condition\n * }\n * });\n * ```\n *\n * @example Selector subscription\n * ```ts\n * combineLatest([state, owners]).subscribe(\n * ([s, o]) => deriveKey(s, o),\n * (key) => { ... },\n * { equalityFn: keyEq }\n * );\n * ```\n */\nexport function combineLatest<T extends readonly Observable<unknown>[]>(\n sources: [...T]\n): CombinedObservable<InferObservableValues<T>> {\n type Values = InferObservableValues<T>;\n\n const subscribeToSources = (listener: (values: Values) => void): (() => void) => {\n const latest: unknown[] = new Array(sources.length);\n const hasValue: boolean[] = new Array(sources.length).fill(false);\n const unsubscribers: Array<() => void> = [];\n\n for (let i = 0; i < sources.length; i++) {\n const unsubscribe = sources[i]!.subscribe((value) => {\n latest[i] = value;\n hasValue[i] = true;\n if (hasValue.every((has) => has)) {\n listener([...latest] as Values);\n }\n });\n unsubscribers.push(unsubscribe);\n }\n\n return () => {\n for (const unsubscribe of unsubscribers) {\n unsubscribe();\n }\n };\n };\n\n return {\n subscribe<R>(\n listenerOrSelector: ((values: Values) => void) | ((values: Values) => R),\n maybeListener?: (value: R) => void,\n options?: SelectorOptions<R>\n ): () => void {\n if (maybeListener === undefined) {\n return subscribeToSources(listenerOrSelector as (values: Values) => void);\n }\n\n const selector = listenerOrSelector as (values: Values) => R;\n const listener = maybeListener;\n const equalityFn = options?.equalityFn ?? Object.is;\n\n let prevSelected: R | undefined;\n let initialized = false;\n\n return subscribeToSources((values) => {\n const nextSelected = selector(values);\n if (!initialized || !equalityFn(prevSelected as R, nextSelected)) {\n prevSelected = nextSelected;\n initialized = true;\n listener(nextSelected);\n }\n });\n },\n };\n}\n","import { fetchResolvable, getResponseText } from '../../dom/network/fetch';\nimport type { EventStream } from '../events/create-event-stream';\nimport { parseMultivariantPlaylist } from '../hls/parse-multivariant';\nimport { combineLatest } from '../reactive/combine-latest';\nimport type { WritableState } from '../state/create-state';\nimport type { AddressableObject, MediaElementLike, 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}\n\n/**\n * Mutable platform objects.\n */\nexport interface PlatformOwners {\n mediaElement?: MediaElementLike | undefined;\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 event.\n *\n * Resolution conditions:\n * - State-driven: preload is 'auto' or 'metadata'\n * - Event-driven: play event\n *\n * @param state - Current presentation state\n * @param event - Current action/event\n * @returns true if resolution conditions are met\n */\nexport function shouldResolve(state: PresentationState, event: PresentationAction): boolean {\n const { preload } = state;\n return (\n // State-driven: preload allows (auto/metadata)\n ['auto', 'metadata'].includes(preload as any) ||\n // Event-driven: play event\n event.type === 'play'\n );\n}\n\n/**\n * Syncs preload attribute from mediaElement to state.\n *\n * Watches the owners state for mediaElement changes and copies the\n * preload attribute to the immutable state.\n *\n * @param state - Immutable state container\n * @param owners - Mutable platform objects container\n * @returns Cleanup function to stop syncing\n */\nexport function syncPreloadAttribute(\n state: WritableState<PresentationState>,\n owners: WritableState<PlatformOwners>\n): () => void {\n return owners.subscribe((current) => {\n const preload = current.mediaElement?.preload || undefined;\n state.patch({ preload: preload as 'auto' | 'metadata' | 'none' | undefined });\n });\n}\n\n/**\n * Action types for presentation resolution.\n * Event names match HTMLMediaElement events (lowercase).\n */\nexport type PresentationAction = { type: 'play' } | { type: 'pause' } | { type: 'load'; url: string };\n\n/**\n * Resolves unresolved presentations using reactive composition.\n *\n * Uses combineLatest to compose state + events, enabling both state-driven\n * and event-driven resolution triggers.\n *\n * Triggers resolution when:\n * - State-driven: Unresolved presentation + preload allows (auto/metadata)\n * - Event-driven: PLAY event when preload=\"none\"\n *\n * @example\n * ```ts\n * const state = createState({ presentation: undefined, preload: 'auto' });\n * const events = createEventStream<PresentationAction>();\n *\n * const cleanup = resolvePresentation({ state, events });\n *\n * // State-driven: resolves immediately when preload allows\n * state.patch({ presentation: { url: 'http://example.com/playlist.m3u8' } });\n *\n * // Event-driven: resolves on PLAY when preload=\"none\"\n * state.patch({ preload: 'none', presentation: { url: '...' } });\n * events.dispatch({ type: 'PLAY' });\n * ```\n */\nexport function resolvePresentation({\n state,\n events,\n}: {\n state: WritableState<PresentationState>;\n events: EventStream<PresentationAction>;\n}): () => void {\n // This is effectively a very simple finite state model. We can formalize this if needed.\n let resolving = false;\n let abortController: AbortController | null = null;\n\n const cleanup = combineLatest([state, events]).subscribe(async ([currentState, event]) => {\n if (!canResolve(currentState) || !shouldResolve(currentState, event) || resolving) return;\n\n try {\n // This along with the resolving finite state (or more complex) could be pulled into its own abstraction.\n // Set flag before async work\n resolving = true;\n abortController = new AbortController();\n\n const { presentation } = currentState;\n // Fetch and parse playlist\n const response = await fetchResolvable(presentation, { signal: abortController.signal });\n const text = await getResponseText(response);\n const parsed = parseMultivariantPlaylist(text, presentation);\n\n // Update state with resolved presentation\n state.patch({\n presentation: parsed,\n });\n } catch (error) {\n // Ignore AbortError - this is expected when cleanup happens\n if (error instanceof Error && error.name === 'AbortError') {\n return;\n }\n throw error;\n } finally {\n // Always clear flag\n resolving = false;\n abortController = null;\n }\n });\n\n // Return cleanup function that aborts pending fetches\n return () => {\n abortController?.abort();\n cleanup();\n };\n}\n","import type { BandwidthState } from '../abr/bandwidth-estimator';\nimport { getBandwidthEstimate } from '../abr/bandwidth-estimator';\nimport { selectQuality } from '../abr/quality-selection';\nimport type { WritableState } from '../state/create-state';\nimport type { Presentation, VideoSelectionSet } from '../types';\n\n/**\n * State shape for quality switching.\n */\nexport interface QualitySwitchingState {\n presentation?: Presentation;\n bandwidthState?: BandwidthState;\n selectedVideoTrackId?: string;\n // TODO: abrDisabled is a blunt instrument — it requires callers to know they're\n // competing with ABR and explicitly opt out. A better long-term design would\n // separate ABR selection (abrVideoTrackId) from manual selection (manualVideoTrackId)\n // and derive the effective selectedVideoTrackId as manualVideoTrackId ?? abrVideoTrackId.\n // That way the two concerns never write to the same field and consumers are unchanged.\n /** When true, ABR quality switching is suppressed. Use for manual quality selection. */\n abrDisabled?: boolean;\n}\n\n/**\n * Configuration for quality switching behavior.\n */\nexport interface QualitySwitchingConfig {\n /**\n * Safety margin for quality selection (0–1).\n * Track is selected only when bandwidth >= track.bandwidth / safetyMargin.\n * Default: 0.85 (15% headroom).\n */\n safetyMargin?: number;\n\n /**\n * Minimum milliseconds between upgrades.\n * Prevents oscillation when bandwidth fluctuates around a quality threshold.\n * Downgrades are always immediate regardless of this setting.\n * Default: 8000 (8 seconds).\n */\n minUpgradeInterval?: number;\n\n /**\n * Bandwidth estimate in bps to use before enough samples have been collected.\n * Default: 5_000_000 (5 Mbps).\n */\n defaultBandwidth?: number;\n}\n\n/**\n * Default quality switching configuration.\n */\nexport const DEFAULT_SWITCHING_CONFIG: Required<QualitySwitchingConfig> = {\n safetyMargin: 0.85,\n minUpgradeInterval: 8000,\n defaultBandwidth: 5_000_000,\n};\n\n/**\n * Get all video tracks from a presentation's first switching set.\n * Returns [] when the presentation is still unresolved (no selectionSets yet).\n */\nfunction getVideoTracks(presentation: Presentation) {\n const videoSet = presentation.selectionSets?.find((s) => s.type === 'video') as VideoSelectionSet | undefined;\n return videoSet?.switchingSets[0]?.tracks ?? [];\n}\n\n/**\n * Quality switching orchestration (F9).\n *\n * Reacts to bandwidth estimate changes and updates `selectedVideoTrackId`\n * when a different quality is optimal:\n *\n * - **Downgrades** happen immediately to avoid buffering stalls.\n * - **Upgrades** are gated by `minUpgradeInterval` to prevent oscillation.\n * - The first switch (from any track, or no track) is always immediate.\n *\n * Smooth switching is handled downstream: when `selectedVideoTrackId` changes,\n * `resolveTrack` fetches the new playlist and `loadSegments` reloads the init\n * segment, then appends media segments from the current position in the new\n * quality. The browser's SourceBuffer replaces the overlapping buffered range.\n *\n * @example\n * const cleanup = switchQuality({ state });\n * // Later, when done:\n * cleanup();\n */\nexport function switchQuality(\n { state }: { state: WritableState<QualitySwitchingState> },\n config: QualitySwitchingConfig = {}\n): () => void {\n const safetyMargin = config.safetyMargin ?? DEFAULT_SWITCHING_CONFIG.safetyMargin;\n const minUpgradeInterval = config.minUpgradeInterval ?? DEFAULT_SWITCHING_CONFIG.minUpgradeInterval;\n const defaultBandwidth = config.defaultBandwidth ?? DEFAULT_SWITCHING_CONFIG.defaultBandwidth;\n\n // Initialize to creation time so the interval starts counting immediately.\n // The first time we have enough data to make a meaningful quality decision\n // (presentation resolved + bandwidth available), the upgrade gate is skipped\n // so the initial ABR correction does not wait for minUpgradeInterval.\n let lastUpgradeTime = Date.now();\n let firstMeaningfulFire = true;\n\n return state.subscribe((currentState: QualitySwitchingState) => {\n const { presentation, bandwidthState, selectedVideoTrackId, abrDisabled } = currentState;\n\n if (abrDisabled === true) return;\n if (!presentation || !bandwidthState) return;\n\n const videoTracks = getVideoTracks(presentation);\n if (videoTracks.length === 0) return;\n\n // Consume the first-meaningful-fire flag now that we have all required data.\n const isFirst = firstMeaningfulFire;\n firstMeaningfulFire = false;\n\n const bandwidth = getBandwidthEstimate(bandwidthState, defaultBandwidth);\n const optimal = selectQuality(videoTracks as any, bandwidth, { safetyMargin });\n if (!optimal || optimal.id === selectedVideoTrackId) return;\n\n // Determine whether this is an upgrade or downgrade.\n const currentTrack = videoTracks.find((t) => t.id === selectedVideoTrackId);\n const isUpgrade = !currentTrack || optimal.bandwidth > currentTrack.bandwidth;\n\n if (isUpgrade) {\n const now = Date.now();\n // Gate upgrades with minUpgradeInterval to prevent oscillation.\n // Downgrades are always immediate; the first meaningful evaluation is always allowed.\n if (!isFirst && now - lastUpgradeTime < minUpgradeInterval) return;\n lastUpgradeTime = now;\n }\n\n state.patch({ selectedVideoTrackId: optimal.id });\n });\n}\n","import type {\n AddressableObject,\n AudioTrack,\n PartiallyResolvedAudioTrack,\n PartiallyResolvedTextTrack,\n PartiallyResolvedVideoTrack,\n Presentation,\n TextTrack,\n TrackType,\n VideoTrack,\n} from '../types';\n\n/**\n * State shape for track selection.\n * Minimal shape containing presentation and selected track IDs.\n */\nexport interface TrackSelectionState {\n presentation?: AddressableObject | Presentation | undefined;\n selectedVideoTrackId?: string | undefined;\n selectedAudioTrackId?: string | undefined;\n selectedTextTrackId?: string | undefined;\n}\n\n/**\n * Map track type to selected track ID property key in state.\n */\nexport const SelectedTrackIdKeyByType = {\n video: 'selectedVideoTrackId',\n audio: 'selectedAudioTrackId',\n text: 'selectedTextTrackId',\n} as const;\n\n/**\n * Map track type to buffer owner property key.\n * Used for SourceBuffer references in owners.\n */\nexport const BufferKeyByType = {\n video: 'videoBuffer',\n audio: 'audioBuffer',\n} as const;\n\n/**\n * Get selected track from state by type.\n * Returns properly typed track (partially or fully resolved) or undefined.\n * Type parameter T is inferred from the type argument.\n *\n * @example\n * const videoTrack = getSelectedTrack(state, 'video');\n * if (videoTrack && isResolvedTrack(videoTrack)) {\n * // videoTrack is VideoTrack\n * }\n */\nexport function getSelectedTrack<T extends TrackType>(\n state: TrackSelectionState,\n type: T\n): T extends 'video'\n ? PartiallyResolvedVideoTrack | VideoTrack | undefined\n : T extends 'audio'\n ? PartiallyResolvedAudioTrack | AudioTrack | undefined\n : T extends 'text'\n ? PartiallyResolvedTextTrack | TextTrack | undefined\n : never {\n const { presentation } = state;\n\n /** @TODO Consider moving and reusing isUnresolved(presentation) (CJP) */\n if (!presentation || !('id' in presentation)) return undefined as any;\n\n // Get track ID based on type\n const trackIdKey = SelectedTrackIdKeyByType[type];\n const trackId = state[trackIdKey];\n return presentation.selectionSets\n .find(({ type: selectionSetType }) => selectionSetType === type)\n ?.switchingSets[0]?.tracks.find(({ id }) => id === trackId) as any;\n}\n","import { calculateBackBufferFlushPoint } from '../../core/buffer/back-buffer';\nimport { calculateForwardFlushPoint, getSegmentsToLoad } from '../../core/buffer/forward-buffer';\nimport type { AddressableObject, AudioTrack, Segment, VideoTrack } from '../../core/types';\nimport type {\n AppendInitMessage,\n AppendSegmentMessage,\n RemoveMessage,\n SourceBufferActor,\n} from '../media/source-buffer-actor';\n\n// ============================================================================\n// BUFFER STATE TYPES\n// ============================================================================\n\n/**\n * Buffer state for a single SourceBuffer.\n * Tracks which init segment and media segments are loaded.\n */\nexport interface SourceBufferState {\n /** Track ID of the loaded init segment */\n initTrackId?: string;\n /** Loaded media segments (unordered - selectors derive ordering) */\n segments: Array<{ id: string; trackId: string }>;\n}\n\n/**\n * Buffer state for all SourceBuffers.\n */\nexport interface BufferState {\n video?: SourceBufferState;\n audio?: SourceBufferState;\n}\n\n// ============================================================================\n// MESSAGE PROTOCOL\n// ============================================================================\n\n/** Track types that have SourceBuffers (video and audio only). */\nexport type SegmentLoaderTrack = VideoTrack | AudioTrack;\n\n/**\n * Message sent to a SegmentLoaderActor.\n *\n * `range` is optional to distinguish loading modes:\n * - No range: load init segment only (metadata preload mode)\n * - With range: load init + all segments overlapping [start, end]\n *\n * `start` and `end` are raw time values — no segment snapping.\n * The actor maps them onto segment boundaries internally.\n */\nexport type SegmentLoaderMessage = {\n type: 'load';\n track: SegmentLoaderTrack;\n range?: { start: number; end: number };\n};\n\n// ============================================================================\n// LOAD TASK\n// ============================================================================\n\n/**\n * A LoadTask is the intent to perform one unit of SegmentLoader work.\n * Unlike SourceBufferMessage, fetch-based tasks carry a URL rather than\n * pre-fetched data — the runner fetches and appends them in sequence.\n *\n * Derived from SourceBufferMessage types by removing `data` and adding\n * a fetch URL via AddressableObject.\n *\n * @todo Rename — \"LoadTask\" risks confusion with the `Task` class used for\n * SourceBufferActor scheduling. These are closer to operation descriptors or\n * messages than tasks in that sense.\n */\nexport type LoadTask =\n | (Omit<AppendInitMessage, 'data'> & AddressableObject)\n | (Omit<AppendSegmentMessage, 'data'> & AddressableObject)\n | RemoveMessage;\n\n// ============================================================================\n// ACTOR INTERFACE\n// ============================================================================\n\nexport interface SegmentLoaderActor {\n send(message: SegmentLoaderMessage): void;\n destroy(): void;\n}\n\n// ============================================================================\n// IMPLEMENTATION\n// ============================================================================\n\n/**\n * Creates a SegmentLoaderActor for one track type (video or audio).\n *\n * Receives load assignments via `send()` and owns all execution: planning,\n * removes, fetches, and appends. Coordinates with the SourceBufferActor for\n * all physical SourceBuffer operations.\n *\n * Planning (Cases 1–3) happens in `send()` on every incoming message, producing\n * an ordered LoadTask list. The runner drains that list sequentially. When a new\n * message arrives mid-run, send() replans and either continues the in-flight\n * operation (if still needed) or preempts it.\n *\n * @param sourceBufferActor - Shared SourceBufferActor reference (not owned)\n * @param fetchBytes - Tracked fetch closure (owns throughput sampling)\n */\nexport function createSegmentLoaderActor(\n sourceBufferActor: SourceBufferActor,\n fetchBytes: (addressable: AddressableObject, options?: RequestInit) => Promise<ArrayBuffer>\n): SegmentLoaderActor {\n let pendingTasks: LoadTask[] | null = null;\n let inFlightInitTrackId: string | null = null;\n let inFlightSegmentId: string | null = null;\n let abortController: AbortController | null = null;\n let running = false;\n let destroyed = false;\n\n const getBufferedSegments = (allSegments: readonly Segment[]): Segment[] => {\n const bufferedIds = new Set(sourceBufferActor.snapshot.context.segments.map((s) => s.id));\n return allSegments.filter((s) => bufferedIds.has(s.id));\n };\n\n /**\n * Translate a load message into an ordered LoadTask list based on committed\n * actor state. In-flight awareness is handled separately in send().\n *\n * @todo Rename alongside LoadTask (e.g. planOps).\n *\n * Case 1 — Removes: forward and back buffer flush points, segment-aligned.\n * No flush on track switch: appending new content overwrites existing buffer\n * ranges, and the actor's time-aligned deduplication keeps the segment model\n * accurate as new segments arrive.\n *\n * Case 2 — Init: schedule if not yet committed for this track.\n *\n * Case 3 — Segments: all segments in the load window not yet committed.\n */\n const planTasks = (message: SegmentLoaderMessage): LoadTask[] => {\n const { track, range } = message;\n const actorCtx = sourceBufferActor.snapshot.context;\n const bufferedSegments = getBufferedSegments(track.segments);\n const currentTime = range?.start ?? 0;\n const tasks: LoadTask[] = [];\n\n // Case 1: Removes\n if (range) {\n const forwardFlushStart = calculateForwardFlushPoint(bufferedSegments, currentTime);\n if (forwardFlushStart < Infinity) {\n tasks.push({ type: 'remove', start: forwardFlushStart, end: Infinity });\n }\n const backFlushEnd = calculateBackBufferFlushPoint(bufferedSegments, currentTime);\n if (backFlushEnd > 0) {\n tasks.push({ type: 'remove', start: 0, end: backFlushEnd });\n }\n }\n\n // Case 2: Init\n if (actorCtx.initTrackId !== track.id) {\n tasks.push({ type: 'append-init', meta: { trackId: track.id }, url: track.initialization.url });\n }\n\n // Case 3: Segments\n if (range) {\n const EPSILON = 0.0001;\n const segmentsToLoad = getSegmentsToLoad(track.segments, bufferedSegments, currentTime).filter((seg) => {\n // Quality-aware filter: skip segments already covered by equal-or-higher-quality\n // content in the actor context. Preserves buffered high-quality content during\n // ABR downgrades; loads during upgrades and for uncovered positions.\n const existing = actorCtx.segments.find((s) => Math.abs(s.startTime - seg.startTime) < EPSILON);\n if (!existing?.trackBandwidth || !track.bandwidth) return true;\n return track.bandwidth > existing.trackBandwidth;\n });\n for (const segment of segmentsToLoad) {\n tasks.push({\n type: 'append-segment',\n meta: {\n id: segment.id,\n startTime: segment.startTime,\n duration: segment.duration,\n trackId: track.id,\n trackBandwidth: track.bandwidth,\n },\n url: segment.url,\n });\n }\n }\n\n return tasks;\n };\n\n /**\n * Execute a single LoadTask: fetch (if needed) then forward to SourceBufferActor.\n * Sets/clears in-flight tracking around async operations so send() can make\n * accurate continue/preempt decisions at any point during execution.\n *\n * @todo Rename alongside LoadTask (e.g. executeOp).\n */\n const executeLoadTask = async (task: LoadTask): Promise<void> => {\n const signal = abortController!.signal;\n try {\n if (task.type === 'remove') {\n await sourceBufferActor.send(task, signal);\n return;\n }\n\n if (task.type === 'append-init') {\n inFlightInitTrackId = task.meta.trackId;\n if (!signal.aborted) {\n const data = await fetchBytes(task, { signal });\n // For seeks on the same track: commit even if aborted — avoids re-fetching the\n // same init next time. For track switches: don't commit the old track's init;\n // the new track's init follows in pendingTasks.\n const isTrackSwitch = pendingTasks?.some(\n (t) => t.type === 'append-init' && t.meta.trackId !== task.meta.trackId\n );\n if (!signal.aborted || !isTrackSwitch) {\n const appendSignal = signal.aborted ? new AbortController().signal : signal;\n await sourceBufferActor.send({ type: 'append-init', data, meta: task.meta }, appendSignal);\n }\n }\n return;\n }\n\n // append-segment\n inFlightSegmentId = task.meta.id;\n if (!signal.aborted) {\n const data = await fetchBytes(task, { signal });\n if (!signal.aborted) {\n await sourceBufferActor.send({ type: 'append-segment', data, meta: task.meta }, signal);\n }\n }\n } finally {\n inFlightInitTrackId = null;\n inFlightSegmentId = null;\n }\n };\n\n /**\n * Drain the scheduled task list sequentially.\n * After each task completes, checks for a pending replacement plan from send().\n * If the signal was aborted and no new plan arrived, stops immediately.\n */\n const runScheduled = async (initialTasks: LoadTask[]): Promise<void> => {\n running = true;\n abortController = new AbortController();\n let scheduled = initialTasks;\n\n while (scheduled.length > 0 && !destroyed) {\n const task = scheduled[0]!;\n scheduled = scheduled.slice(1);\n\n try {\n await executeLoadTask(task);\n } catch (error) {\n if (error instanceof Error && error.name === 'AbortError') {\n // Abort is handled in the post-task check below.\n } else {\n console.error('Unexpected error in segment loader:', error);\n // Non-abort error (e.g. network failure on init): don't continue with\n // remaining tasks in this sequence — they depend on the failed step.\n // A pending replacement plan (if any) will still be picked up below.\n scheduled = [];\n }\n }\n\n if (pendingTasks !== null) {\n // A new plan arrived while this task was running — switch to it.\n scheduled = pendingTasks;\n pendingTasks = null;\n abortController = new AbortController();\n } else if (abortController.signal.aborted) {\n // Aborted with no replacement plan — stop.\n break;\n }\n }\n\n abortController = null;\n running = false;\n };\n\n return {\n send(message: SegmentLoaderMessage) {\n if (destroyed) return;\n\n const allTasks = planTasks(message);\n\n if (!running) {\n if (allTasks.length === 0) return;\n runScheduled(allTasks);\n return;\n }\n\n // Determine whether the in-flight operation is still needed for the new plan.\n const inFlightStillNeeded =\n (inFlightSegmentId !== null &&\n allTasks.some((t) => t.type === 'append-segment' && t.meta.id === inFlightSegmentId)) ||\n (inFlightInitTrackId !== null &&\n allTasks.some((t) => t.type === 'append-init' && t.meta.trackId === inFlightInitTrackId));\n\n if (inFlightStillNeeded) {\n // Continue: the in-flight operation covers something the new plan needs.\n // Queue everything except the in-flight item — it will complete on its own.\n pendingTasks = allTasks.filter(\n (t) =>\n !(t.type === 'append-segment' && t.meta.id === inFlightSegmentId) &&\n !(t.type === 'append-init' && t.meta.trackId === inFlightInitTrackId)\n );\n } else {\n // Preempt: in-flight work is not needed for the new plan. Abort and replan.\n pendingTasks = allTasks;\n abortController?.abort();\n }\n },\n\n destroy() {\n destroyed = true;\n abortController?.abort();\n },\n };\n}\n","import { combineLatest } from '../../all';\nimport { type BandwidthState, sampleBandwidth } from '../../core/abr/bandwidth-estimator';\nimport { DEFAULT_FORWARD_BUFFER_CONFIG } from '../../core/buffer/forward-buffer';\nimport { createState, type WritableState } from '../../core/state/create-state';\nimport type { AddressableObject, Presentation, ResolvedTrack } from '../../core/types';\nimport { isResolvedTrack } from '../../core/types';\nimport { getSelectedTrack, type TrackSelectionState } from '../../core/utils/track-selection';\nimport type { SourceBufferActor } from '../media/source-buffer-actor';\nimport { fetchResolvableBytes } from '../network/fetch';\nimport {\n type BufferState,\n createSegmentLoaderActor,\n type SegmentLoaderActor,\n type SourceBufferState,\n} from './segment-loader-actor';\nimport type { MediaTrackType } from './setup-sourcebuffer';\n\n// Re-export buffer state types for consumers that import them from this module.\nexport type { BufferState, SourceBufferState };\n\n// Map track type to SourceBufferActor owner property key.\nconst ActorKeyByType = {\n video: 'videoBufferActor',\n audio: 'audioBufferActor',\n} as const;\n\n// ============================================================================\n// TRACKED FETCH\n// ============================================================================\n\n/**\n * Creates a fetch function that transparently samples bandwidth after each\n * completed request. Callers receive bytes; throughput tracking is invisible.\n *\n * `onSample` is an optional callback invoked after each sample is recorded,\n * used for bridging throughput state outward (e.g. migration bridge to global\n * state). A callback is used rather than a subscription so that no immediate\n * fire occurs at setup time — subscriptions fire on registration and would\n * trigger spurious state changes before any work has started.\n */\nfunction createTrackedFetch(\n throughput: WritableState<BandwidthState>,\n onSample?: (next: BandwidthState) => void\n): (addressable: AddressableObject, options?: RequestInit) => Promise<ArrayBuffer> {\n return async (addressable, options) => {\n const start = performance.now();\n const data = await fetchResolvableBytes(addressable, options);\n const elapsed = performance.now() - start;\n const next = sampleBandwidth(throughput.current, elapsed, data.byteLength);\n throughput.patch(next);\n throughput.flush();\n onSample?.(next);\n return data;\n };\n}\n\n// ============================================================================\n// STATE & OWNERS\n// ============================================================================\n\n/**\n * State shape for segment loading.\n */\nexport interface SegmentLoadingState extends TrackSelectionState {\n presentation?: Presentation;\n preload?: string;\n bandwidthState?: BandwidthState;\n /** Current playback position in seconds. Defaults to 0 when undefined. */\n currentTime?: number;\n /** True once the user has initiated playback. Allows segment loading regardless of preload setting. */\n playbackInitiated?: boolean;\n}\n\n/**\n * Owners shape for segment loading.\n */\nexport interface SegmentLoadingOwners {\n videoBuffer?: SourceBuffer;\n audioBuffer?: SourceBuffer;\n videoBufferActor?: SourceBufferActor;\n audioBufferActor?: SourceBufferActor;\n}\n\n// ============================================================================\n// STATE SELECTOR\n// ============================================================================\n\n/**\n * The subset of state that drives segment loading decisions.\n * A plain \"pick\" with one derived field (resolved track ID).\n */\ntype LoadingInputs = {\n playbackInitiated: boolean | undefined;\n preload: string | undefined;\n currentTime: number | undefined;\n /** @TODO cleanup type precision via inference+generics */\n track: ReturnType<typeof getSelectedTrack>;\n segmentsCanLoad: boolean;\n};\n\nfunction selectLoadingInputs(\n [segmentsCanLoad, state]: [boolean, SegmentLoadingState],\n type: MediaTrackType\n): LoadingInputs {\n const { playbackInitiated, preload, currentTime } = state;\n const track = getSelectedTrack(state, type);\n return {\n playbackInitiated,\n preload,\n currentTime,\n track,\n segmentsCanLoad,\n };\n}\n\n/**\n * Equality function encoding the condition hierarchy for relevant changes.\n *\n * Pre-play (!playbackInitiated):\n * Only preload changes matter. currentTime and resolvedTrackId are ignored\n * (track changes not supported pre-play; currentTime value is used at\n * trigger time but changes don't re-trigger).\n *\n * playbackInitiated transition:\n * Always fires (handled in the subscriber; preload='auto' suppression\n * applied there since equality functions have no memory of prior values).\n *\n * Post-play (playbackInitiated):\n * resolvedTrackId changes (track switch or previously-unresolved track\n * resolving) and currentTime changes both trigger. preload is irrelevant.\n */\nconst segmentStartFor = (currentTime: number | undefined, track: ResolvedTrack | undefined) => {\n if (currentTime == null) return undefined;\n return track?.segments.find(\n ({ startTime, duration }, i, segments) =>\n currentTime >= startTime && (currentTime < startTime + duration || i === segments.length - 1)\n )?.startTime;\n};\n\n/**\n * Returns true when the inputs are equal (no meaningful change — don't fire).\n * Returns false when the inputs differ in a way that requires a new message.\n *\n * This IS the shouldLoadSegments logic, expressed as an equality function.\n */\nfunction loadingInputsEq(prevState: LoadingInputs, curState: LoadingInputs): boolean {\n if (!curState.segmentsCanLoad) return true;\n // Haven't started playback. Only care about preload changes.\n if (!curState.playbackInitiated) {\n if (curState.preload === 'none') return true; // blocked — equal, don't fire\n return curState.preload === prevState.preload; // equal if preload unchanged\n }\n // Transition: !playbackInitiated → playbackInitiated\n if (!prevState.playbackInitiated && curState.playbackInitiated) {\n if (prevState.preload !== 'auto') return false; // fire — message shape changes\n // preload was 'auto': fall through to segment comparison (suppress if same position)\n }\n\n // Don't treat as a change if we don't (yet) have a currently resolved track.\n if (!curState.track || !isResolvedTrack(curState.track)) return true;\n\n // If we *do* have a currently resolved track, treat as a change if the track ids have changed\n if (prevState.track?.id !== curState.track.id && isResolvedTrack(curState.track)) return false;\n\n // Finally, if playback has initiated *and* we have a resolved track, check if currentTime\n // has changed \"significantly\" (based on segment time range boundaries) and treat as a change\n // if so (regardless of whether or not the track has changed)\n return (\n segmentStartFor(prevState.currentTime, curState.track as ResolvedTrack) ===\n segmentStartFor(curState.currentTime, curState.track as ResolvedTrack)\n );\n}\n\n// ============================================================================\n// REACTOR\n// ============================================================================\n\n/**\n * Load segments orchestration — Reactor layer.\n *\n * Sends typed load messages to a SegmentLoaderActor when relevant conditions\n * change. Uses targeted subscriptions rather than broad combineLatest so only\n * meaningful state changes trigger evaluation.\n *\n * Condition hierarchy (see SegmentLoadingKey for detail):\n *\n * !playbackInitiated\n * preload==='none' (or unset) → dormant; no trigger\n * preload==='metadata' → trigger on transition to 'metadata'\n * preload==='auto' → trigger on transition to 'auto'\n *\n * !playbackInitiated → playbackInitiated\n * preload !== 'auto' → trigger (message shape changes)\n * preload === 'auto' → suppressed (was already full-range mode;\n * let segmentStart take over post-play)\n * KNOWN LIMITATION: seek-before-play with\n * preload='auto' is not supported — if the\n * user seeks before pressing play, the\n * first re-send is delayed until the next\n * segment boundary crossing post-play.\n *\n * playbackInitiated\n * resolvedTrackId changes → trigger\n * segmentStart(currentTime) changes → trigger (segment boundary only)\n *\n * @example\n * const cleanup = loadSegments({ state, owners }, { type: 'video' });\n */\nexport function loadSegments(\n {\n state,\n owners,\n }: {\n state: WritableState<SegmentLoadingState>;\n owners: WritableState<SegmentLoadingOwners>;\n },\n config: { type: MediaTrackType }\n): () => void {\n const { type } = config;\n const actorKey = ActorKeyByType[type];\n\n // Local throughput state — owns BandwidthState for this track's fetch loop.\n // Sampling is handled transparently inside fetchBytes; callers never touch it.\n //\n // MIGRATION BRIDGE: the onSample callback below keeps global state.bandwidthState\n // in sync so ABR (selectVideoTrack) continues to work unchanged. Remove this\n // bridge once ABR reads from throughput directly.\n const initialBandwidth = state.current.bandwidthState;\n const throughput = createState<BandwidthState>(\n initialBandwidth ?? {\n fastEstimate: 0,\n fastTotalWeight: 0,\n slowEstimate: 0,\n slowTotalWeight: 0,\n bytesSampled: 0,\n }\n );\n\n const fetchBytes =\n type === 'video'\n ? createTrackedFetch(\n throughput,\n initialBandwidth !== undefined\n ? (next) => {\n state.patch({ bandwidthState: next });\n // Flush immediately so switchQuality sees the new estimate before the\n // next segment fetch starts, rather than waiting for the microtask queue.\n state.flush();\n }\n : undefined\n )\n : fetchResolvableBytes;\n\n const segmentLoader = createState<SegmentLoaderActor | undefined>(undefined);\n\n // SegmentLoaderActor lifecycle — watch only the actor key in owners so this\n // subscription does not fire on unrelated owners changes.\n const unsubActorLifecycle = owners.subscribe(\n (o) => o[actorKey],\n (actor) => {\n if (actor) {\n segmentLoader.patch(createSegmentLoaderActor(actor, fetchBytes));\n } else if (!actor && segmentLoader.current) {\n segmentLoader.current.destroy();\n segmentLoader.patch(undefined);\n }\n return () => {\n segmentLoader.current?.destroy();\n segmentLoader.patch(undefined);\n };\n }\n );\n\n const segmentsCanLoad = createState<boolean>(false);\n const unsubscribeCanLoadSegments = combineLatest([state, segmentLoader]).subscribe(\n ([currentState, currentSegmentLoader]) => {\n const track = getSelectedTrack(currentState, type);\n const trackResolved = !!track && isResolvedTrack(track);\n const segmentLoaderActorExists = !!currentSegmentLoader;\n segmentsCanLoad.patch(trackResolved && segmentLoaderActorExists);\n }\n );\n\n const unsubscribeShouldLoadSegments = combineLatest([segmentsCanLoad, state]).subscribe(\n ([segmentsCanLoad, state]) => selectLoadingInputs([segmentsCanLoad, state], type),\n ({ preload, playbackInitiated, currentTime, track }) => {\n const fullMode = preload === 'auto' || !!playbackInitiated;\n\n if (!fullMode) {\n // Metadata mode: init only, no range.\n /** @ts-expect-error */\n segmentLoader.current?.send({ type: 'load', track });\n } else {\n segmentLoader.current?.send({\n type: 'load',\n /** @ts-expect-error */\n track,\n range: {\n start: currentTime as number,\n end: (currentTime as number) + DEFAULT_FORWARD_BUFFER_CONFIG.bufferDuration,\n },\n });\n }\n },\n { equalityFn: loadingInputsEq }\n );\n\n return () => {\n unsubscribeCanLoadSegments();\n unsubscribeShouldLoadSegments();\n unsubActorLifecycle();\n };\n}\n","/**\n * Parse a VTT segment using browser's native parser.\n *\n * Creates a dummy video element with a track element to leverage\n * the browser's optimized VTT parsing. Returns parsed VTTCue objects.\n */\n\n// Singleton dummy video (reused across all parsing)\nlet dummyVideo: HTMLVideoElement | null = null;\n\nfunction ensureDummyVideo(): HTMLVideoElement {\n if (!dummyVideo) {\n dummyVideo = document.createElement('video');\n dummyVideo.muted = true;\n dummyVideo.preload = 'none';\n dummyVideo.style.display = 'none';\n dummyVideo.crossOrigin = 'anonymous';\n }\n return dummyVideo;\n}\n\nexport function parseVttSegment(url: string): Promise<VTTCue[]> {\n const video = ensureDummyVideo();\n const track = document.createElement('track');\n track.kind = 'subtitles';\n track.default = true;\n\n return new Promise((resolve, reject) => {\n const onLoad = (): void => {\n const cues: VTTCue[] = [];\n const textTrack = track.track;\n\n if (textTrack.cues) {\n for (let i = 0; i < textTrack.cues.length; i++) {\n const cue = textTrack.cues[i];\n if (cue) {\n cues.push(cue as VTTCue);\n }\n }\n }\n\n cleanup();\n resolve(cues);\n };\n\n const onError = (): void => {\n cleanup();\n reject(new Error(`Failed to load VTT segment: ${url}`));\n };\n\n const cleanup = (): void => {\n track.removeEventListener('load', onLoad);\n track.removeEventListener('error', onError);\n video.removeChild(track);\n };\n\n track.addEventListener('load', onLoad);\n track.addEventListener('error', onError);\n video.appendChild(track);\n track.src = url;\n });\n}\n\nexport function destroyVttParser(): void {\n dummyVideo = null;\n}\n","import { getSegmentsToLoad } from '../../core/buffer/forward-buffer';\nimport { combineLatest } from '../../core/reactive/combine-latest';\nimport type { WritableState } from '../../core/state/create-state';\nimport type { Presentation, Segment, TextTrack } from '../../core/types';\nimport { isResolvedTrack } from '../../core/types';\nimport { parseVttSegment } from '../text/parse-vtt-segment';\n\nfunction isDuplicateCue(cue: VTTCue, textTrack: globalThis.TextTrack): boolean {\n const { cues } = textTrack;\n if (!cues) return false;\n for (let i = 0; i < cues.length; i++) {\n const existing = cues[i] as VTTCue;\n if (existing.startTime === cue.startTime && existing.endTime === cue.endTime && existing.text === cue.text) {\n return true;\n }\n }\n return false;\n}\n\nconst loadVttSegmentTask = async (\n { segment }: { segment: Segment },\n context: { textTrack: globalThis.TextTrack }\n): Promise<void> => {\n const cues = await parseVttSegment(segment.url);\n for (const cue of cues) {\n if (!isDuplicateCue(cue, context.textTrack)) {\n context.textTrack.addCue(cue);\n }\n }\n};\n\n// ============================================================================\n// MAIN TASK (composite - orchestrates subtasks)\n// ============================================================================\n\n/**\n * Load text track cues task (composite - orchestrates VTT segment subtasks).\n */\nconst loadTextTrackCuesTask = async (\n { currentState }: { currentState: TextTrackCueLoadingState },\n context: {\n signal: AbortSignal;\n textTrack: globalThis.TextTrack;\n state: WritableState<TextTrackCueLoadingState>;\n }\n): Promise<void> => {\n const track = findSelectedTextTrack(currentState);\n if (!track || !isResolvedTrack(track)) return;\n\n const { segments } = track;\n if (segments.length === 0) return;\n\n const trackId = track.id;\n\n // Resolve segments already recorded in the state model for this track.\n // Keyed by track ID so multiple text tracks don't interfere with each other.\n const loadedIds = new Set((currentState.textBufferState?.[trackId]?.segments ?? []).map((s) => s.id));\n const alreadyLoaded = segments.filter((s) => loadedIds.has(s.id));\n\n // Apply the same forward buffer window as audio/video segment loading.\n const currentTime = currentState.currentTime ?? 0;\n const segmentsToLoad = getSegmentsToLoad(segments, alreadyLoaded, currentTime).filter((s) => !loadedIds.has(s.id));\n\n if (segmentsToLoad.length === 0) return;\n\n // Execute subtasks sequentially, recording each loaded segment in state.\n for (const segment of segmentsToLoad) {\n if (context.signal.aborted) break;\n\n try {\n await loadVttSegmentTask({ segment }, { textTrack: context.textTrack });\n\n // Record the loaded segment in shared state — mirrors bufferState for\n // audio/video and supports N text tracks keyed by track ID.\n const latest = context.state.current.textBufferState ?? {};\n const trackState = latest[trackId] ?? { segments: [] };\n context.state.patch({\n textBufferState: {\n ...latest,\n [trackId]: { segments: [...trackState.segments, { id: segment.id }] },\n },\n });\n } catch (error) {\n if (error instanceof Error && error.name === 'AbortError') break;\n console.error('Failed to load VTT segment:', error);\n // Continue to next segment (graceful degradation)\n }\n }\n\n // Chrome bug: after a track goes through mode='disabled' (which clears cues) and back\n // to 'showing', cues added to the track aren't activated. Re-adding all cues forces\n // Chrome to re-process them. Safe no-op in other browsers.\n // Mirrors the workaround in HlsMediaTextTracksMixin (packages/core/src/dom/media/hls/text-tracks.ts).\n if (context.textTrack.mode === 'showing' && context.textTrack.cues) {\n Array.from(context.textTrack.cues).forEach((cue) => {\n context.textTrack.addCue(cue);\n });\n }\n\n // Wait a frame before completing to allow state updates to flush\n await new Promise((resolve) => requestAnimationFrame(resolve));\n};\n\n// ============================================================================\n// STATE & OWNERS\n// ============================================================================\n\n/**\n * Loaded-segment record for a single text track.\n */\nexport interface TextTrackSegmentState {\n segments: Array<{ id: string }>;\n}\n\n/**\n * Buffer model for text track cues — keyed by track ID.\n *\n * Using a per-track-ID map (rather than fixed 'video'/'audio' keys) because\n * there can be N text tracks — one per language/subtitle variant.\n */\nexport type TextTrackBufferState = Record<string, TextTrackSegmentState>;\n\n/**\n * State shape for text track cue loading.\n */\nexport interface TextTrackCueLoadingState {\n selectedTextTrackId?: string;\n presentation?: Presentation;\n /** Current playback position — used to gate VTT segment fetching to the forward buffer window. */\n currentTime?: number;\n /** Loaded-segment model for text tracks, keyed by track ID. */\n textBufferState?: TextTrackBufferState;\n}\n\n/**\n * Owners shape for text track cue loading.\n */\nexport interface TextTrackCueLoadingOwners {\n textTracks?: Map<string, HTMLTrackElement>;\n}\n\n/**\n * Find the selected text track in the presentation.\n */\nfunction findSelectedTextTrack(state: TextTrackCueLoadingState): TextTrack | undefined {\n if (!state.presentation || !state.selectedTextTrackId) {\n return undefined;\n }\n\n const textSet = state.presentation.selectionSets.find((set) => set.type === 'text');\n if (!textSet?.switchingSets?.[0]?.tracks) {\n return undefined;\n }\n\n const track = textSet.switchingSets[0].tracks.find((t) => t.id === state.selectedTextTrackId);\n\n return track as TextTrack | undefined;\n}\n\n/**\n * Get the browser's TextTrack object for the selected text track.\n *\n * Retrieves the live TextTrack interface from the track element in owners,\n * which is used for adding cues, checking mode, and managing track state.\n *\n * Note: Returns the DOM TextTrack interface (HTMLTrackElement.track),\n * not the presentation Track metadata type.\n *\n * @param state - Current playback state (track selection)\n * @param owners - DOM owners containing track elements map\n * @returns DOM TextTrack interface or undefined if not found\n */\nfunction getSelectedTextTrackFromOwners(\n state: TextTrackCueLoadingState,\n owners: TextTrackCueLoadingOwners\n): globalThis.TextTrack | undefined {\n const trackId = state.selectedTextTrackId;\n if (!trackId || !owners.textTracks) {\n return undefined;\n }\n\n const trackElement = owners.textTracks.get(trackId);\n return trackElement?.track;\n}\n\n/**\n * Check if we can load text track cues.\n *\n * Requires:\n * - Selected text track ID exists\n * - Track elements map exists\n * - Track element exists for selected track\n */\nexport function canLoadTextTrackCues(state: TextTrackCueLoadingState, owners: TextTrackCueLoadingOwners): boolean {\n return !!state.selectedTextTrackId && !!owners.textTracks && owners.textTracks.has(state.selectedTextTrackId);\n}\n\n/**\n * Check if we should load text track cues.\n *\n * Only load if:\n * - Track is resolved (has segments)\n * - Track has at least one segment\n * - Track element exists\n */\nexport function shouldLoadTextTrackCues(state: TextTrackCueLoadingState, owners: TextTrackCueLoadingOwners): boolean {\n if (!canLoadTextTrackCues(state, owners)) {\n return false;\n }\n\n const track = findSelectedTextTrack(state);\n if (!track || !isResolvedTrack(track) || track.segments.length === 0) {\n return false;\n }\n\n const textTrack = getSelectedTextTrackFromOwners(state, owners);\n if (!textTrack) {\n return false;\n }\n\n return true;\n}\n\n/**\n * Load text track cues orchestration.\n *\n * Triggers when:\n * - Text track is selected\n * - Track is resolved (has segments)\n * - Track element exists\n *\n * Fetches and parses VTT segments within the forward buffer window, then adds\n * cues to the track incrementally. Continues on segment errors to provide\n * partial subtitles.\n *\n * @example\n * const cleanup = loadTextTrackCues({ state, owners });\n */\nexport function loadTextTrackCues({\n state,\n owners,\n}: {\n state: WritableState<TextTrackCueLoadingState>;\n owners: WritableState<TextTrackCueLoadingOwners>;\n}): () => void {\n let currentTask: Promise<void> | null = null;\n let abortController: AbortController | null = null;\n let lastTrackId: string | undefined;\n\n const cleanup = combineLatest([state, owners]).subscribe(\n async ([currentState, currentOwners]: [TextTrackCueLoadingState, TextTrackCueLoadingOwners]) => {\n // Abort any in-progress task when the selected track changes.\n // The new track's textBufferState entry will be empty, so the task\n // naturally starts fresh without needing any explicit reset.\n if (currentState.selectedTextTrackId !== lastTrackId) {\n lastTrackId = currentState.selectedTextTrackId;\n abortController?.abort();\n currentTask = null;\n }\n\n if (currentTask) return; // Task already in progress\n if (!shouldLoadTextTrackCues(currentState, currentOwners)) return;\n\n const textTrack = getSelectedTextTrackFromOwners(currentState, currentOwners);\n if (!textTrack) return;\n\n // Create abort controller and invoke task\n abortController = new AbortController();\n currentTask = loadTextTrackCuesTask(\n { currentState },\n { signal: abortController.signal, textTrack, state }\n ).finally(() => {\n currentTask = null;\n });\n }\n );\n\n return () => {\n abortController?.abort();\n cleanup();\n };\n}\n","import { listen } from '@videojs/utils/dom';\nimport type { WritableState } from '../../core/state/create-state';\n\n/**\n * State shape for current time tracking.\n */\nexport interface CurrentTimeState {\n currentTime?: number;\n}\n\n/**\n * Owners shape for current time tracking.\n */\nexport interface CurrentTimeOwners {\n mediaElement?: HTMLMediaElement | undefined;\n}\n\n/**\n * Check if we can track current time.\n *\n * Requires:\n * - mediaElement exists in owners\n */\nexport function canTrackCurrentTime(owners: CurrentTimeOwners): boolean {\n return !!owners.mediaElement;\n}\n\n/**\n * Track current playback position from the media element.\n *\n * Mirrors `mediaElement.currentTime` into reactive state on:\n * - `timeupdate` — fires during playback (~4 Hz)\n * - `seeking` — fires when a seek begins; per spec, `currentTime` is\n * already at the new position when this event dispatches, so buffer\n * management can react immediately rather than waiting for `timeupdate`,\n * which does not fire while paused.\n *\n * Also syncs immediately when a media element becomes available.\n *\n * @example\n * const cleanup = trackCurrentTime({ state, owners });\n */\nexport function trackCurrentTime({\n state,\n owners,\n}: {\n state: WritableState<CurrentTimeState>;\n owners: WritableState<CurrentTimeOwners>;\n}): () => void {\n let lastMediaElement: HTMLMediaElement | undefined;\n let removeListeners: (() => void) | null = null;\n\n const unsubscribe = owners.subscribe((currentOwners) => {\n const { mediaElement } = currentOwners;\n\n if (mediaElement === lastMediaElement) return;\n\n removeListeners?.();\n removeListeners = null;\n lastMediaElement = mediaElement;\n\n if (!mediaElement) return;\n\n // Sync immediately so consumers don't wait for the first event\n state.patch({ currentTime: mediaElement.currentTime });\n\n const sync = () => state.patch({ currentTime: mediaElement.currentTime });\n const removeTimeupdate = listen(mediaElement, 'timeupdate', sync);\n const removeSeeking = listen(mediaElement, 'seeking', sync);\n removeListeners = () => {\n removeTimeupdate();\n removeSeeking();\n };\n });\n\n return () => {\n removeListeners?.();\n unsubscribe();\n };\n}\n","import { listen } from '@videojs/utils/dom';\nimport type { EventStream } from '../../core/events/create-event-stream';\nimport type { PresentationAction } from '../../core/features/resolve-presentation';\nimport type { WritableState } from '../../core/state/create-state';\n\n/**\n * State shape for playback initiation tracking.\n */\nexport interface PlaybackInitiatedState {\n /** True once the user has initiated playback (play event fired). */\n playbackInitiated?: boolean;\n /** Current presentation — watched for URL changes to reset playbackInitiated. */\n presentation?: { url?: string };\n}\n\n/**\n * Owners shape for playback initiation tracking.\n */\nexport interface PlaybackInitiatedOwners {\n mediaElement?: HTMLMediaElement | undefined;\n}\n\n/**\n * Track whether playback has been initiated by the user.\n *\n * Sets `state.playbackInitiated = true` when the media element fires a `play`\n * event (via `element.play()`, native controls, or autoplay) and simultaneously\n * dispatches `{ type: 'play' }` to the event stream so `resolvePresentation`\n * can react.\n *\n * Resets `state.playbackInitiated = false` when `presentation.url` changes,\n * so a new source with `preload=\"none\"` won't load segments until play is\n * triggered again.\n *\n * This flag is used by `shouldLoadSegments` to allow segment loading after\n * play is initiated regardless of the initial `preload` setting — `preload`\n * is a startup hint, not a runtime gate.\n *\n * @example\n * const cleanup = trackPlaybackInitiated({ state, owners, events });\n */\nexport function trackPlaybackInitiated({\n state,\n owners,\n events,\n}: {\n state: WritableState<PlaybackInitiatedState>;\n owners: WritableState<PlaybackInitiatedOwners>;\n events: EventStream<PresentationAction>;\n}): () => void {\n let lastMediaElement: HTMLMediaElement | undefined;\n let removeListener: (() => void) | null = null;\n let lastPresentationUrl: string | undefined;\n\n // Watch for presentation URL changes to reset playbackInitiated\n const unsubscribeState = state.subscribe((currentState) => {\n const url = currentState.presentation?.url;\n if (url !== lastPresentationUrl) {\n if (lastPresentationUrl !== undefined) {\n // URL changed to a new value — reset so new source requires play again\n state.patch({ playbackInitiated: false });\n }\n lastPresentationUrl = url;\n }\n });\n\n // Bridge media element play event → state flag + event stream\n const unsubscribeOwners = owners.subscribe((currentOwners) => {\n const { mediaElement } = currentOwners;\n\n if (mediaElement === lastMediaElement) return;\n\n removeListener?.();\n removeListener = null;\n lastMediaElement = mediaElement;\n\n if (!mediaElement) return;\n\n removeListener = listen(mediaElement, 'play', () => {\n state.patch({ playbackInitiated: true });\n events.dispatch({ type: 'play' });\n });\n });\n\n return () => {\n removeListener?.();\n unsubscribeState();\n unsubscribeOwners();\n };\n}\n","/**\n * Segment appender helper (P11)\n *\n * Appends media segments (ArrayBuffer) to SourceBuffer.\n */\n\n/**\n * Append a media segment to a SourceBuffer.\n *\n * Waits for the SourceBuffer to be ready (not updating), then appends\n * the segment data. Returns a promise that resolves when append completes.\n *\n * @param sourceBuffer - The SourceBuffer to append to\n * @param segmentData - The segment data as ArrayBuffer\n * @returns Promise that resolves when append completes\n *\n * @example\n * const data = await fetch(segmentUrl).then(r => r.arrayBuffer());\n * await appendSegment(videoSourceBuffer, data);\n */\nexport async function appendSegment(sourceBuffer: SourceBuffer, segmentData: ArrayBuffer): Promise<void> {\n // Wait for SourceBuffer to be ready (not currently updating)\n if (sourceBuffer.updating) {\n await new Promise<void>((resolve) => {\n const onUpdateEnd = () => {\n sourceBuffer.removeEventListener('updateend', onUpdateEnd);\n resolve();\n };\n sourceBuffer.addEventListener('updateend', onUpdateEnd);\n });\n }\n\n // Append the segment data\n return new Promise<void>((resolve, reject) => {\n const onUpdateEnd = () => {\n cleanup();\n resolve();\n };\n\n const onError = (event: Event) => {\n cleanup();\n reject(new Error(`SourceBuffer append error: ${event.type}`));\n };\n\n const cleanup = () => {\n sourceBuffer.removeEventListener('updateend', onUpdateEnd);\n sourceBuffer.removeEventListener('error', onError);\n };\n\n sourceBuffer.addEventListener('updateend', onUpdateEnd);\n sourceBuffer.addEventListener('error', onError);\n\n try {\n sourceBuffer.appendBuffer(segmentData);\n } catch (error) {\n cleanup();\n reject(error);\n }\n });\n}\n","/**\n * Buffer flusher helper (P12)\n *\n * Removes a time range from a SourceBuffer to manage memory.\n */\n\n/**\n * Remove a time range from a SourceBuffer.\n *\n * Waits for the SourceBuffer to be ready (not updating), then removes\n * the specified range. Returns a promise that resolves when removal completes.\n *\n * @param sourceBuffer - The SourceBuffer to remove data from\n * @param start - Start of the time range to remove (seconds)\n * @param end - End of the time range to remove (seconds)\n * @returns Promise that resolves when removal completes\n *\n * @example\n * await flushBuffer(videoSourceBuffer, 0, 30);\n */\nexport async function flushBuffer(sourceBuffer: SourceBuffer, start: number, end: number): Promise<void> {\n // Wait for SourceBuffer to be ready (not currently updating)\n if (sourceBuffer.updating) {\n await new Promise<void>((resolve) => {\n const onUpdateEnd = () => {\n sourceBuffer.removeEventListener('updateend', onUpdateEnd);\n resolve();\n };\n sourceBuffer.addEventListener('updateend', onUpdateEnd);\n });\n }\n\n return new Promise<void>((resolve, reject) => {\n const onUpdateEnd = () => {\n cleanup();\n resolve();\n };\n\n const onError = (event: Event) => {\n cleanup();\n reject(new Error(`SourceBuffer remove error: ${event.type}`));\n };\n\n const cleanup = () => {\n sourceBuffer.removeEventListener('updateend', onUpdateEnd);\n sourceBuffer.removeEventListener('error', onError);\n };\n\n sourceBuffer.addEventListener('updateend', onUpdateEnd);\n sourceBuffer.addEventListener('error', onError);\n\n try {\n sourceBuffer.remove(start, end);\n } catch (error) {\n cleanup();\n reject(error);\n }\n });\n}\n","import { combineLatest } from '../reactive/combine-latest';\nimport type { WritableState } from '../state/create-state';\nimport type { AudioTrack, Presentation, VideoTrack } from '../types';\nimport { isResolvedTrack } from '../types';\nimport { getSelectedTrack } from '../utils/track-selection';\n\nexport interface PresentationDurationState {\n presentation?: Presentation;\n selectedVideoTrackId?: string;\n selectedAudioTrackId?: string;\n}\n\n/**\n * Check if we can calculate presentation duration (have required data).\n */\nexport function canCalculateDuration(state: PresentationDurationState): boolean {\n if (!state.presentation) return false;\n // Need at least one selected track\n return !!(state.selectedVideoTrackId || state.selectedAudioTrackId);\n}\n\n/**\n * Check if we should calculate presentation duration (conditions met).\n */\nexport function shouldCalculateDuration(state: PresentationDurationState): boolean {\n if (!canCalculateDuration(state)) return false;\n\n const { presentation } = state;\n\n // Don't recalculate if already set\n if (presentation!.duration !== undefined) return false;\n\n // Check if any selected track is resolved\n const videoTrack = state.selectedVideoTrackId ? getSelectedTrack(state, 'video') : undefined;\n const audioTrack = state.selectedAudioTrackId ? getSelectedTrack(state, 'audio') : undefined;\n\n // At least one track must be resolved (has segments and duration)\n return !!((videoTrack && isResolvedTrack(videoTrack)) || (audioTrack && isResolvedTrack(audioTrack)));\n}\n\n/**\n * Get duration from the first resolved track (prefer video, fallback to audio).\n */\nexport function getDurationFromResolvedTracks(state: PresentationDurationState): number | undefined {\n // Try video track first\n const videoTrack = state.selectedVideoTrackId\n ? (getSelectedTrack(state, 'video') as VideoTrack | undefined)\n : undefined;\n if (videoTrack && isResolvedTrack(videoTrack)) {\n return videoTrack.duration;\n }\n\n // Fallback to audio track\n const audioTrack = state.selectedAudioTrackId\n ? (getSelectedTrack(state, 'audio') as AudioTrack | undefined)\n : undefined;\n if (audioTrack && isResolvedTrack(audioTrack)) {\n return audioTrack.duration;\n }\n\n return undefined;\n}\n\n/**\n * Calculate and set presentation duration from resolved tracks.\n */\nexport function calculatePresentationDuration({\n state,\n}: {\n state: WritableState<PresentationDurationState>;\n}): () => void {\n return combineLatest([state]).subscribe(([currentState]: [PresentationDurationState]) => {\n if (!shouldCalculateDuration(currentState)) return;\n\n const duration = getDurationFromResolvedTracks(currentState);\n if (duration === undefined || !Number.isFinite(duration)) return;\n\n const { presentation } = currentState;\n\n // Patch presentation with duration\n state.patch({\n presentation: {\n ...presentation!,\n duration,\n },\n });\n });\n}\n","import { 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 ? AbortSignal.any([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","import { fetchResolvable, getResponseText } from '../../dom/network/fetch';\nimport type { EventStream } from '../events/create-event-stream';\nimport { parseMediaPlaylist } from '../hls/parse-media-playlist';\nimport { combineLatest } from '../reactive/combine-latest';\nimport type { WritableState } from '../state/create-state';\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 * Determines if track resolution conditions are met.\n *\n * Currently always returns true - conditions are checked by canResolveTrack()\n * and resolving flag. Kept as placeholder for future conditional logic.\n *\n * @param state - Current track resolution state\n * @param event - Current action/event\n * @returns true (conditions checked elsewhere)\n */\nexport function shouldResolve(_state: TrackResolutionState, _event: TrackResolutionAction): boolean {\n return true;\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 * Action types for track resolution.\n * Event names match HTMLMediaElement events (lowercase).\n */\nexport type TrackResolutionAction = { type: 'play' } | { type: 'pause' };\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 * The subscribe closure is pure scheduling logic: it checks conditions and\n * creates a Task for the selected track when appropriate. The ConcurrentRunner\n * handles all concurrency concerns — deduplication, parallel execution, and\n * 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<T extends TrackType>(\n {\n state,\n events,\n }: {\n state: WritableState<TrackResolutionState>;\n events: EventStream<TrackResolutionAction>;\n },\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 = combineLatest([state, events]).subscribe(([currentState, event]) => {\n if (!canResolve(currentState, config) || !shouldResolve(currentState, event)) 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.current.presentation at patch 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 patched the\n // presentation with its own resolved track. Using the snapshot would overwrite\n // that update and lose the other track's resolution. Reading live state ensures\n // each task builds on top of whatever has been committed so far.\n //\n // This is a limitation of the current architecture: tasks receive a snapshot\n // at scheduling time but need live state at write time. A future improvement\n // could dispatch resolved tracks through a single serialised writer (e.g. a\n // reducer pattern) to eliminate this hazard entirely.\n const latestPresentation = state.current.presentation!;\n const updatedPresentation = updateTrackInPresentation(latestPresentation, mediaTrack);\n state.patch({ presentation: updatedPresentation });\n },\n { id: track.id }\n )\n );\n });\n\n return () => {\n runner.abortAll();\n cleanup();\n };\n}\n","import { DEFAULT_QUALITY_CONFIG, selectQuality } from '../abr/quality-selection';\nimport type { EventStream } from '../events/create-event-stream';\nimport type { WritableState } from '../state/create-state';\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(\n {\n state,\n }: {\n state: WritableState<TrackSelectionState>;\n owners: WritableState<TrackSelectionOwners>;\n events: EventStream<TrackSelectionAction>;\n },\n config: VideoSelectionConfig = { type: 'video' }\n): () => void {\n let selecting = false;\n\n return state.subscribe(async (currentState: TrackSelectionState) => {\n if (!canSelectTrack(currentState, config) || !shouldSelectTrack(currentState, config) || selecting) return;\n\n try {\n selecting = true;\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 state.patch({ [selectedTrackKey]: selectedTrackId });\n }\n } finally {\n selecting = false;\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(\n {\n state,\n }: {\n state: WritableState<TrackSelectionState>;\n owners: WritableState<TrackSelectionOwners>;\n events: EventStream<TrackSelectionAction>;\n },\n config: AudioSelectionConfig = { type: 'audio' }\n): () => void {\n let selecting = false;\n\n return state.subscribe(async (currentState: TrackSelectionState) => {\n if (!canSelectTrack(currentState, config) || !shouldSelectTrack(currentState, config) || selecting) return;\n\n try {\n selecting = true;\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 state.patch({ selectedAudioTrackId: selectedTrackId });\n }\n } finally {\n selecting = false;\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(\n {\n state,\n }: {\n state: WritableState<TrackSelectionState>;\n owners: WritableState<TrackSelectionOwners>;\n events: EventStream<TrackSelectionAction>;\n },\n config: TextSelectionConfig = { type: 'text' }\n): () => void {\n let selecting = false;\n\n return state.subscribe(async (currentState: TrackSelectionState) => {\n if (!canSelectTrack(currentState, config) || !shouldSelectTrack(currentState, config) || selecting) return;\n\n try {\n selecting = true;\n\n // Text tracks are user opt-in - don't auto-select\n const selectedTextTrackId = pickTextTrack(currentState.presentation!, config);\n\n if (selectedTextTrackId) {\n state.patch({ selectedTextTrackId });\n }\n } finally {\n selecting = false;\n }\n });\n}\n","import { combineLatest } from '../../core/reactive/combine-latest';\nimport type { WritableState } from '../../core/state/create-state';\nimport type { Presentation } from '../../core/types';\nimport { isResolvedTrack } from '../../core/types';\nimport { getSelectedTrack, type TrackSelectionState } from '../../core/utils/track-selection';\nimport type { SourceBufferActor } from '../media/source-buffer-actor';\n\nexport interface EndOfStreamState extends TrackSelectionState {\n presentation?: Presentation;\n}\n\nexport interface EndOfStreamOwners {\n mediaSource?: MediaSource;\n mediaElement?: HTMLMediaElement | undefined;\n videoBuffer?: SourceBuffer;\n audioBuffer?: SourceBuffer;\n videoBufferActor?: SourceBufferActor;\n audioBufferActor?: SourceBufferActor;\n}\n\n// ## When to call endOfStream()\n//\n// Per the MSE spec, endOfStream() should be called once the last media\n// segments — temporally speaking — have been completely appended to all\n// active SourceBuffers. Specifically it signals two things:\n// 1. The temporally latest segments for both audio and video have been\n// appended (i.e. the buffer covers the end of the stream content).\n// 2. The MediaSource will transition from 'open' to 'ended'. Appending\n// additional (earlier) segments after this — e.g. for a seek-back or\n// back-buffer refill — will re-open the MediaSource, at which point\n// endOfStream() must be called again once loading reaches the end.\n//\n// The browser uses this signal to finalise MediaSource.duration and allow\n// the media element to fire the `ended` event. Without it, playback stalls\n// at the end of the buffered range waiting for data that will never arrive.\n//\n// The \"any track\" qualifier is intentional: per the HLS spec, all renditions\n// in a switching set are time-aligned, so the last segment of any rendition\n// covers the same end-of-stream content. We don't need to be tied to the\n// currently selected track.\n//\n// The right long-term condition is therefore:\n// - The last segment of the video content (from any resolved video track)\n// has been completely appended to the video SourceBuffer, AND\n// - The last segment of the audio content (from any resolved audio track)\n// has been completely appended to the audio SourceBuffer (when active), AND\n// - currentTime is within the time range of that last segment.\n//\n// The currentTime gate prevents unnecessary re-invocations when back-buffer\n// refills or other mid-stream appends briefly re-open the MediaSource while\n// the user is far from the end.\n//\n/**\n * Check if the last segment of a track has been appended to a SourceBuffer.\n *\n * Checks by segment ID rather than a pipeline flag, so it is robust across\n * quality switches (different tracks have different segment IDs) and\n * back-buffer flushes (flushed segment IDs are removed from the model).\n */\nfunction isLastSegmentAppended(segments: readonly { id: string }[], actor: SourceBufferActor | undefined): boolean {\n if (segments.length === 0) return true;\n const lastSeg = segments[segments.length - 1];\n if (!lastSeg) return false;\n return actor?.snapshot.context.segments.some((s) => s.id === lastSeg.id) ?? false;\n}\n\n/**\n * Check if the last segment has been appended for each selected track.\n *\n * Handles video-only, audio-only, and video+audio scenarios.\n * A track with no segments (e.g. unresolved) is considered not ready.\n */\nexport function hasLastSegmentLoaded(state: EndOfStreamState, owners: EndOfStreamOwners): boolean {\n const videoTrack = state.selectedVideoTrackId ? getSelectedTrack(state, 'video') : undefined;\n const audioTrack = state.selectedAudioTrackId ? getSelectedTrack(state, 'audio') : undefined;\n\n // An unresolved track means we don't yet know its segments — cannot be done.\n // Fast-paths the quality-switch window: when selectedVideoTrackId has changed\n // to a new (unresolved) track, we cannot yet determine if its last segment\n // is loaded.\n if (videoTrack && !isResolvedTrack(videoTrack)) return false;\n if (audioTrack && !isResolvedTrack(audioTrack)) return false;\n\n if (videoTrack && isResolvedTrack(videoTrack)) {\n if (!isLastSegmentAppended(videoTrack.segments, owners.videoBufferActor)) return false;\n }\n\n if (audioTrack && isResolvedTrack(audioTrack)) {\n if (!isLastSegmentAppended(audioTrack.segments, owners.audioBufferActor)) return false;\n }\n\n return true;\n}\n\n/**\n * Check if we can call endOfStream.\n */\nexport function canEndStream(state: EndOfStreamState, owners: EndOfStreamOwners): boolean {\n return !!(owners.mediaSource && state.presentation);\n}\n\n/**\n * Check if we should call endOfStream.\n */\nexport function shouldEndStream(state: EndOfStreamState, owners: EndOfStreamOwners): boolean {\n if (!canEndStream(state, owners)) return false;\n\n const { mediaSource, mediaElement } = owners;\n\n // MediaSource must be open\n if (mediaSource!.readyState !== 'open') return false;\n\n // CRITICAL: MediaElement must have metadata before calling endOfStream\n // Calling endOfStream before HAVE_METADATA causes DEMUXER_ERROR\n // https://github.com/chromium/chromium/blob/main/media/filters/chunk_demuxer.cc\n if (mediaElement && mediaElement.readyState < HTMLMediaElement.HAVE_METADATA) {\n return false;\n }\n\n // SourceBuffers must exist for selected tracks before we can end the stream\n // (otherwise we'd close the MediaSource before SourceBuffers are created)\n const hasVideoTrack = !!state.selectedVideoTrackId;\n const hasAudioTrack = !!state.selectedAudioTrackId;\n\n if (hasVideoTrack && !owners.videoBuffer) return false;\n if (hasAudioTrack && !owners.audioBuffer) return false;\n\n // SourceBufferActors must be idle — setting duration while a SourceBuffer is\n // updating throws InvalidStateError. The actor subscriber in endOfStream() will\n // re-evaluate when each actor transitions back to idle.\n if (owners.videoBufferActor?.snapshot.status === 'updating') return false;\n if (owners.audioBufferActor?.snapshot.status === 'updating') return false;\n\n // Last segment must be appended for each selected track\n if (!hasLastSegmentLoaded(state, owners)) return false;\n\n // currentTime must have reached the last segment. Guards against re-ending\n // the stream when a back-buffer remove() re-opens the MediaSource while the\n // user is far from the end (remove() re-opens 'ended' → 'open' per MSE spec,\n // same as appendBuffer()).\n if (mediaElement) {\n const videoTrack = hasVideoTrack ? getSelectedTrack(state, 'video') : undefined;\n const audioTrack = hasAudioTrack ? getSelectedTrack(state, 'audio') : undefined;\n const refTrack =\n videoTrack && isResolvedTrack(videoTrack)\n ? videoTrack\n : audioTrack && isResolvedTrack(audioTrack)\n ? audioTrack\n : undefined;\n if (refTrack && refTrack.segments.length > 0) {\n const lastSeg = refTrack.segments[refTrack.segments.length - 1]!;\n if (mediaElement.currentTime < lastSeg.startTime) return false;\n }\n }\n\n return true;\n}\n\n/**\n * Wait for all currently-updating SourceBufferActors to finish.\n * Uses actor status rather than raw SourceBuffer.updating so the wait is\n * aligned with the same abstraction that owns all buffer operations.\n */\nfunction waitForSourceBuffersReady(owners: EndOfStreamOwners): Promise<void> {\n const updatingActors = [owners.videoBufferActor, owners.audioBufferActor].filter(\n (actor): actor is SourceBufferActor => actor !== undefined && actor.snapshot.status === 'updating'\n );\n\n if (updatingActors.length === 0) return Promise.resolve();\n\n return Promise.all(\n updatingActors.map(\n (actor) =>\n new Promise<void>((resolve) => {\n const unsub = actor.subscribe((snapshot) => {\n if (snapshot.status !== 'updating') {\n unsub();\n resolve();\n }\n });\n })\n )\n ).then(() => undefined);\n}\n\n/**\n * Get the highest buffered end time across all active SourceBuffers.\n * Used to set the final duration from actual container timestamps rather\n * than playlist metadata, which handles both shorter and longer cases.\n */\nfunction getMaxBufferedEnd(owners: EndOfStreamOwners): number {\n let max = 0;\n for (const buf of [owners.videoBuffer, owners.audioBuffer]) {\n if (buf && buf.buffered.length > 0) {\n const end = buf.buffered.end(buf.buffered.length - 1);\n if (end > max) max = end;\n }\n }\n return max;\n}\n\n/**\n * End of stream task (module-level, pure).\n * Sets the final duration from actual buffered end time, then calls endOfStream().\n */\nconst endOfStreamTask = async (\n { currentOwners }: { currentOwners: EndOfStreamOwners },\n _context: object\n): Promise<void> => {\n const { mediaSource } = currentOwners;\n\n // Double-check MediaSource isn't already ended (in case of race)\n if (mediaSource!.readyState === 'ended') {\n return;\n }\n\n // Wait for any in-progress SourceBuffer operations to finish before calling\n // endOfStream() — the MSE spec forbids it while any buffer has updating === true.\n await waitForSourceBuffersReady(currentOwners);\n\n // Re-check after the async wait\n if (mediaSource!.readyState !== 'open') return;\n\n // Set the final duration from actual buffered container timestamps.\n // This is more accurate than the playlist-derived duration and correctly\n // handles both shorter (common with CMAF) and longer actual media durations.\n // Per MSE spec, endOfStream() will only *increase* duration if needed, so\n // setting it here first ensures the value from the buffer wins in all cases.\n const bufferedEnd = getMaxBufferedEnd(currentOwners);\n if (bufferedEnd > 0) {\n mediaSource!.duration = bufferedEnd;\n }\n\n mediaSource!.endOfStream();\n\n // Wait a frame to allow async state updates to flush\n await new Promise((resolve) => requestAnimationFrame(resolve));\n};\n\n/**\n * Call endOfStream when the last segment has been appended.\n * This signals to the browser that the stream is complete.\n *\n * Per the MSE spec, appendBuffer() remains valid after endOfStream() —\n * seeks that require re-appending earlier segments will still work.\n * What becomes blocked is calling endOfStream() again, addSourceBuffer(),\n * and MediaSource.duration updates.\n */\nexport function endOfStream({\n state,\n owners,\n}: {\n state: WritableState<EndOfStreamState>;\n owners: WritableState<EndOfStreamOwners>;\n}): () => void {\n let hasEnded = false;\n let destroyed = false;\n const activeActorUnsubs: Array<() => void> = [];\n\n const runEvaluate = async () => {\n if (destroyed) return;\n const currentState = state.current;\n const currentOwners = owners.current;\n if (hasEnded) {\n // Per the MSE spec, calling appendBuffer() on a SourceBuffer when\n // readyState is 'ended' automatically transitions it back to 'open'.\n // This happens on seek-back after end-of-stream — allow endOfStream()\n // to be called again once the last segment is reloaded.\n if (currentOwners.mediaSource?.readyState !== 'open') return;\n hasEnded = false;\n }\n if (!shouldEndStream(currentState, currentOwners)) return;\n\n // Set flag before awaiting to close the re-entry window between\n // endOfStream() being called and the async task completing.\n hasEnded = true;\n try {\n await endOfStreamTask({ currentOwners }, {});\n } catch (error) {\n console.error('Failed to call endOfStream:', error);\n }\n };\n\n // Subscribe to actor snapshot changes so endOfStream re-evaluates when a\n // segment is appended (actor context updated), not just on state/owners changes.\n // Re-subscribe whenever owners changes (actors may have appeared or changed).\n const cleanupOwners = owners.subscribe((currentOwners) => {\n activeActorUnsubs.forEach((u) => u());\n activeActorUnsubs.length = 0;\n\n for (const actor of [currentOwners.videoBufferActor, currentOwners.audioBufferActor]) {\n if (!actor) continue;\n // Skip the immediate fire — the combineLatest subscription handles the\n // initial evaluation. Only react to subsequent snapshot changes.\n let isFirst = true;\n activeActorUnsubs.push(\n actor.subscribe(() => {\n if (isFirst) {\n isFirst = false;\n return;\n }\n runEvaluate();\n })\n );\n }\n });\n\n const cleanupCombineLatest = combineLatest([state, owners]).subscribe(async () => runEvaluate());\n\n return () => {\n destroyed = true;\n activeActorUnsubs.forEach((u) => u());\n cleanupOwners();\n cleanupCombineLatest();\n };\n}\n","import { isNil } from '@videojs/utils/predicate';\nimport { combineLatest } from '../../core/reactive/combine-latest';\nimport type { WritableState } from '../../core/state/create-state';\nimport type { Presentation } from '../../core/types';\nimport { attachMediaSource, createMediaSource, waitForSourceOpen } from '../media/mediasource-setup';\n\n/**\n * State shape required for MediaSource setup.\n */\nexport interface MediaSourceState {\n presentation?: Presentation;\n}\n\n/**\n * Owners shape for MediaSource setup.\n */\nexport interface MediaSourceOwners {\n mediaElement?: HTMLMediaElement | undefined;\n mediaSource?: MediaSource;\n}\n\n/**\n * Check if we have the minimum requirements to create MediaSource.\n */\nexport function canSetup(state: MediaSourceState, owners: MediaSourceOwners): boolean {\n return !isNil(owners.mediaElement) && !isNil(state.presentation?.url);\n}\n\n/**\n * Check if we should proceed with MediaSource creation.\n * Placeholder for future conditions (e.g., checking if already created).\n */\nexport function shouldSetup(_state: MediaSourceState, owners: MediaSourceOwners): boolean {\n // Don't create if we already have one\n return isNil(owners.mediaSource);\n}\n\n/**\n * Setup MediaSource orchestration.\n *\n * Creates and attaches MediaSource when:\n * - mediaElement exists in owners\n * - presentation.url exists in state\n *\n * Updates owners.mediaSource after successful setup.\n */\nexport function setupMediaSource({\n state,\n owners,\n}: {\n state: WritableState<MediaSourceState>;\n owners: WritableState<MediaSourceOwners>;\n}): () => void {\n let settingUp = false;\n let abortController: AbortController | null = null;\n\n const unsubscribe = combineLatest([state, owners]).subscribe(\n async ([currentState, currentOwners]: [MediaSourceState, MediaSourceOwners]) => {\n if (!canSetup(currentState, currentOwners) || !shouldSetup(currentState, currentOwners) || settingUp) return;\n\n try {\n settingUp = true;\n abortController = new AbortController();\n\n // Prefer ManagedMediaSource when available (Safari 17+). attachMediaSource\n // handles the srcObject path and sets disableRemotePlayback automatically.\n const mediaSource = createMediaSource({ preferManaged: true });\n\n // Attach to element\n attachMediaSource(mediaSource, currentOwners.mediaElement!);\n\n // Wait for sourceopen — aborted if destroy() is called before it fires\n await waitForSourceOpen(mediaSource, abortController.signal);\n\n owners.patch({ mediaSource });\n } catch (error) {\n if (error instanceof DOMException && error.name === 'AbortError') return;\n throw error;\n } finally {\n settingUp = false;\n }\n }\n );\n\n return () => {\n abortController?.abort();\n unsubscribe();\n };\n}\n","import type { Actor, ActorSnapshot } from '../../core/actor';\nimport { createState } from '../../core/state/create-state';\nimport { SerialRunner, Task } from '../../core/task';\nimport type { Segment, Track } from '../../core/types';\nimport { appendSegment } from './append-segment';\nimport { flushBuffer } from './buffer-flusher';\n\n// =============================================================================\n// Types\n// =============================================================================\n\nexport interface BufferedRange {\n start: number;\n end: number;\n}\n\nexport type AppendSegmentMeta = Pick<Segment, 'id' | 'startTime' | 'duration'> & {\n trackId: Track['id'];\n /** Declared track bandwidth in bps (from playlist BANDWIDTH attribute). */\n trackBandwidth?: number;\n};\n\nexport type AppendInitMessage = { type: 'append-init'; data: ArrayBuffer; meta: { trackId: Track['id'] } };\nexport type AppendSegmentMessage = { type: 'append-segment'; data: ArrayBuffer; meta: AppendSegmentMeta };\nexport type RemoveMessage = { type: 'remove'; start: number; end: number };\nexport type SourceBufferMessage = AppendInitMessage | AppendSegmentMessage | RemoveMessage;\n\n/** Finite (bounded) operational modes of the actor. */\nexport type SourceBufferActorStatus = 'idle' | 'updating' | 'destroyed';\n\n/** Non-finite (extended) data managed by the actor — the XState \"context\". */\nexport interface SourceBufferActorContext {\n initTrackId?: string | undefined;\n segments: Array<Pick<Segment, 'id' | 'startTime' | 'duration'> & { trackId: Track['id']; trackBandwidth?: number }>;\n bufferedRanges: BufferedRange[];\n}\n\n/** Complete snapshot of a SourceBufferActor. */\nexport type SourceBufferActorSnapshot = ActorSnapshot<SourceBufferActorStatus, SourceBufferActorContext>;\n\n/**\n * Thrown when a message is sent to the actor in a state that does not\n * accept messages (currently: 'updating').\n */\nexport class SourceBufferActorError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'SourceBufferActorError';\n }\n}\n\n/** SourceBuffer actor: queues operations, owns its snapshot. */\nexport interface SourceBufferActor extends Actor<SourceBufferActorStatus, SourceBufferActorContext> {\n send(message: SourceBufferMessage, signal: AbortSignal): Promise<void>;\n batch(messages: SourceBufferMessage[], signal: AbortSignal): Promise<void>;\n}\n\n// =============================================================================\n// Helpers\n// =============================================================================\n\nfunction snapshotBuffered(buffered: TimeRanges): BufferedRange[] {\n const ranges: BufferedRange[] = [];\n for (let i = 0; i < buffered.length; i++) {\n ranges.push({ start: buffered.start(i), end: buffered.end(i) });\n }\n return ranges;\n}\n\n// =============================================================================\n// Message task factories\n// =============================================================================\n\n// Context is read lazily via getCtx at task execution time — not at creation\n// time — so each task always operates on the most recent context regardless of\n// when it was scheduled.\n\ninterface MessageTaskOptions {\n signal: AbortSignal;\n getCtx: () => SourceBufferActorContext;\n sourceBuffer: SourceBuffer;\n}\n\nfunction appendInitTask(\n message: AppendInitMessage,\n { signal, getCtx, sourceBuffer }: MessageTaskOptions\n): Task<SourceBufferActorContext> {\n return new Task(\n async (taskSignal) => {\n const ctx = getCtx();\n if (taskSignal.aborted) return ctx;\n await appendSegment(sourceBuffer, message.data);\n // No abort check here: the physical SourceBuffer has been modified, so\n // the model must be updated to match regardless of signal state.\n return { ...ctx, initTrackId: message.meta.trackId };\n },\n { signal }\n );\n}\n\nfunction appendSegmentTask(\n message: AppendSegmentMessage,\n { signal, getCtx, sourceBuffer }: MessageTaskOptions\n): Task<SourceBufferActorContext> {\n return new Task(\n async (taskSignal) => {\n const ctx = getCtx();\n if (taskSignal.aborted) return ctx;\n await appendSegment(sourceBuffer, message.data);\n // No abort check here: the physical SourceBuffer has been modified, so\n // the model must be updated to match regardless of signal state.\n const { meta } = message;\n // Remove any existing entry at the same start time (same \"slot\" in the\n // timeline), then record the new segment. Assumes time-aligned segments\n // across playlists. The epsilon guards against floating-point drift in\n // parsed timestamps.\n const EPSILON = 0.0001;\n const filtered = ctx.segments.filter((s) => Math.abs(s.startTime - meta.startTime) >= EPSILON);\n return {\n ...ctx,\n segments: [\n ...filtered,\n {\n id: meta.id,\n startTime: meta.startTime,\n duration: meta.duration,\n trackId: meta.trackId,\n ...(meta.trackBandwidth !== undefined && { trackBandwidth: meta.trackBandwidth }),\n },\n ],\n bufferedRanges: snapshotBuffered(sourceBuffer.buffered),\n };\n },\n { signal }\n );\n}\n\nfunction removeTask(\n message: RemoveMessage,\n { signal, getCtx, sourceBuffer }: MessageTaskOptions\n): Task<SourceBufferActorContext> {\n return new Task(\n async (taskSignal) => {\n const ctx = getCtx();\n if (taskSignal.aborted) return ctx;\n await flushBuffer(sourceBuffer, message.start, message.end);\n // No abort check here: the physical SourceBuffer has been modified, so\n // the model must be updated to match regardless of signal state.\n //\n // Use the post-flush buffered ranges as ground truth. A segment is kept\n // in the model only if its midpoint falls within a buffered range.\n // Midpoint-based membership handles flush boundaries that don't align\n // exactly with segment edges without over-removing adjacent segments.\n const bufferedRanges = snapshotBuffered(sourceBuffer.buffered);\n const filtered = ctx.segments.filter((s) => {\n const midpoint = s.startTime + s.duration / 2;\n return bufferedRanges.some((r) => midpoint >= r.start && midpoint < r.end);\n });\n return { ...ctx, segments: filtered, bufferedRanges };\n },\n { signal }\n );\n}\n\ntype MessageTaskFactory<T extends SourceBufferMessage> = (\n message: T,\n options: MessageTaskOptions\n) => Task<SourceBufferActorContext>;\n\nconst messageTaskFactories = {\n 'append-init': appendInitTask,\n 'append-segment': appendSegmentTask,\n remove: removeTask,\n} satisfies { [K in SourceBufferMessage['type']]: MessageTaskFactory<Extract<SourceBufferMessage, { type: K }>> };\n\nfunction messageToTask(message: SourceBufferMessage, options: MessageTaskOptions): Task<SourceBufferActorContext> {\n const factory = messageTaskFactories[message.type] as MessageTaskFactory<typeof message>;\n return factory(message, options);\n}\n\n// =============================================================================\n// Implementation\n// =============================================================================\n\nexport function createSourceBufferActor(\n sourceBuffer: SourceBuffer,\n initialContext?: Partial<SourceBufferActorContext>\n): SourceBufferActor {\n const state = createState<SourceBufferActorSnapshot>({\n status: 'idle',\n context: { segments: [], bufferedRanges: [], initTrackId: undefined, ...initialContext },\n });\n\n const runner = new SerialRunner();\n\n // Applies the completed context atomically with the idle transition and\n // flushes synchronously so consumers observe the final snapshot when their\n // awaited Promise resolves.\n // If the actor was destroyed while the operation was in flight, preserve\n // 'destroyed' — do not regress to 'idle'.\n function applyResult(newContext: SourceBufferActorContext): void {\n const status = state.current.status === 'destroyed' ? 'destroyed' : 'idle';\n state.patch({ status, context: newContext });\n state.flush();\n }\n\n function handleError(e: unknown): never {\n // TODO: QuotaExceededError and other SourceBuffer errors leave the physical\n // buffer in an unknown partial state while context goes unchanged. A future\n // improvement should detect QuotaExceededError specifically and use total\n // bytes-in-buffer as a heuristic to identify the effective buffer capacity,\n // enabling targeted flush-and-retry rather than silent model drift.\n const status = state.current.status === 'destroyed' ? 'destroyed' : 'idle';\n state.patch({ status });\n state.flush();\n throw e;\n }\n\n return {\n get snapshot(): SourceBufferActorSnapshot {\n return state.current;\n },\n\n subscribe(listener: (snapshot: SourceBufferActorSnapshot) => void): () => void {\n return state.subscribe(listener);\n },\n\n send(message: SourceBufferMessage, signal: AbortSignal): Promise<void> {\n if (state.current.status !== 'idle') {\n return Promise.reject(new SourceBufferActorError(`send() called while actor is ${state.current.status}`));\n }\n\n // Transition synchronously so any subsequent send/batch within the same\n // tick is rejected — the actor is now committed to this operation.\n state.patch({ status: 'updating' });\n\n const task = messageToTask(message, { signal, getCtx: () => state.current.context, sourceBuffer });\n\n return runner.schedule(task).then(applyResult).catch(handleError);\n },\n\n batch(messages: SourceBufferMessage[], signal: AbortSignal): Promise<void> {\n if (state.current.status !== 'idle') {\n return Promise.reject(new SourceBufferActorError(`batch() called while actor is ${state.current.status}`));\n }\n\n if (messages.length === 0) return Promise.resolve();\n\n // Transition synchronously — the entire batch is one 'updating' period.\n state.patch({ status: 'updating' });\n\n // Each message is its own Task on the runner, executed in submission order.\n // workingCtx threads the result of each task into the next without\n // patching shared state between steps — context is only written to state\n // atomically when the last task completes.\n //\n // workingCtx is captured here (synchronously after status → 'updating'),\n // so it reflects the current context at the moment the batch was accepted.\n // This is correct: status is now 'updating' so no other sender can modify\n // context between this line and the first task executing.\n //\n // NOTE: if an intermediate task fails (e.g. SourceBuffer error event),\n // workingCtx is not updated for that step and subsequent tasks in the\n // batch will operate on a stale context. This is an edge case — happy-path\n // appends do not fail — but worth revisiting if MSE error recovery lands.\n let workingCtx = state.current.context;\n\n for (const message of messages.slice(0, -1)) {\n const task = messageToTask(message, { signal, getCtx: () => workingCtx, sourceBuffer });\n const result = runner.schedule(task);\n result.then((newCtx) => {\n workingCtx = newCtx;\n });\n }\n\n const lastTask = messageToTask(messages[messages.length - 1]!, {\n signal,\n getCtx: () => workingCtx,\n sourceBuffer,\n });\n return runner.schedule(lastTask).then(applyResult).catch(handleError);\n },\n\n destroy(): void {\n state.patch({ status: 'destroyed' });\n state.flush();\n runner.destroy();\n },\n };\n}\n","import { isUndefined } from '@videojs/utils/predicate';\nimport { combineLatest } from '../../core/reactive/combine-latest';\nimport type { WritableState } from '../../core/state/create-state';\nimport type { Presentation, ResolvedTrack } from '../../core/types';\nimport { isResolvedTrack } from '../../core/types';\nimport { BufferKeyByType, getSelectedTrack, type TrackSelectionState } from '../../core/utils/track-selection';\nimport { createSourceBuffer } from '../media/mediasource-setup';\nimport { createSourceBufferActor, type SourceBufferActor } from '../media/source-buffer-actor';\n\n/** Map track type to SourceBufferActor owner property key. */\nconst ActorKeyByType = {\n video: 'videoBufferActor',\n audio: 'audioBufferActor',\n} as const;\n\n/**\n * Media track type for SourceBuffer setup.\n * Text tracks are excluded as they don't use MSE SourceBuffers.\n */\nexport type MediaTrackType = 'video' | 'audio';\n\n/**\n * Setup SourceBuffer task (module-level, pure).\n * Creates SourceBuffer for resolved track and waits a frame before completing.\n */\nconst setupSourceBufferTask = async <T extends MediaTrackType>(\n { currentState, currentOwners }: { currentState: SourceBufferState; currentOwners: SourceBufferOwners },\n context: { owners: WritableState<SourceBufferOwners>; config: SourceBufferConfig<T> }\n): Promise<void> => {\n // Wait for track to be resolved with codecs before creating SourceBuffer\n const track = getSelectedTrack(currentState, context.config.type);\n if (!track || !isResolvedTrack(track)) return;\n if (!track.codecs || track.codecs.length === 0) return;\n\n const mimeCodec = buildMimeCodec(track);\n\n // Create SourceBuffer and its actor together — single owners.patch so\n // subscribers see both arrive at the same time with no intermediate state.\n const buffer = createSourceBuffer(currentOwners.mediaSource!, mimeCodec);\n const actor = createSourceBufferActor(buffer);\n\n const bufferKey = BufferKeyByType[context.config.type];\n const actorKey = ActorKeyByType[context.config.type];\n context.owners.patch({ [bufferKey]: buffer, [actorKey]: actor });\n\n // Wait a frame to allow async state updates to flush\n await new Promise((resolve) => requestAnimationFrame(resolve));\n};\n\n/**\n * State shape for SourceBuffer setup.\n */\nexport interface SourceBufferState extends TrackSelectionState {\n presentation?: Presentation;\n selectedVideoTrackId?: string;\n selectedAudioTrackId?: string;\n}\n\n/**\n * Owners shape for SourceBuffer setup.\n */\nexport interface SourceBufferOwners {\n mediaSource?: MediaSource;\n videoBuffer?: SourceBuffer;\n audioBuffer?: SourceBuffer;\n videoBufferActor?: SourceBufferActor;\n audioBufferActor?: SourceBufferActor;\n}\n\n/**\n * Build MIME codec string from track metadata.\n *\n * @param track - Resolved track with mimeType and codecs\n * @returns MIME codec string (e.g., 'video/mp4; codecs=\"avc1.42E01E,mp4a.40.2\"')\n *\n * @example\n * buildMimeCodec({ mimeType: 'video/mp4', codecs: ['avc1.42E01E'] })\n * // => 'video/mp4; codecs=\"avc1.42E01E\"'\n */\nexport function buildMimeCodec(track: ResolvedTrack): string {\n const codecString = track.codecs?.join(',') ?? '';\n return `${track.mimeType}; codecs=\"${codecString}\"`;\n}\n\n/**\n * Check if we can setup SourceBuffer for track type.\n *\n * Requires:\n * - MediaSource exists in owners\n * - Track is selected\n *\n * Note: We don't check mediaSource.readyState because owners holds references\n * to mutable objects. Changes to properties on those objects won't trigger\n * observations. Instead, setupMediaSource only patches owners.mediaSource after\n * it's already open, so if it exists in owners, it's ready to use.\n *\n * Note: Track does not need to be resolved yet. The orchestration will wait\n * for the track to be resolved (via resolveTrack) before creating the SourceBuffer.\n */\nexport function canSetupBuffer(state: SourceBufferState, owners: SourceBufferOwners, type: MediaTrackType): boolean {\n // MediaSource exists (already open - see note above)\n if (!owners.mediaSource) {\n return false;\n }\n\n // Need selected track (doesn't need to be resolved yet)\n const track = getSelectedTrack(state, type);\n if (!track) {\n return false;\n }\n\n return true;\n}\n\n/**\n * Check if we should create SourceBuffer (not already created).\n */\nexport function shouldSetupBuffer(owners: SourceBufferOwners, type: MediaTrackType): boolean {\n const bufferKey = BufferKeyByType[type];\n return isUndefined(owners[bufferKey]);\n}\n\n/**\n * Configuration for SourceBuffer setup.\n */\nexport interface SourceBufferConfig<T extends MediaTrackType = MediaTrackType> {\n type: T;\n}\n\n/**\n * Setup SourceBuffer orchestration.\n *\n * Triggers when:\n * - MediaSource exists and is in 'open' state\n * - Track is selected (same condition as resolveTrack)\n *\n * Creates SourceBuffer when track becomes resolved with codecs.\n * This allows setupSourceBuffer to run in parallel with resolveTrack.\n *\n * Note: Text tracks don't use SourceBuffers and should be handled separately.\n *\n * Generic over track type - create one orchestration per track type:\n * @example\n * const videoCleanup = setupSourceBuffer({ state, owners }, { type: 'video' });\n * const audioCleanup = setupSourceBuffer({ state, owners }, { type: 'audio' });\n */\nexport function setupSourceBuffer<T extends MediaTrackType>(\n {\n state,\n owners,\n }: {\n state: WritableState<SourceBufferState>;\n owners: WritableState<SourceBufferOwners>;\n },\n config: SourceBufferConfig<T>\n): () => void {\n let currentTask: Promise<void> | null = null;\n\n const cleanup = combineLatest([state, owners]).subscribe(\n async ([currentState, currentOwners]: [SourceBufferState, SourceBufferOwners]) => {\n // Check orchestration conditions (track selected, MediaSource open)\n if (currentTask) return; // Task already in progress\n if (!canSetupBuffer(currentState, currentOwners, config.type) || !shouldSetupBuffer(currentOwners, config.type))\n return;\n\n // Invoke task (no abort needed - synchronous SourceBuffer creation + frame wait)\n currentTask = setupSourceBufferTask({ currentState, currentOwners }, { owners, config });\n\n try {\n await currentTask;\n } finally {\n // Cleanup orchestration state\n currentTask = null;\n }\n }\n );\n\n return cleanup;\n}\n","import { combineLatest } from '../../core/reactive/combine-latest';\nimport type { WritableState } from '../../core/state/create-state';\nimport type { PartiallyResolvedTextTrack, Presentation } from '../../core/types';\n\n/**\n * State shape for text track setup.\n */\nexport interface TextTrackState {\n presentation?: Presentation | undefined;\n selectedTextTrackId?: string | undefined;\n}\n\n/**\n * Owners shape for text track setup.\n */\nexport interface TextTrackOwners {\n mediaElement?: HTMLMediaElement | undefined;\n textTracks?: Map<string, HTMLTrackElement>;\n}\n\n/**\n * Get all text tracks from presentation.\n */\nfunction getTextTracks(presentation?: Presentation): PartiallyResolvedTextTrack[] {\n if (!presentation?.selectionSets) return [];\n\n const textSet = presentation.selectionSets.find((set) => set.type === 'text');\n if (!textSet?.switchingSets?.[0]?.tracks) return [];\n\n return textSet.switchingSets[0].tracks as PartiallyResolvedTextTrack[];\n}\n\n/**\n * Check if we can setup text tracks.\n *\n * Requires:\n * - mediaElement exists\n * - presentation has text tracks to setup\n */\nexport function canSetupTextTracks(state: TextTrackState, owners: TextTrackOwners): boolean {\n return !!owners.mediaElement && !!getTextTracks(state.presentation).length;\n}\n\n/**\n * Check if we should setup text tracks (not already set up).\n */\nexport function shouldSetupTextTracks(owners: TextTrackOwners): boolean {\n return !owners.textTracks;\n}\n\n/**\n * Create a track element for a text track.\n *\n * Note: We use DOM <track> elements instead of the TextTrack JS API\n * because there's no way to remove TextTracks added via addTextTrack().\n */\nfunction createTrackElement(track: PartiallyResolvedTextTrack): HTMLTrackElement {\n const trackElement = document.createElement('track');\n\n trackElement.id = track.id;\n trackElement.kind = track.kind;\n trackElement.label = track.label;\n\n if (track.language) {\n trackElement.srclang = track.language;\n }\n\n if (track.default) {\n trackElement.default = true;\n }\n\n // Set src to track URL\n trackElement.src = track.url;\n\n return trackElement;\n}\n\n/**\n * Setup text tracks orchestration.\n *\n * Triggers when:\n * - mediaElement exists\n * - presentation is resolved (has text tracks)\n *\n * Creates <track> elements for all text tracks and adds them as children\n * to the media element. This allows the browser's native text track rendering.\n *\n * Note: Uses DOM track elements instead of TextTrack API because tracks\n * added via addTextTrack() cannot be removed.\n *\n * @example\n * const cleanup = setupTextTracks({ state, owners });\n */\nexport function setupTextTracks({\n state,\n owners,\n}: {\n state: WritableState<TextTrackState>;\n owners: WritableState<TextTrackOwners>;\n}): () => void {\n let hasSetup = false;\n let createdTracks: HTMLTrackElement[] = [];\n\n const unsubscribe = combineLatest([state, owners]).subscribe(([s, o]: [TextTrackState, TextTrackOwners]) => {\n // Check orchestration conditions\n if (hasSetup) return; // Only run once\n if (!canSetupTextTracks(s, o) || !shouldSetupTextTracks(o)) return;\n\n // Mark as setup before doing any work to prevent re-entry\n hasSetup = true;\n\n const textTracks = getTextTracks(s.presentation);\n if (textTracks.length === 0) return;\n\n const trackMap = new Map<string, HTMLTrackElement>();\n\n // Create track elements for all text tracks\n for (const track of textTracks) {\n const trackElement = createTrackElement(track);\n o.mediaElement!.appendChild(trackElement);\n trackMap.set(track.id, trackElement);\n createdTracks.push(trackElement);\n }\n\n // Update owners with track element map\n owners.patch({ textTracks: trackMap });\n });\n\n // Return cleanup function\n return () => {\n // Remove all created track elements\n for (const trackElement of createdTracks) {\n trackElement.remove();\n }\n createdTracks = [];\n unsubscribe();\n };\n}\n","import { listen } from '@videojs/utils/dom';\nimport type { WritableState } from '../../core/state/create-state';\nimport type { TextTrackBufferState } from './load-text-track-cues';\n\n/**\n * State shape for DOM-driven text track selection.\n */\nexport interface SelectedTextTrackFromDomState {\n selectedTextTrackId?: string | undefined;\n textBufferState?: TextTrackBufferState | undefined;\n}\n\n/**\n * Owners shape for DOM-driven text track selection.\n */\nexport interface SelectedTextTrackFromDomOwners {\n mediaElement?: HTMLMediaElement | undefined;\n}\n\n/**\n * Sync selectedTextTrackId from DOM text track mode changes.\n *\n * Listens to the `change` event on `media.textTracks` and updates\n * `selectedTextTrackId` when external code (e.g. the captions button via\n * `toggleSubtitles()`) changes a subtitle/caption track mode to 'showing'.\n *\n * This bridges the core store's `toggleSubtitles()` with SPF's reactive text\n * track pipeline (`syncTextTrackModes`, `loadTextTrackCues`). Without this\n * bridge, direct DOM mode changes would be immediately overridden by\n * `syncTextTrackModes` on the next SPF state update.\n *\n * When a subtitle/caption track's mode is 'showing', its DOM `id` — which\n * matches the SPF track ID set by `setupTextTracks` — is written to\n * `selectedTextTrackId`. When no subtitle/caption track is 'showing',\n * `selectedTextTrackId` is cleared along with the deselected track's\n * `textBufferState` entry — setting mode to 'disabled' clears native cues from\n * the track element, so the buffer must be reset to re-fetch cues on re-enable.\n *\n * @example\n * const cleanup = syncSelectedTextTrackFromDom({ state, owners });\n */\nexport function syncSelectedTextTrackFromDom({\n state,\n owners,\n}: {\n state: WritableState<SelectedTextTrackFromDomState>;\n owners: WritableState<SelectedTextTrackFromDomOwners>;\n}): () => void {\n let lastMediaElement: HTMLMediaElement | undefined;\n let removeListener: (() => void) | null = null;\n\n const unsubscribe = owners.subscribe((currentOwners) => {\n const { mediaElement } = currentOwners;\n\n if (mediaElement === lastMediaElement) return;\n\n removeListener?.();\n removeListener = null;\n lastMediaElement = mediaElement;\n\n if (!mediaElement) return;\n\n const sync = () => {\n const showingTrack = Array.from(mediaElement.textTracks).find(\n (t) => t.mode === 'showing' && (t.kind === 'subtitles' || t.kind === 'captions')\n );\n\n // showingTrack.id is set from the SPF presentation track ID by setupTextTracks.\n // Fall back to undefined for empty-string IDs (non-SPF-managed tracks).\n const newId = showingTrack?.id || undefined;\n const current = state.current;\n\n // Guard against redundant patches — e.g. syncTextTrackModes confirming the\n // current selection, which would otherwise create a feedback loop.\n if (current.selectedTextTrackId === newId) return;\n\n if (newId) {\n state.patch({ selectedTextTrackId: newId });\n } else {\n // When deselecting, clear the textBufferState entry for the previous track.\n // Setting mode to 'disabled' (as toggleSubtitles() does) clears native cues\n // from the track element, so the buffer must be reset to allow re-fetching\n // on re-enable.\n const prevId = current.selectedTextTrackId;\n if (prevId && current.textBufferState?.[prevId]) {\n const next = { ...current.textBufferState };\n delete next[prevId];\n state.patch({ selectedTextTrackId: undefined, textBufferState: next });\n } else {\n state.patch({ selectedTextTrackId: undefined });\n }\n }\n };\n\n removeListener = listen(mediaElement.textTracks, 'change', sync);\n });\n\n return () => {\n removeListener?.();\n unsubscribe();\n };\n}\n","import { combineLatest } from '../../core/reactive/combine-latest';\nimport type { WritableState } from '../../core/state/create-state';\n\n/**\n * State shape for text track mode synchronization.\n */\nexport interface TextTrackModeState {\n selectedTextTrackId?: string | undefined;\n}\n\n/**\n * Owners shape for text track mode synchronization.\n */\nexport interface TextTrackModeOwners {\n textTracks?: Map<string, HTMLTrackElement>;\n}\n\n/**\n * Check if we can sync text track modes.\n *\n * Requires:\n * - textTracks map exists (track elements created)\n */\nexport function canSyncTextTrackModes(owners: TextTrackModeOwners): boolean {\n return !!owners.textTracks && owners.textTracks.size > 0;\n}\n\n/**\n * Sync text track modes orchestration.\n *\n * Manages track element modes based on selectedTextTrackId:\n * - Selected track: mode = \"showing\"\n * - Other tracks: mode = \"hidden\"\n * - No selection: all tracks mode = \"hidden\"\n *\n * Note: Uses \"hidden\" instead of \"disabled\" for non-selected tracks\n * so they remain available in the browser's track menu.\n *\n * @example\n * const cleanup = syncTextTrackModes({ state, owners });\n */\nexport function syncTextTrackModes({\n state,\n owners,\n}: {\n state: WritableState<TextTrackModeState>;\n owners: WritableState<TextTrackModeOwners>;\n}): () => void {\n return combineLatest([state, owners]).subscribe(([s, o]: [TextTrackModeState, TextTrackModeOwners]) => {\n // Check orchestration conditions\n if (!canSyncTextTrackModes(o)) return;\n\n const selectedId = s.selectedTextTrackId;\n\n // Update all track element modes\n for (const [trackId, trackElement] of o.textTracks!) {\n if (trackId === selectedId) {\n // Selected track: show it\n trackElement.track.mode = 'showing';\n } else {\n // Other tracks: hide them (but keep available in menu)\n trackElement.track.mode = 'hidden';\n }\n }\n });\n}\n","import { combineLatest } from '../../core/reactive/combine-latest';\nimport type { WritableState } from '../../core/state/create-state';\nimport type { Presentation } from '../../core/types';\nimport { hasPresentationDuration } from '../../core/types';\n\nexport interface DurationUpdateState {\n presentation?: Presentation;\n}\n\nexport interface DurationUpdateOwners {\n mediaSource?: MediaSource;\n videoSourceBuffer?: SourceBuffer;\n audioSourceBuffer?: SourceBuffer;\n}\n\n/**\n * Check if we can update MediaSource duration (have required data).\n */\nexport function canUpdateDuration(state: DurationUpdateState, owners: DurationUpdateOwners): boolean {\n return !!(owners.mediaSource && state.presentation && hasPresentationDuration(state.presentation));\n}\n\n/**\n * Get the maximum buffered end time across all SourceBuffers.\n */\nexport function getMaxBufferedEnd(owners: DurationUpdateOwners): number {\n let maxEnd = 0;\n\n const buffers = [owners.videoSourceBuffer, owners.audioSourceBuffer].filter(\n (buf): buf is SourceBuffer => buf !== undefined\n );\n\n for (const buffer of buffers) {\n const { buffered } = buffer;\n if (buffered.length > 0) {\n const end = buffered.end(buffered.length - 1);\n if (end > maxEnd) {\n maxEnd = end;\n }\n }\n }\n\n return maxEnd;\n}\n\n/**\n * Check if we should update MediaSource duration (conditions met).\n */\nexport function shouldUpdateDuration(state: DurationUpdateState, owners: DurationUpdateOwners): boolean {\n if (!canUpdateDuration(state, owners)) return false;\n\n const { mediaSource } = owners;\n const { presentation } = state;\n\n // MediaSource must be open\n if (mediaSource!.readyState !== 'open') return false;\n\n const duration = presentation!.duration!;\n\n // Validate duration: finite, positive, not NaN\n if (!Number.isFinite(duration) || Number.isNaN(duration) || duration <= 0) return false;\n\n // Only set duration on initial MediaSource setup, when it hasn't been set yet.\n // A freshly opened MediaSource has duration === NaN. Once set (either by this\n // task or by endOfStreamTask from buffered.end()), we leave it alone — attempting\n // to re-sync a slight drift between mediaSource.duration and presentation.duration\n // races with concurrent appendBuffer() calls from loadSegmentsTask.\n return Number.isNaN(mediaSource!.duration);\n}\n\n/**\n * Wait for all currently-updating SourceBuffers to finish.\n *\n * The MSE spec forbids setting MediaSource.duration while any attached\n * SourceBuffer has updating === true. This defers until all are idle.\n */\nfunction waitForSourceBuffersReady(owners: DurationUpdateOwners): Promise<void> {\n const updating = [owners.videoSourceBuffer, owners.audioSourceBuffer].filter(\n (buf): buf is SourceBuffer => buf?.updating === true\n );\n\n if (updating.length === 0) return Promise.resolve();\n\n return Promise.all(\n updating.map(\n (buf) => new Promise<void>((resolve) => buf.addEventListener('updateend', () => resolve(), { once: true }))\n )\n ).then(() => undefined);\n}\n\n/**\n * Update MediaSource duration when presentation duration becomes available.\n */\nexport function updateDuration({\n state,\n owners,\n}: {\n state: WritableState<DurationUpdateState>;\n owners: WritableState<DurationUpdateOwners>;\n}): () => void {\n let destroyed = false;\n\n const unsubscribe = combineLatest([state, owners]).subscribe(\n async ([currentState, currentOwners]: [DurationUpdateState, DurationUpdateOwners]) => {\n if (!shouldUpdateDuration(currentState, currentOwners)) return;\n\n const { mediaSource } = currentOwners;\n\n // MSE spec: duration cannot be set while any SourceBuffer is updating\n await waitForSourceBuffersReady(currentOwners);\n\n // Re-check after async wait: destroyed, or readyState changed (e.g. endOfStream\n // already called endOfStream(), transitioning 'open' → 'ended').\n if (destroyed || mediaSource!.readyState !== 'open') return;\n\n let duration = currentState.presentation!.duration!;\n\n // Get max buffered end time across all SourceBuffers\n const maxBufferedEnd = getMaxBufferedEnd(currentOwners);\n\n // MSE spec: duration cannot be less than any buffered range\n // If buffered ranges exceed calculated duration, extend to match\n if (maxBufferedEnd > duration) {\n duration = maxBufferedEnd;\n }\n\n // Set duration on MediaSource\n mediaSource!.duration = duration;\n }\n );\n\n return () => {\n destroyed = true;\n unsubscribe();\n };\n}\n","import type { BandwidthState } from '../../core/abr/bandwidth-estimator';\nimport { createEventStream } from '../../core/events/create-event-stream';\nimport { calculatePresentationDuration } from '../../core/features/calculate-presentation-duration';\nimport { switchQuality } from '../../core/features/quality-switching';\nimport {\n type PresentationAction,\n resolvePresentation,\n syncPreloadAttribute,\n} from '../../core/features/resolve-presentation';\nimport { resolveTrack, type TrackResolutionAction } from '../../core/features/resolve-track';\nimport {\n selectAudioTrack,\n selectTextTrack,\n selectVideoTrack,\n type TrackSelectionAction,\n} from '../../core/features/select-tracks';\nimport { createState } from '../../core/state/create-state';\nimport { endOfStream } from '../features/end-of-stream';\nimport { loadSegments } from '../features/load-segments';\nimport type { TextTrackBufferState } from '../features/load-text-track-cues';\nimport { loadTextTrackCues } from '../features/load-text-track-cues';\nimport { setupMediaSource } from '../features/setup-mediasource';\nimport { setupSourceBuffer } from '../features/setup-sourcebuffer';\nimport { setupTextTracks } from '../features/setup-text-tracks';\nimport { syncSelectedTextTrackFromDom } from '../features/sync-selected-text-track-from-dom';\nimport { syncTextTrackModes } from '../features/sync-text-track-modes';\nimport { trackCurrentTime } from '../features/track-current-time';\nimport { trackPlaybackInitiated } from '../features/track-playback-initiated';\nimport { updateDuration } from '../features/update-duration';\nimport type { SourceBufferActor } from '../media/source-buffer-actor';\nimport { destroyVttParser } from '../text/parse-vtt-segment';\n\n/**\n * Union of all action types used by playback engine orchestrations.\n * Includes synthetic @@INITIALIZE@@ event for combineLatest bootstrapping.\n */\nexport type PlaybackEngineAction =\n | PresentationAction\n | TrackResolutionAction\n | TrackSelectionAction\n | { type: '@@INITIALIZE@@' };\n\n/**\n * Configuration for the playback engine.\n */\nexport interface PlaybackEngineConfig {\n /**\n * Initial bandwidth estimate for cold start (bits per second).\n * Default: 1 Mbps (conservative).\n */\n initialBandwidth?: number;\n\n /**\n * Preferred audio language (ISO 639 code, e.g., \"en\", \"es\").\n * If not specified, selects first audio track.\n */\n preferredAudioLanguage?: string;\n\n /**\n * Preferred subtitle language (ISO 639 code, e.g., \"en\", \"es\").\n * If specified, selects matching text track if available.\n */\n preferredSubtitleLanguage?: string;\n\n /**\n * Include FORCED subtitle tracks in selection.\n * Default: false (follows hls.js/http-streaming pattern)\n */\n includeForcedTracks?: boolean;\n\n /**\n * Auto-select DEFAULT track (requires DEFAULT=YES + AUTOSELECT=YES in HLS).\n * Default: false (user opt-in, matches hls.js/http-streaming)\n */\n enableDefaultTrack?: boolean;\n}\n\n/**\n * Combined state shape for the playback engine.\n * Includes all state needed by orchestrations.\n */\nexport interface PlaybackEngineState {\n // Presentation state\n presentation?: any;\n preload?: string;\n\n // Track selection state\n selectedVideoTrackId?: string;\n selectedAudioTrackId?: string;\n // NOTE: Text Tracks (subtitles/ccs) can be unselected\n selectedTextTrackId?: string;\n\n // Bandwidth estimation state\n bandwidthState?: BandwidthState;\n\n // ABR control — set to true to suppress automatic quality switching (manual selection mode).\n // TODO: replace with separate manualVideoTrackId / abrVideoTrackId fields so the two\n // concerns don't share a field; see quality-switching.ts for the full design note.\n abrDisabled?: boolean;\n\n // Text track buffer state (tracks loaded VTT segments per text track ID)\n textBufferState?: TextTrackBufferState;\n\n // Current playback position (mirrored from mediaElement via trackCurrentTime)\n currentTime?: number;\n\n // True once the user has initiated playback (enables segment loading regardless of preload)\n playbackInitiated?: boolean;\n}\n\n/**\n * Combined owners shape for the playback engine.\n * Includes all mutable platform objects.\n */\nexport interface PlaybackEngineOwners {\n // Media element\n mediaElement?: HTMLMediaElement | undefined;\n\n // MediaSource\n mediaSource?: MediaSource;\n\n // SourceBuffers and their actors (created together by setupSourceBuffer)\n videoBuffer?: SourceBuffer;\n audioBuffer?: SourceBuffer;\n videoBufferActor?: SourceBufferActor;\n audioBufferActor?: SourceBufferActor;\n\n // Text tracks (track elements by ID)\n textTracks?: Map<string, HTMLTrackElement>;\n}\n\n/**\n * Playback engine instance.\n */\nexport interface PlaybackEngine {\n /**\n * Reactive state (for inspection/testing).\n */\n state: ReturnType<typeof createState<PlaybackEngineState>>;\n\n /**\n * Mutable owners (for inspection/testing).\n */\n owners: ReturnType<typeof createState<PlaybackEngineOwners>>;\n\n /**\n * Shared event stream (for inspection/testing/triggering events).\n */\n events: ReturnType<typeof createEventStream<PlaybackEngineAction>>;\n\n /**\n * Cleanup function to destroy all orchestrations.\n */\n destroy: () => void;\n}\n\n/**\n * Create a POC playback engine.\n *\n * Wires together all orchestrations to create a reactive playback pipeline:\n * 1. Resolve presentation (multivariant playlist)\n * 2. Select initial video and audio tracks\n * 3. Resolve selected tracks (media playlists)\n * 4. Setup MediaSource\n * 5. Setup SourceBuffers for video and audio\n *\n * Note: This is a POC - does not yet load/append segments.\n *\n * @param config - Playback engine configuration\n * @returns Playback engine instance with state, owners, and destroy function\n *\n * @example\n * const engine = createPlaybackEngine({\n * initialBandwidth: 2_000_000,\n * preferredAudioLanguage: 'en',\n * });\n *\n * // Initialize by patching state and owners\n * engine.owners.patch({ mediaElement: document.querySelector('video') });\n * engine.state.patch({\n * presentation: { url: 'https://example.com/playlist.m3u8' },\n * preload: 'auto',\n * });\n *\n * // Inspect state\n * console.log(engine.state.current);\n *\n * // Cleanup\n * engine.destroy();\n */\nexport function createPlaybackEngine(config: PlaybackEngineConfig = {}): PlaybackEngine {\n // Create reactive state and owners (initially empty)\n const state = createState<PlaybackEngineState>({\n bandwidthState: {\n fastEstimate: 0,\n fastTotalWeight: 0,\n slowEstimate: 0,\n slowTotalWeight: 0,\n bytesSampled: 0,\n },\n });\n const owners = createState<PlaybackEngineOwners>({});\n\n // Create single shared event stream for all orchestrations\n const events = createEventStream<PlaybackEngineAction>();\n\n // Wire up orchestrations (all share single event stream)\n // Note: @ts-expect-error needed due to EventStream invariance - each orchestration expects\n // specific event types, but shared stream has union of all types. Proper fix would\n // require making EventStream covariant or refactoring event system.\n const cleanups = [\n // 0a. Sync preload attribute from mediaElement → state.preload\n // Only re-reads when the mediaElement reference changes (lastMediaElement guard).\n // Normalises '' (absent attribute) to 'auto' (browser default).\n // @ts-expect-error - WritableState type variance\n syncPreloadAttribute(state, owners),\n\n // 0b. Bridge media element play event → state.playbackInitiated + event stream\n // Enables preload=\"none\"/\"metadata\" resolution via native controls / element.play()\n // @ts-expect-error - EventStream type variance\n trackPlaybackInitiated({ state, owners, events }),\n\n // 1. Resolve presentation (URL already in state)\n // @ts-expect-error - EventStream type variance\n resolvePresentation({ state, events }),\n\n // 2. Select initial tracks (when presentation loads)\n selectVideoTrack(\n // @ts-expect-error - Owners and EventStream type compatibility\n { state, owners, events },\n {\n type: 'video',\n ...(config.initialBandwidth !== undefined && { initialBandwidth: config.initialBandwidth }),\n }\n ),\n selectAudioTrack(\n // @ts-expect-error - Owners and EventStream type compatibility\n { state, owners, events },\n {\n type: 'audio',\n ...(config.preferredAudioLanguage !== undefined && { preferredAudioLanguage: config.preferredAudioLanguage }),\n }\n ),\n selectTextTrack(\n // @ts-expect-error - Owners and EventStream type compatibility\n { state, owners, events },\n {\n type: 'text',\n ...(config.preferredSubtitleLanguage !== undefined && {\n preferredSubtitleLanguage: config.preferredSubtitleLanguage,\n }),\n ...(config.includeForcedTracks !== undefined && { includeForcedTracks: config.includeForcedTracks }),\n ...(config.enableDefaultTrack !== undefined && { enableDefaultTrack: config.enableDefaultTrack }),\n }\n ),\n\n // 3. Resolve selected tracks (fetch media playlists)\n // @ts-expect-error - EventStream type variance\n resolveTrack({ state, events }, { type: 'video' as const }),\n // @ts-expect-error - EventStream type variance\n resolveTrack({ state, events }, { type: 'audio' as const }),\n // @ts-expect-error - EventStream type variance\n resolveTrack({ state, events }, { type: 'text' as const }),\n\n // 3.5. Calculate presentation duration from resolved tracks\n calculatePresentationDuration({ state }),\n\n // 4. Setup MediaSource (when presentation loaded)\n setupMediaSource({ state, owners }),\n\n // 4.5. Update MediaSource duration (when presentation duration available)\n updateDuration({ state, owners }),\n\n // 5. Setup SourceBuffers (when MediaSource ready and tracks resolved)\n setupSourceBuffer({ state, owners }, { type: 'video' }),\n setupSourceBuffer({ state, owners }, { type: 'audio' }),\n\n // 5.5. Track currentTime from mediaElement (feeds forward buffer management)\n //\n // NOTE: SourceBufferActor wiring is intentionally absent here in Phase 1.\n //\n // Attempting to wire actors into the engine at this layer revealed a\n // brittleness in the current architecture: every patch to `owners` —\n // regardless of which field changed — wakes up ALL combineLatest subscribers\n // (loadSegments, endOfStream, etc.). Storing actor references in owners\n // caused loadSegments to re-evaluate mid-task, store a spurious pending\n // state, and run a second loading cycle on completion.\n //\n // This means any future feature that needs to introduce new reactive state\n // alongside SourceBuffers faces the same hazard. The right fix is for\n // loadSegments (and other orchestrations) to route their MSE operations\n // through the actor directly, at which point the actor lifecycle is\n // co-located with its consumer rather than managed centrally here.\n //\n // Actor wiring will be introduced in Phase 2 as part of the loadSegments\n // refactor. See .claude/plans/spf/buffer-state-shadow-actual-model.md.\n trackCurrentTime({ state, owners }),\n\n // 5.75. ABR quality switching (reacts to bandwidth samples from loadSegments)\n switchQuality({ state }),\n\n // 6. Load segments (when SourceBuffer ready and track resolved)\n loadSegments({ state, owners }, { type: 'video' }),\n loadSegments({ state, owners }, { type: 'audio' }),\n\n // 6.5. Signal end of stream when all segments loaded\n endOfStream({ state, owners }),\n\n // 7. Setup text tracks (when mediaElement and presentation ready)\n setupTextTracks({ state, owners }),\n\n // 8. Sync text track modes (when track selected and track elements created)\n syncTextTrackModes({ state, owners }),\n\n // 8.5. Bridge DOM text track mode changes → selectedTextTrackId\n // Detects when external code (e.g. captions button via toggleSubtitles())\n // sets a subtitle/caption track to 'showing' and reflects that into SPF\n // state, which in turn drives loadTextTrackCues.\n syncSelectedTextTrackFromDom({ state, owners }),\n\n // 9. Load text track cues (when track resolved and mode set)\n loadTextTrackCues({ state, owners }),\n ];\n\n // Dispatch synthetic initialize event to satisfy combineLatest\n // (combineLatest waits for all sources to emit before triggering listeners)\n events.dispatch({ type: '@@INITIALIZE@@' });\n\n // Return engine instance\n return {\n state,\n owners,\n events,\n destroy: () => {\n cleanups.forEach((cleanup) => cleanup());\n destroyVttParser();\n },\n };\n}\n","import type { PlaybackEngineConfig } from './engine';\nimport { createPlaybackEngine, type PlaybackEngine } from './engine';\n\n/**\n * HTMLMediaElement-compatible adapter for the SPF playback engine.\n *\n * Implements the src/play() contract per the WHATWG HTML spec so that SPF can\n * be used anywhere a media element API is expected.\n *\n * A new engine is created on every src assignment — this fully tears down all\n * state, SourceBuffers, and in-flight requests from the previous source before\n * the next one begins. The media element reference is preserved across src\n * changes and re-applied to the new engine automatically.\n *\n * @example\n * const media = new SpfMedia({ preferredAudioLanguage: 'en' });\n * media.attach(document.querySelector('video'));\n * media.src = 'https://stream.mux.com/abc123.m3u8';\n *\n * // Change source — old engine is destroyed, new one starts clean:\n * media.src = 'https://stream.mux.com/xyz456.m3u8';\n *\n * // Explicit teardown:\n * media.destroy();\n */\nexport class SpfMedia {\n #engine: PlaybackEngine;\n #config: PlaybackEngineConfig;\n\n /** Pending loadstart listener from a deferred play() retry, if any. */\n #loadstartListener: (() => void) | null = null;\n\n constructor(config: PlaybackEngineConfig = {}) {\n this.#config = config;\n this.#engine = createPlaybackEngine(config);\n }\n\n get engine(): PlaybackEngine {\n return this.#engine;\n }\n\n // ---------------------------------------------------------------------------\n // Media element lifecycle\n // ---------------------------------------------------------------------------\n\n attach(mediaElement: HTMLMediaElement): void {\n this.#engine.owners.patch({ mediaElement });\n }\n\n detach(): void {\n this.#cancelPendingPlay();\n this.#engine.owners.patch({ mediaElement: undefined });\n }\n\n destroy(): void {\n this.#cancelPendingPlay();\n this.#engine.destroy();\n }\n\n // ---------------------------------------------------------------------------\n // src — synchronous IDL attribute (WHATWG §4.8.11.2)\n // Each assignment destroys the current engine and starts a fresh one, exactly\n // as the browser's load algorithm resets all media element state on src change.\n // ---------------------------------------------------------------------------\n\n get src(): string {\n return this.#engine.state.current.presentation?.url ?? '';\n }\n\n set src(value: string) {\n const prevMediaElement = this.#engine.owners.current.mediaElement;\n\n this.#cancelPendingPlay();\n this.#engine.destroy();\n this.#engine = createPlaybackEngine(this.#config);\n\n if (prevMediaElement) {\n this.#engine.owners.patch({ mediaElement: prevMediaElement });\n }\n\n if (value) {\n this.#engine.state.patch({ presentation: { url: value } });\n }\n }\n\n // ---------------------------------------------------------------------------\n // play() — WHATWG §4.8.11.8\n // Delegates to the attached media element's native play().\n // ---------------------------------------------------------------------------\n\n play(): Promise<void> {\n const { mediaElement } = this.#engine.owners.current;\n if (!mediaElement) {\n return Promise.reject(new Error('SpfMedia: no media element attached'));\n }\n\n // Signal play intent — enables loading even with preload=\"none\"\n this.#engine.state.patch({ playbackInitiated: true });\n\n return mediaElement.play().catch((err: unknown) => {\n // If we have a pending HLS source, the rejection may be because MSE\n // hasn't attached a blob URL yet. Wait for loadstart (src assigned\n // by MSE setup) and retry once.\n if (this.src) {\n return new Promise<void>((resolve, reject) => {\n const listener = () => {\n this.#loadstartListener = null;\n mediaElement.play().then(resolve, reject);\n };\n this.#loadstartListener = listener;\n mediaElement.addEventListener('loadstart', listener, { once: true });\n });\n }\n throw err;\n });\n }\n\n // ---------------------------------------------------------------------------\n // Private\n // ---------------------------------------------------------------------------\n\n #cancelPendingPlay(): void {\n if (!this.#loadstartListener) return;\n const { mediaElement } = this.#engine.owners.current;\n mediaElement?.removeEventListener('loadstart', this.#loadstartListener);\n this.#loadstartListener = null;\n }\n}\n"],"mappings":";;;;;;;;;AAUA,MAAM,eAAe,OAAO,qBAAqB;;;;AA0FjD,SAAS,gBAAmB,GAAM,GAAe;AAC/C,QAAO,OAAO,GAAG,GAAG,EAAE;;;;;AAUxB,IAAM,iBAAN,MAAoD;CAClD,CAAC,gBAAgB;CAEjB;CACA,WAAqB;CACrB,gBAAgB;CAChB;CAGA,6BAAa,IAAI,KAAuB;CAGxC,qCAAqB,IAAI,KAAgC;CAEzD,YAAY,SAAY,QAAyB;AAC/C,QAAKA,UAAW,OAAO,YAAY,YAAY,YAAY,OAAQ,EAAE,GAAG,SAAS,GAAS;AAC1F,QAAKC,aAAc,QAAQ,cAAc;;CAG3C,IAAI,UAAa;AAEf,SAAO,MAAKC,WAAY,MAAKF;;CAG/B,MAAM,SAAkD;EACtD,MAAM,OAAO,MAAKE,WAAY,MAAKF;AAEnC,MAAI,OAAO,SAAS,YAAY,SAAS,MAAM;GAE7C,MAAM,QAAQ;AACd,OAAI,CAAC,OAAO,GAAG,MAAM,MAAM,EAAE;AAC3B,UAAKE,UAAW;AAChB,UAAKC,eAAgB;;AAEvB;;EAGF,MAAM,OAAO,EAAE,GAAG,MAAM;EACxB,IAAI,UAAU;AAGd,OAAK,MAAM,OAAO,SAAS;AACzB,OAAI,CAAC,OAAO,OAAO,SAAgB,IAAI,CAAE;GAEzC,MAAM,QAAS,QAAuB;AAEtC,OAAI,CAAC,OAAO,GAAI,KAAoB,MAAiB,MAAM,EAAE;AAC3D,IAAC,KAAoB,OAAkB;AACvC,cAAU;;;AAId,MAAI,SAAS;AACX,SAAKD,UAAW;AAChB,SAAKC,eAAgB;;;CAMzB,UACE,oBACA,eACA,SACY;AAEZ,MAAI,kBAAkB,QAAW;GAC/B,MAAMC,aAAW;AACjB,SAAKC,UAAW,IAAID,WAAS;AAG7B,cAAS,KAAK,QAAQ;AAEtB,gBAAa;AACX,UAAKC,UAAW,OAAOD,WAAS;;;EAKpC,MAAM,WAAW;EACjB,MAAM,WAAW;EAGjB,MAAME,QAAmC;GAC7B;GACA;GACV,SALW,WAAW,EAAE;GAMzB;AAED,QAAKC,kBAAmB,IAAI,MAAM;AAIlC,WADiB,SAAS,KAAK,QAAQ,CACrB;AAElB,eAAa;AACX,SAAKA,kBAAmB,OAAO,MAAM;;;CAIzC,QAAc;AACZ,MAAI,MAAKL,YAAa,KAAM;EAE5B,MAAM,OAAO,MAAKF;EAClB,MAAM,OAAO,MAAKE;AAClB,QAAKA,UAAW;AAChB,QAAKM,eAAgB;AAGrB,MAAI,MAAKP,WAAY,MAAM,KAAK,CAAE;AAElC,QAAKD,UAAW;AAGhB,OAAK,MAAM,YAAY,MAAKK,UAC1B,UAAS,MAAKL,QAAS;AAIzB,OAAK,MAAM,SAAS,MAAKO,mBAAoB;GAC3C,MAAM,eAAe,MAAM,SAAS,KAAK;GACzC,MAAM,eAAe,MAAM,SAAS,MAAKP,QAAS;AAIlD,OAAI,EAFe,MAAM,QAAQ,cAAc,OAAO,IAEtC,cAAc,aAAa,CACzC,OAAM,SAAS,aAAa;;;CAKlC,iBAAuB;AACrB,MAAI,MAAKQ,aAAe;AACxB,QAAKA,eAAgB;AACrB,uBAAqB,KAAK,OAAO,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6CtC,SAAgB,YAAe,SAAY,QAA2C;AACpF,QAAO,IAAI,eAAe,SAAS,OAAO;;;;;;;;;AC7R5C,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;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,MAAMC,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,IAAIC;AACJ,KAAI,MAAM,OAAO,QAAW;AAC1B,UAAQ,OAAO,SAAS,MAAM,IAAI,GAAG;AACrC,MAAI,OAAO,MAAM,MAAM,CAAE,QAAO;YACvB,gBAAgB,OACzB,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,OAAW,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,OAAW,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;AACnB,UAAO,gBAAgB,MAAM,IAAI;;EAGnC,aAAa,KAAoC;GAC/C,MAAM,QAAQ,IAAI,IAAI,IAAI;AAC1B,OAAI,CAAC,MAAO,QAAO;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;;;;;;;;AC5KvD,SAAgB,WAAW,KAAa,SAAyB;AAC/D,QAAO,IAAI,IAAI,KAAK,QAAQ,CAAC;;;;;;;;;;;;;;;AC8B/B,SAAgB,mBACd,MACA,YACiB;CACjB,MAAM,QAAQ,KAAK,MAAM,QAAQ;CAGjC,MAAM,UAAU,WAAW;CAG3B,MAAMC,WAAsB,EAAE;CAC9B,IAAIC;CACJ,IAAIC;CAEJ,IAAI,kBAAkB;CACtB,IAAIC;CACJ,IAAI,cAAc;CAClB,IAAI,eAAe;CACnB,IAAIC;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;;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;AAC9E;;AAGF,MAAI,YAAY,iBACd;AAIF,MAAI,CAAC,QAAQ,WAAW,IAAI,IAAI,kBAAkB,GAAG;GACnD,MAAMC,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;AAGzB,YAAS,KAAK,QAAQ;AACtB,kBAAe;AACf;AAEA,qBAAkB;AAClB,sBAAmB;;;CAIvB,MAAM,gBAAgB;CAGtB,MAAM,iBACJ,WAAW,SAAS,UAAU,CAAC,iBAC3B,SACA,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;;;;;;;;;;;;;;;;;;ACnIH,SAAgB,aAAqB;AACnC,QAAO,GAAG,KAAK,KAAK,CAAC,GAAG,KAAK,MAAM,KAAK,QAAQ,GAAG,IAAQ;;;;;;;;;;;;;;;;ACgB7D,SAAgB,0BAA0B,MAAc,YAA6C;CACnG,MAAM,UAAU,WAAW;CAC3B,MAAM,QAAQ,KAAK,MAAM,QAAQ;CA+BjC,MAAMC,UAAwB,EAAE;CAChC,MAAMC,kBAAwC,EAAE;CAChD,MAAMC,qBAA8C,EAAE;CAGtD,IAAIC,oBAAoD;AAExD,MAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,UAAU,KAAK,MAAM;AAG3B,MAAI,CAAC,WAAY,QAAQ,WAAW,IAAI,IAAI,CAAC,QAAQ,WAAW,OAAO,CACrE;AAIF,MACE,YAAY,aACZ,QAAQ,WAAW,kBAAkB,IACrC,QAAQ,WAAW,8BAA8B,CAEjD;EAIF,MAAM,aAAa,SAAS,SAAS,cAAc;AACnD,MAAI,YAAY;GACd,MAAM,OAAO,WAAW,IAAI,OAAO;GACnC,MAAM,UAAU,WAAW,IAAI,WAAW;GAC1C,MAAM,OAAO,WAAW,IAAI,OAAO;AAEnC,OAAI,SAAS,WAAW,WAAW,MAAM;IACvC,MAAM,MAAM,WAAW,IAAI,MAAM;AACjC,oBAAgB,KAAK;KACnB;KACA;KACA,UAAU,WAAW,IAAI,WAAW;KACpC,KAAK,MAAM,WAAW,KAAK,QAAQ,GAAG;KACtC,SAAS,WAAW,QAAQ,UAAU;KACtC,YAAY,WAAW,QAAQ,aAAa;KAC7C,CAAC;;AAGJ,OAAI,SAAS,eAAe,WAAW,MAAM;IAC3C,MAAM,MAAM,WAAW,IAAI,MAAM;AAEjC,QAAI,IACF,oBAAmB,KAAK;KACtB;KACA;KACA,UAAU,WAAW,IAAI,WAAW;KACpC,KAAK,WAAW,KAAK,QAAQ;KAC7B,SAAS,WAAW,QAAQ,UAAU;KACtC,YAAY,WAAW,QAAQ,aAAa;KAC5C,QAAQ,WAAW,QAAQ,SAAS;KACrC,CAAC;;AAGN;;EAIF,MAAM,iBAAiB,SAAS,SAAS,mBAAmB;AAC5D,MAAI,gBAAgB;AAClB,uBAAoB;IAClB,WAAW,eAAe,OAAO,aAAa,EAAE;IAChD,YAAY,eAAe,cAAc,aAAa;IACtD,QAAQ,eAAe,IAAI,SAAS;IACpC,WAAW,eAAe,aAAa,aAAa;IACpD,cAAc,eAAe,IAAI,QAAQ;IAC1C;AACD;;AAIF,MAAI,CAAC,QAAQ,WAAW,IAAI,IAAI,mBAAmB;AACjD,WAAQ,KAAK;IACX,GAAG;IACH,KAAK,WAAW,SAAS,QAAQ;IAClC,CAAC;AACF,uBAAoB;;;CAMxB,MAAMC,eAA+B,EAAE;CACvC,MAAMC,mBAAmC,EAAE;AAE3C,MAAK,MAAM,UAAU,SAAS;AAC5B,MAAI,CAAC,OAAO,QAAQ;AAElB,gBAAa,KAAK,OAAO;AACzB;;EAGF,MAAM,eAAe,YAAY,OAAO,OAAO;AAI/C,MAHmB,OAAO,OAAO,MAAM,IAAI,CAAC,WAGzB,EACjB,KAAI,aAAa,SAAS,CAAC,aAAa,MAEtC,kBAAiB,KAAK,OAAO;MAG7B,cAAa,KAAK,OAAO;MAI3B,cAAa,KAAK,OAAO;;CAK7B,MAAMC,cAA6C,aAAa,KAAK,WAAW;EAC9E,MAAM,SAAS,OAAO,SAAS,YAAY,OAAO,OAAO,GAAG;EAE5D,MAAMC,QAAqC;GACzC,MAAM;GACN,IAAI,YAAY;GAChB,KAAK,OAAO;GACZ,WAAW,OAAO;GAElB,UAAU;GACV,QAAQ,EAAE;GACX;AAED,MAAI,OAAO,YAAY,UAAU,OAC/B,OAAM,QAAQ,OAAO,WAAW;AAElC,MAAI,OAAO,YAAY,WAAW,OAChC,OAAM,SAAS,OAAO,WAAW;AAEnC,MAAI,QAAQ,MACV,OAAM,SAAS,CAAC,OAAO,MAAM;AAE/B,MAAI,OAAO,UACT,OAAM,YAAY,OAAO;AAE3B,MAAI,OAAO,aACT,OAAM,eAAe,OAAO;AAG9B,SAAO;GACP;CAGF,MAAMC,kBAAiD,iBAAiB,KAAK,WAAW;EACtF,MAAM,SAAS,OAAO,SAAS,YAAY,OAAO,OAAO,GAAG;AAe5D,SAb2C;GACzC,MAAM;GACN,IAAI,YAAY;GAChB,KAAK,OAAO;GACZ,WAAW,OAAO;GAClB,UAAU;GACV,QAAQ,QAAQ,QAAQ,CAAC,OAAO,MAAM,GAAG,EAAE;GAC3C,SAAS,OAAO,gBAAgB;GAChC,MAAM;GACN,YAAY;GACZ,UAAU;GACX;GAGD;CA+CF,MAAM,cAAc,CAAC,GA3CuC,gBAAgB,KAAK,cAAc;EAC7F,IAAIC;AACJ,OAAK,MAAM,UAAU,QACnB,KAAI,OAAO,iBAAiB,UAAU,WAAW,OAAO,QAAQ;GAC9D,MAAM,SAAS,YAAY,OAAO,OAAO;AACzC,OAAI,OAAO,OAAO;AAChB,kBAAc,CAAC,OAAO,MAAM;AAC5B;;;EAKN,MAAMC,QAAqC;GACzC,MAAM;GACN,IAAI,YAAY;GAChB,KAAK,UAAU,OAAO;GACtB,SAAS,UAAU;GACnB,MAAM,UAAU;GAEhB,UAAU;GACV,WAAW;GACX,YAAY;GACZ,UAAU;GACV,QAAQ,EAAE;GACX;AAED,MAAI,UAAU,SACZ,OAAM,WAAW,UAAU;AAE7B,MAAI,YACF,OAAM,SAAS;AAEjB,MAAI,UAAU,QACZ,OAAM,UAAU,UAAU;AAE5B,MAAI,UAAU,WACZ,OAAM,aAAa,UAAU;AAG/B,SAAO;GACP,EAG4C,GAAG,gBAAgB;CAGjE,MAAMC,aAA2C,mBAAmB,KAAK,cAAc;EACrF,MAAMC,QAAoC;GACxC,MAAM;GACN,IAAI,YAAY;GAChB,KAAK,UAAU;GACf,SAAS,UAAU;GACnB,OAAO,UAAU;GACjB,MAAM;GAEN,UAAU;GACV,WAAW;GACZ;AAED,MAAI,UAAU,SACZ,OAAM,WAAW,UAAU;AAG7B,MAAI,UAAU,WAAW,UAAU,WACjC,OAAM,UAAU;AAElB,MAAI,UAAU,WACZ,OAAM,aAAa,UAAU;AAE/B,MAAI,UAAU,OACZ,OAAM,SAAS,UAAU;AAG3B,SAAO;GACP;CAGF,MAAMC,gBAAgC,EAAE;AAExC,KAAI,YAAY,SAAS,GAAG;EAC1B,MAAMC,oBAAuC;GAC3C,IAAI,YAAY;GAChB,MAAM;GACN,QAAQ;GACT;EAED,MAAMC,oBAAuC;GAC3C,IAAI,YAAY;GAChB,MAAM;GACN,eAAe,CAAC,kBAAkB;GACnC;AAED,gBAAc,KAAK,kBAAkB;;AAGvC,KAAI,YAAY,SAAS,GAAG;EAC1B,MAAMC,oBAAuC;GAC3C,IAAI,YAAY;GAChB,MAAM;GACN,QAAQ;GACT;EAED,MAAMC,oBAAuC;GAC3C,IAAI,YAAY;GAChB,MAAM;GACN,eAAe,CAAC,kBAAkB;GACnC;AAED,gBAAc,KAAK,kBAAkB;;AAGvC,KAAI,WAAW,SAAS,GAAG;EACzB,MAAMC,mBAAqC;GACzC,IAAI,YAAY;GAChB,MAAM;GACN,QAAQ;GACT;EAED,MAAMC,mBAAqC;GACzC,IAAI,YAAY;GAChB,MAAM;GACN,eAAe,CAAC,iBAAiB;GAClC;AAED,gBAAc,KAAK,iBAAiB;;AAItC,QAAO;EACL,IAAI,YAAY;EAChB,KAAK,WAAW;EAChB,WAAW;EAEX;EACD;;;;;;;;;;;;;;;;;;;;;;;;;ACzVH,SAAgB,eAAe,UAA0B;AACvD,QAAO,KAAK,IAAI,KAAK,IAAI,GAAI,GAAG,SAAS;;;;;;;;;;;;;;;;;;;AAoB3C,SAAgB,cAAc,cAAsB,OAAe,QAAgB,UAA0B;CAE3G,MAAM,WADQ,eAAe,SAAS,IACZ;AAC1B,QAAO,SAAS,IAAI,YAAY,WAAW;;;;;;;;;;;;;;;;;;;;AAqB7C,SAAgB,gBAAgB,UAAkB,aAAqB,UAA0B;AAC/F,KAAI,gBAAgB,EAClB,QAAO;AAKT,QAAO,YADY,IADL,eAAe,SAAS,IACN;;;;;;;;;;AChBlC,MAAaC,2BAA4C;CACvD,cAAc;CACd,cAAc;CACd,eAAe;CACf,UAAU;CACV,aAAa;CACd;;;;;;;;;;;;;;;;;;;;;AAsBD,SAAgB,gBACd,OACA,YACA,UACA,SAA0B,0BACV;CAEhB,MAAM,sBAAsB,MAAM,eAAe;AAKjD,KAAI,WAAW,OAAO,SACpB,QAAO;EAAE,GAAG;EAAO,cAAc;EAAqB;AAMxD,KAAI,aAAa,OAAO,YACtB,QAAO;EAAE,GAAG;EAAO,cAAc;EAAqB;CAKxD,MAAM,YAAa,MAAO,WAAY;CAItC,MAAM,SAAS,aAAa;AAE5B,QAAO;EACL,cAAc,cAAc,MAAM,cAAc,WAAW,QAAQ,OAAO,aAAa;EACvF,iBAAiB,MAAM,kBAAkB;EACzC,cAAc,cAAc,MAAM,cAAc,WAAW,QAAQ,OAAO,aAAa;EACvF,iBAAiB,MAAM,kBAAkB;EACzC,cAAc;EACf;;;;;;;;;;;;;;;;;;;;AAqBH,SAAgB,qBACd,OACA,iBACA,SAA0B,0BAClB;AAER,KAAI,MAAM,eAAe,OAAO,cAC9B,QAAO;CAIT,MAAM,eAAe,gBAAgB,MAAM,cAAc,MAAM,iBAAiB,OAAO,aAAa;CAEpG,MAAM,eAAe,gBAAgB,MAAM,cAAc,MAAM,iBAAiB,OAAO,aAAa;AAIpG,QAAO,KAAK,IAAI,cAAc,aAAa;;;;;;;;;ACjI7C,MAAaC,yBAAwC,EACnD,cAAc,KACf;;;;;;;;;;;;;;;;;;;;;;;;AAyBD,SAAgB,cACd,QACA,kBACA,SAAwB,wBACiB;AACzC,KAAI,OAAO,WAAW,EACpB;CAIF,MAAM,eAAe,OAAO,OAAO,CAAC,MAAM,GAAG,MAAM,EAAE,YAAY,EAAE,UAAU;CAG7E,IAAIC;AAEJ,MAAK,MAAM,SAAS,aAKlB,KAAI,oBAFsB,MAAM,YAAY,OAAO,cAIjD;MACE,CAAC,UACD,MAAM,YAAY,OAAO,aACxB,MAAM,cAAc,OAAO,aAAa,oBAAoB,OAAO,OAAO,CAE3E,UAAS;;AAMf,QAAO,UAAU,aAAa;;;;;;;;;;AAWhC,SAAS,oBAAoB,QAAqC,QAA8C;AAG9G,SAFiB,OAAO,SAAS,MAAM,OAAO,UAAU,MACvC,OAAO,SAAS,MAAM,OAAO,UAAU;;;;;;;;AChF1D,MAAaC,6BAA+C,EAC1D,cAAc,GACf;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BD,SAAgB,8BACd,UACA,aACA,SAA2B,4BACnB;AACR,KAAI,SAAS,WAAW,EACtB,QAAO;CAIT,MAAM,iBAAiB,SAAS,QAAQ,QAAQ,IAAI,YAAY,YAAY;AAG5E,KAAI,eAAe,WAAW,EAC5B,QAAO;CAKT,MAAM,kBAAkB,eAAe,SAAS,OAAO;AAGvD,KAAI,mBAAmB,EACrB,QAAO;AAKT,KAAI,mBAAmB,eAAe,OACpC,QAAO;AAKT,QAAO,eAAe,iBAAkB;;;;;;;;AClE1C,MAAaC,gCAAqD,EAChE,gBAAgB,IACjB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgDD,SAAgB,2BACd,kBACA,aACA,SAA8B,+BACtB;AACR,KAAI,iBAAiB,WAAW,EAAG,QAAO;CAE1C,MAAM,YAAY,cAAc,OAAO;CAGvC,MAAM,SAAS,iBAAiB,QAAQ,QAAQ,IAAI,aAAa,UAAU;AAE3E,KAAI,OAAO,WAAW,EAAG,QAAO;AAGhC,QAAO,KAAK,IAAI,GAAG,OAAO,KAAK,QAAQ,IAAI,UAAU,CAAC;;AAGxD,SAAgB,kBACd,UACA,kBACA,aACA,SAA8B,+BACnB;AACX,KAAI,SAAS,WAAW,EACtB,QAAO,EAAE;CAIX,MAAM,aAAa,cAAc,OAAO;CAKxC,MAAM,qBAAqB,IAAI,IAAI,iBAAiB,KAAK,QAAQ,IAAI,UAAU,CAAC;AAgBhF,QAXe,SAAS,QAAQ,QAAQ;EAEtC,MAAM,aAAa,IAAI,YAAY,IAAI;EACvC,MAAM,YAAY,IAAI,YAAY,cAAc,aAAa;EAG7D,MAAM,gBAAgB,CAAC,mBAAmB,IAAI,IAAI,UAAU;AAE5D,SAAO,aAAa;GACpB;;;;;AC6MJ,SAAgB,gBAAgB,OAAuE;AACrG,QAAO,cAAc;;;;;;AAOvB,SAAgB,wBACd,cACqD;AACrD,QAAO,aAAa,aAAa;;;;;;;;;;;;;;;;ACrUnC,SAAgB,sBAA+B;AAC7C,QAAO,OAAO,gBAAgB;;;;;;AAOhC,SAAgB,6BAAsC;AACpD,QAAO,OAAO,uBAAuB;;;;;;;;;;;;;;AAuBvC,SAAgB,kBAAkB,UAAoC,EAAE,EAAe;CACrF,MAAM,EAAE,gBAAgB,UAAU;AAElC,KAAI,iBAAiB,4BAA4B,CAC/C,QAAO,IAAI,oBAAqB;AAGlC,KAAI,qBAAqB,CACvB,QAAO,IAAI,aAAa;AAG1B,OAAM,IAAI,MAAM,mCAAmC;;;;;;;;;;;;;;;;;;;AA8BrD,SAAgB,kBAAkB,aAA0B,cAAyD;AAInH,KAF6B,4BAA4B,IAAI,uBAAuB,oBAE1D;AAGxB,EAAC,aAAuE,wBAAwB;AAGhG,EAAC,aAAsE,YAAY;EAEnF,MAAMC,iBAAqB;AACzB,GAAC,aAAsE,YAAY;AACnF,gBAAa,MAAM;;AAGrB,SAAO;GAAE,KAAK;GAAI;GAAQ;;CAI5B,MAAM,MAAM,IAAI,gBAAgB,YAAY;AAC5C,cAAa,MAAM;CAEnB,MAAM,eAAqB;AACzB,eAAa,gBAAgB,MAAM;AACnC,eAAa,MAAM;AACnB,MAAI,gBAAgB,IAAI;;AAG1B,QAAO;EAAE;EAAK;EAAQ;;;;;;;;;;;;;;;;AAiBxB,SAAgB,kBAAkB,aAA0B,QAAqC;AAC/F,QAAO,IAAI,SAAS,SAAS,WAAW;AACtC,MAAI,YAAY,eAAe,QAAQ;AACrC,YAAS;AACT;;AAIF,MAAI,QAAQ,SAAS;AACnB,UAAO,IAAI,aAAa,WAAW,aAAa,CAAC;AACjD;;EAIF,MAAM,aAAa,IAAI,iBAAiB;EACxC,MAAM,UAAU,EAAE,QAAQ,WAAW,QAAQ;AAE7C,cAAY,iBACV,oBACM;AACJ,cAAW,OAAO;AAClB,YAAS;KAEX,QACD;AAED,UAAQ,iBACN,eACM;AACJ,cAAW,OAAO;AAClB,UAAO,IAAI,aAAa,WAAW,aAAa,CAAC;KAEnD,QACD;GACD;;;;;;;;;;;;;;AAeJ,SAAgB,mBAAmB,aAA0B,WAAiC;AAC5F,KAAI,YAAY,eAAe,OAC7B,OAAM,IAAI,MAAM,0BAA0B;AAG5C,KAAI,CAAC,iBAAiB,UAAU,CAC9B,OAAM,IAAI,MAAM,wBAAwB,YAAY;AAGtD,QAAO,YAAY,gBAAgB,UAAU;;;;;;;;;;;;;AAc/C,SAAgB,iBAAiB,WAA4B;AAC3D,KAAI,CAAC,qBAAqB,CACxB,QAAO;AAGT,QAAO,YAAY,gBAAgB,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;AC3K/C,eAAsB,gBAAgB,aAAgC,SAA0C;CAC9G,MAAM,UAAU,IAAI,QAAQ,SAAS,QAAQ;AAG7C,KAAI,YAAY,WAAW;EACzB,MAAM,EAAE,OAAO,QAAQ,YAAY;AACnC,UAAQ,IAAI,SAAS,SAAS,MAAM,GAAG,MAAM;;CAG/C,MAAM,UAAU,IAAI,QAAQ,YAAY,KAAK;EAC3C,QAAQ;EACR;EACA,GAAG;EACJ,CAAC;AAEF,QAAO,MAAM,QAAQ;;;;;;;;;AAUvB,eAAsB,qBACpB,aACA,SACsB;AAEtB,SADiB,MAAM,gBAAgB,aAAa,QAAQ,EAC5C,aAAa;;;;;;;;;;;;;;;AAgB/B,SAAgB,gBAAgB,UAAyC;AACvE,QAAO,SAAS,MAAM;;;;;;;;;;;AC9ExB,MAAM,sBAAsB,OAAO,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;AAsC3D,SAAgB,oBAAmE;CACjF,MAAM,8BAAc,IAAI,KAAuB;AAE/C,QAAO;GACJ,sBAAsB;EAEvB,SAAS,OAAgB;GAEvB,MAAM,UAAU,MAAM,KAAK,YAAY;AACvC,QAAK,MAAM,YAAY,QACrB,UAAS,MAAM;;EAInB,UAAU,UAAwC;AAChD,eAAY,IAAI,SAAS;AACzB,gBAAa,YAAY,OAAO,SAAS;;EAE5C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACQH,SAAgB,cACd,SAC8C;CAG9C,MAAM,sBAAsB,aAAqD;EAC/E,MAAMC,SAAoB,IAAI,MAAM,QAAQ,OAAO;EACnD,MAAMC,WAAsB,IAAI,MAAM,QAAQ,OAAO,CAAC,KAAK,MAAM;EACjE,MAAMC,gBAAmC,EAAE;AAE3C,OAAK,IAAI,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;GACvC,MAAM,cAAc,QAAQ,GAAI,WAAW,UAAU;AACnD,WAAO,KAAK;AACZ,aAAS,KAAK;AACd,QAAI,SAAS,OAAO,QAAQ,IAAI,CAC9B,UAAS,CAAC,GAAG,OAAO,CAAW;KAEjC;AACF,iBAAc,KAAK,YAAY;;AAGjC,eAAa;AACX,QAAK,MAAM,eAAe,cACxB,cAAa;;;AAKnB,QAAO,EACL,UACE,oBACA,eACA,SACY;AACZ,MAAI,kBAAkB,OACpB,QAAO,mBAAmB,mBAA+C;EAG3E,MAAM,WAAW;EACjB,MAAM,WAAW;EACjB,MAAM,aAAa,SAAS,cAAc,OAAO;EAEjD,IAAIC;EACJ,IAAI,cAAc;AAElB,SAAO,oBAAoB,WAAW;GACpC,MAAM,eAAe,SAAS,OAAO;AACrC,OAAI,CAAC,eAAe,CAAC,WAAW,cAAmB,aAAa,EAAE;AAChE,mBAAe;AACf,kBAAc;AACd,aAAS,aAAa;;IAExB;IAEL;;;;;;;;AC9FH,SAAgB,aACd,cACwC;AACxC,QAAO,iBAAiB,UAAa,SAAS,gBAAgB,EAAE,QAAQ;;AAG1E,SAAgBC,aACd,OACuE;AACvE,QAAO,aAAa,MAAM,aAAa;;;;;;;;;;;;;AAczC,SAAgBC,gBAAc,OAA0B,OAAoC;CAC1F,MAAM,EAAE,YAAY;AACpB,QAEE,CAAC,QAAQ,WAAW,CAAC,SAAS,QAAe,IAE7C,MAAM,SAAS;;;;;;;;;;;;AAcnB,SAAgB,qBACd,OACA,QACY;AACZ,QAAO,OAAO,WAAW,YAAY;EACnC,MAAM,UAAU,QAAQ,cAAc,WAAW;AACjD,QAAM,MAAM,EAAW,SAAqD,CAAC;GAC7E;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCJ,SAAgB,oBAAoB,EAClC,OACA,UAIa;CAEb,IAAI,YAAY;CAChB,IAAIC,kBAA0C;CAE9C,MAAM,UAAU,cAAc,CAAC,OAAO,OAAO,CAAC,CAAC,UAAU,OAAO,CAAC,cAAc,WAAW;AACxF,MAAI,CAACF,aAAW,aAAa,IAAI,CAACC,gBAAc,cAAc,MAAM,IAAI,UAAW;AAEnF,MAAI;AAGF,eAAY;AACZ,qBAAkB,IAAI,iBAAiB;GAEvC,MAAM,EAAE,iBAAiB;GAIzB,MAAM,SAAS,0BADF,MAAM,gBADF,MAAM,gBAAgB,cAAc,EAAE,QAAQ,gBAAgB,QAAQ,CAAC,CAC5C,EACG,aAAa;AAG5D,SAAM,MAAM,EACV,cAAc,QACf,CAAC;WACK,OAAO;AAEd,OAAI,iBAAiB,SAAS,MAAM,SAAS,aAC3C;AAEF,SAAM;YACE;AAER,eAAY;AACZ,qBAAkB;;GAEpB;AAGF,cAAa;AACX,mBAAiB,OAAO;AACxB,WAAS;;;;;;;;;AC9Gb,MAAaE,2BAA6D;CACxE,cAAc;CACd,oBAAoB;CACpB,kBAAkB;CACnB;;;;;AAMD,SAAS,eAAe,cAA4B;AAElD,SADiB,aAAa,eAAe,MAAM,MAAM,EAAE,SAAS,QAAQ,GAC3D,cAAc,IAAI,UAAU,EAAE;;;;;;;;;;;;;;;;;;;;;;AAuBjD,SAAgB,cACd,EAAE,SACF,SAAiC,EAAE,EACvB;CACZ,MAAM,eAAe,OAAO,gBAAgB,yBAAyB;CACrE,MAAM,qBAAqB,OAAO,sBAAsB,yBAAyB;CACjF,MAAM,mBAAmB,OAAO,oBAAoB,yBAAyB;CAM7E,IAAI,kBAAkB,KAAK,KAAK;CAChC,IAAI,sBAAsB;AAE1B,QAAO,MAAM,WAAW,iBAAwC;EAC9D,MAAM,EAAE,cAAc,gBAAgB,sBAAsB,gBAAgB;AAE5E,MAAI,gBAAgB,KAAM;AAC1B,MAAI,CAAC,gBAAgB,CAAC,eAAgB;EAEtC,MAAM,cAAc,eAAe,aAAa;AAChD,MAAI,YAAY,WAAW,EAAG;EAG9B,MAAM,UAAU;AAChB,wBAAsB;EAGtB,MAAM,UAAU,cAAc,aADZ,qBAAqB,gBAAgB,iBAAiB,EACX,EAAE,cAAc,CAAC;AAC9E,MAAI,CAAC,WAAW,QAAQ,OAAO,qBAAsB;EAGrD,MAAM,eAAe,YAAY,MAAM,MAAM,EAAE,OAAO,qBAAqB;AAG3E,MAFkB,CAAC,gBAAgB,QAAQ,YAAY,aAAa,WAErD;GACb,MAAM,MAAM,KAAK,KAAK;AAGtB,OAAI,CAAC,WAAW,MAAM,kBAAkB,mBAAoB;AAC5D,qBAAkB;;AAGpB,QAAM,MAAM,EAAE,sBAAsB,QAAQ,IAAI,CAAC;GACjD;;;;;;;;ACzGJ,MAAa,2BAA2B;CACtC,OAAO;CACP,OAAO;CACP,MAAM;CACP;;;;;AAMD,MAAa,kBAAkB;CAC7B,OAAO;CACP,OAAO;CACR;;;;;;;;;;;;AAaD,SAAgB,iBACd,OACA,MAOY;CACZ,MAAM,EAAE,iBAAiB;;AAGzB,KAAI,CAAC,gBAAgB,EAAE,QAAQ,cAAe,QAAO;CAIrD,MAAM,UAAU,MADG,yBAAyB;AAE5C,QAAO,aAAa,cACjB,MAAM,EAAE,MAAM,uBAAuB,qBAAqB,KAAK,EAC9D,cAAc,IAAI,OAAO,MAAM,EAAE,SAAS,OAAO,QAAQ;;;;;;;;;;;;;;;;;;;;ACiC/D,SAAgB,yBACd,mBACA,YACoB;CACpB,IAAIC,eAAkC;CACtC,IAAIC,sBAAqC;CACzC,IAAIC,oBAAmC;CACvC,IAAIC,kBAA0C;CAC9C,IAAI,UAAU;CACd,IAAI,YAAY;CAEhB,MAAM,uBAAuB,gBAA+C;EAC1E,MAAM,cAAc,IAAI,IAAI,kBAAkB,SAAS,QAAQ,SAAS,KAAK,MAAM,EAAE,GAAG,CAAC;AACzF,SAAO,YAAY,QAAQ,MAAM,YAAY,IAAI,EAAE,GAAG,CAAC;;;;;;;;;;;;;;;;;CAkBzD,MAAM,aAAa,YAA8C;EAC/D,MAAM,EAAE,OAAO,UAAU;EACzB,MAAM,WAAW,kBAAkB,SAAS;EAC5C,MAAM,mBAAmB,oBAAoB,MAAM,SAAS;EAC5D,MAAM,cAAc,OAAO,SAAS;EACpC,MAAMC,QAAoB,EAAE;AAG5B,MAAI,OAAO;GACT,MAAM,oBAAoB,2BAA2B,kBAAkB,YAAY;AACnF,OAAI,oBAAoB,SACtB,OAAM,KAAK;IAAE,MAAM;IAAU,OAAO;IAAmB,KAAK;IAAU,CAAC;GAEzE,MAAM,eAAe,8BAA8B,kBAAkB,YAAY;AACjF,OAAI,eAAe,EACjB,OAAM,KAAK;IAAE,MAAM;IAAU,OAAO;IAAG,KAAK;IAAc,CAAC;;AAK/D,MAAI,SAAS,gBAAgB,MAAM,GACjC,OAAM,KAAK;GAAE,MAAM;GAAe,MAAM,EAAE,SAAS,MAAM,IAAI;GAAE,KAAK,MAAM,eAAe;GAAK,CAAC;AAIjG,MAAI,OAAO;GACT,MAAM,UAAU;GAChB,MAAM,iBAAiB,kBAAkB,MAAM,UAAU,kBAAkB,YAAY,CAAC,QAAQ,QAAQ;IAItG,MAAM,WAAW,SAAS,SAAS,MAAM,MAAM,KAAK,IAAI,EAAE,YAAY,IAAI,UAAU,GAAG,QAAQ;AAC/F,QAAI,CAAC,UAAU,kBAAkB,CAAC,MAAM,UAAW,QAAO;AAC1D,WAAO,MAAM,YAAY,SAAS;KAClC;AACF,QAAK,MAAM,WAAW,eACpB,OAAM,KAAK;IACT,MAAM;IACN,MAAM;KACJ,IAAI,QAAQ;KACZ,WAAW,QAAQ;KACnB,UAAU,QAAQ;KAClB,SAAS,MAAM;KACf,gBAAgB,MAAM;KACvB;IACD,KAAK,QAAQ;IACd,CAAC;;AAIN,SAAO;;;;;;;;;CAUT,MAAM,kBAAkB,OAAO,SAAkC;EAC/D,MAAM,SAAS,gBAAiB;AAChC,MAAI;AACF,OAAI,KAAK,SAAS,UAAU;AAC1B,UAAM,kBAAkB,KAAK,MAAM,OAAO;AAC1C;;AAGF,OAAI,KAAK,SAAS,eAAe;AAC/B,0BAAsB,KAAK,KAAK;AAChC,QAAI,CAAC,OAAO,SAAS;KACnB,MAAM,OAAO,MAAM,WAAW,MAAM,EAAE,QAAQ,CAAC;KAI/C,MAAM,gBAAgB,cAAc,MACjC,MAAM,EAAE,SAAS,iBAAiB,EAAE,KAAK,YAAY,KAAK,KAAK,QACjE;AACD,SAAI,CAAC,OAAO,WAAW,CAAC,eAAe;MACrC,MAAM,eAAe,OAAO,UAAU,IAAI,iBAAiB,CAAC,SAAS;AACrE,YAAM,kBAAkB,KAAK;OAAE,MAAM;OAAe;OAAM,MAAM,KAAK;OAAM,EAAE,aAAa;;;AAG9F;;AAIF,uBAAoB,KAAK,KAAK;AAC9B,OAAI,CAAC,OAAO,SAAS;IACnB,MAAM,OAAO,MAAM,WAAW,MAAM,EAAE,QAAQ,CAAC;AAC/C,QAAI,CAAC,OAAO,QACV,OAAM,kBAAkB,KAAK;KAAE,MAAM;KAAkB;KAAM,MAAM,KAAK;KAAM,EAAE,OAAO;;YAGnF;AACR,yBAAsB;AACtB,uBAAoB;;;;;;;;CASxB,MAAM,eAAe,OAAO,iBAA4C;AACtE,YAAU;AACV,oBAAkB,IAAI,iBAAiB;EACvC,IAAI,YAAY;AAEhB,SAAO,UAAU,SAAS,KAAK,CAAC,WAAW;GACzC,MAAM,OAAO,UAAU;AACvB,eAAY,UAAU,MAAM,EAAE;AAE9B,OAAI;AACF,UAAM,gBAAgB,KAAK;YACpB,OAAO;AACd,QAAI,iBAAiB,SAAS,MAAM,SAAS,cAAc,QAEpD;AACL,aAAQ,MAAM,uCAAuC,MAAM;AAI3D,iBAAY,EAAE;;;AAIlB,OAAI,iBAAiB,MAAM;AAEzB,gBAAY;AACZ,mBAAe;AACf,sBAAkB,IAAI,iBAAiB;cAC9B,gBAAgB,OAAO,QAEhC;;AAIJ,oBAAkB;AAClB,YAAU;;AAGZ,QAAO;EACL,KAAK,SAA+B;AAClC,OAAI,UAAW;GAEf,MAAM,WAAW,UAAU,QAAQ;AAEnC,OAAI,CAAC,SAAS;AACZ,QAAI,SAAS,WAAW,EAAG;AAC3B,iBAAa,SAAS;AACtB;;AAUF,OALG,sBAAsB,QACrB,SAAS,MAAM,MAAM,EAAE,SAAS,oBAAoB,EAAE,KAAK,OAAO,kBAAkB,IACrF,wBAAwB,QACvB,SAAS,MAAM,MAAM,EAAE,SAAS,iBAAiB,EAAE,KAAK,YAAY,oBAAoB,CAK1F,gBAAe,SAAS,QACrB,MACC,EAAE,EAAE,SAAS,oBAAoB,EAAE,KAAK,OAAO,sBAC/C,EAAE,EAAE,SAAS,iBAAiB,EAAE,KAAK,YAAY,qBACpD;QACI;AAEL,mBAAe;AACf,qBAAiB,OAAO;;;EAI5B,UAAU;AACR,eAAY;AACZ,oBAAiB,OAAO;;EAE3B;;;;;ACxSH,MAAMC,mBAAiB;CACrB,OAAO;CACP,OAAO;CACR;;;;;;;;;;;AAgBD,SAAS,mBACP,YACA,UACiF;AACjF,QAAO,OAAO,aAAa,YAAY;EACrC,MAAM,QAAQ,YAAY,KAAK;EAC/B,MAAM,OAAO,MAAM,qBAAqB,aAAa,QAAQ;EAC7D,MAAM,UAAU,YAAY,KAAK,GAAG;EACpC,MAAM,OAAO,gBAAgB,WAAW,SAAS,SAAS,KAAK,WAAW;AAC1E,aAAW,MAAM,KAAK;AACtB,aAAW,OAAO;AAClB,aAAW,KAAK;AAChB,SAAO;;;AAgDX,SAAS,oBACP,CAAC,iBAAiB,QAClB,MACe;CACf,MAAM,EAAE,mBAAmB,SAAS,gBAAgB;AAEpD,QAAO;EACL;EACA;EACA;EACA,OALY,iBAAiB,OAAO,KAAK;EAMzC;EACD;;;;;;;;;;;;;;;;;;AAmBH,MAAM,mBAAmB,aAAiC,UAAqC;AAC7F,KAAI,eAAe,KAAM,QAAO;AAChC,QAAO,OAAO,SAAS,MACpB,EAAE,WAAW,YAAY,GAAG,aAC3B,eAAe,cAAc,cAAc,YAAY,YAAY,MAAM,SAAS,SAAS,GAC9F,EAAE;;;;;;;;AASL,SAAS,gBAAgB,WAA0B,UAAkC;AACnF,KAAI,CAAC,SAAS,gBAAiB,QAAO;AAEtC,KAAI,CAAC,SAAS,mBAAmB;AAC/B,MAAI,SAAS,YAAY,OAAQ,QAAO;AACxC,SAAO,SAAS,YAAY,UAAU;;AAGxC,KAAI,CAAC,UAAU,qBAAqB,SAAS,mBAC3C;MAAI,UAAU,YAAY,OAAQ,QAAO;;AAK3C,KAAI,CAAC,SAAS,SAAS,CAAC,gBAAgB,SAAS,MAAM,CAAE,QAAO;AAGhE,KAAI,UAAU,OAAO,OAAO,SAAS,MAAM,MAAM,gBAAgB,SAAS,MAAM,CAAE,QAAO;AAKzF,QACE,gBAAgB,UAAU,aAAa,SAAS,MAAuB,KACvE,gBAAgB,SAAS,aAAa,SAAS,MAAuB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuC1E,SAAgB,aACd,EACE,OACA,UAKF,QACY;CACZ,MAAM,EAAE,SAAS;CACjB,MAAM,WAAWA,iBAAe;CAQhC,MAAM,mBAAmB,MAAM,QAAQ;CACvC,MAAM,aAAa,YACjB,oBAAoB;EAClB,cAAc;EACd,iBAAiB;EACjB,cAAc;EACd,iBAAiB;EACjB,cAAc;EACf,CACF;CAED,MAAM,aACJ,SAAS,UACL,mBACE,YACA,qBAAqB,UAChB,SAAS;AACR,QAAM,MAAM,EAAE,gBAAgB,MAAM,CAAC;AAGrC,QAAM,OAAO;KAEf,OACL,GACD;CAEN,MAAM,gBAAgB,YAA4C,OAAU;CAI5E,MAAM,sBAAsB,OAAO,WAChC,MAAM,EAAE,YACR,UAAU;AACT,MAAI,MACF,eAAc,MAAM,yBAAyB,OAAO,WAAW,CAAC;WACvD,CAAC,SAAS,cAAc,SAAS;AAC1C,iBAAc,QAAQ,SAAS;AAC/B,iBAAc,MAAM,OAAU;;AAEhC,eAAa;AACX,iBAAc,SAAS,SAAS;AAChC,iBAAc,MAAM,OAAU;;GAGnC;CAED,MAAM,kBAAkB,YAAqB,MAAM;CACnD,MAAM,6BAA6B,cAAc,CAAC,OAAO,cAAc,CAAC,CAAC,WACtE,CAAC,cAAc,0BAA0B;EACxC,MAAM,QAAQ,iBAAiB,cAAc,KAAK;EAClD,MAAM,gBAAgB,CAAC,CAAC,SAAS,gBAAgB,MAAM;EACvD,MAAM,2BAA2B,CAAC,CAAC;AACnC,kBAAgB,MAAM,iBAAiB,yBAAyB;GAEnE;CAED,MAAM,gCAAgC,cAAc,CAAC,iBAAiB,MAAM,CAAC,CAAC,WAC3E,CAACC,mBAAiBC,aAAW,oBAAoB,CAACD,mBAAiBC,QAAM,EAAE,KAAK,GAChF,EAAE,SAAS,mBAAmB,aAAa,YAAY;AAGtD,MAAI,EAFa,YAAY,UAAU,CAAC,CAAC;;AAKvC,gBAAc,SAAS,KAAK;GAAE,MAAM;GAAQ;GAAO,CAAC;MAEpD,eAAc,SAAS,KAAK;GAC1B,MAAM;GAEN;GACA,OAAO;IACL,OAAO;IACP,KAAM,cAAyB,8BAA8B;IAC9D;GACF,CAAC;IAGN,EAAE,YAAY,iBAAiB,CAChC;AAED,cAAa;AACX,8BAA4B;AAC5B,iCAA+B;AAC/B,uBAAqB;;;;;;;;;;;;AC9SzB,IAAIC,aAAsC;AAE1C,SAAS,mBAAqC;AAC5C,KAAI,CAAC,YAAY;AACf,eAAa,SAAS,cAAc,QAAQ;AAC5C,aAAW,QAAQ;AACnB,aAAW,UAAU;AACrB,aAAW,MAAM,UAAU;AAC3B,aAAW,cAAc;;AAE3B,QAAO;;AAGT,SAAgB,gBAAgB,KAAgC;CAC9D,MAAM,QAAQ,kBAAkB;CAChC,MAAM,QAAQ,SAAS,cAAc,QAAQ;AAC7C,OAAM,OAAO;AACb,OAAM,UAAU;AAEhB,QAAO,IAAI,SAAS,SAAS,WAAW;EACtC,MAAM,eAAqB;GACzB,MAAMC,OAAiB,EAAE;GACzB,MAAM,YAAY,MAAM;AAExB,OAAI,UAAU,KACZ,MAAK,IAAI,IAAI,GAAG,IAAI,UAAU,KAAK,QAAQ,KAAK;IAC9C,MAAM,MAAM,UAAU,KAAK;AAC3B,QAAI,IACF,MAAK,KAAK,IAAc;;AAK9B,YAAS;AACT,WAAQ,KAAK;;EAGf,MAAM,gBAAsB;AAC1B,YAAS;AACT,0BAAO,IAAI,MAAM,+BAA+B,MAAM,CAAC;;EAGzD,MAAM,gBAAsB;AAC1B,SAAM,oBAAoB,QAAQ,OAAO;AACzC,SAAM,oBAAoB,SAAS,QAAQ;AAC3C,SAAM,YAAY,MAAM;;AAG1B,QAAM,iBAAiB,QAAQ,OAAO;AACtC,QAAM,iBAAiB,SAAS,QAAQ;AACxC,QAAM,YAAY,MAAM;AACxB,QAAM,MAAM;GACZ;;AAGJ,SAAgB,mBAAyB;AACvC,cAAa;;;;;ACzDf,SAAS,eAAe,KAAa,WAA0C;CAC7E,MAAM,EAAE,SAAS;AACjB,KAAI,CAAC,KAAM,QAAO;AAClB,MAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;EACpC,MAAM,WAAW,KAAK;AACtB,MAAI,SAAS,cAAc,IAAI,aAAa,SAAS,YAAY,IAAI,WAAW,SAAS,SAAS,IAAI,KACpG,QAAO;;AAGX,QAAO;;AAGT,MAAM,qBAAqB,OACzB,EAAE,WACF,YACkB;CAClB,MAAM,OAAO,MAAM,gBAAgB,QAAQ,IAAI;AAC/C,MAAK,MAAM,OAAO,KAChB,KAAI,CAAC,eAAe,KAAK,QAAQ,UAAU,CACzC,SAAQ,UAAU,OAAO,IAAI;;;;;AAYnC,MAAM,wBAAwB,OAC5B,EAAE,gBACF,YAKkB;CAClB,MAAM,QAAQ,sBAAsB,aAAa;AACjD,KAAI,CAAC,SAAS,CAAC,gBAAgB,MAAM,CAAE;CAEvC,MAAM,EAAE,aAAa;AACrB,KAAI,SAAS,WAAW,EAAG;CAE3B,MAAM,UAAU,MAAM;CAItB,MAAM,YAAY,IAAI,KAAK,aAAa,kBAAkB,UAAU,YAAY,EAAE,EAAE,KAAK,MAAM,EAAE,GAAG,CAAC;CAKrG,MAAM,iBAAiB,kBAAkB,UAJnB,SAAS,QAAQ,MAAM,UAAU,IAAI,EAAE,GAAG,CAAC,EAG7C,aAAa,eAAe,EAC8B,CAAC,QAAQ,MAAM,CAAC,UAAU,IAAI,EAAE,GAAG,CAAC;AAElH,KAAI,eAAe,WAAW,EAAG;AAGjC,MAAK,MAAM,WAAW,gBAAgB;AACpC,MAAI,QAAQ,OAAO,QAAS;AAE5B,MAAI;AACF,SAAM,mBAAmB,EAAE,SAAS,EAAE,EAAE,WAAW,QAAQ,WAAW,CAAC;GAIvE,MAAM,SAAS,QAAQ,MAAM,QAAQ,mBAAmB,EAAE;GAC1D,MAAM,aAAa,OAAO,YAAY,EAAE,UAAU,EAAE,EAAE;AACtD,WAAQ,MAAM,MAAM,EAClB,iBAAiB;IACf,GAAG;KACF,UAAU,EAAE,UAAU,CAAC,GAAG,WAAW,UAAU,EAAE,IAAI,QAAQ,IAAI,CAAC,EAAE;IACtE,EACF,CAAC;WACK,OAAO;AACd,OAAI,iBAAiB,SAAS,MAAM,SAAS,aAAc;AAC3D,WAAQ,MAAM,+BAA+B,MAAM;;;AASvD,KAAI,QAAQ,UAAU,SAAS,aAAa,QAAQ,UAAU,KAC5D,OAAM,KAAK,QAAQ,UAAU,KAAK,CAAC,SAAS,QAAQ;AAClD,UAAQ,UAAU,OAAO,IAAI;GAC7B;AAIJ,OAAM,IAAI,SAAS,YAAY,sBAAsB,QAAQ,CAAC;;;;;AA4ChE,SAAS,sBAAsB,OAAwD;AACrF,KAAI,CAAC,MAAM,gBAAgB,CAAC,MAAM,oBAChC;CAGF,MAAM,UAAU,MAAM,aAAa,cAAc,MAAM,QAAQ,IAAI,SAAS,OAAO;AACnF,KAAI,CAAC,SAAS,gBAAgB,IAAI,OAChC;AAKF,QAFc,QAAQ,cAAc,GAAG,OAAO,MAAM,MAAM,EAAE,OAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;AAkB/F,SAAS,+BACP,OACA,QACkC;CAClC,MAAM,UAAU,MAAM;AACtB,KAAI,CAAC,WAAW,CAAC,OAAO,WACtB;AAIF,QADqB,OAAO,WAAW,IAAI,QAAQ,EAC9B;;;;;;;;;;AAWvB,SAAgB,qBAAqB,OAAiC,QAA4C;AAChH,QAAO,CAAC,CAAC,MAAM,uBAAuB,CAAC,CAAC,OAAO,cAAc,OAAO,WAAW,IAAI,MAAM,oBAAoB;;;;;;;;;;AAW/G,SAAgB,wBAAwB,OAAiC,QAA4C;AACnH,KAAI,CAAC,qBAAqB,OAAO,OAAO,CACtC,QAAO;CAGT,MAAM,QAAQ,sBAAsB,MAAM;AAC1C,KAAI,CAAC,SAAS,CAAC,gBAAgB,MAAM,IAAI,MAAM,SAAS,WAAW,EACjE,QAAO;AAIT,KAAI,CADc,+BAA+B,OAAO,OAAO,CAE7D,QAAO;AAGT,QAAO;;;;;;;;;;;;;;;;;AAkBT,SAAgB,kBAAkB,EAChC,OACA,UAIa;CACb,IAAIC,cAAoC;CACxC,IAAIC,kBAA0C;CAC9C,IAAIC;CAEJ,MAAM,UAAU,cAAc,CAAC,OAAO,OAAO,CAAC,CAAC,UAC7C,OAAO,CAAC,cAAc,mBAA0E;AAI9F,MAAI,aAAa,wBAAwB,aAAa;AACpD,iBAAc,aAAa;AAC3B,oBAAiB,OAAO;AACxB,iBAAc;;AAGhB,MAAI,YAAa;AACjB,MAAI,CAAC,wBAAwB,cAAc,cAAc,CAAE;EAE3D,MAAM,YAAY,+BAA+B,cAAc,cAAc;AAC7E,MAAI,CAAC,UAAW;AAGhB,oBAAkB,IAAI,iBAAiB;AACvC,gBAAc,sBACZ,EAAE,cAAc,EAChB;GAAE,QAAQ,gBAAgB;GAAQ;GAAW;GAAO,CACrD,CAAC,cAAc;AACd,iBAAc;IACd;GAEL;AAED,cAAa;AACX,mBAAiB,OAAO;AACxB,WAAS;;;;;;;;;;;;;;;;;;;;;AC7Ob,SAAgB,iBAAiB,EAC/B,OACA,UAIa;CACb,IAAIC;CACJ,IAAIC,kBAAuC;CAE3C,MAAM,cAAc,OAAO,WAAW,kBAAkB;EACtD,MAAM,EAAE,iBAAiB;AAEzB,MAAI,iBAAiB,iBAAkB;AAEvC,qBAAmB;AACnB,oBAAkB;AAClB,qBAAmB;AAEnB,MAAI,CAAC,aAAc;AAGnB,QAAM,MAAM,EAAE,aAAa,aAAa,aAAa,CAAC;EAEtD,MAAM,aAAa,MAAM,MAAM,EAAE,aAAa,aAAa,aAAa,CAAC;EACzE,MAAM,mBAAmB,OAAO,cAAc,cAAc,KAAK;EACjE,MAAM,gBAAgB,OAAO,cAAc,WAAW,KAAK;AAC3D,0BAAwB;AACtB,qBAAkB;AAClB,kBAAe;;GAEjB;AAEF,cAAa;AACX,qBAAmB;AACnB,eAAa;;;;;;;;;;;;;;;;;;;;;;;;;ACpCjB,SAAgB,uBAAuB,EACrC,OACA,QACA,UAKa;CACb,IAAIC;CACJ,IAAIC,iBAAsC;CAC1C,IAAIC;CAGJ,MAAM,mBAAmB,MAAM,WAAW,iBAAiB;EACzD,MAAM,MAAM,aAAa,cAAc;AACvC,MAAI,QAAQ,qBAAqB;AAC/B,OAAI,wBAAwB,OAE1B,OAAM,MAAM,EAAE,mBAAmB,OAAO,CAAC;AAE3C,yBAAsB;;GAExB;CAGF,MAAM,oBAAoB,OAAO,WAAW,kBAAkB;EAC5D,MAAM,EAAE,iBAAiB;AAEzB,MAAI,iBAAiB,iBAAkB;AAEvC,oBAAkB;AAClB,mBAAiB;AACjB,qBAAmB;AAEnB,MAAI,CAAC,aAAc;AAEnB,mBAAiB,OAAO,cAAc,cAAc;AAClD,SAAM,MAAM,EAAE,mBAAmB,MAAM,CAAC;AACxC,UAAO,SAAS,EAAE,MAAM,QAAQ,CAAC;IACjC;GACF;AAEF,cAAa;AACX,oBAAkB;AAClB,oBAAkB;AAClB,qBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;ACnEvB,eAAsB,cAAc,cAA4B,aAAyC;AAEvG,KAAI,aAAa,SACf,OAAM,IAAI,SAAe,YAAY;EACnC,MAAM,oBAAoB;AACxB,gBAAa,oBAAoB,aAAa,YAAY;AAC1D,YAAS;;AAEX,eAAa,iBAAiB,aAAa,YAAY;GACvD;AAIJ,QAAO,IAAI,SAAe,SAAS,WAAW;EAC5C,MAAM,oBAAoB;AACxB,YAAS;AACT,YAAS;;EAGX,MAAM,WAAW,UAAiB;AAChC,YAAS;AACT,0BAAO,IAAI,MAAM,8BAA8B,MAAM,OAAO,CAAC;;EAG/D,MAAM,gBAAgB;AACpB,gBAAa,oBAAoB,aAAa,YAAY;AAC1D,gBAAa,oBAAoB,SAAS,QAAQ;;AAGpD,eAAa,iBAAiB,aAAa,YAAY;AACvD,eAAa,iBAAiB,SAAS,QAAQ;AAE/C,MAAI;AACF,gBAAa,aAAa,YAAY;WAC/B,OAAO;AACd,YAAS;AACT,UAAO,MAAM;;GAEf;;;;;;;;;;;;;;;;;;;;;;;;ACtCJ,eAAsB,YAAY,cAA4B,OAAe,KAA4B;AAEvG,KAAI,aAAa,SACf,OAAM,IAAI,SAAe,YAAY;EACnC,MAAM,oBAAoB;AACxB,gBAAa,oBAAoB,aAAa,YAAY;AAC1D,YAAS;;AAEX,eAAa,iBAAiB,aAAa,YAAY;GACvD;AAGJ,QAAO,IAAI,SAAe,SAAS,WAAW;EAC5C,MAAM,oBAAoB;AACxB,YAAS;AACT,YAAS;;EAGX,MAAM,WAAW,UAAiB;AAChC,YAAS;AACT,0BAAO,IAAI,MAAM,8BAA8B,MAAM,OAAO,CAAC;;EAG/D,MAAM,gBAAgB;AACpB,gBAAa,oBAAoB,aAAa,YAAY;AAC1D,gBAAa,oBAAoB,SAAS,QAAQ;;AAGpD,eAAa,iBAAiB,aAAa,YAAY;AACvD,eAAa,iBAAiB,SAAS,QAAQ;AAE/C,MAAI;AACF,gBAAa,OAAO,OAAO,IAAI;WACxB,OAAO;AACd,YAAS;AACT,UAAO,MAAM;;GAEf;;;;;;;;AC1CJ,SAAgB,qBAAqB,OAA2C;AAC9E,KAAI,CAAC,MAAM,aAAc,QAAO;AAEhC,QAAO,CAAC,EAAE,MAAM,wBAAwB,MAAM;;;;;AAMhD,SAAgB,wBAAwB,OAA2C;AACjF,KAAI,CAAC,qBAAqB,MAAM,CAAE,QAAO;CAEzC,MAAM,EAAE,iBAAiB;AAGzB,KAAI,aAAc,aAAa,OAAW,QAAO;CAGjD,MAAM,aAAa,MAAM,uBAAuB,iBAAiB,OAAO,QAAQ,GAAG;CACnF,MAAM,aAAa,MAAM,uBAAuB,iBAAiB,OAAO,QAAQ,GAAG;AAGnF,QAAO,CAAC,EAAG,cAAc,gBAAgB,WAAW,IAAM,cAAc,gBAAgB,WAAW;;;;;AAMrG,SAAgB,8BAA8B,OAAsD;CAElG,MAAM,aAAa,MAAM,uBACpB,iBAAiB,OAAO,QAAQ,GACjC;AACJ,KAAI,cAAc,gBAAgB,WAAW,CAC3C,QAAO,WAAW;CAIpB,MAAM,aAAa,MAAM,uBACpB,iBAAiB,OAAO,QAAQ,GACjC;AACJ,KAAI,cAAc,gBAAgB,WAAW,CAC3C,QAAO,WAAW;;;;;AAStB,SAAgB,8BAA8B,EAC5C,SAGa;AACb,QAAO,cAAc,CAAC,MAAM,CAAC,CAAC,WAAW,CAAC,kBAA+C;AACvF,MAAI,CAAC,wBAAwB,aAAa,CAAE;EAE5C,MAAM,WAAW,8BAA8B,aAAa;AAC5D,MAAI,aAAa,UAAa,CAAC,OAAO,SAAS,SAAS,CAAE;EAE1D,MAAM,EAAE,iBAAiB;AAGzB,QAAM,MAAM,EACV,cAAc;GACZ,GAAG;GACH;GACD,EACF,CAAC;GACF;;;;;;;;;;;;;;;;;ACvBJ,IAAa,OAAb,MAAuF;CACrF,AAAS;CACT,CAASC;CACT,CAASC,kBAAmB,IAAI,iBAAiB;CACjD,CAASC;CAET,UAAsB;CACtB,SAA6B;CAC7B,SAA6B;CAE7B,YAAY,OAAiD,QAAqB;AAChF,QAAKF,QAAS;EACd,MAAM,QAAQ,QAAQ;AACtB,OAAK,KAAK,OAAO,UAAU,aAAa,OAAO,GAAI,SAAS,YAAY;AACxE,QAAKE,SAAU,QAAQ,SACnB,YAAY,IAAI,CAAC,MAAKD,gBAAiB,QAAQ,OAAO,OAAO,CAAC,GAC9D,MAAKA,gBAAiB;;CAG5B,IAAI,SAAqB;AACvB,SAAO,MAAKE;;CAGd,IAAI,QAA0C;AAC5C,SAAO,MAAKC;;CAGd,IAAI,QAA0C;AAC5C,SAAO,MAAKC;;CAGd,MAAM,MAAuB;AAC3B,QAAKF,SAAU;AACf,MAAI;GACF,MAAM,SAAS,MAAM,MAAKH,MAAO,MAAKE,OAAQ;AAC9C,SAAKE,QAAS;AACd,SAAKD,SAAU;AACf,UAAO;WACA,GAAG;AACV,SAAKE,QAAS;AACd,SAAKF,SAAU;AACf,SAAM;;;CAIV,QAAc;AACZ,QAAKF,gBAAiB,OAAO;;;;;;;;;;AAejC,IAAa,mBAAb,MAA8B;CAC5B,CAASK,0BAAW,IAAI,KAAyC;CAEjE,SAA0C,MAAsC;AAC9E,MAAI,MAAKA,QAAS,IAAI,KAAK,GAAG,CAAE;AAEhC,QAAKA,QAAS,IAAI,KAAK,IAAI,KAAmC;AAC9D,OACG,KAAK,CACL,OAAO,UAAU;AAChB,OAAI,EAAE,iBAAiB,SAAS,MAAM,SAAS,cAAe,OAAM;IACpE,CACD,cAAc;AACb,SAAKA,QAAS,OAAO,KAAK,GAAG;IAC7B;;CAGN,WAAiB;AACf,OAAK,MAAM,QAAQ,MAAKA,QAAS,QAAQ,CAAE,MAAK,OAAO;AACvD,QAAKA,QAAS,OAAO;;;;;;;;;;;;;;;;;AAsBzB,IAAa,eAAb,MAA0B;CACxB,SAA2B,QAAQ,SAAS;CAC5C,CAASA,0BAAW,IAAI,KAAiC;CACzD,WAA8C;CAE9C,SAA0C,MAAiD;EACzF,MAAM,IAAI;AACV,QAAKA,QAAS,IAAI,EAAE;EAEpB,MAAM,SAAS,MAAKC,MACjB,WAAW;AACV,SAAKD,QAAS,OAAO,EAAE;AACvB,SAAKE,UAAW;AAChB,UAAO,KAAK,KAAK;IACjB,CACD,cAAc;AACb,SAAKA,UAAW;IAChB;AAGJ,QAAKD,QAAS,OAAO,WACb,UACA,GACP;AAED,SAAO;;CAGT,WAAiB;AACf,OAAK,MAAM,QAAQ,MAAKD,QAAU,MAAK,OAAO;AAC9C,QAAKA,QAAS,OAAO;AACrB,QAAKE,SAAU,OAAO;;CAGxB,UAAgB;AACd,OAAK,UAAU;;;;;;ACpLnB,SAAgB,WACd,OACA,QACS;CACT,MAAM,QAAQ,iBAAiB,OAAO,OAAO,KAAK;AAClD,KAAI,CAAC,MAAO,QAAO;AAEnB,QAAO,CAAC,gBAAgB,MAAM;;;;;;;;;;;;AAahC,SAAgB,cAAc,QAA8B,QAAwC;AAClG,QAAO;;;;;;AAWT,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;;;;;;;;;;;;;AA2BH,SAAgB,aACd,EACE,OACA,UAKF,QACY;CAIZ,MAAM,SAAS,IAAI,kBAAkB;CAErC,MAAM,UAAU,cAAc,CAAC,OAAO,OAAO,CAAC,CAAC,WAAW,CAAC,cAAc,WAAW;AAClF,MAAI,CAAC,WAAW,cAAc,OAAO,IAAI,CAAC,cAAc,cAAc,MAAM,CAAE;EAE9E,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;GAc1D,MAAM,qBAAqB,MAAM,QAAQ;GACzC,MAAM,sBAAsB,0BAA0B,oBAAoB,WAAW;AACrF,SAAM,MAAM,EAAE,cAAc,qBAAqB,CAAC;KAEpD,EAAE,IAAI,MAAM,IAAI,CACjB,CACF;GACD;AAEF,cAAa;AACX,SAAO,UAAU;AACjB,WAAS;;;;;;;;;;;;;;;;;;;;ACoDb,SAAgB,cAAc,cAA4B,QAAiD;CACzG,MAAM,UAAU,aAAa,cAAc,MAAM,QAAQ,IAAI,SAAS,OAAO;AAC7E,KAAI,CAAC,SAAS,gBAAgB,IAAI,OAAO,OAAQ,QAAO;CAExD,MAAM,SAAS,QAAQ,cAAc,GAAG;CAIxC,MAAM,kBAAkB,OAAO,sBAAsB,SAAS,OAAO,QAAQ,UAAU,CAAC,MAAM,OAAO;AAErG,KAAI,gBAAgB,WAAW,EAAG,QAAO;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,EACE,SAMF,SAA+B,EAAE,MAAM,SAAS,EACpC;CACZ,IAAI,YAAY;AAEhB,QAAO,MAAM,UAAU,OAAO,iBAAsC;AAClE,MAAI,CAAC,eAAe,cAAc,OAAO,IAAI,CAAC,kBAAkB,cAAc,OAAO,IAAI,UAAW;AAEpG,MAAI;AACF,eAAY;GAGZ,MAAM,kBAAkB,aAAa,cAAc,cAAc,MAAM,EAAE,WAAW,SAAS,OAAO,KAAK,EACrG,cAAc,IAAI,OAAO,IAAI;AAEjC,OAAI,iBAAiB;IACnB,MAAM,mBAAmB,yBAAyB,OAAO;AACzD,UAAM,MAAM,GAAG,mBAAmB,iBAAiB,CAAC;;YAE9C;AACR,eAAY;;GAEd;;;;;;;;;;;;;;;;;AAkBJ,SAAgB,iBACd,EACE,SAMF,SAA+B,EAAE,MAAM,SAAS,EACpC;CACZ,IAAI,YAAY;AAEhB,QAAO,MAAM,UAAU,OAAO,iBAAsC;AAClE,MAAI,CAAC,eAAe,cAAc,OAAO,IAAI,CAAC,kBAAkB,cAAc,OAAO,IAAI,UAAW;AAEpG,MAAI;AACF,eAAY;GAGZ,MAAM,kBAAkB,aAAa,cAAc,cAAc,MAAM,EAAE,WAAW,SAAS,QAAQ,EACjG,cAAc,IAAI,OAAO,IAAI;AAEjC,OAAI,gBACF,OAAM,MAAM,EAAE,sBAAsB,iBAAiB,CAAC;YAEhD;AACR,eAAY;;GAEd;;;;;;;;;;;;;;AAeJ,SAAgB,gBACd,EACE,SAMF,SAA8B,EAAE,MAAM,QAAQ,EAClC;CACZ,IAAI,YAAY;AAEhB,QAAO,MAAM,UAAU,OAAO,iBAAsC;AAClE,MAAI,CAAC,eAAe,cAAc,OAAO,IAAI,CAAC,kBAAkB,cAAc,OAAO,IAAI,UAAW;AAEpG,MAAI;AACF,eAAY;GAGZ,MAAM,sBAAsB,cAAc,aAAa,cAAe,OAAO;AAE7E,OAAI,oBACF,OAAM,MAAM,EAAE,qBAAqB,CAAC;YAE9B;AACR,eAAY;;GAEd;;;;;;;;;;;;ACvVJ,SAAS,sBAAsB,UAAqC,OAA+C;AACjH,KAAI,SAAS,WAAW,EAAG,QAAO;CAClC,MAAM,UAAU,SAAS,SAAS,SAAS;AAC3C,KAAI,CAAC,QAAS,QAAO;AACrB,QAAO,OAAO,SAAS,QAAQ,SAAS,MAAM,MAAM,EAAE,OAAO,QAAQ,GAAG,IAAI;;;;;;;;AAS9E,SAAgB,qBAAqB,OAAyB,QAAoC;CAChG,MAAM,aAAa,MAAM,uBAAuB,iBAAiB,OAAO,QAAQ,GAAG;CACnF,MAAM,aAAa,MAAM,uBAAuB,iBAAiB,OAAO,QAAQ,GAAG;AAMnF,KAAI,cAAc,CAAC,gBAAgB,WAAW,CAAE,QAAO;AACvD,KAAI,cAAc,CAAC,gBAAgB,WAAW,CAAE,QAAO;AAEvD,KAAI,cAAc,gBAAgB,WAAW,EAC3C;MAAI,CAAC,sBAAsB,WAAW,UAAU,OAAO,iBAAiB,CAAE,QAAO;;AAGnF,KAAI,cAAc,gBAAgB,WAAW,EAC3C;MAAI,CAAC,sBAAsB,WAAW,UAAU,OAAO,iBAAiB,CAAE,QAAO;;AAGnF,QAAO;;;;;AAMT,SAAgB,aAAa,OAAyB,QAAoC;AACxF,QAAO,CAAC,EAAE,OAAO,eAAe,MAAM;;;;;AAMxC,SAAgB,gBAAgB,OAAyB,QAAoC;AAC3F,KAAI,CAAC,aAAa,OAAO,OAAO,CAAE,QAAO;CAEzC,MAAM,EAAE,aAAa,iBAAiB;AAGtC,KAAI,YAAa,eAAe,OAAQ,QAAO;AAK/C,KAAI,gBAAgB,aAAa,aAAa,iBAAiB,cAC7D,QAAO;CAKT,MAAM,gBAAgB,CAAC,CAAC,MAAM;CAC9B,MAAM,gBAAgB,CAAC,CAAC,MAAM;AAE9B,KAAI,iBAAiB,CAAC,OAAO,YAAa,QAAO;AACjD,KAAI,iBAAiB,CAAC,OAAO,YAAa,QAAO;AAKjD,KAAI,OAAO,kBAAkB,SAAS,WAAW,WAAY,QAAO;AACpE,KAAI,OAAO,kBAAkB,SAAS,WAAW,WAAY,QAAO;AAGpE,KAAI,CAAC,qBAAqB,OAAO,OAAO,CAAE,QAAO;AAMjD,KAAI,cAAc;EAChB,MAAM,aAAa,gBAAgB,iBAAiB,OAAO,QAAQ,GAAG;EACtE,MAAM,aAAa,gBAAgB,iBAAiB,OAAO,QAAQ,GAAG;EACtE,MAAM,WACJ,cAAc,gBAAgB,WAAW,GACrC,aACA,cAAc,gBAAgB,WAAW,GACvC,aACA;AACR,MAAI,YAAY,SAAS,SAAS,SAAS,GAAG;GAC5C,MAAM,UAAU,SAAS,SAAS,SAAS,SAAS,SAAS;AAC7D,OAAI,aAAa,cAAc,QAAQ,UAAW,QAAO;;;AAI7D,QAAO;;;;;;;AAQT,SAASC,4BAA0B,QAA0C;CAC3E,MAAM,iBAAiB,CAAC,OAAO,kBAAkB,OAAO,iBAAiB,CAAC,QACvE,UAAsC,UAAU,UAAa,MAAM,SAAS,WAAW,WACzF;AAED,KAAI,eAAe,WAAW,EAAG,QAAO,QAAQ,SAAS;AAEzD,QAAO,QAAQ,IACb,eAAe,KACZ,UACC,IAAI,SAAe,YAAY;EAC7B,MAAM,QAAQ,MAAM,WAAW,aAAa;AAC1C,OAAI,SAAS,WAAW,YAAY;AAClC,WAAO;AACP,aAAS;;IAEX;GACF,CACL,CACF,CAAC,WAAW,OAAU;;;;;;;AAQzB,SAASC,oBAAkB,QAAmC;CAC5D,IAAI,MAAM;AACV,MAAK,MAAM,OAAO,CAAC,OAAO,aAAa,OAAO,YAAY,CACxD,KAAI,OAAO,IAAI,SAAS,SAAS,GAAG;EAClC,MAAM,MAAM,IAAI,SAAS,IAAI,IAAI,SAAS,SAAS,EAAE;AACrD,MAAI,MAAM,IAAK,OAAM;;AAGzB,QAAO;;;;;;AAOT,MAAM,kBAAkB,OACtB,EAAE,iBACF,aACkB;CAClB,MAAM,EAAE,gBAAgB;AAGxB,KAAI,YAAa,eAAe,QAC9B;AAKF,OAAMD,4BAA0B,cAAc;AAG9C,KAAI,YAAa,eAAe,OAAQ;CAOxC,MAAM,cAAcC,oBAAkB,cAAc;AACpD,KAAI,cAAc,EAChB,aAAa,WAAW;AAG1B,aAAa,aAAa;AAG1B,OAAM,IAAI,SAAS,YAAY,sBAAsB,QAAQ,CAAC;;;;;;;;;;;AAYhE,SAAgB,YAAY,EAC1B,OACA,UAIa;CACb,IAAI,WAAW;CACf,IAAI,YAAY;CAChB,MAAMC,oBAAuC,EAAE;CAE/C,MAAM,cAAc,YAAY;AAC9B,MAAI,UAAW;EACf,MAAM,eAAe,MAAM;EAC3B,MAAM,gBAAgB,OAAO;AAC7B,MAAI,UAAU;AAKZ,OAAI,cAAc,aAAa,eAAe,OAAQ;AACtD,cAAW;;AAEb,MAAI,CAAC,gBAAgB,cAAc,cAAc,CAAE;AAInD,aAAW;AACX,MAAI;AACF,SAAM,gBAAgB,EAAE,eAAe,EAAE,EAAE,CAAC;WACrC,OAAO;AACd,WAAQ,MAAM,+BAA+B,MAAM;;;CAOvD,MAAM,gBAAgB,OAAO,WAAW,kBAAkB;AACxD,oBAAkB,SAAS,MAAM,GAAG,CAAC;AACrC,oBAAkB,SAAS;AAE3B,OAAK,MAAM,SAAS,CAAC,cAAc,kBAAkB,cAAc,iBAAiB,EAAE;AACpF,OAAI,CAAC,MAAO;GAGZ,IAAI,UAAU;AACd,qBAAkB,KAChB,MAAM,gBAAgB;AACpB,QAAI,SAAS;AACX,eAAU;AACV;;AAEF,iBAAa;KACb,CACH;;GAEH;CAEF,MAAM,uBAAuB,cAAc,CAAC,OAAO,OAAO,CAAC,CAAC,UAAU,YAAY,aAAa,CAAC;AAEhG,cAAa;AACX,cAAY;AACZ,oBAAkB,SAAS,MAAM,GAAG,CAAC;AACrC,iBAAe;AACf,wBAAsB;;;;;;;;;ACjS1B,SAAgB,SAAS,OAAyB,QAAoC;AACpF,QAAO,CAAC,MAAM,OAAO,aAAa,IAAI,CAAC,MAAM,MAAM,cAAc,IAAI;;;;;;AAOvE,SAAgB,YAAY,QAA0B,QAAoC;AAExF,QAAO,MAAM,OAAO,YAAY;;;;;;;;;;;AAYlC,SAAgB,iBAAiB,EAC/B,OACA,UAIa;CACb,IAAI,YAAY;CAChB,IAAIC,kBAA0C;CAE9C,MAAM,cAAc,cAAc,CAAC,OAAO,OAAO,CAAC,CAAC,UACjD,OAAO,CAAC,cAAc,mBAA0D;AAC9E,MAAI,CAAC,SAAS,cAAc,cAAc,IAAI,CAAC,YAAY,cAAc,cAAc,IAAI,UAAW;AAEtG,MAAI;AACF,eAAY;AACZ,qBAAkB,IAAI,iBAAiB;GAIvC,MAAM,cAAc,kBAAkB,EAAE,eAAe,MAAM,CAAC;AAG9D,qBAAkB,aAAa,cAAc,aAAc;AAG3D,SAAM,kBAAkB,aAAa,gBAAgB,OAAO;AAE5D,UAAO,MAAM,EAAE,aAAa,CAAC;WACtB,OAAO;AACd,OAAI,iBAAiB,gBAAgB,MAAM,SAAS,aAAc;AAClE,SAAM;YACE;AACR,eAAY;;GAGjB;AAED,cAAa;AACX,mBAAiB,OAAO;AACxB,eAAa;;;;;;;;;;AC1CjB,IAAa,yBAAb,cAA4C,MAAM;CAChD,YAAY,SAAiB;AAC3B,QAAM,QAAQ;AACd,OAAK,OAAO;;;AAchB,SAAS,iBAAiB,UAAuC;CAC/D,MAAMC,SAA0B,EAAE;AAClC,MAAK,IAAI,IAAI,GAAG,IAAI,SAAS,QAAQ,IACnC,QAAO,KAAK;EAAE,OAAO,SAAS,MAAM,EAAE;EAAE,KAAK,SAAS,IAAI,EAAE;EAAE,CAAC;AAEjE,QAAO;;AAiBT,SAAS,eACP,SACA,EAAE,QAAQ,QAAQ,gBACc;AAChC,QAAO,IAAI,KACT,OAAO,eAAe;EACpB,MAAM,MAAM,QAAQ;AACpB,MAAI,WAAW,QAAS,QAAO;AAC/B,QAAM,cAAc,cAAc,QAAQ,KAAK;AAG/C,SAAO;GAAE,GAAG;GAAK,aAAa,QAAQ,KAAK;GAAS;IAEtD,EAAE,QAAQ,CACX;;AAGH,SAAS,kBACP,SACA,EAAE,QAAQ,QAAQ,gBACc;AAChC,QAAO,IAAI,KACT,OAAO,eAAe;EACpB,MAAM,MAAM,QAAQ;AACpB,MAAI,WAAW,QAAS,QAAO;AAC/B,QAAM,cAAc,cAAc,QAAQ,KAAK;EAG/C,MAAM,EAAE,SAAS;EAKjB,MAAM,UAAU;EAChB,MAAM,WAAW,IAAI,SAAS,QAAQ,MAAM,KAAK,IAAI,EAAE,YAAY,KAAK,UAAU,IAAI,QAAQ;AAC9F,SAAO;GACL,GAAG;GACH,UAAU,CACR,GAAG,UACH;IACE,IAAI,KAAK;IACT,WAAW,KAAK;IAChB,UAAU,KAAK;IACf,SAAS,KAAK;IACd,GAAI,KAAK,mBAAmB,UAAa,EAAE,gBAAgB,KAAK,gBAAgB;IACjF,CACF;GACD,gBAAgB,iBAAiB,aAAa,SAAS;GACxD;IAEH,EAAE,QAAQ,CACX;;AAGH,SAAS,WACP,SACA,EAAE,QAAQ,QAAQ,gBACc;AAChC,QAAO,IAAI,KACT,OAAO,eAAe;EACpB,MAAM,MAAM,QAAQ;AACpB,MAAI,WAAW,QAAS,QAAO;AAC/B,QAAM,YAAY,cAAc,QAAQ,OAAO,QAAQ,IAAI;EAQ3D,MAAM,iBAAiB,iBAAiB,aAAa,SAAS;EAC9D,MAAM,WAAW,IAAI,SAAS,QAAQ,MAAM;GAC1C,MAAM,WAAW,EAAE,YAAY,EAAE,WAAW;AAC5C,UAAO,eAAe,MAAM,MAAM,YAAY,EAAE,SAAS,WAAW,EAAE,IAAI;IAC1E;AACF,SAAO;GAAE,GAAG;GAAK,UAAU;GAAU;GAAgB;IAEvD,EAAE,QAAQ,CACX;;AAQH,MAAM,uBAAuB;CAC3B,eAAe;CACf,kBAAkB;CAClB,QAAQ;CACT;AAED,SAAS,cAAc,SAA8B,SAA6D;CAChH,MAAM,UAAU,qBAAqB,QAAQ;AAC7C,QAAO,QAAQ,SAAS,QAAQ;;AAOlC,SAAgB,wBACd,cACA,gBACmB;CACnB,MAAM,QAAQ,YAAuC;EACnD,QAAQ;EACR,SAAS;GAAE,UAAU,EAAE;GAAE,gBAAgB,EAAE;GAAE,aAAa;GAAW,GAAG;GAAgB;EACzF,CAAC;CAEF,MAAM,SAAS,IAAI,cAAc;CAOjC,SAAS,YAAY,YAA4C;EAC/D,MAAM,SAAS,MAAM,QAAQ,WAAW,cAAc,cAAc;AACpE,QAAM,MAAM;GAAE;GAAQ,SAAS;GAAY,CAAC;AAC5C,QAAM,OAAO;;CAGf,SAAS,YAAY,GAAmB;EAMtC,MAAM,SAAS,MAAM,QAAQ,WAAW,cAAc,cAAc;AACpE,QAAM,MAAM,EAAE,QAAQ,CAAC;AACvB,QAAM,OAAO;AACb,QAAM;;AAGR,QAAO;EACL,IAAI,WAAsC;AACxC,UAAO,MAAM;;EAGf,UAAU,UAAqE;AAC7E,UAAO,MAAM,UAAU,SAAS;;EAGlC,KAAK,SAA8B,QAAoC;AACrE,OAAI,MAAM,QAAQ,WAAW,OAC3B,QAAO,QAAQ,OAAO,IAAI,uBAAuB,gCAAgC,MAAM,QAAQ,SAAS,CAAC;AAK3G,SAAM,MAAM,EAAE,QAAQ,YAAY,CAAC;GAEnC,MAAM,OAAO,cAAc,SAAS;IAAE;IAAQ,cAAc,MAAM,QAAQ;IAAS;IAAc,CAAC;AAElG,UAAO,OAAO,SAAS,KAAK,CAAC,KAAK,YAAY,CAAC,MAAM,YAAY;;EAGnE,MAAM,UAAiC,QAAoC;AACzE,OAAI,MAAM,QAAQ,WAAW,OAC3B,QAAO,QAAQ,OAAO,IAAI,uBAAuB,iCAAiC,MAAM,QAAQ,SAAS,CAAC;AAG5G,OAAI,SAAS,WAAW,EAAG,QAAO,QAAQ,SAAS;AAGnD,SAAM,MAAM,EAAE,QAAQ,YAAY,CAAC;GAgBnC,IAAI,aAAa,MAAM,QAAQ;AAE/B,QAAK,MAAM,WAAW,SAAS,MAAM,GAAG,GAAG,EAAE;IAC3C,MAAM,OAAO,cAAc,SAAS;KAAE;KAAQ,cAAc;KAAY;KAAc,CAAC;AAEvF,IADe,OAAO,SAAS,KAAK,CAC7B,MAAM,WAAW;AACtB,kBAAa;MACb;;GAGJ,MAAM,WAAW,cAAc,SAAS,SAAS,SAAS,IAAK;IAC7D;IACA,cAAc;IACd;IACD,CAAC;AACF,UAAO,OAAO,SAAS,SAAS,CAAC,KAAK,YAAY,CAAC,MAAM,YAAY;;EAGvE,UAAgB;AACd,SAAM,MAAM,EAAE,QAAQ,aAAa,CAAC;AACpC,SAAM,OAAO;AACb,UAAO,SAAS;;EAEnB;;;;;;ACtRH,MAAM,iBAAiB;CACrB,OAAO;CACP,OAAO;CACR;;;;;AAYD,MAAM,wBAAwB,OAC5B,EAAE,cAAc,iBAChB,YACkB;CAElB,MAAM,QAAQ,iBAAiB,cAAc,QAAQ,OAAO,KAAK;AACjE,KAAI,CAAC,SAAS,CAAC,gBAAgB,MAAM,CAAE;AACvC,KAAI,CAAC,MAAM,UAAU,MAAM,OAAO,WAAW,EAAG;CAEhD,MAAM,YAAY,eAAe,MAAM;CAIvC,MAAM,SAAS,mBAAmB,cAAc,aAAc,UAAU;CACxE,MAAM,QAAQ,wBAAwB,OAAO;CAE7C,MAAM,YAAY,gBAAgB,QAAQ,OAAO;CACjD,MAAM,WAAW,eAAe,QAAQ,OAAO;AAC/C,SAAQ,OAAO,MAAM;GAAG,YAAY;GAAS,WAAW;EAAO,CAAC;AAGhE,OAAM,IAAI,SAAS,YAAY,sBAAsB,QAAQ,CAAC;;;;;;;;;;;;AAiChE,SAAgB,eAAe,OAA8B;CAC3D,MAAM,cAAc,MAAM,QAAQ,KAAK,IAAI,IAAI;AAC/C,QAAO,GAAG,MAAM,SAAS,YAAY,YAAY;;;;;;;;;;;;;;;;;AAkBnD,SAAgB,eAAe,OAA0B,QAA4B,MAA+B;AAElH,KAAI,CAAC,OAAO,YACV,QAAO;AAKT,KAAI,CADU,iBAAiB,OAAO,KAAK,CAEzC,QAAO;AAGT,QAAO;;;;;AAMT,SAAgB,kBAAkB,QAA4B,MAA+B;CAC3F,MAAM,YAAY,gBAAgB;AAClC,QAAO,YAAY,OAAO,WAAW;;;;;;;;;;;;;;;;;;;AA2BvC,SAAgB,kBACd,EACE,OACA,UAKF,QACY;CACZ,IAAIC,cAAoC;AAqBxC,QAnBgB,cAAc,CAAC,OAAO,OAAO,CAAC,CAAC,UAC7C,OAAO,CAAC,cAAc,mBAA4D;AAEhF,MAAI,YAAa;AACjB,MAAI,CAAC,eAAe,cAAc,eAAe,OAAO,KAAK,IAAI,CAAC,kBAAkB,eAAe,OAAO,KAAK,CAC7G;AAGF,gBAAc,sBAAsB;GAAE;GAAc;GAAe,EAAE;GAAE;GAAQ;GAAQ,CAAC;AAExF,MAAI;AACF,SAAM;YACE;AAER,iBAAc;;GAGnB;;;;;;;;ACxJH,SAAS,cAAc,cAA2D;AAChF,KAAI,CAAC,cAAc,cAAe,QAAO,EAAE;CAE3C,MAAM,UAAU,aAAa,cAAc,MAAM,QAAQ,IAAI,SAAS,OAAO;AAC7E,KAAI,CAAC,SAAS,gBAAgB,IAAI,OAAQ,QAAO,EAAE;AAEnD,QAAO,QAAQ,cAAc,GAAG;;;;;;;;;AAUlC,SAAgB,mBAAmB,OAAuB,QAAkC;AAC1F,QAAO,CAAC,CAAC,OAAO,gBAAgB,CAAC,CAAC,cAAc,MAAM,aAAa,CAAC;;;;;AAMtE,SAAgB,sBAAsB,QAAkC;AACtE,QAAO,CAAC,OAAO;;;;;;;;AASjB,SAAS,mBAAmB,OAAqD;CAC/E,MAAM,eAAe,SAAS,cAAc,QAAQ;AAEpD,cAAa,KAAK,MAAM;AACxB,cAAa,OAAO,MAAM;AAC1B,cAAa,QAAQ,MAAM;AAE3B,KAAI,MAAM,SACR,cAAa,UAAU,MAAM;AAG/B,KAAI,MAAM,QACR,cAAa,UAAU;AAIzB,cAAa,MAAM,MAAM;AAEzB,QAAO;;;;;;;;;;;;;;;;;;AAmBT,SAAgB,gBAAgB,EAC9B,OACA,UAIa;CACb,IAAI,WAAW;CACf,IAAIC,gBAAoC,EAAE;CAE1C,MAAM,cAAc,cAAc,CAAC,OAAO,OAAO,CAAC,CAAC,WAAW,CAAC,GAAG,OAA0C;AAE1G,MAAI,SAAU;AACd,MAAI,CAAC,mBAAmB,GAAG,EAAE,IAAI,CAAC,sBAAsB,EAAE,CAAE;AAG5D,aAAW;EAEX,MAAM,aAAa,cAAc,EAAE,aAAa;AAChD,MAAI,WAAW,WAAW,EAAG;EAE7B,MAAM,2BAAW,IAAI,KAA+B;AAGpD,OAAK,MAAM,SAAS,YAAY;GAC9B,MAAM,eAAe,mBAAmB,MAAM;AAC9C,KAAE,aAAc,YAAY,aAAa;AACzC,YAAS,IAAI,MAAM,IAAI,aAAa;AACpC,iBAAc,KAAK,aAAa;;AAIlC,SAAO,MAAM,EAAE,YAAY,UAAU,CAAC;GACtC;AAGF,cAAa;AAEX,OAAK,MAAM,gBAAgB,cACzB,cAAa,QAAQ;AAEvB,kBAAgB,EAAE;AAClB,eAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;AC9FjB,SAAgB,6BAA6B,EAC3C,OACA,UAIa;CACb,IAAIC;CACJ,IAAIC,iBAAsC;CAE1C,MAAM,cAAc,OAAO,WAAW,kBAAkB;EACtD,MAAM,EAAE,iBAAiB;AAEzB,MAAI,iBAAiB,iBAAkB;AAEvC,oBAAkB;AAClB,mBAAiB;AACjB,qBAAmB;AAEnB,MAAI,CAAC,aAAc;EAEnB,MAAM,aAAa;GAOjB,MAAM,QANe,MAAM,KAAK,aAAa,WAAW,CAAC,MACtD,MAAM,EAAE,SAAS,cAAc,EAAE,SAAS,eAAe,EAAE,SAAS,YACtE,EAI2B,MAAM;GAClC,MAAM,UAAU,MAAM;AAItB,OAAI,QAAQ,wBAAwB,MAAO;AAE3C,OAAI,MACF,OAAM,MAAM,EAAE,qBAAqB,OAAO,CAAC;QACtC;IAKL,MAAM,SAAS,QAAQ;AACvB,QAAI,UAAU,QAAQ,kBAAkB,SAAS;KAC/C,MAAM,OAAO,EAAE,GAAG,QAAQ,iBAAiB;AAC3C,YAAO,KAAK;AACZ,WAAM,MAAM;MAAE,qBAAqB;MAAW,iBAAiB;MAAM,CAAC;UAEtE,OAAM,MAAM,EAAE,qBAAqB,QAAW,CAAC;;;AAKrD,mBAAiB,OAAO,aAAa,YAAY,UAAU,KAAK;GAChE;AAEF,cAAa;AACX,oBAAkB;AAClB,eAAa;;;;;;;;;;;;AC5EjB,SAAgB,sBAAsB,QAAsC;AAC1E,QAAO,CAAC,CAAC,OAAO,cAAc,OAAO,WAAW,OAAO;;;;;;;;;;;;;;;;AAiBzD,SAAgB,mBAAmB,EACjC,OACA,UAIa;AACb,QAAO,cAAc,CAAC,OAAO,OAAO,CAAC,CAAC,WAAW,CAAC,GAAG,OAAkD;AAErG,MAAI,CAAC,sBAAsB,EAAE,CAAE;EAE/B,MAAM,aAAa,EAAE;AAGrB,OAAK,MAAM,CAAC,SAAS,iBAAiB,EAAE,WACtC,KAAI,YAAY,WAEd,cAAa,MAAM,OAAO;MAG1B,cAAa,MAAM,OAAO;GAG9B;;;;;;;;AC9CJ,SAAgB,kBAAkB,OAA4B,QAAuC;AACnG,QAAO,CAAC,EAAE,OAAO,eAAe,MAAM,gBAAgB,wBAAwB,MAAM,aAAa;;;;;AAMnG,SAAgB,kBAAkB,QAAsC;CACtE,IAAI,SAAS;CAEb,MAAM,UAAU,CAAC,OAAO,mBAAmB,OAAO,kBAAkB,CAAC,QAClE,QAA6B,QAAQ,OACvC;AAED,MAAK,MAAM,UAAU,SAAS;EAC5B,MAAM,EAAE,aAAa;AACrB,MAAI,SAAS,SAAS,GAAG;GACvB,MAAM,MAAM,SAAS,IAAI,SAAS,SAAS,EAAE;AAC7C,OAAI,MAAM,OACR,UAAS;;;AAKf,QAAO;;;;;AAMT,SAAgB,qBAAqB,OAA4B,QAAuC;AACtG,KAAI,CAAC,kBAAkB,OAAO,OAAO,CAAE,QAAO;CAE9C,MAAM,EAAE,gBAAgB;CACxB,MAAM,EAAE,iBAAiB;AAGzB,KAAI,YAAa,eAAe,OAAQ,QAAO;CAE/C,MAAM,WAAW,aAAc;AAG/B,KAAI,CAAC,OAAO,SAAS,SAAS,IAAI,OAAO,MAAM,SAAS,IAAI,YAAY,EAAG,QAAO;AAOlF,QAAO,OAAO,MAAM,YAAa,SAAS;;;;;;;;AAS5C,SAAS,0BAA0B,QAA6C;CAC9E,MAAM,WAAW,CAAC,OAAO,mBAAmB,OAAO,kBAAkB,CAAC,QACnE,QAA6B,KAAK,aAAa,KACjD;AAED,KAAI,SAAS,WAAW,EAAG,QAAO,QAAQ,SAAS;AAEnD,QAAO,QAAQ,IACb,SAAS,KACN,QAAQ,IAAI,SAAe,YAAY,IAAI,iBAAiB,mBAAmB,SAAS,EAAE,EAAE,MAAM,MAAM,CAAC,CAAC,CAC5G,CACF,CAAC,WAAW,OAAU;;;;;AAMzB,SAAgB,eAAe,EAC7B,OACA,UAIa;CACb,IAAI,YAAY;CAEhB,MAAM,cAAc,cAAc,CAAC,OAAO,OAAO,CAAC,CAAC,UACjD,OAAO,CAAC,cAAc,mBAAgE;AACpF,MAAI,CAAC,qBAAqB,cAAc,cAAc,CAAE;EAExD,MAAM,EAAE,gBAAgB;AAGxB,QAAM,0BAA0B,cAAc;AAI9C,MAAI,aAAa,YAAa,eAAe,OAAQ;EAErD,IAAI,WAAW,aAAa,aAAc;EAG1C,MAAM,iBAAiB,kBAAkB,cAAc;AAIvD,MAAI,iBAAiB,SACnB,YAAW;AAIb,cAAa,WAAW;GAE3B;AAED,cAAa;AACX,cAAY;AACZ,eAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACyDjB,SAAgB,qBAAqB,SAA+B,EAAE,EAAkB;CAEtF,MAAM,QAAQ,YAAiC,EAC7C,gBAAgB;EACd,cAAc;EACd,iBAAiB;EACjB,cAAc;EACd,iBAAiB;EACjB,cAAc;EACf,EACF,CAAC;CACF,MAAM,SAAS,YAAkC,EAAE,CAAC;CAGpD,MAAM,SAAS,mBAAyC;CAMxD,MAAM,WAAW;EAKf,qBAAqB,OAAO,OAAO;EAKnC,uBAAuB;GAAE;GAAO;GAAQ;GAAQ,CAAC;EAIjD,oBAAoB;GAAE;GAAO;GAAQ,CAAC;EAGtC,iBAEE;GAAE;GAAO;GAAQ;GAAQ,EACzB;GACE,MAAM;GACN,GAAI,OAAO,qBAAqB,UAAa,EAAE,kBAAkB,OAAO,kBAAkB;GAC3F,CACF;EACD,iBAEE;GAAE;GAAO;GAAQ;GAAQ,EACzB;GACE,MAAM;GACN,GAAI,OAAO,2BAA2B,UAAa,EAAE,wBAAwB,OAAO,wBAAwB;GAC7G,CACF;EACD,gBAEE;GAAE;GAAO;GAAQ;GAAQ,EACzB;GACE,MAAM;GACN,GAAI,OAAO,8BAA8B,UAAa,EACpD,2BAA2B,OAAO,2BACnC;GACD,GAAI,OAAO,wBAAwB,UAAa,EAAE,qBAAqB,OAAO,qBAAqB;GACnG,GAAI,OAAO,uBAAuB,UAAa,EAAE,oBAAoB,OAAO,oBAAoB;GACjG,CACF;EAID,aAAa;GAAE;GAAO;GAAQ,EAAE,EAAE,MAAM,SAAkB,CAAC;EAE3D,aAAa;GAAE;GAAO;GAAQ,EAAE,EAAE,MAAM,SAAkB,CAAC;EAE3D,aAAa;GAAE;GAAO;GAAQ,EAAE,EAAE,MAAM,QAAiB,CAAC;EAG1D,8BAA8B,EAAE,OAAO,CAAC;EAGxC,iBAAiB;GAAE;GAAO;GAAQ,CAAC;EAGnC,eAAe;GAAE;GAAO;GAAQ,CAAC;EAGjC,kBAAkB;GAAE;GAAO;GAAQ,EAAE,EAAE,MAAM,SAAS,CAAC;EACvD,kBAAkB;GAAE;GAAO;GAAQ,EAAE,EAAE,MAAM,SAAS,CAAC;EAqBvD,iBAAiB;GAAE;GAAO;GAAQ,CAAC;EAGnC,cAAc,EAAE,OAAO,CAAC;EAGxB,aAAa;GAAE;GAAO;GAAQ,EAAE,EAAE,MAAM,SAAS,CAAC;EAClD,aAAa;GAAE;GAAO;GAAQ,EAAE,EAAE,MAAM,SAAS,CAAC;EAGlD,YAAY;GAAE;GAAO;GAAQ,CAAC;EAG9B,gBAAgB;GAAE;GAAO;GAAQ,CAAC;EAGlC,mBAAmB;GAAE;GAAO;GAAQ,CAAC;EAMrC,6BAA6B;GAAE;GAAO;GAAQ,CAAC;EAG/C,kBAAkB;GAAE;GAAO;GAAQ,CAAC;EACrC;AAID,QAAO,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAG3C,QAAO;EACL;EACA;EACA;EACA,eAAe;AACb,YAAS,SAAS,YAAY,SAAS,CAAC;AACxC,qBAAkB;;EAErB;;;;;;;;;;;;;;;;;;;;;;;;;;;ACxTH,IAAa,WAAb,MAAsB;CACpB;CACA;;CAGA,qBAA0C;CAE1C,YAAY,SAA+B,EAAE,EAAE;AAC7C,QAAKC,SAAU;AACf,QAAKC,SAAU,qBAAqB,OAAO;;CAG7C,IAAI,SAAyB;AAC3B,SAAO,MAAKA;;CAOd,OAAO,cAAsC;AAC3C,QAAKA,OAAQ,OAAO,MAAM,EAAE,cAAc,CAAC;;CAG7C,SAAe;AACb,QAAKC,mBAAoB;AACzB,QAAKD,OAAQ,OAAO,MAAM,EAAE,cAAc,QAAW,CAAC;;CAGxD,UAAgB;AACd,QAAKC,mBAAoB;AACzB,QAAKD,OAAQ,SAAS;;CASxB,IAAI,MAAc;AAChB,SAAO,MAAKA,OAAQ,MAAM,QAAQ,cAAc,OAAO;;CAGzD,IAAI,IAAI,OAAe;EACrB,MAAM,mBAAmB,MAAKA,OAAQ,OAAO,QAAQ;AAErD,QAAKC,mBAAoB;AACzB,QAAKD,OAAQ,SAAS;AACtB,QAAKA,SAAU,qBAAqB,MAAKD,OAAQ;AAEjD,MAAI,iBACF,OAAKC,OAAQ,OAAO,MAAM,EAAE,cAAc,kBAAkB,CAAC;AAG/D,MAAI,MACF,OAAKA,OAAQ,MAAM,MAAM,EAAE,cAAc,EAAE,KAAK,OAAO,EAAE,CAAC;;CAS9D,OAAsB;EACpB,MAAM,EAAE,iBAAiB,MAAKA,OAAQ,OAAO;AAC7C,MAAI,CAAC,aACH,QAAO,QAAQ,uBAAO,IAAI,MAAM,sCAAsC,CAAC;AAIzE,QAAKA,OAAQ,MAAM,MAAM,EAAE,mBAAmB,MAAM,CAAC;AAErD,SAAO,aAAa,MAAM,CAAC,OAAO,QAAiB;AAIjD,OAAI,KAAK,IACP,QAAO,IAAI,SAAe,SAAS,WAAW;IAC5C,MAAM,iBAAiB;AACrB,WAAKE,oBAAqB;AAC1B,kBAAa,MAAM,CAAC,KAAK,SAAS,OAAO;;AAE3C,UAAKA,oBAAqB;AAC1B,iBAAa,iBAAiB,aAAa,UAAU,EAAE,MAAM,MAAM,CAAC;KACpE;AAEJ,SAAM;IACN;;CAOJ,qBAA2B;AACzB,MAAI,CAAC,MAAKA,kBAAoB;EAC9B,MAAM,EAAE,iBAAiB,MAAKF,OAAQ,OAAO;AAC7C,gBAAc,oBAAoB,aAAa,MAAKE,kBAAmB;AACvE,QAAKA,oBAAqB"}