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

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 (180) hide show
  1. package/dist/default/background-video.js +3 -0
  2. package/dist/default/core/composition/create-composition.js +1 -1
  3. package/dist/default/core/composition/create-composition.js.map +1 -1
  4. package/dist/default/core/composition/share-signals.js +13 -11
  5. package/dist/default/core/composition/share-signals.js.map +1 -1
  6. package/dist/default/hls.js +3 -1
  7. package/dist/default/media/abr/quality-selection.js +6 -68
  8. package/dist/default/media/abr/quality-selection.js.map +1 -1
  9. package/dist/default/media/dom/capabilities.js +70 -0
  10. package/dist/default/media/dom/capabilities.js.map +1 -0
  11. package/dist/default/media/dom/mse/mediasource-setup.js +1 -1
  12. package/dist/default/media/dom/text/text-track-slots.js +3 -3
  13. package/dist/default/media/dom/text/text-track-slots.js.map +1 -1
  14. package/dist/default/media/hls/parse-attributes.js +16 -2
  15. package/dist/default/media/hls/parse-attributes.js.map +1 -1
  16. package/dist/default/media/hls/parse-media-playlist.js +25 -2
  17. package/dist/default/media/hls/parse-media-playlist.js.map +1 -1
  18. package/dist/default/media/hls/parse-multivariant.js +15 -6
  19. package/dist/default/media/hls/parse-multivariant.js.map +1 -1
  20. package/dist/default/media/primitives/select-tracks.js +98 -13
  21. package/dist/default/media/primitives/select-tracks.js.map +1 -1
  22. package/dist/default/media/types/index.js.map +1 -1
  23. package/dist/default/media/utils/cdn.js +56 -0
  24. package/dist/default/media/utils/cdn.js.map +1 -0
  25. package/dist/default/media/utils/tracks.js +28 -1
  26. package/dist/default/media/utils/tracks.js.map +1 -1
  27. package/dist/default/network/fetch.js +7 -1
  28. package/dist/default/network/fetch.js.map +1 -1
  29. package/dist/default/playback/actors/dom/segment-loader.js +42 -11
  30. package/dist/default/playback/actors/dom/segment-loader.js.map +1 -1
  31. package/dist/default/playback/actors/dom/source-buffer.js +2 -1
  32. package/dist/default/playback/actors/dom/source-buffer.js.map +1 -1
  33. package/dist/default/playback/behaviors/calculate-presentation-duration.js +21 -43
  34. package/dist/default/playback/behaviors/calculate-presentation-duration.js.map +1 -1
  35. package/dist/default/playback/behaviors/derive-cdn-priority.js +68 -0
  36. package/dist/default/playback/behaviors/derive-cdn-priority.js.map +1 -0
  37. package/dist/default/playback/behaviors/dom/end-of-stream.js +15 -103
  38. package/dist/default/playback/behaviors/dom/end-of-stream.js.map +1 -1
  39. package/dist/default/playback/behaviors/dom/load-segments.js +1 -1
  40. package/dist/default/playback/behaviors/dom/load-segments.js.map +1 -1
  41. package/dist/default/playback/behaviors/dom/setup-buffer-actors.js +33 -14
  42. package/dist/default/playback/behaviors/dom/setup-buffer-actors.js.map +1 -1
  43. package/dist/default/playback/behaviors/dom/sync-text-tracks.js +43 -18
  44. package/dist/default/playback/behaviors/dom/sync-text-tracks.js.map +1 -1
  45. package/dist/default/playback/behaviors/dom/track-current-time.js +6 -0
  46. package/dist/default/playback/behaviors/dom/track-current-time.js.map +1 -1
  47. package/dist/default/playback/behaviors/dom/update-mediasource-duration.js +1 -1
  48. package/dist/default/playback/behaviors/resolve-track.js +45 -22
  49. package/dist/default/playback/behaviors/resolve-track.js.map +1 -1
  50. package/dist/default/playback/behaviors/select-tracks.js +46 -77
  51. package/dist/default/playback/behaviors/select-tracks.js.map +1 -1
  52. package/dist/default/playback/behaviors/setup-failover-monitor.js +66 -0
  53. package/dist/default/playback/behaviors/setup-failover-monitor.js.map +1 -0
  54. package/dist/default/playback/behaviors/track-switching.js +341 -0
  55. package/dist/default/playback/behaviors/track-switching.js.map +1 -0
  56. package/dist/default/playback/engines/background-video/adapter.js +158 -0
  57. package/dist/default/playback/engines/background-video/adapter.js.map +1 -0
  58. package/dist/default/playback/engines/background-video/engine.js +75 -0
  59. package/dist/default/playback/engines/background-video/engine.js.map +1 -0
  60. package/dist/default/playback/engines/hls/adapter-audio-only.js +119 -0
  61. package/dist/default/playback/engines/hls/adapter-audio-only.js.map +1 -0
  62. package/dist/default/playback/engines/hls/adapter.js +7 -0
  63. package/dist/default/playback/engines/hls/adapter.js.map +1 -1
  64. package/dist/default/playback/engines/hls/engine-audio-only.js +80 -0
  65. package/dist/default/playback/engines/hls/engine-audio-only.js.map +1 -0
  66. package/dist/default/playback/engines/hls/engine.js +20 -7
  67. package/dist/default/playback/engines/hls/engine.js.map +1 -1
  68. package/dist/default/playback/primitives/failover-fetch.js +42 -0
  69. package/dist/default/playback/primitives/failover-fetch.js.map +1 -0
  70. package/dist/{dev/playback/behaviors → default/playback/primitives}/track-types.js +1 -1
  71. package/dist/default/playback/primitives/track-types.js.map +1 -0
  72. package/dist/dev/background-video.d.ts +3 -0
  73. package/dist/dev/background-video.js +3 -0
  74. package/dist/dev/core/composition/create-composition.js +1 -1
  75. package/dist/dev/core/composition/create-composition.js.map +1 -1
  76. package/dist/dev/core/composition/share-signals.d.ts +11 -9
  77. package/dist/dev/core/composition/share-signals.d.ts.map +1 -1
  78. package/dist/dev/core/composition/share-signals.js +13 -11
  79. package/dist/dev/core/composition/share-signals.js.map +1 -1
  80. package/dist/dev/hls.d.ts +3 -1
  81. package/dist/dev/hls.js +3 -1
  82. package/dist/dev/media/abr/quality-selection.js +6 -68
  83. package/dist/dev/media/abr/quality-selection.js.map +1 -1
  84. package/dist/dev/media/dom/capabilities.js +70 -0
  85. package/dist/dev/media/dom/capabilities.js.map +1 -0
  86. package/dist/dev/media/dom/mse/mediasource-setup.js +1 -1
  87. package/dist/dev/media/dom/text/text-track-slots.d.ts.map +1 -1
  88. package/dist/dev/media/dom/text/text-track-slots.js +3 -3
  89. package/dist/dev/media/dom/text/text-track-slots.js.map +1 -1
  90. package/dist/dev/media/hls/parse-attributes.js +16 -2
  91. package/dist/dev/media/hls/parse-attributes.js.map +1 -1
  92. package/dist/dev/media/hls/parse-media-playlist.js +25 -2
  93. package/dist/dev/media/hls/parse-media-playlist.js.map +1 -1
  94. package/dist/dev/media/hls/parse-multivariant.js +15 -6
  95. package/dist/dev/media/hls/parse-multivariant.js.map +1 -1
  96. package/dist/dev/media/primitives/select-tracks.d.ts +36 -0
  97. package/dist/dev/media/primitives/select-tracks.d.ts.map +1 -0
  98. package/dist/dev/media/primitives/select-tracks.js +98 -13
  99. package/dist/dev/media/primitives/select-tracks.js.map +1 -1
  100. package/dist/dev/media/types/index.d.ts +26 -2
  101. package/dist/dev/media/types/index.d.ts.map +1 -1
  102. package/dist/dev/media/types/index.js.map +1 -1
  103. package/dist/dev/media/utils/cdn.d.ts +13 -0
  104. package/dist/dev/media/utils/cdn.d.ts.map +1 -0
  105. package/dist/dev/media/utils/cdn.js +56 -0
  106. package/dist/dev/media/utils/cdn.js.map +1 -0
  107. package/dist/dev/media/utils/tracks.js +28 -1
  108. package/dist/dev/media/utils/tracks.js.map +1 -1
  109. package/dist/dev/network/fetch.js +7 -1
  110. package/dist/dev/network/fetch.js.map +1 -1
  111. package/dist/dev/playback/actors/dom/segment-loader.js +42 -11
  112. package/dist/dev/playback/actors/dom/segment-loader.js.map +1 -1
  113. package/dist/dev/playback/actors/dom/source-buffer.d.ts +15 -0
  114. package/dist/dev/playback/actors/dom/source-buffer.d.ts.map +1 -1
  115. package/dist/dev/playback/actors/dom/source-buffer.js +2 -1
  116. package/dist/dev/playback/actors/dom/source-buffer.js.map +1 -1
  117. package/dist/dev/playback/behaviors/calculate-presentation-duration.d.ts +8 -0
  118. package/dist/dev/playback/behaviors/calculate-presentation-duration.d.ts.map +1 -1
  119. package/dist/dev/playback/behaviors/calculate-presentation-duration.js +21 -43
  120. package/dist/dev/playback/behaviors/calculate-presentation-duration.js.map +1 -1
  121. package/dist/dev/playback/behaviors/derive-cdn-priority.js +68 -0
  122. package/dist/dev/playback/behaviors/derive-cdn-priority.js.map +1 -0
  123. package/dist/dev/playback/behaviors/dom/end-of-stream.js +15 -103
  124. package/dist/dev/playback/behaviors/dom/end-of-stream.js.map +1 -1
  125. package/dist/dev/playback/behaviors/dom/load-segments.js +1 -1
  126. package/dist/dev/playback/behaviors/dom/load-segments.js.map +1 -1
  127. package/dist/dev/playback/behaviors/dom/setup-buffer-actors.js +33 -14
  128. package/dist/dev/playback/behaviors/dom/setup-buffer-actors.js.map +1 -1
  129. package/dist/dev/playback/behaviors/dom/sync-text-tracks.js +43 -18
  130. package/dist/dev/playback/behaviors/dom/sync-text-tracks.js.map +1 -1
  131. package/dist/dev/playback/behaviors/dom/track-current-time.d.ts.map +1 -1
  132. package/dist/dev/playback/behaviors/dom/track-current-time.js +6 -0
  133. package/dist/dev/playback/behaviors/dom/track-current-time.js.map +1 -1
  134. package/dist/dev/playback/behaviors/dom/update-mediasource-duration.js +1 -1
  135. package/dist/dev/playback/behaviors/resolve-track.js +45 -22
  136. package/dist/dev/playback/behaviors/resolve-track.js.map +1 -1
  137. package/dist/dev/playback/behaviors/select-tracks.d.ts +13 -0
  138. package/dist/dev/playback/behaviors/select-tracks.d.ts.map +1 -0
  139. package/dist/dev/playback/behaviors/select-tracks.js +46 -77
  140. package/dist/dev/playback/behaviors/select-tracks.js.map +1 -1
  141. package/dist/dev/playback/behaviors/setup-failover-monitor.d.ts +12 -0
  142. package/dist/dev/playback/behaviors/setup-failover-monitor.d.ts.map +1 -0
  143. package/dist/dev/playback/behaviors/setup-failover-monitor.js +66 -0
  144. package/dist/dev/playback/behaviors/setup-failover-monitor.js.map +1 -0
  145. package/dist/dev/playback/behaviors/track-switching.js +341 -0
  146. package/dist/dev/playback/behaviors/track-switching.js.map +1 -0
  147. package/dist/dev/playback/engines/background-video/adapter.d.ts +60 -0
  148. package/dist/dev/playback/engines/background-video/adapter.d.ts.map +1 -0
  149. package/dist/dev/playback/engines/background-video/adapter.js +158 -0
  150. package/dist/dev/playback/engines/background-video/adapter.js.map +1 -0
  151. package/dist/dev/playback/engines/background-video/engine.d.ts +107 -0
  152. package/dist/dev/playback/engines/background-video/engine.d.ts.map +1 -0
  153. package/dist/dev/playback/engines/background-video/engine.js +75 -0
  154. package/dist/dev/playback/engines/background-video/engine.js.map +1 -0
  155. package/dist/dev/playback/engines/hls/adapter-audio-only.d.ts +45 -0
  156. package/dist/dev/playback/engines/hls/adapter-audio-only.d.ts.map +1 -0
  157. package/dist/dev/playback/engines/hls/adapter-audio-only.js +119 -0
  158. package/dist/dev/playback/engines/hls/adapter-audio-only.js.map +1 -0
  159. package/dist/dev/playback/engines/hls/adapter.d.ts.map +1 -1
  160. package/dist/dev/playback/engines/hls/adapter.js +7 -0
  161. package/dist/dev/playback/engines/hls/adapter.js.map +1 -1
  162. package/dist/dev/playback/engines/hls/engine-audio-only.d.ts +127 -0
  163. package/dist/dev/playback/engines/hls/engine-audio-only.d.ts.map +1 -0
  164. package/dist/dev/playback/engines/hls/engine-audio-only.js +80 -0
  165. package/dist/dev/playback/engines/hls/engine-audio-only.js.map +1 -0
  166. package/dist/dev/playback/engines/hls/engine.d.ts +61 -2
  167. package/dist/dev/playback/engines/hls/engine.d.ts.map +1 -1
  168. package/dist/dev/playback/engines/hls/engine.js +20 -7
  169. package/dist/dev/playback/engines/hls/engine.js.map +1 -1
  170. package/dist/dev/playback/primitives/failover-fetch.js +42 -0
  171. package/dist/dev/playback/primitives/failover-fetch.js.map +1 -0
  172. package/dist/{default/playback/behaviors → dev/playback/primitives}/track-types.js +1 -1
  173. package/dist/dev/playback/primitives/track-types.js.map +1 -0
  174. package/package.json +7 -2
  175. package/dist/default/playback/behaviors/quality-switching.js +0 -96
  176. package/dist/default/playback/behaviors/quality-switching.js.map +0 -1
  177. package/dist/default/playback/behaviors/track-types.js.map +0 -1
  178. package/dist/dev/playback/behaviors/quality-switching.js +0 -96
  179. package/dist/dev/playback/behaviors/quality-switching.js.map +0 -1
  180. package/dist/dev/playback/behaviors/track-types.js.map +0 -1
