@videojs/spf 10.0.0-beta.24 → 10.0.0-beta.26

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 (375) hide show
  1. package/README.md +5 -0
  2. package/dist/default/background-video.js +3 -0
  3. package/dist/default/core/actors/create-machine-actor.js.map +1 -1
  4. package/dist/default/core/actors/create-transition-actor.js.map +1 -1
  5. package/dist/default/core/composition/create-composition.js +2 -2
  6. package/dist/default/core/composition/create-composition.js.map +1 -1
  7. package/dist/default/core/composition/share-signals.js +13 -11
  8. package/dist/default/core/composition/share-signals.js.map +1 -1
  9. package/dist/default/core/machine.js.map +1 -1
  10. package/dist/default/core/reactors/create-machine-reactor.js.map +1 -1
  11. package/dist/default/core/signals/effect.js.map +1 -1
  12. package/dist/default/core/signals/primitives.js.map +1 -1
  13. package/dist/default/core/tasks/task.js.map +1 -1
  14. package/dist/default/hls.js +4 -1
  15. package/dist/default/media/abr/quality-selection.js +6 -68
  16. package/dist/default/media/abr/quality-selection.js.map +1 -1
  17. package/dist/default/media/buffer/back-buffer.js.map +1 -1
  18. package/dist/default/media/buffer/forward-buffer.js +37 -3
  19. package/dist/default/media/buffer/forward-buffer.js.map +1 -1
  20. package/dist/default/media/dom/capabilities.js +70 -0
  21. package/dist/default/media/dom/capabilities.js.map +1 -0
  22. package/dist/default/media/dom/mse/append-segment.js.map +1 -1
  23. package/dist/default/media/dom/mse/buffer-flusher.js.map +1 -1
  24. package/dist/default/media/dom/mse/duration.js +27 -10
  25. package/dist/default/media/dom/mse/duration.js.map +1 -1
  26. package/dist/default/media/dom/mse/end-of-stream.js.map +1 -1
  27. package/dist/default/media/dom/mse/mediasource-setup.js +68 -17
  28. package/dist/default/media/dom/mse/mediasource-setup.js.map +1 -1
  29. package/dist/default/media/dom/text/resolve-vtt-segment.js +1 -7
  30. package/dist/default/media/dom/text/resolve-vtt-segment.js.map +1 -1
  31. package/dist/default/media/dom/text/text-track-slots.js +3 -3
  32. package/dist/default/media/dom/text/text-track-slots.js.map +1 -1
  33. package/dist/default/media/hls/parse-attributes.js +17 -3
  34. package/dist/default/media/hls/parse-attributes.js.map +1 -1
  35. package/dist/default/media/hls/parse-media-playlist.js +29 -1
  36. package/dist/default/media/hls/parse-media-playlist.js.map +1 -1
  37. package/dist/default/media/hls/parse-multivariant.js +15 -6
  38. package/dist/default/media/hls/parse-multivariant.js.map +1 -1
  39. package/dist/default/media/hls/resolve-url.js.map +1 -1
  40. package/dist/default/media/media-tracks/media-tracks.js +101 -0
  41. package/dist/default/media/media-tracks/media-tracks.js.map +1 -0
  42. package/dist/default/media/mp4/box.js +58 -0
  43. package/dist/default/media/mp4/box.js.map +1 -0
  44. package/dist/default/media/mp4/timestamp-origin.js +107 -0
  45. package/dist/default/media/mp4/timestamp-origin.js.map +1 -0
  46. package/dist/default/media/primitives/select-tracks.js +98 -13
  47. package/dist/default/media/primitives/select-tracks.js.map +1 -1
  48. package/dist/default/media/text/parse-vtt-timestamp-map.js +42 -0
  49. package/dist/default/media/text/parse-vtt-timestamp-map.js.map +1 -0
  50. package/dist/default/media/text/resolve-vtt-metadata.js +23 -0
  51. package/dist/default/media/text/resolve-vtt-metadata.js.map +1 -0
  52. package/dist/default/media/types/index.js.map +1 -1
  53. package/dist/default/media/utils/cdn.js +56 -0
  54. package/dist/default/media/utils/cdn.js.map +1 -0
  55. package/dist/default/media/utils/preload.js.map +1 -1
  56. package/dist/default/media/utils/track-selection.js +1 -1
  57. package/dist/default/media/utils/track-selection.js.map +1 -1
  58. package/dist/default/media/utils/tracks.js +31 -4
  59. package/dist/default/media/utils/tracks.js.map +1 -1
  60. package/dist/default/media-tracks.js +2 -0
  61. package/dist/default/network/bandwidth-estimator.js.map +1 -1
  62. package/dist/default/network/chunked-stream-iterable.js.map +1 -1
  63. package/dist/default/network/ewma.js +1 -1
  64. package/dist/default/network/ewma.js.map +1 -1
  65. package/dist/default/network/fetch.js +7 -1
  66. package/dist/default/network/fetch.js.map +1 -1
  67. package/dist/default/playback/actors/dom/segment-loader.js +103 -106
  68. package/dist/default/playback/actors/dom/segment-loader.js.map +1 -1
  69. package/dist/default/playback/actors/dom/source-buffer.js +3 -1
  70. package/dist/default/playback/actors/dom/source-buffer.js.map +1 -1
  71. package/dist/default/playback/actors/dom/text-tracks.js.map +1 -1
  72. package/dist/default/playback/actors/text-track-segment-loader.js +30 -16
  73. package/dist/default/playback/actors/text-track-segment-loader.js.map +1 -1
  74. package/dist/default/playback/behaviors/calculate-presentation-duration.js +21 -43
  75. package/dist/default/playback/behaviors/calculate-presentation-duration.js.map +1 -1
  76. package/dist/default/playback/behaviors/derive-cdn-priority.js +68 -0
  77. package/dist/default/playback/behaviors/derive-cdn-priority.js.map +1 -0
  78. package/dist/default/playback/behaviors/dom/airplay.js +211 -0
  79. package/dist/default/playback/behaviors/dom/airplay.js.map +1 -0
  80. package/dist/default/playback/behaviors/dom/apply-start-position.js +93 -0
  81. package/dist/default/playback/behaviors/dom/apply-start-position.js.map +1 -0
  82. package/dist/default/playback/behaviors/dom/end-of-stream.js +25 -102
  83. package/dist/default/playback/behaviors/dom/end-of-stream.js.map +1 -1
  84. package/dist/default/playback/behaviors/dom/load-segments.js +6 -2
  85. package/dist/default/playback/behaviors/dom/load-segments.js.map +1 -1
  86. package/dist/default/playback/behaviors/dom/recover-end-stall.js +77 -0
  87. package/dist/default/playback/behaviors/dom/recover-end-stall.js.map +1 -0
  88. package/dist/default/playback/behaviors/dom/setup-buffer-actors.js +50 -18
  89. package/dist/default/playback/behaviors/dom/setup-buffer-actors.js.map +1 -1
  90. package/dist/default/playback/behaviors/dom/setup-mediasource.js +36 -11
  91. package/dist/default/playback/behaviors/dom/setup-mediasource.js.map +1 -1
  92. package/dist/default/playback/behaviors/dom/setup-text-track-actors.js +11 -25
  93. package/dist/default/playback/behaviors/dom/setup-text-track-actors.js.map +1 -1
  94. package/dist/default/playback/behaviors/dom/sync-text-tracks.js +43 -18
  95. package/dist/default/playback/behaviors/dom/sync-text-tracks.js.map +1 -1
  96. package/dist/default/playback/behaviors/dom/track-current-time.js +6 -0
  97. package/dist/default/playback/behaviors/dom/track-current-time.js.map +1 -1
  98. package/dist/default/playback/behaviors/dom/track-load-triggers.js +4 -4
  99. package/dist/default/playback/behaviors/dom/track-load-triggers.js.map +1 -1
  100. package/dist/default/playback/behaviors/dom/track-playback-rate.js.map +1 -1
  101. package/dist/default/playback/behaviors/dom/update-mediasource-duration.js +3 -2
  102. package/dist/default/playback/behaviors/dom/update-mediasource-duration.js.map +1 -1
  103. package/dist/default/playback/behaviors/establish-start-media-time.js +123 -0
  104. package/dist/default/playback/behaviors/establish-start-media-time.js.map +1 -0
  105. package/dist/default/playback/behaviors/resolve-presentation.js.map +1 -1
  106. package/dist/default/playback/behaviors/resolve-track.js +45 -22
  107. package/dist/default/playback/behaviors/resolve-track.js.map +1 -1
  108. package/dist/default/playback/behaviors/select-tracks.js +46 -77
  109. package/dist/default/playback/behaviors/select-tracks.js.map +1 -1
  110. package/dist/default/playback/behaviors/setup-failover-monitor.js +66 -0
  111. package/dist/default/playback/behaviors/setup-failover-monitor.js.map +1 -0
  112. package/dist/default/playback/behaviors/sync-preload.js.map +1 -1
  113. package/dist/default/playback/behaviors/track-switching.js +403 -0
  114. package/dist/default/playback/behaviors/track-switching.js.map +1 -0
  115. package/dist/default/playback/engines/background-video/adapter.js +158 -0
  116. package/dist/default/playback/engines/background-video/adapter.js.map +1 -0
  117. package/dist/default/playback/engines/background-video/engine.js +75 -0
  118. package/dist/default/playback/engines/background-video/engine.js.map +1 -0
  119. package/dist/default/playback/engines/hls/adapter-audio-only.js +124 -0
  120. package/dist/default/playback/engines/hls/adapter-audio-only.js.map +1 -0
  121. package/dist/default/playback/engines/hls/adapter.js +20 -11
  122. package/dist/default/playback/engines/hls/adapter.js.map +1 -1
  123. package/dist/default/playback/engines/hls/engine-audio-only.js +94 -0
  124. package/dist/default/playback/engines/hls/engine-audio-only.js.map +1 -0
  125. package/dist/default/playback/engines/hls/engine.js +38 -8
  126. package/dist/default/playback/engines/hls/engine.js.map +1 -1
  127. package/dist/default/playback/primitives/failover-fetch.js +42 -0
  128. package/dist/default/playback/primitives/failover-fetch.js.map +1 -0
  129. package/dist/default/playback/primitives/head-peek.js +44 -0
  130. package/dist/default/playback/primitives/head-peek.js.map +1 -0
  131. package/dist/default/playback/primitives/relocation-pipelines.js +203 -0
  132. package/dist/default/playback/primitives/relocation-pipelines.js.map +1 -0
  133. package/dist/default/playback/primitives/segment-load-pipeline.js +96 -0
  134. package/dist/default/playback/primitives/segment-load-pipeline.js.map +1 -0
  135. package/dist/default/playback/primitives/text-segment-load-pipeline.js +37 -0
  136. package/dist/default/playback/primitives/text-segment-load-pipeline.js.map +1 -0
  137. package/dist/{dev/playback/behaviors → default/playback/primitives}/track-types.js +1 -1
  138. package/dist/default/playback/primitives/track-types.js.map +1 -0
  139. package/dist/dev/background-video.d.ts +3 -0
  140. package/dist/dev/background-video.js +3 -0
  141. package/dist/dev/core/actors/actor.d.ts +0 -1
  142. package/dist/dev/core/actors/actor.d.ts.map +1 -1
  143. package/dist/dev/core/actors/create-machine-actor.d.ts +9 -6
  144. package/dist/dev/core/actors/create-machine-actor.d.ts.map +1 -1
  145. package/dist/dev/core/actors/create-machine-actor.js.map +1 -1
  146. package/dist/dev/core/actors/create-transition-actor.d.ts +0 -1
  147. package/dist/dev/core/actors/create-transition-actor.d.ts.map +1 -1
  148. package/dist/dev/core/actors/create-transition-actor.js.map +1 -1
  149. package/dist/dev/core/composition/create-composition.d.ts +10 -11
  150. package/dist/dev/core/composition/create-composition.d.ts.map +1 -1
  151. package/dist/dev/core/composition/create-composition.js +2 -2
  152. package/dist/dev/core/composition/create-composition.js.map +1 -1
  153. package/dist/dev/core/composition/share-signals.d.ts +11 -10
  154. package/dist/dev/core/composition/share-signals.d.ts.map +1 -1
  155. package/dist/dev/core/composition/share-signals.js +13 -11
  156. package/dist/dev/core/composition/share-signals.js.map +1 -1
  157. package/dist/dev/core/machine.d.ts +1 -0
  158. package/dist/dev/core/machine.d.ts.map +1 -1
  159. package/dist/dev/core/machine.js.map +1 -1
  160. package/dist/dev/core/reactors/create-machine-reactor.d.ts +2 -2
  161. package/dist/dev/core/reactors/create-machine-reactor.d.ts.map +1 -1
  162. package/dist/dev/core/reactors/create-machine-reactor.js.map +1 -1
  163. package/dist/dev/core/signals/effect.d.ts.map +1 -1
  164. package/dist/dev/core/signals/effect.js.map +1 -1
  165. package/dist/dev/core/signals/primitives.d.ts +1 -2
  166. package/dist/dev/core/signals/primitives.d.ts.map +1 -1
  167. package/dist/dev/core/signals/primitives.js.map +1 -1
  168. package/dist/dev/core/tasks/task.d.ts +2 -2
  169. package/dist/dev/core/tasks/task.d.ts.map +1 -1
  170. package/dist/dev/core/tasks/task.js.map +1 -1
  171. package/dist/dev/hls.d.ts +5 -1
  172. package/dist/dev/hls.js +4 -1
  173. package/dist/dev/index.d.ts +0 -1
  174. package/dist/dev/index.d.ts.map +1 -1
  175. package/dist/dev/media/abr/quality-selection.d.ts.map +1 -1
  176. package/dist/dev/media/abr/quality-selection.js +6 -68
  177. package/dist/dev/media/abr/quality-selection.js.map +1 -1
  178. package/dist/dev/media/buffer/back-buffer.d.ts.map +1 -1
  179. package/dist/dev/media/buffer/back-buffer.js.map +1 -1
  180. package/dist/dev/media/buffer/forward-buffer.d.ts.map +1 -1
  181. package/dist/dev/media/buffer/forward-buffer.js +37 -3
  182. package/dist/dev/media/buffer/forward-buffer.js.map +1 -1
  183. package/dist/dev/media/dom/capabilities.js +70 -0
  184. package/dist/dev/media/dom/capabilities.js.map +1 -0
  185. package/dist/dev/media/dom/mse/append-segment.d.ts +3 -9
  186. package/dist/dev/media/dom/mse/append-segment.d.ts.map +1 -1
  187. package/dist/dev/media/dom/mse/append-segment.js.map +1 -1
  188. package/dist/dev/media/dom/mse/buffer-flusher.d.ts.map +1 -1
  189. package/dist/dev/media/dom/mse/buffer-flusher.js.map +1 -1
  190. package/dist/dev/media/dom/mse/duration.js +27 -10
  191. package/dist/dev/media/dom/mse/duration.js.map +1 -1
  192. package/dist/dev/media/dom/mse/end-of-stream.js.map +1 -1
  193. package/dist/dev/media/dom/mse/mediasource-setup.js +68 -17
  194. package/dist/dev/media/dom/mse/mediasource-setup.js.map +1 -1
  195. package/dist/dev/media/dom/text/resolve-vtt-segment.d.ts +0 -6
  196. package/dist/dev/media/dom/text/resolve-vtt-segment.d.ts.map +1 -1
  197. package/dist/dev/media/dom/text/resolve-vtt-segment.js +1 -7
  198. package/dist/dev/media/dom/text/resolve-vtt-segment.js.map +1 -1
  199. package/dist/dev/media/dom/text/text-track-slots.d.ts +0 -1
  200. package/dist/dev/media/dom/text/text-track-slots.d.ts.map +1 -1
  201. package/dist/dev/media/dom/text/text-track-slots.js +3 -3
  202. package/dist/dev/media/dom/text/text-track-slots.js.map +1 -1
  203. package/dist/dev/media/hls/parse-attributes.js +17 -3
  204. package/dist/dev/media/hls/parse-attributes.js.map +1 -1
  205. package/dist/dev/media/hls/parse-media-playlist.js +29 -1
  206. package/dist/dev/media/hls/parse-media-playlist.js.map +1 -1
  207. package/dist/dev/media/hls/parse-multivariant.js +15 -6
  208. package/dist/dev/media/hls/parse-multivariant.js.map +1 -1
  209. package/dist/dev/media/hls/resolve-url.js.map +1 -1
  210. package/dist/dev/media/media-tracks/media-tracks.d.ts +51 -0
  211. package/dist/dev/media/media-tracks/media-tracks.d.ts.map +1 -0
  212. package/dist/dev/media/media-tracks/media-tracks.js +101 -0
  213. package/dist/dev/media/media-tracks/media-tracks.js.map +1 -0
  214. package/dist/dev/media/mp4/box.js +58 -0
  215. package/dist/dev/media/mp4/box.js.map +1 -0
  216. package/dist/dev/media/mp4/timestamp-origin.js +107 -0
  217. package/dist/dev/media/mp4/timestamp-origin.js.map +1 -0
  218. package/dist/dev/media/primitives/select-tracks.d.ts +35 -0
  219. package/dist/dev/media/primitives/select-tracks.d.ts.map +1 -0
  220. package/dist/dev/media/primitives/select-tracks.js +98 -13
  221. package/dist/dev/media/primitives/select-tracks.js.map +1 -1
  222. package/dist/dev/media/text/parse-vtt-timestamp-map.js +42 -0
  223. package/dist/dev/media/text/parse-vtt-timestamp-map.js.map +1 -0
  224. package/dist/dev/media/text/resolve-vtt-metadata.js +23 -0
  225. package/dist/dev/media/text/resolve-vtt-metadata.js.map +1 -0
  226. package/dist/dev/media/types/index.d.ts +69 -2
  227. package/dist/dev/media/types/index.d.ts.map +1 -1
  228. package/dist/dev/media/types/index.js.map +1 -1
  229. package/dist/dev/media/utils/cdn.d.ts +13 -0
  230. package/dist/dev/media/utils/cdn.d.ts.map +1 -0
  231. package/dist/dev/media/utils/cdn.js +56 -0
  232. package/dist/dev/media/utils/cdn.js.map +1 -0
  233. package/dist/dev/media/utils/preload.js.map +1 -1
  234. package/dist/dev/media/utils/track-selection.js +1 -1
  235. package/dist/dev/media/utils/track-selection.js.map +1 -1
  236. package/dist/dev/media/utils/tracks.js +31 -4
  237. package/dist/dev/media/utils/tracks.js.map +1 -1
  238. package/dist/dev/media-tracks.d.ts +3 -0
  239. package/dist/dev/media-tracks.js +2 -0
  240. package/dist/dev/network/bandwidth-estimator.d.ts.map +1 -1
  241. package/dist/dev/network/bandwidth-estimator.js.map +1 -1
  242. package/dist/dev/network/chunked-stream-iterable.js.map +1 -1
  243. package/dist/dev/network/ewma.js +1 -1
  244. package/dist/dev/network/ewma.js.map +1 -1
  245. package/dist/dev/network/fetch.js +7 -1
  246. package/dist/dev/network/fetch.js.map +1 -1
  247. package/dist/dev/playback/actors/dom/segment-loader.d.ts +16 -4
  248. package/dist/dev/playback/actors/dom/segment-loader.d.ts.map +1 -1
  249. package/dist/dev/playback/actors/dom/segment-loader.js +103 -106
  250. package/dist/dev/playback/actors/dom/segment-loader.js.map +1 -1
  251. package/dist/dev/playback/actors/dom/source-buffer.d.ts +10 -25
  252. package/dist/dev/playback/actors/dom/source-buffer.d.ts.map +1 -1
  253. package/dist/dev/playback/actors/dom/source-buffer.js +3 -1
  254. package/dist/dev/playback/actors/dom/source-buffer.js.map +1 -1
  255. package/dist/dev/playback/actors/dom/text-tracks.d.ts +3 -1
  256. package/dist/dev/playback/actors/dom/text-tracks.js.map +1 -1
  257. package/dist/dev/playback/actors/text-track-segment-loader.d.ts +7 -12
  258. package/dist/dev/playback/actors/text-track-segment-loader.d.ts.map +1 -1
  259. package/dist/dev/playback/actors/text-track-segment-loader.js +30 -16
  260. package/dist/dev/playback/actors/text-track-segment-loader.js.map +1 -1
  261. package/dist/dev/playback/actors/text-tracks.d.ts +3 -12
  262. package/dist/dev/playback/actors/text-tracks.d.ts.map +1 -1
  263. package/dist/dev/playback/behaviors/calculate-presentation-duration.d.ts +11 -2
  264. package/dist/dev/playback/behaviors/calculate-presentation-duration.d.ts.map +1 -1
  265. package/dist/dev/playback/behaviors/calculate-presentation-duration.js +21 -43
  266. package/dist/dev/playback/behaviors/calculate-presentation-duration.js.map +1 -1
  267. package/dist/dev/playback/behaviors/derive-cdn-priority.js +68 -0
  268. package/dist/dev/playback/behaviors/derive-cdn-priority.js.map +1 -0
  269. package/dist/dev/playback/behaviors/dom/airplay.js +211 -0
  270. package/dist/dev/playback/behaviors/dom/airplay.js.map +1 -0
  271. package/dist/dev/playback/behaviors/dom/apply-start-position.js +93 -0
  272. package/dist/dev/playback/behaviors/dom/apply-start-position.js.map +1 -0
  273. package/dist/dev/playback/behaviors/dom/end-of-stream.js +25 -102
  274. package/dist/dev/playback/behaviors/dom/end-of-stream.js.map +1 -1
  275. package/dist/dev/playback/behaviors/dom/load-segments.d.ts +14 -4
  276. package/dist/dev/playback/behaviors/dom/load-segments.d.ts.map +1 -1
  277. package/dist/dev/playback/behaviors/dom/load-segments.js +6 -2
  278. package/dist/dev/playback/behaviors/dom/load-segments.js.map +1 -1
  279. package/dist/dev/playback/behaviors/dom/recover-end-stall.js +77 -0
  280. package/dist/dev/playback/behaviors/dom/recover-end-stall.js.map +1 -0
  281. package/dist/dev/playback/behaviors/dom/setup-buffer-actors.js +50 -18
  282. package/dist/dev/playback/behaviors/dom/setup-buffer-actors.js.map +1 -1
  283. package/dist/dev/playback/behaviors/dom/setup-mediasource.js +36 -11
  284. package/dist/dev/playback/behaviors/dom/setup-mediasource.js.map +1 -1
  285. package/dist/dev/playback/behaviors/dom/setup-text-track-actors.d.ts +16 -19
  286. package/dist/dev/playback/behaviors/dom/setup-text-track-actors.d.ts.map +1 -1
  287. package/dist/dev/playback/behaviors/dom/setup-text-track-actors.js +11 -25
  288. package/dist/dev/playback/behaviors/dom/setup-text-track-actors.js.map +1 -1
  289. package/dist/dev/playback/behaviors/dom/sync-text-tracks.js +43 -18
  290. package/dist/dev/playback/behaviors/dom/sync-text-tracks.js.map +1 -1
  291. package/dist/dev/playback/behaviors/dom/track-current-time.d.ts +1 -2
  292. package/dist/dev/playback/behaviors/dom/track-current-time.d.ts.map +1 -1
  293. package/dist/dev/playback/behaviors/dom/track-current-time.js +6 -0
  294. package/dist/dev/playback/behaviors/dom/track-current-time.js.map +1 -1
  295. package/dist/dev/playback/behaviors/dom/track-load-triggers.d.ts +1 -2
  296. package/dist/dev/playback/behaviors/dom/track-load-triggers.d.ts.map +1 -1
  297. package/dist/dev/playback/behaviors/dom/track-load-triggers.js +4 -4
  298. package/dist/dev/playback/behaviors/dom/track-load-triggers.js.map +1 -1
  299. package/dist/dev/playback/behaviors/dom/track-playback-rate.d.ts +1 -2
  300. package/dist/dev/playback/behaviors/dom/track-playback-rate.d.ts.map +1 -1
  301. package/dist/dev/playback/behaviors/dom/track-playback-rate.js.map +1 -1
  302. package/dist/dev/playback/behaviors/dom/update-mediasource-duration.js +3 -2
  303. package/dist/dev/playback/behaviors/dom/update-mediasource-duration.js.map +1 -1
  304. package/dist/dev/playback/behaviors/establish-start-media-time.d.ts +31 -0
  305. package/dist/dev/playback/behaviors/establish-start-media-time.d.ts.map +1 -0
  306. package/dist/dev/playback/behaviors/establish-start-media-time.js +123 -0
  307. package/dist/dev/playback/behaviors/establish-start-media-time.js.map +1 -0
  308. package/dist/dev/playback/behaviors/resolve-presentation.d.ts +2 -1
  309. package/dist/dev/playback/behaviors/resolve-presentation.d.ts.map +1 -1
  310. package/dist/dev/playback/behaviors/resolve-presentation.js.map +1 -1
  311. package/dist/dev/playback/behaviors/resolve-track.js +45 -22
  312. package/dist/dev/playback/behaviors/resolve-track.js.map +1 -1
  313. package/dist/dev/playback/behaviors/select-tracks.d.ts +14 -0
  314. package/dist/dev/playback/behaviors/select-tracks.d.ts.map +1 -0
  315. package/dist/dev/playback/behaviors/select-tracks.js +46 -77
  316. package/dist/dev/playback/behaviors/select-tracks.js.map +1 -1
  317. package/dist/dev/playback/behaviors/setup-failover-monitor.d.ts +14 -0
  318. package/dist/dev/playback/behaviors/setup-failover-monitor.d.ts.map +1 -0
  319. package/dist/dev/playback/behaviors/setup-failover-monitor.js +66 -0
  320. package/dist/dev/playback/behaviors/setup-failover-monitor.js.map +1 -0
  321. package/dist/dev/playback/behaviors/sync-preload.js.map +1 -1
  322. package/dist/dev/playback/behaviors/track-switching.js +403 -0
  323. package/dist/dev/playback/behaviors/track-switching.js.map +1 -0
  324. package/dist/dev/playback/engines/background-video/adapter.d.ts +59 -0
  325. package/dist/dev/playback/engines/background-video/adapter.d.ts.map +1 -0
  326. package/dist/dev/playback/engines/background-video/adapter.js +158 -0
  327. package/dist/dev/playback/engines/background-video/adapter.js.map +1 -0
  328. package/dist/dev/playback/engines/background-video/engine.d.ts +106 -0
  329. package/dist/dev/playback/engines/background-video/engine.d.ts.map +1 -0
  330. package/dist/dev/playback/engines/background-video/engine.js +75 -0
  331. package/dist/dev/playback/engines/background-video/engine.js.map +1 -0
  332. package/dist/dev/playback/engines/hls/adapter-audio-only.d.ts +46 -0
  333. package/dist/dev/playback/engines/hls/adapter-audio-only.d.ts.map +1 -0
  334. package/dist/dev/playback/engines/hls/adapter-audio-only.js +124 -0
  335. package/dist/dev/playback/engines/hls/adapter-audio-only.js.map +1 -0
  336. package/dist/dev/playback/engines/hls/adapter.d.ts +3 -5
  337. package/dist/dev/playback/engines/hls/adapter.d.ts.map +1 -1
  338. package/dist/dev/playback/engines/hls/adapter.js +20 -11
  339. package/dist/dev/playback/engines/hls/adapter.js.map +1 -1
  340. package/dist/dev/playback/engines/hls/engine-audio-only.d.ts +156 -0
  341. package/dist/dev/playback/engines/hls/engine-audio-only.d.ts.map +1 -0
  342. package/dist/dev/playback/engines/hls/engine-audio-only.js +94 -0
  343. package/dist/dev/playback/engines/hls/engine-audio-only.js.map +1 -0
  344. package/dist/dev/playback/engines/hls/engine.d.ts +109 -4
  345. package/dist/dev/playback/engines/hls/engine.d.ts.map +1 -1
  346. package/dist/dev/playback/engines/hls/engine.js +38 -8
  347. package/dist/dev/playback/engines/hls/engine.js.map +1 -1
  348. package/dist/dev/playback/primitives/derive-start-media-time.d.ts +16 -0
  349. package/dist/dev/playback/primitives/derive-start-media-time.d.ts.map +1 -0
  350. package/dist/dev/playback/primitives/failover-fetch.js +42 -0
  351. package/dist/dev/playback/primitives/failover-fetch.js.map +1 -0
  352. package/dist/dev/playback/primitives/head-peek.js +44 -0
  353. package/dist/dev/playback/primitives/head-peek.js.map +1 -0
  354. package/dist/dev/playback/primitives/relocation-pipelines.js +203 -0
  355. package/dist/dev/playback/primitives/relocation-pipelines.js.map +1 -0
  356. package/dist/dev/playback/primitives/segment-load-pipeline.d.ts +2 -0
  357. package/dist/dev/playback/primitives/segment-load-pipeline.js +96 -0
  358. package/dist/dev/playback/primitives/segment-load-pipeline.js.map +1 -0
  359. package/dist/dev/playback/primitives/source-buffer-messages.d.ts +43 -0
  360. package/dist/dev/playback/primitives/source-buffer-messages.d.ts.map +1 -0
  361. package/dist/dev/playback/primitives/text-segment-load-pipeline.d.ts +65 -0
  362. package/dist/dev/playback/primitives/text-segment-load-pipeline.d.ts.map +1 -0
  363. package/dist/dev/playback/primitives/text-segment-load-pipeline.js +37 -0
  364. package/dist/dev/playback/primitives/text-segment-load-pipeline.js.map +1 -0
  365. package/dist/dev/playback/primitives/text-track-messages.d.ts +14 -0
  366. package/dist/dev/playback/primitives/text-track-messages.d.ts.map +1 -0
  367. package/dist/{default/playback/behaviors → dev/playback/primitives}/track-types.js +1 -1
  368. package/dist/dev/playback/primitives/track-types.js.map +1 -0
  369. package/package.json +17 -7
  370. package/dist/default/playback/behaviors/quality-switching.js +0 -96
  371. package/dist/default/playback/behaviors/quality-switching.js.map +0 -1
  372. package/dist/default/playback/behaviors/track-types.js.map +0 -1
  373. package/dist/dev/playback/behaviors/quality-switching.js +0 -96
  374. package/dist/dev/playback/behaviors/quality-switching.js.map +0 -1
  375. package/dist/dev/playback/behaviors/track-types.js.map +0 -1
