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

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 (238) hide show
  1. package/dist/default/core/signals/when.js +38 -0
  2. package/dist/default/core/signals/when.js.map +1 -0
  3. package/dist/default/core/tasks/delayed-reschedule.js +31 -0
  4. package/dist/default/core/tasks/delayed-reschedule.js.map +1 -0
  5. package/dist/default/core/tasks/task.js +133 -3
  6. package/dist/default/core/tasks/task.js.map +1 -1
  7. package/dist/default/hls-audio.js +3 -0
  8. package/dist/default/hls-background-video.js +3 -0
  9. package/dist/default/hls-video.js +4 -0
  10. package/dist/default/hls.js +6 -5
  11. package/dist/default/media/dom/capabilities.js +2 -0
  12. package/dist/default/media/dom/capabilities.js.map +1 -1
  13. package/dist/default/media/errors.js +62 -0
  14. package/dist/default/media/errors.js.map +1 -0
  15. package/dist/default/media/hls/parse-media-playlist.js +167 -16
  16. package/dist/default/media/hls/parse-media-playlist.js.map +1 -1
  17. package/dist/default/media/hls/parse-multivariant.js +14 -3
  18. package/dist/default/media/hls/parse-multivariant.js.map +1 -1
  19. package/dist/default/media/hls/reload-policy.js +71 -0
  20. package/dist/default/media/hls/reload-policy.js.map +1 -0
  21. package/dist/default/media/live-window.js +36 -0
  22. package/dist/default/media/live-window.js.map +1 -0
  23. package/dist/default/media/primitives/select-tracks.js +1 -24
  24. package/dist/default/media/primitives/select-tracks.js.map +1 -1
  25. package/dist/default/media/types/index.js +15 -1
  26. package/dist/default/media/types/index.js.map +1 -1
  27. package/dist/default/media/utils/tracks.js +14 -0
  28. package/dist/default/media/utils/tracks.js.map +1 -1
  29. package/dist/default/mux-audio.js +3 -0
  30. package/dist/default/mux-background-video.js +3 -0
  31. package/dist/default/mux-video.js +3 -0
  32. package/dist/default/playback/adapters/hls-audio/adapter.js +186 -0
  33. package/dist/default/playback/adapters/hls-audio/adapter.js.map +1 -0
  34. package/dist/default/playback/adapters/hls-audio/media.js +9 -0
  35. package/dist/default/playback/adapters/hls-audio/media.js.map +1 -0
  36. package/dist/default/playback/adapters/hls-background-video/adapter.js +123 -0
  37. package/dist/default/playback/adapters/hls-background-video/adapter.js.map +1 -0
  38. package/dist/default/playback/adapters/hls-background-video/host.js +60 -0
  39. package/dist/default/playback/adapters/hls-background-video/host.js.map +1 -0
  40. package/dist/default/playback/adapters/hls-background-video/media.js +21 -0
  41. package/dist/default/playback/adapters/hls-background-video/media.js.map +1 -0
  42. package/dist/default/playback/adapters/hls-video/adapter.js +316 -0
  43. package/dist/default/playback/adapters/hls-video/adapter.js.map +1 -0
  44. package/dist/default/playback/adapters/hls-video/error-surface.js +51 -0
  45. package/dist/default/playback/adapters/hls-video/error-surface.js.map +1 -0
  46. package/dist/default/playback/adapters/hls-video/media-tracks.js +126 -0
  47. package/dist/default/playback/adapters/hls-video/media-tracks.js.map +1 -0
  48. package/dist/default/playback/adapters/hls-video/media.js +11 -0
  49. package/dist/default/playback/adapters/hls-video/media.js.map +1 -0
  50. package/dist/default/playback/adapters/mux-audio/media.js +26 -0
  51. package/dist/default/playback/adapters/mux-audio/media.js.map +1 -0
  52. package/dist/default/playback/adapters/mux-video/adapter.js +92 -0
  53. package/dist/default/playback/adapters/mux-video/adapter.js.map +1 -0
  54. package/dist/default/playback/adapters/mux-video/media.js +23 -0
  55. package/dist/default/playback/adapters/mux-video/media.js.map +1 -0
  56. package/dist/default/playback/behaviors/collect-errors.js +77 -0
  57. package/dist/default/playback/behaviors/collect-errors.js.map +1 -0
  58. package/dist/default/playback/behaviors/dom/end-of-stream.js +1 -0
  59. package/dist/default/playback/behaviors/dom/end-of-stream.js.map +1 -1
  60. package/dist/default/playback/behaviors/dom/seek-to-live-edge.js +132 -0
  61. package/dist/default/playback/behaviors/dom/seek-to-live-edge.js.map +1 -0
  62. package/dist/default/playback/behaviors/dom/sync-live-seekable-range.js +26 -0
  63. package/dist/default/playback/behaviors/dom/sync-live-seekable-range.js.map +1 -0
  64. package/dist/default/playback/behaviors/dom/update-mediasource-duration.js +29 -7
  65. package/dist/default/playback/behaviors/dom/update-mediasource-duration.js.map +1 -1
  66. package/dist/default/playback/behaviors/establish-start-media-time.js +78 -4
  67. package/dist/default/playback/behaviors/establish-start-media-time.js.map +1 -1
  68. package/dist/default/playback/behaviors/resolve-track.js +29 -8
  69. package/dist/default/playback/behaviors/resolve-track.js.map +1 -1
  70. package/dist/default/playback/behaviors/track-switching.js +18 -9
  71. package/dist/default/playback/behaviors/track-switching.js.map +1 -1
  72. package/dist/default/playback/engines/hls/engine-audio-only.js +9 -5
  73. package/dist/default/playback/engines/hls/engine-audio-only.js.map +1 -1
  74. package/dist/{dev/playback/engines/background-video/engine.js → default/playback/engines/hls/engine-background-video.js} +3 -3
  75. package/dist/default/playback/engines/hls/engine-background-video.js.map +1 -0
  76. package/dist/default/playback/engines/hls/engine.js +19 -6
  77. package/dist/default/playback/engines/hls/engine.js.map +1 -1
  78. package/dist/default/playback/primitives/error-messages.js +39 -0
  79. package/dist/default/playback/primitives/error-messages.js.map +1 -0
  80. package/dist/default/playback/primitives/live-window.js +66 -0
  81. package/dist/default/playback/primitives/live-window.js.map +1 -0
  82. package/dist/default/playback/primitives/report-track-conditions.js +86 -0
  83. package/dist/default/playback/primitives/report-track-conditions.js.map +1 -0
  84. package/dist/dev/core/signals/when.js +38 -0
  85. package/dist/dev/core/signals/when.js.map +1 -0
  86. package/dist/dev/core/tasks/delayed-reschedule.js +31 -0
  87. package/dist/dev/core/tasks/delayed-reschedule.js.map +1 -0
  88. package/dist/dev/core/tasks/task.d.ts +52 -1
  89. package/dist/dev/core/tasks/task.d.ts.map +1 -1
  90. package/dist/dev/core/tasks/task.js +133 -3
  91. package/dist/dev/core/tasks/task.js.map +1 -1
  92. package/dist/dev/hls-audio.d.ts +3 -0
  93. package/dist/dev/hls-audio.js +3 -0
  94. package/dist/dev/hls-background-video.d.ts +3 -0
  95. package/dist/dev/hls-background-video.js +3 -0
  96. package/dist/dev/hls-video.d.ts +5 -0
  97. package/dist/dev/hls-video.js +4 -0
  98. package/dist/dev/hls.d.ts +6 -5
  99. package/dist/dev/hls.js +6 -5
  100. package/dist/dev/media/dom/capabilities.js +2 -0
  101. package/dist/dev/media/dom/capabilities.js.map +1 -1
  102. package/dist/dev/media/errors.d.ts +82 -0
  103. package/dist/dev/media/errors.d.ts.map +1 -0
  104. package/dist/dev/media/errors.js +62 -0
  105. package/dist/dev/media/errors.js.map +1 -0
  106. package/dist/dev/media/hls/parse-media-playlist.js +167 -16
  107. package/dist/dev/media/hls/parse-media-playlist.js.map +1 -1
  108. package/dist/dev/media/hls/parse-multivariant.js +14 -3
  109. package/dist/dev/media/hls/parse-multivariant.js.map +1 -1
  110. package/dist/dev/media/hls/reload-policy.js +71 -0
  111. package/dist/dev/media/hls/reload-policy.js.map +1 -0
  112. package/dist/dev/media/live-window.js +36 -0
  113. package/dist/dev/media/live-window.js.map +1 -0
  114. package/dist/dev/media/primitives/select-tracks.js +1 -24
  115. package/dist/dev/media/primitives/select-tracks.js.map +1 -1
  116. package/dist/dev/media/types/index.d.ts +125 -9
  117. package/dist/dev/media/types/index.d.ts.map +1 -1
  118. package/dist/dev/media/types/index.js +15 -1
  119. package/dist/dev/media/types/index.js.map +1 -1
  120. package/dist/dev/media/utils/tracks.js +14 -0
  121. package/dist/dev/media/utils/tracks.js.map +1 -1
  122. package/dist/dev/mux-audio.d.ts +4 -0
  123. package/dist/dev/mux-audio.js +3 -0
  124. package/dist/dev/mux-background-video.d.ts +3 -0
  125. package/dist/dev/mux-background-video.js +3 -0
  126. package/dist/dev/mux-video.d.ts +4 -0
  127. package/dist/dev/mux-video.js +3 -0
  128. package/dist/dev/playback/adapters/hls-audio/adapter.d.ts +54 -0
  129. package/dist/dev/playback/adapters/hls-audio/adapter.d.ts.map +1 -0
  130. package/dist/dev/playback/adapters/hls-audio/adapter.js +186 -0
  131. package/dist/dev/playback/adapters/hls-audio/adapter.js.map +1 -0
  132. package/dist/dev/playback/adapters/hls-audio/media.d.ts +10 -0
  133. package/dist/dev/playback/adapters/hls-audio/media.d.ts.map +1 -0
  134. package/dist/dev/playback/adapters/hls-audio/media.js +9 -0
  135. package/dist/dev/playback/adapters/hls-audio/media.js.map +1 -0
  136. package/dist/dev/playback/adapters/hls-background-video/adapter.d.ts +59 -0
  137. package/dist/dev/playback/adapters/hls-background-video/adapter.d.ts.map +1 -0
  138. package/dist/dev/playback/adapters/hls-background-video/adapter.js +123 -0
  139. package/dist/dev/playback/adapters/hls-background-video/adapter.js.map +1 -0
  140. package/dist/dev/playback/adapters/hls-background-video/host.d.ts +38 -0
  141. package/dist/dev/playback/adapters/hls-background-video/host.d.ts.map +1 -0
  142. package/dist/dev/playback/adapters/hls-background-video/host.js +60 -0
  143. package/dist/dev/playback/adapters/hls-background-video/host.js.map +1 -0
  144. package/dist/dev/playback/adapters/hls-background-video/media.d.ts +20 -0
  145. package/dist/dev/playback/adapters/hls-background-video/media.d.ts.map +1 -0
  146. package/dist/dev/playback/adapters/hls-background-video/media.js +21 -0
  147. package/dist/dev/playback/adapters/hls-background-video/media.js.map +1 -0
  148. package/dist/dev/playback/adapters/hls-video/adapter.d.ts +63 -0
  149. package/dist/dev/playback/adapters/hls-video/adapter.d.ts.map +1 -0
  150. package/dist/dev/playback/adapters/hls-video/adapter.js +316 -0
  151. package/dist/dev/playback/adapters/hls-video/adapter.js.map +1 -0
  152. package/dist/dev/playback/adapters/hls-video/error-surface.d.ts +18 -0
  153. package/dist/dev/playback/adapters/hls-video/error-surface.d.ts.map +1 -0
  154. package/dist/dev/playback/adapters/hls-video/error-surface.js +51 -0
  155. package/dist/dev/playback/adapters/hls-video/error-surface.js.map +1 -0
  156. package/dist/dev/playback/adapters/hls-video/media-tracks.d.ts +22 -0
  157. package/dist/dev/playback/adapters/hls-video/media-tracks.d.ts.map +1 -0
  158. package/dist/dev/playback/adapters/hls-video/media-tracks.js +126 -0
  159. package/dist/dev/playback/adapters/hls-video/media-tracks.js.map +1 -0
  160. package/dist/dev/playback/adapters/hls-video/media.d.ts +10 -0
  161. package/dist/dev/playback/adapters/hls-video/media.d.ts.map +1 -0
  162. package/dist/dev/playback/adapters/hls-video/media.js +11 -0
  163. package/dist/dev/playback/adapters/hls-video/media.js.map +1 -0
  164. package/dist/dev/playback/adapters/mux-audio/media.d.ts +28 -0
  165. package/dist/dev/playback/adapters/mux-audio/media.d.ts.map +1 -0
  166. package/dist/dev/playback/adapters/mux-audio/media.js +26 -0
  167. package/dist/dev/playback/adapters/mux-audio/media.js.map +1 -0
  168. package/dist/dev/playback/adapters/mux-video/adapter.d.ts +33 -0
  169. package/dist/dev/playback/adapters/mux-video/adapter.d.ts.map +1 -0
  170. package/dist/dev/playback/adapters/mux-video/adapter.js +92 -0
  171. package/dist/dev/playback/adapters/mux-video/adapter.js.map +1 -0
  172. package/dist/dev/playback/adapters/mux-video/media.d.ts +24 -0
  173. package/dist/dev/playback/adapters/mux-video/media.d.ts.map +1 -0
  174. package/dist/dev/playback/adapters/mux-video/media.js +23 -0
  175. package/dist/dev/playback/adapters/mux-video/media.js.map +1 -0
  176. package/dist/dev/playback/behaviors/collect-errors.js +77 -0
  177. package/dist/dev/playback/behaviors/collect-errors.js.map +1 -0
  178. package/dist/dev/playback/behaviors/dom/end-of-stream.js +1 -0
  179. package/dist/dev/playback/behaviors/dom/end-of-stream.js.map +1 -1
  180. package/dist/dev/playback/behaviors/dom/seek-to-live-edge.js +132 -0
  181. package/dist/dev/playback/behaviors/dom/seek-to-live-edge.js.map +1 -0
  182. package/dist/dev/playback/behaviors/dom/sync-live-seekable-range.js +26 -0
  183. package/dist/dev/playback/behaviors/dom/sync-live-seekable-range.js.map +1 -0
  184. package/dist/dev/playback/behaviors/dom/update-mediasource-duration.js +29 -7
  185. package/dist/dev/playback/behaviors/dom/update-mediasource-duration.js.map +1 -1
  186. package/dist/dev/playback/behaviors/establish-start-media-time.d.ts +5 -0
  187. package/dist/dev/playback/behaviors/establish-start-media-time.d.ts.map +1 -1
  188. package/dist/dev/playback/behaviors/establish-start-media-time.js +78 -4
  189. package/dist/dev/playback/behaviors/establish-start-media-time.js.map +1 -1
  190. package/dist/dev/playback/behaviors/resolve-track.js +29 -8
  191. package/dist/dev/playback/behaviors/resolve-track.js.map +1 -1
  192. package/dist/dev/playback/behaviors/track-switching.js +18 -9
  193. package/dist/dev/playback/behaviors/track-switching.js.map +1 -1
  194. package/dist/dev/playback/engines/hls/engine-audio-only.d.ts +34 -17
  195. package/dist/dev/playback/engines/hls/engine-audio-only.d.ts.map +1 -1
  196. package/dist/dev/playback/engines/hls/engine-audio-only.js +9 -5
  197. package/dist/dev/playback/engines/hls/engine-audio-only.js.map +1 -1
  198. package/dist/dev/playback/engines/{background-video/engine.d.ts → hls/engine-background-video.d.ts} +7 -7
  199. package/dist/dev/playback/engines/hls/engine-background-video.d.ts.map +1 -0
  200. package/dist/{default/playback/engines/background-video/engine.js → dev/playback/engines/hls/engine-background-video.js} +3 -3
  201. package/dist/dev/playback/engines/hls/engine-background-video.js.map +1 -0
  202. package/dist/dev/playback/engines/hls/engine.d.ts +43 -15
  203. package/dist/dev/playback/engines/hls/engine.d.ts.map +1 -1
  204. package/dist/dev/playback/engines/hls/engine.js +19 -6
  205. package/dist/dev/playback/engines/hls/engine.js.map +1 -1
  206. package/dist/dev/playback/primitives/error-messages.js +39 -0
  207. package/dist/dev/playback/primitives/error-messages.js.map +1 -0
  208. package/dist/dev/playback/primitives/live-window.js +66 -0
  209. package/dist/dev/playback/primitives/live-window.js.map +1 -0
  210. package/dist/dev/playback/primitives/report-track-conditions.d.ts +11 -0
  211. package/dist/dev/playback/primitives/report-track-conditions.d.ts.map +1 -0
  212. package/dist/dev/playback/primitives/report-track-conditions.js +86 -0
  213. package/dist/dev/playback/primitives/report-track-conditions.js.map +1 -0
  214. package/package.json +32 -6
  215. package/dist/default/background-video.js +0 -3
  216. package/dist/default/playback/engines/background-video/adapter.js +0 -158
  217. package/dist/default/playback/engines/background-video/adapter.js.map +0 -1
  218. package/dist/default/playback/engines/background-video/engine.js.map +0 -1
  219. package/dist/default/playback/engines/hls/adapter-audio-only.js +0 -124
  220. package/dist/default/playback/engines/hls/adapter-audio-only.js.map +0 -1
  221. package/dist/default/playback/engines/hls/adapter.js +0 -120
  222. package/dist/default/playback/engines/hls/adapter.js.map +0 -1
  223. package/dist/dev/background-video.d.ts +0 -3
  224. package/dist/dev/background-video.js +0 -3
  225. package/dist/dev/playback/engines/background-video/adapter.d.ts +0 -59
  226. package/dist/dev/playback/engines/background-video/adapter.d.ts.map +0 -1
  227. package/dist/dev/playback/engines/background-video/adapter.js +0 -158
  228. package/dist/dev/playback/engines/background-video/adapter.js.map +0 -1
  229. package/dist/dev/playback/engines/background-video/engine.d.ts.map +0 -1
  230. package/dist/dev/playback/engines/background-video/engine.js.map +0 -1
  231. package/dist/dev/playback/engines/hls/adapter-audio-only.d.ts +0 -46
  232. package/dist/dev/playback/engines/hls/adapter-audio-only.d.ts.map +0 -1
  233. package/dist/dev/playback/engines/hls/adapter-audio-only.js +0 -124
  234. package/dist/dev/playback/engines/hls/adapter-audio-only.js.map +0 -1
  235. package/dist/dev/playback/engines/hls/adapter.d.ts +0 -42
  236. package/dist/dev/playback/engines/hls/adapter.d.ts.map +0 -1
  237. package/dist/dev/playback/engines/hls/adapter.js +0 -120
  238. package/dist/dev/playback/engines/hls/adapter.js.map +0 -1
