@videojs/spf 10.0.0-beta.25 → 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 (329) hide show
  1. package/README.md +5 -0
  2. package/dist/default/core/actors/create-machine-actor.js.map +1 -1
  3. package/dist/default/core/actors/create-transition-actor.js.map +1 -1
  4. package/dist/default/core/composition/create-composition.js +1 -1
  5. package/dist/default/core/composition/create-composition.js.map +1 -1
  6. package/dist/default/core/composition/share-signals.js.map +1 -1
  7. package/dist/default/core/machine.js.map +1 -1
  8. package/dist/default/core/reactors/create-machine-reactor.js.map +1 -1
  9. package/dist/default/core/signals/effect.js.map +1 -1
  10. package/dist/default/core/signals/primitives.js.map +1 -1
  11. package/dist/default/core/tasks/task.js.map +1 -1
  12. package/dist/default/hls.js +2 -1
  13. package/dist/default/media/abr/quality-selection.js.map +1 -1
  14. package/dist/default/media/buffer/back-buffer.js.map +1 -1
  15. package/dist/default/media/buffer/forward-buffer.js +37 -3
  16. package/dist/default/media/buffer/forward-buffer.js.map +1 -1
  17. package/dist/default/media/dom/capabilities.js.map +1 -1
  18. package/dist/default/media/dom/mse/append-segment.js.map +1 -1
  19. package/dist/default/media/dom/mse/buffer-flusher.js.map +1 -1
  20. package/dist/default/media/dom/mse/duration.js +27 -10
  21. package/dist/default/media/dom/mse/duration.js.map +1 -1
  22. package/dist/default/media/dom/mse/end-of-stream.js.map +1 -1
  23. package/dist/default/media/dom/mse/mediasource-setup.js +68 -17
  24. package/dist/default/media/dom/mse/mediasource-setup.js.map +1 -1
  25. package/dist/default/media/dom/text/resolve-vtt-segment.js +1 -7
  26. package/dist/default/media/dom/text/resolve-vtt-segment.js.map +1 -1
  27. package/dist/default/media/dom/text/text-track-slots.js.map +1 -1
  28. package/dist/default/media/hls/parse-attributes.js +1 -1
  29. package/dist/default/media/hls/parse-attributes.js.map +1 -1
  30. package/dist/default/media/hls/parse-media-playlist.js +8 -3
  31. package/dist/default/media/hls/parse-media-playlist.js.map +1 -1
  32. package/dist/default/media/hls/parse-multivariant.js.map +1 -1
  33. package/dist/default/media/hls/resolve-url.js.map +1 -1
  34. package/dist/default/media/media-tracks/media-tracks.js +101 -0
  35. package/dist/default/media/media-tracks/media-tracks.js.map +1 -0
  36. package/dist/default/media/mp4/box.js +58 -0
  37. package/dist/default/media/mp4/box.js.map +1 -0
  38. package/dist/default/media/mp4/timestamp-origin.js +107 -0
  39. package/dist/default/media/mp4/timestamp-origin.js.map +1 -0
  40. package/dist/default/media/primitives/select-tracks.js.map +1 -1
  41. package/dist/default/media/text/parse-vtt-timestamp-map.js +42 -0
  42. package/dist/default/media/text/parse-vtt-timestamp-map.js.map +1 -0
  43. package/dist/default/media/text/resolve-vtt-metadata.js +23 -0
  44. package/dist/default/media/text/resolve-vtt-metadata.js.map +1 -0
  45. package/dist/default/media/types/index.js.map +1 -1
  46. package/dist/default/media/utils/cdn.js.map +1 -1
  47. package/dist/default/media/utils/preload.js.map +1 -1
  48. package/dist/default/media/utils/track-selection.js +1 -1
  49. package/dist/default/media/utils/track-selection.js.map +1 -1
  50. package/dist/default/media/utils/tracks.js +3 -3
  51. package/dist/default/media/utils/tracks.js.map +1 -1
  52. package/dist/default/media-tracks.js +2 -0
  53. package/dist/default/network/bandwidth-estimator.js.map +1 -1
  54. package/dist/default/network/chunked-stream-iterable.js.map +1 -1
  55. package/dist/default/network/ewma.js +1 -1
  56. package/dist/default/network/ewma.js.map +1 -1
  57. package/dist/default/network/fetch.js.map +1 -1
  58. package/dist/default/playback/actors/dom/segment-loader.js +61 -95
  59. package/dist/default/playback/actors/dom/segment-loader.js.map +1 -1
  60. package/dist/default/playback/actors/dom/source-buffer.js +1 -0
  61. package/dist/default/playback/actors/dom/source-buffer.js.map +1 -1
  62. package/dist/default/playback/actors/dom/text-tracks.js.map +1 -1
  63. package/dist/default/playback/actors/text-track-segment-loader.js +30 -16
  64. package/dist/default/playback/actors/text-track-segment-loader.js.map +1 -1
  65. package/dist/default/playback/behaviors/calculate-presentation-duration.js.map +1 -1
  66. package/dist/default/playback/behaviors/derive-cdn-priority.js.map +1 -1
  67. package/dist/default/playback/behaviors/dom/airplay.js +211 -0
  68. package/dist/default/playback/behaviors/dom/airplay.js.map +1 -0
  69. package/dist/default/playback/behaviors/dom/apply-start-position.js +93 -0
  70. package/dist/default/playback/behaviors/dom/apply-start-position.js.map +1 -0
  71. package/dist/default/playback/behaviors/dom/end-of-stream.js +12 -1
  72. package/dist/default/playback/behaviors/dom/end-of-stream.js.map +1 -1
  73. package/dist/default/playback/behaviors/dom/load-segments.js +5 -1
  74. package/dist/default/playback/behaviors/dom/load-segments.js.map +1 -1
  75. package/dist/default/playback/behaviors/dom/recover-end-stall.js +77 -0
  76. package/dist/default/playback/behaviors/dom/recover-end-stall.js.map +1 -0
  77. package/dist/default/playback/behaviors/dom/setup-buffer-actors.js +17 -4
  78. package/dist/default/playback/behaviors/dom/setup-buffer-actors.js.map +1 -1
  79. package/dist/default/playback/behaviors/dom/setup-mediasource.js +36 -11
  80. package/dist/default/playback/behaviors/dom/setup-mediasource.js.map +1 -1
  81. package/dist/default/playback/behaviors/dom/setup-text-track-actors.js +11 -25
  82. package/dist/default/playback/behaviors/dom/setup-text-track-actors.js.map +1 -1
  83. package/dist/default/playback/behaviors/dom/sync-text-tracks.js.map +1 -1
  84. package/dist/default/playback/behaviors/dom/track-current-time.js.map +1 -1
  85. package/dist/default/playback/behaviors/dom/track-load-triggers.js +4 -4
  86. package/dist/default/playback/behaviors/dom/track-load-triggers.js.map +1 -1
  87. package/dist/default/playback/behaviors/dom/track-playback-rate.js.map +1 -1
  88. package/dist/default/playback/behaviors/dom/update-mediasource-duration.js +2 -1
  89. package/dist/default/playback/behaviors/dom/update-mediasource-duration.js.map +1 -1
  90. package/dist/default/playback/behaviors/establish-start-media-time.js +123 -0
  91. package/dist/default/playback/behaviors/establish-start-media-time.js.map +1 -0
  92. package/dist/default/playback/behaviors/resolve-presentation.js.map +1 -1
  93. package/dist/default/playback/behaviors/resolve-track.js.map +1 -1
  94. package/dist/default/playback/behaviors/select-tracks.js.map +1 -1
  95. package/dist/default/playback/behaviors/setup-failover-monitor.js +1 -1
  96. package/dist/default/playback/behaviors/setup-failover-monitor.js.map +1 -1
  97. package/dist/default/playback/behaviors/sync-preload.js.map +1 -1
  98. package/dist/default/playback/behaviors/track-switching.js +63 -1
  99. package/dist/default/playback/behaviors/track-switching.js.map +1 -1
  100. package/dist/default/playback/engines/background-video/adapter.js.map +1 -1
  101. package/dist/default/playback/engines/background-video/engine.js.map +1 -1
  102. package/dist/default/playback/engines/hls/adapter-audio-only.js +14 -9
  103. package/dist/default/playback/engines/hls/adapter-audio-only.js.map +1 -1
  104. package/dist/default/playback/engines/hls/adapter.js +13 -11
  105. package/dist/default/playback/engines/hls/adapter.js.map +1 -1
  106. package/dist/default/playback/engines/hls/engine-audio-only.js +16 -2
  107. package/dist/default/playback/engines/hls/engine-audio-only.js.map +1 -1
  108. package/dist/default/playback/engines/hls/engine.js +19 -2
  109. package/dist/default/playback/engines/hls/engine.js.map +1 -1
  110. package/dist/default/playback/primitives/failover-fetch.js.map +1 -1
  111. package/dist/default/playback/primitives/head-peek.js +44 -0
  112. package/dist/default/playback/primitives/head-peek.js.map +1 -0
  113. package/dist/default/playback/primitives/relocation-pipelines.js +203 -0
  114. package/dist/default/playback/primitives/relocation-pipelines.js.map +1 -0
  115. package/dist/default/playback/primitives/segment-load-pipeline.js +96 -0
  116. package/dist/default/playback/primitives/segment-load-pipeline.js.map +1 -0
  117. package/dist/default/playback/primitives/text-segment-load-pipeline.js +37 -0
  118. package/dist/default/playback/primitives/text-segment-load-pipeline.js.map +1 -0
  119. package/dist/default/playback/primitives/track-types.js.map +1 -1
  120. package/dist/dev/core/actors/actor.d.ts +0 -1
  121. package/dist/dev/core/actors/actor.d.ts.map +1 -1
  122. package/dist/dev/core/actors/create-machine-actor.d.ts +9 -6
  123. package/dist/dev/core/actors/create-machine-actor.d.ts.map +1 -1
  124. package/dist/dev/core/actors/create-machine-actor.js.map +1 -1
  125. package/dist/dev/core/actors/create-transition-actor.d.ts +0 -1
  126. package/dist/dev/core/actors/create-transition-actor.d.ts.map +1 -1
  127. package/dist/dev/core/actors/create-transition-actor.js.map +1 -1
  128. package/dist/dev/core/composition/create-composition.d.ts +10 -11
  129. package/dist/dev/core/composition/create-composition.d.ts.map +1 -1
  130. package/dist/dev/core/composition/create-composition.js +1 -1
  131. package/dist/dev/core/composition/create-composition.js.map +1 -1
  132. package/dist/dev/core/composition/share-signals.d.ts +0 -1
  133. package/dist/dev/core/composition/share-signals.d.ts.map +1 -1
  134. package/dist/dev/core/composition/share-signals.js.map +1 -1
  135. package/dist/dev/core/machine.d.ts +1 -0
  136. package/dist/dev/core/machine.d.ts.map +1 -1
  137. package/dist/dev/core/machine.js.map +1 -1
  138. package/dist/dev/core/reactors/create-machine-reactor.d.ts +2 -2
  139. package/dist/dev/core/reactors/create-machine-reactor.d.ts.map +1 -1
  140. package/dist/dev/core/reactors/create-machine-reactor.js.map +1 -1
  141. package/dist/dev/core/signals/effect.d.ts.map +1 -1
  142. package/dist/dev/core/signals/effect.js.map +1 -1
  143. package/dist/dev/core/signals/primitives.d.ts +1 -2
  144. package/dist/dev/core/signals/primitives.d.ts.map +1 -1
  145. package/dist/dev/core/signals/primitives.js.map +1 -1
  146. package/dist/dev/core/tasks/task.d.ts +2 -2
  147. package/dist/dev/core/tasks/task.d.ts.map +1 -1
  148. package/dist/dev/core/tasks/task.js.map +1 -1
  149. package/dist/dev/hls.d.ts +3 -1
  150. package/dist/dev/hls.js +2 -1
  151. package/dist/dev/index.d.ts +0 -1
  152. package/dist/dev/index.d.ts.map +1 -1
  153. package/dist/dev/media/abr/quality-selection.d.ts.map +1 -1
  154. package/dist/dev/media/abr/quality-selection.js.map +1 -1
  155. package/dist/dev/media/buffer/back-buffer.d.ts.map +1 -1
  156. package/dist/dev/media/buffer/back-buffer.js.map +1 -1
  157. package/dist/dev/media/buffer/forward-buffer.d.ts.map +1 -1
  158. package/dist/dev/media/buffer/forward-buffer.js +37 -3
  159. package/dist/dev/media/buffer/forward-buffer.js.map +1 -1
  160. package/dist/dev/media/dom/capabilities.js.map +1 -1
  161. package/dist/dev/media/dom/mse/append-segment.d.ts +3 -9
  162. package/dist/dev/media/dom/mse/append-segment.d.ts.map +1 -1
  163. package/dist/dev/media/dom/mse/append-segment.js.map +1 -1
  164. package/dist/dev/media/dom/mse/buffer-flusher.d.ts.map +1 -1
  165. package/dist/dev/media/dom/mse/buffer-flusher.js.map +1 -1
  166. package/dist/dev/media/dom/mse/duration.js +27 -10
  167. package/dist/dev/media/dom/mse/duration.js.map +1 -1
  168. package/dist/dev/media/dom/mse/end-of-stream.js.map +1 -1
  169. package/dist/dev/media/dom/mse/mediasource-setup.js +68 -17
  170. package/dist/dev/media/dom/mse/mediasource-setup.js.map +1 -1
  171. package/dist/dev/media/dom/text/resolve-vtt-segment.d.ts +0 -6
  172. package/dist/dev/media/dom/text/resolve-vtt-segment.d.ts.map +1 -1
  173. package/dist/dev/media/dom/text/resolve-vtt-segment.js +1 -7
  174. package/dist/dev/media/dom/text/resolve-vtt-segment.js.map +1 -1
  175. package/dist/dev/media/dom/text/text-track-slots.d.ts +0 -1
  176. package/dist/dev/media/dom/text/text-track-slots.d.ts.map +1 -1
  177. package/dist/dev/media/dom/text/text-track-slots.js.map +1 -1
  178. package/dist/dev/media/hls/parse-attributes.js +1 -1
  179. package/dist/dev/media/hls/parse-attributes.js.map +1 -1
  180. package/dist/dev/media/hls/parse-media-playlist.js +8 -3
  181. package/dist/dev/media/hls/parse-media-playlist.js.map +1 -1
  182. package/dist/dev/media/hls/parse-multivariant.js.map +1 -1
  183. package/dist/dev/media/hls/resolve-url.js.map +1 -1
  184. package/dist/dev/media/media-tracks/media-tracks.d.ts +51 -0
  185. package/dist/dev/media/media-tracks/media-tracks.d.ts.map +1 -0
  186. package/dist/dev/media/media-tracks/media-tracks.js +101 -0
  187. package/dist/dev/media/media-tracks/media-tracks.js.map +1 -0
  188. package/dist/dev/media/mp4/box.js +58 -0
  189. package/dist/dev/media/mp4/box.js.map +1 -0
  190. package/dist/dev/media/mp4/timestamp-origin.js +107 -0
  191. package/dist/dev/media/mp4/timestamp-origin.js.map +1 -0
  192. package/dist/dev/media/primitives/select-tracks.d.ts +0 -1
  193. package/dist/dev/media/primitives/select-tracks.d.ts.map +1 -1
  194. package/dist/dev/media/primitives/select-tracks.js.map +1 -1
  195. package/dist/dev/media/text/parse-vtt-timestamp-map.js +42 -0
  196. package/dist/dev/media/text/parse-vtt-timestamp-map.js.map +1 -0
  197. package/dist/dev/media/text/resolve-vtt-metadata.js +23 -0
  198. package/dist/dev/media/text/resolve-vtt-metadata.js.map +1 -0
  199. package/dist/dev/media/types/index.d.ts +44 -1
  200. package/dist/dev/media/types/index.d.ts.map +1 -1
  201. package/dist/dev/media/types/index.js.map +1 -1
  202. package/dist/dev/media/utils/cdn.d.ts.map +1 -1
  203. package/dist/dev/media/utils/cdn.js.map +1 -1
  204. package/dist/dev/media/utils/preload.js.map +1 -1
  205. package/dist/dev/media/utils/track-selection.js +1 -1
  206. package/dist/dev/media/utils/track-selection.js.map +1 -1
  207. package/dist/dev/media/utils/tracks.js +3 -3
  208. package/dist/dev/media/utils/tracks.js.map +1 -1
  209. package/dist/dev/media-tracks.d.ts +3 -0
  210. package/dist/dev/media-tracks.js +2 -0
  211. package/dist/dev/network/bandwidth-estimator.d.ts.map +1 -1
  212. package/dist/dev/network/bandwidth-estimator.js.map +1 -1
  213. package/dist/dev/network/chunked-stream-iterable.js.map +1 -1
  214. package/dist/dev/network/ewma.js +1 -1
  215. package/dist/dev/network/ewma.js.map +1 -1
  216. package/dist/dev/network/fetch.js.map +1 -1
  217. package/dist/dev/playback/actors/dom/segment-loader.d.ts +16 -4
  218. package/dist/dev/playback/actors/dom/segment-loader.d.ts.map +1 -1
  219. package/dist/dev/playback/actors/dom/segment-loader.js +61 -95
  220. package/dist/dev/playback/actors/dom/segment-loader.js.map +1 -1
  221. package/dist/dev/playback/actors/dom/source-buffer.d.ts +3 -33
  222. package/dist/dev/playback/actors/dom/source-buffer.d.ts.map +1 -1
  223. package/dist/dev/playback/actors/dom/source-buffer.js +1 -0
  224. package/dist/dev/playback/actors/dom/source-buffer.js.map +1 -1
  225. package/dist/dev/playback/actors/dom/text-tracks.d.ts +3 -1
  226. package/dist/dev/playback/actors/dom/text-tracks.js.map +1 -1
  227. package/dist/dev/playback/actors/text-track-segment-loader.d.ts +7 -12
  228. package/dist/dev/playback/actors/text-track-segment-loader.d.ts.map +1 -1
  229. package/dist/dev/playback/actors/text-track-segment-loader.js +30 -16
  230. package/dist/dev/playback/actors/text-track-segment-loader.js.map +1 -1
  231. package/dist/dev/playback/actors/text-tracks.d.ts +3 -12
  232. package/dist/dev/playback/actors/text-tracks.d.ts.map +1 -1
  233. package/dist/dev/playback/behaviors/calculate-presentation-duration.d.ts +3 -2
  234. package/dist/dev/playback/behaviors/calculate-presentation-duration.d.ts.map +1 -1
  235. package/dist/dev/playback/behaviors/calculate-presentation-duration.js.map +1 -1
  236. package/dist/dev/playback/behaviors/derive-cdn-priority.js.map +1 -1
  237. package/dist/dev/playback/behaviors/dom/airplay.js +211 -0
  238. package/dist/dev/playback/behaviors/dom/airplay.js.map +1 -0
  239. package/dist/dev/playback/behaviors/dom/apply-start-position.js +93 -0
  240. package/dist/dev/playback/behaviors/dom/apply-start-position.js.map +1 -0
  241. package/dist/dev/playback/behaviors/dom/end-of-stream.js +12 -1
  242. package/dist/dev/playback/behaviors/dom/end-of-stream.js.map +1 -1
  243. package/dist/dev/playback/behaviors/dom/load-segments.d.ts +14 -4
  244. package/dist/dev/playback/behaviors/dom/load-segments.d.ts.map +1 -1
  245. package/dist/dev/playback/behaviors/dom/load-segments.js +5 -1
  246. package/dist/dev/playback/behaviors/dom/load-segments.js.map +1 -1
  247. package/dist/dev/playback/behaviors/dom/recover-end-stall.js +77 -0
  248. package/dist/dev/playback/behaviors/dom/recover-end-stall.js.map +1 -0
  249. package/dist/dev/playback/behaviors/dom/setup-buffer-actors.js +17 -4
  250. package/dist/dev/playback/behaviors/dom/setup-buffer-actors.js.map +1 -1
  251. package/dist/dev/playback/behaviors/dom/setup-mediasource.js +36 -11
  252. package/dist/dev/playback/behaviors/dom/setup-mediasource.js.map +1 -1
  253. package/dist/dev/playback/behaviors/dom/setup-text-track-actors.d.ts +16 -19
  254. package/dist/dev/playback/behaviors/dom/setup-text-track-actors.d.ts.map +1 -1
  255. package/dist/dev/playback/behaviors/dom/setup-text-track-actors.js +11 -25
  256. package/dist/dev/playback/behaviors/dom/setup-text-track-actors.js.map +1 -1
  257. package/dist/dev/playback/behaviors/dom/sync-text-tracks.js.map +1 -1
  258. package/dist/dev/playback/behaviors/dom/track-current-time.d.ts +1 -2
  259. package/dist/dev/playback/behaviors/dom/track-current-time.d.ts.map +1 -1
  260. package/dist/dev/playback/behaviors/dom/track-current-time.js.map +1 -1
  261. package/dist/dev/playback/behaviors/dom/track-load-triggers.d.ts +1 -2
  262. package/dist/dev/playback/behaviors/dom/track-load-triggers.d.ts.map +1 -1
  263. package/dist/dev/playback/behaviors/dom/track-load-triggers.js +4 -4
  264. package/dist/dev/playback/behaviors/dom/track-load-triggers.js.map +1 -1
  265. package/dist/dev/playback/behaviors/dom/track-playback-rate.d.ts +1 -2
  266. package/dist/dev/playback/behaviors/dom/track-playback-rate.d.ts.map +1 -1
  267. package/dist/dev/playback/behaviors/dom/track-playback-rate.js.map +1 -1
  268. package/dist/dev/playback/behaviors/dom/update-mediasource-duration.js +2 -1
  269. package/dist/dev/playback/behaviors/dom/update-mediasource-duration.js.map +1 -1
  270. package/dist/dev/playback/behaviors/establish-start-media-time.d.ts +31 -0
  271. package/dist/dev/playback/behaviors/establish-start-media-time.d.ts.map +1 -0
  272. package/dist/dev/playback/behaviors/establish-start-media-time.js +123 -0
  273. package/dist/dev/playback/behaviors/establish-start-media-time.js.map +1 -0
  274. package/dist/dev/playback/behaviors/resolve-presentation.d.ts +2 -1
  275. package/dist/dev/playback/behaviors/resolve-presentation.d.ts.map +1 -1
  276. package/dist/dev/playback/behaviors/resolve-presentation.js.map +1 -1
  277. package/dist/dev/playback/behaviors/resolve-track.js.map +1 -1
  278. package/dist/dev/playback/behaviors/select-tracks.d.ts +2 -1
  279. package/dist/dev/playback/behaviors/select-tracks.d.ts.map +1 -1
  280. package/dist/dev/playback/behaviors/select-tracks.js.map +1 -1
  281. package/dist/dev/playback/behaviors/setup-failover-monitor.d.ts +2 -0
  282. package/dist/dev/playback/behaviors/setup-failover-monitor.d.ts.map +1 -1
  283. package/dist/dev/playback/behaviors/setup-failover-monitor.js +1 -1
  284. package/dist/dev/playback/behaviors/setup-failover-monitor.js.map +1 -1
  285. package/dist/dev/playback/behaviors/sync-preload.js.map +1 -1
  286. package/dist/dev/playback/behaviors/track-switching.js +63 -1
  287. package/dist/dev/playback/behaviors/track-switching.js.map +1 -1
  288. package/dist/dev/playback/engines/background-video/adapter.d.ts +0 -1
  289. package/dist/dev/playback/engines/background-video/adapter.d.ts.map +1 -1
  290. package/dist/dev/playback/engines/background-video/adapter.js.map +1 -1
  291. package/dist/dev/playback/engines/background-video/engine.d.ts +1 -2
  292. package/dist/dev/playback/engines/background-video/engine.d.ts.map +1 -1
  293. package/dist/dev/playback/engines/background-video/engine.js.map +1 -1
  294. package/dist/dev/playback/engines/hls/adapter-audio-only.d.ts +4 -3
  295. package/dist/dev/playback/engines/hls/adapter-audio-only.d.ts.map +1 -1
  296. package/dist/dev/playback/engines/hls/adapter-audio-only.js +14 -9
  297. package/dist/dev/playback/engines/hls/adapter-audio-only.js.map +1 -1
  298. package/dist/dev/playback/engines/hls/adapter.d.ts +3 -5
  299. package/dist/dev/playback/engines/hls/adapter.d.ts.map +1 -1
  300. package/dist/dev/playback/engines/hls/adapter.js +13 -11
  301. package/dist/dev/playback/engines/hls/adapter.js.map +1 -1
  302. package/dist/dev/playback/engines/hls/engine-audio-only.d.ts +31 -2
  303. package/dist/dev/playback/engines/hls/engine-audio-only.d.ts.map +1 -1
  304. package/dist/dev/playback/engines/hls/engine-audio-only.js +16 -2
  305. package/dist/dev/playback/engines/hls/engine-audio-only.js.map +1 -1
  306. package/dist/dev/playback/engines/hls/engine.d.ts +49 -3
  307. package/dist/dev/playback/engines/hls/engine.d.ts.map +1 -1
  308. package/dist/dev/playback/engines/hls/engine.js +19 -2
  309. package/dist/dev/playback/engines/hls/engine.js.map +1 -1
  310. package/dist/dev/playback/primitives/derive-start-media-time.d.ts +16 -0
  311. package/dist/dev/playback/primitives/derive-start-media-time.d.ts.map +1 -0
  312. package/dist/dev/playback/primitives/failover-fetch.js.map +1 -1
  313. package/dist/dev/playback/primitives/head-peek.js +44 -0
  314. package/dist/dev/playback/primitives/head-peek.js.map +1 -0
  315. package/dist/dev/playback/primitives/relocation-pipelines.js +203 -0
  316. package/dist/dev/playback/primitives/relocation-pipelines.js.map +1 -0
  317. package/dist/dev/playback/primitives/segment-load-pipeline.d.ts +2 -0
  318. package/dist/dev/playback/primitives/segment-load-pipeline.js +96 -0
  319. package/dist/dev/playback/primitives/segment-load-pipeline.js.map +1 -0
  320. package/dist/dev/playback/primitives/source-buffer-messages.d.ts +43 -0
  321. package/dist/dev/playback/primitives/source-buffer-messages.d.ts.map +1 -0
  322. package/dist/dev/playback/primitives/text-segment-load-pipeline.d.ts +65 -0
  323. package/dist/dev/playback/primitives/text-segment-load-pipeline.d.ts.map +1 -0
  324. package/dist/dev/playback/primitives/text-segment-load-pipeline.js +37 -0
  325. package/dist/dev/playback/primitives/text-segment-load-pipeline.js.map +1 -0
  326. package/dist/dev/playback/primitives/text-track-messages.d.ts +14 -0
  327. package/dist/dev/playback/primitives/text-track-messages.d.ts.map +1 -0
  328. package/dist/dev/playback/primitives/track-types.js.map +1 -1
  329. package/package.json +12 -7