@@ -0,0 +1,107 @@
1
+ import { findBox, iterateBoxesOfType, readFourCC, readFullBoxVersion, toDataView } from "./box.js";
2
+ //#region src/media/mp4/timestamp-origin.ts
3
+ /**
4
+ * Decode-time origin extraction from fMP4/CMAF segments.
5
+ *
6
+ * Non-zero-PTS sources encode media at a non-zero start time (an instant clip
7
+ * starting at asset second 60, an Apple bipbop asset starting at 10s, …). To
8
+ * relocate such a source onto a 0-based presentation timeline via
9
+ * `SourceBuffer.timestampOffset`, the engine needs that start time — the
10
+ * **decode-time origin** — read straight from the container:
11
+ *
12
+ * - `tfdt.baseMediaDecodeTime` (from a media segment's `moof`) — the decode
13
+ * time of the segment's first sample, in the track's media timescale. This is
14
+ * a DTS: relocating by `−(baseMediaDecodeTime / timescale)` lands the earliest
15
+ * DTS at exactly 0, so a negative-DTS append failure on Chromium is impossible
16
+ * by construction.
17
+ * - `mdhd.timescale` (from the init segment's `moov`) — ticks per second for
18
+ * that track, needed to convert the raw tick count to seconds.
19
+ *
20
+ * ## Presumptive vs. track-selected reads
21
+ *
22
+ * The two variants share only the leaf field-readers (`mdhd` timescale, `tfdt`
23
+ * baseMediaDecodeTime) and the box walker. Everything else differs, and the
24
+ * split is deliberate so the presumptive pair is *proportionally* smaller under
25
+ * tree-shaking:
26
+ *
27
+ * - **Presumptive** — {@link readFirstMediaTimescale} / {@link readFirstBaseMediaDecodeTime}
28
+ * read the first `mdhd` timescale and first `tfdt` baseMediaDecodeTime. There's
29
+ * no `track_id` (nothing to match against) and no `trak`/`traf` iteration — a
30
+ * direct `findBox` to the first leaf. Correct when the init/segment holds a
31
+ * single media track (the common CMAF case). A caption-free platform imports
32
+ * only this pair and tree-shakes away all the matching machinery below.
33
+ * - **Track-selected** — {@link findMediaTrack} / {@link readBaseMediaDecodeTime}.
34
+ * `findMediaTrack` returns `{ trackId, timescale }` for the `trak` whose `hdlr`
35
+ * handler matches; `readBaseMediaDecodeTime` takes that `trackId` and reads the
36
+ * `traf` whose `tfhd.track_id` matches. The `track_id` is the join that ties one
37
+ * track's timescale to the *same* track's baseMediaDecodeTime — required when a
38
+ * source muxes CEA-608/708 captions (a `clcp` track shares the same `moov` and
39
+ * `moof`, each track with its own timescale + baseMediaDecodeTime, so a
40
+ * presumptive read there risks `300000 / 6000 = 50s` instead of
41
+ * `60000 / 6000 = 10s`). This pair adds `trak`/`traf` iteration plus the handler
42
+ * and `track_id` reads. We only ever ask for buffered media handlers
43
+ * (`vide` / `soun`); the caption track is never selected — we read the origin,
44
+ * not the captions (caption *rendering* is out of scope).
45
+ *
46
+ * Both raw values are returned un-divided to leave room for an edit-list (`elst`)
47
+ * presentation-time correction term if a source ever carries one — the validated
48
+ * streams do not.
49
+ */
50
+ /**
51
+ * The `track_ID` + timescale of the `trak` whose `mdhd` handler matches
52
+ * `handlerType`, skipping a muxed `clcp` caption track. `undefined` if no
53
+ * matching track exists.
54
+ */
55
+ function findMediaTrack(initSegment, handlerType) {
56
+ const view = toDataView(initSegment);
57
+ const moov = findBox(view, ["moov"]);
58
+ if (!moov) return void 0;
59
+ for (const trak of iterateBoxesOfType(view, "trak", moov.dataStart, moov.end)) {
60
+ if (readTrakHandler(view, trak) !== handlerType) continue;
61
+ const tkhd = findBox(view, ["tkhd"], trak.dataStart, trak.end);
62
+ const mdhd = findBox(view, ["mdia", "mdhd"], trak.dataStart, trak.end);
63
+ if (!tkhd || !mdhd) return void 0;
64
+ return {
65
+ trackId: view.getUint32(tkhd.dataStart + 4 + (readFullBoxVersion(view, tkhd.dataStart) === 1 ? 16 : 8)),
66
+ timescale: readMdhdTimescale(view, mdhd)
67
+ };
68
+ }
69
+ }
70
+ /**
71
+ * `baseMediaDecodeTime` (in the track's media timescale) from the `traf` matching
72
+ * `trackId` (via `tfhd.track_id`) — required for muxed multi-`traf` segments.
73
+ * `undefined` if no matching `tfdt` exists.
74
+ */
75
+ function readBaseMediaDecodeTime(mediaSegment, trackId) {
76
+ const view = toDataView(mediaSegment);
77
+ const moof = findBox(view, ["moof"]);
78
+ if (!moof) return void 0;
79
+ for (const traf of iterateBoxesOfType(view, "traf", moof.dataStart, moof.end)) {
80
+ if (readTrafTrackId(view, traf) !== trackId) continue;
81
+ const tfdt = findBox(view, ["tfdt"], traf.dataStart, traf.end);
82
+ return tfdt ? readTfdtBaseMediaDecodeTime(view, tfdt) : void 0;
83
+ }
84
+ }
85
+ /** `mdhd.timescale`: FullBox version(1)+flags(3) + dates (v0: 4+4, v1: 8+8) + timescale. */
86
+ function readMdhdTimescale(view, mdhd) {
87
+ return view.getUint32(mdhd.dataStart + 4 + (readFullBoxVersion(view, mdhd.dataStart) === 1 ? 16 : 8));
88
+ }
89
+ /** `tfdt.baseMediaDecodeTime`: FullBox, then the value — v0: (4), v1: (8). */
90
+ function readTfdtBaseMediaDecodeTime(view, tfdt) {
91
+ const at = tfdt.dataStart + 4;
92
+ return readFullBoxVersion(view, tfdt.dataStart) === 1 ? Number(view.getBigUint64(at)) : view.getUint32(at);
93
+ }
94
+ /** `hdlr.handler_type` for a `trak`: FullBox version(1)+flags(3) + pre_defined(4) + handler_type(4). */
95
+ function readTrakHandler(view, trak) {
96
+ const hdlr = findBox(view, ["mdia", "hdlr"], trak.dataStart, trak.end);
97
+ return hdlr ? readFourCC(view, hdlr.dataStart + 8) : void 0;
98
+ }
99
+ /** `tfhd.track_id` for a `traf`: FullBox version(1)+flags(3) + track_id(4). */
100
+ function readTrafTrackId(view, traf) {
101
+ const tfhd = findBox(view, ["tfhd"], traf.dataStart, traf.end);
102
+ return tfhd ? view.getUint32(tfhd.dataStart + 4) : void 0;
103
+ }
104
+ //#endregion
105
+ export { findMediaTrack, readBaseMediaDecodeTime };
106
+
107
+ //# sourceMappingURL=timestamp-origin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"timestamp-origin.js","names":[],"sources":["../../../../src/media/mp4/timestamp-origin.ts"],"sourcesContent":["/**\n * Decode-time origin extraction from fMP4/CMAF segments.\n *\n * Non-zero-PTS sources encode media at a non-zero start time (an instant clip\n * starting at asset second 60, an Apple bipbop asset starting at 10s, …). To\n * relocate such a source onto a 0-based presentation timeline via\n * `SourceBuffer.timestampOffset`, the engine needs that start time — the\n * **decode-time origin** — read straight from the container:\n *\n * - `tfdt.baseMediaDecodeTime` (from a media segment's `moof`) — the decode\n * time of the segment's first sample, in the track's media timescale. This is\n * a DTS: relocating by `−(baseMediaDecodeTime / timescale)` lands the earliest\n * DTS at exactly 0, so a negative-DTS append failure on Chromium is impossible\n * by construction.\n * - `mdhd.timescale` (from the init segment's `moov`) — ticks per second for\n * that track, needed to convert the raw tick count to seconds.\n *\n * ## Presumptive vs. track-selected reads\n *\n * The two variants share only the leaf field-readers (`mdhd` timescale, `tfdt`\n * baseMediaDecodeTime) and the box walker. Everything else differs, and the\n * split is deliberate so the presumptive pair is *proportionally* smaller under\n * tree-shaking:\n *\n * - **Presumptive** — {@link readFirstMediaTimescale} / {@link readFirstBaseMediaDecodeTime}\n * read the first `mdhd` timescale and first `tfdt` baseMediaDecodeTime. There's\n * no `track_id` (nothing to match against) and no `trak`/`traf` iteration — a\n * direct `findBox` to the first leaf. Correct when the init/segment holds a\n * single media track (the common CMAF case). A caption-free platform imports\n * only this pair and tree-shakes away all the matching machinery below.\n * - **Track-selected** — {@link findMediaTrack} / {@link readBaseMediaDecodeTime}.\n * `findMediaTrack` returns `{ trackId, timescale }` for the `trak` whose `hdlr`\n * handler matches; `readBaseMediaDecodeTime` takes that `trackId` and reads the\n * `traf` whose `tfhd.track_id` matches. The `track_id` is the join that ties one\n * track's timescale to the *same* track's baseMediaDecodeTime — required when a\n * source muxes CEA-608/708 captions (a `clcp` track shares the same `moov` and\n * `moof`, each track with its own timescale + baseMediaDecodeTime, so a\n * presumptive read there risks `300000 / 6000 = 50s` instead of\n * `60000 / 6000 = 10s`). This pair adds `trak`/`traf` iteration plus the handler\n * and `track_id` reads. We only ever ask for buffered media handlers\n * (`vide` / `soun`); the caption track is never selected — we read the origin,\n * not the captions (caption *rendering* is out of scope).\n *\n * Both raw values are returned un-divided to leave room for an edit-list (`elst`)\n * presentation-time correction term if a source ever carries one — the validated\n * streams do not.\n */\nimport { type Box, findBox, iterateBoxesOfType, readFourCC, readFullBoxVersion, toDataView } from './box';\n\n/** MSE-buffered media handler types (`mdhd`/`hdlr`). Captions/subtitles excluded. */\nexport type MediaHandlerType = 'vide' | 'soun';\n\nexport interface MediaTrackInfo {\n /** `tkhd.track_id` — used to match the corresponding `traf` in media segments. */\n trackId: number;\n /** `mdhd.timescale` — ticks per second for this track. */\n timescale: number;\n}\n\n// --- presumptive: first leaf, no track_id, no iteration -----------------------\n\n/**\n * Presumptive: the `timescale` of the **first** `mdhd` in an init segment.\n * Correct only for single-media-track inits — for muxed captions use\n * {@link findMediaTrack}. `undefined` if no `mdhd` exists.\n */\nexport function readFirstMediaTimescale(initSegment: ArrayBuffer | Uint8Array): number | undefined {\n const view = toDataView(initSegment);\n const mdhd = findBox(view, ['moov', 'trak', 'mdia', 'mdhd']);\n return mdhd ? readMdhdTimescale(view, mdhd) : undefined;\n}\n\n/**\n * Presumptive: `baseMediaDecodeTime` from the **first** `tfdt` of a media\n * segment. Correct only for single-`traf` segments — for muxed captions use\n * {@link readBaseMediaDecodeTime}. `undefined` if no `tfdt` exists.\n */\nexport function readFirstBaseMediaDecodeTime(mediaSegment: ArrayBuffer | Uint8Array): number | undefined {\n const view = toDataView(mediaSegment);\n const tfdt = findBox(view, ['moof', 'traf', 'tfdt']);\n return tfdt ? readTfdtBaseMediaDecodeTime(view, tfdt) : undefined;\n}\n\n// --- track-selected: iterate + match by handler / track_id --------------------\n\n/**\n * The `track_ID` + timescale of the `trak` whose `mdhd` handler matches\n * `handlerType`, skipping a muxed `clcp` caption track. `undefined` if no\n * matching track exists.\n */\nexport function findMediaTrack(\n initSegment: ArrayBuffer | Uint8Array,\n handlerType: MediaHandlerType\n): MediaTrackInfo | undefined {\n const view = toDataView(initSegment);\n const moov = findBox(view, ['moov']);\n if (!moov) return undefined;\n\n for (const trak of iterateBoxesOfType(view, 'trak', moov.dataStart, moov.end)) {\n if (readTrakHandler(view, trak) !== handlerType) continue;\n const tkhd = findBox(view, ['tkhd'], trak.dataStart, trak.end);\n const mdhd = findBox(view, ['mdia', 'mdhd'], trak.dataStart, trak.end);\n if (!tkhd || !mdhd) return undefined;\n // tkhd FullBox: version(1)+flags(3), creation/modification dates (v0: 4+4, v1:\n // 8+8), then track_id.\n const trackId = view.getUint32(tkhd.dataStart + 4 + (readFullBoxVersion(view, tkhd.dataStart) === 1 ? 16 : 8));\n return { trackId, timescale: readMdhdTimescale(view, mdhd) };\n }\n return undefined;\n}\n\n/**\n * `baseMediaDecodeTime` (in the track's media timescale) from the `traf` matching\n * `trackId` (via `tfhd.track_id`) — required for muxed multi-`traf` segments.\n * `undefined` if no matching `tfdt` exists.\n */\nexport function readBaseMediaDecodeTime(mediaSegment: ArrayBuffer | Uint8Array, trackId: number): number | undefined {\n const view = toDataView(mediaSegment);\n const moof = findBox(view, ['moof']);\n if (!moof) return undefined;\n\n for (const traf of iterateBoxesOfType(view, 'traf', moof.dataStart, moof.end)) {\n if (readTrafTrackId(view, traf) !== trackId) continue;\n const tfdt = findBox(view, ['tfdt'], traf.dataStart, traf.end);\n return tfdt ? readTfdtBaseMediaDecodeTime(view, tfdt) : undefined;\n }\n return undefined;\n}\n\n// --- shared leaf field-readers ------------------------------------------------\n\n/** `mdhd.timescale`: FullBox version(1)+flags(3) + dates (v0: 4+4, v1: 8+8) + timescale. */\nfunction readMdhdTimescale(view: DataView, mdhd: Box): number {\n return view.getUint32(mdhd.dataStart + 4 + (readFullBoxVersion(view, mdhd.dataStart) === 1 ? 16 : 8));\n}\n\n/** `tfdt.baseMediaDecodeTime`: FullBox, then the value — v0: (4), v1: (8). */\nfunction readTfdtBaseMediaDecodeTime(view: DataView, tfdt: Box): number {\n const at = tfdt.dataStart + 4;\n return readFullBoxVersion(view, tfdt.dataStart) === 1 ? Number(view.getBigUint64(at)) : view.getUint32(at);\n}\n\n// --- track-selection readers (referenced only by the track-selected variants) -\n\n/** `hdlr.handler_type` for a `trak`: FullBox version(1)+flags(3) + pre_defined(4) + handler_type(4). */\nfunction readTrakHandler(view: DataView, trak: Box): string | undefined {\n const hdlr = findBox(view, ['mdia', 'hdlr'], trak.dataStart, trak.end);\n return hdlr ? readFourCC(view, hdlr.dataStart + 8) : undefined;\n}\n\n/** `tfhd.track_id` for a `traf`: FullBox version(1)+flags(3) + track_id(4). */\nfunction readTrafTrackId(view: DataView, traf: Box): number | undefined {\n const tfhd = findBox(view, ['tfhd'], traf.dataStart, traf.end);\n return tfhd ? view.getUint32(tfhd.dataStart + 4) : undefined;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0FA,SAAgB,eACd,aACA,aAC4B;CAC5B,MAAM,OAAO,WAAW,WAAW;CACnC,MAAM,OAAO,QAAQ,MAAM,CAAC,MAAM,CAAC;CACnC,IAAI,CAAC,MAAM,OAAO,KAAA;CAElB,KAAK,MAAM,QAAQ,mBAAmB,MAAM,QAAQ,KAAK,WAAW,KAAK,GAAG,GAAG;EAC7E,IAAI,gBAAgB,MAAM,IAAI,MAAM,aAAa;EACjD,MAAM,OAAO,QAAQ,MAAM,CAAC,MAAM,GAAG,KAAK,WAAW,KAAK,GAAG;EAC7D,MAAM,OAAO,QAAQ,MAAM,CAAC,QAAQ,MAAM,GAAG,KAAK,WAAW,KAAK,GAAG;EACrE,IAAI,CAAC,QAAQ,CAAC,MAAM,OAAO,KAAA;EAI3B,OAAO;GAAE,SADO,KAAK,UAAU,KAAK,YAAY,KAAK,mBAAmB,MAAM,KAAK,SAAS,MAAM,IAAI,KAAK,EAC5F;GAAG,WAAW,kBAAkB,MAAM,IAAI;EAAE;CAC7D;AAEF;;;;;;AAOA,SAAgB,wBAAwB,cAAwC,SAAqC;CACnH,MAAM,OAAO,WAAW,YAAY;CACpC,MAAM,OAAO,QAAQ,MAAM,CAAC,MAAM,CAAC;CACnC,IAAI,CAAC,MAAM,OAAO,KAAA;CAElB,KAAK,MAAM,QAAQ,mBAAmB,MAAM,QAAQ,KAAK,WAAW,KAAK,GAAG,GAAG;EAC7E,IAAI,gBAAgB,MAAM,IAAI,MAAM,SAAS;EAC7C,MAAM,OAAO,QAAQ,MAAM,CAAC,MAAM,GAAG,KAAK,WAAW,KAAK,GAAG;EAC7D,OAAO,OAAO,4BAA4B,MAAM,IAAI,IAAI,KAAA;CAC1D;AAEF;;AAKA,SAAS,kBAAkB,MAAgB,MAAmB;CAC5D,OAAO,KAAK,UAAU,KAAK,YAAY,KAAK,mBAAmB,MAAM,KAAK,SAAS,MAAM,IAAI,KAAK,EAAE;AACtG;;AAGA,SAAS,4BAA4B,MAAgB,MAAmB;CACtE,MAAM,KAAK,KAAK,YAAY;CAC5B,OAAO,mBAAmB,MAAM,KAAK,SAAS,MAAM,IAAI,OAAO,KAAK,aAAa,EAAE,CAAC,IAAI,KAAK,UAAU,EAAE;AAC3G;;AAKA,SAAS,gBAAgB,MAAgB,MAA+B;CACtE,MAAM,OAAO,QAAQ,MAAM,CAAC,QAAQ,MAAM,GAAG,KAAK,WAAW,KAAK,GAAG;CACrE,OAAO,OAAO,WAAW,MAAM,KAAK,YAAY,CAAC,IAAI,KAAA;AACvD;;AAGA,SAAS,gBAAgB,MAAgB,MAA+B;CACtE,MAAM,OAAO,QAAQ,MAAM,CAAC,MAAM,GAAG,KAAK,WAAW,KAAK,GAAG;CAC7D,OAAO,OAAO,KAAK,UAAU,KAAK,YAAY,CAAC,IAAI,KAAA;AACrD"}
@@ -1,5 +1,22 @@
1
1
  //#region src/media/primitives/select-tracks.ts