@@ -0,0 +1,75 @@
1
+ import { createComposition } from "../../../core/composition/create-composition.js";
2
+ import { makeShareSignals } from "../../../core/composition/share-signals.js";
3
+ import { loadVideoSegments } from "../../behaviors/dom/load-segments.js";
4
+ import { trackCurrentTime } from "../../behaviors/dom/track-current-time.js";
5
+ import { parseMultivariantPlaylist } from "../../../media/hls/parse-multivariant.js";
6
+ import { getResolvedSelectedTrackDuration } from "../../../media/utils/track-selection.js";
7
+ import { calculatePresentationDuration } from "../../behaviors/calculate-presentation-duration.js";
8
+ import { endOfStream } from "../../behaviors/dom/end-of-stream.js";
9
+ import { setupVideoBufferActors } from "../../behaviors/dom/setup-buffer-actors.js";
10
+ import { setupMediaSource } from "../../behaviors/dom/setup-mediasource.js";
11
+ import { updateMediaSourceDuration } from "../../behaviors/dom/update-mediasource-duration.js";
12
+ import { resolvePresentation } from "../../behaviors/resolve-presentation.js";
13
+ import { resolveVideoTrack } from "../../behaviors/resolve-track.js";
14
+ import { pickHighestResolutionVideoTrack } from "../../../media/primitives/select-tracks.js";
15
+ import { selectVideoTrack } from "../../behaviors/select-tracks.js";
16
+ //#region src/playback/engines/background-video/engine.ts
17
+ const shareSignals = makeShareSignals();
18
+ /**
19
+ * Create a background-video playback engine.
20
+ *
21
+ * Subtractive composition over the HLS engine baseline:
22
+ * audio-side, text-side, ABR-driven, preload-monitoring, and play/seek
23
+ * load-trigger behaviors are removed. `selectVideoTrack` (with a
24
+ * max-resolution picker by default) replaces `switchVideoQuality`, pinning
25
+ * a single rendition for the session. The initial state seeds
26
+ * `loadActivated: true` so the composition behaves as if preload has
27
+ * already been activated — appropriate for ambient / hero / GIF-replacement
28
+ * surfaces that should start loading the moment a src is set.
29
+ *
30
+ * Native `loop` / `muted` / `autoplay` are adapter concerns and live on
31
+ * `BackgroundVideoMediaElement` rather than the engine.
32
+ *
33
+ * @example
34
+ * ```ts
35
+ * let signals: BackgroundVideoEngineSignals;
36
+ * const engine = createBackgroundVideoEngine({
37
+ * onSignalsReady: (refs) => {
38
+ * signals = refs;
39
+ * },
40
+ * });
41
+ *
42
+ * signals.context.mediaElement.set(videoEl);
43
+ * signals.state.presentation.set({ url: 'https://example.com/stream.m3u8' });
44
+ *
45
+ * await engine.destroy();
46
+ * ```
47
+ */
48
+ function createBackgroundVideoEngine(config = {}) {
49
+ const finalConfig = {
50
+ ...config,
51
+ picker: config.picker ?? pickHighestResolutionVideoTrack,
52
+ parsePresentation: config.parsePresentation ?? parseMultivariantPlaylist,
53
+ resolveDuration: getResolvedSelectedTrackDuration
54
+ };
55
+ return createComposition([
56
+ resolvePresentation,
57
+ calculatePresentationDuration,
58
+ selectVideoTrack,
59
+ resolveVideoTrack,
60
+ loadVideoSegments,
61
+ setupMediaSource,
62
+ updateMediaSourceDuration,
63
+ setupVideoBufferActors,
64
+ trackCurrentTime,
65
+ endOfStream,
66
+ shareSignals
67
+ ], {
68
+ config: finalConfig,
69
+ initialState: { loadActivated: true }
70
+ });
71
+ }
72
+ //#endregion
73
+ export { createBackgroundVideoEngine };
74
+
75
+ //# sourceMappingURL=engine.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine.js","names":[],"sources":["../../../../../src/playback/engines/background-video/engine.ts"],"sourcesContent":["import {\n type Composition,\n type ContextSignals,\n createComposition,\n type StateSignals,\n} from '../../../core/composition/create-composition';\nimport { makeShareSignals, type ShareSignalsConfig } from '../../../core/composition/share-signals';\nimport { parseMultivariantPlaylist } from '../../../media/hls/parse-multivariant';\nimport { pickHighestResolutionVideoTrack, type TrackPicker } from '../../../media/primitives/select-tracks';\nimport type { MaybeResolvedPresentation } from '../../../media/types';\nimport { getResolvedSelectedTrackDuration } from '../../../media/utils/track-selection';\nimport type { SegmentLoaderActor } from '../../actors/dom/segment-loader';\nimport type { SourceBufferActor } from '../../actors/dom/source-buffer';\nimport { calculatePresentationDuration } from '../../behaviors/calculate-presentation-duration';\nimport { endOfStream } from '../../behaviors/dom/end-of-stream';\nimport { loadVideoSegments } from '../../behaviors/dom/load-segments';\nimport { setupVideoBufferActors } from '../../behaviors/dom/setup-buffer-actors';\nimport { setupMediaSource } from '../../behaviors/dom/setup-mediasource';\nimport { trackCurrentTime } from '../../behaviors/dom/track-current-time';\nimport { updateMediaSourceDuration } from '../../behaviors/dom/update-mediasource-duration';\nimport { type ParsePresentation, resolvePresentation } from '../../behaviors/resolve-presentation';\nimport { resolveVideoTrack } from '../../behaviors/resolve-track';\nimport { type SelectVideoTrackConfig, selectVideoTrack } from '../../behaviors/select-tracks';\n\n// ============================================================================\n// Background-video engine state & context\n// ============================================================================\n\n/**\n * State shape for the background-video playback engine.\n *\n * Narrower than `SimpleHlsEngineState`: audio/text track slots are absent\n * because their selection/resolution behaviors are subtracted. `bandwidthState`\n * is present because `setupVideoBufferActors` declares it and `loadVideoSegments`\n * samples into it (wasted work in this variant — a Phase 3 alt-impl will skip\n * sampling).\n */\nexport interface BackgroundVideoEngineState {\n /**\n * The presentation being played. A caller writes `{ url }`;\n * `resolvePresentation` parses the manifest and populates the rest.\n */\n presentation?: MaybeResolvedPresentation;\n preload?: 'auto' | 'metadata' | 'none';\n selectedVideoTrackId?: string;\n loadActivated?: boolean;\n}\n\n/**\n * Context shape for the background-video engine.\n */\nexport interface BackgroundVideoEngineContext {\n mediaElement?: HTMLMediaElement | undefined;\n mediaSource?: MediaSource;\n videoBufferActor?: SourceBufferActor;\n videoSegmentLoaderActor?: SegmentLoaderActor;\n}\n\n/**\n * The composition signal refs handed to `onSignalsReady` callers — the\n * canonical way to drive the engine externally (writes) or observe its\n * state (reads) without touching `composition.state` / `composition.context`\n * directly.\n */\nexport type BackgroundVideoEngineSignals = {\n state: StateSignals<BackgroundVideoEngineState>;\n context: ContextSignals<BackgroundVideoEngineContext>;\n};\n\n/**\n * Configuration for the background-video engine.\n *\n * Each option is consumed by the appropriate behavior — the engine itself\n * has no config beyond what its behaviors read. Compared to\n * `SimpleHlsEngineConfig`, audio/text/ABR/bandwidth/quality knobs are\n * dropped: the variant subtracts the behaviors that read them.\n */\nexport interface BackgroundVideoEngineConfig\n extends ShareSignalsConfig<BackgroundVideoEngineState, BackgroundVideoEngineContext> {\n /**\n * Track picker handed to `selectVideoTrack`. Default:\n * `pickHighestResolutionVideoTrack` — picks the highest-resolution variant on\n * presentation resolve and pins it for the session. Override for\n * mobile-aware or content-aware caps.\n *\n * Adapters (e.g. `BackgroundVideoMediaElement`) install their own\n * picker; this default applies when the engine is constructed directly.\n */\n picker?: TrackPicker<SelectVideoTrackConfig>;\n /**\n * Manifest parser handed to `resolvePresentation`. Defaults to the HLS\n * multivariant-playlist parser.\n */\n parsePresentation?: ParsePresentation;\n}\n\n// ============================================================================\n// Background-video playback engine\n// ============================================================================\n\nconst shareSignals = makeShareSignals<BackgroundVideoEngineState, BackgroundVideoEngineContext>();\n\n/**\n * Create a background-video playback engine.\n *\n * Subtractive composition over the HLS engine baseline:\n * audio-side, text-side, ABR-driven, preload-monitoring, and play/seek\n * load-trigger behaviors are removed. `selectVideoTrack` (with a\n * max-resolution picker by default) replaces `switchVideoQuality`, pinning\n * a single rendition for the session. The initial state seeds\n * `loadActivated: true` so the composition behaves as if preload has\n * already been activated — appropriate for ambient / hero / GIF-replacement\n * surfaces that should start loading the moment a src is set.\n *\n * Native `loop` / `muted` / `autoplay` are adapter concerns and live on\n * `BackgroundVideoMediaElement` rather than the engine.\n *\n * @example\n * ```ts\n * let signals: BackgroundVideoEngineSignals;\n * const engine = createBackgroundVideoEngine({\n * onSignalsReady: (refs) => {\n * signals = refs;\n * },\n * });\n *\n * signals.context.mediaElement.set(videoEl);\n * signals.state.presentation.set({ url: 'https://example.com/stream.m3u8' });\n *\n * await engine.destroy();\n * ```\n */\nexport function createBackgroundVideoEngine(\n config: BackgroundVideoEngineConfig = {}\n): Composition<BackgroundVideoEngineState, BackgroundVideoEngineContext> {\n const finalConfig = {\n ...config,\n picker: config.picker ?? pickHighestResolutionVideoTrack,\n parsePresentation: config.parsePresentation ?? parseMultivariantPlaylist,\n resolveDuration: getResolvedSelectedTrackDuration,\n };\n\n return createComposition(\n [\n resolvePresentation,\n // Presentation duration\n calculatePresentationDuration,\n\n // Track selection - pinned single-rendition pick on presentation resolve.\n selectVideoTrack,\n // Resolve selected video track (fetch its media playlist)\n resolveVideoTrack,\n // Segment loading — video-only.\n loadVideoSegments,\n\n // MSE setup — video-only.\n setupMediaSource,\n updateMediaSourceDuration,\n setupVideoBufferActors,\n\n // Playback tracking\n trackCurrentTime,\n\n // End of stream coordination\n endOfStream,\n\n // Behavior whose sole purpose is to expose signal refs via a callback\n // (e.g. to an adapter). Listed last so initial signal setup has run\n // before the callback fires.\n shareSignals,\n ],\n {\n config: finalConfig,\n initialState: {\n // Note: Set to true until we add preload configuration\n loadActivated: true,\n },\n }\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAoGA,MAAM,eAAe,kBAA4E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCjG,SAAgB,4BACd,SAAsC,EAAE,EAC+B;CACvE,MAAM,cAAc;EAClB,GAAG;EACH,QAAQ,OAAO,UAAU;EACzB,mBAAmB,OAAO,qBAAqB;EAC/C,iBAAiB;EAClB;AAED,QAAO,kBACL;EACE;EAEA;EAGA;EAEA;EAEA;EAGA;EACA;EACA;EAGA;EAGA;EAKA;EACD,EACD;EACE,QAAQ;EACR,cAAc,EAEZ,eAAe,MAChB;EACF,CACF"}
@@ -0,0 +1,119 @@
1
+ import { createHlsAudioOnlyEngine } from "./engine-audio-only.js";
2
+ //#region src/playback/engines/hls/adapter-audio-only.ts
3
+ const simpleHlsAudioOnlyMediaDefaultProps = {
4
+ src: "",
5
+ preload: ""
6
+ };
7
+ /**
8
+ * Mixin that adds SPF audio-only HLS playback to any base class.
9
+ *
10
+ * Parallel to `SimpleHlsMediaMixin` with one substantive difference: the
11
+ * underlying engine is the audio-only variant (`createHlsAudioOnlyEngine`),
12
+ * which omits video and text-track behaviors. The src / preload / play()
13
+ * contract per the WHATWG HTML spec is identical to the default adapter.
14
+ *
15
+ * Selecting this adapter is the variant decision: instantiating
16
+ * `SimpleHlsAudioOnlyMediaElement` opts the consumer into audio-only
17
+ * delivery even when the source is a mixed-AV HLS manifest.
18
+ *
19
+ * @example
20
+ * class SimpleHlsAudioOnlyMedia extends SimpleHlsAudioOnlyMediaMixin(HTMLVideoElementHost) {}
21
+ *
22
+ * const media = new SimpleHlsAudioOnlyMedia();
23
+ * media.attach(document.querySelector('video'));
24
+ * media.src = 'https://stream.mux.com/abc123.m3u8';
25
+ */
26
+ function SimpleHlsAudioOnlyMediaMixin(BaseClass) {
27
+ class SimpleHlsAudioOnlyMediaImpl extends BaseClass {
28
+ #engine;
29
+ #config;
30
+ #signals;
31
+ #preload = simpleHlsAudioOnlyMediaDefaultProps.preload;
32
+ /** Pending loadstart listener from a deferred play() retry, if any. */
33
+ #loadstartListener = null;
34
+ constructor(...args) {
35
+ super(...args);
36
+ const { config } = args?.[0] ?? {};
37
+ this.#config = config;
38
+ this.#engine = this.#createEngine();
39
+ }
40
+ /**
41
+ * Underlying playback engine — the low-level SPF reactive composition that
42
+ * drives playback. An advanced escape hatch for direct engine access;
43
+ * normal playback is driven through this element's own properties and
44
+ * methods.
45
+ */
46
+ get engine() {
47
+ return this.#engine;
48
+ }
49
+ attach(mediaElement) {
50
+ super.attach?.(mediaElement);
51
+ this.#signals.context.mediaElement.set(mediaElement);
52
+ }
53
+ detach() {
54
+ this.#cancelPendingPlay();
55
+ this.#signals.context.mediaElement.set(void 0);
56
+ super.detach?.();
57
+ }
58
+ destroy() {
59
+ this.#cancelPendingPlay();
60
+ this.#engine.destroy();
61
+ }
62
+ /** Preload type (`'none'` / `'metadata'` / `'auto'`). */
63
+ get preload() {
64
+ return this.#preload;
65
+ }
66
+ set preload(value) {
67
+ this.#preload = value;
68
+ if (value) this.#signals.state.preload.set(value);
69
+ }
70
+ get src() {
71
+ return this.#signals.state.presentation.get()?.url ?? "";
72
+ }
73
+ set src(value) {
74
+ const prevMediaElement = this.#signals.context.mediaElement.get();
75
+ this.#cancelPendingPlay();
76
+ this.#engine.destroy();
77
+ this.#engine = this.#createEngine();
78
+ if (this.#preload) this.#signals.state.preload.set(this.#preload);
79
+ if (prevMediaElement) this.#signals.context.mediaElement.set(prevMediaElement);
80
+ if (value) this.#signals.state.presentation.set({ url: value });
81
+ }
82
+ play() {
83
+ const mediaElement = this.#signals.context.mediaElement.get();
84
+ if (!mediaElement) return Promise.reject(/* @__PURE__ */ new Error("SimpleHlsAudioOnlyMediaElement: no media element attached"));
85
+ this.#signals.state.loadActivated.set(true);
86
+ return mediaElement.play().catch((err) => {
87
+ if (this.src) return new Promise((resolve, reject) => {
88
+ const listener = () => {
89
+ this.#loadstartListener = null;
90
+ mediaElement.play().then(resolve, reject);
91
+ };
92
+ this.#loadstartListener = listener;
93
+ mediaElement.addEventListener("loadstart", listener, { once: true });
94
+ });
95
+ throw err;
96
+ });
97
+ }
98
+ #createEngine() {
99
+ return createHlsAudioOnlyEngine({
100
+ ...this.#config,
101
+ onSignalsReady: (signals) => {
102
+ this.#signals = signals;
103
+ }
104
+ });
105
+ }
106
+ #cancelPendingPlay() {
107
+ if (!this.#loadstartListener) return;
108
+ this.#signals.context.mediaElement.get()?.removeEventListener("loadstart", this.#loadstartListener);
109
+ this.#loadstartListener = null;
110
+ }
111
+ }
112
+ return SimpleHlsAudioOnlyMediaImpl;
113
+ }
114
+ /** Standalone SPF audio-only media adapter with no base class. */
115
+ var SimpleHlsAudioOnlyMediaElement = class extends SimpleHlsAudioOnlyMediaMixin(class {}) {};
116
+ //#endregion
117
+ export { SimpleHlsAudioOnlyMediaElement, SimpleHlsAudioOnlyMediaMixin, simpleHlsAudioOnlyMediaDefaultProps };
118
+
119
+ //# sourceMappingURL=adapter-audio-only.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter-audio-only.js","names":["#config","#engine","#createEngine","#signals","#cancelPendingPlay","#preload","#loadstartListener"],"sources":["../../../../../src/playback/engines/hls/adapter-audio-only.ts"],"sourcesContent":["import type { Constructor, MixinReturn } from '@videojs/utils/types';\nimport type { Composition } from '../../../core/composition/create-composition';\nimport {\n createHlsAudioOnlyEngine,\n type SimpleHlsAudioOnlyEngineConfig,\n type SimpleHlsAudioOnlyEngineContext,\n type SimpleHlsAudioOnlyEngineSignals,\n type SimpleHlsAudioOnlyEngineState,\n} from './engine-audio-only';\n\nexport interface SimpleHlsAudioOnlyMediaProps {\n src: string;\n preload: '' | 'none' | 'metadata' | 'auto';\n}\n\nexport const simpleHlsAudioOnlyMediaDefaultProps: SimpleHlsAudioOnlyMediaProps = {\n src: '',\n preload: '',\n};\n\nexport interface SimpleHlsAudioOnlyMediaAPI extends SimpleHlsAudioOnlyMediaProps {\n readonly engine: Composition<SimpleHlsAudioOnlyEngineState, SimpleHlsAudioOnlyEngineContext>;\n attach(mediaElement: HTMLMediaElement): void;\n detach(): void;\n destroy(): void;\n play(): Promise<void>;\n}\n\n/**\n * Mixin that adds SPF audio-only HLS playback to any base class.\n *\n * Parallel to `SimpleHlsMediaMixin` with one substantive difference: the\n * underlying engine is the audio-only variant (`createHlsAudioOnlyEngine`),\n * which omits video and text-track behaviors. The src / preload / play()\n * contract per the WHATWG HTML spec is identical to the default adapter.\n *\n * Selecting this adapter is the variant decision: instantiating\n * `SimpleHlsAudioOnlyMediaElement` opts the consumer into audio-only\n * delivery even when the source is a mixed-AV HLS manifest.\n *\n * @example\n * class SimpleHlsAudioOnlyMedia extends SimpleHlsAudioOnlyMediaMixin(HTMLVideoElementHost) {}\n *\n * const media = new SimpleHlsAudioOnlyMedia();\n * media.attach(document.querySelector('video'));\n * media.src = 'https://stream.mux.com/abc123.m3u8';\n */\nexport function SimpleHlsAudioOnlyMediaMixin<Base extends Constructor<any>>(BaseClass: Base) {\n class SimpleHlsAudioOnlyMediaImpl extends BaseClass {\n #engine: Composition<SimpleHlsAudioOnlyEngineState, SimpleHlsAudioOnlyEngineContext>;\n #config: SimpleHlsAudioOnlyEngineConfig;\n #signals!: SimpleHlsAudioOnlyEngineSignals;\n #preload: '' | 'none' | 'metadata' | 'auto' = simpleHlsAudioOnlyMediaDefaultProps.preload;\n\n /** Pending loadstart listener from a deferred play() retry, if any. */\n #loadstartListener: (() => void) | null = null;\n\n constructor(...args: any[]) {\n super(...args);\n\n const { config } = args?.[0] ?? {};\n this.#config = config;\n this.#engine = this.#createEngine();\n }\n\n /**\n * Underlying playback engine — the low-level SPF reactive composition that\n * drives playback. An advanced escape hatch for direct engine access;\n * normal playback is driven through this element's own properties and\n * methods.\n */\n get engine(): Composition<SimpleHlsAudioOnlyEngineState, SimpleHlsAudioOnlyEngineContext> {\n return this.#engine;\n }\n\n // -------------------------------------------------------------------------\n // Media element lifecycle\n // -------------------------------------------------------------------------\n\n attach(mediaElement: HTMLMediaElement): void {\n super.attach?.(mediaElement);\n this.#signals.context.mediaElement.set(mediaElement);\n }\n\n detach(): void {\n this.#cancelPendingPlay();\n this.#signals.context.mediaElement.set(undefined);\n super.detach?.();\n }\n\n destroy(): void {\n this.#cancelPendingPlay();\n this.#engine.destroy();\n }\n\n // -------------------------------------------------------------------------\n // preload — synchronous IDL attribute (WHATWG §4.8.11.2)\n // -------------------------------------------------------------------------\n\n /** Preload type (`'none'` / `'metadata'` / `'auto'`). */\n get preload(): '' | 'none' | 'metadata' | 'auto' {\n return this.#preload;\n }\n\n set preload(value: '' | 'none' | 'metadata' | 'auto') {\n this.#preload = value;\n if (value) {\n this.#signals.state.preload.set(value);\n }\n }\n\n // -------------------------------------------------------------------------\n // src — synchronous IDL attribute (WHATWG §4.8.11.2)\n // -------------------------------------------------------------------------\n\n get src(): string {\n return this.#signals.state.presentation.get()?.url ?? '';\n }\n\n set src(value: string) {\n const prevMediaElement = this.#signals.context.mediaElement.get();\n\n this.#cancelPendingPlay();\n this.#engine.destroy();\n this.#engine = this.#createEngine();\n\n if (this.#preload) {\n this.#signals.state.preload.set(this.#preload);\n }\n\n if (prevMediaElement) {\n this.#signals.context.mediaElement.set(prevMediaElement);\n }\n\n if (value) {\n this.#signals.state.presentation.set({ url: value });\n }\n }\n\n // -------------------------------------------------------------------------\n // play() — WHATWG §4.8.11.8\n // -------------------------------------------------------------------------\n\n play(): Promise<void> {\n const mediaElement = this.#signals.context.mediaElement.get();\n if (!mediaElement) {\n return Promise.reject(new Error('SimpleHlsAudioOnlyMediaElement: no media element attached'));\n }\n\n this.#signals.state.loadActivated.set(true);\n\n return mediaElement.play().catch((err: unknown) => {\n if (this.src) {\n return new Promise<void>((resolve, reject) => {\n const listener = () => {\n this.#loadstartListener = null;\n mediaElement.play().then(resolve, reject);\n };\n this.#loadstartListener = listener;\n mediaElement.addEventListener('loadstart', listener, { once: true });\n });\n }\n throw err;\n });\n }\n\n // -------------------------------------------------------------------------\n // Private\n // -------------------------------------------------------------------------\n\n #createEngine(): Composition<SimpleHlsAudioOnlyEngineState, SimpleHlsAudioOnlyEngineContext> {\n return createHlsAudioOnlyEngine({\n ...this.#config,\n onSignalsReady: (signals) => {\n this.#signals = signals;\n },\n });\n }\n\n #cancelPendingPlay(): void {\n if (!this.#loadstartListener) return;\n const mediaElement = this.#signals.context.mediaElement.get();\n mediaElement?.removeEventListener('loadstart', this.#loadstartListener);\n this.#loadstartListener = null;\n }\n }\n\n return SimpleHlsAudioOnlyMediaImpl as unknown as MixinReturn<Base, SimpleHlsAudioOnlyMediaAPI>;\n}\n\n/** Standalone SPF audio-only media adapter with no base class. */\nexport class SimpleHlsAudioOnlyMediaElement extends SimpleHlsAudioOnlyMediaMixin(class {}) {}\n"],"mappings":";;AAeA,MAAa,sCAAoE;CAC/E,KAAK;CACL,SAAS;CACV;;;;;;;;;;;;;;;;;;;;AA6BD,SAAgB,6BAA4D,WAAiB;CAC3F,MAAM,oCAAoC,UAAU;EAClD;EACA;EACA;EACA,WAA8C,oCAAoC;;EAGlF,qBAA0C;EAE1C,YAAY,GAAG,MAAa;AAC1B,SAAM,GAAG,KAAK;GAEd,MAAM,EAAE,WAAW,OAAO,MAAM,EAAE;AAClC,SAAA,SAAe;AACf,SAAA,SAAe,MAAA,cAAoB;;;;;;;;EASrC,IAAI,SAAsF;AACxF,UAAO,MAAA;;EAOT,OAAO,cAAsC;AAC3C,SAAM,SAAS,aAAa;AAC5B,SAAA,QAAc,QAAQ,aAAa,IAAI,aAAa;;EAGtD,SAAe;AACb,SAAA,mBAAyB;AACzB,SAAA,QAAc,QAAQ,aAAa,IAAI,KAAA,EAAU;AACjD,SAAM,UAAU;;EAGlB,UAAgB;AACd,SAAA,mBAAyB;AACzB,SAAA,OAAa,SAAS;;;EAQxB,IAAI,UAA6C;AAC/C,UAAO,MAAA;;EAGT,IAAI,QAAQ,OAA0C;AACpD,SAAA,UAAgB;AAChB,OAAI,MACF,OAAA,QAAc,MAAM,QAAQ,IAAI,MAAM;;EAQ1C,IAAI,MAAc;AAChB,UAAO,MAAA,QAAc,MAAM,aAAa,KAAK,EAAE,OAAO;;EAGxD,IAAI,IAAI,OAAe;GACrB,MAAM,mBAAmB,MAAA,QAAc,QAAQ,aAAa,KAAK;AAEjE,SAAA,mBAAyB;AACzB,SAAA,OAAa,SAAS;AACtB,SAAA,SAAe,MAAA,cAAoB;AAEnC,OAAI,MAAA,QACF,OAAA,QAAc,MAAM,QAAQ,IAAI,MAAA,QAAc;AAGhD,OAAI,iBACF,OAAA,QAAc,QAAQ,aAAa,IAAI,iBAAiB;AAG1D,OAAI,MACF,OAAA,QAAc,MAAM,aAAa,IAAI,EAAE,KAAK,OAAO,CAAC;;EAQxD,OAAsB;GACpB,MAAM,eAAe,MAAA,QAAc,QAAQ,aAAa,KAAK;AAC7D,OAAI,CAAC,aACH,QAAO,QAAQ,uBAAO,IAAI,MAAM,4DAA4D,CAAC;AAG/F,SAAA,QAAc,MAAM,cAAc,IAAI,KAAK;AAE3C,UAAO,aAAa,MAAM,CAAC,OAAO,QAAiB;AACjD,QAAI,KAAK,IACP,QAAO,IAAI,SAAe,SAAS,WAAW;KAC5C,MAAM,iBAAiB;AACrB,YAAA,oBAA0B;AAC1B,mBAAa,MAAM,CAAC,KAAK,SAAS,OAAO;;AAE3C,WAAA,oBAA0B;AAC1B,kBAAa,iBAAiB,aAAa,UAAU,EAAE,MAAM,MAAM,CAAC;MACpE;AAEJ,UAAM;KACN;;EAOJ,gBAA6F;AAC3F,UAAO,yBAAyB;IAC9B,GAAG,MAAA;IACH,iBAAiB,YAAY;AAC3B,WAAA,UAAgB;;IAEnB,CAAC;;EAGJ,qBAA2B;AACzB,OAAI,CAAC,MAAA,kBAAyB;AACT,SAAA,QAAc,QAAQ,aAAa,KAAK,EAC/C,oBAAoB,aAAa,MAAA,kBAAwB;AACvE,SAAA,oBAA0B;;;AAI9B,QAAO;;;AAIT,IAAa,iCAAb,cAAoD,6BAA6B,MAAM,GAAG,CAAC"}
@@ -36,6 +36,12 @@ function SimpleHlsMediaMixin(BaseClass) {
36
36
  this.#config = config;
37
37
  this.#engine = this.#createEngine();
38
38
  }