@@ -1 +1 @@
1
- {"version":3,"file":"parse-multivariant.js","names":[],"sources":["../../../../src/media/hls/parse-multivariant.ts"],"sourcesContent":["import { generateId } from '@videojs/utils/string';\nimport type {\n AddressableObject,\n AudioSelectionSet,\n AudioSwitchingSet,\n FrameRate,\n PartiallyResolvedAudioTrack,\n PartiallyResolvedTextTrack,\n PartiallyResolvedVideoTrack,\n Presentation,\n SelectionSet,\n TextSelectionSet,\n TextSwitchingSet,\n VideoSelectionSet,\n VideoSwitchingSet,\n} from '../types';\nimport { matchTag, parseCodecs } from './parse-attributes';\nimport { resolveUrl } from './resolve-url';\n\n/**\n * Parse HLS multivariant playlist into a Presentation.\n *\n * Returns Presentation with partially resolved tracks (no segment information).\n * Tracks contain metadata from multivariant playlist (bandwidth, resolution, codecs)\n * but segment information is added when media playlists are fetched.\n *\n * @param text - Raw playlist text content\n * @param unresolved - Unresolved presentation (contains URL for base URL resolution)\n * @returns Presentation with partially resolved tracks (duration is undefined)\n */\nexport function parseMultivariantPlaylist(text: string, unresolved: AddressableObject): Presentation {\n const baseUrl = unresolved.url;\n const lines = text.split(/\\r?\\n/);\n\n // Intermediate parsing structures\n interface StreamInfo {\n uri: string;\n bandwidth: number;\n resolution?: { width: number; height: number } | undefined;\n codecs?: string | undefined;\n frameRate?: FrameRate | undefined;\n audioGroupId?: string | undefined;\n }\n\n interface AudioRenditionInfo {\n groupId: string;\n name: string;\n language?: string | undefined;\n uri?: string | undefined;\n default?: boolean | undefined;\n autoselect?: boolean | undefined;\n channels?: number | undefined;\n }\n\n interface SubtitleRenditionInfo {\n groupId: string;\n name: string;\n language?: string | undefined;\n uri: string;\n default?: boolean | undefined;\n autoselect?: boolean | undefined;\n forced?: boolean | undefined;\n }\n\n const streams: StreamInfo[] = [];\n const audioRenditions: AudioRenditionInfo[] = [];\n const subtitleRenditions: SubtitleRenditionInfo[] = [];\n\n // State for STREAM-INF parsing (URI follows on next line)\n let pendingStreamInfo: Omit<StreamInfo, 'uri'> | null = null;\n\n for (const line of lines) {\n const trimmed = line.trim();\n\n // Skip empty lines and comments\n if (!trimmed || (trimmed.startsWith('#') && !trimmed.startsWith('#EXT'))) {\n continue;\n }\n\n // Skip tags not used in Presentation model\n if (\n trimmed === '#EXTM3U' ||\n trimmed.startsWith('#EXT-X-VERSION:') ||\n trimmed.startsWith('#EXT-X-INDEPENDENT-SEGMENTS')\n ) {\n continue;\n }\n\n // #EXT-X-MEDIA:TYPE=AUDIO/SUBTITLES\n const mediaAttrs = matchTag(trimmed, 'EXT-X-MEDIA');\n if (mediaAttrs) {\n const type = mediaAttrs.get('TYPE');\n const groupId = mediaAttrs.get('GROUP-ID');\n const name = mediaAttrs.get('NAME');\n\n if (type === 'AUDIO' && groupId && name) {\n const uri = mediaAttrs.get('URI');\n audioRenditions.push({\n groupId,\n name,\n language: mediaAttrs.get('LANGUAGE'),\n uri: uri ? resolveUrl(uri, baseUrl) : undefined,\n default: mediaAttrs.getBool('DEFAULT'),\n autoselect: mediaAttrs.getBool('AUTOSELECT'),\n // CHANNELS is a quoted string whose first parameter is the channel\n // count (\"6\", or \"16/JOC\" for spatial audio); getInt reads the\n // leading integer.\n channels: mediaAttrs.getInt('CHANNELS'),\n });\n }\n\n if (type === 'SUBTITLES' && groupId && name) {\n const uri = mediaAttrs.get('URI');\n // URI is required for subtitle tracks\n if (uri) {\n subtitleRenditions.push({\n groupId,\n name,\n language: mediaAttrs.get('LANGUAGE'),\n uri: resolveUrl(uri, baseUrl),\n default: mediaAttrs.getBool('DEFAULT'),\n autoselect: mediaAttrs.getBool('AUTOSELECT'),\n forced: mediaAttrs.getBool('FORCED'),\n });\n }\n }\n continue;\n }\n\n // #EXT-X-STREAM-INF:BANDWIDTH=...\n const streamInfAttrs = matchTag(trimmed, 'EXT-X-STREAM-INF');\n if (streamInfAttrs) {\n pendingStreamInfo = {\n bandwidth: streamInfAttrs.getInt('BANDWIDTH', 0)!,\n resolution: streamInfAttrs.getResolution('RESOLUTION'),\n codecs: streamInfAttrs.get('CODECS'),\n frameRate: streamInfAttrs.getFrameRate('FRAME-RATE'),\n audioGroupId: streamInfAttrs.get('AUDIO'),\n };\n continue;\n }\n\n // URI line following STREAM-INF\n if (!trimmed.startsWith('#') && pendingStreamInfo) {\n streams.push({\n ...pendingStreamInfo,\n uri: resolveUrl(trimmed, baseUrl),\n });\n pendingStreamInfo = null;\n }\n }\n\n // Separate streams into video and audio based on codecs\n // If CODECS has single codec, use parseCodecs to determine type\n const videoStreams: typeof streams = [];\n const audioOnlyStreams: typeof streams = [];\n\n for (const stream of streams) {\n if (!stream.codecs) {\n // No codecs - assume video (default behavior)\n videoStreams.push(stream);\n continue;\n }\n\n const parsedCodecs = parseCodecs(stream.codecs);\n const codecCount = stream.codecs.split(',').length;\n\n // Single codec - determine type from parseCodecs result\n if (codecCount === 1) {\n if (parsedCodecs.audio && !parsedCodecs.video) {\n // Audio-only stream\n audioOnlyStreams.push(stream);\n } else {\n // Video stream (or unknown - default to video)\n videoStreams.push(stream);\n }\n } else {\n // Multiple codecs - video stream with muxed audio\n videoStreams.push(stream);\n }\n }\n\n // Build PartiallyResolvedVideoTracks from video streams, de-duplicating the\n // HLS cross-product: one video rendition is listed across several\n // `EXT-X-STREAM-INF` entries — one per audio group it can pair with, all\n // sharing the same media-playlist URI. Collapse them to one track per URI,\n // accumulating every advertised audio group. (Redundant-stream renditions\n // live at *distinct* per-CDN URIs, so they stay separate — only the same-URI\n // cross-product merges.)\n const videoTracksByUrl = new Map<string, PartiallyResolvedVideoTrack>();\n for (const stream of videoStreams) {\n const existing = videoTracksByUrl.get(stream.uri);\n if (existing) {\n if (stream.audioGroupId && !existing.audioGroupIds?.includes(stream.audioGroupId)) {\n existing.audioGroupIds = [...(existing.audioGroupIds ?? []), stream.audioGroupId];\n }\n // BANDWIDTH is video + audio combined; the duplicates differ only in the\n // paired audio. Keep the lowest as the closest proxy to video-only, which\n // is what ABR should rank on.\n if (stream.bandwidth < existing.bandwidth) {\n existing.bandwidth = stream.bandwidth;\n }\n continue;\n }\n\n const codecs = stream.codecs ? parseCodecs(stream.codecs) : undefined;\n\n const track: PartiallyResolvedVideoTrack = {\n type: 'video' as const,\n id: generateId(),\n url: stream.uri,\n bandwidth: stream.bandwidth,\n // Type-specific defaults (CMAF video)\n mimeType: 'video/mp4',\n codecs: [],\n };\n\n if (stream.resolution?.width !== undefined) {\n track.width = stream.resolution.width;\n }\n if (stream.resolution?.height !== undefined) {\n track.height = stream.resolution.height;\n }\n if (codecs?.video) {\n track.codecs = [codecs.video];\n }\n if (stream.frameRate) {\n track.frameRate = stream.frameRate;\n }\n if (stream.audioGroupId) {\n track.audioGroupIds = [stream.audioGroupId];\n }\n\n videoTracksByUrl.set(stream.uri, track);\n }\n const videoTracks: PartiallyResolvedVideoTrack[] = [...videoTracksByUrl.values()];\n\n // Build PartiallyResolvedAudioTracks from audio-only streams\n const audioOnlyTracks: PartiallyResolvedAudioTrack[] = audioOnlyStreams.map((stream) => {\n const codecs = stream.codecs ? parseCodecs(stream.codecs) : undefined;\n\n const track: PartiallyResolvedAudioTrack = {\n type: 'audio' as const,\n id: generateId(),\n url: stream.uri,\n bandwidth: stream.bandwidth,\n mimeType: 'audio/mp4',\n codecs: codecs?.audio ? [codecs.audio] : [],\n groupId: stream.audioGroupId || 'default',\n name: 'Default',\n sampleRate: 48000, // Default - will be in media playlist if available\n channels: 2, // Default - will be in media playlist if available\n };\n\n return track;\n });\n\n // Build PartiallyResolvedAudioTracks from audio renditions (EXT-X-MEDIA)\n // Extract audio codecs from referencing streams\n const audioRenditionTracks: PartiallyResolvedAudioTrack[] = audioRenditions.map((rendition) => {\n let audioCodecs: string[] | undefined;\n for (const stream of streams) {\n if (stream.audioGroupId === rendition.groupId && stream.codecs) {\n const codecs = parseCodecs(stream.codecs);\n if (codecs.audio) {\n audioCodecs = [codecs.audio];\n break;\n }\n }\n }\n\n const track: PartiallyResolvedAudioTrack = {\n type: 'audio' as const,\n id: generateId(),\n url: rendition.uri ?? '',\n groupId: rendition.groupId,\n name: rendition.name,\n // Type-specific defaults (CMAF audio)\n mimeType: 'audio/mp4',\n bandwidth: 0, // Not available in multivariant for demuxed audio\n sampleRate: 48000, // CMAF default\n channels: rendition.channels ?? 2, // From EXT-X-MEDIA CHANNELS; stereo default\n codecs: [],\n };\n\n if (rendition.language) {\n track.language = rendition.language;\n }\n if (audioCodecs) {\n track.codecs = audioCodecs;\n }\n if (rendition.default) {\n track.default = rendition.default;\n }\n if (rendition.autoselect) {\n track.autoselect = rendition.autoselect;\n }\n\n return track;\n });\n\n // Combine audio tracks from both EXT-X-MEDIA renditions and audio-only STREAM-INF\n const audioTracks = [...audioRenditionTracks, ...audioOnlyTracks];\n\n // Build PartiallyResolvedTextTracks from subtitle renditions\n const textTracks: PartiallyResolvedTextTrack[] = subtitleRenditions.map((rendition) => {\n const track: PartiallyResolvedTextTrack = {\n type: 'text' as const,\n id: generateId(),\n url: rendition.uri,\n groupId: rendition.groupId,\n label: rendition.name,\n kind: 'subtitles' as const,\n // Type-specific defaults (VTT)\n mimeType: 'text/vtt',\n bandwidth: 0, // Text tracks don't consume bandwidth\n };\n\n if (rendition.language) {\n track.language = rendition.language;\n }\n // Match hls.js/http-streaming: only set default=true when BOTH DEFAULT=YES AND AUTOSELECT=YES\n if (rendition.default && rendition.autoselect) {\n track.default = true;\n }\n if (rendition.autoselect) {\n track.autoselect = rendition.autoselect;\n }\n if (rendition.forced) {\n track.forced = rendition.forced;\n }\n\n return track;\n });\n\n // Build selection sets\n const selectionSets: SelectionSet[] = [];\n\n if (videoTracks.length > 0) {\n const videoSwitchingSet: VideoSwitchingSet = {\n id: generateId(),\n type: 'video',\n tracks: videoTracks,\n };\n\n const videoSelectionSet: VideoSelectionSet = {\n id: generateId(),\n type: 'video',\n switchingSets: [videoSwitchingSet],\n };\n\n selectionSets.push(videoSelectionSet);\n }\n\n if (audioTracks.length > 0) {\n const audioSwitchingSet: AudioSwitchingSet = {\n id: generateId(),\n type: 'audio',\n tracks: audioTracks,\n };\n\n const audioSelectionSet: AudioSelectionSet = {\n id: generateId(),\n type: 'audio',\n switchingSets: [audioSwitchingSet],\n };\n\n selectionSets.push(audioSelectionSet);\n }\n\n if (textTracks.length > 0) {\n const textSwitchingSet: TextSwitchingSet = {\n id: generateId(),\n type: 'text',\n tracks: textTracks,\n };\n\n const textSelectionSet: TextSelectionSet = {\n id: generateId(),\n type: 'text',\n switchingSets: [textSwitchingSet],\n };\n\n selectionSets.push(textSelectionSet);\n }\n\n // Build presentation (duration is undefined until tracks are resolved)\n return {\n id: generateId(),\n url: unresolved.url,\n startTime: 0,\n // duration: undefined, // Won't be known until after at least one media playlist is fetched + parsed\n selectionSets,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;AA8BA,SAAgB,0BAA0B,MAAc,YAA6C;CACnG,MAAM,UAAU,WAAW;CAC3B,MAAM,QAAQ,KAAK,MAAM,QAAQ;CAgCjC,MAAM,UAAwB,EAAE;CAChC,MAAM,kBAAwC,EAAE;CAChD,MAAM,qBAA8C,EAAE;CAGtD,IAAI,oBAAoD;AAExD,MAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,UAAU,KAAK,MAAM;AAG3B,MAAI,CAAC,WAAY,QAAQ,WAAW,IAAI,IAAI,CAAC,QAAQ,WAAW,OAAO,CACrE;AAIF,MACE,YAAY,aACZ,QAAQ,WAAW,kBAAkB,IACrC,QAAQ,WAAW,8BAA8B,CAEjD;EAIF,MAAM,aAAa,SAAS,SAAS,cAAc;AACnD,MAAI,YAAY;GACd,MAAM,OAAO,WAAW,IAAI,OAAO;GACnC,MAAM,UAAU,WAAW,IAAI,WAAW;GAC1C,MAAM,OAAO,WAAW,IAAI,OAAO;AAEnC,OAAI,SAAS,WAAW,WAAW,MAAM;IACvC,MAAM,MAAM,WAAW,IAAI,MAAM;AACjC,oBAAgB,KAAK;KACnB;KACA;KACA,UAAU,WAAW,IAAI,WAAW;KACpC,KAAK,MAAM,WAAW,KAAK,QAAQ,GAAG,KAAA;KACtC,SAAS,WAAW,QAAQ,UAAU;KACtC,YAAY,WAAW,QAAQ,aAAa;KAI5C,UAAU,WAAW,OAAO,WAAW;KACxC,CAAC;;AAGJ,OAAI,SAAS,eAAe,WAAW,MAAM;IAC3C,MAAM,MAAM,WAAW,IAAI,MAAM;AAEjC,QAAI,IACF,oBAAmB,KAAK;KACtB;KACA;KACA,UAAU,WAAW,IAAI,WAAW;KACpC,KAAK,WAAW,KAAK,QAAQ;KAC7B,SAAS,WAAW,QAAQ,UAAU;KACtC,YAAY,WAAW,QAAQ,aAAa;KAC5C,QAAQ,WAAW,QAAQ,SAAS;KACrC,CAAC;;AAGN;;EAIF,MAAM,iBAAiB,SAAS,SAAS,mBAAmB;AAC5D,MAAI,gBAAgB;AAClB,uBAAoB;IAClB,WAAW,eAAe,OAAO,aAAa,EAAE;IAChD,YAAY,eAAe,cAAc,aAAa;IACtD,QAAQ,eAAe,IAAI,SAAS;IACpC,WAAW,eAAe,aAAa,aAAa;IACpD,cAAc,eAAe,IAAI,QAAQ;IAC1C;AACD;;AAIF,MAAI,CAAC,QAAQ,WAAW,IAAI,IAAI,mBAAmB;AACjD,WAAQ,KAAK;IACX,GAAG;IACH,KAAK,WAAW,SAAS,QAAQ;IAClC,CAAC;AACF,uBAAoB;;;CAMxB,MAAM,eAA+B,EAAE;CACvC,MAAM,mBAAmC,EAAE;AAE3C,MAAK,MAAM,UAAU,SAAS;AAC5B,MAAI,CAAC,OAAO,QAAQ;AAElB,gBAAa,KAAK,OAAO;AACzB;;EAGF,MAAM,eAAe,YAAY,OAAO,OAAO;AAI/C,MAHmB,OAAO,OAAO,MAAM,IAAI,CAAC,WAGzB,EACjB,KAAI,aAAa,SAAS,CAAC,aAAa,MAEtC,kBAAiB,KAAK,OAAO;MAG7B,cAAa,KAAK,OAAO;MAI3B,cAAa,KAAK,OAAO;;CAW7B,MAAM,mCAAmB,IAAI,KAA0C;AACvE,MAAK,MAAM,UAAU,cAAc;EACjC,MAAM,WAAW,iBAAiB,IAAI,OAAO,IAAI;AACjD,MAAI,UAAU;AACZ,OAAI,OAAO,gBAAgB,CAAC,SAAS,eAAe,SAAS,OAAO,aAAa,CAC/E,UAAS,gBAAgB,CAAC,GAAI,SAAS,iBAAiB,EAAE,EAAG,OAAO,aAAa;AAKnF,OAAI,OAAO,YAAY,SAAS,UAC9B,UAAS,YAAY,OAAO;AAE9B;;EAGF,MAAM,SAAS,OAAO,SAAS,YAAY,OAAO,OAAO,GAAG,KAAA;EAE5D,MAAM,QAAqC;GACzC,MAAM;GACN,IAAI,YAAY;GAChB,KAAK,OAAO;GACZ,WAAW,OAAO;GAElB,UAAU;GACV,QAAQ,EAAE;GACX;AAED,MAAI,OAAO,YAAY,UAAU,KAAA,EAC/B,OAAM,QAAQ,OAAO,WAAW;AAElC,MAAI,OAAO,YAAY,WAAW,KAAA,EAChC,OAAM,SAAS,OAAO,WAAW;AAEnC,MAAI,QAAQ,MACV,OAAM,SAAS,CAAC,OAAO,MAAM;AAE/B,MAAI,OAAO,UACT,OAAM,YAAY,OAAO;AAE3B,MAAI,OAAO,aACT,OAAM,gBAAgB,CAAC,OAAO,aAAa;AAG7C,mBAAiB,IAAI,OAAO,KAAK,MAAM;;CAEzC,MAAM,cAA6C,CAAC,GAAG,iBAAiB,QAAQ,CAAC;CAGjF,MAAM,kBAAiD,iBAAiB,KAAK,WAAW;EACtF,MAAM,SAAS,OAAO,SAAS,YAAY,OAAO,OAAO,GAAG,KAAA;AAe5D,SAb2C;GACzC,MAAM;GACN,IAAI,YAAY;GAChB,KAAK,OAAO;GACZ,WAAW,OAAO;GAClB,UAAU;GACV,QAAQ,QAAQ,QAAQ,CAAC,OAAO,MAAM,GAAG,EAAE;GAC3C,SAAS,OAAO,gBAAgB;GAChC,MAAM;GACN,YAAY;GACZ,UAAU;GACX;GAGD;CA+CF,MAAM,cAAc,CAAC,GA3CuC,gBAAgB,KAAK,cAAc;EAC7F,IAAI;AACJ,OAAK,MAAM,UAAU,QACnB,KAAI,OAAO,iBAAiB,UAAU,WAAW,OAAO,QAAQ;GAC9D,MAAM,SAAS,YAAY,OAAO,OAAO;AACzC,OAAI,OAAO,OAAO;AAChB,kBAAc,CAAC,OAAO,MAAM;AAC5B;;;EAKN,MAAM,QAAqC;GACzC,MAAM;GACN,IAAI,YAAY;GAChB,KAAK,UAAU,OAAO;GACtB,SAAS,UAAU;GACnB,MAAM,UAAU;GAEhB,UAAU;GACV,WAAW;GACX,YAAY;GACZ,UAAU,UAAU,YAAY;GAChC,QAAQ,EAAE;GACX;AAED,MAAI,UAAU,SACZ,OAAM,WAAW,UAAU;AAE7B,MAAI,YACF,OAAM,SAAS;AAEjB,MAAI,UAAU,QACZ,OAAM,UAAU,UAAU;AAE5B,MAAI,UAAU,WACZ,OAAM,aAAa,UAAU;AAG/B,SAAO;GACP,EAG4C,GAAG,gBAAgB;CAGjE,MAAM,aAA2C,mBAAmB,KAAK,cAAc;EACrF,MAAM,QAAoC;GACxC,MAAM;GACN,IAAI,YAAY;GAChB,KAAK,UAAU;GACf,SAAS,UAAU;GACnB,OAAO,UAAU;GACjB,MAAM;GAEN,UAAU;GACV,WAAW;GACZ;AAED,MAAI,UAAU,SACZ,OAAM,WAAW,UAAU;AAG7B,MAAI,UAAU,WAAW,UAAU,WACjC,OAAM,UAAU;AAElB,MAAI,UAAU,WACZ,OAAM,aAAa,UAAU;AAE/B,MAAI,UAAU,OACZ,OAAM,SAAS,UAAU;AAG3B,SAAO;GACP;CAGF,MAAM,gBAAgC,EAAE;AAExC,KAAI,YAAY,SAAS,GAAG;EAC1B,MAAM,oBAAuC;GAC3C,IAAI,YAAY;GAChB,MAAM;GACN,QAAQ;GACT;EAED,MAAM,oBAAuC;GAC3C,IAAI,YAAY;GAChB,MAAM;GACN,eAAe,CAAC,kBAAkB;GACnC;AAED,gBAAc,KAAK,kBAAkB;;AAGvC,KAAI,YAAY,SAAS,GAAG;EAC1B,MAAM,oBAAuC;GAC3C,IAAI,YAAY;GAChB,MAAM;GACN,QAAQ;GACT;EAED,MAAM,oBAAuC;GAC3C,IAAI,YAAY;GAChB,MAAM;GACN,eAAe,CAAC,kBAAkB;GACnC;AAED,gBAAc,KAAK,kBAAkB;;AAGvC,KAAI,WAAW,SAAS,GAAG;EACzB,MAAM,mBAAqC;GACzC,IAAI,YAAY;GAChB,MAAM;GACN,QAAQ;GACT;EAED,MAAM,mBAAqC;GACzC,IAAI,YAAY;GAChB,MAAM;GACN,eAAe,CAAC,iBAAiB;GAClC;AAED,gBAAc,KAAK,iBAAiB;;AAItC,QAAO;EACL,IAAI,YAAY;EAChB,KAAK,WAAW;EAChB,WAAW;EAEX;EACD"}
1
+ {"version":3,"file":"parse-multivariant.js","names":[],"sources":["../../../../src/media/hls/parse-multivariant.ts"],"sourcesContent":["import { generateId } from '@videojs/utils/string';\nimport type {\n AddressableObject,\n AudioSelectionSet,\n AudioSwitchingSet,\n FrameRate,\n PartiallyResolvedAudioTrack,\n PartiallyResolvedTextTrack,\n PartiallyResolvedVideoTrack,\n Presentation,\n SelectionSet,\n TextSelectionSet,\n TextSwitchingSet,\n VideoSelectionSet,\n VideoSwitchingSet,\n} from '../types';\nimport { matchTag, parseCodecs } from './parse-attributes';\nimport { resolveUrl } from './resolve-url';\n\n/**\n * Parse HLS multivariant playlist into a Presentation.\n *\n * Returns Presentation with partially resolved tracks (no segment information).\n * Tracks contain metadata from multivariant playlist (bandwidth, resolution, codecs)\n * but segment information is added when media playlists are fetched.\n *\n * @param text - Raw playlist text content\n * @param unresolved - Unresolved presentation (contains URL for base URL resolution)\n * @returns Presentation with partially resolved tracks (duration is undefined)\n */\nexport function parseMultivariantPlaylist(text: string, unresolved: AddressableObject): Presentation {\n const baseUrl = unresolved.url;\n const lines = text.split(/\\r?\\n/);\n\n // Intermediate parsing structures\n interface StreamInfo {\n uri: string;\n bandwidth: number;\n resolution?: { width: number; height: number } | undefined;\n codecs?: string | undefined;\n frameRate?: FrameRate | undefined;\n audioGroupId?: string | undefined;\n }\n\n interface AudioRenditionInfo {\n groupId: string;\n name: string;\n language?: string | undefined;\n uri?: string | undefined;\n default?: boolean | undefined;\n autoselect?: boolean | undefined;\n channels?: number | undefined;\n }\n\n interface SubtitleRenditionInfo {\n groupId: string;\n name: string;\n language?: string | undefined;\n uri: string;\n default?: boolean | undefined;\n autoselect?: boolean | undefined;\n forced?: boolean | undefined;\n }\n\n const streams: StreamInfo[] = [];\n const audioRenditions: AudioRenditionInfo[] = [];\n const subtitleRenditions: SubtitleRenditionInfo[] = [];\n\n // State for STREAM-INF parsing (URI follows on next line)\n let pendingStreamInfo: Omit<StreamInfo, 'uri'> | null = null;\n\n for (const line of lines) {\n const trimmed = line.trim();\n\n // Skip empty lines and comments\n if (!trimmed || (trimmed.startsWith('#') && !trimmed.startsWith('#EXT'))) {\n continue;\n }\n\n // Skip tags not used in Presentation model\n if (\n trimmed === '#EXTM3U' ||\n trimmed.startsWith('#EXT-X-VERSION:') ||\n trimmed.startsWith('#EXT-X-INDEPENDENT-SEGMENTS')\n ) {\n continue;\n }\n\n // #EXT-X-MEDIA:TYPE=AUDIO/SUBTITLES\n const mediaAttrs = matchTag(trimmed, 'EXT-X-MEDIA');\n if (mediaAttrs) {\n const type = mediaAttrs.get('TYPE');\n const groupId = mediaAttrs.get('GROUP-ID');\n const name = mediaAttrs.get('NAME');\n\n if (type === 'AUDIO' && groupId && name) {\n const uri = mediaAttrs.get('URI');\n audioRenditions.push({\n groupId,\n name,\n language: mediaAttrs.get('LANGUAGE'),\n uri: uri ? resolveUrl(uri, baseUrl) : undefined,\n default: mediaAttrs.getBool('DEFAULT'),\n autoselect: mediaAttrs.getBool('AUTOSELECT'),\n // CHANNELS is a quoted string whose first parameter is the channel\n // count (\"6\", or \"16/JOC\" for spatial audio); getInt reads the\n // leading integer.\n channels: mediaAttrs.getInt('CHANNELS'),\n });\n }\n\n if (type === 'SUBTITLES' && groupId && name) {\n const uri = mediaAttrs.get('URI');\n // URI is required for subtitle tracks\n if (uri) {\n subtitleRenditions.push({\n groupId,\n name,\n language: mediaAttrs.get('LANGUAGE'),\n uri: resolveUrl(uri, baseUrl),\n default: mediaAttrs.getBool('DEFAULT'),\n autoselect: mediaAttrs.getBool('AUTOSELECT'),\n forced: mediaAttrs.getBool('FORCED'),\n });\n }\n }\n continue;\n }\n\n // #EXT-X-STREAM-INF:BANDWIDTH=...\n const streamInfAttrs = matchTag(trimmed, 'EXT-X-STREAM-INF');\n if (streamInfAttrs) {\n pendingStreamInfo = {\n bandwidth: streamInfAttrs.getInt('BANDWIDTH', 0)!,\n resolution: streamInfAttrs.getResolution('RESOLUTION'),\n codecs: streamInfAttrs.get('CODECS'),\n frameRate: streamInfAttrs.getFrameRate('FRAME-RATE'),\n audioGroupId: streamInfAttrs.get('AUDIO'),\n };\n continue;\n }\n\n // URI line following STREAM-INF\n if (!trimmed.startsWith('#') && pendingStreamInfo) {\n streams.push({\n ...pendingStreamInfo,\n uri: resolveUrl(trimmed, baseUrl),\n });\n pendingStreamInfo = null;\n }\n }\n\n // Separate streams into video and audio based on codecs\n // If CODECS has single codec, use parseCodecs to determine type\n const videoStreams: typeof streams = [];\n const audioOnlyStreams: typeof streams = [];\n\n for (const stream of streams) {\n if (!stream.codecs) {\n // No codecs - assume video (default behavior)\n videoStreams.push(stream);\n continue;\n }\n\n const parsedCodecs = parseCodecs(stream.codecs);\n const codecCount = stream.codecs.split(',').length;\n\n // Single codec - determine type from parseCodecs result\n if (codecCount === 1) {\n if (parsedCodecs.audio && !parsedCodecs.video) {\n // Audio-only stream\n audioOnlyStreams.push(stream);\n } else {\n // Video stream (or unknown - default to video)\n videoStreams.push(stream);\n }\n } else {\n // Multiple codecs - video stream with muxed audio\n videoStreams.push(stream);\n }\n }\n\n // Build PartiallyResolvedVideoTracks from video streams, de-duplicating the\n // HLS cross-product: one video rendition is listed across several\n // `EXT-X-STREAM-INF` entries — one per audio group it can pair with, all\n // sharing the same media-playlist URI. Collapse them to one track per URI,\n // accumulating every advertised audio group. (Redundant-stream renditions\n // live at *distinct* per-CDN URIs, so they stay separate — only the same-URI\n // cross-product merges.)\n const videoTracksByUrl = new Map<string, PartiallyResolvedVideoTrack>();\n for (const stream of videoStreams) {\n const existing = videoTracksByUrl.get(stream.uri);\n if (existing) {\n if (stream.audioGroupId && !existing.audioGroupIds?.includes(stream.audioGroupId)) {\n existing.audioGroupIds = [...(existing.audioGroupIds ?? []), stream.audioGroupId];\n }\n // BANDWIDTH is video + audio combined; the duplicates differ only in the\n // paired audio. Keep the lowest as the closest proxy to video-only, which\n // is what ABR should rank on.\n if (stream.bandwidth < existing.bandwidth) {\n existing.bandwidth = stream.bandwidth;\n }\n continue;\n }\n\n const codecs = stream.codecs ? parseCodecs(stream.codecs) : undefined;\n\n const track: PartiallyResolvedVideoTrack = {\n type: 'video' as const,\n id: generateId(),\n url: stream.uri,\n bandwidth: stream.bandwidth,\n // Type-specific defaults (CMAF video)\n mimeType: 'video/mp4',\n codecs: [],\n };\n\n if (stream.resolution?.width !== undefined) {\n track.width = stream.resolution.width;\n }\n if (stream.resolution?.height !== undefined) {\n track.height = stream.resolution.height;\n }\n if (codecs?.video) {\n track.codecs = [codecs.video];\n }\n if (stream.frameRate) {\n track.frameRate = stream.frameRate;\n }\n if (stream.audioGroupId) {\n track.audioGroupIds = [stream.audioGroupId];\n }\n\n videoTracksByUrl.set(stream.uri, track);\n }\n const videoTracks: PartiallyResolvedVideoTrack[] = [...videoTracksByUrl.values()];\n\n // Build PartiallyResolvedAudioTracks from audio-only streams\n const audioOnlyTracks: PartiallyResolvedAudioTrack[] = audioOnlyStreams.map((stream) => {\n const codecs = stream.codecs ? parseCodecs(stream.codecs) : undefined;\n\n const track: PartiallyResolvedAudioTrack = {\n type: 'audio' as const,\n id: generateId(),\n url: stream.uri,\n bandwidth: stream.bandwidth,\n mimeType: 'audio/mp4',\n codecs: codecs?.audio ? [codecs.audio] : [],\n groupId: stream.audioGroupId || 'default',\n name: 'Default',\n sampleRate: 48000, // Default - will be in media playlist if available\n channels: 2, // Default - will be in media playlist if available\n };\n\n return track;\n });\n\n // Build PartiallyResolvedAudioTracks from audio renditions (EXT-X-MEDIA)\n // Extract audio codecs from referencing streams\n const audioRenditionTracks: PartiallyResolvedAudioTrack[] = audioRenditions.map((rendition) => {\n let audioCodecs: string[] | undefined;\n for (const stream of streams) {\n if (stream.audioGroupId === rendition.groupId && stream.codecs) {\n const codecs = parseCodecs(stream.codecs);\n if (codecs.audio) {\n audioCodecs = [codecs.audio];\n break;\n }\n }\n }\n\n const track: PartiallyResolvedAudioTrack = {\n type: 'audio' as const,\n id: generateId(),\n url: rendition.uri ?? '',\n groupId: rendition.groupId,\n name: rendition.name,\n // Type-specific defaults (CMAF audio)\n mimeType: 'audio/mp4',\n bandwidth: 0, // Not available in multivariant for demuxed audio\n sampleRate: 48000, // CMAF default\n channels: rendition.channels ?? 2, // From EXT-X-MEDIA CHANNELS; stereo default\n codecs: [],\n };\n\n if (rendition.language) {\n track.language = rendition.language;\n }\n if (audioCodecs) {\n track.codecs = audioCodecs;\n }\n if (rendition.default) {\n track.default = rendition.default;\n }\n if (rendition.autoselect) {\n track.autoselect = rendition.autoselect;\n }\n\n return track;\n });\n\n // Combine audio tracks from both EXT-X-MEDIA renditions and audio-only STREAM-INF\n const audioTracks = [...audioRenditionTracks, ...audioOnlyTracks];\n\n // Build PartiallyResolvedTextTracks from subtitle renditions\n const textTracks: PartiallyResolvedTextTrack[] = subtitleRenditions.map((rendition) => {\n const track: PartiallyResolvedTextTrack = {\n type: 'text' as const,\n id: generateId(),\n url: rendition.uri,\n groupId: rendition.groupId,\n label: rendition.name,\n kind: 'subtitles' as const,\n // Type-specific defaults (VTT)\n mimeType: 'text/vtt',\n bandwidth: 0, // Text tracks don't consume bandwidth\n };\n\n if (rendition.language) {\n track.language = rendition.language;\n }\n // Match hls.js/http-streaming: only set default=true when BOTH DEFAULT=YES AND AUTOSELECT=YES\n if (rendition.default && rendition.autoselect) {\n track.default = true;\n }\n if (rendition.autoselect) {\n track.autoselect = rendition.autoselect;\n }\n if (rendition.forced) {\n track.forced = rendition.forced;\n }\n\n return track;\n });\n\n // Build selection sets\n const selectionSets: SelectionSet[] = [];\n\n if (videoTracks.length > 0) {\n const videoSwitchingSet: VideoSwitchingSet = {\n id: generateId(),\n type: 'video',\n tracks: videoTracks,\n };\n\n const videoSelectionSet: VideoSelectionSet = {\n id: generateId(),\n type: 'video',\n switchingSets: [videoSwitchingSet],\n };\n\n selectionSets.push(videoSelectionSet);\n }\n\n if (audioTracks.length > 0) {\n const audioSwitchingSet: AudioSwitchingSet = {\n id: generateId(),\n type: 'audio',\n tracks: audioTracks,\n };\n\n const audioSelectionSet: AudioSelectionSet = {\n id: generateId(),\n type: 'audio',\n switchingSets: [audioSwitchingSet],\n };\n\n selectionSets.push(audioSelectionSet);\n }\n\n if (textTracks.length > 0) {\n const textSwitchingSet: TextSwitchingSet = {\n id: generateId(),\n type: 'text',\n tracks: textTracks,\n };\n\n const textSelectionSet: TextSelectionSet = {\n id: generateId(),\n type: 'text',\n switchingSets: [textSwitchingSet],\n };\n\n selectionSets.push(textSelectionSet);\n }\n\n // Build presentation (duration is undefined until tracks are resolved)\n return {\n id: generateId(),\n url: unresolved.url,\n startTime: 0,\n // duration: undefined, // Won't be known until after at least one media playlist is fetched + parsed\n selectionSets,\n };\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"resolve-url.js","names":[],"sources":["../../../../src/media/hls/resolve-url.ts"],"sourcesContent":["/**\n * Resolve a potentially relative URL against a base URL using native URL API.\n */\nexport function resolveUrl(url: string, baseUrl: string): string {\n return new URL(url, baseUrl).href;\n}\n"],"mappings":";;;;AAGA,SAAgB,WAAW,KAAa,SAAyB;AAC/D,QAAO,IAAI,IAAI,KAAK,QAAQ,CAAC"}
1
+ {"version":3,"file":"resolve-url.js","names":[],"sources":["../../../../src/media/hls/resolve-url.ts"],"sourcesContent":["/**\n * Resolve a potentially relative URL against a base URL using native URL API.\n */\nexport function resolveUrl(url: string, baseUrl: string): string {\n return new URL(url, baseUrl).href;\n}\n"],"mappings":";;;;AAGA,SAAgB,WAAW,KAAa,SAAyB;CAC/D,OAAO,IAAI,IAAI,KAAK,OAAO,CAAC,CAAC;AAC/B"}
@@ -0,0 +1,101 @@
1
+ import { findTrackById, getTracksByType } from "../utils/tracks.js";
2
+ //#region src/media/media-tracks/media-tracks.ts
3
+ /**
4
+ * The distinct video tracks of a presentation, deduped by {@link VideoDedupeKey} (first occurrence wins).
5
+ *
6
+ * Returns `[]` when the presentation is unresolved or has no video tracks.
7
+ */
8
+ function dedupedVideoTracks(presentation) {
9
+ if (!presentation) return [];
10
+ return dedupe({
11
+ tracks: getTracksByType(presentation, "video"),
12
+ keyFn: toUserVideoTrackSelection
13
+ });
14
+ }
15
+ /**
16
+ * The distinct audio tracks of a presentation, deduped by `language` + `name`
17
+ * (first occurrence wins). Returns `[]` when the presentation is unresolved or has no audio tracks.
18
+ */
19
+ function dedupedAudioTracks(presentation) {
20
+ if (!presentation) return [];
21
+ return dedupe({
22
+ tracks: getTracksByType(presentation, "audio"),
23
+ keyFn: toUserAudioTrackSelection
24
+ });
25
+ }
26
+ /**
27
+ * Find a video track by id, searching the same candidate set the engine resolves
28
+ * against ({@link dedupedVideoTracks}'s pre-dedupe source). Returns `undefined`
29
+ * when absent. Maps the engine's resolved `selectedVideoTrackId` back to its
30
+ * properties for `active` reflection — the resolved id may be a per-CDN copy that
31
+ * isn't the representative {@link dedupedVideoTracks} kept.
32
+ */
33
+ function findVideoTrackById(presentation, id) {
34
+ if (!presentation || !id) return void 0;
35
+ const track = findTrackById(presentation, id);
36
+ return track?.type === "video" ? track : void 0;
37
+ }
38
+ /** Audio counterpart of {@link findVideoTrackById}, for `enabled` reflection. */
39
+ function findAudioTrackById(presentation, id) {
40
+ if (!presentation || !id) return void 0;
41
+ const track = findTrackById(presentation, id);
42
+ return track?.type === "audio" ? track : void 0;
43
+ }
44
+ /**
45
+ * Shallow-equal two key objects by their own properties. Both come from the same
46
+ * key builder, so they carry the same keys — a one-directional scan suffices.
47
+ */
48
+ function sameKey(a, b) {
49
+ for (const attr in a) if (a[attr] !== b[attr]) return false;
50
+ return true;
51
+ }
52
+ /**
53
+ * Dedupe tracks by a key function, keeping the first occurrence of each key.
54
+ * Keys are compared field-by-field ({@link sameKey}).
55
+ */
56
+ function dedupe({ tracks, keyFn }) {
57
+ const seen = [];
58
+ const kept = [];
59
+ for (const track of tracks) {
60
+ const key = keyFn(track);
61
+ if (!key || seen.some((other) => sameKey(other, key))) continue;
62
+ seen.push(key);
63
+ kept.push(track);
64
+ }
65
+ return kept;
66
+ }
67
+ /**
68
+ * Build a partial video track that can be used as `userVideoTrackSelection`.
69
+ */
70
+ function toUserVideoTrackSelection(rendition) {
71
+ return rendition ? {
72
+ width: rendition.width,
73
+ height: rendition.height,
74
+ bandwidth: rendition.bandwidth
75
+ } : void 0;
76
+ }
77
+ /**
78
+ * Build a partial audio track that can be used as a `userAudioTrackSelection`.
79
+ */
80
+ function toUserAudioTrackSelection(track) {
81
+ return track ? {
82
+ language: track.language,
83
+ name: track.name
84
+ } : void 0;
85
+ }
86
+ /** Whether two video tracks are the same by dedupe key */
87
+ function isSameVideoTrack(a, b) {
88
+ return !!b && a.width === b.width && a.height === b.height && a.bandwidth === b.bandwidth;
89
+ }
90
+ /** Whether two audio tracks are the same by dedupe key */
91
+ function isSameAudioTrack(a, b) {
92
+ return !!b && (a.language ?? "") === (b.language ?? "") && a.name === b.name;
93
+ }
94
+ /** Collapse a rational frame rate (numerator/denominator) to frames per second. */
95
+ const frameRateToNumber = (frameRate) => {
96
+ return frameRate.frameRateNumerator / (frameRate.frameRateDenominator ?? 1);
97
+ };
98
+ //#endregion
99
+ export { dedupedAudioTracks, dedupedVideoTracks, findAudioTrackById, findVideoTrackById, frameRateToNumber, isSameAudioTrack, isSameVideoTrack, toUserAudioTrackSelection, toUserVideoTrackSelection };
100
+
101
+ //# sourceMappingURL=media-tracks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"media-tracks.js","names":[],"sources":["../../../../src/media/media-tracks/media-tracks.ts"],"sourcesContent":["/**\n * Media-track translation utilities.\n *\n * Pure, DOM-free transforms from SPF's CMAF-HAM track model onto the deduped\n * lists a media-element adapter exposes (video renditions, audio tracks), plus\n * the selection-criteria builders that turn a chosen rendition/track back into a\n * `user*TrackSelection` partial the engine's track-switching reads.\n *\n * These return SPF *model vocabulary* (`bandwidth`, `codecs: string[]`,\n * `frameRate` as a rational `FrameRate`); the consuming adapter owns the\n * mapping (e.g. for DOM `bandwidth` -> `bitrate`, `codecs.join(',')` -> `codec`,\n * `name` -> `label`).\n *\n * Deduplication is by *properties*, never URL, so a multi-CDN source that lists\n * the same rendition on several hosts collapses to one entry: video renditions\n * by `width` + `height` + `bandwidth`, audio tracks by `language` + `name`.\n * The selection builders emit those same properties as the match criteria, so\n * selecting a collapsed entry re-selects, for example, every underlying per-CDN track.\n */\n\nimport type { AudioTrack, FrameRate, MaybeResolvedPresentation, VideoTrack } from '../types';\nimport { findTrackById, getTracksByType } from '../utils/tracks';\n\nexport type { AudioTrack, VideoTrack };\n\n/** Properties that identify a distinct video rendition (multi-CDN copies share them). */\nexport interface VideoDedupeKey {\n width?: VideoTrack['width'];\n height?: VideoTrack['height'];\n bandwidth?: VideoTrack['bandwidth'];\n}\n\n/** Properties that identify a distinct audio track (multi-CDN copies share them). */\nexport interface AudioDedupeKey {\n language?: AudioTrack['language'];\n name?: AudioTrack['name'];\n}\n\n/**\n * The distinct video tracks of a presentation, deduped by {@link VideoDedupeKey} (first occurrence wins).\n *\n * Returns `[]` when the presentation is unresolved or has no video tracks.\n */\nexport function dedupedVideoTracks(presentation: MaybeResolvedPresentation | undefined): VideoTrack[] {\n if (!presentation) return [];\n\n return dedupe({\n tracks: getTracksByType(presentation, 'video') as readonly VideoTrack[],\n keyFn: toUserVideoTrackSelection,\n });\n}\n\n/**\n * The distinct audio tracks of a presentation, deduped by `language` + `name`\n * (first occurrence wins). Returns `[]` when the presentation is unresolved or has no audio tracks.\n */\nexport function dedupedAudioTracks(presentation: MaybeResolvedPresentation | undefined): AudioTrack[] {\n if (!presentation) return [];\n\n return dedupe({\n tracks: getTracksByType(presentation, 'audio') as readonly AudioTrack[],\n keyFn: toUserAudioTrackSelection,\n });\n}\n\n/**\n * Find a video track by id, searching the same candidate set the engine resolves\n * against ({@link dedupedVideoTracks}'s pre-dedupe source). Returns `undefined`\n * when absent. Maps the engine's resolved `selectedVideoTrackId` back to its\n * properties for `active` reflection — the resolved id may be a per-CDN copy that\n * isn't the representative {@link dedupedVideoTracks} kept.\n */\nexport function findVideoTrackById(\n presentation: MaybeResolvedPresentation | undefined,\n id: string | undefined\n): VideoTrack | undefined {\n if (!presentation || !id) return undefined;\n const track = findTrackById(presentation, id);\n return track?.type === 'video' ? (track as VideoTrack) : undefined;\n}\n\n/** Audio counterpart of {@link findVideoTrackById}, for `enabled` reflection. */\nexport function findAudioTrackById(\n presentation: MaybeResolvedPresentation | undefined,\n id: string | undefined\n): AudioTrack | undefined {\n if (!presentation || !id) return undefined;\n const track = findTrackById(presentation, id);\n return track?.type === 'audio' ? (track as AudioTrack) : undefined;\n}\n\n/**\n * Shallow-equal two key objects by their own properties. Both come from the same\n * key builder, so they carry the same keys — a one-directional scan suffices.\n */\nfunction sameKey<K extends object>(a: K, b: K): boolean {\n for (const attr in a) {\n if (a[attr] !== b[attr]) return false;\n }\n return true;\n}\n\n/**\n * Dedupe tracks by a key function, keeping the first occurrence of each key.\n * Keys are compared field-by-field ({@link sameKey}).\n */\nfunction dedupe<T, K extends object>({\n tracks,\n keyFn,\n}: {\n tracks: readonly T[];\n keyFn: (track: T) => K | undefined;\n}): T[] {\n const seen: K[] = [];\n const kept: T[] = [];\n for (const track of tracks) {\n const key = keyFn(track);\n if (!key || seen.some((other) => sameKey(other, key))) continue;\n seen.push(key);\n kept.push(track);\n }\n\n return kept;\n}\n\n/**\n * Build a partial video track that can be used as `userVideoTrackSelection`.\n */\nexport function toUserVideoTrackSelection<T extends VideoDedupeKey>(rendition?: T): Partial<VideoTrack> | undefined {\n return rendition ? { width: rendition.width, height: rendition.height, bandwidth: rendition.bandwidth } : undefined;\n}\n\n/**\n * Build a partial audio track that can be used as a `userAudioTrackSelection`.\n */\nexport function toUserAudioTrackSelection<T extends AudioDedupeKey>(track?: T): Partial<AudioTrack> | undefined {\n return track ? { language: track.language, name: track.name } : undefined;\n}\n\n/** Whether two video tracks are the same by dedupe key */\nexport function isSameVideoTrack(a: VideoDedupeKey, b: VideoDedupeKey | undefined): boolean {\n return !!b && a.width === b.width && a.height === b.height && a.bandwidth === b.bandwidth;\n}\n\n/** Whether two audio tracks are the same by dedupe key */\nexport function isSameAudioTrack(a: AudioDedupeKey, b: AudioDedupeKey | undefined): boolean {\n return !!b && (a.language ?? '') === (b.language ?? '') && a.name === b.name;\n}\n\n/** Collapse a rational frame rate (numerator/denominator) to frames per second. */\nexport const frameRateToNumber = (frameRate: FrameRate) => {\n return frameRate.frameRateNumerator / (frameRate.frameRateDenominator ?? 1);\n};\n"],"mappings":";;;;;;;AA2CA,SAAgB,mBAAmB,cAAmE;CACpG,IAAI,CAAC,cAAc,OAAO,CAAC;CAE3B,OAAO,OAAO;EACZ,QAAQ,gBAAgB,cAAc,OAAO;EAC7C,OAAO;CACT,CAAC;AACH;;;;;AAMA,SAAgB,mBAAmB,cAAmE;CACpG,IAAI,CAAC,cAAc,OAAO,CAAC;CAE3B,OAAO,OAAO;EACZ,QAAQ,gBAAgB,cAAc,OAAO;EAC7C,OAAO;CACT,CAAC;AACH;;;;;;;;AASA,SAAgB,mBACd,cACA,IACwB;CACxB,IAAI,CAAC,gBAAgB,CAAC,IAAI,OAAO,KAAA;CACjC,MAAM,QAAQ,cAAc,cAAc,EAAE;CAC5C,OAAO,OAAO,SAAS,UAAW,QAAuB,KAAA;AAC3D;;AAGA,SAAgB,mBACd,cACA,IACwB;CACxB,IAAI,CAAC,gBAAgB,CAAC,IAAI,OAAO,KAAA;CACjC,MAAM,QAAQ,cAAc,cAAc,EAAE;CAC5C,OAAO,OAAO,SAAS,UAAW,QAAuB,KAAA;AAC3D;;;;;AAMA,SAAS,QAA0B,GAAM,GAAe;CACtD,KAAK,MAAM,QAAQ,GACjB,IAAI,EAAE,UAAU,EAAE,OAAO,OAAO;CAElC,OAAO;AACT;;;;;AAMA,SAAS,OAA4B,EACnC,QACA,SAIM;CACN,MAAM,OAAY,CAAC;CACnB,MAAM,OAAY,CAAC;CACnB,KAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,MAAM,MAAM,KAAK;EACvB,IAAI,CAAC,OAAO,KAAK,MAAM,UAAU,QAAQ,OAAO,GAAG,CAAC,GAAG;EACvD,KAAK,KAAK,GAAG;EACb,KAAK,KAAK,KAAK;CACjB;CAEA,OAAO;AACT;;;;AAKA,SAAgB,0BAAoD,WAAgD;CAClH,OAAO,YAAY;EAAE,OAAO,UAAU;EAAO,QAAQ,UAAU;EAAQ,WAAW,UAAU;CAAU,IAAI,KAAA;AAC5G;;;;AAKA,SAAgB,0BAAoD,OAA4C;CAC9G,OAAO,QAAQ;EAAE,UAAU,MAAM;EAAU,MAAM,MAAM;CAAK,IAAI,KAAA;AAClE;;AAGA,SAAgB,iBAAiB,GAAmB,GAAwC;CAC1F,OAAO,CAAC,CAAC,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,WAAW,EAAE,UAAU,EAAE,cAAc,EAAE;AAClF;;AAGA,SAAgB,iBAAiB,GAAmB,GAAwC;CAC1F,OAAO,CAAC,CAAC,MAAM,EAAE,YAAY,SAAS,EAAE,YAAY,OAAO,EAAE,SAAS,EAAE;AAC1E;;AAGA,MAAa,qBAAqB,cAAyB;CACzD,OAAO,UAAU,sBAAsB,UAAU,wBAAwB;AAC3E"}
@@ -0,0 +1,58 @@
1
+ //#region src/media/mp4/box.ts
2
+ function toDataView(data) {
3
+ return data instanceof Uint8Array ? new DataView(data.buffer, data.byteOffset, data.byteLength) : new DataView(data);
4
+ }
5
+ /** Read a 4-character box type / FourCC at `offset`. */
6
+ function readFourCC(view, offset) {
7
+ return String.fromCharCode(view.getUint8(offset), view.getUint8(offset + 1), view.getUint8(offset + 2), view.getUint8(offset + 3));
8
+ }
9
+ /** Iterate the boxes directly contained in `[start, end)`. */
10
+ function* iterateBoxes(view, start = 0, end = view.byteLength) {
11
+ let offset = start;
12
+ while (offset + 8 <= end) {
13
+ let size = view.getUint32(offset);
14
+ const type = readFourCC(view, offset + 4);
15
+ let dataStart = offset + 8;
16
+ if (size === 1) {
17
+ size = Number(view.getBigUint64(offset + 8));
18
+ dataStart = offset + 16;
19
+ } else if (size === 0) size = end - offset;
20
+ if (size < dataStart - offset) return;
21
+ yield {
22
+ type,
23
+ start: offset,
24
+ dataStart,
25
+ end: offset + size
26
+ };
27
+ offset += size;
28
+ }
29
+ }
30
+ /** Iterate the direct child boxes of a given `type` within `[start, end)`. */
31
+ function* iterateBoxesOfType(view, type, start = 0, end = view.byteLength) {
32
+ for (const box of iterateBoxes(view, start, end)) if (box.type === type) yield box;
33
+ }
34
+ /**
35
+ * Depth-first descent to the first box matching a nested path, e.g.
36
+ * `['moov', 'trak', 'mdia', 'mdhd']`. Returns `undefined` if any level is
37
+ * absent.
38
+ */
39
+ function findBox(view, path, start = 0, end = view.byteLength) {
40
+ const [head, ...rest] = path;
41
+ for (const box of iterateBoxes(view, start, end)) {
42
+ if (box.type !== head) continue;
43
+ if (rest.length === 0) return box;
44
+ const found = findBox(view, rest, box.dataStart, box.end);
45
+ if (found) return found;
46
+ }
47
+ }
48
+ /**
49
+ * Read the version byte of a FullBox — the `version(1) + flags(3)` header at the
50
+ * start of the payload of `mdhd` / `tkhd` / `tfdt` / `hdlr` / `elst` / etc.
51
+ */
52
+ function readFullBoxVersion(view, dataStart) {
53
+ return view.getUint8(dataStart);
54
+ }
55
+ //#endregion
56
+ export { findBox, iterateBoxes, iterateBoxesOfType, readFourCC, readFullBoxVersion, toDataView };
57
+
58
+ //# sourceMappingURL=box.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"box.js","names":[],"sources":["../../../../src/media/mp4/box.ts"],"sourcesContent":["/**\n * Minimal ISO-BMFF (MP4/CMAF) box walker.\n *\n * Just enough to locate boxes by nested path — the framework needs a couple of\n * leaf fields (`mdhd` timescale, `tfdt` baseMediaDecodeTime) to derive a\n * segment's decode-time origin, not a full demuxer. DOM-free: operates on an\n * `ArrayBuffer` / `Uint8Array` via `DataView`.\n *\n * Box layout: `[u32 size][u32 type][payload]`. `size === 1` means a `u64\n * largesize` follows the type (payload after it); `size === 0` means the box\n * runs to the end of its container.\n */\n\n/** A located box: its 4-char type and byte offsets within the buffer. */\nexport interface Box {\n type: string;\n /** Offset of the box's first byte (its size field). */\n start: number;\n /** Offset of the box's payload — after `size` + `type` (+ `largesize`). */\n dataStart: number;\n /** Offset one past the box's last byte. */\n end: number;\n}\n\nexport function toDataView(data: ArrayBuffer | Uint8Array): DataView {\n return data instanceof Uint8Array ? new DataView(data.buffer, data.byteOffset, data.byteLength) : new DataView(data);\n}\n\n/** Read a 4-character box type / FourCC at `offset`. */\nexport function readFourCC(view: DataView, offset: number): string {\n return String.fromCharCode(\n view.getUint8(offset),\n view.getUint8(offset + 1),\n view.getUint8(offset + 2),\n view.getUint8(offset + 3)\n );\n}\n\n/** Iterate the boxes directly contained in `[start, end)`. */\nexport function* iterateBoxes(view: DataView, start = 0, end = view.byteLength): Generator<Box> {\n let offset = start;\n while (offset + 8 <= end) {\n let size = view.getUint32(offset);\n const type = readFourCC(view, offset + 4);\n let dataStart = offset + 8;\n if (size === 1) {\n size = Number(view.getBigUint64(offset + 8));\n dataStart = offset + 16;\n } else if (size === 0) {\n size = end - offset;\n }\n // A size smaller than its own header is malformed — stop rather than loop.\n if (size < dataStart - offset) return;\n yield { type, start: offset, dataStart, end: offset + size };\n offset += size;\n }\n}\n\n/** Iterate the direct child boxes of a given `type` within `[start, end)`. */\nexport function* iterateBoxesOfType(view: DataView, type: string, start = 0, end = view.byteLength): Generator<Box> {\n for (const box of iterateBoxes(view, start, end)) {\n if (box.type === type) yield box;\n }\n}\n\n/**\n * Depth-first descent to the first box matching a nested path, e.g.\n * `['moov', 'trak', 'mdia', 'mdhd']`. Returns `undefined` if any level is\n * absent.\n */\nexport function findBox(view: DataView, path: readonly string[], start = 0, end = view.byteLength): Box | undefined {\n const [head, ...rest] = path;\n for (const box of iterateBoxes(view, start, end)) {\n if (box.type !== head) continue;\n if (rest.length === 0) return box;\n const found = findBox(view, rest, box.dataStart, box.end);\n if (found) return found;\n }\n return undefined;\n}\n\n/**\n * Read the version byte of a FullBox — the `version(1) + flags(3)` header at the\n * start of the payload of `mdhd` / `tkhd` / `tfdt` / `hdlr` / `elst` / etc.\n */\nexport function readFullBoxVersion(view: DataView, dataStart: number): number {\n return view.getUint8(dataStart);\n}\n"],"mappings":";AAwBA,SAAgB,WAAW,MAA0C;CACnE,OAAO,gBAAgB,aAAa,IAAI,SAAS,KAAK,QAAQ,KAAK,YAAY,KAAK,UAAU,IAAI,IAAI,SAAS,IAAI;AACrH;;AAGA,SAAgB,WAAW,MAAgB,QAAwB;CACjE,OAAO,OAAO,aACZ,KAAK,SAAS,MAAM,GACpB,KAAK,SAAS,SAAS,CAAC,GACxB,KAAK,SAAS,SAAS,CAAC,GACxB,KAAK,SAAS,SAAS,CAAC,CAC1B;AACF;;AAGA,UAAiB,aAAa,MAAgB,QAAQ,GAAG,MAAM,KAAK,YAA4B;CAC9F,IAAI,SAAS;CACb,OAAO,SAAS,KAAK,KAAK;EACxB,IAAI,OAAO,KAAK,UAAU,MAAM;EAChC,MAAM,OAAO,WAAW,MAAM,SAAS,CAAC;EACxC,IAAI,YAAY,SAAS;EACzB,IAAI,SAAS,GAAG;GACd,OAAO,OAAO,KAAK,aAAa,SAAS,CAAC,CAAC;GAC3C,YAAY,SAAS;EACvB,OAAO,IAAI,SAAS,GAClB,OAAO,MAAM;EAGf,IAAI,OAAO,YAAY,QAAQ;EAC/B,MAAM;GAAE;GAAM,OAAO;GAAQ;GAAW,KAAK,SAAS;EAAK;EAC3D,UAAU;CACZ;AACF;;AAGA,UAAiB,mBAAmB,MAAgB,MAAc,QAAQ,GAAG,MAAM,KAAK,YAA4B;CAClH,KAAK,MAAM,OAAO,aAAa,MAAM,OAAO,GAAG,GAC7C,IAAI,IAAI,SAAS,MAAM,MAAM;AAEjC;;;;;;AAOA,SAAgB,QAAQ,MAAgB,MAAyB,QAAQ,GAAG,MAAM,KAAK,YAA6B;CAClH,MAAM,CAAC,MAAM,GAAG,QAAQ;CACxB,KAAK,MAAM,OAAO,aAAa,MAAM,OAAO,GAAG,GAAG;EAChD,IAAI,IAAI,SAAS,MAAM;EACvB,IAAI,KAAK,WAAW,GAAG,OAAO;EAC9B,MAAM,QAAQ,QAAQ,MAAM,MAAM,IAAI,WAAW,IAAI,GAAG;EACxD,IAAI,OAAO,OAAO;CACpB;AAEF;;;;;AAMA,SAAgB,mBAAmB,MAAgB,WAA2B;CAC5E,OAAO,KAAK,SAAS,SAAS;AAChC"}
@@ -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 +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 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;AAC5E,MAAK,MAAM,OAAO,QAAQ;EACxB,MAAM,cAAc,OAAO;AAC3B,MAAI,gBAAgB,KAAA,KAAa,MAAM,SAAoB,YAAa,QAAO;;AAEjF,QAAO;;;;;;;;;;;AAYT,SAAgB,iBAAiB,cAAyC,MAAqC;AAC7G,QAAO,aAAa,eAAe,MAAM,QAAQ,IAAI,SAAS,KAAK,EAAE,cAAc,IAAI,OAAO,IAAI;;;;;;;;;;;;;;;;AAoDpG,SAAgB,yBAAyB,OAA4C;AACnF,KAAI,UAAU,KAAA,KAAa,UAAU,KAAM,QAAO,OAAO;AACzD,KAAI,OAAO,UAAU,SAAU,QAAO,OAAO,SAAS,MAAM,IAAI,QAAQ,IAAI,QAAQ,OAAO;CAC3F,MAAM,QAAQ,MAAM,MAAM,CAAC,MAAM,aAAa;AAC9C,KAAI,CAAC,MAAO,QAAO,OAAO;CAC1B,MAAM,SAAS,OAAO,MAAM,GAAG;AAC/B,KAAI,EAAE,OAAO,SAAS,OAAO,IAAI,SAAS,GAAI,QAAO,OAAO;AAC5D,QAAQ,SAAS,SAAS,KAAM;;;;;;;;AAWlC,SAAgB,wBACd,QACA,eAAuB,OAAO,mBACf;AACf,KAAI,OAAO,WAAW,EAAG,QAAO,KAAA;CAIhC,MAAM,SAAS,CAAC,GAAG,OAAO,CAAC,MACxB,GAAG,OACD,EAAE,SAAS,MAAM,EAAE,UAAU,MAAM,EAAE,SAAS,MAAM,EAAE,UAAU,OAAO,EAAE,aAAa,MAAM,EAAE,aAAa,GAC/G;AAED,QAAO,OAAO,MAAM,OAAO,EAAE,SAAS,MAAM,EAAE,UAAU,MAAM,aAAa,IAAI,OAAO,OAAO,SAAS;;;;;;;;AASxG,SAAgB,gCAAgC,cAA6D;CAE3G,MAAM,UADW,aAAa,eAAe,MAAM,QAAQ,IAAI,SAAS,QAAQ,GACvD,cAAc,IAAI;AAC3C,KAAI,CAAC,QAAQ,OAAQ,QAAO,KAAA;AAC5B,QAAO,wBAAwB,OAAO,EAAE;;;;;;;;;;;;;;AAe1C,SAAgB,eACd,cACA,QACoB;CACpB,MAAM,WAAW,aAAa,eAAe,MAAM,QAAQ,IAAI,SAAS,QAAQ;AAEhF,KAAI,CAAC,YAAY,SAAS,cAAc,WAAW,EACjD;CAIF,MAAM,eAAe,SAAS,cAAc;AAC5C,KAAI,CAAC,gBAAgB,aAAa,OAAO,WAAW,EAClD;CAGF,MAAM,SAAS,aAAa;AAG5B,KAAI,QAAQ,wBAAwB;EAClC,MAAM,gBAAgB,OAAO,MAAM,UAAU,MAAM,aAAa,OAAO,uBAAuB;AAC9F,MAAI,cACF,QAAO,cAAc;;CAKzB,MAAM,eAAe,OAAO,MAAM,UAAU,MAAM,YAAY,KAAK;AACnE,KAAI,aACF,QAAO,aAAa;AAItB,QAAO,OAAO,IAAI;;;;;;;;;;;;;;AAqCpB,SAAgB,wBACd,QACA,QACoB;CACpB,MAAM,kBAAkB,QAAQ,sBAAsB,SAAS,OAAO,QAAQ,UAAU,CAAC,MAAM,OAAO;AACtG,KAAI,gBAAgB,WAAW,EAAG,QAAO,KAAA;CAEzC,MAAM,EAAE,2BAA2B,qBAAqB,UAAU,UAAU,EAAE;AAE9E,KAAI,2BAA2B;EAC7B,MAAM,gBAAgB,gBAAgB,MAAM,UAAU,MAAM,aAAa,0BAA0B;AACnG,MAAI,cAAe,QAAO,cAAc;;AAG1C,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"}