2
2
  /**
3
+ * Test whether a track matches a partial-track description: every present,
4
+ * defined field of `filter` equals the track's. Absent or `undefined` filter
5
+ * fields don't constrain. Used to narrow candidates by a user selection
6
+ * (`{ id }`, `{ language }`, `{ height }`, …).
7
+ *
8
+ * @param track - The track to test
9
+ * @param filter - Partial-track description; only present, defined fields constrain
10
+ * @returns `true` when the track matches every constraining field
11
+ */
12
+ function matchesPartialTrack(track, filter) {
13
+ for (const key in filter) {
14
+ const filterValue = filter[key];
15
+ if (filterValue !== void 0 && track[key] !== filterValue) return false;
16
+ }
17
+ return true;
18
+ }
19
+ /**
3
20
  * Pick the first track of the given type from a presentation.
4
21
  *
5
22
  * Returns the first track in the first switching set of the matching
@@ -12,21 +29,89 @@ function pickFirstTrackId(presentation, type) {
12
29
  return presentation.selectionSets?.find((set) => set.type === type)?.switchingSets[0]?.tracks[0]?.id;
13
30
  }
14
31
  /**
15
- * Pick text track to activate. Conforms to the `TrackPicker` contract so it
16
- * can be used directly as a default picker for `selectTextTrack` without an
17
- * adapter wrapper.
32
+ * Translates a "max resolution" into a total total pixel area
33
+ * for comparisons with video track resolutions with an assumed
34
+ * 16:9 ratio.
35
+ *
36
+ * Example: "720p" translates to a 921600 pixel area.
37
+ *
38
+ * Because 720 * 1280 = 720 * (720 * (16/9) ) = 921_600
39
+ *
40
+ * Accepts:
41
+ * - string with the format '{height}p'. ('720p')
42
+ * - bare number, interpreted as pixel area. (921_600)
43
+ * - anything else will translate to `+Infinity`, meaning no cap specified
44
+ */
45
+ function maxResolutionToPixelArea(value) {
46
+ if (value === void 0 || value === null) return Number.POSITIVE_INFINITY;
47
+ if (typeof value === "number") return Number.isFinite(value) && value > 0 ? value : Number.POSITIVE_INFINITY;
48
+ const match = value.trim().match(/^(\d+)p?$/i);
49
+ if (!match) return Number.POSITIVE_INFINITY;
50
+ const height = Number(match[1]);
51
+ if (!(Number.isFinite(height) && height > 0)) return Number.POSITIVE_INFINITY;
52
+ return height * height * 16 / 9;
53
+ }
54
+ /**
55
+ * Pick the track with the highest pixel area at or below `maxPixelArea`.
56
+ * Falls back to the lowest track when nothing satisfies the cap (the
57
+ * lowest of the above-cap set is the closest to the cap from above).
58
+ * Tiebreak on bandwidth. Missing dimensions are treated as area `0`.
59
+ */
60
+ function pickTrackUnderPixelArea(tracks, maxPixelArea = Number.POSITIVE_INFINITY) {
61
+ if (tracks.length === 0) return void 0;
62
+ const sorted = [...tracks].sort((a, b) => (b.width ?? 0) * (b.height ?? 0) - (a.width ?? 0) * (a.height ?? 0) || (b.bandwidth ?? 0) - (a.bandwidth ?? 0));
63
+ return sorted.find((t) => (t.width ?? 0) * (t.height ?? 0) <= maxPixelArea) ?? sorted[sorted.length - 1];
64
+ }
65
+ /**
66
+ * Pick the video track with the highest pixel area.
67
+ *
68
+ * Pair with `selectVideoTrack`; compose `switchVideoQuality` instead
69
+ * for runtime-adapted quality.
70
+ */
71
+ function pickHighestResolutionVideoTrack(presentation) {
72
+ const tracks = (presentation.selectionSets?.find((set) => set.type === "video"))?.switchingSets[0]?.tracks;
73
+ if (!tracks?.length) return void 0;
74
+ return pickTrackUnderPixelArea(tracks)?.id;
75
+ }
76
+ /**
77
+ * Pick audio track.
18
78
  *
19
- * Selection priority (if enabled):
20
- * 1. User preference (preferredSubtitleLanguage)
21
- * 2. DEFAULT track (if enableDefaultTrack is true and track has DEFAULT=YES + AUTOSELECT=YES)
22
- * 3. No auto-selection (user opt-in)
79
+ * Selection priority:
80
+ * 1. First track matching preferred language (if specified)
81
+ * 2. First default track
82
+ * 3. First audio track
83
+ *
84
+ * @param presentation - Presentation with audio tracks
85
+ * @param config - Selection configuration (preferred language)
86
+ * @returns Selected audio track ID, or undefined if no audio tracks
87
+ */
88
+ function pickAudioTrack(presentation, config) {
89
+ const audioSet = presentation.selectionSets?.find((set) => set.type === "audio");
90
+ if (!audioSet || audioSet.switchingSets.length === 0) return;
91
+ const switchingSet = audioSet.switchingSets[0];
92
+ if (!switchingSet || switchingSet.tracks.length === 0) return;
93
+ const tracks = switchingSet.tracks;
94
+ if (config?.preferredAudioLanguage) {
95
+ const languageMatch = tracks.find((track) => track.language === config.preferredAudioLanguage);
96
+ if (languageMatch) return languageMatch.id;
97
+ }
98
+ const defaultTrack = tracks.find((track) => track.default === true);
99
+ if (defaultTrack) return defaultTrack.id;
100
+ return tracks[0]?.id;
101
+ }
102
+ /**
103
+ * Default text-track policy over an explicit candidate list (rather than a whole
104
+ * presentation): the opt-in three-tier pick `pickTextTrack` delegates to, factored
105
+ * out so a caller that has already narrowed the candidates — a constrained,
106
+ * CDN-scoped track-switching chain — applies the same policy without re-deriving
107
+ * from the presentation.
23
108
  *
24
- * By default, FORCED tracks are excluded per Apple's HLS spec.
109
+ * Priority: `preferredSubtitleLanguage` match `DEFAULT=YES + AUTOSELECT=YES`
110
+ * (only when `enableDefaultTrack`) → `undefined` (opt-in). FORCED tracks are
111
+ * excluded unless `includeForcedTracks` (Apple-spec: a regular track must carry
112
+ * forced content when both exist, so a forced-only track is redundant).
25
113
  */