39
+ /**
40
+ * Underlying playback engine — the low-level SPF reactive composition that
41
+ * drives playback. An advanced escape hatch for direct engine access;
42
+ * normal playback is driven through this element's own properties and
43
+ * methods.
44
+ */
39
45
  get engine() {
40
46
  return this.#engine;
41
47
  }
@@ -52,6 +58,7 @@ function SimpleHlsMediaMixin(BaseClass) {
52
58
  this.#cancelPendingPlay();
53
59
  this.#engine.destroy();
54
60
  }
61
+ /** Preload type (`'none'` / `'metadata'` / `'auto'`). */
55
62
  get preload() {
56
63
  return this.#preload;
57
64
  }
@@ -1 +1 @@
1
- {"version":3,"file":"adapter.js","names":["#config","#engine","#createEngine","#signals","#cancelPendingPlay","#preload","#loadstartListener"],"sources":["../../../../../src/playback/engines/hls/adapter.ts"],"sourcesContent":["import type { Constructor, MixinReturn } from '@videojs/utils/types';\nimport type { Composition } from '../../../core/composition/create-composition';\nimport {\n createSimpleHlsEngine,\n type SimpleHlsEngineConfig,\n type SimpleHlsEngineContext,\n type SimpleHlsEngineSignals,\n type SimpleHlsEngineState,\n} from './engine';\n\nexport interface SimpleHlsMediaProps {\n src: string;\n preload: '' | 'none' | 'metadata' | 'auto';\n}\n\nexport const simpleHlsMediaDefaultProps: SimpleHlsMediaProps = {\n src: '',\n preload: '',\n};\n\nexport interface SimpleHlsMediaAPI extends SimpleHlsMediaProps {\n readonly engine: Composition<SimpleHlsEngineState, SimpleHlsEngineContext>;\n attach(mediaElement: HTMLMediaElement): void;\n detach(): void;\n destroy(): void;\n play(): Promise<void>;\n}\n\n/**\n * Mixin that adds SPF playback engine behavior to any base class.\n *\n * Implements the src/play() contract per the WHATWG HTML spec so that SPF can\n * be used anywhere a media element API is expected.\n *\n * A new engine is created on every src assignment — this fully tears down all\n * state, SourceBuffers, and in-flight requests from the previous source before\n * the next one begins. The media element reference is preserved across src\n * changes and re-applied to the new engine automatically.\n *\n * @example\n * class SimpleHlsMedia extends SimpleHlsMediaMixin(HTMLVideoElementHost) {}\n *\n * const media = new SimpleHlsMedia();\n * media.attach(document.querySelector('video'));\n * media.src = 'https://stream.mux.com/abc123.m3u8';\n */\nexport function SimpleHlsMediaMixin<Base extends Constructor<any>>(BaseClass: Base) {\n class SimpleHlsMediaImpl extends BaseClass {\n #engine: Composition<SimpleHlsEngineState, SimpleHlsEngineContext>;\n #config: SimpleHlsEngineConfig;\n #signals!: SimpleHlsEngineSignals;\n #preload: '' | 'none' | 'metadata' | 'auto' = simpleHlsMediaDefaultProps.preload;\n\n /** Pending loadstart listener from a deferred play() retry, if any. */\n #loadstartListener: (() => void) | null = null;\n\n constructor(...args: any[]) {\n super(...args);\n\n const { config } = args?.[0] ?? {};\n this.#config = config;\n this.#engine = this.#createEngine();\n }\n\n get engine(): Composition<SimpleHlsEngineState, SimpleHlsEngineContext> {\n return this.#engine;\n }\n\n // -------------------------------------------------------------------------\n // Media element lifecycle\n // -------------------------------------------------------------------------\n\n attach(mediaElement: HTMLMediaElement): void {\n super.attach?.(mediaElement);\n this.#signals.context.mediaElement.set(mediaElement);\n }\n\n detach(): void {\n this.#cancelPendingPlay();\n this.#signals.context.mediaElement.set(undefined);\n super.detach?.();\n }\n\n destroy(): void {\n this.#cancelPendingPlay();\n this.#engine.destroy();\n }\n\n // -------------------------------------------------------------------------\n // preload — synchronous IDL attribute (WHATWG §4.8.11.2)\n // -------------------------------------------------------------------------\n\n get preload(): '' | 'none' | 'metadata' | 'auto' {\n return this.#preload;\n }\n\n set preload(value: '' | 'none' | 'metadata' | 'auto') {\n this.#preload = value;\n if (value) {\n this.#signals.state.preload.set(value);\n }\n // value = '' clears #preload (so the next engine recreation won't re-apply\n // an explicit value) but does not patch current state — the existing preload\n // stays in effect until the next src change creates a fresh engine.\n }\n\n // -------------------------------------------------------------------------\n // src — synchronous IDL attribute (WHATWG §4.8.11.2)\n // Each assignment destroys the current engine and starts a fresh one, exactly\n // as the browser's load algorithm resets all media element state on src change.\n // -------------------------------------------------------------------------\n\n get src(): string {\n return this.#signals.state.presentation.get()?.url ?? '';\n }\n\n set src(value: string) {\n const prevMediaElement = this.#signals.context.mediaElement.get();\n\n this.#cancelPendingPlay();\n this.#engine.destroy();\n this.#engine = this.#createEngine();\n\n // Apply explicit preload before setting context so it's already in\n // state.preload when syncPreload's read effect runs on the attach —\n // the read effect only overwrites when the element's `preload` is a\n // W3C value (which a freshly-created <video> with no attribute is not).\n if (this.#preload) {\n this.#signals.state.preload.set(this.#preload);\n }\n\n if (prevMediaElement) {\n this.#signals.context.mediaElement.set(prevMediaElement);\n }\n\n if (value) {\n this.#signals.state.presentation.set({ url: value });\n }\n }\n\n // -------------------------------------------------------------------------\n // play() — WHATWG §4.8.11.8\n // Delegates to the attached media element's native play().\n // -------------------------------------------------------------------------\n\n play(): Promise<void> {\n const mediaElement = this.#signals.context.mediaElement.get();\n if (!mediaElement) {\n return Promise.reject(new Error('SimpleHlsMediaElement: no media element attached'));\n }\n\n // Signal play intent — enables loading even with preload=\"none\"\n this.#signals.state.loadActivated.set(true);\n\n return mediaElement.play().catch((err: unknown) => {\n // If we have a pending HLS source, the rejection may be because MSE\n // hasn't attached a blob URL yet. Wait for loadstart (src assigned\n // by MSE setup) and retry once.\n if (this.src) {\n return new Promise<void>((resolve, reject) => {\n const listener = () => {\n this.#loadstartListener = null;\n mediaElement.play().then(resolve, reject);\n };\n this.#loadstartListener = listener;\n mediaElement.addEventListener('loadstart', listener, { once: true });\n });\n }\n throw err;\n });\n }\n\n // -------------------------------------------------------------------------\n // Private\n // -------------------------------------------------------------------------\n\n #createEngine(): Composition<SimpleHlsEngineState, SimpleHlsEngineContext> {\n return createSimpleHlsEngine({\n ...this.#config,\n onSignalsReady: (signals) => {\n this.#signals = signals;\n },\n });\n }\n\n #cancelPendingPlay(): void {\n if (!this.#loadstartListener) return;\n const mediaElement = this.#signals.context.mediaElement.get();\n mediaElement?.removeEventListener('loadstart', this.#loadstartListener);\n this.#loadstartListener = null;\n }\n }\n\n return SimpleHlsMediaImpl as unknown as MixinReturn<Base, SimpleHlsMediaAPI>;\n}\n\n/** Standalone SPF media adapter with no base class. */\nexport class SimpleHlsMediaElement extends SimpleHlsMediaMixin(class {}) {}\n"],"mappings":";;AAeA,MAAa,6BAAkD;CAC7D,KAAK;CACL,SAAS;CACV;;;;;;;;;;;;;;;;;;;AA4BD,SAAgB,oBAAmD,WAAiB;CAClF,MAAM,2BAA2B,UAAU;EACzC;EACA;EACA;EACA,WAA8C,2BAA2B;;EAGzE,qBAA0C;EAE1C,YAAY,GAAG,MAAa;AAC1B,SAAM,GAAG,KAAK;GAEd,MAAM,EAAE,WAAW,OAAO,MAAM,EAAE;AAClC,SAAA,SAAe;AACf,SAAA,SAAe,MAAA,cAAoB;;EAGrC,IAAI,SAAoE;AACtE,UAAO,MAAA;;EAOT,OAAO,cAAsC;AAC3C,SAAM,SAAS,aAAa;AAC5B,SAAA,QAAc,QAAQ,aAAa,IAAI,aAAa;;EAGtD,SAAe;AACb,SAAA,mBAAyB;AACzB,SAAA,QAAc,QAAQ,aAAa,IAAI,KAAA,EAAU;AACjD,SAAM,UAAU;;EAGlB,UAAgB;AACd,SAAA,mBAAyB;AACzB,SAAA,OAAa,SAAS;;EAOxB,IAAI,UAA6C;AAC/C,UAAO,MAAA;;EAGT,IAAI,QAAQ,OAA0C;AACpD,SAAA,UAAgB;AAChB,OAAI,MACF,OAAA,QAAc,MAAM,QAAQ,IAAI,MAAM;;EAa1C,IAAI,MAAc;AAChB,UAAO,MAAA,QAAc,MAAM,aAAa,KAAK,EAAE,OAAO;;EAGxD,IAAI,IAAI,OAAe;GACrB,MAAM,mBAAmB,MAAA,QAAc,QAAQ,aAAa,KAAK;AAEjE,SAAA,mBAAyB;AACzB,SAAA,OAAa,SAAS;AACtB,SAAA,SAAe,MAAA,cAAoB;AAMnC,OAAI,MAAA,QACF,OAAA,QAAc,MAAM,QAAQ,IAAI,MAAA,QAAc;AAGhD,OAAI,iBACF,OAAA,QAAc,QAAQ,aAAa,IAAI,iBAAiB;AAG1D,OAAI,MACF,OAAA,QAAc,MAAM,aAAa,IAAI,EAAE,KAAK,OAAO,CAAC;;EASxD,OAAsB;GACpB,MAAM,eAAe,MAAA,QAAc,QAAQ,aAAa,KAAK;AAC7D,OAAI,CAAC,aACH,QAAO,QAAQ,uBAAO,IAAI,MAAM,mDAAmD,CAAC;AAItF,SAAA,QAAc,MAAM,cAAc,IAAI,KAAK;AAE3C,UAAO,aAAa,MAAM,CAAC,OAAO,QAAiB;AAIjD,QAAI,KAAK,IACP,QAAO,IAAI,SAAe,SAAS,WAAW;KAC5C,MAAM,iBAAiB;AACrB,YAAA,oBAA0B;AAC1B,mBAAa,MAAM,CAAC,KAAK,SAAS,OAAO;;AAE3C,WAAA,oBAA0B;AAC1B,kBAAa,iBAAiB,aAAa,UAAU,EAAE,MAAM,MAAM,CAAC;MACpE;AAEJ,UAAM;KACN;;EAOJ,gBAA2E;AACzE,UAAO,sBAAsB;IAC3B,GAAG,MAAA;IACH,iBAAiB,YAAY;AAC3B,WAAA,UAAgB;;IAEnB,CAAC;;EAGJ,qBAA2B;AACzB,OAAI,CAAC,MAAA,kBAAyB;AACT,SAAA,QAAc,QAAQ,aAAa,KAAK,EAC/C,oBAAoB,aAAa,MAAA,kBAAwB;AACvE,SAAA,oBAA0B;;;AAI9B,QAAO;;;AAIT,IAAa,wBAAb,cAA2C,oBAAoB,MAAM,GAAG,CAAC"}
1
+ {"version":3,"file":"adapter.js","names":["#config","#engine","#createEngine","#signals","#cancelPendingPlay","#preload","#loadstartListener"],"sources":["../../../../../src/playback/engines/hls/adapter.ts"],"sourcesContent":["import type { Constructor, MixinReturn } from '@videojs/utils/types';\nimport type { Composition } from '../../../core/composition/create-composition';\nimport {\n createSimpleHlsEngine,\n type SimpleHlsEngineConfig,\n type SimpleHlsEngineContext,\n type SimpleHlsEngineSignals,\n type SimpleHlsEngineState,\n} from './engine';\n\nexport interface SimpleHlsMediaProps {\n src: string;\n preload: '' | 'none' | 'metadata' | 'auto';\n}\n\nexport const simpleHlsMediaDefaultProps: SimpleHlsMediaProps = {\n src: '',\n preload: '',\n};\n\nexport interface SimpleHlsMediaAPI extends SimpleHlsMediaProps {\n readonly engine: Composition<SimpleHlsEngineState, SimpleHlsEngineContext>;\n attach(mediaElement: HTMLMediaElement): void;\n detach(): void;\n destroy(): void;\n play(): Promise<void>;\n}\n\n/**\n * Mixin that adds SPF playback engine behavior to any base class.\n *\n * Implements the src/play() contract per the WHATWG HTML spec so that SPF can\n * be used anywhere a media element API is expected.\n *\n * A new engine is created on every src assignment — this fully tears down all\n * state, SourceBuffers, and in-flight requests from the previous source before\n * the next one begins. The media element reference is preserved across src\n * changes and re-applied to the new engine automatically.\n *\n * @example\n * class SimpleHlsMedia extends SimpleHlsMediaMixin(HTMLVideoElementHost) {}\n *\n * const media = new SimpleHlsMedia();\n * media.attach(document.querySelector('video'));\n * media.src = 'https://stream.mux.com/abc123.m3u8';\n */\nexport function SimpleHlsMediaMixin<Base extends Constructor<any>>(BaseClass: Base) {\n class SimpleHlsMediaImpl extends BaseClass {\n #engine: Composition<SimpleHlsEngineState, SimpleHlsEngineContext>;\n #config: SimpleHlsEngineConfig;\n #signals!: SimpleHlsEngineSignals;\n #preload: '' | 'none' | 'metadata' | 'auto' = simpleHlsMediaDefaultProps.preload;\n\n /** Pending loadstart listener from a deferred play() retry, if any. */\n #loadstartListener: (() => void) | null = null;\n\n constructor(...args: any[]) {\n super(...args);\n\n const { config } = args?.[0] ?? {};\n this.#config = config;\n this.#engine = this.#createEngine();\n }\n\n /**\n * Underlying playback engine — the low-level SPF reactive composition that\n * drives playback. An advanced escape hatch for direct engine access;\n * normal playback is driven through this element's own properties and\n * methods.\n */\n get engine(): Composition<SimpleHlsEngineState, SimpleHlsEngineContext> {\n return this.#engine;\n }\n\n // -------------------------------------------------------------------------\n // Media element lifecycle\n // -------------------------------------------------------------------------\n\n attach(mediaElement: HTMLMediaElement): void {\n super.attach?.(mediaElement);\n this.#signals.context.mediaElement.set(mediaElement);\n }\n\n detach(): void {\n this.#cancelPendingPlay();\n this.#signals.context.mediaElement.set(undefined);\n super.detach?.();\n }\n\n destroy(): void {\n this.#cancelPendingPlay();\n this.#engine.destroy();\n }\n\n // -------------------------------------------------------------------------\n // preload — synchronous IDL attribute (WHATWG §4.8.11.2)\n // -------------------------------------------------------------------------\n\n /** Preload type (`'none'` / `'metadata'` / `'auto'`). */\n get preload(): '' | 'none' | 'metadata' | 'auto' {\n return this.#preload;\n }\n\n set preload(value: '' | 'none' | 'metadata' | 'auto') {\n this.#preload = value;\n if (value) {\n this.#signals.state.preload.set(value);\n }\n // value = '' clears #preload (so the next engine recreation won't re-apply\n // an explicit value) but does not patch current state — the existing preload\n // stays in effect until the next src change creates a fresh engine.\n }\n\n // -------------------------------------------------------------------------\n // src — synchronous IDL attribute (WHATWG §4.8.11.2)\n // Each assignment destroys the current engine and starts a fresh one, exactly\n // as the browser's load algorithm resets all media element state on src change.\n // -------------------------------------------------------------------------\n\n get src(): string {\n return this.#signals.state.presentation.get()?.url ?? '';\n }\n\n set src(value: string) {\n const prevMediaElement = this.#signals.context.mediaElement.get();\n\n this.#cancelPendingPlay();\n this.#engine.destroy();\n this.#engine = this.#createEngine();\n\n // Apply explicit preload before setting context so it's already in\n // state.preload when syncPreload's read effect runs on the attach —\n // the read effect only overwrites when the element's `preload` is a\n // W3C value (which a freshly-created <video> with no attribute is not).\n if (this.#preload) {\n this.#signals.state.preload.set(this.#preload);\n }\n\n if (prevMediaElement) {\n this.#signals.context.mediaElement.set(prevMediaElement);\n }\n\n if (value) {\n this.#signals.state.presentation.set({ url: value });\n }\n }\n\n // -------------------------------------------------------------------------\n // play() — WHATWG §4.8.11.8\n // Delegates to the attached media element's native play().\n // -------------------------------------------------------------------------\n\n play(): Promise<void> {\n const mediaElement = this.#signals.context.mediaElement.get();\n if (!mediaElement) {\n return Promise.reject(new Error('SimpleHlsMediaElement: no media element attached'));\n }\n\n // Signal play intent — enables loading even with preload=\"none\"\n this.#signals.state.loadActivated.set(true);\n\n return mediaElement.play().catch((err: unknown) => {\n // If we have a pending HLS source, the rejection may be because MSE\n // hasn't attached a blob URL yet. Wait for loadstart (src assigned\n // by MSE setup) and retry once.\n if (this.src) {\n return new Promise<void>((resolve, reject) => {\n const listener = () => {\n this.#loadstartListener = null;\n mediaElement.play().then(resolve, reject);\n };\n this.#loadstartListener = listener;\n mediaElement.addEventListener('loadstart', listener, { once: true });\n });\n }\n throw err;\n });\n }\n\n // -------------------------------------------------------------------------\n // Private\n // -------------------------------------------------------------------------\n\n #createEngine(): Composition<SimpleHlsEngineState, SimpleHlsEngineContext> {\n return createSimpleHlsEngine({\n ...this.#config,\n onSignalsReady: (signals) => {\n this.#signals = signals;\n },\n });\n }\n\n #cancelPendingPlay(): void {\n if (!this.#loadstartListener) return;\n const mediaElement = this.#signals.context.mediaElement.get();\n mediaElement?.removeEventListener('loadstart', this.#loadstartListener);\n this.#loadstartListener = null;\n }\n }\n\n return SimpleHlsMediaImpl as unknown as MixinReturn<Base, SimpleHlsMediaAPI>;\n}\n\n/** Standalone SPF media adapter with no base class. */\nexport class SimpleHlsMediaElement extends SimpleHlsMediaMixin(class {}) {}\n"],"mappings":";;AAeA,MAAa,6BAAkD;CAC7D,KAAK;CACL,SAAS;CACV;;;;;;;;;;;;;;;;;;;AA4BD,SAAgB,oBAAmD,WAAiB;CAClF,MAAM,2BAA2B,UAAU;EACzC;EACA;EACA;EACA,WAA8C,2BAA2B;;EAGzE,qBAA0C;EAE1C,YAAY,GAAG,MAAa;AAC1B,SAAM,GAAG,KAAK;GAEd,MAAM,EAAE,WAAW,OAAO,MAAM,EAAE;AAClC,SAAA,SAAe;AACf,SAAA,SAAe,MAAA,cAAoB;;;;;;;;EASrC,IAAI,SAAoE;AACtE,UAAO,MAAA;;EAOT,OAAO,cAAsC;AAC3C,SAAM,SAAS,aAAa;AAC5B,SAAA,QAAc,QAAQ,aAAa,IAAI,aAAa;;EAGtD,SAAe;AACb,SAAA,mBAAyB;AACzB,SAAA,QAAc,QAAQ,aAAa,IAAI,KAAA,EAAU;AACjD,SAAM,UAAU;;EAGlB,UAAgB;AACd,SAAA,mBAAyB;AACzB,SAAA,OAAa,SAAS;;;EAQxB,IAAI,UAA6C;AAC/C,UAAO,MAAA;;EAGT,IAAI,QAAQ,OAA0C;AACpD,SAAA,UAAgB;AAChB,OAAI,MACF,OAAA,QAAc,MAAM,QAAQ,IAAI,MAAM;;EAa1C,IAAI,MAAc;AAChB,UAAO,MAAA,QAAc,MAAM,aAAa,KAAK,EAAE,OAAO;;EAGxD,IAAI,IAAI,OAAe;GACrB,MAAM,mBAAmB,MAAA,QAAc,QAAQ,aAAa,KAAK;AAEjE,SAAA,mBAAyB;AACzB,SAAA,OAAa,SAAS;AACtB,SAAA,SAAe,MAAA,cAAoB;AAMnC,OAAI,MAAA,QACF,OAAA,QAAc,MAAM,QAAQ,IAAI,MAAA,QAAc;AAGhD,OAAI,iBACF,OAAA,QAAc,QAAQ,aAAa,IAAI,iBAAiB;AAG1D,OAAI,MACF,OAAA,QAAc,MAAM,aAAa,IAAI,EAAE,KAAK,OAAO,CAAC;;EASxD,OAAsB;GACpB,MAAM,eAAe,MAAA,QAAc,QAAQ,aAAa,KAAK;AAC7D,OAAI,CAAC,aACH,QAAO,QAAQ,uBAAO,IAAI,MAAM,mDAAmD,CAAC;AAItF,SAAA,QAAc,MAAM,cAAc,IAAI,KAAK;AAE3C,UAAO,aAAa,MAAM,CAAC,OAAO,QAAiB;AAIjD,QAAI,KAAK,IACP,QAAO,IAAI,SAAe,SAAS,WAAW;KAC5C,MAAM,iBAAiB;AACrB,YAAA,oBAA0B;AAC1B,mBAAa,MAAM,CAAC,KAAK,SAAS,OAAO;;AAE3C,WAAA,oBAA0B;AAC1B,kBAAa,iBAAiB,aAAa,UAAU,EAAE,MAAM,MAAM,CAAC;MACpE;AAEJ,UAAM;KACN;;EAOJ,gBAA2E;AACzE,UAAO,sBAAsB;IAC3B,GAAG,MAAA;IACH,iBAAiB,YAAY;AAC3B,WAAA,UAAgB;;IAEnB,CAAC;;EAGJ,qBAA2B;AACzB,OAAI,CAAC,MAAA,kBAAyB;AACT,SAAA,QAAc,QAAQ,aAAa,KAAK,EAC/C,oBAAoB,aAAa,MAAA,kBAAwB;AACvE,SAAA,oBAA0B;;;AAI9B,QAAO;;;AAIT,IAAa,wBAAb,cAA2C,oBAAoB,MAAM,GAAG,CAAC"}
@@ -0,0 +1,80 @@
1
+ import { createComposition } from "../../../core/composition/create-composition.js";
2
+ import { makeShareSignals } from "../../../core/composition/share-signals.js";
3
+ import { loadAudioSegments } from "../../behaviors/dom/load-segments.js";
4
+ import { trackCurrentTime } from "../../behaviors/dom/track-current-time.js";
5
+ import { trackLoadTriggers } from "../../behaviors/dom/track-load-triggers.js";
6
+ import { canPlayTrack } from "../../../media/dom/capabilities.js";
7
+ import { parseMultivariantPlaylist } from "../../../media/hls/parse-multivariant.js";
8
+ import { getResolvedSelectedTrackDuration } from "../../../media/utils/track-selection.js";
9
+ import { calculatePresentationDuration } from "../../behaviors/calculate-presentation-duration.js";
10
+ import { deriveCdnPriority } from "../../behaviors/derive-cdn-priority.js";
11
+ import { endOfStream } from "../../behaviors/dom/end-of-stream.js";
12
+ import { setupAudioBufferActors } from "../../behaviors/dom/setup-buffer-actors.js";
13
+ import { setupMediaSource } from "../../behaviors/dom/setup-mediasource.js";
14
+ import { updateMediaSourceDuration } from "../../behaviors/dom/update-mediasource-duration.js";
15
+ import { resolvePresentation } from "../../behaviors/resolve-presentation.js";
16
+ import { resolveAudioTrack } from "../../behaviors/resolve-track.js";
17
+ import { setupFailoverMonitor } from "../../behaviors/setup-failover-monitor.js";
18
+ import { syncPreload } from "../../behaviors/sync-preload.js";
19
+ import { switchAudioTrack } from "../../behaviors/track-switching.js";
20
+ //#region src/playback/engines/hls/engine-audio-only.ts
21
+ const shareSignals = makeShareSignals(["userAudioTrackSelection"]);
22
+ /**
23
+ * Create an audio-only HLS playback engine.
24
+ *
25
+ * Subtractive composition variant of `createSimpleHlsEngine`: omits
26
+ * video-side behaviors (`resolveVideoTrack`, `switchVideoTrack`,
27
+ * `setupVideoBufferActors`, `loadVideoSegments`) and text-track behaviors
28
+ * (`switchTextTrack`, `resolveTextTrack`, `syncTextTracks`,
29
+ * `setupTextTrackActors`, `loadTextTrackSegments`). The remaining audio
30
+ * pipeline composes unchanged.
31
+ *
32
+ * Handles both truly audio-only HLS sources (no video stream-inf) and
33
+ * mixed-AV HLS sources where the audio rendition is selected and video /
34
+ * subtitle renditions are ignored at composition time. The variant decision
35
+ * is encoded by adapter choice; this engine does not branch on source
36
+ * shape.
37
+ *
38
+ * @example
39
+ * ```ts
40
+ * let signals: SimpleHlsAudioOnlyEngineSignals;
41
+ * const engine = createHlsAudioOnlyEngine({
42
+ * preferredAudioLanguage: 'en',
43
+ * onSignalsReady: (refs) => {
44
+ * signals = refs;
45
+ * },
46
+ * });
47
+ *
48
+ * signals.context.mediaElement.set(audioEl);
49
+ * signals.state.presentation.set({ url: 'https://example.com/stream.m3u8' });
50
+ * ```
51
+ */
52
+ function createHlsAudioOnlyEngine(config = {}) {
53
+ const finalConfig = {
54
+ ...config,
55
+ canPlayTrack: config.canPlayTrack ?? canPlayTrack,
56
+ resolveDuration: config.resolveDuration ?? getResolvedSelectedTrackDuration,
57
+ parsePresentation: config.parsePresentation ?? parseMultivariantPlaylist
58
+ };
59
+ return createComposition([
60
+ syncPreload,
61
+ trackLoadTriggers,
62
+ resolvePresentation,
63
+ deriveCdnPriority,
64
+ setupFailoverMonitor,
65
+ switchAudioTrack,
66
+ resolveAudioTrack,
67
+ calculatePresentationDuration,
68
+ setupMediaSource,
69
+ updateMediaSourceDuration,
70
+ setupAudioBufferActors,
71
+ trackCurrentTime,
72
+ loadAudioSegments,
73
+ endOfStream,
74
+ shareSignals
75
+ ], { config: finalConfig });
76
+ }
77
+ //#endregion
78
+ export { createHlsAudioOnlyEngine };
79
+
80
+ //# sourceMappingURL=engine-audio-only.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine-audio-only.js","names":[],"sources":["../../../../../src/playback/engines/hls/engine-audio-only.ts"],"sourcesContent":["import {\n type Composition,\n type ContextSignals,\n createComposition,\n type StateSignals,\n} from '../../../core/composition/create-composition';\nimport { makeShareSignals, type ShareSignalsConfig } from '../../../core/composition/share-signals';\nimport type { BackBufferConfig } from '../../../media/buffer/back-buffer';\nimport type { ForwardBufferConfig } from '../../../media/buffer/forward-buffer';\nimport { canPlayTrack } from '../../../media/dom/capabilities';\nimport { parseMultivariantPlaylist } from '../../../media/hls/parse-multivariant';\nimport type { AudioTrack, CanPlayTrack, MaybeResolvedPresentation } from '../../../media/types';\nimport type { GetCdnId } from '../../../media/utils/cdn';\nimport { getResolvedSelectedTrackDuration } from '../../../media/utils/track-selection';\nimport type { SegmentLoaderActor } from '../../actors/dom/segment-loader';\nimport type { SourceBufferActor } from '../../actors/dom/source-buffer';\nimport {\n calculatePresentationDuration,\n type PresentationDurationResolver,\n} from '../../behaviors/calculate-presentation-duration';\nimport { deriveCdnPriority } from '../../behaviors/derive-cdn-priority';\nimport { endOfStream } from '../../behaviors/dom/end-of-stream';\nimport { loadAudioSegments } from '../../behaviors/dom/load-segments';\nimport { setupAudioBufferActors } from '../../behaviors/dom/setup-buffer-actors';\nimport { setupMediaSource } from '../../behaviors/dom/setup-mediasource';\nimport { trackCurrentTime } from '../../behaviors/dom/track-current-time';\nimport { trackLoadTriggers } from '../../behaviors/dom/track-load-triggers';\nimport { updateMediaSourceDuration } from '../../behaviors/dom/update-mediasource-duration';\nimport { type ParsePresentation, resolvePresentation } from '../../behaviors/resolve-presentation';\nimport { resolveAudioTrack } from '../../behaviors/resolve-track';\nimport { type FailoverMonitorConfig, setupFailoverMonitor } from '../../behaviors/setup-failover-monitor';\nimport { syncPreload } from '../../behaviors/sync-preload';\nimport { switchAudioTrack } from '../../behaviors/track-switching';\n\n// ============================================================================\n// Audio-Only HLS Engine State & Context\n// ============================================================================\n\n/**\n * State shape for the audio-only HLS playback engine.\n *\n * Subset of `SimpleHlsEngineState` covering only the slots written and read\n * by audio-side behaviors. Video and text-track slots are absent —\n * subtractive composition removes the behaviors that declare them.\n */\nexport interface SimpleHlsAudioOnlyEngineState {\n presentation?: MaybeResolvedPresentation;\n preload?: 'auto' | 'metadata' | 'none';\n selectedAudioTrackId?: string;\n /**\n * Consumer-driven constraint narrowing the audio candidate set. Sibling\n * of `userVideoTrackSelection` in the default engine. Partial-track\n * shape — `{ language: 'es' }`, `{ id: 'audio-en' }`, etc.\n * `selectAudioTrack` reads this and re-picks when it changes.\n * Multi-language-audio Tier 2 programmatic-write path.\n */\n userAudioTrackSelection?: Partial<AudioTrack>;\n /**\n * The CDNs the source is served from, in manifest priority order (mirrors\n * HLS content steering's `PATHWAY-PRIORITY`). Owned by `deriveCdnPriority`,\n * read by `track-switching`'s `preferActiveCdn` scope. Only meaningful for\n * redundant-stream sources; a single-CDN source has one entry.\n */\n cdnPriority?: string[];\n /**\n * CDN ids currently in failover cooldown — read by `track-switching`'s\n * `excludeFailedCdns` constraint, which prunes their tracks so the active-CDN\n * scope falls to the next CDN. Empty / absent means all CDNs are eligible.\n */\n failedCdns?: string[];\n currentTime?: number;\n loadActivated?: boolean;\n}\n\n/**\n * Context shape for the audio-only HLS playback engine.\n *\n * Subset of `SimpleHlsEngineContext` covering only the platform objects and\n * actor refs managed by audio-side behaviors.\n */\nexport interface SimpleHlsAudioOnlyEngineContext {\n mediaElement?: HTMLMediaElement | undefined;\n mediaSource?: MediaSource;\n audioBufferActor?: SourceBufferActor;\n audioSegmentLoaderActor?: SegmentLoaderActor;\n}\n\nexport type SimpleHlsAudioOnlyEngineSignals = {\n state: StateSignals<SimpleHlsAudioOnlyEngineState>;\n context: ContextSignals<SimpleHlsAudioOnlyEngineContext>;\n};\n\n/**\n * Configuration for the audio-only HLS playback engine.\n *\n * Subset of `SimpleHlsEngineConfig` — video-quality, bandwidth-estimator,\n * and text-track config fields are omitted (no behavior consumes them).\n */\nexport interface SimpleHlsAudioOnlyEngineConfig\n extends ShareSignalsConfig<SimpleHlsAudioOnlyEngineState, SimpleHlsAudioOnlyEngineContext> {\n preferredAudioLanguage?: string;\n /**\n * Codec capability probe read by `track-switching`'s `excludeUnplayableTracks`\n * constraint. Defaults to the `MediaSource.isTypeSupported`-backed\n * `canPlayTrack`; override to force-exclude a codec. Mirrors the default\n * engine — without it, capability probing (and TS / raw-AAC detection) would\n * be inert for audio-only playback.\n */\n canPlayTrack?: CanPlayTrack;\n resolveDuration?: PresentationDurationResolver;\n parsePresentation?: ParsePresentation;\n forwardBuffer?: Partial<ForwardBufferConfig>;\n backBuffer?: Partial<BackBufferConfig>;\n /** Multi-CDN failover monitor tuning. Defaults: `DEFAULT_FAILOVER_MONITOR_CONFIG`. */\n failover?: Partial<FailoverMonitorConfig>;\n /**\n * Derive a CDN grouping key from a track URL (used by `cdnPriority`, the\n * failover trip, and the track-switching CDN rules — one function read by all).\n * Defaults to the URL origin; override to key on e.g. Mux's `cdn=` param.\n */\n getCdnId?: GetCdnId;\n}\n\n// ============================================================================\n// Audio-Only HLS Playback Engine\n// ============================================================================\n\n// Materializes input slots no composed behavior produces — `userAudioTrackSelection`\n// (switchAudioTrack only reads it) — in addition to forwarding refs. `failedCdns`\n// is owned by `setupFailoverMonitor`, so it's already materialized and reachable\n// on the `onSignalsReady` refs without being listed here.\nconst shareSignals = makeShareSignals<SimpleHlsAudioOnlyEngineState, SimpleHlsAudioOnlyEngineContext>([\n 'userAudioTrackSelection',\n]);\n\n/**\n * Create an audio-only HLS playback engine.\n *\n * Subtractive composition variant of `createSimpleHlsEngine`: omits\n * video-side behaviors (`resolveVideoTrack`, `switchVideoTrack`,\n * `setupVideoBufferActors`, `loadVideoSegments`) and text-track behaviors\n * (`switchTextTrack`, `resolveTextTrack`, `syncTextTracks`,\n * `setupTextTrackActors`, `loadTextTrackSegments`). The remaining audio\n * pipeline composes unchanged.\n *\n * Handles both truly audio-only HLS sources (no video stream-inf) and\n * mixed-AV HLS sources where the audio rendition is selected and video /\n * subtitle renditions are ignored at composition time. The variant decision\n * is encoded by adapter choice; this engine does not branch on source\n * shape.\n *\n * @example\n * ```ts\n * let signals: SimpleHlsAudioOnlyEngineSignals;\n * const engine = createHlsAudioOnlyEngine({\n * preferredAudioLanguage: 'en',\n * onSignalsReady: (refs) => {\n * signals = refs;\n * },\n * });\n *\n * signals.context.mediaElement.set(audioEl);\n * signals.state.presentation.set({ url: 'https://example.com/stream.m3u8' });\n * ```\n */\nexport function createHlsAudioOnlyEngine(\n config: SimpleHlsAudioOnlyEngineConfig = {}\n): Composition<SimpleHlsAudioOnlyEngineState, SimpleHlsAudioOnlyEngineContext> {\n const finalConfig = {\n ...config,\n canPlayTrack: config.canPlayTrack ?? canPlayTrack,\n resolveDuration: config.resolveDuration ?? getResolvedSelectedTrackDuration,\n parsePresentation: config.parsePresentation ?? parseMultivariantPlaylist,\n };\n\n return createComposition(\n [\n syncPreload,\n trackLoadTriggers,\n resolvePresentation,\n\n // Session-level CDN priority for redundant-stream sources. Owns\n // `cdnPriority`; switchAudioTrack's preferActiveCdn scope reads it. No-op\n // for single-CDN sources.\n //\n // With a single track type there's no cross-type coherence to enforce and\n // the first pick is the primary CDN regardless, so composition order is\n // not load-bearing here today. It earns its place for forward-consistency\n // with the default engine and for future failover / steering, where the\n // active CDN changes dynamically (and selection stays reactive either way).\n deriveCdnPriority,\n\n // CDN failover cooldown: watches `failedCdns` (tripped directly by audio\n // track resolution on a failed media-playlist fetch) and removes each CDN\n // once its cooldown lapses.\n setupFailoverMonitor,\n\n // Audio track selection — slot owner with filter reactivity.\n // Mid-stream flush on language switch is handled in segment-loader's\n // planTasks, not here.\n switchAudioTrack,\n\n // Resolve selected tracks — audio only.\n resolveAudioTrack,\n\n // Presentation duration\n calculatePresentationDuration,\n\n // MSE setup. Single audio buffer; no video buffer to coordinate with,\n // so the Firefox `mozHasAudio` registration ordering is moot here.\n setupMediaSource,\n updateMediaSourceDuration,\n setupAudioBufferActors,\n\n // Playback tracking\n trackCurrentTime,\n\n // Segment loading — audio only.\n loadAudioSegments,\n\n // End of stream coordination. `endOfStream` iterates buffer actors via\n // `[videoBufferActor, audioBufferActor].filter(Boolean)` and reads\n // `mediaSource.sourceBuffers` aggregately — composes unchanged with\n // only audio in scope.\n endOfStream,\n\n // Adapter signal callback.\n shareSignals,\n ],\n {\n config: finalConfig,\n }\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAmIA,MAAM,eAAe,iBAAiF,CACpG,0BACD,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCF,SAAgB,yBACd,SAAyC,EAAE,EACkC;CAC7E,MAAM,cAAc;EAClB,GAAG;EACH,cAAc,OAAO,gBAAgB;EACrC,iBAAiB,OAAO,mBAAmB;EAC3C,mBAAmB,OAAO,qBAAqB;EAChD;AAED,QAAO,kBACL;EACE;EACA;EACA;EAWA;EAKA;EAKA;EAGA;EAGA;EAIA;EACA;EACA;EAGA;EAGA;EAMA;EAGA;EACD,EACD,EACE,QAAQ,aACT,CACF"}
@@ -5,27 +5,37 @@ import { loadAudioSegments, loadTextTrackSegments, loadVideoSegments } from "../
5
5
  import { setupTextTrackActors } from "../../behaviors/dom/setup-text-track-actors.js";