@@ -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;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"}
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;;;;;;;AAqEA,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"}
@@ -5,13 +5,20 @@
5
5
  * Based on CMAF-HAM (Common Media Application Format - Hypothetical Application Model)
6
6
  * Protocol-agnostic representation of streaming media content.
7
7
  *
8
- * @see https://github.com/AcademySoftwareFoundation/common-media-library
8
+ * @see https://github.com/streaming-video-technology-alliance/common-media-library
9
9
  */
10
10
  /**
11
11
  * Base identifier type for all HAM objects.
12
12
  */
13
13
  interface Ham {
14
14
  id: string;
15
+ /**
16
+ * Format-/protocol-specific values that aren't part of the generic CMAF-HAM
17
+ * model — kept in an open bag so the model stays format-neutral (mirrors how
18
+ * CMAF-HAM itself stashes protocol extras rather than growing the model).
19
+ * Typed reads go through dedicated accessors (e.g. `getMediaPlaylistMetadata`).
20
+ */
21
+ metadata?: Record<string, unknown>;
15
22
  }
16
23
  /**
17
24
  * Addressable resource with optional byte range.
@@ -64,7 +71,9 @@ type PartiallyResolved<T extends Track = Track> = Omit<T, 'segments' | 'initiali
64
71
  * All URLs are fully qualified (parsers resolve relative URLs).
65
72
  */
66
73
  /**
67
- * Track startTime is always 0 (for future multi-period support).
74
+ * Track startTime is always 0 the presentation-timeline origin (and future
75
+ * multi-period base). The live sliding-window edge is `segments[0].startTime`,
76
+ * derived — never stored here.
68
77
  */