26
- function pickTextTrack(presentation, config) {
27
- const textSet = presentation.selectionSets?.find((set) => set.type === "text");
28
- if (!textSet?.switchingSets?.[0]?.tracks.length) return void 0;
29
- const tracks = textSet.switchingSets[0].tracks;
114
+ function pickTextTrackFromTracks(tracks, config) {
30
115
  const availableTracks = config?.includeForcedTracks ? tracks : tracks.filter((track) => !track.forced);
31
116
  if (availableTracks.length === 0) return void 0;
32
117
  const { preferredSubtitleLanguage, enableDefaultTrack = false } = config ?? {};
@@ -40,6 +125,6 @@ function pickTextTrack(presentation, config) {
40
125
  }
41
126
  }
42
127
  //#endregion
43
- export { pickFirstTrackId, pickTextTrack };
128
+ export { matchesPartialTrack, maxResolutionToPixelArea, pickAudioTrack, pickFirstTrackId, pickHighestResolutionVideoTrack, pickTextTrackFromTracks, pickTrackUnderPixelArea };
44
129
 
45
130
  //# sourceMappingURL=select-tracks.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"select-tracks.js","names":[],"sources":["../../../../src/media/primitives/select-tracks.ts"],"sourcesContent":["import { DEFAULT_QUALITY_CONFIG, selectQuality } from '../abr/quality-selection';\nimport type {\n AudioSelectionSet,\n MaybeResolvedPresentation,\n Presentation,\n TrackType,\n VideoSelectionSet,\n} 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?: MaybeResolvedPresentation;\n selectedVideoTrackId?: string;\n selectedAudioTrackId?: string;\n selectedTextTrackId?: string;\n}\n\n/**\n * Context shape for track selection.\n * Currently empty - reserved for future use (e.g., bandwidth estimator).\n */\nexport type TrackSelectionContext = 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 *\n * Standalone (not extending `TrackSelectionConfig<'text'>`) because\n * `pickTextTrack` hardcodes the `'text'` type lookup internally — the\n * discriminant field carries no information at runtime, so we don't\n * require callers to pass it.\n */\nexport interface TextSelectionConfig {\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 * Contract for a track picker — a pure function that consults a\n * presentation (and optional config) and returns the id of the track to\n * select, or `undefined` to leave the slot unset.\n *\n * Behaviors that own a track-selection slot (`selectAudioTrack`,\n * `selectTextTrack`, `selectVideoTrack`, `switchVideoQuality`) accept a\n * `TrackPicker` via config. The behavior passes its own config straight\n * through as the picker's second argument — pickers that need richer\n * options (language preferences, default-track filtering, bandwidth-aware\n * selection) read from `config`; pickers that don't (e.g., first-track)\n * ignore it.\n */\nexport type TrackPicker<Config = unknown> = (\n presentation: MaybeResolvedPresentation,\n config?: Config\n) => string | undefined;\n\n/**\n * Pick the first track of the given type from a presentation.\n *\n * Returns the first track in the first switching set of the matching\n * selection set, or `undefined` if either is missing. POC-shaped\n * default-pick — `pickVideoTrack` / `pickAudioTrack` honor bandwidth +\n * language preferences and will replace this once selection callers are\n * ready.\n */\nexport function pickFirstTrackId(presentation: MaybeResolvedPresentation, type: TrackType): string | undefined {\n return presentation.selectionSets?.find((set) => set.type === type)?.switchingSets[0]?.tracks[0]?.id;\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. Conforms to the `TrackPicker` contract so it\n * can be used directly as a default picker for `selectTextTrack` without an\n * adapter wrapper.\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 */\nexport function pickTextTrack(\n presentation: MaybeResolvedPresentation,\n config?: TextSelectionConfig\n): 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"],"mappings":";;;;;;;;;;AAgJA,SAAgB,iBAAiB,cAAyC,MAAqC;AAC7G,QAAO,aAAa,eAAe,MAAM,QAAQ,IAAI,SAAS,KAAK,EAAE,cAAc,IAAI,OAAO,IAAI;;;;;;;;;;;;;;AA4FpG,SAAgB,cACd,cACA,QACoB;CACpB,MAAM,UAAU,aAAa,eAAe,MAAM,QAAQ,IAAI,SAAS,OAAO;AAC9E,KAAI,CAAC,SAAS,gBAAgB,IAAI,OAAO,OAAQ,QAAO,KAAA;CAExD,MAAM,SAAS,QAAQ,cAAc,GAAG;CAIxC,MAAM,kBAAkB,QAAQ,sBAAsB,SAAS,OAAO,QAAQ,UAAU,CAAC,MAAM,OAAO;AAEtG,KAAI,gBAAgB,WAAW,EAAG,QAAO,KAAA;CAEzC,MAAM,EAAE,2BAA2B,qBAAqB,UAAU,UAAU,EAAE;AAG9E,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"}
1
+ {"version":3,"file":"select-tracks.js","names":[],"sources":["../../../../src/media/primitives/select-tracks.ts"],"sourcesContent":["import { DEFAULT_QUALITY_CONFIG, selectQuality } from '../abr/quality-selection';\nimport type {\n AudioSelectionSet,\n MaybeResolvedPresentation,\n PartiallyResolvedTextTrack,\n TextTrack,\n TrackType,\n VideoSelectionSet,\n} 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?: MaybeResolvedPresentation;\n selectedVideoTrackId?: string;\n selectedAudioTrackId?: string;\n selectedTextTrackId?: string;\n}\n\n/**\n * Context shape for track selection.\n * Currently empty - reserved for future use (e.g., bandwidth estimator).\n */\nexport type TrackSelectionContext = 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 * Configuration for video track selection.\n */\nexport interface VideoSelectionConfig {\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 */\nexport interface AudioSelectionConfig {\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 */\nexport interface TextSelectionConfig {\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 * Contract for a track picker — a pure function that consults a\n * presentation (and optional config) and returns the id of the track to\n * select, or `undefined` to leave the slot unset.\n *\n * Behaviors that own a track-selection slot (`selectAudioTrack`,\n * `selectVideoTrack`, `switchVideoTrack`) accept a\n * `TrackPicker` via config. The behavior passes its own config straight\n * through as the picker's second argument — pickers that need richer\n * options (language preferences, default-track filtering, bandwidth-aware\n * selection) read from `config`; pickers that don't (e.g., first-track)\n * ignore it.\n */\nexport type TrackPicker<Config = unknown> = (\n presentation: MaybeResolvedPresentation,\n config?: Config\n) => string | undefined;\n\n/**\n * Test whether a track matches a partial-track description: every present,\n * defined field of `filter` equals the track's. Absent or `undefined` filter\n * fields don't constrain. Used to narrow candidates by a user selection\n * (`{ id }`, `{ language }`, `{ height }`, …).\n *\n * @param track - The track to test\n * @param filter - Partial-track description; only present, defined fields constrain\n * @returns `true` when the track matches every constraining field\n */\nexport function matchesPartialTrack<T>(track: T, filter: Partial<T>): boolean {\n for (const key in filter) {\n const filterValue = filter[key as keyof T];\n if (filterValue !== undefined && track[key as keyof T] !== filterValue) return false;\n }\n return true;\n}\n\n/**\n * Pick the first track of the given type from a presentation.\n *\n * Returns the first track in the first switching set of the matching\n * selection set, or `undefined` if either is missing. POC-shaped\n * default-pick — `pickVideoTrack` / `pickAudioTrack` honor bandwidth +\n * language preferences and will replace this once selection callers are\n * ready.\n */\nexport function pickFirstTrackId(presentation: MaybeResolvedPresentation, type: TrackType): string | undefined {\n return presentation.selectionSets?.find((set) => set.type === type)?.switchingSets[0]?.tracks[0]?.id;\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(\n presentation: MaybeResolvedPresentation,\n config?: VideoSelectionConfig\n): 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, { bandwidth: initialBandwidth, safetyMargin });\n\n return selected?.id;\n}\n\n/**\n * Translates a \"max resolution\" into a total total pixel area\n * for comparisons with video track resolutions with an assumed\n * 16:9 ratio.\n *\n * Example: \"720p\" translates to a 921600 pixel area.\n *\n * Because 720 * 1280 = 720 * (720 * (16/9) ) = 921_600\n *\n * Accepts:\n * - string with the format '{height}p'. ('720p')\n * - bare number, interpreted as pixel area. (921_600)\n * - anything else will translate to `+Infinity`, meaning no cap specified\n */\nexport function maxResolutionToPixelArea(value: string | number | undefined): number {\n if (value === undefined || value === null) return Number.POSITIVE_INFINITY;\n if (typeof value === 'number') return Number.isFinite(value) && value > 0 ? value : Number.POSITIVE_INFINITY;\n const match = value.trim().match(/^(\\d+)p?$/i);\n if (!match) return Number.POSITIVE_INFINITY;\n const height = Number(match[1]);\n if (!(Number.isFinite(height) && height > 0)) return Number.POSITIVE_INFINITY;\n return (height * height * 16) / 9;\n}\n\ntype RankableTrack = { id: string; width?: number; height?: number; bandwidth?: number };\n\n/**\n * Pick the track with the highest pixel area at or below `maxPixelArea`.\n * Falls back to the lowest track when nothing satisfies the cap (the\n * lowest of the above-cap set is the closest to the cap from above).\n * Tiebreak on bandwidth. Missing dimensions are treated as area `0`.\n */\nexport function pickTrackUnderPixelArea<T extends RankableTrack>(\n tracks: readonly T[],\n maxPixelArea: number = Number.POSITIVE_INFINITY\n): T | undefined {\n if (tracks.length === 0) return undefined;\n\n // Sort descending by pixel area, bandwidth as tiebreaker. List sizes\n // are small (HLS variant counts) — no need to optimize past a sort.\n const sorted = [...tracks].sort(\n (a, b) =>\n (b.width ?? 0) * (b.height ?? 0) - (a.width ?? 0) * (a.height ?? 0) || (b.bandwidth ?? 0) - (a.bandwidth ?? 0)\n );\n\n return sorted.find((t) => (t.width ?? 0) * (t.height ?? 0) <= maxPixelArea) ?? sorted[sorted.length - 1];\n}\n\n/**\n * Pick the video track with the highest pixel area.\n *\n * Pair with `selectVideoTrack`; compose `switchVideoQuality` instead\n * for runtime-adapted quality.\n */\nexport function pickHighestResolutionVideoTrack(presentation: MaybeResolvedPresentation): string | undefined {\n const videoSet = presentation.selectionSets?.find((set) => set.type === 'video') as VideoSelectionSet | undefined;\n const tracks = videoSet?.switchingSets[0]?.tracks;\n if (!tracks?.length) return undefined;\n return pickTrackUnderPixelArea(tracks)?.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(\n presentation: MaybeResolvedPresentation,\n config?: AudioSelectionConfig\n): 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 from a presentation. Conforms to the\n * `TrackPicker` contract. The candidate-list core (`pickTextTrackFromTracks`)\n * is the opt-in default policy `switchTextTrack`'s terminal applies once it has\n * narrowed the renditions.\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 */\nexport function pickTextTrack(\n presentation: MaybeResolvedPresentation,\n config?: TextSelectionConfig\n): string | undefined {\n const tracks = presentation.selectionSets?.find((set) => set.type === 'text')?.switchingSets?.[0]?.tracks;\n if (!tracks?.length) return undefined;\n return pickTextTrackFromTracks(tracks, config);\n}\n\n/**\n * Default text-track policy over an explicit candidate list (rather than a whole\n * presentation): the opt-in three-tier pick `pickTextTrack` delegates to, factored\n * out so a caller that has already narrowed the candidates — a constrained,\n * CDN-scoped track-switching chain — applies the same policy without re-deriving\n * from the presentation.\n *\n * Priority: `preferredSubtitleLanguage` match → `DEFAULT=YES + AUTOSELECT=YES`\n * (only when `enableDefaultTrack`) → `undefined` (opt-in). FORCED tracks are\n * excluded unless `includeForcedTracks` (Apple-spec: a regular track must carry\n * forced content when both exist, so a forced-only track is redundant).\n */\nexport function pickTextTrackFromTracks(\n tracks: readonly (PartiallyResolvedTextTrack | TextTrack)[],\n config?: TextSelectionConfig\n): string | undefined {\n const availableTracks = config?.includeForcedTracks ? tracks : tracks.filter((track) => !track.forced);\n if (availableTracks.length === 0) return undefined;\n\n const { preferredSubtitleLanguage, enableDefaultTrack = false } = config ?? {};\n\n if (preferredSubtitleLanguage) {\n const languageMatch = availableTracks.find((track) => track.language === preferredSubtitleLanguage);\n if (languageMatch) return languageMatch.id;\n }\n\n if (enableDefaultTrack) {\n const defaultTrack = availableTracks.find((track) => track.default === true);\n if (defaultTrack) return defaultTrack.id;\n }\n\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(state: TrackSelectionState, type: TrackType): boolean {\n return !!state?.presentation?.selectionSets?.find((set) => set.type === type)?.switchingSets?.[0]?.tracks.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(state: TrackSelectionState, type: TrackType): boolean {\n return !state[SelectedTrackIdKeyByType[type]];\n}\n"],"mappings":";;;;;;;;;;;AAmIA,SAAgB,oBAAuB,OAAU,QAA6B;CAC5E,KAAK,MAAM,OAAO,QAAQ;EACxB,MAAM,cAAc,OAAO;EAC3B,IAAI,gBAAgB,KAAA,KAAa,MAAM,SAAoB,aAAa,OAAO;CACjF;CACA,OAAO;AACT;;;;;;;;;;AAWA,SAAgB,iBAAiB,cAAyC,MAAqC;CAC7G,OAAO,aAAa,eAAe,MAAM,QAAQ,IAAI,SAAS,IAAI,CAAC,EAAE,cAAc,EAAE,EAAE,OAAO,EAAE,EAAE;AACpG;;;;;;;;;;;;;;;AAmDA,SAAgB,yBAAyB,OAA4C;CACnF,IAAI,UAAU,KAAA,KAAa,UAAU,MAAM,OAAO,OAAO;CACzD,IAAI,OAAO,UAAU,UAAU,OAAO,OAAO,SAAS,KAAK,KAAK,QAAQ,IAAI,QAAQ,OAAO;CAC3F,MAAM,QAAQ,MAAM,KAAK,CAAC,CAAC,MAAM,YAAY;CAC7C,IAAI,CAAC,OAAO,OAAO,OAAO;CAC1B,MAAM,SAAS,OAAO,MAAM,EAAE;CAC9B,IAAI,EAAE,OAAO,SAAS,MAAM,KAAK,SAAS,IAAI,OAAO,OAAO;CAC5D,OAAQ,SAAS,SAAS,KAAM;AAClC;;;;;;;AAUA,SAAgB,wBACd,QACA,eAAuB,OAAO,mBACf;CACf,IAAI,OAAO,WAAW,GAAG,OAAO,KAAA;CAIhC,MAAM,SAAS,CAAC,GAAG,MAAM,CAAC,CAAC,MACxB,GAAG,OACD,EAAE,SAAS,MAAM,EAAE,UAAU,MAAM,EAAE,SAAS,MAAM,EAAE,UAAU,OAAO,EAAE,aAAa,MAAM,EAAE,aAAa,EAChH;CAEA,OAAO,OAAO,MAAM,OAAO,EAAE,SAAS,MAAM,EAAE,UAAU,MAAM,YAAY,KAAK,OAAO,OAAO,SAAS;AACxG;;;;;;;AAQA,SAAgB,gCAAgC,cAA6D;CAE3G,MAAM,UADW,aAAa,eAAe,MAAM,QAAQ,IAAI,SAAS,OAAO,EAAA,EACtD,cAAc,EAAE,EAAE;CAC3C,IAAI,CAAC,QAAQ,QAAQ,OAAO,KAAA;CAC5B,OAAO,wBAAwB,MAAM,CAAC,EAAE;AAC1C;;;;;;;;;;;;;AAcA,SAAgB,eACd,cACA,QACoB;CACpB,MAAM,WAAW,aAAa,eAAe,MAAM,QAAQ,IAAI,SAAS,OAAO;CAE/E,IAAI,CAAC,YAAY,SAAS,cAAc,WAAW,GACjD;CAIF,MAAM,eAAe,SAAS,cAAc;CAC5C,IAAI,CAAC,gBAAgB,aAAa,OAAO,WAAW,GAClD;CAGF,MAAM,SAAS,aAAa;CAG5B,IAAI,QAAQ,wBAAwB;EAClC,MAAM,gBAAgB,OAAO,MAAM,UAAU,MAAM,aAAa,OAAO,sBAAsB;EAC7F,IAAI,eACF,OAAO,cAAc;CAEzB;CAGA,MAAM,eAAe,OAAO,MAAM,UAAU,MAAM,YAAY,IAAI;CAClE,IAAI,cACF,OAAO,aAAa;CAItB,OAAO,OAAO,EAAE,EAAE;AACpB;;;;;;;;;;;;;AAoCA,SAAgB,wBACd,QACA,QACoB;CACpB,MAAM,kBAAkB,QAAQ,sBAAsB,SAAS,OAAO,QAAQ,UAAU,CAAC,MAAM,MAAM;CACrG,IAAI,gBAAgB,WAAW,GAAG,OAAO,KAAA;CAEzC,MAAM,EAAE,2BAA2B,qBAAqB,UAAU,UAAU,CAAC;CAE7E,IAAI,2BAA2B;EAC7B,MAAM,gBAAgB,gBAAgB,MAAM,UAAU,MAAM,aAAa,yBAAyB;EAClG,IAAI,eAAe,OAAO,cAAc;CAC1C;CAEA,IAAI,oBAAoB;EACtB,MAAM,eAAe,gBAAgB,MAAM,UAAU,MAAM,YAAY,IAAI;EAC3E,IAAI,cAAc,OAAO,aAAa;CACxC;AAGF"}
@@ -0,0 +1,42 @@
1
+ //#region src/media/text/parse-vtt-timestamp-map.ts
2
+ const TIMESTAMP_MAP_PREFIX = "X-TIMESTAMP-MAP=";
3
+ /**
4
+ * Scrape a WebVTT segment's `X-TIMESTAMP-MAP` header into a {@link TimestampMap}
5
+ * — the only header line we need to correlate LOCAL cue times with the media
6
+ * presentation timeline. Deliberately *not* a WebVTT parser: cue parsing stays
7
+ * with the browser's native `<track>` parser (which drops this line); this reads
8
+ * just the one header field the native path discards.
9
+ *
10
+ * Returns `undefined` when the segment carries no map (e.g. cues already in
11
+ * absolute presentation time) — per the HLS spec that means LOCAL 0 maps to
12
+ * MPEGTS 0. Tolerant of attribute order and `[HH:]MM:SS.mmm` LOCAL forms.
13
+ */
14
+ function parseVttTimestampMap(text) {
15
+ const timestampMapLine = text.split(/\r\n|\r|\n/).find((line) => line.startsWith(TIMESTAMP_MAP_PREFIX));
16
+ return timestampMapLine ? parseTimestampMapBody(timestampMapLine.slice(16)) : void 0;
17
+ }
18
+ const TimeStampMapParserMap = {
19
+ LOCAL: parseWebVttTimestamp,
20
+ MPEGTS: (v) => +v
21
+ };
22
+ function parseTimestampMapBody(body) {
23
+ return Object.fromEntries(body.split(",").map((kvStr) => {
24
+ const [k, v] = kvStr.split(/:(.*)/).map((kOrV) => kOrV.trim());
25
+ return [k?.toLowerCase(), TimeStampMapParserMap[k](v)];
26
+ }));
27
+ }
28
+ /** Seconds-per-unit for the `[HH:]MM:SS.mmm` parts, right-aligned so a missing HH just drops the leading weight. */
29
+ const VTT_TIMESTAMP_WEIGHTS = [
30
+ 3600,
31
+ 60,
32
+ 1,
33
+ .001
34
+ ];
35
+ function parseWebVttTimestamp(value) {
36
+ const parts = value.split(/[:.]/);
37
+ return parts.reduce((acc, val, i) => acc + +val * (VTT_TIMESTAMP_WEIGHTS[i + 4 - parts.length] ?? 0), 0);
38
+ }
39
+ //#endregion
40
+ export { parseVttTimestampMap };
41
+
42
+ //# sourceMappingURL=parse-vtt-timestamp-map.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse-vtt-timestamp-map.js","names":[],"sources":["../../../../src/media/text/parse-vtt-timestamp-map.ts"],"sourcesContent":["/**\n * WebVTT-in-HLS `X-TIMESTAMP-MAP`: correlates a cue's LOCAL (in-file) time with\n * an MPEG-2 presentation timestamp, so LOCAL-authored cues can be placed on the\n * media presentation timeline. Stored raw — the LOCAL→native correction is\n * `mpegts / 90000 - local` — so it stays independent of how (or whether) the\n * presentation is later re-origined. See the HLS spec, RFC 8216bis §3.5.\n */\nexport interface TimestampMap {\n /** The MPEG-2 presentation timestamp, in 90 kHz ticks, as authored. */\n mpegts: number;\n /** The LOCAL cue time the `mpegts` value maps to, in seconds. */\n local: number;\n}\n\nconst TIMESTAMP_MAP_PREFIX = 'X-TIMESTAMP-MAP=';\n\n/**\n * Scrape a WebVTT segment's `X-TIMESTAMP-MAP` header into a {@link TimestampMap}\n * — the only header line we need to correlate LOCAL cue times with the media\n * presentation timeline. Deliberately *not* a WebVTT parser: cue parsing stays\n * with the browser's native `<track>` parser (which drops this line); this reads\n * just the one header field the native path discards.\n *\n * Returns `undefined` when the segment carries no map (e.g. cues already in\n * absolute presentation time) — per the HLS spec that means LOCAL 0 maps to\n * MPEGTS 0. Tolerant of attribute order and `[HH:]MM:SS.mmm` LOCAL forms.\n */\nexport function parseVttTimestampMap(text: string): TimestampMap | undefined {\n const timestampMapLine = text.split(/\\r\\n|\\r|\\n/).find((line) => line.startsWith(TIMESTAMP_MAP_PREFIX));\n return timestampMapLine ? parseTimestampMapBody(timestampMapLine.slice(TIMESTAMP_MAP_PREFIX.length)) : undefined;\n}\n\nconst TimeStampMapParserMap = {\n LOCAL: parseWebVttTimestamp,\n MPEGTS: (v: string) => +v,\n} as const;\n\ntype TimeStampMapParserMap = typeof TimeStampMapParserMap;\n\nfunction parseTimestampMapBody(body: string): TimestampMap | undefined {\n return Object.fromEntries(\n body.split(',').map((kvStr) => {\n const [k, v] = kvStr.split(/:(.*)/).map((kOrV) => kOrV.trim());\n return [k?.toLowerCase(), TimeStampMapParserMap[k as keyof TimeStampMapParserMap](v as string)];\n })\n ) as TimestampMap;\n}\n\n/** Seconds-per-unit for the `[HH:]MM:SS.mmm` parts, right-aligned so a missing HH just drops the leading weight. */\nconst VTT_TIMESTAMP_WEIGHTS = [3600, 60, 1, 0.001];\n\nfunction parseWebVttTimestamp(value: string): number {\n const parts = value.split(/[:.]/);\n return parts.reduce((acc, val, i) => acc + +val * (VTT_TIMESTAMP_WEIGHTS[i + 4 - parts.length] ?? 0), 0);\n}\n"],"mappings":";AAcA,MAAM,uBAAuB;;;;;;;;;;;;AAa7B,SAAgB,qBAAqB,MAAwC;CAC3E,MAAM,mBAAmB,KAAK,MAAM,YAAY,CAAC,CAAC,MAAM,SAAS,KAAK,WAAW,oBAAoB,CAAC;CACtG,OAAO,mBAAmB,sBAAsB,iBAAiB,MAAM,EAA2B,CAAC,IAAI,KAAA;AACzG;AAEA,MAAM,wBAAwB;CAC5B,OAAO;CACP,SAAS,MAAc,CAAC;AAC1B;AAIA,SAAS,sBAAsB,MAAwC;CACrE,OAAO,OAAO,YACZ,KAAK,MAAM,GAAG,CAAC,CAAC,KAAK,UAAU;EAC7B,MAAM,CAAC,GAAG,KAAK,MAAM,MAAM,OAAO,CAAC,CAAC,KAAK,SAAS,KAAK,KAAK,CAAC;EAC7D,OAAO,CAAC,GAAG,YAAY,GAAG,sBAAsB,EAAiC,CAAC,CAAW,CAAC;CAChG,CAAC,CACH;AACF;;AAGA,MAAM,wBAAwB;CAAC;CAAM;CAAI;CAAG;AAAK;AAEjD,SAAS,qBAAqB,OAAuB;CACnD,MAAM,QAAQ,MAAM,MAAM,MAAM;CAChC,OAAO,MAAM,QAAQ,KAAK,KAAK,MAAM,MAAM,CAAC,OAAO,sBAAsB,IAAI,IAAI,MAAM,WAAW,IAAI,CAAC;AACzG"}
@@ -0,0 +1,23 @@
1
+ import { parseVttTimestampMap } from "./parse-vtt-timestamp-map.js";
2
+ //#region src/media/text/resolve-vtt-metadata.ts
3
+ /**
4
+ * Header-level text-segment metadata — the `X-TIMESTAMP-MAP` correlation scraped from
5
+ * a VTT segment's raw bytes. DOM-free (a plain fetch + regex parse): the native
6
+ * `<track>` parser in `media/dom/text` (`resolveVttSegment`) discards this header, so a
7
+ * caller that needs it (e.g. non-zero-PTS relocation) fetches the bytes itself.
8
+ */
9
+ /**
10
+ * Fetch a VTT segment and scrape only its header metadata (no cue parsing).
11
+ *
12
+ * The native `<track>` parser (`media/dom/text`'s `resolveVttSegment`) discards
13
+ * `X-TIMESTAMP-MAP`, so reading it requires the raw bytes. This is a separate,
14
+ * caller-controlled fetch — the caller decides *when* metadata is needed (e.g.
15
+ * once per source) rather than paying for it on every segment.
16
+ */
17
+ async function resolveVttSegmentMetadata(url) {
18
+ return { timestampMap: parseVttTimestampMap(await fetch(url).then((response) => response.text())) };
19
+ }
20
+ //#endregion
21
+ export { resolveVttSegmentMetadata };
22
+
23
+ //# sourceMappingURL=resolve-vtt-metadata.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve-vtt-metadata.js","names":[],"sources":["../../../../src/media/text/resolve-vtt-metadata.ts"],"sourcesContent":["/**\n * Header-level text-segment metadata — the `X-TIMESTAMP-MAP` correlation scraped from\n * a VTT segment's raw bytes. DOM-free (a plain fetch + regex parse): the native\n * `<track>` parser in `media/dom/text` (`resolveVttSegment`) discards this header, so a\n * caller that needs it (e.g. non-zero-PTS relocation) fetches the bytes itself.\n */\nimport { parseVttTimestampMap, type TimestampMap } from './parse-vtt-timestamp-map';\n\n/**\n * Header-level metadata for a text segment, surfaced alongside its cues. Each\n * field is present only when the segment declared it.\n */\nexport interface TextSegmentMetadata {\n timestampMap?: TimestampMap;\n}\n\n/**\n * Fetch a VTT segment and scrape only its header metadata (no cue parsing).\n *\n * The native `<track>` parser (`media/dom/text`'s `resolveVttSegment`) discards\n * `X-TIMESTAMP-MAP`, so reading it requires the raw bytes. This is a separate,\n * caller-controlled fetch — the caller decides *when* metadata is needed (e.g.\n * once per source) rather than paying for it on every segment.\n */\nexport async function resolveVttSegmentMetadata(url: string): Promise<TextSegmentMetadata> {\n const text = await fetch(url).then((response) => response.text());\n return { timestampMap: parseVttTimestampMap(text) };\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAwBA,eAAsB,0BAA0B,KAA2C;CAEzF,OAAO,EAAE,cAAc,qBAAqB,MADzB,MAAM,GAAG,CAAC,CAAC,MAAM,aAAa,SAAS,KAAK,CAAC,CAChB,EAAE;AACpD"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../../../src/media/types/index.ts"],"sourcesContent":["/**\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 * Minimal text-track cue shape — start time, end time, and display text.\n *\n * Host-agnostic representation. `VTTCue` structurally satisfies this\n * interface, so DOM consumers pass `VTTCue` values directly. Non-DOM\n * hosts (workers, test fakes, non-browser engines) can satisfy the same\n * shape without pulling in DOM types.\n */\nexport interface Cue {\n startTime: number;\n endTime: number;\n text: string;\n}\n\n/**\n * Media element with an iterable text-track list, host-agnostic.\n *\n * Extends `MediaElementLike` with the minimum surface needed to observe\n * which text tracks are currently mounted on the media. `HTMLMediaElement`\n * structurally satisfies this (its `textTracks` is a `TextTrackList`,\n * which is iterable with `{ id }` items).\n */\nexport interface MediaElementWithTextTracks extends MediaElementLike {\n readonly textTracks: Iterable<{ readonly id: string }>;\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 * 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// 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 * Floating-point tolerance for matching segments by `startTime`. Two\n * segments are considered the same position when\n * `Math.abs(a.startTime - b.startTime) < SEGMENT_TIME_EPSILON`. Used by\n * the source-buffer dedup and segment-loader quality-aware filter to\n * tolerate sub-millisecond drift in segment timestamps across multiple\n * playlists / quality levels.\n */\nexport const SEGMENT_TIME_EPSILON = 0.0001;\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 * State-shaped presentation that may or may not be resolved yet.\n *\n * The lifecycle is a single value: a caller writes `{ url }`, and the\n * resolver populates the rest in place. `url` is always present; resolved\n * fields (`id`, `selectionSets`, duration) appear once parsing succeeds.\n *\n * Use `isResolvedPresentation` to narrow to `Presentation`.\n */\nexport type MaybeResolvedPresentation = AddressableObject & Partial<Omit<Presentation, keyof AddressableObject>>;\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: MaybeResolvedPresentation\n): presentation is MaybeResolvedPresentation & { duration: number } {\n return presentation.duration !== undefined;\n}\n\n/**\n * Narrows a `MaybeResolvedPresentation` to a fully resolved `Presentation`.\n *\n * A presentation is resolved once `resolvePresentation` has parsed the\n * manifest and populated both `id` and `selectionSets`. Both must be\n * present — a partial value with only one of them isn't usable, and\n * letting it through would have downstream behaviors crash when they\n * access `selectionSets`.\n */\nexport function isResolvedPresentation(\n presentation: MaybeResolvedPresentation | undefined\n): presentation is Presentation {\n return presentation !== undefined && presentation.id !== undefined && presentation.selectionSets !== undefined;\n}\n"],"mappings":";;;;;;;;;AAmTA,MAAa,uBAAuB;AA4DpC,SAAgB,gBAAgB,OAAuE;AACrG,QAAO,cAAc;;;;;;AAOvB,SAAgB,wBACd,cACkE;AAClE,QAAO,aAAa,aAAa,KAAA;;;;;;;;;;;AAYnC,SAAgB,uBACd,cAC8B;AAC9B,QAAO,iBAAiB,KAAA,KAAa,aAAa,OAAO,KAAA,KAAa,aAAa,kBAAkB,KAAA"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../../src/media/types/index.ts"],"sourcesContent":["/**\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 * Media-timeline (decode/encode) coordinate of the track's timeline origin\n * (`startTime`) — the media-time base value of the coordinate model, peer to\n * `startTime` (presentation). Derived from the container\n * (`tfdt.baseMediaDecodeTime ÷ mdhd.timescale`); the relocation offset is\n * `startTime − startMediaTime`, never stored.\n *\n * Optional: absent until established (0-PTS sources never set it — their\n * origin is already 0). Established once per source by the\n * `establishStartMediaTime` reactor. See\n * `internal/design/spf/presentation-timeline-model.md`.\n */\n startMediaTime?: number;\n };\n\n/**\n * Per-track-type origin-establishment data, accumulated across appends (the media\n * track's `track_id` + `mdhd` timescale from the init, `tfdt` baseMediaDecodeTime of\n * that same track from the first media segment) — hence optional. The transient input\n * the `establishStartMediaTime` reactor reduces into `Track.startMediaTime`.\n *\n * `trackId` is the ISO-BMFF `track_ID` of the buffered media track (`vide`/`soun`),\n * read from the init's `tkhd`; it ties the timescale to the *same* track's\n * `baseMediaDecodeTime` (matched via `tfhd.track_id`) so a muxed segment carrying a\n * second track (e.g. `clcp` captions) reads the right `tfdt` rather than the first one.\n *\n * `segmentStartTime` is the 0-based presentation start of the segment\n * `baseMediaDecodeTime` was read from — *not* a container value (it's the playlist\n * position), but co-located because the origin is `baseMediaDecodeTime/timescale −\n * segmentStartTime`: the first *loaded* segment isn't necessarily the 0th (a\n * non-zero initial `currentTime`, or live/DVR), so the decode time alone isn't the\n * stream origin.\n */\nexport interface MediaContainerData {\n trackId?: number;\n timescale?: number;\n baseMediaDecodeTime?: number;\n segmentStartTime?: number;\n}\n\n/**\n * Raw media-segment bytes — a complete buffer or a byte stream. The transport-neutral\n * payload the loader pipeline carries; `AppendData` (the MSE `SourceBuffer` append\n * input in `media/dom/mse`) is an alias of this at the DOM boundary.\n */\nexport type SegmentData = ArrayBuffer | AsyncIterable<Uint8Array>;\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 /**\n * Audio groups (`EXT-X-STREAM-INF:AUDIO`) this video rendition can pair\n * with. A list because one rendition is typically listed across multiple\n * `EXT-X-STREAM-INF` entries — one per audio group (the HLS cross-product) —\n * which the parser collapses into a single track carrying every group it\n * advertised.\n */\n audioGroupIds?: 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 * Predicate that answers \"can this environment decode this track?\" — the\n * capability-probing surface, read by the track-switching hard-constraint\n * pre-pass (`excludeUnplayableTracks`) to drop undecodable renditions before\n * selection. Kept DOM-free here (a plain function type over a minimal track\n * shape) so DOM-free behaviors can consume it; the DOM implementation\n * (`canPlayTrack` in `media/dom/capabilities.ts`) wraps\n * `MediaSource.isTypeSupported`.\n *\n * Takes the minimal codec-bearing shape both video and audio candidates\n * carry. `mimeType` is optional so unprobeable candidates (no MIME) can be\n * passed straight through as playable rather than dropped.\n */\nexport type CanPlayTrack = (track: { mimeType?: string; codecs?: string[] }) => boolean;\n\n/**\n * Minimal text-track cue shape — start time, end time, and display text.\n *\n * Host-agnostic representation. `VTTCue` structurally satisfies this\n * interface, so DOM consumers pass `VTTCue` values directly. Non-DOM\n * hosts (workers, test fakes, non-browser engines) can satisfy the same\n * shape without pulling in DOM types.\n */\nexport interface Cue {\n startTime: number;\n endTime: number;\n text: string;\n}\n\n/**\n * Media element with an iterable text-track list, host-agnostic.\n *\n * Extends `MediaElementLike` with the minimum surface needed to observe\n * which text tracks are currently mounted on the media. `HTMLMediaElement`\n * structurally satisfies this (its `textTracks` is a `TextTrackList`,\n * which is iterable with `{ id }` items).\n */\nexport interface MediaElementWithTextTracks extends MediaElementLike {\n readonly textTracks: Iterable<{ readonly id: string }>;\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 * 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// 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 * Floating-point tolerance for matching segments by `startTime`. Two\n * segments are considered the same position when\n * `Math.abs(a.startTime - b.startTime) < SEGMENT_TIME_EPSILON`. Used by\n * the source-buffer dedup and segment-loader quality-aware filter to\n * tolerate sub-millisecond drift in segment timestamps across multiple\n * playlists / quality levels.\n */\nexport const SEGMENT_TIME_EPSILON = 0.0001;\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 * State-shaped presentation that may or may not be resolved yet.\n *\n * The lifecycle is a single value: a caller writes `{ url }`, and the\n * resolver populates the rest in place. `url` is always present; resolved\n * fields (`id`, `selectionSets`, duration) appear once parsing succeeds.\n *\n * Use `isResolvedPresentation` to narrow to `Presentation`.\n */\nexport type MaybeResolvedPresentation = AddressableObject & Partial<Omit<Presentation, keyof AddressableObject>>;\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: MaybeResolvedPresentation\n): presentation is MaybeResolvedPresentation & { duration: number } {\n return presentation.duration !== undefined;\n}\n\n/**\n * Narrows a `MaybeResolvedPresentation` to a fully resolved `Presentation`.\n *\n * A presentation is resolved once `resolvePresentation` has parsed the\n * manifest and populated both `id` and `selectionSets`. Both must be\n * present — a partial value with only one of them isn't usable, and\n * letting it through would have downstream behaviors crash when they\n * access `selectionSets`.\n */\nexport function isResolvedPresentation(\n presentation: MaybeResolvedPresentation | undefined\n): presentation is Presentation {\n return presentation !== undefined && presentation.id !== undefined && presentation.selectionSets !== undefined;\n}\n"],"mappings":";;;;;;;;;AAsXA,MAAa,uBAAuB;AA4DpC,SAAgB,gBAAgB,OAAuE;CACrG,OAAO,cAAc;AACvB;;;;;AAMA,SAAgB,wBACd,cACkE;CAClE,OAAO,aAAa,aAAa,KAAA;AACnC;;;;;;;;;;AAWA,SAAgB,uBACd,cAC8B;CAC9B,OAAO,iBAAiB,KAAA,KAAa,aAAa,OAAO,KAAA,KAAa,aAAa,kBAAkB,KAAA;AACvG"}
@@ -0,0 +1,56 @@
1
+ //#region src/media/utils/cdn.ts
2
+ /**
3
+ * Default {@link GetCdnId}: the URL's origin (scheme + host + port); falls back
4
+ * to the raw string when the URL can't be parsed, so the return value is always
5
+ * a stable grouping key.
6
+ */
7
+ function getCdnId(url) {
8
+ try {
9
+ return new URL(url).origin;
10
+ } catch {
11
+ return url;
12
+ }
13
+ }
14
+ const CDN_TYPE_PRIORITY = {
15
+ video: 0,
16
+ audio: 1,
17
+ text: 2
18
+ };
19
+ /**
20
+ * The distinct CDNs a presentation's tracks are served from, ordered video CDNs
21
+ * first, then audio, then text (manifest order within a type). The head is the
22
+ * primary CDN — the one a sticky pick defaults to — and is always video-derived
23
+ * when the source has video. Returns `[]` for an unresolved presentation with
24
+ * no tracks.
25
+ *
26
+ * Redundant-stream sources list the same content on multiple hosts (e.g. Mux's
27
+ * `?redundant_streams=true`), so each host contributes its own candidate tracks;
28
+ * this collapses them to the set of CDNs across every track type. The CDN-id
29
+ * derivation defaults to {@link getCdnId}; pass a consumer-configured `getId` to
30
+ * key on something other than origin.
31
+ */
32
+ function getOrderedCdnIds(presentation, getId = getCdnId) {
33
+ const seen = /* @__PURE__ */ new Set();
34
+ const ids = [];
35
+ const selectionSets = [...presentation.selectionSets ?? []].sort((a, b) => CDN_TYPE_PRIORITY[a.type] - CDN_TYPE_PRIORITY[b.type]);
36
+ for (const selectionSet of selectionSets) for (const switchingSet of selectionSet.switchingSets) for (const track of switchingSet.tracks) {
37
+ const id = getId(track.url);
38
+ if (seen.has(id)) continue;
39
+ seen.add(id);
40
+ ids.push(id);
41
+ }
42
+ return ids;
43
+ }
44
+ /**
45
+ * Add a CDN id to a failed-CDN list, preserving order and ignoring duplicates.
46
+ * Idempotent: re-adding an already-present id returns the same array reference
47
+ * (so a no-op trip doesn't churn the `failedCdns` signal). The failover trip in
48
+ * `resolve-track` and the segment loaders feed this into `failedCdns` via `update`.
49
+ */
50
+ function addFailedCdn(failed, cdn) {
51
+ return failed?.includes(cdn) ? failed : [...failed ?? [], cdn];
52
+ }
53
+ //#endregion
54
+ export { addFailedCdn, getCdnId, getOrderedCdnIds };
55
+
56
+ //# sourceMappingURL=cdn.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cdn.js","names":[],"sources":["../../../../src/media/utils/cdn.ts"],"sourcesContent":["import type { MaybeResolvedPresentation, TrackType } from '../types';\n\n/**\n * Derive a stable grouping key for the CDN a URL is served from. Synchronous and\n * pure (deliberately not a `resolve*` — no fetch). Consumers override the\n * default via the engine's `getCdnId` config (e.g. to key on Mux's `cdn=` query\n * param instead of the host); every CDN-identity site reads that same function\n * so keys stay comparable across `cdnPriority`, `failedCdns`, and the\n * track-switching constraint + scope.\n */\nexport type GetCdnId = (url: string) => string;\n\n/**\n * Default {@link GetCdnId}: the URL's origin (scheme + host + port); falls back\n * to the raw string when the URL can't be parsed, so the return value is always\n * a stable grouping key.\n */\nexport function getCdnId(url: string): string {\n try {\n return new URL(url).origin;\n } catch {\n return url;\n }\n}\n\n// Track-type priority for CDN ordering: video first, then audio, then text.\n// Selection sets are visited in this order so the head of the returned list is\n// always video-derived. `preferActiveCdn` anchors every track type to the\n// first CDN with surviving tracks (the head), so this makes \"the primary CDN\n// is the video CDN\" a guarantee of `getOrderedCdnIds` rather than a side effect\n// of the order tracks happen to be parsed in.\nconst CDN_TYPE_PRIORITY: Record<TrackType, number> = { video: 0, audio: 1, text: 2 };\n\n/**\n * The distinct CDNs a presentation's tracks are served from, ordered video CDNs\n * first, then audio, then text (manifest order within a type). The head is the\n * primary CDN — the one a sticky pick defaults to — and is always video-derived\n * when the source has video. Returns `[]` for an unresolved presentation with\n * no tracks.\n *\n * Redundant-stream sources list the same content on multiple hosts (e.g. Mux's\n * `?redundant_streams=true`), so each host contributes its own candidate tracks;\n * this collapses them to the set of CDNs across every track type. The CDN-id\n * derivation defaults to {@link getCdnId}; pass a consumer-configured `getId` to\n * key on something other than origin.\n */\nexport function getOrderedCdnIds(presentation: MaybeResolvedPresentation, getId: GetCdnId = getCdnId): string[] {\n const seen = new Set<string>();\n const ids: string[] = [];\n // Stable sort keeps manifest order among same-type selection sets.\n const selectionSets = [...(presentation.selectionSets ?? [])].sort(\n (a, b) => CDN_TYPE_PRIORITY[a.type] - CDN_TYPE_PRIORITY[b.type]\n );\n for (const selectionSet of selectionSets) {\n for (const switchingSet of selectionSet.switchingSets) {\n for (const track of switchingSet.tracks) {\n const id = getId(track.url);\n if (seen.has(id)) continue;\n seen.add(id);\n ids.push(id);\n }\n }\n }\n return ids;\n}\n\n/**\n * Add a CDN id to a failed-CDN list, preserving order and ignoring duplicates.\n * Idempotent: re-adding an already-present id returns the same array reference\n * (so a no-op trip doesn't churn the `failedCdns` signal). The failover trip in\n * `resolve-track` and the segment loaders feed this into `failedCdns` via `update`.\n */\nexport function addFailedCdn(failed: string[] | undefined, cdn: string): string[] {\n return failed?.includes(cdn) ? failed : [...(failed ?? []), cdn];\n}\n"],"mappings":";;;;;;AAiBA,SAAgB,SAAS,KAAqB;CAC5C,IAAI;EACF,OAAO,IAAI,IAAI,GAAG,CAAC,CAAC;CACtB,QAAQ;EACN,OAAO;CACT;AACF;AAQA,MAAM,oBAA+C;CAAE,OAAO;CAAG,OAAO;CAAG,MAAM;AAAE;;;;;;;;;;;;;;AAenF,SAAgB,iBAAiB,cAAyC,QAAkB,UAAoB;CAC9G,MAAM,uBAAO,IAAI,IAAY;CAC7B,MAAM,MAAgB,CAAC;CAEvB,MAAM,gBAAgB,CAAC,GAAI,aAAa,iBAAiB,CAAC,CAAE,CAAC,CAAC,MAC3D,GAAG,MAAM,kBAAkB,EAAE,QAAQ,kBAAkB,EAAE,KAC5D;CACA,KAAK,MAAM,gBAAgB,eACzB,KAAK,MAAM,gBAAgB,aAAa,eACtC,KAAK,MAAM,SAAS,aAAa,QAAQ;EACvC,MAAM,KAAK,MAAM,MAAM,GAAG;EAC1B,IAAI,KAAK,IAAI,EAAE,GAAG;EAClB,KAAK,IAAI,EAAE;EACX,IAAI,KAAK,EAAE;CACb;CAGJ,OAAO;AACT;;;;;;;AAQA,SAAgB,aAAa,QAA8B,KAAuB;CAChF,OAAO,QAAQ,SAAS,GAAG,IAAI,SAAS,CAAC,GAAI,UAAU,CAAC,GAAI,GAAG;AACjE"}
@@ -1 +1 @@
1
- {"version":3,"file":"preload.js","names":[],"sources":["../../../../src/media/utils/preload.ts"],"sourcesContent":["/**\n * The W3C-standard set of `<video>`/`<audio>` `preload` attribute values.\n * SPF allows extended values (e.g. `'canplay'`) on `state.preload`, which\n * are *not* reflected to the DOM — predicate below is the discriminator.\n */\nexport type StandardPreload = 'auto' | 'metadata' | 'none';\n\nexport function isStandardPreload(value: unknown): value is StandardPreload {\n return value === 'auto' || value === 'metadata' || value === 'none';\n}\n\n/**\n * Default `preload` value used as the fallback across behaviors\n * (`syncPreload`, `resolvePresentation`, `isBlockingPreload`). Matches the\n * `<video>`/`<audio>` element's implicit default.\n */\nexport const DEFAULT_PRELOAD = 'metadata';\n\n/**\n * True when the preload value blocks initial resolution / loading.\n * Falsy values (undefined, empty) fall back to `defaultPreload` (default\n * `DEFAULT_PRELOAD`); the resolved value blocks iff it is `'none'`.\n */\nexport function isBlockingPreload(\n preload: string | undefined,\n defaultPreload: StandardPreload = DEFAULT_PRELOAD\n): boolean {\n return (preload || defaultPreload) === 'none';\n}\n"],"mappings":";AAOA,SAAgB,kBAAkB,OAA0C;AAC1E,QAAO,UAAU,UAAU,UAAU,cAAc,UAAU;;;;;;;AAQ/D,MAAa,kBAAkB;;;;;;AAO/B,SAAgB,kBACd,SACA,iBAAkC,iBACzB;AACT,SAAQ,WAAW,oBAAoB"}
1
+ {"version":3,"file":"preload.js","names":[],"sources":["../../../../src/media/utils/preload.ts"],"sourcesContent":["/**\n * The W3C-standard set of `<video>`/`<audio>` `preload` attribute values.\n * SPF allows extended values (e.g. `'canplay'`) on `state.preload`, which\n * are *not* reflected to the DOM — predicate below is the discriminator.\n */\nexport type StandardPreload = 'auto' | 'metadata' | 'none';\n\nexport function isStandardPreload(value: unknown): value is StandardPreload {\n return value === 'auto' || value === 'metadata' || value === 'none';\n}\n\n/**\n * Default `preload` value used as the fallback across behaviors\n * (`syncPreload`, `resolvePresentation`, `isBlockingPreload`). Matches the\n * `<video>`/`<audio>` element's implicit default.\n */\nexport const DEFAULT_PRELOAD = 'metadata';\n\n/**\n * True when the preload value blocks initial resolution / loading.\n * Falsy values (undefined, empty) fall back to `defaultPreload` (default\n * `DEFAULT_PRELOAD`); the resolved value blocks iff it is `'none'`.\n */\nexport function isBlockingPreload(\n preload: string | undefined,\n defaultPreload: StandardPreload = DEFAULT_PRELOAD\n): boolean {\n return (preload || defaultPreload) === 'none';\n}\n"],"mappings":";AAOA,SAAgB,kBAAkB,OAA0C;CAC1E,OAAO,UAAU,UAAU,UAAU,cAAc,UAAU;AAC/D;;;;;;AAOA,MAAa,kBAAkB;;;;;;AAO/B,SAAgB,kBACd,SACA,iBAAkC,iBACzB;CACT,QAAQ,WAAW,oBAAoB;AACzC"}
@@ -42,6 +42,6 @@ function getResolvedSelectedTrackDuration(state) {
42
42
  }
43
43
  }