6
6
  import { trackCurrentTime } from "../../behaviors/dom/track-current-time.js";
7
7
  import { trackLoadTriggers } from "../../behaviors/dom/track-load-triggers.js";
8
+ import { canPlayTrack } from "../../../media/dom/capabilities.js";
8
9
  import { addSubtitlesTracksToMedia, getShowingSubtitlesTrackFromMedia, removeAllSubtitlesTracksFromMedia } from "../../../media/dom/text/text-track-slots.js";
9
10
  import { parseMultivariantPlaylist } from "../../../media/hls/parse-multivariant.js";
10
11
  import { getResolvedSelectedTrackDuration } from "../../../media/utils/track-selection.js";
11
12
  import { calculatePresentationDuration } from "../../behaviors/calculate-presentation-duration.js";
13
+ import { deriveCdnPriority } from "../../behaviors/derive-cdn-priority.js";
12
14
  import { endOfStream } from "../../behaviors/dom/end-of-stream.js";
13
15
  import { setupAudioBufferActors, setupVideoBufferActors } from "../../behaviors/dom/setup-buffer-actors.js";
14
16
  import { setupMediaSource } from "../../behaviors/dom/setup-mediasource.js";
15
17
  import { syncTextTracks } from "../../behaviors/dom/sync-text-tracks.js";