69
78
  type Track = Ham & AddressableObject & TimeSpan & {
70
79
  type: TrackType;
@@ -76,17 +85,32 @@ type Track = Ham & AddressableObject & TimeSpan & {
76
85
  segments: Segment[];
77
86
  /**
78
87
  * Media-timeline (decode/encode) coordinate of the track's timeline origin
79
- * (`startTime`) — the media-time base value of the coordinate model, peer to
80
- * `startTime` (presentation). Derived from the container
88
+ * (presentation-0) — the media-time base value of the coordinate model, peer
89
+ * to `startTime` (presentation). Derived from the container
81
90
  * (`tfdt.baseMediaDecodeTime ÷ mdhd.timescale`); the relocation offset is
82
- * `startTime startMediaTime`, never stored.
91
+ * `−startMediaTime` (targets presentation-0 `Track.startTime` plays no
92
+ * part), never stored.
83
93
  *
84
- * Optional: absent until established (0-PTS sources never set it — their
85
- * origin is already 0). Established once per source by the
94
+ * Optional: `undefined` means not yet established, or never establishable
95
+ * (e.g. text tracks carry no container origin); a near-zero/native origin is
96
+ * established as `0` (no relocation). Established once per source by the
86
97
  * `establishStartMediaTime` reactor. See
87
98
  * `internal/design/spf/presentation-timeline-model.md`.
88
99
  */
89
100
  startMediaTime?: number;
101
+ /**
102
+ * Wall-clock time (epoch seconds) at the track's timeline origin
103
+ * (presentation-0) — pure playlist arithmetic over any PDT-bearing segment:
104
+ * `segment.startDate − segment.startTime`, invariant along a linear
105
+ * timeline. Optional: absent when no segment carries `startDate`.
106
+ *
107
+ * The wall-clock member of the coordinate triple (peer to `startTime` and
108
+ * `startMediaTime`). For live it is the anchor segments are PDT-placed
109
+ * against on every parse; equal `startDate` across tracks marks the same
110
+ * presentation instant. See
111
+ * `internal/design/spf/live-presentation-timeline-model.md`.
112
+ */
113
+ startDate?: number;
90
114
  };
91
115
  /**
92
116
  * Per-track-type origin-establishment data, accumulated across appends (the media
@@ -175,6 +199,7 @@ type TextTrack = Track & {
175
199
  type CanPlayTrack = (track: {
176
200
  mimeType?: string;
177
201
  codecs?: string[];
202
+ metadata?: Record<string, unknown>;
178
203
  }) => boolean;
179
204
  /**
180
205
  * Minimal text-track cue shape — start time, end time, and display text.
@@ -194,6 +219,10 @@ interface Cue {
194
219
  * Has metadata but no segments or initialization yet (media playlist not fetched).
195
220
  */
196
221
  type PartiallyResolvedTextTrack = PartiallyResolved<TextTrack>;
222
+ /**
223
+ * Union of all resolved track types.
224
+ */
225
+ type ResolvedTrack = VideoTrack | AudioTrack | TextTrack;
197
226
  /**
198
227
  * Generic switching set type.
199
228
  * A group of tracks that can be switched between seamlessly.
@@ -234,8 +263,89 @@ type SelectionSet = VideoSelectionSet | AudioSelectionSet | TextSelectionSet;
234
263
  /**
235
264
  * Media segment with timing information.
236
265
  * Follows CMAF-HAM composition pattern.
266
+ *
267
+ * `startDate` is the absolute wall-clock time of the segment's first
268
+ * sample, in **epoch seconds** (unit-consistent with `startTime`/`duration`),
269
+ * derived from `#EXT-X-PROGRAM-DATE-TIME` (explicit or interpolated forward via
270
+ * `EXTINF`). Unlike the per-track-relative `startTime`, it is comparable across
271
+ * tracks, so it is the cross-track sync anchor for demuxed audio/video and the
272
+ * exact recovery value on a full live-window turnover. Optional: absent when the
273
+ * source carries no PDT (allowed by RFC 8216, required by Apple's HLS authoring
274
+ * spec — so present on conformant content).
237
275
  */
238
- type Segment = Ham & AddressableObject & TimeSpan;
276
+ type Segment = Ham & AddressableObject & TimeSpan & {
277
+ startDate?: number;
278
+ };
279
+ /**
280
+ * Playlist-level metadata surfaced from a parsed media playlist. HLS delivery
281
+ * specifics — not part of the generic CMAF-HAM model — so they live under
282
+ * `Ham.metadata` (read via `getMediaPlaylistMetadata`) rather than as
283
+ * first-class `Track` fields:
284
+ *
285
+ * - `targetDuration` (`#EXT-X-TARGETDURATION`) — reload-cadence basis.
286
+ * - `mediaSequence` (`#EXT-X-MEDIA-SEQUENCE`, default 0) — sequence number of
287
+ * `segments[0]`; the join key for merging successive reload snapshots.
288
+ * - `playlistType` (`#EXT-X-PLAYLIST-TYPE`) — `VOD` / `EVENT` / undefined.
289
+ * - `endList` (`#EXT-X-ENDLIST`) — playlist is complete; stop reloading.
290
+ */
291
+ interface MediaPlaylistMetadata {
292
+ targetDuration: number;
293
+ mediaSequence: number;
294
+ playlistType?: 'VOD' | 'EVENT';
295
+ endList: boolean;
296
+ /**
297
+ * Whether this rendition carries encrypted segments — any `#EXT-X-KEY` whose
298
+ * `METHOD` isn't `NONE`. Detection only: enough to tell that playback needs
299
+ * decryption support, not enough to perform it.
300
+ *
301
+ * Deliberately *not* a model-level `protection` shape. CMAF-HAM puts
302
+ * `protection` on `SwitchingSet`, but that can't express two real cases: a
303
+ * clear lead (`METHOD=NONE` segments followed by encrypted ones — protection
304
+ * varies along the timeline within one rendition) or key rotation (its single
305
+ * `defaultKid` can't represent a key changing over time). Modeling it properly
306
+ * belongs to DRM support; until then this records the one fact a playlist
307
+ * reliably gives us. Per-rendition because that's HLS's granularity —
308
+ * `EXT-X-KEY` is a media-playlist tag.
309
+ *
310
+ * Conservative for a clear lead: a rendition whose opening segments are clear
311
+ * still reads as encrypted, so it's judged unplayable rather than played until
312
+ * it breaks.
313
+ */
314
+ encrypted?: boolean;
315
+ /**
316
+ * `EXT-X-SERVER-CONTROL` `HOLD-BACK` (seconds) — the server's declared distance
317
+ * from the live edge for clients playing *complete* segments. Absent when the
318
+ * server doesn't advertise it, in which case the spec default (3 × target
319
+ * duration) applies. Deliberately HLS vocabulary living in the playlist
320
+ * metadata rather than on `Track`: whether a wall-clock holdback generalizes
321
+ * across delivery formats is unresolved.
322
+ *
323
+ * `PART-HOLD-BACK` is **not** captured — it only applies to clients playing
324
+ * partial segments, and using it while fetching whole segments would put the
325
+ * playhead ahead of the last complete segment. Add it with LL-HLS support.
326
+ */
327
+ holdBack?: number;
328
+ /**
329
+ * Whether the server is delivering this rendition as Low-Latency HLS — any of
330
+ * `#EXT-X-PART`, `#EXT-X-PART-INF`, or `EXT-X-SERVER-CONTROL`'s
331
+ * `PART-HOLD-BACK`.
332
+ *
333
+ * Detection only, and deliberately so: partial segments are ignored by the
334
+ * parser and the loader fetches whole segments, so an LL-HLS playlist plays as
335
+ * standard live at standard latency. Recording the fact is what lets a
336
+ * composition *say* that rather than silently under-delivering the latency the
337
+ * publisher configured.
338
+ */
339
+ lowLatency?: boolean;
340
+ }
341
+ /** Typed read of the media-playlist metadata stashed in `ham.metadata`. */
342
+ declare function getMediaPlaylistMetadata(ham: Pick<Ham, 'metadata'>): MediaPlaylistMetadata | undefined;
343
+ /**
344
+ * The source's semantic nature — live vs on-demand. A model concept
345
+ * (consumer-facing), distinct from completeness / duration: a live stream that
346
+ * has *ended* is still `'live'`.
347
+ */
348
+ type StreamType = 'live' | 'on-demand';
239
349
  /**
240
350
  * Presentation - a single playable period of content.
241
351
  * Uses TimeSpan fields (startTime always 0, duration optional until track resolved).
@@ -245,6 +355,12 @@ type Segment = Ham & AddressableObject & TimeSpan;
245
355
  */
246
356
  type Presentation = Ham & AddressableObject & Partial<TimeSpan> & {
247
357
  selectionSets: SelectionSet[];
358
+ /**
359
+ * Live vs on-demand — the source's semantic nature. Populated once a media
360
+ * playlist is parsed (derived from `#EXT-X-PLAYLIST-TYPE` via
361
+ * `deriveStreamType`); orthogonal to duration / completeness.
362
+ */
363
+ streamType?: StreamType;
248
364
  };
249
365
  /**
250
366
  * State-shaped presentation that may or may not be resolved yet.
@@ -257,5 +373,5 @@ type Presentation = Ham & AddressableObject & Partial<TimeSpan> & {
257
373
  */
258
374
  type MaybeResolvedPresentation = AddressableObject & Partial<Omit<Presentation, keyof AddressableObject>>;
259
375
  //#endregion
260
- export { AddressableObject, AudioSelectionSet, AudioTrack, CanPlayTrack, Cue, FrameRate, Ham, MaybeResolvedPresentation, MediaContainerData, PartiallyResolved, PartiallyResolvedTextTrack, Presentation, Segment, SegmentData, SelectionSet, SelectionSetOf, SwitchingSetOf, TextSelectionSet, TextTrack, TimeSpan, Track, TrackType, VideoSelectionSet, VideoTrack };
376
+ export { AddressableObject, AudioSelectionSet, AudioTrack, CanPlayTrack, Cue, FrameRate, Ham, MaybeResolvedPresentation, MediaContainerData, MediaPlaylistMetadata, PartiallyResolved, PartiallyResolvedTextTrack, Presentation, ResolvedTrack, Segment, SegmentData, SelectionSet, SelectionSetOf, StreamType, SwitchingSetOf, TextSelectionSet, TextTrack, TimeSpan, Track, TrackType, VideoSelectionSet, VideoTrack, getMediaPlaylistMetadata };
261
377
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../../../../src/media/types/index.ts"],"mappings":";;;;;;;;;;;;UAgBiB;EACf;;;;;UAMe;EACf;EACA;IACE;IACA;;;;;;;UAyBa;EACf;EACA;;;;;KAUU;;;;;;;;UAaK;EACf;EACA;;;;;;;;KAaU,kBAAkB,UAAU,QAAQ,SAAS,KAAK,yCAAyC;EACrG;EACA;EACA;EACA;;;;;;;;;;KA2BU,QAAQ,MAClB,oBACA;EACE,MAAM;EACN;EACA;EACA;EACA;EACA,iBAAiB;EACjB,UAAU;;;;;;;;;;;;;EAaV;;;;;;;;;;;;;;;;;;;;UAqBa;EACf;EACA;EACA;EACA;;;;;;;KAQU,cAAc,cAAc,cAAc;;;;KAK1C,aAAa,QACvB,SAAS,KAAK;EACZ;EAGA;EACA;EACA,YAAY;;;;;;;;EAQZ;;;;;KAMQ,aAAa,QACvB,SAAS,KAAK;EACZ;EACA;EACA;EACA;EACA;EACA;EACA;;;;;KAMQ,YAAY;EACtB;EACA;EACA;EACA;EACA;EACA;EACA;;;;;;;;;;;;;;;KAgBU,gBAAgB;EAAS;EAAmB;;;;;;;;;;UAUvC;EACf;EACA;EACA;;;;;;KAmBU,6BAA6B,kBAAkB;;;;;;;KAyB/C,eAAe,UAAU,QAAQ,SAAS;EACpD,MAAM;EACN,SAAS,kBAAkB,KAAK;;;;;;;;KA8BtB,eAAe,UAAU,QAAQ,SAAS;EACpD,MAAM;EACN,eAAe,eAAe;;;;;KAMpB,oBAAoB,eAAe;;;;KAKnC,oBAAoB,eAAe;;;;KAKnC,mBAAmB,eAAe;;;;;KAMlC,eAAe,oBAAoB,oBAAoB;;;;;KAUvD,UAAU,MAAM,oBAAoB;;;;;;;;KAyCpC,eAAe,MACzB,oBACA,QAAQ;EACN,eAAe;;;;;;;;;;;KAYP,4BAA4B,oBAAoB,QAAQ,KAAK,oBAAoB"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../../../src/media/types/index.ts"],"mappings":";;;;;;;;;;;;UAgBiB;EACf;;;;;;;EAOA,WAAW;;;;;UAMI;EACf;EACA;IACE;IACA;;;;;;;UAyBa;EACf;EACA;;;;;KAUU;;;;;;;;UAaK;EACf;EACA;;;;;;;;KAaU,kBAAkB,UAAU,QAAQ,SAAS,KAAK,yCAAyC;EACrG;EACA;EACA;EACA;;;;;;;;;;;;KA6BU,QAAQ,MAClB,oBACA;EACE,MAAM;EACN;EACA;EACA;EACA;EACA,iBAAiB;EACjB,UAAU;;;;;;;;;;;;;;;EAeV;;;;;;;;;;;;;EAaA;;;;;;;;;;;;;;;;;;;;UAqBa;EACf;EACA;EACA;EACA;;;;;;;KAQU,cAAc,cAAc,cAAc;;;;KAK1C,aAAa,QACvB,SAAS,KAAK;EACZ;EAGA;EACA;EACA,YAAY;;;;;;;;EAQZ;;;;;KAMQ,aAAa,QACvB,SAAS,KAAK;EACZ;EACA;EACA;EACA;EACA;EACA;EACA;;;;;KAMQ,YAAY;EACtB;EACA;EACA;EACA;EACA;EACA;EACA;;;;;;;;;;;;;;;KAgBU,gBAAgB;EAC1B;EACA;EACA,WAAW;;;;;;;;;;UAWI;EACf;EACA;EACA;;;;;;KAmBU,6BAA6B,kBAAkB;;;;KAK/C,gBAAgB,aAAa,aAAa;;;;;;;KAoB1C,eAAe,UAAU,QAAQ,SAAS;EACpD,MAAM;EACN,SAAS,kBAAkB,KAAK;;;;;;;;KA8BtB,eAAe,UAAU,QAAQ,SAAS;EACpD,MAAM;EACN,eAAe,eAAe;;;;;KAMpB,oBAAoB,eAAe;;;;KAKnC,oBAAoB,eAAe;;;;KAKnC,mBAAmB,eAAe;;;;;KAMlC,eAAe,oBAAoB,oBAAoB;;;;;;;;;;;;;;KAmBvD,UAAU,MAAM,oBAAoB;EAAa;;;;;;;;;;;;;;UA4B5C;EACf;EACA;EACA;EACA;;;;;;;;;;;;;;;;;;;EAmBA;;;;;;;;;;;;;EAaA;;;;;;;;;;;;EAYA;;;iBAOc,yBAAyB,KAAK,KAAK,mBAAmB;;;;;;KAa1D;;;;;;;;KAwCA,eAAe,MACzB,oBACA,QAAQ;EACN,eAAe;;;;;;EAMf,aAAa;;;;;;;;;;;KAYL,4BAA4B,oBAAoB,QAAQ,KAAK,oBAAoB"}
@@ -8,6 +8,20 @@
8
8
  * playlists / quality levels.
9
9
  */
10
10
  const SEGMENT_TIME_EPSILON = 1e-4;
11
+ /** Key under `Ham.metadata` where {@link MediaPlaylistMetadata} is stored. */
12
+ const MEDIA_PLAYLIST_METADATA_KEY = "mediaPlaylist";
13
+ /** Typed read of the media-playlist metadata stashed in `ham.metadata`. */
14
+ function getMediaPlaylistMetadata(ham) {
15
+ return ham.metadata?.[MEDIA_PLAYLIST_METADATA_KEY];
16
+ }
17
+ /**
18
+ * Derive {@link StreamType} from a media playlist's metadata. Per the model,
19
+ * only `#EXT-X-PLAYLIST-TYPE:VOD` marks on-demand; everything else (EVENT, or
20
+ * the tag absent) is live — completeness (`endList`) never factors in.
21
+ */
22
+ function deriveStreamType(metadata) {
23
+ return metadata?.playlistType === "VOD" ? "on-demand" : "live";
24
+ }
11
25
  function isResolvedTrack(track) {
12
26
  return "segments" in track;
13
27
  }
@@ -31,6 +45,6 @@ function isResolvedPresentation(presentation) {
31
45
  return presentation !== void 0 && presentation.id !== void 0 && presentation.selectionSets !== void 0;
32
46
  }
33
47
  //#endregion
34
- export { SEGMENT_TIME_EPSILON, hasPresentationDuration, isResolvedPresentation, isResolvedTrack };
48
+ export { MEDIA_PLAYLIST_METADATA_KEY, SEGMENT_TIME_EPSILON, deriveStreamType, getMediaPlaylistMetadata, hasPresentationDuration, isResolvedPresentation, isResolvedTrack };
35
49
 