44
44
  //#endregion
45
- export { getResolvedSelectedTrackDuration, getSelectedTrack };
45
+ export { SelectedTrackIdKeyByType, getResolvedSelectedTrackDuration, getSelectedTrack };
46
46
 
47
47
  //# sourceMappingURL=track-selection.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"track-selection.js","names":[],"sources":["../../../../src/media/utils/track-selection.ts"],"sourcesContent":["import type {\n AudioTrack,\n MaybeResolvedPresentation,\n PartiallyResolvedAudioTrack,\n PartiallyResolvedTextTrack,\n PartiallyResolvedVideoTrack,\n TextTrack,\n TrackType,\n VideoTrack,\n} from '../types';\nimport { isResolvedTrack } 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?: MaybeResolvedPresentation;\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 * 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 if (!presentation?.selectionSets) 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\n/**\n * Returns the duration of the first resolved selected track, preferring\n * video over audio. A track is \"resolved\" once its media playlist has been\n * parsed (per {@link isResolvedTrack}). Returns `undefined` if neither\n * selected track is resolved.\n */\nexport function getResolvedSelectedTrackDuration(state: TrackSelectionState): number | undefined {\n if (state.selectedVideoTrackId) {\n const video = getSelectedTrack(state, 'video');\n if (video && isResolvedTrack(video)) return video.duration;\n }\n if (state.selectedAudioTrackId) {\n const audio = getSelectedTrack(state, 'audio');\n if (audio && isResolvedTrack(audio)) return audio.duration;\n }\n return undefined;\n}\n"],"mappings":";;;;;AA0BA,MAAa,2BAA2B;CACtC,OAAO;CACP,OAAO;CACP,MAAM;CACP;;;;;;;;;;;;AAaD,SAAgB,iBACd,OACA,MAOY;CACZ,MAAM,EAAE,iBAAiB;AAEzB,KAAI,CAAC,cAAc,cAAe,QAAO,KAAA;CAIzC,MAAM,UAAU,MADG,yBAAyB;AAE5C,QAAO,aAAa,cACjB,MAAM,EAAE,MAAM,uBAAuB,qBAAqB,KAAK,EAC9D,cAAc,IAAI,OAAO,MAAM,EAAE,SAAS,OAAO,QAAQ;;;;;;;;AAS/D,SAAgB,iCAAiC,OAAgD;AAC/F,KAAI,MAAM,sBAAsB;EAC9B,MAAM,QAAQ,iBAAiB,OAAO,QAAQ;AAC9C,MAAI,SAAS,gBAAgB,MAAM,CAAE,QAAO,MAAM;;AAEpD,KAAI,MAAM,sBAAsB;EAC9B,MAAM,QAAQ,iBAAiB,OAAO,QAAQ;AAC9C,MAAI,SAAS,gBAAgB,MAAM,CAAE,QAAO,MAAM"}