16
18
  import { updateMediaSourceDuration } from "../../behaviors/dom/update-mediasource-duration.js";
17
- import { switchVideoQuality } from "../../behaviors/quality-switching.js";
18
19
  import { resolvePresentation } from "../../behaviors/resolve-presentation.js";
19
20
  import { resolveAudioTrack, resolveTextTrack, resolveVideoTrack } from "../../behaviors/resolve-track.js";
20
- import { selectAudioTrack, selectTextTrack } from "../../behaviors/select-tracks.js";
21
+ import { setupFailoverMonitor } from "../../behaviors/setup-failover-monitor.js";
21
22
  import { syncPreload } from "../../behaviors/sync-preload.js";
23
+ import { switchAudioTrack, switchTextTrack, switchVideoTrack } from "../../behaviors/track-switching.js";
22
24
  //#region src/playback/engines/hls/engine.ts
23
25
  /**
24
26
  * Generic `shareSignals` instantiated against the HLS engine's full state
25
27
  * and context — captures composition signal refs into the consumer's
26
- * `onSignalsReady` callback at setup time.
28
+ * `onSignalsReady` callback at setup time, and materializes input slots that no
29
+ * composed behavior produces: `user*TrackSelection` (track-switching only reads
30
+ * them). `failedCdns` is owned by `setupFailoverMonitor`, so it's already
31
+ * materialized and reachable on the `onSignalsReady` refs without being listed
32
+ * here.
27
33
  */