36
50
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../../../src/media/types/index.ts"],"sourcesContent":["/**\n * Core SPF Types\n *\n * Based on CMAF-HAM (Common Media Application Format - Hypothetical Application Model)\n * Protocol-agnostic representation of streaming media content.\n *\n * @see https://github.com/AcademySoftwareFoundation/common-media-library\n */\n\n// =============================================================================\n// Base Types\n// =============================================================================\n\n/**\n * Base identifier type for all HAM objects.\n */\nexport interface Ham {\n id: string;\n}\n\n/**\n * Addressable resource with optional byte range.\n */\nexport interface AddressableObject {\n url: string;\n byteRange?: {\n start: number;\n end: number;\n };\n}\n\n// =============================================================================\n// Platform-agnostic Media Element\n// =============================================================================\n\n/**\n * Platform-agnostic media element interface.\n * Captures minimal shape needed for orchestration without DOM dependencies.\n * HTMLMediaElement satisfies this interface.\n */\nexport interface MediaElementLike {\n preload: string;\n}\n\n// =============================================================================\n// Time and Duration\n// =============================================================================\n\n/**\n * Time span with start time and duration.\n * Used for segments and other timed ranges.\n */\nexport interface TimeSpan {\n startTime: number;\n duration: number;\n}\n\n// =============================================================================\n// Enums\n// =============================================================================\n\n/**\n * Track content type.\n */\nexport type TrackType = 'video' | 'audio' | 'text';\n\n// =============================================================================\n// Frame Rate\n// =============================================================================\n\n/**\n * Video frame rate expressed as numerator/denominator.\n *\n * Examples:\n * - 30 fps: { frameRateNumerator: 30 }\n * - 29.97 fps: { frameRateNumerator: 30000, frameRateDenominator: 1001 }\n */\nexport interface FrameRate {\n frameRateNumerator: number;\n frameRateDenominator?: number;\n}\n\n// =============================================================================\n// Partially Resolved Tracks (before media playlist is fetched)\n// =============================================================================\n\n/**\n * Generic type for partially resolved tracks.\n * Removes fields that come from media playlist parsing.\n *\n * @param T - Track type to make partially resolved (must extend Track)\n */\nexport type PartiallyResolved<T extends Track = Track> = Omit<T, 'segments' | 'initialization' | keyof TimeSpan> & {\n segments?: never;\n duration?: never;\n startTime?: never;\n initialization?: never;\n};\n\n/**\n * Partially resolved video track from multivariant playlist.\n * Has metadata but no segments or initialization yet (media playlist not fetched).\n */\nexport type PartiallyResolvedVideoTrack = PartiallyResolved<VideoTrack>;\n\n/**\n * Partially resolved audio track from multivariant playlist.\n * Has metadata but no segments or initialization yet (media playlist not fetched).\n */\nexport type PartiallyResolvedAudioTrack = PartiallyResolved<AudioTrack>;\n\n// =============================================================================\n// Resolved Track Types (with segments from media playlist)\n// =============================================================================\n\n/**\n * Base track type containing common properties for all resolved tracks.\n * A resolved track has segments, duration, and initialization data.\n * All URLs are fully qualified (parsers resolve relative URLs).\n */\n/**\n * Track startTime is always 0 (for future multi-period support).\n */\nexport type Track = Ham &\n AddressableObject &\n TimeSpan & {\n type: TrackType;\n codecs?: string[]; // Optional per HLS spec\n mimeType: string;\n language?: string | undefined;\n bandwidth: number;\n initialization?: AddressableObject;\n segments: Segment[];\n /**\n * Media-timeline (decode/encode) coordinate of the track's timeline origin\n * (`startTime`) — the media-time base value of the coordinate model, peer to\n * `startTime` (presentation). Derived from the container\n * (`tfdt.baseMediaDecodeTime ÷ mdhd.timescale`); the relocation offset is\n * `startTime − startMediaTime`, never stored.\n *\n * Optional: absent until established (0-PTS sources never set it — their\n * origin is already 0). Established once per source by the\n * `establishStartMediaTime` reactor. See\n * `internal/design/spf/presentation-timeline-model.md`.\n */\n startMediaTime?: number;\n };\n\n/**\n * Per-track-type origin-establishment data, accumulated across appends (the media\n * track's `track_id` + `mdhd` timescale from the init, `tfdt` baseMediaDecodeTime of\n * that same track from the first media segment) — hence optional. The transient input\n * the `establishStartMediaTime` reactor reduces into `Track.startMediaTime`.\n *\n * `trackId` is the ISO-BMFF `track_ID` of the buffered media track (`vide`/`soun`),\n * read from the init's `tkhd`; it ties the timescale to the *same* track's\n * `baseMediaDecodeTime` (matched via `tfhd.track_id`) so a muxed segment carrying a\n * second track (e.g. `clcp` captions) reads the right `tfdt` rather than the first one.\n *\n * `segmentStartTime` is the 0-based presentation start of the segment\n * `baseMediaDecodeTime` was read from — *not* a container value (it's the playlist\n * position), but co-located because the origin is `baseMediaDecodeTime/timescale −\n * segmentStartTime`: the first *loaded* segment isn't necessarily the 0th (a\n * non-zero initial `currentTime`, or live/DVR), so the decode time alone isn't the\n * stream origin.\n */\nexport interface MediaContainerData {\n trackId?: number;\n timescale?: number;\n baseMediaDecodeTime?: number;\n segmentStartTime?: number;\n}\n\n/**\n * Raw media-segment bytes — a complete buffer or a byte stream. The transport-neutral\n * payload the loader pipeline carries; `AppendData` (the MSE `SourceBuffer` append\n * input in `media/dom/mse`) is an alias of this at the DOM boundary.\n */\nexport type SegmentData = ArrayBuffer | AsyncIterable<Uint8Array>;\n\n/**\n * Resolved video track with segments.\n */\nexport type VideoTrack = Track &\n Required<Pick<Track, 'initialization' | 'codecs'>> & {\n type: 'video';\n\n // Optional metadata from multivariant (per HLS spec)\n width?: number;\n height?: number;\n frameRate?: FrameRate;\n /**\n * Audio groups (`EXT-X-STREAM-INF:AUDIO`) this video rendition can pair\n * with. A list because one rendition is typically listed across multiple\n * `EXT-X-STREAM-INF` entries — one per audio group (the HLS cross-product) —\n * which the parser collapses into a single track carrying every group it\n * advertised.\n */\n audioGroupIds?: string[];\n };\n\n/**\n * Resolved audio track with segments.\n */\nexport type AudioTrack = Track &\n Required<Pick<Track, 'initialization' | 'codecs'>> & {\n type: 'audio';\n groupId: string;\n name: string;\n sampleRate: number;\n channels: number;\n default?: boolean;\n autoselect?: boolean;\n };\n\n/**\n * Resolved text track with segments.\n */\nexport type TextTrack = Track & {\n type: 'text';\n groupId: string;\n label: string;\n kind: 'subtitles' | 'captions';\n default?: boolean;\n autoselect?: boolean;\n forced?: boolean;\n};\n\n/**\n * Predicate that answers \"can this environment decode this track?\" — the\n * capability-probing surface, read by the track-switching hard-constraint\n * pre-pass (`excludeUnplayableTracks`) to drop undecodable renditions before\n * selection. Kept DOM-free here (a plain function type over a minimal track\n * shape) so DOM-free behaviors can consume it; the DOM implementation\n * (`canPlayTrack` in `media/dom/capabilities.ts`) wraps\n * `MediaSource.isTypeSupported`.\n *\n * Takes the minimal codec-bearing shape both video and audio candidates\n * carry. `mimeType` is optional so unprobeable candidates (no MIME) can be\n * passed straight through as playable rather than dropped.\n */\nexport type CanPlayTrack = (track: { mimeType?: string; codecs?: string[] }) => boolean;\n\n/**\n * Minimal text-track cue shape — start time, end time, and display text.\n *\n * Host-agnostic representation. `VTTCue` structurally satisfies this\n * interface, so DOM consumers pass `VTTCue` values directly. Non-DOM\n * hosts (workers, test fakes, non-browser engines) can satisfy the same\n * shape without pulling in DOM types.\n */\nexport interface Cue {\n startTime: number;\n endTime: number;\n text: string;\n}\n\n/**\n * Media element with an iterable text-track list, host-agnostic.\n *\n * Extends `MediaElementLike` with the minimum surface needed to observe\n * which text tracks are currently mounted on the media. `HTMLMediaElement`\n * structurally satisfies this (its `textTracks` is a `TextTrackList`,\n * which is iterable with `{ id }` items).\n */\nexport interface MediaElementWithTextTracks extends MediaElementLike {\n readonly textTracks: Iterable<{ readonly id: string }>;\n}\n\n/**\n * Partially resolved text track from multivariant playlist.\n * Has metadata but no segments or initialization yet (media playlist not fetched).\n */\nexport type PartiallyResolvedTextTrack = PartiallyResolved<TextTrack>;\n\n/**\n * Union of all resolved track types.\n */\nexport type ResolvedTrack = VideoTrack | AudioTrack | TextTrack;\n\n/**\n * Union of all partially resolved track types.\n */\nexport type PartiallyResolvedTrack =\n | PartiallyResolvedVideoTrack\n | PartiallyResolvedAudioTrack\n | PartiallyResolvedTextTrack;\n\n// =============================================================================\n// Switching and Selection Sets\n// =============================================================================\n\n/**\n * Generic switching set type.\n * A group of tracks that can be switched between seamlessly.\n *\n * @param T - Track type (VideoTrack, AudioTrack, or TextTrack)\n */\nexport type SwitchingSetOf<T extends Track = Track> = Ham & {\n type: T['type'];\n tracks: (PartiallyResolved<T> | T)[];\n};\n\n/**\n * Video switching set - contains only video tracks (partially resolved or fully resolved).\n */\nexport type VideoSwitchingSet = SwitchingSetOf<VideoTrack>;\n\n/**\n * Audio switching set - contains only audio tracks (partially resolved or fully resolved).\n */\nexport type AudioSwitchingSet = SwitchingSetOf<AudioTrack>;\n\n/**\n * Text switching set - contains only text tracks (partially resolved or fully resolved).\n */\nexport type TextSwitchingSet = SwitchingSetOf<TextTrack>;\n\n/**\n * Switching set - a group of tracks that can be switched between seamlessly.\n * Discriminated by track type.\n */\nexport type SwitchingSet = VideoSwitchingSet | AudioSwitchingSet | TextSwitchingSet;\n\n/**\n * Generic selection set type.\n * Groups switching sets by track type.\n *\n * @param T - Track type (VideoTrack, AudioTrack, or TextTrack)\n */\nexport type SelectionSetOf<T extends Track = Track> = Ham & {\n type: T['type'];\n switchingSets: SwitchingSetOf<T>[];\n};\n\n/**\n * Video selection set - contains only video switching sets.\n */\nexport type VideoSelectionSet = SelectionSetOf<VideoTrack>;\n\n/**\n * Audio selection set - contains only audio switching sets.\n */\nexport type AudioSelectionSet = SelectionSetOf<AudioTrack>;\n\n/**\n * Text selection set - contains only text switching sets.\n */\nexport type TextSelectionSet = SelectionSetOf<TextTrack>;\n\n/**\n * Selection set - groups switching sets by track type.\n * Discriminated union ensures type-safe track access.\n */\nexport type SelectionSet = VideoSelectionSet | AudioSelectionSet | TextSelectionSet;\n\n// =============================================================================\n// Segment\n// =============================================================================\n\n/**\n * Media segment with timing information.\n * Follows CMAF-HAM composition pattern.\n */\nexport type Segment = Ham & AddressableObject & TimeSpan;\n\n/**\n * Floating-point tolerance for matching segments by `startTime`. Two\n * segments are considered the same position when\n * `Math.abs(a.startTime - b.startTime) < SEGMENT_TIME_EPSILON`. Used by\n * the source-buffer dedup and segment-loader quality-aware filter to\n * tolerate sub-millisecond drift in segment timestamps across multiple\n * playlists / quality levels.\n */\nexport const SEGMENT_TIME_EPSILON = 0.0001;\n\n// =============================================================================\n// Media Playlist Info\n// =============================================================================\n\n/**\n * Intermediate representation of a parsed media playlist.\n * Used internally before assembling into full Track structure.\n */\nexport interface MediaPlaylistInfo {\n version: number;\n targetDuration: number;\n playlistType: 'VOD' | 'EVENT' | undefined;\n initSegment: AddressableObject | null;\n segments: Segment[];\n duration: number;\n endList: boolean;\n}\n\n// =============================================================================\n// Presentation\n// =============================================================================\n\n/**\n * Presentation - a single playable period of content.\n * Uses TimeSpan fields (startTime always 0, duration optional until track resolved).\n *\n * Extends AddressableObject so `url` contains the original manifest URL.\n * All URLs are fully qualified (parsers resolve relative URLs).\n */\nexport type Presentation = Ham &\n AddressableObject &\n Partial<TimeSpan> & {\n selectionSets: SelectionSet[];\n };\n\n/**\n * State-shaped presentation that may or may not be resolved yet.\n *\n * The lifecycle is a single value: a caller writes `{ url }`, and the\n * resolver populates the rest in place. `url` is always present; resolved\n * fields (`id`, `selectionSets`, duration) appear once parsing succeeds.\n *\n * Use `isResolvedPresentation` to narrow to `Presentation`.\n */\nexport type MaybeResolvedPresentation = AddressableObject & Partial<Omit<Presentation, keyof AddressableObject>>;\n\n// =============================================================================\n// Type Guards\n// =============================================================================\n\n/**\n * Check if a track is resolved (has segments).\n * Works for all track types with overloaded signatures for type narrowing.\n */\nexport function isResolvedTrack(track: PartiallyResolvedVideoTrack | VideoTrack): track is VideoTrack;\nexport function isResolvedTrack(track: PartiallyResolvedAudioTrack | AudioTrack): track is AudioTrack;\nexport function isResolvedTrack(track: PartiallyResolvedTextTrack | TextTrack): track is TextTrack;\nexport function isResolvedTrack(track: PartiallyResolvedTrack | ResolvedTrack): track is ResolvedTrack;\nexport function isResolvedTrack(track: PartiallyResolvedTrack | ResolvedTrack): track is ResolvedTrack {\n return 'segments' in track;\n}\n\n/**\n * Check if a presentation has duration (at least one track resolved).\n * Narrows type to include required duration.\n */\nexport function hasPresentationDuration(\n presentation: MaybeResolvedPresentation\n): presentation is MaybeResolvedPresentation & { duration: number } {\n return presentation.duration !== undefined;\n}\n\n/**\n * Narrows a `MaybeResolvedPresentation` to a fully resolved `Presentation`.\n *\n * A presentation is resolved once `resolvePresentation` has parsed the\n * manifest and populated both `id` and `selectionSets`. Both must be\n * present — a partial value with only one of them isn't usable, and\n * letting it through would have downstream behaviors crash when they\n * access `selectionSets`.\n */\nexport function isResolvedPresentation(\n presentation: MaybeResolvedPresentation | undefined\n): presentation is Presentation {\n return presentation !== undefined && presentation.id !== undefined && presentation.selectionSets !== undefined;\n}\n"],"mappings":";;;;;;;;;AAsXA,MAAa,uBAAuB;AA4DpC,SAAgB,gBAAgB,OAAuE;CACrG,OAAO,cAAc;AACvB;;;;;AAMA,SAAgB,wBACd,cACkE;CAClE,OAAO,aAAa,aAAa,KAAA;AACnC;;;;;;;;;;AAWA,SAAgB,uBACd,cAC8B;CAC9B,OAAO,iBAAiB,KAAA,KAAa,aAAa,OAAO,KAAA,KAAa,aAAa,kBAAkB,KAAA;AACvG"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../../src/media/types/index.ts"],"sourcesContent":["/**\n * Core SPF Types\n *\n * Based on CMAF-HAM (Common Media Application Format - Hypothetical Application Model)\n * Protocol-agnostic representation of streaming media content.\n *\n * @see https://github.com/streaming-video-technology-alliance/common-media-library\n */\n\n// =============================================================================\n// Base Types\n// =============================================================================\n\n/**\n * Base identifier type for all HAM objects.\n */\nexport interface Ham {\n id: string;\n /**\n * Format-/protocol-specific values that aren't part of the generic CMAF-HAM\n * model — kept in an open bag so the model stays format-neutral (mirrors how\n * CMAF-HAM itself stashes protocol extras rather than growing the model).\n * Typed reads go through dedicated accessors (e.g. `getMediaPlaylistMetadata`).\n */\n metadata?: Record<string, unknown>;\n}\n\n/**\n * Addressable resource with optional byte range.\n */\nexport interface AddressableObject {\n url: string;\n byteRange?: {\n start: number;\n end: number;\n };\n}\n\n// =============================================================================\n// Platform-agnostic Media Element\n// =============================================================================\n\n/**\n * Platform-agnostic media element interface.\n * Captures minimal shape needed for orchestration without DOM dependencies.\n * HTMLMediaElement satisfies this interface.\n */\nexport interface MediaElementLike {\n preload: string;\n}\n\n// =============================================================================\n// Time and Duration\n// =============================================================================\n\n/**\n * Time span with start time and duration.\n * Used for segments and other timed ranges.\n */\nexport interface TimeSpan {\n startTime: number;\n duration: number;\n}\n\n// =============================================================================\n// Enums\n// =============================================================================\n\n/**\n * Track content type.\n */\nexport type TrackType = 'video' | 'audio' | 'text';\n\n// =============================================================================\n// Frame Rate\n// =============================================================================\n\n/**\n * Video frame rate expressed as numerator/denominator.\n *\n * Examples:\n * - 30 fps: { frameRateNumerator: 30 }\n * - 29.97 fps: { frameRateNumerator: 30000, frameRateDenominator: 1001 }\n */\nexport interface FrameRate {\n frameRateNumerator: number;\n frameRateDenominator?: number;\n}\n\n// =============================================================================\n// Partially Resolved Tracks (before media playlist is fetched)\n// =============================================================================\n\n/**\n * Generic type for partially resolved tracks.\n * Removes fields that come from media playlist parsing.\n *\n * @param T - Track type to make partially resolved (must extend Track)\n */\nexport type PartiallyResolved<T extends Track = Track> = Omit<T, 'segments' | 'initialization' | keyof TimeSpan> & {\n segments?: never;\n duration?: never;\n startTime?: never;\n initialization?: never;\n};\n\n/**\n * Partially resolved video track from multivariant playlist.\n * Has metadata but no segments or initialization yet (media playlist not fetched).\n */\nexport type PartiallyResolvedVideoTrack = PartiallyResolved<VideoTrack>;\n\n/**\n * Partially resolved audio track from multivariant playlist.\n * Has metadata but no segments or initialization yet (media playlist not fetched).\n */\nexport type PartiallyResolvedAudioTrack = PartiallyResolved<AudioTrack>;\n\n// =============================================================================\n// Resolved Track Types (with segments from media playlist)\n// =============================================================================\n\n/**\n * Base track type containing common properties for all resolved tracks.\n * A resolved track has segments, duration, and initialization data.\n * All URLs are fully qualified (parsers resolve relative URLs).\n */\n/**\n * Track startTime is always 0 — the presentation-timeline origin (and future\n * multi-period base). The live sliding-window edge is `segments[0].startTime`,\n * derived — never stored here.\n */\nexport type Track = Ham &\n AddressableObject &\n TimeSpan & {\n type: TrackType;\n codecs?: string[]; // Optional per HLS spec\n mimeType: string;\n language?: string | undefined;\n bandwidth: number;\n initialization?: AddressableObject;\n segments: Segment[];\n /**\n * Media-timeline (decode/encode) coordinate of the track's timeline origin\n * (presentation-0) — the media-time base value of the coordinate model, peer\n * to `startTime` (presentation). Derived from the container\n * (`tfdt.baseMediaDecodeTime ÷ mdhd.timescale`); the relocation offset is\n * `−startMediaTime` (targets presentation-0 — `Track.startTime` plays no\n * part), never stored.\n *\n * Optional: `undefined` means not yet established, or never establishable\n * (e.g. text tracks carry no container origin); a near-zero/native origin is\n * established as `0` (no relocation). Established once per source by the\n * `establishStartMediaTime` reactor. See\n * `internal/design/spf/presentation-timeline-model.md`.\n */\n startMediaTime?: number;\n /**\n * Wall-clock time (epoch seconds) at the track's timeline origin\n * (presentation-0) — pure playlist arithmetic over any PDT-bearing segment:\n * `segment.startDate − segment.startTime`, invariant along a linear\n * timeline. Optional: absent when no segment carries `startDate`.\n *\n * The wall-clock member of the coordinate triple (peer to `startTime` and\n * `startMediaTime`). For live it is the anchor segments are PDT-placed\n * against on every parse; equal `startDate` across tracks marks the same\n * presentation instant. See\n * `internal/design/spf/live-presentation-timeline-model.md`.\n */\n startDate?: number;\n };\n\n/**\n * Per-track-type origin-establishment data, accumulated across appends (the media\n * track's `track_id` + `mdhd` timescale from the init, `tfdt` baseMediaDecodeTime of\n * that same track from the first media segment) — hence optional. The transient input\n * the `establishStartMediaTime` reactor reduces into `Track.startMediaTime`.\n *\n * `trackId` is the ISO-BMFF `track_ID` of the buffered media track (`vide`/`soun`),\n * read from the init's `tkhd`; it ties the timescale to the *same* track's\n * `baseMediaDecodeTime` (matched via `tfhd.track_id`) so a muxed segment carrying a\n * second track (e.g. `clcp` captions) reads the right `tfdt` rather than the first one.\n *\n * `segmentStartTime` is the 0-based presentation start of the segment\n * `baseMediaDecodeTime` was read from — *not* a container value (it's the playlist\n * position), but co-located because the origin is `baseMediaDecodeTime/timescale −\n * segmentStartTime`: the first *loaded* segment isn't necessarily the 0th (a\n * non-zero initial `currentTime`, or live/DVR), so the decode time alone isn't the\n * stream origin.\n */\nexport interface MediaContainerData {\n trackId?: number;\n timescale?: number;\n baseMediaDecodeTime?: number;\n segmentStartTime?: number;\n}\n\n/**\n * Raw media-segment bytes — a complete buffer or a byte stream. The transport-neutral\n * payload the loader pipeline carries; `AppendData` (the MSE `SourceBuffer` append\n * input in `media/dom/mse`) is an alias of this at the DOM boundary.\n */\nexport type SegmentData = ArrayBuffer | AsyncIterable<Uint8Array>;\n\n/**\n * Resolved video track with segments.\n */\nexport type VideoTrack = Track &\n Required<Pick<Track, 'initialization' | 'codecs'>> & {\n type: 'video';\n\n // Optional metadata from multivariant (per HLS spec)\n width?: number;\n height?: number;\n frameRate?: FrameRate;\n /**\n * Audio groups (`EXT-X-STREAM-INF:AUDIO`) this video rendition can pair\n * with. A list because one rendition is typically listed across multiple\n * `EXT-X-STREAM-INF` entries — one per audio group (the HLS cross-product) —\n * which the parser collapses into a single track carrying every group it\n * advertised.\n */\n audioGroupIds?: string[];\n };\n\n/**\n * Resolved audio track with segments.\n */\nexport type AudioTrack = Track &\n Required<Pick<Track, 'initialization' | 'codecs'>> & {\n type: 'audio';\n groupId: string;\n name: string;\n sampleRate: number;\n channels: number;\n default?: boolean;\n autoselect?: boolean;\n };\n\n/**\n * Resolved text track with segments.\n */\nexport type TextTrack = Track & {\n type: 'text';\n groupId: string;\n label: string;\n kind: 'subtitles' | 'captions';\n default?: boolean;\n autoselect?: boolean;\n forced?: boolean;\n};\n\n/**\n * Predicate that answers \"can this environment decode this track?\" — the\n * capability-probing surface, read by the track-switching hard-constraint\n * pre-pass (`excludeUnplayableTracks`) to drop undecodable renditions before\n * selection. Kept DOM-free here (a plain function type over a minimal track\n * shape) so DOM-free behaviors can consume it; the DOM implementation\n * (`canPlayTrack` in `media/dom/capabilities.ts`) wraps\n * `MediaSource.isTypeSupported`.\n *\n * Takes the minimal codec-bearing shape both video and audio candidates\n * carry. `mimeType` is optional so unprobeable candidates (no MIME) can be\n * passed straight through as playable rather than dropped.\n */\nexport type CanPlayTrack = (track: {\n mimeType?: string;\n codecs?: string[];\n metadata?: Record<string, unknown>;\n}) => boolean;\n\n/**\n * Minimal text-track cue shape — start time, end time, and display text.\n *\n * Host-agnostic representation. `VTTCue` structurally satisfies this\n * interface, so DOM consumers pass `VTTCue` values directly. Non-DOM\n * hosts (workers, test fakes, non-browser engines) can satisfy the same\n * shape without pulling in DOM types.\n */\nexport interface Cue {\n startTime: number;\n endTime: number;\n text: string;\n}\n\n/**\n * Media element with an iterable text-track list, host-agnostic.\n *\n * Extends `MediaElementLike` with the minimum surface needed to observe\n * which text tracks are currently mounted on the media. `HTMLMediaElement`\n * structurally satisfies this (its `textTracks` is a `TextTrackList`,\n * which is iterable with `{ id }` items).\n */\nexport interface MediaElementWithTextTracks extends MediaElementLike {\n readonly textTracks: Iterable<{ readonly id: string }>;\n}\n\n/**\n * Partially resolved text track from multivariant playlist.\n * Has metadata but no segments or initialization yet (media playlist not fetched).\n */\nexport type PartiallyResolvedTextTrack = PartiallyResolved<TextTrack>;\n\n/**\n * Union of all resolved track types.\n */\nexport type ResolvedTrack = VideoTrack | AudioTrack | TextTrack;\n\n/**\n * Union of all partially resolved track types.\n */\nexport type PartiallyResolvedTrack =\n | PartiallyResolvedVideoTrack\n | PartiallyResolvedAudioTrack\n | PartiallyResolvedTextTrack;\n\n// =============================================================================\n// Switching and Selection Sets\n// =============================================================================\n\n/**\n * Generic switching set type.\n * A group of tracks that can be switched between seamlessly.\n *\n * @param T - Track type (VideoTrack, AudioTrack, or TextTrack)\n */\nexport type SwitchingSetOf<T extends Track = Track> = Ham & {\n type: T['type'];\n tracks: (PartiallyResolved<T> | T)[];\n};\n\n/**\n * Video switching set - contains only video tracks (partially resolved or fully resolved).\n */\nexport type VideoSwitchingSet = SwitchingSetOf<VideoTrack>;\n\n/**\n * Audio switching set - contains only audio tracks (partially resolved or fully resolved).\n */\nexport type AudioSwitchingSet = SwitchingSetOf<AudioTrack>;\n\n/**\n * Text switching set - contains only text tracks (partially resolved or fully resolved).\n */\nexport type TextSwitchingSet = SwitchingSetOf<TextTrack>;\n\n/**\n * Switching set - a group of tracks that can be switched between seamlessly.\n * Discriminated by track type.\n */\nexport type SwitchingSet = VideoSwitchingSet | AudioSwitchingSet | TextSwitchingSet;\n\n/**\n * Generic selection set type.\n * Groups switching sets by track type.\n *\n * @param T - Track type (VideoTrack, AudioTrack, or TextTrack)\n */\nexport type SelectionSetOf<T extends Track = Track> = Ham & {\n type: T['type'];\n switchingSets: SwitchingSetOf<T>[];\n};\n\n/**\n * Video selection set - contains only video switching sets.\n */\nexport type VideoSelectionSet = SelectionSetOf<VideoTrack>;\n\n/**\n * Audio selection set - contains only audio switching sets.\n */\nexport type AudioSelectionSet = SelectionSetOf<AudioTrack>;\n\n/**\n * Text selection set - contains only text switching sets.\n */\nexport type TextSelectionSet = SelectionSetOf<TextTrack>;\n\n/**\n * Selection set - groups switching sets by track type.\n * Discriminated union ensures type-safe track access.\n */\nexport type SelectionSet = VideoSelectionSet | AudioSelectionSet | TextSelectionSet;\n\n// =============================================================================\n// Segment\n// =============================================================================\n\n/**\n * Media segment with timing information.\n * Follows CMAF-HAM composition pattern.\n *\n * `startDate` is the absolute wall-clock time of the segment's first\n * sample, in **epoch seconds** (unit-consistent with `startTime`/`duration`),\n * derived from `#EXT-X-PROGRAM-DATE-TIME` (explicit or interpolated forward via\n * `EXTINF`). Unlike the per-track-relative `startTime`, it is comparable across\n * tracks, so it is the cross-track sync anchor for demuxed audio/video and the\n * exact recovery value on a full live-window turnover. Optional: absent when the\n * source carries no PDT (allowed by RFC 8216, required by Apple's HLS authoring\n * spec — so present on conformant content).\n */\nexport type Segment = Ham & AddressableObject & TimeSpan & { startDate?: number };\n\n/**\n * Floating-point tolerance for matching segments by `startTime`. Two\n * segments are considered the same position when\n * `Math.abs(a.startTime - b.startTime) < SEGMENT_TIME_EPSILON`. Used by\n * the source-buffer dedup and segment-loader quality-aware filter to\n * tolerate sub-millisecond drift in segment timestamps across multiple\n * playlists / quality levels.\n */\nexport const SEGMENT_TIME_EPSILON = 0.0001;\n\n// =============================================================================\n// Media Playlist Metadata\n// =============================================================================\n\n/**\n * Playlist-level metadata surfaced from a parsed media playlist. HLS delivery\n * specifics — not part of the generic CMAF-HAM model — so they live under\n * `Ham.metadata` (read via `getMediaPlaylistMetadata`) rather than as\n * first-class `Track` fields:\n *\n * - `targetDuration` (`#EXT-X-TARGETDURATION`) — reload-cadence basis.\n * - `mediaSequence` (`#EXT-X-MEDIA-SEQUENCE`, default 0) — sequence number of\n * `segments[0]`; the join key for merging successive reload snapshots.\n * - `playlistType` (`#EXT-X-PLAYLIST-TYPE`) — `VOD` / `EVENT` / undefined.\n * - `endList` (`#EXT-X-ENDLIST`) — playlist is complete; stop reloading.\n */\nexport interface MediaPlaylistMetadata {\n targetDuration: number;\n mediaSequence: number;\n playlistType?: 'VOD' | 'EVENT';\n endList: boolean;\n /**\n * Whether this rendition carries encrypted segments — any `#EXT-X-KEY` whose\n * `METHOD` isn't `NONE`. Detection only: enough to tell that playback needs\n * decryption support, not enough to perform it.\n *\n * Deliberately *not* a model-level `protection` shape. CMAF-HAM puts\n * `protection` on `SwitchingSet`, but that can't express two real cases: a\n * clear lead (`METHOD=NONE` segments followed by encrypted ones — protection\n * varies along the timeline within one rendition) or key rotation (its single\n * `defaultKid` can't represent a key changing over time). Modeling it properly\n * belongs to DRM support; until then this records the one fact a playlist\n * reliably gives us. Per-rendition because that's HLS's granularity —\n * `EXT-X-KEY` is a media-playlist tag.\n *\n * Conservative for a clear lead: a rendition whose opening segments are clear\n * still reads as encrypted, so it's judged unplayable rather than played until\n * it breaks.\n */\n encrypted?: boolean;\n /**\n * `EXT-X-SERVER-CONTROL` `HOLD-BACK` (seconds) — the server's declared distance\n * from the live edge for clients playing *complete* segments. Absent when the\n * server doesn't advertise it, in which case the spec default (3 × target\n * duration) applies. Deliberately HLS vocabulary living in the playlist\n * metadata rather than on `Track`: whether a wall-clock holdback generalizes\n * across delivery formats is unresolved.\n *\n * `PART-HOLD-BACK` is **not** captured — it only applies to clients playing\n * partial segments, and using it while fetching whole segments would put the\n * playhead ahead of the last complete segment. Add it with LL-HLS support.\n */\n holdBack?: number;\n /**\n * Whether the server is delivering this rendition as Low-Latency HLS — any of\n * `#EXT-X-PART`, `#EXT-X-PART-INF`, or `EXT-X-SERVER-CONTROL`'s\n * `PART-HOLD-BACK`.\n *\n * Detection only, and deliberately so: partial segments are ignored by the\n * parser and the loader fetches whole segments, so an LL-HLS playlist plays as\n * standard live at standard latency. Recording the fact is what lets a\n * composition *say* that rather than silently under-delivering the latency the\n * publisher configured.\n */\n lowLatency?: boolean;\n}\n\n/** Key under `Ham.metadata` where {@link MediaPlaylistMetadata} is stored. */\nexport const MEDIA_PLAYLIST_METADATA_KEY = 'mediaPlaylist';\n\n/** Typed read of the media-playlist metadata stashed in `ham.metadata`. */\nexport function getMediaPlaylistMetadata(ham: Pick<Ham, 'metadata'>): MediaPlaylistMetadata | undefined {\n return ham.metadata?.[MEDIA_PLAYLIST_METADATA_KEY] as MediaPlaylistMetadata | undefined;\n}\n\n// =============================================================================\n// Stream Type\n// =============================================================================\n\n/**\n * The source's semantic nature — live vs on-demand. A model concept\n * (consumer-facing), distinct from completeness / duration: a live stream that\n * has *ended* is still `'live'`.\n */\nexport type StreamType = 'live' | 'on-demand';\n\n/**\n * Derive {@link StreamType} from a media playlist's metadata. Per the model,\n * only `#EXT-X-PLAYLIST-TYPE:VOD` marks on-demand; everything else (EVENT, or\n * the tag absent) is live — completeness (`endList`) never factors in.\n */\nexport function deriveStreamType(metadata: MediaPlaylistMetadata | undefined): StreamType {\n return metadata?.playlistType === 'VOD' ? 'on-demand' : 'live';\n}\n\n// =============================================================================\n// Media Playlist Info\n// =============================================================================\n\n/**\n * Intermediate representation of a parsed media playlist.\n * Used internally before assembling into full Track structure.\n */\nexport interface MediaPlaylistInfo {\n version: number;\n targetDuration: number;\n playlistType: 'VOD' | 'EVENT' | undefined;\n initSegment: AddressableObject | null;\n segments: Segment[];\n duration: number;\n endList: boolean;\n}\n\n// =============================================================================\n// Presentation\n// =============================================================================\n\n/**\n * Presentation - a single playable period of content.\n * Uses TimeSpan fields (startTime always 0, duration optional until track resolved).\n *\n * Extends AddressableObject so `url` contains the original manifest URL.\n * All URLs are fully qualified (parsers resolve relative URLs).\n */\nexport type Presentation = Ham &\n AddressableObject &\n Partial<TimeSpan> & {\n selectionSets: SelectionSet[];\n /**\n * Live vs on-demand — the source's semantic nature. Populated once a media\n * playlist is parsed (derived from `#EXT-X-PLAYLIST-TYPE` via\n * `deriveStreamType`); orthogonal to duration / completeness.\n */\n streamType?: StreamType;\n };\n\n/**\n * State-shaped presentation that may or may not be resolved yet.\n *\n * The lifecycle is a single value: a caller writes `{ url }`, and the\n * resolver populates the rest in place. `url` is always present; resolved\n * fields (`id`, `selectionSets`, duration) appear once parsing succeeds.\n *\n * Use `isResolvedPresentation` to narrow to `Presentation`.\n */\nexport type MaybeResolvedPresentation = AddressableObject & Partial<Omit<Presentation, keyof AddressableObject>>;\n\n// =============================================================================\n// Type Guards\n// =============================================================================\n\n/**\n * Check if a track is resolved (has segments).\n * Works for all track types with overloaded signatures for type narrowing.\n */\nexport function isResolvedTrack(track: PartiallyResolvedVideoTrack | VideoTrack): track is VideoTrack;\nexport function isResolvedTrack(track: PartiallyResolvedAudioTrack | AudioTrack): track is AudioTrack;\nexport function isResolvedTrack(track: PartiallyResolvedTextTrack | TextTrack): track is TextTrack;\nexport function isResolvedTrack(track: PartiallyResolvedTrack | ResolvedTrack): track is ResolvedTrack;\nexport function isResolvedTrack(track: PartiallyResolvedTrack | ResolvedTrack): track is ResolvedTrack {\n return 'segments' in track;\n}\n\n/**\n * Check if a presentation has duration (at least one track resolved).\n * Narrows type to include required duration.\n */\nexport function hasPresentationDuration(\n presentation: MaybeResolvedPresentation\n): presentation is MaybeResolvedPresentation & { duration: number } {\n return presentation.duration !== undefined;\n}\n\n/**\n * Narrows a `MaybeResolvedPresentation` to a fully resolved `Presentation`.\n *\n * A presentation is resolved once `resolvePresentation` has parsed the\n * manifest and populated both `id` and `selectionSets`. Both must be\n * present — a partial value with only one of them isn't usable, and\n * letting it through would have downstream behaviors crash when they\n * access `selectionSets`.\n */\nexport function isResolvedPresentation(\n presentation: MaybeResolvedPresentation | undefined\n): presentation is Presentation {\n return presentation !== undefined && presentation.id !== undefined && presentation.selectionSets !== undefined;\n}\n"],"mappings":";;;;;;;;;AA2ZA,MAAa,uBAAuB;;AAsEpC,MAAa,8BAA8B;;AAG3C,SAAgB,yBAAyB,KAA+D;CACtG,OAAO,IAAI,WAAW;AACxB;;;;;;AAkBA,SAAgB,iBAAiB,UAAyD;CACxF,OAAO,UAAU,iBAAiB,QAAQ,cAAc;AAC1D;AAkEA,SAAgB,gBAAgB,OAAuE;CACrG,OAAO,cAAc;AACvB;;;;;AAMA,SAAgB,wBACd,cACkE;CAClE,OAAO,aAAa,aAAa,KAAA;AACnC;;;;;;;;;;AAWA,SAAgB,uBACd,cAC8B;CAC9B,OAAO,iBAAiB,KAAA,KAAa,aAAa,OAAO,KAAA,KAAa,aAAa,kBAAkB,KAAA;AACvG"}
@@ -91,6 +91,20 @@ function hasCodecs(track) {
91
91
  * for mixed-container sources (e.g. muxed-TS video + raw-`.aac` audio) and races
92
92
  * concurrent per-type resolution. Same-type writes are disjoint and safe.
93
93
  * Idempotent — tracks already at `mimeType` are left as-is.
94
+ *
95
+ * **Assumes one container per type, which the HLS specs don't guarantee.** Apple's
96
+ * HLS Authoring Specification not only permits a mixed ladder, it produces one:
97
+ * §1.5 requires HEVC in fMP4, while §9.22 says a 192 kbit/s H.264 variant
98
+ * *packaged in a transport stream* SHOULD be provided for cellular — so a
99
+ * conformant HEVC ladder with that fallback is necessarily mixed. RFC 8216 is
100
+ * silent too (§6.2.4 constrains variant *content*, not container).
101
+ *
102
+ * Where that happens, resolving the TS variant first relabels the whole type,
103
+ * capability probing prunes all of it, and a source whose fMP4 variants were fine
104
+ * reports as unplayable. Accepted because the current target is CMAF-compliant
105
+ * single-container delivery (plus Apple's official fMP4 example); revisit by
106
+ * narrowing the relabel to the resolved track, or gating propagation on config,
107
+ * if mixed ladders come into scope.
94
108
  */
95
109
  function applyContainerMimeType(presentation, type, mimeType) {
96
110
  return {
@@ -1 +1 @@
1
- {"version":3,"file":"tracks.js","names":[],"sources":["../../../../src/media/utils/tracks.ts"],"sourcesContent":["import type {\n AudioTrack,\n MaybeResolvedPresentation,\n PartiallyResolvedTrack,\n Presentation,\n ResolvedTrack,\n TextTrack,\n TrackType,\n VideoTrack,\n} from '../types';\nimport { isResolvedTrack } from '../types';\n\n/**\n * Get the tracks of the given type from a presentation's first switching set.\n *\n * Returns `[]` when the presentation is unresolved, when no selection set of\n * `type` exists, or when its first switching set is empty. Returned tracks may\n * be partially resolved (URL only) or fully resolved (with segments) — callers\n * narrow as needed.\n *\n * The \"first switching set\" assumption matches the rest of the codebase\n * (HLS typically has one switching set per type); multi-group / multi-period\n * support would generalize this.\n */\nexport function getTracksByType(\n presentation: MaybeResolvedPresentation,\n type: TrackType\n): readonly (PartiallyResolvedTrack | ResolvedTrack)[] {\n return presentation.selectionSets?.find(({ type: t }) => t === type)?.switchingSets[0]?.tracks ?? [];\n}\n\n/**\n * Find a track of the given type and id within a presentation.\n *\n * Returns the matching track from the first switching set of the matching\n * selection set, or `undefined` if either is missing. The returned track may\n * be partially resolved (URL only) or fully resolved (with segments) — callers\n * narrow as needed.\n */\nexport function findTrack(\n presentation: MaybeResolvedPresentation,\n type: TrackType,\n trackId: string\n): PartiallyResolvedTrack | ResolvedTrack | undefined {\n return getTracksByType(presentation, type).find(({ id }) => id === trackId);\n}\n\n/**\n * Find a track by id across all selection sets in a presentation, without\n * knowing its type up front. Used when the caller has a track id obtained\n * from a downstream consumer (e.g. `SourceBufferActor.initTrackId`) and\n * needs to locate the corresponding track in the presentation.\n *\n * Track ids are unique within a presentation per the HLS spec; the first\n * match wins.\n */\nexport function findTrackById(\n presentation: MaybeResolvedPresentation,\n trackId: string\n): PartiallyResolvedTrack | ResolvedTrack | undefined {\n for (const selectionSet of presentation.selectionSets ?? []) {\n const track = selectionSet.switchingSets[0]?.tracks.find(({ id }) => id === trackId);\n if (track) return track;\n }\n return undefined;\n}\n\n/**\n * Find a text track of the given id within a presentation and narrow it to\n * the fully-resolved `TextTrack` shape (segments populated). Returns\n * `undefined` if no track matches the id, the matching track isn't a text\n * track, or it hasn't been resolved yet.\n *\n * The segments-non-empty check stays at the call site — a resolved track\n * with zero segments is a valid state, distinct from \"ready to load.\"\n */\nexport function findResolvedTextTrack(\n presentation: MaybeResolvedPresentation | undefined,\n trackId: string | undefined\n): TextTrack | undefined {\n if (!presentation || !trackId) return undefined;\n const track = findTrack(presentation, 'text', trackId);\n // `findTrack` returns the wide union; narrow via discriminant before\n // applying `isResolvedTrack`'s text-specific overload.\n if (track?.type !== 'text' || !isResolvedTrack(track)) return undefined;\n return track;\n}\n\nexport function findResolvedVideoTrack(\n presentation: MaybeResolvedPresentation | undefined,\n trackId: string | undefined\n): VideoTrack | undefined {\n if (!presentation || !trackId) return undefined;\n const track = findTrack(presentation, 'video', trackId);\n if (track?.type !== 'video' || !isResolvedTrack(track)) return undefined;\n return track;\n}\n\nexport function findResolvedAudioTrack(\n presentation: MaybeResolvedPresentation | undefined,\n trackId: string | undefined\n): AudioTrack | undefined {\n if (!presentation || !trackId) return undefined;\n const track = findTrack(presentation, 'audio', trackId);\n if (track?.type !== 'audio' || !isResolvedTrack(track)) return undefined;\n return track;\n}\n\n/**\n * Whether a track carries a non-empty `codecs` array. Both partially-\n * resolved and fully-resolved tracks may carry codecs — they come from\n * the multivariant playlist's `EXT-X-STREAM-INF` line, not from the\n * per-type media playlist — so this works at either resolution stage.\n *\n * `TextTrack` doesn't declare a `codecs` field; the `'codecs' in track`\n * check narrows it out for the false branch.\n */\nexport function hasCodecs(track: PartiallyResolvedTrack | ResolvedTrack | undefined): boolean {\n return !!track && 'codecs' in track && !!track.codecs?.length;\n}\n\n/**\n * Set `mimeType` on every track of one `type` (immutably). Used to propagate a\n * detected container across a type's renditions: an ABR ladder is the same\n * content at different bitrates, so one rendition's container holds for all of\n * them — capability probing + SourceBuffer setup then get the right MIME for the\n * whole type from a single resolved media playlist, without fetching the rest.\n *\n * Scoped to one type on purpose: propagating *across* audio/video would be wrong\n * for mixed-container sources (e.g. muxed-TS video + raw-`.aac` audio) and races\n * concurrent per-type resolution. Same-type writes are disjoint and safe.\n * Idempotent — tracks already at `mimeType` are left as-is.\n */\nexport function applyContainerMimeType(presentation: Presentation, type: TrackType, mimeType: string): Presentation {\n return {\n ...presentation,\n selectionSets: presentation.selectionSets.map((selectionSet) =>\n selectionSet.type === type\n ? {\n ...selectionSet,\n switchingSets: selectionSet.switchingSets.map((switchingSet) => ({\n ...switchingSet,\n tracks: switchingSet.tracks.map((track) =>\n track.mimeType === mimeType ? track : { ...track, mimeType }\n ),\n })),\n }\n : selectionSet\n ),\n } as Presentation;\n}\n\n/**\n * Updates a track within a presentation (immutably). Generic — works for\n * video, audio, or text tracks.\n */\nexport function updateTrackInPresentation<T extends ResolvedTrack>(\n presentation: Presentation,\n resolvedTrack: T\n): Presentation {\n const trackId = resolvedTrack.id;\n return {\n ...presentation,\n selectionSets: presentation.selectionSets.map((selectionSet) => ({\n ...selectionSet,\n switchingSets: selectionSet.switchingSets.map((switchingSet) => ({\n ...switchingSet,\n tracks: switchingSet.tracks.map((track) => (track.id === trackId ? resolvedTrack : track)),\n })),\n })),\n } as Presentation;\n}\n"],"mappings":";;;;;;;;;;;;;;AAwBA,SAAgB,gBACd,cACA,MACqD;CACrD,OAAO,aAAa,eAAe,MAAM,EAAE,MAAM,QAAQ,MAAM,IAAI,CAAC,EAAE,cAAc,EAAE,EAAE,UAAU,CAAC;AACrG;;;;;;;;;AAUA,SAAgB,UACd,cACA,MACA,SACoD;CACpD,OAAO,gBAAgB,cAAc,IAAI,CAAC,CAAC,MAAM,EAAE,SAAS,OAAO,OAAO;AAC5E;;;;;;;;;;AAWA,SAAgB,cACd,cACA,SACoD;CACpD,KAAK,MAAM,gBAAgB,aAAa,iBAAiB,CAAC,GAAG;EAC3D,MAAM,QAAQ,aAAa,cAAc,EAAE,EAAE,OAAO,MAAM,EAAE,SAAS,OAAO,OAAO;EACnF,IAAI,OAAO,OAAO;CACpB;AAEF;;;;;;;;;;AAWA,SAAgB,sBACd,cACA,SACuB;CACvB,IAAI,CAAC,gBAAgB,CAAC,SAAS,OAAO,KAAA;CACtC,MAAM,QAAQ,UAAU,cAAc,QAAQ,OAAO;CAGrD,IAAI,OAAO,SAAS,UAAU,CAAC,gBAAgB,KAAK,GAAG,OAAO,KAAA;CAC9D,OAAO;AACT;AAEA,SAAgB,uBACd,cACA,SACwB;CACxB,IAAI,CAAC,gBAAgB,CAAC,SAAS,OAAO,KAAA;CACtC,MAAM,QAAQ,UAAU,cAAc,SAAS,OAAO;CACtD,IAAI,OAAO,SAAS,WAAW,CAAC,gBAAgB,KAAK,GAAG,OAAO,KAAA;CAC/D,OAAO;AACT;AAEA,SAAgB,uBACd,cACA,SACwB;CACxB,IAAI,CAAC,gBAAgB,CAAC,SAAS,OAAO,KAAA;CACtC,MAAM,QAAQ,UAAU,cAAc,SAAS,OAAO;CACtD,IAAI,OAAO,SAAS,WAAW,CAAC,gBAAgB,KAAK,GAAG,OAAO,KAAA;CAC/D,OAAO;AACT;;;;;;;;;;AAWA,SAAgB,UAAU,OAAoE;CAC5F,OAAO,CAAC,CAAC,SAAS,YAAY,SAAS,CAAC,CAAC,MAAM,QAAQ;AACzD;;;;;;;;;;;;;AAcA,SAAgB,uBAAuB,cAA4B,MAAiB,UAAgC;CAClH,OAAO;EACL,GAAG;EACH,eAAe,aAAa,cAAc,KAAK,iBAC7C,aAAa,SAAS,OAClB;GACE,GAAG;GACH,eAAe,aAAa,cAAc,KAAK,kBAAkB;IAC/D,GAAG;IACH,QAAQ,aAAa,OAAO,KAAK,UAC/B,MAAM,aAAa,WAAW,QAAQ;KAAE,GAAG;KAAO;IAAS,CAC7D;GACF,EAAE;EACJ,IACA,YACN;CACF;AACF;;;;;AAMA,SAAgB,0BACd,cACA,eACc;CACd,MAAM,UAAU,cAAc;CAC9B,OAAO;EACL,GAAG;EACH,eAAe,aAAa,cAAc,KAAK,kBAAkB;GAC/D,GAAG;GACH,eAAe,aAAa,cAAc,KAAK,kBAAkB;IAC/D,GAAG;IACH,QAAQ,aAAa,OAAO,KAAK,UAAW,MAAM,OAAO,UAAU,gBAAgB,KAAM;GAC3F,EAAE;EACJ,EAAE;CACJ;AACF"}
1
+ {"version":3,"file":"tracks.js","names":[],"sources":["../../../../src/media/utils/tracks.ts"],"sourcesContent":["import type {\n AudioTrack,\n MaybeResolvedPresentation,\n PartiallyResolvedTrack,\n Presentation,\n ResolvedTrack,\n TextTrack,\n TrackType,\n VideoTrack,\n} from '../types';\nimport { isResolvedTrack } from '../types';\n\n/**\n * Get the tracks of the given type from a presentation's first switching set.\n *\n * Returns `[]` when the presentation is unresolved, when no selection set of\n * `type` exists, or when its first switching set is empty. Returned tracks may\n * be partially resolved (URL only) or fully resolved (with segments) — callers\n * narrow as needed.\n *\n * The \"first switching set\" assumption matches the rest of the codebase\n * (HLS typically has one switching set per type); multi-group / multi-period\n * support would generalize this.\n */\nexport function getTracksByType(\n presentation: MaybeResolvedPresentation,\n type: TrackType\n): readonly (PartiallyResolvedTrack | ResolvedTrack)[] {\n return presentation.selectionSets?.find(({ type: t }) => t === type)?.switchingSets[0]?.tracks ?? [];\n}\n\n/**\n * Find a track of the given type and id within a presentation.\n *\n * Returns the matching track from the first switching set of the matching\n * selection set, or `undefined` if either is missing. The returned track may\n * be partially resolved (URL only) or fully resolved (with segments) — callers\n * narrow as needed.\n */\nexport function findTrack(\n presentation: MaybeResolvedPresentation,\n type: TrackType,\n trackId: string\n): PartiallyResolvedTrack | ResolvedTrack | undefined {\n return getTracksByType(presentation, type).find(({ id }) => id === trackId);\n}\n\n/**\n * Find a track by id across all selection sets in a presentation, without\n * knowing its type up front. Used when the caller has a track id obtained\n * from a downstream consumer (e.g. `SourceBufferActor.initTrackId`) and\n * needs to locate the corresponding track in the presentation.\n *\n * Track ids are unique within a presentation per the HLS spec; the first\n * match wins.\n */\nexport function findTrackById(\n presentation: MaybeResolvedPresentation,\n trackId: string\n): PartiallyResolvedTrack | ResolvedTrack | undefined {\n for (const selectionSet of presentation.selectionSets ?? []) {\n const track = selectionSet.switchingSets[0]?.tracks.find(({ id }) => id === trackId);\n if (track) return track;\n }\n return undefined;\n}\n\n/**\n * Find a text track of the given id within a presentation and narrow it to\n * the fully-resolved `TextTrack` shape (segments populated). Returns\n * `undefined` if no track matches the id, the matching track isn't a text\n * track, or it hasn't been resolved yet.\n *\n * The segments-non-empty check stays at the call site — a resolved track\n * with zero segments is a valid state, distinct from \"ready to load.\"\n */\nexport function findResolvedTextTrack(\n presentation: MaybeResolvedPresentation | undefined,\n trackId: string | undefined\n): TextTrack | undefined {\n if (!presentation || !trackId) return undefined;\n const track = findTrack(presentation, 'text', trackId);\n // `findTrack` returns the wide union; narrow via discriminant before\n // applying `isResolvedTrack`'s text-specific overload.\n if (track?.type !== 'text' || !isResolvedTrack(track)) return undefined;\n return track;\n}\n\nexport function findResolvedVideoTrack(\n presentation: MaybeResolvedPresentation | undefined,\n trackId: string | undefined\n): VideoTrack | undefined {\n if (!presentation || !trackId) return undefined;\n const track = findTrack(presentation, 'video', trackId);\n if (track?.type !== 'video' || !isResolvedTrack(track)) return undefined;\n return track;\n}\n\nexport function findResolvedAudioTrack(\n presentation: MaybeResolvedPresentation | undefined,\n trackId: string | undefined\n): AudioTrack | undefined {\n if (!presentation || !trackId) return undefined;\n const track = findTrack(presentation, 'audio', trackId);\n if (track?.type !== 'audio' || !isResolvedTrack(track)) return undefined;\n return track;\n}\n\n/**\n * Whether a track carries a non-empty `codecs` array. Both partially-\n * resolved and fully-resolved tracks may carry codecs — they come from\n * the multivariant playlist's `EXT-X-STREAM-INF` line, not from the\n * per-type media playlist — so this works at either resolution stage.\n *\n * `TextTrack` doesn't declare a `codecs` field; the `'codecs' in track`\n * check narrows it out for the false branch.\n */\nexport function hasCodecs(track: PartiallyResolvedTrack | ResolvedTrack | undefined): boolean {\n return !!track && 'codecs' in track && !!track.codecs?.length;\n}\n\n/**\n * Set `mimeType` on every track of one `type` (immutably). Used to propagate a\n * detected container across a type's renditions: an ABR ladder is the same\n * content at different bitrates, so one rendition's container holds for all of\n * them — capability probing + SourceBuffer setup then get the right MIME for the\n * whole type from a single resolved media playlist, without fetching the rest.\n *\n * Scoped to one type on purpose: propagating *across* audio/video would be wrong\n * for mixed-container sources (e.g. muxed-TS video + raw-`.aac` audio) and races\n * concurrent per-type resolution. Same-type writes are disjoint and safe.\n * Idempotent — tracks already at `mimeType` are left as-is.\n *\n * **Assumes one container per type, which the HLS specs don't guarantee.** Apple's\n * HLS Authoring Specification not only permits a mixed ladder, it produces one:\n * §1.5 requires HEVC in fMP4, while §9.22 says a 192 kbit/s H.264 variant\n * *packaged in a transport stream* SHOULD be provided for cellular — so a\n * conformant HEVC ladder with that fallback is necessarily mixed. RFC 8216 is\n * silent too (§6.2.4 constrains variant *content*, not container).\n *\n * Where that happens, resolving the TS variant first relabels the whole type,\n * capability probing prunes all of it, and a source whose fMP4 variants were fine\n * reports as unplayable. Accepted because the current target is CMAF-compliant\n * single-container delivery (plus Apple's official fMP4 example); revisit by\n * narrowing the relabel to the resolved track, or gating propagation on config,\n * if mixed ladders come into scope.\n */\nexport function applyContainerMimeType(presentation: Presentation, type: TrackType, mimeType: string): Presentation {\n return {\n ...presentation,\n selectionSets: presentation.selectionSets.map((selectionSet) =>\n selectionSet.type === type\n ? {\n ...selectionSet,\n switchingSets: selectionSet.switchingSets.map((switchingSet) => ({\n ...switchingSet,\n tracks: switchingSet.tracks.map((track) =>\n track.mimeType === mimeType ? track : { ...track, mimeType }\n ),\n })),\n }\n : selectionSet\n ),\n } as Presentation;\n}\n\n/**\n * Updates a track within a presentation (immutably). Generic — works for\n * video, audio, or text tracks.\n */\nexport function updateTrackInPresentation<T extends ResolvedTrack>(\n presentation: Presentation,\n resolvedTrack: T\n): Presentation {\n const trackId = resolvedTrack.id;\n return {\n ...presentation,\n selectionSets: presentation.selectionSets.map((selectionSet) => ({\n ...selectionSet,\n switchingSets: selectionSet.switchingSets.map((switchingSet) => ({\n ...switchingSet,\n tracks: switchingSet.tracks.map((track) => (track.id === trackId ? resolvedTrack : track)),\n })),\n })),\n } as Presentation;\n}\n"],"mappings":";;;;;;;;;;;;;;AAwBA,SAAgB,gBACd,cACA,MACqD;CACrD,OAAO,aAAa,eAAe,MAAM,EAAE,MAAM,QAAQ,MAAM,IAAI,CAAC,EAAE,cAAc,EAAE,EAAE,UAAU,CAAC;AACrG;;;;;;;;;AAUA,SAAgB,UACd,cACA,MACA,SACoD;CACpD,OAAO,gBAAgB,cAAc,IAAI,CAAC,CAAC,MAAM,EAAE,SAAS,OAAO,OAAO;AAC5E;;;;;;;;;;AAWA,SAAgB,cACd,cACA,SACoD;CACpD,KAAK,MAAM,gBAAgB,aAAa,iBAAiB,CAAC,GAAG;EAC3D,MAAM,QAAQ,aAAa,cAAc,EAAE,EAAE,OAAO,MAAM,EAAE,SAAS,OAAO,OAAO;EACnF,IAAI,OAAO,OAAO;CACpB;AAEF;;;;;;;;;;AAWA,SAAgB,sBACd,cACA,SACuB;CACvB,IAAI,CAAC,gBAAgB,CAAC,SAAS,OAAO,KAAA;CACtC,MAAM,QAAQ,UAAU,cAAc,QAAQ,OAAO;CAGrD,IAAI,OAAO,SAAS,UAAU,CAAC,gBAAgB,KAAK,GAAG,OAAO,KAAA;CAC9D,OAAO;AACT;AAEA,SAAgB,uBACd,cACA,SACwB;CACxB,IAAI,CAAC,gBAAgB,CAAC,SAAS,OAAO,KAAA;CACtC,MAAM,QAAQ,UAAU,cAAc,SAAS,OAAO;CACtD,IAAI,OAAO,SAAS,WAAW,CAAC,gBAAgB,KAAK,GAAG,OAAO,KAAA;CAC/D,OAAO;AACT;AAEA,SAAgB,uBACd,cACA,SACwB;CACxB,IAAI,CAAC,gBAAgB,CAAC,SAAS,OAAO,KAAA;CACtC,MAAM,QAAQ,UAAU,cAAc,SAAS,OAAO;CACtD,IAAI,OAAO,SAAS,WAAW,CAAC,gBAAgB,KAAK,GAAG,OAAO,KAAA;CAC/D,OAAO;AACT;;;;;;;;;;AAWA,SAAgB,UAAU,OAAoE;CAC5F,OAAO,CAAC,CAAC,SAAS,YAAY,SAAS,CAAC,CAAC,MAAM,QAAQ;AACzD;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BA,SAAgB,uBAAuB,cAA4B,MAAiB,UAAgC;CAClH,OAAO;EACL,GAAG;EACH,eAAe,aAAa,cAAc,KAAK,iBAC7C,aAAa,SAAS,OAClB;GACE,GAAG;GACH,eAAe,aAAa,cAAc,KAAK,kBAAkB;IAC/D,GAAG;IACH,QAAQ,aAAa,OAAO,KAAK,UAC/B,MAAM,aAAa,WAAW,QAAQ;KAAE,GAAG;KAAO;IAAS,CAC7D;GACF,EAAE;EACJ,IACA,YACN;CACF;AACF;;;;;AAMA,SAAgB,0BACd,cACA,eACc;CACd,MAAM,UAAU,cAAc;CAC9B,OAAO;EACL,GAAG;EACH,eAAe,aAAa,cAAc,KAAK,kBAAkB;GAC/D,GAAG;GACH,eAAe,aAAa,cAAc,KAAK,kBAAkB;IAC/D,GAAG;IACH,QAAQ,aAAa,OAAO,KAAK,UAAW,MAAM,OAAO,UAAU,gBAAgB,KAAM;GAC3F,EAAE;EACJ,EAAE;CACJ;AACF"}
@@ -0,0 +1,4 @@
1
+ import { MuxMediaAPI, MuxMediaProps, muxMediaDefaultProps } from "./playback/adapters/mux-video/adapter.js";
2
+ import { MuxAudioMedia } from "./playback/adapters/mux-audio/media.js";
3
+ import { MuxContentData, MuxSourceBase } from "@videojs/media/dom/mux/source";
4
+ export { MuxAudioMedia, type MuxContentData, type MuxMediaAPI, type MuxMediaProps, type MuxSourceBase, muxMediaDefaultProps };
@@ -0,0 +1,3 @@
1
+ import { muxMediaDefaultProps } from "./playback/adapters/mux-video/adapter.js";
2
+ import { MuxAudioMedia } from "./playback/adapters/mux-audio/media.js";
3
+ export { MuxAudioMedia, muxMediaDefaultProps };
@@ -0,0 +1,3 @@
1
+ import { HlsBackgroundVideoMediaAPI, HlsBackgroundVideoMediaElement, HlsBackgroundVideoMediaMixin, HlsBackgroundVideoMediaProps, hlsBackgroundVideoMediaDefaultProps } from "./playback/adapters/hls-background-video/adapter.js";
2
+ import { HlsBackgroundVideoMedia } from "./playback/adapters/hls-background-video/media.js";
3
+ export { HlsBackgroundVideoMedia as MuxBackgroundVideoMedia, type HlsBackgroundVideoMediaAPI as MuxBackgroundVideoMediaAPI, HlsBackgroundVideoMediaElement as MuxBackgroundVideoMediaElement, HlsBackgroundVideoMediaMixin as MuxBackgroundVideoMediaMixin, type HlsBackgroundVideoMediaProps as MuxBackgroundVideoMediaProps, hlsBackgroundVideoMediaDefaultProps as muxBackgroundVideoMediaDefaultProps };
@@ -0,0 +1,3 @@
1
+ import { HlsBackgroundVideoMediaElement, HlsBackgroundVideoMediaMixin, hlsBackgroundVideoMediaDefaultProps } from "./playback/adapters/hls-background-video/adapter.js";
2
+ import { HlsBackgroundVideoMedia } from "./playback/adapters/hls-background-video/media.js";
3
+ export { HlsBackgroundVideoMedia as MuxBackgroundVideoMedia, HlsBackgroundVideoMediaElement as MuxBackgroundVideoMediaElement, HlsBackgroundVideoMediaMixin as MuxBackgroundVideoMediaMixin, hlsBackgroundVideoMediaDefaultProps as muxBackgroundVideoMediaDefaultProps };
@@ -0,0 +1,4 @@
1
+ import { MuxMediaAPI, MuxMediaMixin, MuxMediaProps, muxMediaDefaultProps } from "./playback/adapters/mux-video/adapter.js";
2
+ import { MuxVideoMedia } from "./playback/adapters/mux-video/media.js";
3
+ import { MuxContentData, MuxSourceBase } from "@videojs/media/dom/mux/source";
4
+ export { type MuxContentData, type MuxMediaAPI, MuxMediaMixin, type MuxMediaProps, type MuxSourceBase, MuxVideoMedia, muxMediaDefaultProps };
@@ -0,0 +1,3 @@
1
+ import { MuxMediaMixin, muxMediaDefaultProps } from "./playback/adapters/mux-video/adapter.js";
2
+ import { MuxVideoMedia } from "./playback/adapters/mux-video/media.js";
3
+ export { MuxMediaMixin, MuxVideoMedia, muxMediaDefaultProps };
@@ -0,0 +1,54 @@
1
+ import { Composition } from "../../../core/composition/create-composition.js";
2
+ import { HlsAudioEngineContext, HlsAudioEngineState } from "../../engines/hls/engine-audio-only.js";
3
+ import { HlsVideoMediaError } from "../hls-video/error-surface.js";
4
+ import { Constructor, MixinReturn } from "@videojs/utils/types";
5
+ //#region src/playback/adapters/hls-audio/adapter.d.ts
6
+ interface HlsAudioMediaProps {
7
+ src: string;
8
+ preload: '' | 'none' | 'metadata' | 'auto';
9
+ disableRemotePlayback: boolean;
10
+ }
11
+ declare const hlsAudioMediaDefaultProps: HlsAudioMediaProps;
12
+ interface HlsAudioMediaAPI extends HlsAudioMediaProps {
13
+ readonly engine: Composition<HlsAudioEngineState, HlsAudioEngineContext>;
14
+ readonly error: HlsVideoMediaError | null;
15
+ attach(mediaElement: HTMLMediaElement): void;
16
+ detach(): void;
17
+ destroy(): void;
18
+ play(): Promise<void>;
19
+ }
20
+ /**
21
+ * Mixin that adds SPF audio-only HLS playback to any base class.
22
+ *
23
+ * @fires error - Fired when a fatal condition is reported. Read `error` for it.
24
+ *
25
+ * Parallel to `HlsVideoMediaMixin` with one substantive difference: the
26
+ * underlying engine is the audio-only variant (`createHlsAudioEngine`),
27
+ * which omits video and text-track behaviors. The src / preload /
28
+ * disableRemotePlayback / play() contract per the WHATWG HTML spec is identical
29
+ * to the default adapter.
30
+ *
31
+ * Selecting this adapter is the variant decision: instantiating
32
+ * `HlsAudioMediaElement` opts the consumer into audio-only
33
+ * delivery even when the source is a mixed-AV HLS manifest.
34
+ *
35
+ * @example
36
+ * class HlsAudioMedia extends HlsAudioMediaMixin(HTMLVideoElementHost) {}
37
+ *
38
+ * const media = new HlsAudioMedia();
39
+ * media.attach(document.querySelector('video'));
40
+ * media.src = 'https://stream.mux.com/abc123.m3u8';
41
+ */
42
+ declare function HlsAudioMediaMixin<Base extends Constructor<any>>(BaseClass: Base): MixinReturn<Base, HlsAudioMediaAPI> & {
43
+ readonly alternativeMediaSuggestion: string | undefined;
44
+ };
45
+ declare const HlsAudioMediaElement_base: Constructor<{} & HlsAudioMediaAPI, any[]> & Omit<{
46
+ new (): {};
47
+ }, "prototype"> & {
48
+ readonly alternativeMediaSuggestion: string | undefined;
49
+ };
50
+ /** Standalone SPF audio-only media adapter with no base class. */
51
+ declare class HlsAudioMediaElement extends HlsAudioMediaElement_base {}
52
+ //#endregion
53
+ export { HlsAudioMediaAPI, HlsAudioMediaElement, HlsAudioMediaMixin, HlsAudioMediaProps, hlsAudioMediaDefaultProps };
54
+ //# sourceMappingURL=adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter.d.ts","names":[],"sources":["../../../../../src/playback/adapters/hls-audio/adapter.ts"],"mappings":";;;;;UAuBiB;EACf;EACA;EACA;;cAGW,2BAA2B;UAMvB,yBAAyB;WAC/B,QAAQ,YAAY,qBAAqB;WACzC,OAAO;EAChB,OAAO,cAAc;EACrB;EACA;EACA,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;iBAiCM,mBAAmB,aAAa,kBAAkB,WAAW,OAkOpC,YAAY,MAAM;WAC9C;;;;;WAA4B;;;cAK5B,6BAA6B"}