1
+ {"version":3,"file":"track-selection.js","names":[],"sources":["../../../../src/media/utils/track-selection.ts"],"sourcesContent":["import type {\n AudioTrack,\n MaybeResolvedPresentation,\n PartiallyResolvedAudioTrack,\n PartiallyResolvedTextTrack,\n PartiallyResolvedVideoTrack,\n TextTrack,\n TrackType,\n VideoTrack,\n} from '../types';\nimport { isResolvedTrack } 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?: MaybeResolvedPresentation;\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 * 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 if (!presentation?.selectionSets) 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\n/**\n * Returns the duration of the first resolved selected track, preferring\n * video over audio. A track is \"resolved\" once its media playlist has been\n * parsed (per {@link isResolvedTrack}). Returns `undefined` if neither\n * selected track is resolved.\n */\nexport function getResolvedSelectedTrackDuration(state: TrackSelectionState): number | undefined {\n if (state.selectedVideoTrackId) {\n const video = getSelectedTrack(state, 'video');\n if (video && isResolvedTrack(video)) return video.duration;\n }\n if (state.selectedAudioTrackId) {\n const audio = getSelectedTrack(state, 'audio');\n if (audio && isResolvedTrack(audio)) return audio.duration;\n }\n return undefined;\n}\n"],"mappings":";;;;;AA0BA,MAAa,2BAA2B;CACtC,OAAO;CACP,OAAO;CACP,MAAM;AACR;;;;;;;;;;;;AAaA,SAAgB,iBACd,OACA,MAOY;CACZ,MAAM,EAAE,iBAAiB;CAEzB,IAAI,CAAC,cAAc,eAAe,OAAO,KAAA;CAIzC,MAAM,UAAU,MADG,yBAAyB;CAE5C,OAAO,aAAa,cACjB,MAAM,EAAE,MAAM,uBAAuB,qBAAqB,IAAI,CAAC,EAC9D,cAAc,EAAE,EAAE,OAAO,MAAM,EAAE,SAAS,OAAO,OAAO;AAC9D;;;;;;;AAQA,SAAgB,iCAAiC,OAAgD;CAC/F,IAAI,MAAM,sBAAsB;EAC9B,MAAM,QAAQ,iBAAiB,OAAO,OAAO;EAC7C,IAAI,SAAS,gBAAgB,KAAK,GAAG,OAAO,MAAM;CACpD;CACA,IAAI,MAAM,sBAAsB;EAC9B,MAAM,QAAQ,iBAAiB,OAAO,OAAO;EAC7C,IAAI,SAAS,gBAAgB,KAAK,GAAG,OAAO,MAAM;CACpD;AAEF"}