28
- const shareSignals = makeShareSignals();
34
+ const shareSignals = makeShareSignals([
35
+ "userVideoTrackSelection",
36
+ "userAudioTrackSelection",
37
+ "userTextTrackSelection"
38
+ ]);
29
39
  /**
30
40
  * Create an HLS playback engine.
31
41
  *
@@ -55,6 +65,7 @@ const shareSignals = makeShareSignals();
55
65
  function createSimpleHlsEngine(config = {}) {
56
66
  const finalConfig = {
57
67
  ...config,
68
+ canPlayTrack: config.canPlayTrack ?? canPlayTrack,
58
69
  resolveTextTrackSegment: config.resolveTextTrackSegment ?? resolveVttSegment,
59
70
  resolveDuration: config.resolveDuration ?? getResolvedSelectedTrackDuration,
60
71
  parsePresentation: config.parsePresentation ?? parseMultivariantPlaylist,
@@ -66,8 +77,8 @@ function createSimpleHlsEngine(config = {}) {
66
77
  syncPreload,
67
78
  trackLoadTriggers,
68
79
  resolvePresentation,
69
- selectAudioTrack,
70
- selectTextTrack,
80
+ deriveCdnPriority,
81
+ setupFailoverMonitor,
71
82
  resolveVideoTrack,
72
83
  resolveAudioTrack,
73
84
  resolveTextTrack,
@@ -77,7 +88,9 @@ function createSimpleHlsEngine(config = {}) {
77
88
  setupVideoBufferActors,
78
89
  setupAudioBufferActors,
79
90
  trackCurrentTime,
80
- switchVideoQuality,
91
+ switchVideoTrack,
92
+ switchAudioTrack,
93
+ switchTextTrack,
81
94
  loadVideoSegments,
82
95
  loadAudioSegments,
83
96
  endOfStream,
@@ -1 +1 @@
1
- {"version":3,"file":"engine.js","names":[],"sources":["../../../../../src/playback/engines/hls/engine.ts"],"sourcesContent":["import {\n type Composition,\n type ContextSignals,\n createComposition,\n type StateSignals,\n} from '../../../core/composition/create-composition';\nimport { makeShareSignals, type ShareSignalsConfig } from '../../../core/composition/share-signals';\nimport type { QualityConfig } from '../../../media/abr/quality-selection';\nimport type { BackBufferConfig } from '../../../media/buffer/back-buffer';\nimport type { ForwardBufferConfig } from '../../../media/buffer/forward-buffer';\nimport { resolveVttSegment } from '../../../media/dom/text/resolve-vtt-segment';\nimport {\n addSubtitlesTracksToMedia,\n getShowingSubtitlesTrackFromMedia,\n removeAllSubtitlesTracksFromMedia,\n} from '../../../media/dom/text/text-track-slots';\nimport { parseMultivariantPlaylist } from '../../../media/hls/parse-multivariant';\nimport type { MaybeResolvedPresentation, VideoTrack } from '../../../media/types';\nimport { getResolvedSelectedTrackDuration } from '../../../media/utils/track-selection';\nimport type { BandwidthConfig, BandwidthState } from '../../../network/bandwidth-estimator';\nimport type { SegmentLoaderActor } from '../../actors/dom/segment-loader';\nimport type { SourceBufferActor } from '../../actors/dom/source-buffer';\nimport type { TextTracksActor } from '../../actors/dom/text-tracks';\nimport type { TextTrackSegmentLoaderActor, TextTrackSegmentResolver } from '../../actors/text-track-segment-loader';\nimport {\n calculatePresentationDuration,\n type PresentationDurationResolver,\n} from '../../behaviors/calculate-presentation-duration';\nimport { endOfStream } from '../../behaviors/dom/end-of-stream';\nimport { loadAudioSegments, loadTextTrackSegments, loadVideoSegments } from '../../behaviors/dom/load-segments';\nimport { setupAudioBufferActors, setupVideoBufferActors } from '../../behaviors/dom/setup-buffer-actors';\nimport { setupMediaSource } from '../../behaviors/dom/setup-mediasource';\nimport { setupTextTrackActors } from '../../behaviors/dom/setup-text-track-actors';\nimport { syncTextTracks } from '../../behaviors/dom/sync-text-tracks';\nimport { trackCurrentTime } from '../../behaviors/dom/track-current-time';\nimport { trackLoadTriggers } from '../../behaviors/dom/track-load-triggers';\nimport { updateMediaSourceDuration } from '../../behaviors/dom/update-mediasource-duration';\nimport { switchVideoQuality } from '../../behaviors/quality-switching';\nimport { type ParsePresentation, resolvePresentation } from '../../behaviors/resolve-presentation';\nimport { resolveAudioTrack, resolveTextTrack, resolveVideoTrack } from '../../behaviors/resolve-track';\nimport { selectAudioTrack, selectTextTrack } from '../../behaviors/select-tracks';\nimport { syncPreload } from '../../behaviors/sync-preload';\n\n// ============================================================================\n// HLS Engine State & Context\n// ============================================================================\n\n/**\n * State shape for the HLS playback engine.\n *\n * This is the union of all state required by the behaviors composed into\n * the HLS engine. Each behavior declares its own state interface; this\n * type satisfies all of them.\n */\nexport interface SimpleHlsEngineState {\n /**\n * The presentation being played. A caller writes `{ url }`;\n * `resolvePresentation` parses the manifest and populates the rest.\n */\n presentation?: MaybeResolvedPresentation;\n preload?: 'auto' | 'metadata' | 'none';\n selectedVideoTrackId?: string;\n selectedAudioTrackId?: string;\n selectedTextTrackId?: string;\n bandwidthState?: BandwidthState;\n userVideoTrackSelection?: Partial<VideoTrack>;\n currentTime?: number;\n loadActivated?: boolean;\n}\n\n/**\n * Context shape for the HLS playback engine.\n *\n * Platform objects and actor references managed by HLS behaviors.\n */\nexport interface SimpleHlsEngineContext {\n mediaElement?: HTMLMediaElement | undefined;\n mediaSource?: MediaSource;\n videoBufferActor?: SourceBufferActor;\n audioBufferActor?: SourceBufferActor;\n videoSegmentLoaderActor?: SegmentLoaderActor;\n audioSegmentLoaderActor?: SegmentLoaderActor;\n textTracksActor?: TextTracksActor;\n textTrackSegmentLoaderActor?: TextTrackSegmentLoaderActor;\n}\n\n/**\n * The composition signal refs handed to `onSignalsReady` callers — the\n * canonical way to drive the engine externally (writes) or observe its\n * state (reads) without touching `composition.state` / `composition.context`\n * directly.\n */\nexport type SimpleHlsEngineSignals = {\n state: StateSignals<SimpleHlsEngineState>;\n context: ContextSignals<SimpleHlsEngineContext>;\n};\n\n/**\n * Configuration for the HLS playback engine.\n *\n * Each option is consumed by the appropriate behavior — the engine itself\n * has no config beyond what its behaviors read.\n */\nexport interface SimpleHlsEngineConfig extends ShareSignalsConfig<SimpleHlsEngineState, SimpleHlsEngineContext> {\n /**\n * Bandwidth estimate in bps to use before enough samples have been\n * collected. Default: `DEFAULT_INITIAL_BANDWIDTH` (5 Mbps).\n */\n initialBandwidth?: number;\n preferredAudioLanguage?: string;\n preferredSubtitleLanguage?: string;\n includeForcedTracks?: boolean;\n enableDefaultTrack?: boolean;\n /**\n * Resolver that turns a text-track segment fetch into VTT cues.\n * Defaults to the DOM-bound `resolveVttSegment` resolver, which uses an\n * offscreen `<track>` element to parse WebVTT.\n */\n resolveTextTrackSegment?: TextTrackSegmentResolver<VTTCue>;\n /**\n * Resolver for `presentation.duration`. Defaults to picking the first\n * resolved selected track's duration (video preferred, audio fallback) —\n * appropriate for VoD and audio-only. Live engines should supply a\n * resolver that returns `Number.POSITIVE_INFINITY` once the presentation\n * is established as live; downstream `updateMediaSourceDuration` propagates\n * that value to `mediaSource.duration` per the MSE spec.\n */\n resolveDuration?: PresentationDurationResolver;\n /**\n * Manifest parser handed to `resolvePresentation`. Defaults to the HLS\n * multivariant-playlist parser; supply your own for alternate format\n * support without forking the engine.\n */\n parsePresentation?: ParsePresentation;\n /**\n * Allocate SPF-owned text-track slots on the media element. Defaults to\n * the standard `<track>`-element implementation in\n * `media/dom/text/text-track-slots`.\n */\n addSubtitlesTracksToMedia?: typeof addSubtitlesTracksToMedia;\n /**\n * Return the SPF-owned subtitle/caption `TextTrack` currently in showing\n * mode. Defaults to the standard selector-based implementation in\n * `media/dom/text/text-track-slots`.\n */\n getShowingSubtitlesTrackFromMedia?: typeof getShowingSubtitlesTrackFromMedia;\n /**\n * Evict all SPF-owned text-track slots from the media element. Defaults to\n * the standard selector-based implementation in\n * `media/dom/text/text-track-slots`.\n */\n removeAllSubtitlesTracksFromMedia?: typeof removeAllSubtitlesTracksFromMedia;\n /**\n * Forward-buffer tuning. `bufferDuration` controls how far ahead of the\n * playhead segments are loaded (and where forward-flush kicks in).\n * Defaults: see `DEFAULT_FORWARD_BUFFER_CONFIG` (30 seconds). Threaded to\n * segment-loader actors (v/a + text) at construction time and to\n * `loadXSegments` dispatchers for the load-message range.\n */\n forwardBuffer?: Partial<ForwardBufferConfig>;\n /**\n * Back-buffer tuning. `keepSegments` controls how many segments stay\n * behind the playhead before eviction. Defaults: see\n * `DEFAULT_BACK_BUFFER_CONFIG` (2 segments). Threaded to the v/a\n * segment-loader actor only (text tracks don't use back-buffer eviction).\n */\n backBuffer?: Partial<BackBufferConfig>;\n /**\n * Bandwidth-estimator tuning. Overrides any field of `BandwidthConfig`\n * (`fastHalfLife`, `slowHalfLife`, `minTotalBytes`, `minBytes`,\n * `minDuration`). `bandwidth.minTotalBytes` supersedes the flat\n * `minTotalBytes` field above. Defaults: see `DEFAULT_BANDWIDTH_CONFIG`.\n */\n bandwidth?: Partial<BandwidthConfig>;\n /**\n * Quality-selection tuning. `safetyMargin` is the bandwidth-headroom\n * multiplier used by `selectQuality`; `upgradeMargin` is the hysteresis\n * ratio gating ABR upgrades. Defaults: `DEFAULT_QUALITY_CONFIG` (0.85 / 1.15).\n */\n quality?: Partial<QualityConfig>;\n}\n\n// ============================================================================\n// HLS Playback Engine\n// ============================================================================\n\n/**\n * Generic `shareSignals` instantiated against the HLS engine's full state\n * and context — captures composition signal refs into the consumer's\n * `onSignalsReady` callback at setup time.\n */\nconst shareSignals = makeShareSignals<SimpleHlsEngineState, SimpleHlsEngineContext>();\n\n/**\n * Create an HLS playback engine.\n *\n * Composes SPF behaviors into a reactive pipeline for HLS playback over MSE:\n * manifest resolution, track selection, ABR, segment loading, and\n * end-of-stream coordination.\n *\n * @example\n * ```ts\n * let signals: SimpleHlsEngineSignals;\n * const engine = createSimpleHlsEngine({\n * initialBandwidth: 2_000_000,\n * preferredAudioLanguage: 'en',\n * onSignalsReady: (refs) => {\n * signals = refs;\n * },\n * });\n *\n * signals.context.mediaElement.set(videoEl);\n * signals.state.presentation.set({ url: 'https://example.com/stream.m3u8' });\n *\n * videoEl.play();\n *\n * await engine.destroy();\n * ```\n */\nexport function createSimpleHlsEngine(\n config: SimpleHlsEngineConfig = {}\n): Composition<SimpleHlsEngineState, SimpleHlsEngineContext> {\n const finalConfig = {\n ...config,\n resolveTextTrackSegment: config.resolveTextTrackSegment ?? resolveVttSegment,\n resolveDuration: config.resolveDuration ?? getResolvedSelectedTrackDuration,\n parsePresentation: config.parsePresentation ?? parseMultivariantPlaylist,\n addSubtitlesTracksToMedia: config.addSubtitlesTracksToMedia ?? addSubtitlesTracksToMedia,\n getShowingSubtitlesTrackFromMedia: config.getShowingSubtitlesTrackFromMedia ?? getShowingSubtitlesTrackFromMedia,\n removeAllSubtitlesTracksFromMedia: config.removeAllSubtitlesTracksFromMedia ?? removeAllSubtitlesTracksFromMedia,\n };\n\n return createComposition(\n [\n syncPreload,\n trackLoadTriggers,\n resolvePresentation,\n\n // Track selection (reads config for initial preferences).\n // Video selection lives in switchVideoQuality, which owns the\n // default-pick + ABR-driven adjustment for selectedVideoTrackId.\n selectAudioTrack,\n selectTextTrack,\n\n // Resolve selected tracks (fetch media playlists)\n resolveVideoTrack,\n resolveAudioTrack,\n resolveTextTrack,\n\n // Presentation duration\n calculatePresentationDuration,\n\n // MSE setup. Video cluster is registered first so that, when both\n // per-type variants flip to `'buffer-ready'` on the shared gate's\n // monitor evaluation, `addSourceBuffer(video)` runs before\n // `addSourceBuffer(audio)` — see the Firefox `mozHasAudio` invariant\n // in setup-buffer-actors.ts.\n setupMediaSource,\n updateMediaSourceDuration,\n setupVideoBufferActors,\n setupAudioBufferActors,\n\n // Playback tracking\n trackCurrentTime,\n switchVideoQuality,\n\n // Segment loading\n loadVideoSegments,\n loadAudioSegments,\n\n // End of stream coordination\n endOfStream,\n\n // Text tracks\n syncTextTracks,\n setupTextTrackActors,\n loadTextTrackSegments,\n\n // Behavior whose sole purpose is to use a callback to allow for signal writing from the outside (e.g. an adapter)\n // NOTE: While not required, adding at the end since behaviors are setup in order, so this increases the likelihood\n // that initial signal setup will have occurred before shareSignals' callback is invoked. (CJP)\n shareSignals,\n ],\n {\n config: finalConfig,\n // Seed bandwidthState so switchVideoQuality fires on initial subscribe\n // with the `initialBandwidth` fallback rather than waiting for the\n // first chunk. The empty sample buffer means `getBandwidthEstimate`\n // returns the configured initial bandwidth until real samples land.\n initialState: {\n bandwidthState: {\n fastEstimate: 0,\n fastTotalWeight: 0,\n slowEstimate: 0,\n slowTotalWeight: 0,\n bytesSampled: 0,\n },\n },\n }\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AA+LA,MAAM,eAAe,kBAAgE;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BrF,SAAgB,sBACd,SAAgC,EAAE,EACyB;CAC3D,MAAM,cAAc;EAClB,GAAG;EACH,yBAAyB,OAAO,2BAA2B;EAC3D,iBAAiB,OAAO,mBAAmB;EAC3C,mBAAmB,OAAO,qBAAqB;EAC/C,2BAA2B,OAAO,6BAA6B;EAC/D,mCAAmC,OAAO,qCAAqC;EAC/E,mCAAmC,OAAO,qCAAqC;EAChF;AAED,QAAO,kBACL;EACE;EACA;EACA;EAKA;EACA;EAGA;EACA;EACA;EAGA;EAOA;EACA;EACA;EACA;EAGA;EACA;EAGA;EACA;EAGA;EAGA;EACA;EACA;EAKA;EACD,EACD;EACE,QAAQ;EAKR,cAAc,EACZ,gBAAgB;GACd,cAAc;GACd,iBAAiB;GACjB,cAAc;GACd,iBAAiB;GACjB,cAAc;GACf,EACF;EACF,CACF"}
1
+ {"version":3,"file":"engine.js","names":[],"sources":["../../../../../src/playback/engines/hls/engine.ts"],"sourcesContent":["import {\n type Composition,\n type ContextSignals,\n createComposition,\n type StateSignals,\n} from '../../../core/composition/create-composition';\nimport { makeShareSignals, type ShareSignalsConfig } from '../../../core/composition/share-signals';\nimport type { QualityConfig } from '../../../media/abr/quality-selection';\nimport type { BackBufferConfig } from '../../../media/buffer/back-buffer';\nimport type { ForwardBufferConfig } from '../../../media/buffer/forward-buffer';\nimport { canPlayTrack } from '../../../media/dom/capabilities';\nimport { resolveVttSegment } from '../../../media/dom/text/resolve-vtt-segment';\nimport {\n addSubtitlesTracksToMedia,\n getShowingSubtitlesTrackFromMedia,\n removeAllSubtitlesTracksFromMedia,\n} from '../../../media/dom/text/text-track-slots';\nimport { parseMultivariantPlaylist } from '../../../media/hls/parse-multivariant';\nimport type { AudioTrack, CanPlayTrack, MaybeResolvedPresentation, TextTrack, VideoTrack } from '../../../media/types';\nimport type { GetCdnId } from '../../../media/utils/cdn';\nimport { getResolvedSelectedTrackDuration } from '../../../media/utils/track-selection';\nimport type { BandwidthConfig, BandwidthState } from '../../../network/bandwidth-estimator';\nimport type { SegmentLoaderActor } from '../../actors/dom/segment-loader';\nimport type { SourceBufferActor } from '../../actors/dom/source-buffer';\nimport type { TextTracksActor } from '../../actors/dom/text-tracks';\nimport type { TextTrackSegmentLoaderActor, TextTrackSegmentResolver } from '../../actors/text-track-segment-loader';\nimport {\n calculatePresentationDuration,\n type PresentationDurationResolver,\n} from '../../behaviors/calculate-presentation-duration';\nimport { deriveCdnPriority } from '../../behaviors/derive-cdn-priority';\nimport { endOfStream } from '../../behaviors/dom/end-of-stream';\nimport { loadAudioSegments, loadTextTrackSegments, loadVideoSegments } from '../../behaviors/dom/load-segments';\nimport { setupAudioBufferActors, setupVideoBufferActors } from '../../behaviors/dom/setup-buffer-actors';\nimport { setupMediaSource } from '../../behaviors/dom/setup-mediasource';\nimport { setupTextTrackActors } from '../../behaviors/dom/setup-text-track-actors';\nimport { syncTextTracks } from '../../behaviors/dom/sync-text-tracks';\nimport { trackCurrentTime } from '../../behaviors/dom/track-current-time';\nimport { trackLoadTriggers } from '../../behaviors/dom/track-load-triggers';\nimport { updateMediaSourceDuration } from '../../behaviors/dom/update-mediasource-duration';\nimport { type ParsePresentation, resolvePresentation } from '../../behaviors/resolve-presentation';\nimport { resolveAudioTrack, resolveTextTrack, resolveVideoTrack } from '../../behaviors/resolve-track';\nimport { type FailoverMonitorConfig, setupFailoverMonitor } from '../../behaviors/setup-failover-monitor';\nimport { syncPreload } from '../../behaviors/sync-preload';\nimport { switchAudioTrack, switchTextTrack, switchVideoTrack } from '../../behaviors/track-switching';\n\n// ============================================================================\n// HLS Engine State & Context\n// ============================================================================\n\n/**\n * State shape for the HLS playback engine.\n *\n * This is the union of all state required by the behaviors composed into\n * the HLS engine. Each behavior declares its own state interface; this\n * type satisfies all of them.\n */\nexport interface SimpleHlsEngineState {\n /**\n * The presentation being played. A caller writes `{ url }`;\n * `resolvePresentation` parses the manifest and populates the rest.\n */\n presentation?: MaybeResolvedPresentation;\n preload?: 'auto' | 'metadata' | 'none';\n selectedVideoTrackId?: string;\n selectedAudioTrackId?: string;\n selectedTextTrackId?: string;\n bandwidthState?: BandwidthState;\n userVideoTrackSelection?: Partial<VideoTrack>;\n /**\n * Consumer-driven constraint narrowing the audio candidate set. Sibling\n * of `userVideoTrackSelection`. Partial-track shape — `{ language: 'es' }`,\n * `{ id: 'audio-en' }`, etc. `selectAudioTrack` reads this and re-picks\n * when it changes. Multi-language-audio Tier 2 programmatic-write path.\n */\n userAudioTrackSelection?: Partial<AudioTrack>;\n /**\n * Consumer-driven *intent* for text selection, resolved into\n * `selectedTextTrackId` by `switchTextTrack`. A language-based partial\n * (`{ language: 'es' }`) selects captions, `'off'` disables them, and absence\n * means auto (the engine's `preferredSubtitleLanguage` / DEFAULT-track policy).\n * Also the write path for the DOM caption UI (via `syncTextTracks`); unlike the\n * resolved id it persists across source changes (sticky preference).\n */\n userTextTrackSelection?: Partial<TextTrack> | 'off';\n /**\n * The CDNs the source is served from (track-URL origins), in manifest\n * priority order — most-preferred first (mirrors HLS content steering's\n * `PATHWAY-PRIORITY`). Owned by `deriveCdnPriority`, read by\n * `track-switching`'s `preferActiveCdn` scope, which narrows to the\n * highest-priority CDN with surviving tracks so video / audio / text stay on\n * one host. Only meaningful for redundant-stream sources; a single-CDN source\n * has one entry.\n */\n cdnPriority?: string[];\n /**\n * CDN ids (origins) currently in failover cooldown — written by the CDN\n * monitor when a host fails too often, read by `track-switching`'s\n * `excludeFailedCdns` hard constraint, which prunes their tracks so the\n * active-CDN scope falls to the next CDN in `cdnPriority`. Empty / absent\n * means all CDNs are eligible.\n */\n failedCdns?: string[];\n currentTime?: number;\n loadActivated?: boolean;\n}\n\n/**\n * Context shape for the HLS playback engine.\n *\n * Platform objects and actor references managed by HLS behaviors.\n */\nexport interface SimpleHlsEngineContext {\n mediaElement?: HTMLMediaElement | undefined;\n mediaSource?: MediaSource;\n videoBufferActor?: SourceBufferActor;\n audioBufferActor?: SourceBufferActor;\n videoSegmentLoaderActor?: SegmentLoaderActor;\n audioSegmentLoaderActor?: SegmentLoaderActor;\n textTracksActor?: TextTracksActor;\n textTrackSegmentLoaderActor?: TextTrackSegmentLoaderActor;\n}\n\n/**\n * The composition signal refs handed to `onSignalsReady` callers — the\n * canonical way to drive the engine externally (writes) or observe its\n * state (reads) without touching `composition.state` / `composition.context`\n * directly.\n */\nexport type SimpleHlsEngineSignals = {\n state: StateSignals<SimpleHlsEngineState>;\n context: ContextSignals<SimpleHlsEngineContext>;\n};\n\n/**\n * Configuration for the HLS playback engine.\n *\n * Each option is consumed by the appropriate behavior — the engine itself\n * has no config beyond what its behaviors read.\n */\nexport interface SimpleHlsEngineConfig extends ShareSignalsConfig<SimpleHlsEngineState, SimpleHlsEngineContext> {\n /**\n * Bandwidth estimate in bps to use before enough samples have been\n * collected. Default: `DEFAULT_INITIAL_BANDWIDTH` (5 Mbps).\n */\n initialBandwidth?: number;\n /**\n * Codec capability probe injected into `track-switching`'s\n * `excludeUnplayableTracks` constraint — drops renditions the environment\n * can't decode before selection. Defaults to the `MediaSource.isTypeSupported`\n * -backed `canPlayTrack`; supply your own to override (e.g. force-exclude a\n * codec).\n */\n canPlayTrack?: CanPlayTrack;\n preferredAudioLanguage?: string;\n preferredSubtitleLanguage?: string;\n includeForcedTracks?: boolean;\n enableDefaultTrack?: boolean;\n /**\n * Resolver that turns a text-track segment fetch into VTT cues.\n * Defaults to the DOM-bound `resolveVttSegment` resolver, which uses an\n * offscreen `<track>` element to parse WebVTT.\n */\n resolveTextTrackSegment?: TextTrackSegmentResolver<VTTCue>;\n /**\n * Resolver for `presentation.duration`. Defaults to picking the first\n * resolved selected track's duration (video preferred, audio fallback) —\n * appropriate for VoD and audio-only. Live engines should supply a\n * resolver that returns `Number.POSITIVE_INFINITY` once the presentation\n * is established as live; downstream `updateMediaSourceDuration` propagates\n * that value to `mediaSource.duration` per the MSE spec.\n */\n resolveDuration?: PresentationDurationResolver;\n /**\n * Manifest parser handed to `resolvePresentation`. Defaults to the HLS\n * multivariant-playlist parser; supply your own for alternate format\n * support without forking the engine.\n */\n parsePresentation?: ParsePresentation;\n /**\n * Allocate SPF-owned text-track slots on the media element. Defaults to\n * the standard `<track>`-element implementation in\n * `media/dom/text/text-track-slots`.\n */\n addSubtitlesTracksToMedia?: typeof addSubtitlesTracksToMedia;\n /**\n * Return the SPF-owned subtitle/caption `TextTrack` currently in showing\n * mode. Defaults to the standard selector-based implementation in\n * `media/dom/text/text-track-slots`.\n */\n getShowingSubtitlesTrackFromMedia?: typeof getShowingSubtitlesTrackFromMedia;\n /**\n * Evict all SPF-owned text-track slots from the media element. Defaults to\n * the standard selector-based implementation in\n * `media/dom/text/text-track-slots`.\n */\n removeAllSubtitlesTracksFromMedia?: typeof removeAllSubtitlesTracksFromMedia;\n /**\n * Forward-buffer tuning. `bufferDuration` controls how far ahead of the\n * playhead segments are loaded (and where forward-flush kicks in).\n * Defaults: see `DEFAULT_FORWARD_BUFFER_CONFIG` (30 seconds). Threaded to\n * segment-loader actors (v/a + text) at construction time and to\n * `loadXSegments` dispatchers for the load-message range.\n */\n forwardBuffer?: Partial<ForwardBufferConfig>;\n /**\n * Back-buffer tuning. `keepSegments` controls how many segments stay\n * behind the playhead before eviction. Defaults: see\n * `DEFAULT_BACK_BUFFER_CONFIG` (2 segments). Threaded to the v/a\n * segment-loader actor only (text tracks don't use back-buffer eviction).\n */\n backBuffer?: Partial<BackBufferConfig>;\n /**\n * Bandwidth-estimator tuning. Overrides any field of `BandwidthConfig`\n * (`fastHalfLife`, `slowHalfLife`, `minTotalBytes`, `minBytes`,\n * `minDuration`). `bandwidth.minTotalBytes` supersedes the flat\n * `minTotalBytes` field above. Defaults: see `DEFAULT_BANDWIDTH_CONFIG`.\n */\n bandwidth?: Partial<BandwidthConfig>;\n /**\n * Quality-selection tuning. `safetyMargin` is the bandwidth-headroom\n * multiplier used by `selectQuality`; `upgradeMargin` is the hysteresis\n * ratio gating ABR upgrades. Defaults: `DEFAULT_QUALITY_CONFIG` (0.85 / 1.15).\n */\n quality?: Partial<QualityConfig>;\n /**\n * Multi-CDN failover monitor tuning. `cooldownMs` is how long a CDN stays\n * excluded after a failed fetch trips it. Defaults:\n * `DEFAULT_FAILOVER_MONITOR_CONFIG` (300s). Only meaningful for redundant-stream\n * sources.\n */\n failover?: Partial<FailoverMonitorConfig>;\n /**\n * How to derive a CDN grouping key from a track URL — used to build\n * `cdnPriority`, to record the failover trip in `failedCdns`, and by the\n * track-switching CDN scope + failover constraint. One function, read by all of\n * them, so the keys stay comparable. Defaults to the URL origin; override to\n * key on something else (e.g. Mux's `cdn=` query param).\n */\n getCdnId?: GetCdnId;\n}\n\n// ============================================================================\n// HLS Playback Engine\n// ============================================================================\n\n/**\n * Generic `shareSignals` instantiated against the HLS engine's full state\n * and context — captures composition signal refs into the consumer's\n * `onSignalsReady` callback at setup time, and materializes input slots that no\n * composed behavior produces: `user*TrackSelection` (track-switching only reads\n * them). `failedCdns` is owned by `setupFailoverMonitor`, so it's already\n * materialized and reachable on the `onSignalsReady` refs without being listed\n * here.\n */\nconst shareSignals = makeShareSignals<SimpleHlsEngineState, SimpleHlsEngineContext>([\n 'userVideoTrackSelection',\n 'userAudioTrackSelection',\n 'userTextTrackSelection',\n]);\n\n/**\n * Create an HLS playback engine.\n *\n * Composes SPF behaviors into a reactive pipeline for HLS playback over MSE:\n * manifest resolution, track selection, ABR, segment loading, and\n * end-of-stream coordination.\n *\n * @example\n * ```ts\n * let signals: SimpleHlsEngineSignals;\n * const engine = createSimpleHlsEngine({\n * initialBandwidth: 2_000_000,\n * preferredAudioLanguage: 'en',\n * onSignalsReady: (refs) => {\n * signals = refs;\n * },\n * });\n *\n * signals.context.mediaElement.set(videoEl);\n * signals.state.presentation.set({ url: 'https://example.com/stream.m3u8' });\n *\n * videoEl.play();\n *\n * await engine.destroy();\n * ```\n */\nexport function createSimpleHlsEngine(\n config: SimpleHlsEngineConfig = {}\n): Composition<SimpleHlsEngineState, SimpleHlsEngineContext> {\n const finalConfig = {\n ...config,\n canPlayTrack: config.canPlayTrack ?? canPlayTrack,\n resolveTextTrackSegment: config.resolveTextTrackSegment ?? resolveVttSegment,\n resolveDuration: config.resolveDuration ?? getResolvedSelectedTrackDuration,\n parsePresentation: config.parsePresentation ?? parseMultivariantPlaylist,\n addSubtitlesTracksToMedia: config.addSubtitlesTracksToMedia ?? addSubtitlesTracksToMedia,\n getShowingSubtitlesTrackFromMedia: config.getShowingSubtitlesTrackFromMedia ?? getShowingSubtitlesTrackFromMedia,\n removeAllSubtitlesTracksFromMedia: config.removeAllSubtitlesTracksFromMedia ?? removeAllSubtitlesTracksFromMedia,\n };\n\n return createComposition(\n [\n syncPreload,\n trackLoadTriggers,\n resolvePresentation,\n\n // Session-level CDN priority for redundant-stream sources. Owns\n // `cdnPriority`; `track-switching`'s preferActiveCdn scope reads it so\n // every type stays on one CDN. No-op for single-CDN sources.\n //\n // Placed before switch* so `cdnPriority` is set before the first pick —\n // but this ordering is only *mildly* load-bearing, not required for\n // correctness. Selection is reactive: a late `cdnPriority` re-fires the\n // pick and converges on the same result (see the late-arrival test in\n // track-switching.test.ts). Order affects only a transient, and only for\n // an *asymmetric* manifest (a type listing a non-primary CDN first):\n // composing this after switch* would let that type fire one wasted\n // media-playlist fetch to the wrong CDN before correcting. Symmetric\n // redundant streams (the norm) never hit it — the first-listed CDN is\n // already the primary we'd pick anyway.\n deriveCdnPriority,\n\n // CDN failover cooldown: owns the expiry half of failover — watches\n // `failedCdns` (tripped directly by track resolution on a failed\n // media-playlist fetch) and removes each CDN once its cooldown lapses.\n setupFailoverMonitor,\n\n // Resolve selected tracks (fetch media playlists). Composed before the\n // switch* slot owners; selection is reactive, so a resolve* re-fires once\n // its switch* sets the id (same convergence for all three types).\n resolveVideoTrack,\n resolveAudioTrack,\n resolveTextTrack,\n\n // Presentation duration\n calculatePresentationDuration,\n\n // MSE setup. Video cluster is registered first so that, when both\n // per-type variants flip to `'buffer-ready'` on the shared gate's\n // monitor evaluation, `addSourceBuffer(video)` runs before\n // `addSourceBuffer(audio)` — see the Firefox `mozHasAudio` invariant\n // in setup-buffer-actors.ts.\n setupMediaSource,\n updateMediaSourceDuration,\n setupVideoBufferActors,\n setupAudioBufferActors,\n\n // Playback tracking\n trackCurrentTime,\n switchVideoTrack,\n switchAudioTrack,\n // Mid-stream audio-buffer flush on language switch is handled in\n // `segment-loader`'s `planTasks` (predicate: language differs from\n // the previously-buffered track) — not in switchAudioTrack itself.\n\n // Text selection: resolves `userTextTrackSelection` intent (incl. 'off',\n // or the configured preferred-language / DEFAULT-track policy) against the\n // failed-CDN-pruned, active-CDN-scoped text renditions. Optional selection\n // (captions are opt-in), so it can resolve to none.\n switchTextTrack,\n\n // Segment loading\n loadVideoSegments,\n loadAudioSegments,\n\n // End of stream coordination\n endOfStream,\n\n // Text tracks\n syncTextTracks,\n setupTextTrackActors,\n loadTextTrackSegments,\n\n // Behavior whose sole purpose is to use a callback to allow for signal writing from the outside (e.g. an adapter)\n // NOTE: While not required, adding at the end since behaviors are setup in order, so this increases the likelihood\n // that initial signal setup will have occurred before shareSignals' callback is invoked. (CJP)\n shareSignals,\n ],\n {\n config: finalConfig,\n // Seed bandwidthState so switchVideoTrack fires on initial subscribe\n // with the `initialBandwidth` fallback rather than waiting for the\n // first chunk. The empty sample buffer means `getBandwidthEstimate`\n // returns the configured initial bandwidth until real samples land.\n initialState: {\n bandwidthState: {\n fastEstimate: 0,\n fastTotalWeight: 0,\n slowEstimate: 0,\n slowTotalWeight: 0,\n bytesSampled: 0,\n },\n },\n }\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+PA,MAAM,eAAe,iBAA+D;CAClF;CACA;CACA;CACD,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BF,SAAgB,sBACd,SAAgC,EAAE,EACyB;CAC3D,MAAM,cAAc;EAClB,GAAG;EACH,cAAc,OAAO,gBAAgB;EACrC,yBAAyB,OAAO,2BAA2B;EAC3D,iBAAiB,OAAO,mBAAmB;EAC3C,mBAAmB,OAAO,qBAAqB;EAC/C,2BAA2B,OAAO,6BAA6B;EAC/D,mCAAmC,OAAO,qCAAqC;EAC/E,mCAAmC,OAAO,qCAAqC;EAChF;AAED,QAAO,kBACL;EACE;EACA;EACA;EAgBA;EAKA;EAKA;EACA;EACA;EAGA;EAOA;EACA;EACA;EACA;EAGA;EACA;EACA;EASA;EAGA;EACA;EAGA;EAGA;EACA;EACA;EAKA;EACD,EACD;EACE,QAAQ;EAKR,cAAc,EACZ,gBAAgB;GACd,cAAc;GACd,iBAAiB;GACjB,cAAc;GACd,iBAAiB;GACjB,cAAc;GACf,EACF;EACF,CACF"}
@@ -0,0 +1,42 @@
1
+ import { update } from "../../core/signals/primitives.js";
2
+ import { findTrackById } from "../../media/utils/tracks.js";
3
+ import { addFailedCdn, getCdnId } from "../../media/utils/cdn.js";
4
+ //#region src/playback/primitives/failover-fetch.ts
5
+ /**
6
+ * Decorate a fetch so a failed request trips the **selected track's** CDN into
7
+ * `failedCdns`. The decorated fetch's type is preserved, so this wraps both
8
+ * `resolve-track`'s playlist `FetchText` and the segment loaders' `FetchBytes`.
9
+ *
10
+ * The CDN id comes from the selected track's media-playlist URL, never the
11
+ * failed addressable: a segment URL resolves relative to its playlist and, per
12
+ * RFC 3986, drops the playlist's query string (`…/r.m3u8?cdn=fastly` → `…/0.ts`),
13
+ * so a query-keyed `getCdnId` (e.g. Mux's `cdn=`) keyed on it would derive an id
14
+ * that never matches the ones `deriveCdnPriority` / track-switching build from
15
+ * `track.url`. The in-flight fetch belongs to the selected track — a source or
16
+ * track switch aborts it, and aborts don't trip — so the selected track is the
17
+ * right CDN to fail over. For `resolve-track` the resolving track *is* the
18
+ * selected track, so this is identical to keying on its addressable.
19
+ *
20
+ * No-op when no failover monitor is composed (it owns the signal) or the
21
+ * selected track can't be located.
22
+ */
23
+ function failoverFetch(baseFetch, state, config) {
24
+ const getCdnId$1 = config.getCdnId ?? getCdnId;
25
+ return (async (addressable, options) => {
26
+ try {
27
+ return await baseFetch(addressable, options);
28
+ } catch (error) {
29
+ if (!options?.signal?.aborted && state.failedCdns) {
30
+ const presentation = state.presentation.get();
31
+ const trackId = state[config.selectedKey].get();
32
+ const track = presentation && trackId ? findTrackById(presentation, trackId) : void 0;
33
+ if (track) update(state.failedCdns, (cdns) => addFailedCdn(cdns, getCdnId$1(track.url)));
34
+ }
35
+ throw error;
36
+ }
37
+ });
38
+ }
39
+ //#endregion
40
+ export { failoverFetch };
41
+
42
+ //# sourceMappingURL=failover-fetch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"failover-fetch.js","names":["getCdnId","defaultGetCdnId"],"sources":["../../../../src/playback/primitives/failover-fetch.ts"],"sourcesContent":["import { type ReadonlySignal, type Signal, update } from '../../core/signals/primitives';\nimport type { MaybeResolvedPresentation } from '../../media/types';\nimport { addFailedCdn, getCdnId as defaultGetCdnId, type GetCdnId } from '../../media/utils/cdn';\nimport { findTrackById } from '../../media/utils/tracks';\nimport type { FetchOptions, Resource } from '../../network/fetch';\n\ntype SelectedTrackKey = 'selectedVideoTrackId' | 'selectedAudioTrackId' | 'selectedTextTrackId';\n\n/**\n * State a failover-decorated fetch reads: the presentation, the per-type\n * selected-track slot, and the failover monitor's `failedCdns`.\n *\n * `failedCdns` is *optional* — the failover monitor owns that slot, so a\n * behavior's narrow state is assignable here without the behavior declaring it\n * (and the intersection shares keys, so it isn't a weak type). When no monitor\n * is composed the slot is absent and tracking no-ops.\n */\ntype FailoverState<K extends SelectedTrackKey> = {\n presentation: ReadonlySignal<MaybeResolvedPresentation | undefined>;\n failedCdns?: Signal<string[] | undefined>;\n} & { [P in K]: ReadonlySignal<string | undefined> };\n\n/** Any `Resource`-addressable fetch — both `FetchText` and `FetchBytes` qualify. */\ntype FailoverableFetch = (addressable: Resource, options?: FetchOptions) => Promise<unknown>;\n\n/**\n * Decorate a fetch so a failed request trips the **selected track's** CDN into\n * `failedCdns`. The decorated fetch's type is preserved, so this wraps both\n * `resolve-track`'s playlist `FetchText` and the segment loaders' `FetchBytes`.\n *\n * The CDN id comes from the selected track's media-playlist URL, never the\n * failed addressable: a segment URL resolves relative to its playlist and, per\n * RFC 3986, drops the playlist's query string (`…/r.m3u8?cdn=fastly` → `…/0.ts`),\n * so a query-keyed `getCdnId` (e.g. Mux's `cdn=`) keyed on it would derive an id\n * that never matches the ones `deriveCdnPriority` / track-switching build from\n * `track.url`. The in-flight fetch belongs to the selected track — a source or\n * track switch aborts it, and aborts don't trip — so the selected track is the\n * right CDN to fail over. For `resolve-track` the resolving track *is* the\n * selected track, so this is identical to keying on its addressable.\n *\n * No-op when no failover monitor is composed (it owns the signal) or the\n * selected track can't be located.\n */\nexport function failoverFetch<K extends SelectedTrackKey, Fetch extends FailoverableFetch>(\n baseFetch: Fetch,\n state: FailoverState<K>,\n config: { selectedKey: K; getCdnId?: GetCdnId }\n): Fetch {\n const getCdnId = config.getCdnId ?? defaultGetCdnId;\n return (async (addressable: Resource, options?: FetchOptions) => {\n try {\n return await baseFetch(addressable, options);\n } catch (error) {\n if (!options?.signal?.aborted && state.failedCdns) {\n const presentation = state.presentation.get();\n const trackId = state[config.selectedKey].get();\n const track = presentation && trackId ? findTrackById(presentation, trackId) : undefined;\n if (track) update(state.failedCdns, (cdns) => addFailedCdn(cdns, getCdnId(track.url)));\n }\n throw error;\n }\n }) as Fetch;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AA2CA,SAAgB,cACd,WACA,OACA,QACO;CACP,MAAMA,aAAW,OAAO,YAAYC;AACpC,SAAQ,OAAO,aAAuB,YAA2B;AAC/D,MAAI;AACF,UAAO,MAAM,UAAU,aAAa,QAAQ;WACrC,OAAO;AACd,OAAI,CAAC,SAAS,QAAQ,WAAW,MAAM,YAAY;IACjD,MAAM,eAAe,MAAM,aAAa,KAAK;IAC7C,MAAM,UAAU,MAAM,OAAO,aAAa,KAAK;IAC/C,MAAM,QAAQ,gBAAgB,UAAU,cAAc,cAAc,QAAQ,GAAG,KAAA;AAC/E,QAAI,MAAO,QAAO,MAAM,aAAa,SAAS,aAAa,MAAMD,WAAS,MAAM,IAAI,CAAC,CAAC;;AAExF,SAAM"}