@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
@@ -2,12 +2,13 @@ import { computed } from "../../../core/signals/primitives.js";
2
2
  import { defineBehavior } from "../../../core/composition/create-composition.js";
3
3
  import { createMachineReactor } from "../../../core/reactors/create-machine-reactor.js";
4
4
  import { hasCodecs } from "../../../media/utils/tracks.js";
5
- import { AUDIO_TYPE_CONFIG, VIDEO_TYPE_CONFIG } from "../track-types.js";
6
- import { getSelectedTrack } from "../../../media/utils/track-selection.js";
5
+ import { AUDIO_TYPE_CONFIG, VIDEO_TYPE_CONFIG } from "../../primitives/track-types.js";
7
6
  import { buildMimeCodec, createSourceBuffer } from "../../../media/dom/mse/mediasource-setup.js";
7
+ import { getSelectedTrack } from "../../../media/utils/track-selection.js";
8
8
  import { createTrackedFetch, fetchStream } from "../../../network/fetch.js";
9
9
  import { createSegmentLoaderActor } from "../../actors/dom/segment-loader.js";
10
10
  import { createSourceBufferActor } from "../../actors/dom/source-buffer.js";
11
+ import { failoverFetch } from "../../primitives/failover-fetch.js";
11
12
  //#region src/playback/behaviors/dom/setup-buffer-actors.ts
12
13
  /**
13
14
  * **Per-type buffer + segment-loader actor setup.** Per available track
@@ -39,7 +40,7 @@ import { createSourceBufferActor } from "../../actors/dom/source-buffer.js";
39
40
  * with its upstream and downstream siblings:
40
41
  *
41
42
  * 1. **Upstream — default selections land in one `runPending`.**
42
- * `selectAudioTrack` (default audio) and `switchVideoQuality`
43
+ * `selectAudioTrack` (default audio) and `switchVideoTrack`
43
44
  * (default video) both subscribe to `state.presentation` flipping to
44
45
  * resolved; their effects run in the same `runPending` iteration and
45
46
  * write `selectedAudioTrackId` + `selectedVideoTrackId` within it.
@@ -131,13 +132,16 @@ const setupVideoBufferActors = defineBehavior({
131
132
  slowTotalWeight: 0,
132
133
  bytesSampled: 0
133
134
  }, (next) => state.bandwidthState.set(next));
135
+ const typeConfig = {
136
+ ...VIDEO_TYPE_CONFIG,
137
+ ...config
138
+ };
134
139
  return setupBufferActors({
135
140
  state,
136
141
  context,
137
142
  config: {
138
- ...VIDEO_TYPE_CONFIG,
139
- fetch: trackedFetch,
140
- ...config
143
+ ...typeConfig,
144
+ fetch: failoverFetch(trackedFetch, state, typeConfig)
141
145
  }
142
146
  });
143
147
  }
@@ -148,7 +152,17 @@ const setupVideoBufferActors = defineBehavior({
148
152
  * a non-sampling `fetchStream` (no audio ABR); adding audio ABR is a
149
153
  * localized change to this setup body (swap `fetchStream` for a
150
154
  * `createTrackedFetch` call + declare `bandwidthState` writable here)
151
- * without touching the shared helper.
155
+ * without touching the shared helper. See
156
+ * `internal/design/spf/features/audio-abr.md` for the design surface
157
+ * (bandwidth-state sharing, multi-writer coordination, EWMA mixed-
158
+ * source sampling).
159
+ *
160
+ * **Mid-stream audio track switching is NOT this behavior's concern.**
161
+ * Slot writes to `selectedAudioTrackId` (default selection, programmatic
162
+ * filter-driven, future ABR) are owned by `switchAudioTrack` / future
163
+ * `switchAudioQuality` in `track-switching.ts`. Flush orchestration on
164
+ * track change is dispatched from there via `audioBufferActor.send(...)`
165
+ * — keeping this setup behavior focused on per-source actor lifecycle.
152
166
  */
153
167
  const setupAudioBufferActors = defineBehavior({
154
168
  stateKeys: ["presentation", "selectedAudioTrackId"],
@@ -157,15 +171,20 @@ const setupAudioBufferActors = defineBehavior({
157
171
  "audioBufferActor",
158
172
  "audioSegmentLoaderActor"
159
173
  ],
160
- setup: ({ state, context, config = {} }) => setupBufferActors({
161
- state,
162
- context,
163
- config: {
174
+ setup: ({ state, context, config = {} }) => {
175
+ const typeConfig = {
164
176
  ...AUDIO_TYPE_CONFIG,
165
- fetch: fetchStream,
166
177
  ...config
167
- }
168
- })
178
+ };
179
+ return setupBufferActors({
180
+ state,
181
+ context,
182
+ config: {
183
+ ...typeConfig,
184
+ fetch: failoverFetch(fetchStream, state, typeConfig)
185
+ }
186
+ });
187
+ }
169
188
  });
170
189
  //#endregion
171
190
  export { setupAudioBufferActors, setupVideoBufferActors };
@@ -1 +1 @@
1
- {"version":3,"file":"setup-buffer-actors.js","names":[],"sources":["../../../../../src/playback/behaviors/dom/setup-buffer-actors.ts"],"sourcesContent":["/**\n * **Per-type buffer + segment-loader actor setup.** Per available track\n * type (video / audio), when `mediaSource` is attached and the selected\n * track of that type is present in the presentation with codecs (partial\n * resolution from the multivariant playlist is enough — codecs live on\n * the `EXT-X-STREAM-INF` line, not in the per-type media playlist),\n * creates a `SourceBuffer`, a `SourceBufferActor`, and a\n * `SegmentLoaderActor` bound to that buffer-actor; publishes the per-type\n * actor slots. On `mediaSource` detach or behavior destroy, destroys both\n * actors in reverse order and clears the per-type slots so the next\n * source starts fresh.\n *\n * Each per-type variant (`setupVideoBufferActors` /\n * `setupAudioBufferActors`) is a single-positive-state reactor\n * (`'preconditions-unmet'` ↔ `'buffer-ready'`) gating only on its own\n * type. No cross-type coupling in `stateKeys` —\n * `setupVideoBufferActors` carries only `selectedVideoTrackId` (plus\n * `bandwidthState`, written by its trackedFetch), and audio mirrors.\n *\n * # Firefox `mozHasAudio` invariant\n *\n * Appending to a video `SourceBuffer` before the audio `SourceBuffer`\n * exists causes `mozHasAudio` to be permanently false in Firefox. With\n * the two per-type variants decoupled, the invariant is no longer\n * structural to a single `entry` body (as it was when both buffers were\n * created in one synchronous block inside a merged behavior). It's now\n * preserved by a chain of assumptions about how this behavior composes\n * with its upstream and downstream siblings:\n *\n * 1. **Upstream — default selections land in one `runPending`.**\n * `selectAudioTrack` (default audio) and `switchVideoQuality`\n * (default video) both subscribe to `state.presentation` flipping to\n * resolved; their effects run in the same `runPending` iteration and\n * write `selectedAudioTrackId` + `selectedVideoTrackId` within it.\n * 2. **Self — both per-type monitors flip in one `runPending`.**\n * After (1), both monitors re-evaluate and flip to `'buffer-ready'`\n * in the next `runPending`. Both `entry` bodies run synchronously\n * within that iteration — both `addSourceBuffer` calls land before\n * the iteration ends.\n * 3. **Downstream — `appendBuffer` is async.** `loadVideoSegments` /\n * `loadAudioSegments` read the per-type `xSegmentLoaderActor` slots;\n * their effects fire in the *next* `runPending` and the actual\n * `appendBuffer` requires a network round-trip via the\n * `SegmentLoaderActor` — many microtasks past both `addSourceBuffer`\n * calls.\n *\n * The cross-tick failure mode — a user-initiated audio track switch\n * *after* video segments have begun appending — is out of scope for\n * this behavior and would be addressed in the buffer/segment-loading\n * path via `changeType`-aware logic.\n *\n * # Sole writer\n *\n * `setupVideoBufferActors` is sole writer of `videoBufferActor` +\n * `videoSegmentLoaderActor` (and `bandwidthState` via its\n * trackedFetch); `setupAudioBufferActors` is sole writer of\n * `audioBufferActor` + `audioSegmentLoaderActor`. Both read\n * `mediaSource` from `setupMediaSource`. Downstream MSE behaviors\n * (`loadVideoSegments`, `loadAudioSegments`, `endOfStream`,\n * `updateMediaSourceDuration`) only read these slots.\n */\nimport { defineBehavior } from '../../../core/composition/create-composition';\nimport type { Reactor } from '../../../core/reactors/create-machine-reactor';\nimport { createMachineReactor } from '../../../core/reactors/create-machine-reactor';\nimport { computed, type ReadonlySignal, type Signal } from '../../../core/signals/primitives';\nimport { buildMimeCodec, createSourceBuffer } from '../../../media/dom/mse/mediasource-setup';\nimport type { MaybeResolvedPresentation, PartiallyResolvedTrack } from '../../../media/types';\nimport { getSelectedTrack, type TrackSelectionState } from '../../../media/utils/track-selection';\nimport { hasCodecs } from '../../../media/utils/tracks';\nimport type { BandwidthState } from '../../../network/bandwidth-estimator';\nimport { createTrackedFetch, type FetchBytes, fetchStream } from '../../../network/fetch';\nimport {\n createSegmentLoaderActor,\n type SegmentLoaderActor,\n type SegmentLoaderActorConfig,\n} from '../../actors/dom/segment-loader';\nimport { createSourceBufferActor, type SourceBufferActor } from '../../actors/dom/source-buffer';\nimport { AUDIO_TYPE_CONFIG, VIDEO_TYPE_CONFIG } from '../track-types';\n\n/**\n * Media track type for MSE buffer setup.\n * Text tracks are excluded as they don't use MSE SourceBuffers.\n */\nexport type MediaTrackType = 'video' | 'audio';\n\nexport interface BufferActorsState {\n presentation?: MaybeResolvedPresentation;\n selectedVideoTrackId?: string;\n selectedAudioTrackId?: string;\n bandwidthState?: BandwidthState;\n}\n\nexport interface BufferActorsContext {\n mediaSource?: MediaSource;\n videoBufferActor?: SourceBufferActor;\n audioBufferActor?: SourceBufferActor;\n videoSegmentLoaderActor?: SegmentLoaderActor;\n audioSegmentLoaderActor?: SegmentLoaderActor;\n}\n\ntype BufferActorsFsmState = 'preconditions-unmet' | 'buffer-ready';\n\ntype SelectedTrackKey = 'selectedVideoTrackId' | 'selectedAudioTrackId';\ntype BufferActorKey = 'videoBufferActor' | 'audioBufferActor';\ntype SegmentLoaderActorKey = 'videoSegmentLoaderActor' | 'audioSegmentLoaderActor';\n\ntype BufferActorsStateMap<K extends SelectedTrackKey> = {\n presentation: ReadonlySignal<BufferActorsState['presentation']>;\n} & { [P in K]: ReadonlySignal<BufferActorsState[P]> };\n\ntype BufferActorsContextMap<A extends BufferActorKey, L extends SegmentLoaderActorKey> = {\n mediaSource: ReadonlySignal<BufferActorsContext['mediaSource']>;\n} & { [P in A]: Signal<SourceBufferActor | undefined> } & {\n [P in L]: Signal<SegmentLoaderActor | undefined>;\n};\n\n// ============================================================================\n// Specialization helper\n//\n// `setupBufferActors` has the same shape as a Behavior `setup` function:\n// `({ state, context, config }) => reactor`. Each `setupXBufferActors`\n// export below calls it from inside its own `defineBehavior` setup,\n// passing its narrowed `state` / `context` through directly and supplying\n// the per-type `selectedKey`, `actorKey`, `loaderKey`, `type` discriminator,\n// and `fetch` inline.\n//\n// Gating is per-type only: no cross-type coupling. See the file-level\n// JSDoc for how the Firefox `mozHasAudio` invariant survives the split\n// via SPF's effect coalescing.\n// ============================================================================\n\nfunction setupBufferActors<K extends SelectedTrackKey, A extends BufferActorKey, L extends SegmentLoaderActorKey>({\n state,\n context,\n config,\n}: {\n state: BufferActorsStateMap<K>;\n context: BufferActorsContextMap<A, L>;\n config: {\n type: MediaTrackType;\n selectedKey: K;\n actorKey: A;\n loaderKey: L;\n fetch: FetchBytes;\n } & SegmentLoaderActorConfig;\n}): Reactor<BufferActorsFsmState | 'destroying' | 'destroyed'> {\n const { type, selectedKey, actorKey, loaderKey, fetch, forwardBuffer, backBuffer } = config;\n const derivedStateSignal = computed<BufferActorsFsmState>(() => {\n if (!context.mediaSource.get()) return 'preconditions-unmet';\n const selection: TrackSelectionState = {\n presentation: state.presentation.get(),\n [selectedKey]: state[selectedKey].get(),\n };\n return hasCodecs(getSelectedTrack(selection, type)) ? 'buffer-ready' : 'preconditions-unmet';\n });\n\n return createMachineReactor<BufferActorsFsmState>({\n initial: 'preconditions-unmet',\n monitor: () => derivedStateSignal.get(),\n states: {\n 'preconditions-unmet': {},\n\n 'buffer-ready': {\n entry: () => {\n const mediaSource = context.mediaSource.get()!;\n const selection: TrackSelectionState = {\n presentation: state.presentation.get(),\n [selectedKey]: state[selectedKey].get(),\n };\n const track = getSelectedTrack(selection, type) as PartiallyResolvedTrack;\n const buffer = createSourceBuffer(mediaSource, buildMimeCodec(track));\n const bufferActor = createSourceBufferActor(buffer);\n const segmentLoader = createSegmentLoaderActor(bufferActor, fetch, { forwardBuffer, backBuffer });\n\n // Synchronous slot writes — load-bearing for the Firefox\n // `mozHasAudio` invariant (see file-level JSDoc). Both per-type\n // entries' `addSourceBuffer` calls must land in the same\n // `runPending` iteration; synchronous writes keep the contiguous\n // JS frame. Downstream `loadXSegments` effects fire in the\n // *next* `runPending`.\n context[actorKey].set(bufferActor);\n context[loaderKey].set(segmentLoader);\n\n // State-exit cleanup — fires when `mediaSource` detaches, the\n // selection unsets, or the behavior is destroyed. Destroy the\n // loader before its upstream buffer-actor so any in-flight\n // `appendBuffer` is aborted before the buffer-actor's own\n // teardown.\n return () => {\n segmentLoader.destroy();\n bufferActor.destroy();\n context[loaderKey].set(undefined);\n context[actorKey].set(undefined);\n };\n },\n },\n },\n });\n}\n\n// ============================================================================\n// Specialized exports — one per media type\n// ============================================================================\n\n/**\n * Set up the video `SourceBufferActor` + `SegmentLoaderActor`. Fires\n * when `mediaSource` is attached and the selected video track is\n * present in the presentation with codecs. Gates only on video state —\n * no cross-type coupling. Owns a bandwidth-sampling `trackedFetch` and\n * is sole writer of `state.bandwidthState`.\n */\nexport const setupVideoBufferActors = defineBehavior({\n stateKeys: ['presentation', 'selectedVideoTrackId', 'bandwidthState'] as const,\n contextKeys: ['mediaSource', 'videoBufferActor', 'videoSegmentLoaderActor'] as const,\n setup: ({\n state,\n context,\n config = {},\n }: {\n state: BufferActorsStateMap<'selectedVideoTrackId'> & {\n bandwidthState: Signal<BufferActorsState['bandwidthState']>;\n };\n context: BufferActorsContextMap<'videoBufferActor', 'videoSegmentLoaderActor'>;\n config?: object;\n }) => {\n // Bandwidth-sampling fetch. The factory accumulates EWMA state\n // internally; the callback bridges samples to engine state for ABR.\n // Created once at behavior setup time so the EWMA accumulates across\n // source resets (each per-source loader-actor receives the same\n // tracked fetch when reconstructed in `buffer-ready` entry).\n const trackedFetch = createTrackedFetch(\n state.bandwidthState.get() ?? {\n fastEstimate: 0,\n fastTotalWeight: 0,\n slowEstimate: 0,\n slowTotalWeight: 0,\n bytesSampled: 0,\n },\n (next) => state.bandwidthState.set(next)\n );\n return setupBufferActors({\n state,\n context,\n config: { ...VIDEO_TYPE_CONFIG, fetch: trackedFetch, ...config },\n });\n },\n});\n\n/**\n * Set up the audio `SourceBufferActor` + `SegmentLoaderActor`. Same\n * shape as `setupVideoBufferActors`, narrowed to audio. Today supplies\n * a non-sampling `fetchStream` (no audio ABR); adding audio ABR is a\n * localized change to this setup body (swap `fetchStream` for a\n * `createTrackedFetch` call + declare `bandwidthState` writable here)\n * without touching the shared helper.\n */\nexport const setupAudioBufferActors = defineBehavior({\n stateKeys: ['presentation', 'selectedAudioTrackId'] as const,\n contextKeys: ['mediaSource', 'audioBufferActor', 'audioSegmentLoaderActor'] as const,\n setup: ({\n state,\n context,\n config = {},\n }: {\n state: BufferActorsStateMap<'selectedAudioTrackId'>;\n context: BufferActorsContextMap<'audioBufferActor', 'audioSegmentLoaderActor'>;\n config?: object;\n }) =>\n setupBufferActors({\n state,\n context,\n config: { ...AUDIO_TYPE_CONFIG, fetch: fetchStream, ...config },\n }),\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmIA,SAAS,kBAAyG,EAChH,OACA,SACA,UAW6D;CAC7D,MAAM,EAAE,MAAM,aAAa,UAAU,WAAW,OAAO,eAAe,eAAe;CACrF,MAAM,qBAAqB,eAAqC;AAC9D,MAAI,CAAC,QAAQ,YAAY,KAAK,CAAE,QAAO;AAKvC,SAAO,UAAU,iBAJsB;GACrC,cAAc,MAAM,aAAa,KAAK;IACrC,cAAc,MAAM,aAAa,KAAK;GACxC,EAC4C,KAAK,CAAC,GAAG,iBAAiB;GACvE;AAEF,QAAO,qBAA2C;EAChD,SAAS;EACT,eAAe,mBAAmB,KAAK;EACvC,QAAQ;GACN,uBAAuB,EAAE;GAEzB,gBAAgB,EACd,aAAa;IAQX,MAAM,cAAc,wBADL,mBANK,QAAQ,YAAY,KAAK,EAME,eADjC,iBAJyB;KACrC,cAAc,MAAM,aAAa,KAAK;MACrC,cAAc,MAAM,aAAa,KAAK;KACxC,EACyC,KAAK,CACqB,CAAC,CAClB;IACnD,MAAM,gBAAgB,yBAAyB,aAAa,OAAO;KAAE;KAAe;KAAY,CAAC;AAQjG,YAAQ,UAAU,IAAI,YAAY;AAClC,YAAQ,WAAW,IAAI,cAAc;AAOrC,iBAAa;AACX,mBAAc,SAAS;AACvB,iBAAY,SAAS;AACrB,aAAQ,WAAW,IAAI,KAAA,EAAU;AACjC,aAAQ,UAAU,IAAI,KAAA,EAAU;;MAGrC;GACF;EACF,CAAC;;;;;;;;;AAcJ,MAAa,yBAAyB,eAAe;CACnD,WAAW;EAAC;EAAgB;EAAwB;EAAiB;CACrE,aAAa;EAAC;EAAe;EAAoB;EAA0B;CAC3E,QAAQ,EACN,OACA,SACA,SAAS,EAAE,OAOP;EAMJ,MAAM,eAAe,mBACnB,MAAM,eAAe,KAAK,IAAI;GAC5B,cAAc;GACd,iBAAiB;GACjB,cAAc;GACd,iBAAiB;GACjB,cAAc;GACf,GACA,SAAS,MAAM,eAAe,IAAI,KAAK,CACzC;AACD,SAAO,kBAAkB;GACvB;GACA;GACA,QAAQ;IAAE,GAAG;IAAmB,OAAO;IAAc,GAAG;IAAQ;GACjE,CAAC;;CAEL,CAAC;;;;;;;;;AAUF,MAAa,yBAAyB,eAAe;CACnD,WAAW,CAAC,gBAAgB,uBAAuB;CACnD,aAAa;EAAC;EAAe;EAAoB;EAA0B;CAC3E,QAAQ,EACN,OACA,SACA,SAAS,EAAE,OAMX,kBAAkB;EAChB;EACA;EACA,QAAQ;GAAE,GAAG;GAAmB,OAAO;GAAa,GAAG;GAAQ;EAChE,CAAC;CACL,CAAC"}
1
+ {"version":3,"file":"setup-buffer-actors.js","names":[],"sources":["../../../../../src/playback/behaviors/dom/setup-buffer-actors.ts"],"sourcesContent":["/**\n * **Per-type buffer + segment-loader actor setup.** Per available track\n * type (video / audio), when `mediaSource` is attached and the selected\n * track of that type is present in the presentation with codecs (partial\n * resolution from the multivariant playlist is enough — codecs live on\n * the `EXT-X-STREAM-INF` line, not in the per-type media playlist),\n * creates a `SourceBuffer`, a `SourceBufferActor`, and a\n * `SegmentLoaderActor` bound to that buffer-actor; publishes the per-type\n * actor slots. On `mediaSource` detach or behavior destroy, destroys both\n * actors in reverse order and clears the per-type slots so the next\n * source starts fresh.\n *\n * Each per-type variant (`setupVideoBufferActors` /\n * `setupAudioBufferActors`) is a single-positive-state reactor\n * (`'preconditions-unmet'` ↔ `'buffer-ready'`) gating only on its own\n * type. No cross-type coupling in `stateKeys` —\n * `setupVideoBufferActors` carries only `selectedVideoTrackId` (plus\n * `bandwidthState`, written by its trackedFetch), and audio mirrors.\n *\n * # Firefox `mozHasAudio` invariant\n *\n * Appending to a video `SourceBuffer` before the audio `SourceBuffer`\n * exists causes `mozHasAudio` to be permanently false in Firefox. With\n * the two per-type variants decoupled, the invariant is no longer\n * structural to a single `entry` body (as it was when both buffers were\n * created in one synchronous block inside a merged behavior). It's now\n * preserved by a chain of assumptions about how this behavior composes\n * with its upstream and downstream siblings:\n *\n * 1. **Upstream — default selections land in one `runPending`.**\n * `selectAudioTrack` (default audio) and `switchVideoTrack`\n * (default video) both subscribe to `state.presentation` flipping to\n * resolved; their effects run in the same `runPending` iteration and\n * write `selectedAudioTrackId` + `selectedVideoTrackId` within it.\n * 2. **Self — both per-type monitors flip in one `runPending`.**\n * After (1), both monitors re-evaluate and flip to `'buffer-ready'`\n * in the next `runPending`. Both `entry` bodies run synchronously\n * within that iteration — both `addSourceBuffer` calls land before\n * the iteration ends.\n * 3. **Downstream — `appendBuffer` is async.** `loadVideoSegments` /\n * `loadAudioSegments` read the per-type `xSegmentLoaderActor` slots;\n * their effects fire in the *next* `runPending` and the actual\n * `appendBuffer` requires a network round-trip via the\n * `SegmentLoaderActor` — many microtasks past both `addSourceBuffer`\n * calls.\n *\n * The cross-tick failure mode — a user-initiated audio track switch\n * *after* video segments have begun appending — is out of scope for\n * this behavior and would be addressed in the buffer/segment-loading\n * path via `changeType`-aware logic.\n *\n * # Sole writer\n *\n * `setupVideoBufferActors` is sole writer of `videoBufferActor` +\n * `videoSegmentLoaderActor` (and `bandwidthState` via its\n * trackedFetch); `setupAudioBufferActors` is sole writer of\n * `audioBufferActor` + `audioSegmentLoaderActor`. Both read\n * `mediaSource` from `setupMediaSource`. Downstream MSE behaviors\n * (`loadVideoSegments`, `loadAudioSegments`, `endOfStream`,\n * `updateMediaSourceDuration`) only read these slots.\n */\nimport { defineBehavior } from '../../../core/composition/create-composition';\nimport type { Reactor } from '../../../core/reactors/create-machine-reactor';\nimport { createMachineReactor } from '../../../core/reactors/create-machine-reactor';\nimport { computed, type ReadonlySignal, type Signal } from '../../../core/signals/primitives';\nimport { buildMimeCodec, createSourceBuffer } from '../../../media/dom/mse/mediasource-setup';\nimport type { MaybeResolvedPresentation, PartiallyResolvedTrack } from '../../../media/types';\nimport type { GetCdnId } from '../../../media/utils/cdn';\nimport { getSelectedTrack, type TrackSelectionState } from '../../../media/utils/track-selection';\nimport { hasCodecs } from '../../../media/utils/tracks';\nimport type { BandwidthState } from '../../../network/bandwidth-estimator';\nimport { createTrackedFetch, type FetchBytes, fetchStream } from '../../../network/fetch';\nimport {\n createSegmentLoaderActor,\n type SegmentLoaderActor,\n type SegmentLoaderActorConfig,\n} from '../../actors/dom/segment-loader';\nimport { createSourceBufferActor, type SourceBufferActor } from '../../actors/dom/source-buffer';\nimport { failoverFetch } from '../../primitives/failover-fetch';\nimport { AUDIO_TYPE_CONFIG, VIDEO_TYPE_CONFIG } from '../../primitives/track-types';\n\n/**\n * Media track type for MSE buffer setup.\n * Text tracks are excluded as they don't use MSE SourceBuffers.\n */\nexport type MediaTrackType = 'video' | 'audio';\n\nexport interface BufferActorsState {\n presentation?: MaybeResolvedPresentation;\n selectedVideoTrackId?: string;\n selectedAudioTrackId?: string;\n bandwidthState?: BandwidthState;\n}\n\nexport interface BufferActorsContext {\n mediaSource?: MediaSource;\n videoBufferActor?: SourceBufferActor;\n audioBufferActor?: SourceBufferActor;\n videoSegmentLoaderActor?: SegmentLoaderActor;\n audioSegmentLoaderActor?: SegmentLoaderActor;\n}\n\ntype BufferActorsFsmState = 'preconditions-unmet' | 'buffer-ready';\n\ntype SelectedTrackKey = 'selectedVideoTrackId' | 'selectedAudioTrackId';\ntype BufferActorKey = 'videoBufferActor' | 'audioBufferActor';\ntype SegmentLoaderActorKey = 'videoSegmentLoaderActor' | 'audioSegmentLoaderActor';\n\ntype BufferActorsStateMap<K extends SelectedTrackKey> = {\n presentation: ReadonlySignal<BufferActorsState['presentation']>;\n} & { [P in K]: ReadonlySignal<BufferActorsState[P]> };\n\ntype BufferActorsContextMap<A extends BufferActorKey, L extends SegmentLoaderActorKey> = {\n mediaSource: ReadonlySignal<BufferActorsContext['mediaSource']>;\n} & { [P in A]: Signal<SourceBufferActor | undefined> } & {\n [P in L]: Signal<SegmentLoaderActor | undefined>;\n};\n\n// ============================================================================\n// Specialization helper\n//\n// `setupBufferActors` has the same shape as a Behavior `setup` function:\n// `({ state, context, config }) => reactor`. Each `setupXBufferActors`\n// export below calls it from inside its own `defineBehavior` setup,\n// passing its narrowed `state` / `context` through directly and supplying\n// the per-type `selectedKey`, `actorKey`, `loaderKey`, `type` discriminator,\n// and `fetch` inline.\n//\n// Gating is per-type only: no cross-type coupling. See the file-level\n// JSDoc for how the Firefox `mozHasAudio` invariant survives the split\n// via SPF's effect coalescing.\n// ============================================================================\n\nfunction setupBufferActors<K extends SelectedTrackKey, A extends BufferActorKey, L extends SegmentLoaderActorKey>({\n state,\n context,\n config,\n}: {\n state: BufferActorsStateMap<K>;\n context: BufferActorsContextMap<A, L>;\n config: {\n type: MediaTrackType;\n selectedKey: K;\n actorKey: A;\n loaderKey: L;\n fetch: FetchBytes;\n } & SegmentLoaderActorConfig;\n}): Reactor<BufferActorsFsmState | 'destroying' | 'destroyed'> {\n const { type, selectedKey, actorKey, loaderKey, fetch, forwardBuffer, backBuffer } = config;\n const derivedStateSignal = computed<BufferActorsFsmState>(() => {\n if (!context.mediaSource.get()) return 'preconditions-unmet';\n const selection: TrackSelectionState = {\n presentation: state.presentation.get(),\n [selectedKey]: state[selectedKey].get(),\n };\n return hasCodecs(getSelectedTrack(selection, type)) ? 'buffer-ready' : 'preconditions-unmet';\n });\n\n return createMachineReactor<BufferActorsFsmState>({\n initial: 'preconditions-unmet',\n monitor: () => derivedStateSignal.get(),\n states: {\n 'preconditions-unmet': {},\n\n 'buffer-ready': {\n entry: () => {\n const mediaSource = context.mediaSource.get()!;\n const selection: TrackSelectionState = {\n presentation: state.presentation.get(),\n [selectedKey]: state[selectedKey].get(),\n };\n const track = getSelectedTrack(selection, type) as PartiallyResolvedTrack;\n const buffer = createSourceBuffer(mediaSource, buildMimeCodec(track));\n const bufferActor = createSourceBufferActor(buffer);\n const segmentLoader = createSegmentLoaderActor(bufferActor, fetch, { forwardBuffer, backBuffer });\n\n // Synchronous slot writes — load-bearing for the Firefox\n // `mozHasAudio` invariant (see file-level JSDoc). Both per-type\n // entries' `addSourceBuffer` calls must land in the same\n // `runPending` iteration; synchronous writes keep the contiguous\n // JS frame. Downstream `loadXSegments` effects fire in the\n // *next* `runPending`.\n context[actorKey].set(bufferActor);\n context[loaderKey].set(segmentLoader);\n\n // State-exit cleanup — fires when `mediaSource` detaches, the\n // selection unsets, or the behavior is destroyed. Destroy the\n // loader before its upstream buffer-actor so any in-flight\n // `appendBuffer` is aborted before the buffer-actor's own\n // teardown.\n return () => {\n segmentLoader.destroy();\n bufferActor.destroy();\n context[loaderKey].set(undefined);\n context[actorKey].set(undefined);\n };\n },\n },\n },\n });\n}\n\n// ============================================================================\n// Specialized exports — one per media type\n// ============================================================================\n\n/**\n * Set up the video `SourceBufferActor` + `SegmentLoaderActor`. Fires\n * when `mediaSource` is attached and the selected video track is\n * present in the presentation with codecs. Gates only on video state —\n * no cross-type coupling. Owns a bandwidth-sampling `trackedFetch` and\n * is sole writer of `state.bandwidthState`.\n */\nexport const setupVideoBufferActors = defineBehavior({\n stateKeys: ['presentation', 'selectedVideoTrackId', 'bandwidthState'] as const,\n contextKeys: ['mediaSource', 'videoBufferActor', 'videoSegmentLoaderActor'] as const,\n setup: ({\n state,\n context,\n config = {},\n }: {\n state: BufferActorsStateMap<'selectedVideoTrackId'> & {\n bandwidthState: Signal<BufferActorsState['bandwidthState']>;\n };\n context: BufferActorsContextMap<'videoBufferActor', 'videoSegmentLoaderActor'>;\n config?: SegmentLoaderActorConfig & { getCdnId?: GetCdnId };\n }) => {\n // Bandwidth-sampling fetch. The factory accumulates EWMA state\n // internally; the callback bridges samples to engine state for ABR.\n // Created once at behavior setup time so the EWMA accumulates across\n // source resets (each per-source loader-actor receives the same\n // tracked fetch when reconstructed in `buffer-ready` entry).\n const trackedFetch = createTrackedFetch(\n state.bandwidthState.get() ?? {\n fastEstimate: 0,\n fastTotalWeight: 0,\n slowEstimate: 0,\n slowTotalWeight: 0,\n bytesSampled: 0,\n },\n (next) => state.bandwidthState.set(next)\n );\n // Engine `config` layers over the per-type defaults; `failoverFetch` reads\n // its `selectedKey` + `getCdnId` from the merged result.\n const typeConfig = { ...VIDEO_TYPE_CONFIG, ...config };\n return setupBufferActors({\n state,\n context,\n config: { ...typeConfig, fetch: failoverFetch(trackedFetch, state, typeConfig) },\n });\n },\n});\n\n/**\n * Set up the audio `SourceBufferActor` + `SegmentLoaderActor`. Same\n * shape as `setupVideoBufferActors`, narrowed to audio. Today supplies\n * a non-sampling `fetchStream` (no audio ABR); adding audio ABR is a\n * localized change to this setup body (swap `fetchStream` for a\n * `createTrackedFetch` call + declare `bandwidthState` writable here)\n * without touching the shared helper. See\n * `internal/design/spf/features/audio-abr.md` for the design surface\n * (bandwidth-state sharing, multi-writer coordination, EWMA mixed-\n * source sampling).\n *\n * **Mid-stream audio track switching is NOT this behavior's concern.**\n * Slot writes to `selectedAudioTrackId` (default selection, programmatic\n * filter-driven, future ABR) are owned by `switchAudioTrack` / future\n * `switchAudioQuality` in `track-switching.ts`. Flush orchestration on\n * track change is dispatched from there via `audioBufferActor.send(...)`\n * — keeping this setup behavior focused on per-source actor lifecycle.\n */\nexport const setupAudioBufferActors = defineBehavior({\n stateKeys: ['presentation', 'selectedAudioTrackId'] as const,\n contextKeys: ['mediaSource', 'audioBufferActor', 'audioSegmentLoaderActor'] as const,\n setup: ({\n state,\n context,\n config = {},\n }: {\n state: BufferActorsStateMap<'selectedAudioTrackId'>;\n context: BufferActorsContextMap<'audioBufferActor', 'audioSegmentLoaderActor'>;\n config?: SegmentLoaderActorConfig & { getCdnId?: GetCdnId };\n }) => {\n // Key order mirrors setupVideoBufferActors.\n const typeConfig = { ...AUDIO_TYPE_CONFIG, ...config };\n return setupBufferActors({\n state,\n context,\n config: { ...typeConfig, fetch: failoverFetch(fetchStream, state, typeConfig) },\n });\n },\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqIA,SAAS,kBAAyG,EAChH,OACA,SACA,UAW6D;CAC7D,MAAM,EAAE,MAAM,aAAa,UAAU,WAAW,OAAO,eAAe,eAAe;CACrF,MAAM,qBAAqB,eAAqC;AAC9D,MAAI,CAAC,QAAQ,YAAY,KAAK,CAAE,QAAO;AAKvC,SAAO,UAAU,iBAJsB;GACrC,cAAc,MAAM,aAAa,KAAK;IACrC,cAAc,MAAM,aAAa,KAAK;GACxC,EAC4C,KAAK,CAAC,GAAG,iBAAiB;GACvE;AAEF,QAAO,qBAA2C;EAChD,SAAS;EACT,eAAe,mBAAmB,KAAK;EACvC,QAAQ;GACN,uBAAuB,EAAE;GAEzB,gBAAgB,EACd,aAAa;IAQX,MAAM,cAAc,wBADL,mBANK,QAAQ,YAAY,KAAK,EAME,eADjC,iBAJyB;KACrC,cAAc,MAAM,aAAa,KAAK;MACrC,cAAc,MAAM,aAAa,KAAK;KACxC,EACyC,KAAK,CACqB,CAAC,CAClB;IACnD,MAAM,gBAAgB,yBAAyB,aAAa,OAAO;KAAE;KAAe;KAAY,CAAC;AAQjG,YAAQ,UAAU,IAAI,YAAY;AAClC,YAAQ,WAAW,IAAI,cAAc;AAOrC,iBAAa;AACX,mBAAc,SAAS;AACvB,iBAAY,SAAS;AACrB,aAAQ,WAAW,IAAI,KAAA,EAAU;AACjC,aAAQ,UAAU,IAAI,KAAA,EAAU;;MAGrC;GACF;EACF,CAAC;;;;;;;;;AAcJ,MAAa,yBAAyB,eAAe;CACnD,WAAW;EAAC;EAAgB;EAAwB;EAAiB;CACrE,aAAa;EAAC;EAAe;EAAoB;EAA0B;CAC3E,QAAQ,EACN,OACA,SACA,SAAS,EAAE,OAOP;EAMJ,MAAM,eAAe,mBACnB,MAAM,eAAe,KAAK,IAAI;GAC5B,cAAc;GACd,iBAAiB;GACjB,cAAc;GACd,iBAAiB;GACjB,cAAc;GACf,GACA,SAAS,MAAM,eAAe,IAAI,KAAK,CACzC;EAGD,MAAM,aAAa;GAAE,GAAG;GAAmB,GAAG;GAAQ;AACtD,SAAO,kBAAkB;GACvB;GACA;GACA,QAAQ;IAAE,GAAG;IAAY,OAAO,cAAc,cAAc,OAAO,WAAW;IAAE;GACjF,CAAC;;CAEL,CAAC;;;;;;;;;;;;;;;;;;;AAoBF,MAAa,yBAAyB,eAAe;CACnD,WAAW,CAAC,gBAAgB,uBAAuB;CACnD,aAAa;EAAC;EAAe;EAAoB;EAA0B;CAC3E,QAAQ,EACN,OACA,SACA,SAAS,EAAE,OAKP;EAEJ,MAAM,aAAa;GAAE,GAAG;GAAmB,GAAG;GAAQ;AACtD,SAAO,kBAAkB;GACvB;GACA;GACA,QAAQ;IAAE,GAAG;IAAY,OAAO,cAAc,aAAa,OAAO,WAAW;IAAE;GAChF,CAAC;;CAEL,CAAC"}
@@ -11,11 +11,13 @@ import { listen } from "@videojs/utils/dom";
11
11
  * available, allocate one slot in `mediaElement.textTracks` per model text
12
12
  * track — via creating `<track>` children, since that's the only spec
13
13
  * mechanism for adding *and* removing entries to `textTracks` (no
14
- * `removeTextTrack` API exists). Once slots are provisioned, mirror
15
- * `selectedTextTrackId` into their `mode`s, and propagate user-initiated
16
- * DOM `change` events back to `selectedTextTrackId` so non-SPF consumers
17
- * (host-page captions buttons, browser native UI, video.js store) remain
18
- * the canonical write path for selection.
14
+ * `removeTextTrack` API exists). Once slots are provisioned, mirror the
15
+ * resolved `selectedTextTrackId` into their `mode`s (one-way: state → DOM),
16
+ * and propagate user-initiated DOM `change` events back to
17
+ * `userTextTrackSelection` the standing *intent* (a language-based partial,
18
+ * or `'off'`) that `switchTextTrack` resolves into `selectedTextTrackId`. So
19
+ * non-SPF consumers (host-page captions buttons, browser native UI, video.js
20
+ * store) drive selection by expressing intent, not by writing the resolved id.
19
21
  *
20
22
  * Single-positive-state reactor (`'preconditions-unmet'` ↔ `'sync-active'`):
21
23
  * the entry allocates the slots, applies the initial selection, attaches
@@ -34,19 +36,37 @@ import { listen } from "@videojs/utils/dom";
34
36
  * presentation reusing a trackId would have `getSegmentsToLoad` treat
35
37
  * its segments as already-buffered and skip loading them.
36
38
  *
37
- * Multi-writer with `selectTextTrack` is intentional and orthogonal:
38
- * `selectTextTrack` owns the *default-on-load / clear-on-unload* contract
39
- * for `selectedTextTrackId`; this behavior writes only from DOM `change`
40
- * events. They reflect distinct decision-making domains (config-driven
41
- * intent vs DOM-driven user action), so this behavior never clears the
42
- * slot on source unload that's `selectTextTrack`'s contract. (It *does*
43
- * write `undefined` when a user disables all tracks via native UI, since
44
- * that's a real user action that should round-trip into SPF state.)
39
+ * Single-writer separation: `selectedTextTrackId` is the resolved *output*
40
+ * owned solely by `switchTextTrack`; this behavior only reads it (to mirror
41
+ * modes). The write path here is `userTextTrackSelection` the user-intent
42
+ * *input* so DOM action and the resolver never contend for one slot. The
43
+ * intent isn't cleared on source unload (it's a standing preference, like
44
+ * `userAudioTrackSelection`); `'off'` is written when the user disables all
45
+ * tracks via native UI.
46
+ *
47
+ * Echo guard: `selectedTextTrackId` is exactly the id this behavior last drove
48
+ * into the DOM, so a `change` event still showing it is our own echo (or a
49
+ * resolver-driven correction — e.g. the picked track's CDN failed and the
50
+ * resolver disabled it) and is ignored, never written back as a spurious user
51
+ * action. Only a showing id that *differs* from the resolved id is a real user
52
+ * pick. The settling-window guard additionally swallows Chromium's init-time
53
+ * auto-selection before the resolved selection has settled.
45
54
  */
46
55
  function deriveState(presentation, mediaElement) {
47
56
  if (!mediaElement || !presentation) return "preconditions-unmet";
48
57
  return getTracksByType(presentation, "text").length > 0 ? "sync-active" : "preconditions-unmet";
49
58
  }
59
+ /**
60
+ * Map the DOM-showing track back to standing user intent. No showing track is an
61
+ * explicit `'off'`. Otherwise prefer a language-based partial (so the pick
62
+ * persists across source changes); fall back to `{ id }` for a track without a
63
+ * language (precise within a source, just not portable).
64
+ */
65
+ function deriveTextTrackIntent(showingId, modelTextTracks) {
66
+ if (!showingId) return "off";
67
+ const language = modelTextTracks.find((track) => track.id === showingId)?.language;
68
+ return language ? { language } : { id: showingId };
69
+ }
50
70
  function syncTextTracksSetup({ state, context, config }) {
51
71
  const { addSubtitlesTracksToMedia, getShowingSubtitlesTrackFromMedia, removeAllSubtitlesTracksFromMedia } = config;
52
72
  const derivedStateSignal = computed(() => deriveState(state.presentation.get(), context.mediaElement.get()));
@@ -58,7 +78,8 @@ function syncTextTracksSetup({ state, context, config }) {
58
78
  "sync-active": {
59
79
  entry: () => {
60
80
  const mediaElement = context.mediaElement.get();
61
- addSubtitlesTracksToMedia(mediaElement, getTracksByType(state.presentation.get(), "text"));
81
+ const modelTextTracks = getTracksByType(state.presentation.get(), "text");
82
+ addSubtitlesTracksToMedia(mediaElement, modelTextTracks);
62
83
  syncTextTrackModes(mediaElement.textTracks, state.selectedTextTrackId.get());
63
84
  let inSettlingWindow = true;
64
85
  const settlingTimeout = setTimeout(() => {
@@ -69,9 +90,9 @@ function syncTextTracksSetup({ state, context, config }) {
69
90
  syncTextTrackModes(mediaElement.textTracks, state.selectedTextTrackId.get());
70
91
  return;
71
92
  }
72
- const nextId = getShowingSubtitlesTrackFromMedia(mediaElement)?.id || void 0;
73
- if (nextId === state.selectedTextTrackId.get()) return;
74
- state.selectedTextTrackId.set(nextId);
93
+ const showingId = getShowingSubtitlesTrackFromMedia(mediaElement)?.id || void 0;
94
+ if (showingId === state.selectedTextTrackId.get()) return;
95
+ state.userTextTrackSelection.set(deriveTextTrackIntent(showingId, modelTextTracks));
75
96
  };
76
97
  const unlisten = listen(mediaElement.textTracks, "change", onChange);
77
98
  return () => {
@@ -89,7 +110,11 @@ function syncTextTracksSetup({ state, context, config }) {
89
110
  });
90
111
  }
91
112
  const syncTextTracks = defineBehavior({
92
- stateKeys: ["presentation", "selectedTextTrackId"],
113
+ stateKeys: [
114
+ "presentation",
115
+ "selectedTextTrackId",
116
+ "userTextTrackSelection"
117
+ ],
93
118
  contextKeys: ["mediaElement", "textTracksActor"],
94
119
  setup: syncTextTracksSetup
95
120
  });
@@ -1 +1 @@
1
- {"version":3,"file":"sync-text-tracks.js","names":[],"sources":["../../../../../src/playback/behaviors/dom/sync-text-tracks.ts"],"sourcesContent":["/**\n * **Own the text-track slots on the host media element, mirroring the SPF\n * model.** When a presentation is resolved and a media element is\n * available, allocate one slot in `mediaElement.textTracks` per model text\n * track — via creating `<track>` children, since that's the only spec\n * mechanism for adding *and* removing entries to `textTracks` (no\n * `removeTextTrack` API exists). Once slots are provisioned, mirror\n * `selectedTextTrackId` into their `mode`s, and propagate user-initiated\n * DOM `change` events back to `selectedTextTrackId` so non-SPF consumers\n * (host-page captions buttons, browser native UI, video.js store) remain\n * the canonical write path for selection.\n *\n * Single-positive-state reactor (`'preconditions-unmet'` ↔ `'sync-active'`):\n * the entry allocates the slots, applies the initial selection, attaches\n * the `change` listener, and opens a brief Chromium settling-window guard —\n * all transition-driven, fire-once on state entry, with paired cleanup on\n * state exit. A single `effects:` mirrors subsequent\n * `selectedTextTrackId` changes into `mode`s; that's the only\n * continuous-reactivity concern.\n *\n * State-exit cleanup also sends a `'clear'` message to the\n * `TextTracksActor` so its cue+segment cache (keyed by trackId) is\n * dropped alongside the DOM `<track>` slots. The actor itself is owned\n * by `setupTextTrackActors` and bound to mediaElement, not presentation,\n * so it survives source resets; clearing its context here keeps the\n * cache consistent with the DOM. Without this, a subsequent\n * presentation reusing a trackId would have `getSegmentsToLoad` treat\n * its segments as already-buffered and skip loading them.\n *\n * Multi-writer with `selectTextTrack` is intentional and orthogonal:\n * `selectTextTrack` owns the *default-on-load / clear-on-unload* contract\n * for `selectedTextTrackId`; this behavior writes only from DOM `change`\n * events. They reflect distinct decision-making domains (config-driven\n * intent vs DOM-driven user action), so this behavior never clears the\n * slot on source unload — that's `selectTextTrack`'s contract. (It *does*\n * write `undefined` when a user disables all tracks via native UI, since\n * that's a real user action that should round-trip into SPF state.)\n */\n\nimport { listen } from '@videojs/utils/dom';\nimport { defineBehavior } from '../../../core/composition/create-composition';\nimport type { Reactor } from '../../../core/reactors/create-machine-reactor';\nimport { createMachineReactor } from '../../../core/reactors/create-machine-reactor';\nimport { computed, peek, type ReadonlySignal, type Signal } from '../../../core/signals/primitives';\nimport { syncTextTrackModes } from '../../../media/dom/text/text-track-slots';\nimport type { MaybeResolvedPresentation, PartiallyResolvedTextTrack, TextTrack } from '../../../media/types';\nimport { getTracksByType } from '../../../media/utils/tracks';\nimport type { TextTracksActor } from '../../actors/text-tracks';\n\ntype SyncTextTracksFsmState = 'preconditions-unmet' | 'sync-active';\n\nexport interface SyncTextTracksConfig {\n /**\n * Create and append SPF-owned `<track>` slots on `mediaElement`, one per\n * model text track. Implementation tags each element so the read/remove\n * helpers can scope to SPF-owned slots. **Required** — the behavior is\n * DOM-binding-neutral and the composing engine supplies the integration.\n */\n addSubtitlesTracksToMedia: (\n mediaElement: HTMLMediaElement,\n modelTextTracks: readonly (PartiallyResolvedTextTrack | TextTrack)[]\n ) => void;\n /**\n * Return the SPF-owned subtitle/caption `TextTrack` currently in `'showing'`\n * mode, or `undefined` if none. Used by the DOM `change` bridge to mirror\n * native-UI selection back into `selectedTextTrackId`.\n */\n getShowingSubtitlesTrackFromMedia: (mediaElement: HTMLMediaElement) => globalThis.TextTrack | undefined;\n /**\n * Remove every SPF-owned `<track>` child from `mediaElement`. Called on\n * state exit (source unload, behavior destroy) to evict slots.\n */\n removeAllSubtitlesTracksFromMedia: (mediaElement: HTMLMediaElement) => void;\n}\n\nfunction deriveState(\n presentation: MaybeResolvedPresentation | undefined,\n mediaElement: HTMLMediaElement | undefined\n): SyncTextTracksFsmState {\n if (!mediaElement || !presentation) return 'preconditions-unmet';\n return getTracksByType(presentation, 'text').length > 0 ? 'sync-active' : 'preconditions-unmet';\n}\n\nfunction syncTextTracksSetup({\n state,\n context,\n config,\n}: {\n state: {\n presentation: ReadonlySignal<MaybeResolvedPresentation | undefined>;\n selectedTextTrackId: Signal<string | undefined>;\n };\n context: {\n mediaElement: ReadonlySignal<HTMLMediaElement | undefined>;\n textTracksActor: ReadonlySignal<TextTracksActor<VTTCue> | undefined>;\n };\n config: SyncTextTracksConfig;\n}): Reactor<SyncTextTracksFsmState | 'destroying' | 'destroyed'> {\n const { addSubtitlesTracksToMedia, getShowingSubtitlesTrackFromMedia, removeAllSubtitlesTracksFromMedia } = config;\n\n const derivedStateSignal = computed(() => deriveState(state.presentation.get(), context.mediaElement.get()));\n\n return createMachineReactor<SyncTextTracksFsmState>({\n initial: 'preconditions-unmet',\n monitor: () => derivedStateSignal.get(),\n states: {\n 'preconditions-unmet': {},\n\n 'sync-active': {\n // Allocate slots, apply the initial selection, attach the change\n // listener, and open the settling window — all transition-driven\n // (fire once per state entry). Entry is auto-untracked, so `.get()`\n // here doesn't subscribe; the returned cleanup detaches and evicts\n // on state exit (src reset through 'preconditions-unmet') and on\n // destroy.\n entry: () => {\n const mediaElement = context.mediaElement.get()!;\n // `getTracksByType('text', ...)` returns text tracks only — the\n // selection-set filter inside the helper ensures that — but its\n // declared return is the wide track union. Mirror the cast\n // pattern used by `quality-switching` for the video branch.\n const modelTextTracks = getTracksByType(state.presentation.get()!, 'text') as readonly (\n | PartiallyResolvedTextTrack\n | TextTrack\n )[];\n\n addSubtitlesTracksToMedia(mediaElement, modelTextTracks);\n // Apply our selection synchronously so the change-event tasks\n // these mode writes queue land in the macrotask queue *before*\n // the settling-close `setTimeout(0)` queued below. This keeps\n // the settling window open long enough to swallow Chromium's\n // own auto-selection task (also scheduled in the next tick after\n // `<track>` insertion); if the order flipped, our handler would\n // treat Chromium's auto-pick as a user action. The `effects:`\n // block below handles subsequent `selectedTextTrackId` changes.\n syncTextTrackModes(mediaElement.textTracks, state.selectedTextTrackId.get());\n\n // Chromium re-applies its own selection across the next task tick\n // after `<track>` insertion (default-track auto-pick, language\n // preference). During this window, the `change` event may fire\n // with browser-chosen modes that don't reflect a real user action;\n // re-apply our modes silently rather than writing them back.\n let inSettlingWindow = true;\n const settlingTimeout = setTimeout(() => {\n inSettlingWindow = false;\n }, 0);\n\n const onChange = (): void => {\n if (inSettlingWindow) {\n syncTextTrackModes(mediaElement.textTracks, state.selectedTextTrackId.get());\n return;\n }\n const showingTrack = getShowingSubtitlesTrackFromMedia(mediaElement);\n // `showingTrack.id` matches the SPF id we set when the slot was\n // allocated. Empty-string ids fall through to `undefined`.\n const nextId = showingTrack?.id || undefined;\n if (nextId === state.selectedTextTrackId.get()) return;\n state.selectedTextTrackId.set(nextId);\n };\n\n const unlisten = listen(mediaElement.textTracks, 'change', onChange);\n\n return () => {\n unlisten();\n clearTimeout(settlingTimeout);\n removeAllSubtitlesTracksFromMedia(mediaElement);\n // Clear the TextTracksActor's cue+segment cache, which is\n // keyed by trackId. If we don't, a subsequent presentation\n // reusing a trackId would have `getSegmentsToLoad` treat its\n // segments as already-buffered. The DOM cleanup above\n // already evicted the live cues; this drops the cache that\n // tracked them. The actor itself is owned by\n // `setupTextTrackActors` (mediaElement-bound lifecycle), so\n // we send rather than destroy.\n peek(context.textTracksActor)?.send({ type: 'clear' });\n };\n },\n\n // Mirror selection changes into mode. The state machine handles\n // mediaElement/presentation changes via 'preconditions-unmet'\n // round-trips, so we peek mediaElement here.\n effects: () => {\n const mediaElement = peek(context.mediaElement)!;\n syncTextTrackModes(mediaElement.textTracks, state.selectedTextTrackId.get());\n },\n },\n },\n });\n}\n\nexport const syncTextTracks = defineBehavior({\n stateKeys: ['presentation', 'selectedTextTrackId'],\n contextKeys: ['mediaElement', 'textTracksActor'],\n setup: syncTextTracksSetup,\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2EA,SAAS,YACP,cACA,cACwB;AACxB,KAAI,CAAC,gBAAgB,CAAC,aAAc,QAAO;AAC3C,QAAO,gBAAgB,cAAc,OAAO,CAAC,SAAS,IAAI,gBAAgB;;AAG5E,SAAS,oBAAoB,EAC3B,OACA,SACA,UAW+D;CAC/D,MAAM,EAAE,2BAA2B,mCAAmC,sCAAsC;CAE5G,MAAM,qBAAqB,eAAe,YAAY,MAAM,aAAa,KAAK,EAAE,QAAQ,aAAa,KAAK,CAAC,CAAC;AAE5G,QAAO,qBAA6C;EAClD,SAAS;EACT,eAAe,mBAAmB,KAAK;EACvC,QAAQ;GACN,uBAAuB,EAAE;GAEzB,eAAe;IAOb,aAAa;KACX,MAAM,eAAe,QAAQ,aAAa,KAAK;AAU/C,+BAA0B,cALF,gBAAgB,MAAM,aAAa,KAAK,EAAG,OAAO,CAKlB;AASxD,wBAAmB,aAAa,YAAY,MAAM,oBAAoB,KAAK,CAAC;KAO5E,IAAI,mBAAmB;KACvB,MAAM,kBAAkB,iBAAiB;AACvC,yBAAmB;QAClB,EAAE;KAEL,MAAM,iBAAuB;AAC3B,UAAI,kBAAkB;AACpB,0BAAmB,aAAa,YAAY,MAAM,oBAAoB,KAAK,CAAC;AAC5E;;MAKF,MAAM,SAHe,kCAAkC,aAAa,EAGvC,MAAM,KAAA;AACnC,UAAI,WAAW,MAAM,oBAAoB,KAAK,CAAE;AAChD,YAAM,oBAAoB,IAAI,OAAO;;KAGvC,MAAM,WAAW,OAAO,aAAa,YAAY,UAAU,SAAS;AAEpE,kBAAa;AACX,gBAAU;AACV,mBAAa,gBAAgB;AAC7B,wCAAkC,aAAa;AAS/C,WAAK,QAAQ,gBAAgB,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;;;IAO1D,eAAe;AAEb,wBADqB,KAAK,QAAQ,aAAa,CACf,YAAY,MAAM,oBAAoB,KAAK,CAAC;;IAE/E;GACF;EACF,CAAC;;AAGJ,MAAa,iBAAiB,eAAe;CAC3C,WAAW,CAAC,gBAAgB,sBAAsB;CAClD,aAAa,CAAC,gBAAgB,kBAAkB;CAChD,OAAO;CACR,CAAC"}
1
+ {"version":3,"file":"sync-text-tracks.js","names":[],"sources":["../../../../../src/playback/behaviors/dom/sync-text-tracks.ts"],"sourcesContent":["/**\n * **Own the text-track slots on the host media element, mirroring the SPF\n * model.** When a presentation is resolved and a media element is\n * available, allocate one slot in `mediaElement.textTracks` per model text\n * track — via creating `<track>` children, since that's the only spec\n * mechanism for adding *and* removing entries to `textTracks` (no\n * `removeTextTrack` API exists). Once slots are provisioned, mirror the\n * resolved `selectedTextTrackId` into their `mode`s (one-way: state → DOM),\n * and propagate user-initiated DOM `change` events back to\n * `userTextTrackSelection` — the standing *intent* (a language-based partial,\n * or `'off'`) that `switchTextTrack` resolves into `selectedTextTrackId`. So\n * non-SPF consumers (host-page captions buttons, browser native UI, video.js\n * store) drive selection by expressing intent, not by writing the resolved id.\n *\n * Single-positive-state reactor (`'preconditions-unmet'` ↔ `'sync-active'`):\n * the entry allocates the slots, applies the initial selection, attaches\n * the `change` listener, and opens a brief Chromium settling-window guard —\n * all transition-driven, fire-once on state entry, with paired cleanup on\n * state exit. A single `effects:` mirrors subsequent\n * `selectedTextTrackId` changes into `mode`s; that's the only\n * continuous-reactivity concern.\n *\n * State-exit cleanup also sends a `'clear'` message to the\n * `TextTracksActor` so its cue+segment cache (keyed by trackId) is\n * dropped alongside the DOM `<track>` slots. The actor itself is owned\n * by `setupTextTrackActors` and bound to mediaElement, not presentation,\n * so it survives source resets; clearing its context here keeps the\n * cache consistent with the DOM. Without this, a subsequent\n * presentation reusing a trackId would have `getSegmentsToLoad` treat\n * its segments as already-buffered and skip loading them.\n *\n * Single-writer separation: `selectedTextTrackId` is the resolved *output*\n * owned solely by `switchTextTrack`; this behavior only reads it (to mirror\n * modes). The write path here is `userTextTrackSelection` — the user-intent\n * *input* — so DOM action and the resolver never contend for one slot. The\n * intent isn't cleared on source unload (it's a standing preference, like\n * `userAudioTrackSelection`); `'off'` is written when the user disables all\n * tracks via native UI.\n *\n * Echo guard: `selectedTextTrackId` is exactly the id this behavior last drove\n * into the DOM, so a `change` event still showing it is our own echo (or a\n * resolver-driven correction — e.g. the picked track's CDN failed and the\n * resolver disabled it) and is ignored, never written back as a spurious user\n * action. Only a showing id that *differs* from the resolved id is a real user\n * pick. The settling-window guard additionally swallows Chromium's init-time\n * auto-selection before the resolved selection has settled.\n */\n\nimport { listen } from '@videojs/utils/dom';\nimport { defineBehavior } from '../../../core/composition/create-composition';\nimport type { Reactor } from '../../../core/reactors/create-machine-reactor';\nimport { createMachineReactor } from '../../../core/reactors/create-machine-reactor';\nimport { computed, peek, type ReadonlySignal, type Signal } from '../../../core/signals/primitives';\nimport { syncTextTrackModes } from '../../../media/dom/text/text-track-slots';\nimport type { MaybeResolvedPresentation, PartiallyResolvedTextTrack, TextTrack } from '../../../media/types';\nimport { getTracksByType } from '../../../media/utils/tracks';\nimport type { TextTracksActor } from '../../actors/text-tracks';\n\ntype SyncTextTracksFsmState = 'preconditions-unmet' | 'sync-active';\n\nexport interface SyncTextTracksConfig {\n /**\n * Create and append SPF-owned `<track>` slots on `mediaElement`, one per\n * model text track. Implementation tags each element so the read/remove\n * helpers can scope to SPF-owned slots. **Required** — the behavior is\n * DOM-binding-neutral and the composing engine supplies the integration.\n */\n addSubtitlesTracksToMedia: (\n mediaElement: HTMLMediaElement,\n modelTextTracks: readonly (PartiallyResolvedTextTrack | TextTrack)[]\n ) => void;\n /**\n * Return the SPF-owned subtitle/caption `TextTrack` currently in `'showing'`\n * mode, or `undefined` if none. Used by the DOM `change` bridge to mirror\n * native-UI selection back into `selectedTextTrackId`.\n */\n getShowingSubtitlesTrackFromMedia: (mediaElement: HTMLMediaElement) => globalThis.TextTrack | undefined;\n /**\n * Remove every SPF-owned `<track>` child from `mediaElement`. Called on\n * state exit (source unload, behavior destroy) to evict slots.\n */\n removeAllSubtitlesTracksFromMedia: (mediaElement: HTMLMediaElement) => void;\n}\n\nfunction deriveState(\n presentation: MaybeResolvedPresentation | undefined,\n mediaElement: HTMLMediaElement | undefined\n): SyncTextTracksFsmState {\n if (!mediaElement || !presentation) return 'preconditions-unmet';\n return getTracksByType(presentation, 'text').length > 0 ? 'sync-active' : 'preconditions-unmet';\n}\n\n/**\n * Map the DOM-showing track back to standing user intent. No showing track is an\n * explicit `'off'`. Otherwise prefer a language-based partial (so the pick\n * persists across source changes); fall back to `{ id }` for a track without a\n * language (precise within a source, just not portable).\n */\nfunction deriveTextTrackIntent(\n showingId: string | undefined,\n modelTextTracks: readonly (PartiallyResolvedTextTrack | TextTrack)[]\n): Partial<TextTrack> | 'off' {\n if (!showingId) return 'off';\n const language = modelTextTracks.find((track) => track.id === showingId)?.language;\n return language ? { language } : { id: showingId };\n}\n\nfunction syncTextTracksSetup({\n state,\n context,\n config,\n}: {\n state: {\n presentation: ReadonlySignal<MaybeResolvedPresentation | undefined>;\n // Read-only here: the resolved output owned by `switchTextTrack`, mirrored\n // into DOM modes and used as the echo-guard reference.\n selectedTextTrackId: ReadonlySignal<string | undefined>;\n // The write path: standing user intent the DOM bridge feeds.\n userTextTrackSelection: Signal<Partial<TextTrack> | 'off' | undefined>;\n };\n context: {\n mediaElement: ReadonlySignal<HTMLMediaElement | undefined>;\n textTracksActor: ReadonlySignal<TextTracksActor<VTTCue> | undefined>;\n };\n config: SyncTextTracksConfig;\n}): Reactor<SyncTextTracksFsmState | 'destroying' | 'destroyed'> {\n const { addSubtitlesTracksToMedia, getShowingSubtitlesTrackFromMedia, removeAllSubtitlesTracksFromMedia } = config;\n\n const derivedStateSignal = computed(() => deriveState(state.presentation.get(), context.mediaElement.get()));\n\n return createMachineReactor<SyncTextTracksFsmState>({\n initial: 'preconditions-unmet',\n monitor: () => derivedStateSignal.get(),\n states: {\n 'preconditions-unmet': {},\n\n 'sync-active': {\n // Allocate slots, apply the initial selection, attach the change\n // listener, and open the settling window — all transition-driven\n // (fire once per state entry). Entry is auto-untracked, so `.get()`\n // here doesn't subscribe; the returned cleanup detaches and evicts\n // on state exit (src reset through 'preconditions-unmet') and on\n // destroy.\n entry: () => {\n const mediaElement = context.mediaElement.get()!;\n // `getTracksByType('text', ...)` returns text tracks only — the\n // selection-set filter inside the helper ensures that — but its\n // declared return is the wide track union. Mirror the cast\n // pattern used by `track-switching` for the video branch.\n const modelTextTracks = getTracksByType(state.presentation.get()!, 'text') as readonly (\n | PartiallyResolvedTextTrack\n | TextTrack\n )[];\n\n addSubtitlesTracksToMedia(mediaElement, modelTextTracks);\n // Apply our selection synchronously so the change-event tasks\n // these mode writes queue land in the macrotask queue *before*\n // the settling-close `setTimeout(0)` queued below. This keeps\n // the settling window open long enough to swallow Chromium's\n // own auto-selection task (also scheduled in the next tick after\n // `<track>` insertion); if the order flipped, our handler would\n // treat Chromium's auto-pick as a user action. The `effects:`\n // block below handles subsequent `selectedTextTrackId` changes.\n syncTextTrackModes(mediaElement.textTracks, state.selectedTextTrackId.get());\n\n // Chromium re-applies its own selection across the next task tick\n // after `<track>` insertion (default-track auto-pick, language\n // preference). During this window, the `change` event may fire\n // with browser-chosen modes that don't reflect a real user action;\n // re-apply our modes silently rather than writing them back.\n let inSettlingWindow = true;\n const settlingTimeout = setTimeout(() => {\n inSettlingWindow = false;\n }, 0);\n\n const onChange = (): void => {\n if (inSettlingWindow) {\n syncTextTrackModes(mediaElement.textTracks, state.selectedTextTrackId.get());\n return;\n }\n const showingTrack = getShowingSubtitlesTrackFromMedia(mediaElement);\n // `showingTrack.id` matches the SPF id we set when the slot was\n // allocated. Empty-string ids fall through to `undefined`.\n const showingId = showingTrack?.id || undefined;\n // Echo guard: selectedTextTrackId is the id we last drove into the\n // DOM (mirror / resolver correction). A change still showing it is our\n // own echo — ignore it rather than write spurious intent.\n if (showingId === state.selectedTextTrackId.get()) return;\n // Genuine user action → write intent (resolved into selectedTextTrackId\n // by switchTextTrack), not the resolved id.\n state.userTextTrackSelection.set(deriveTextTrackIntent(showingId, modelTextTracks));\n };\n\n const unlisten = listen(mediaElement.textTracks, 'change', onChange);\n\n return () => {\n unlisten();\n clearTimeout(settlingTimeout);\n removeAllSubtitlesTracksFromMedia(mediaElement);\n // Clear the TextTracksActor's cue+segment cache, which is\n // keyed by trackId. If we don't, a subsequent presentation\n // reusing a trackId would have `getSegmentsToLoad` treat its\n // segments as already-buffered. The DOM cleanup above\n // already evicted the live cues; this drops the cache that\n // tracked them. The actor itself is owned by\n // `setupTextTrackActors` (mediaElement-bound lifecycle), so\n // we send rather than destroy.\n peek(context.textTracksActor)?.send({ type: 'clear' });\n };\n },\n\n // Mirror selection changes into mode. The state machine handles\n // mediaElement/presentation changes via 'preconditions-unmet'\n // round-trips, so we peek mediaElement here.\n effects: () => {\n const mediaElement = peek(context.mediaElement)!;\n syncTextTrackModes(mediaElement.textTracks, state.selectedTextTrackId.get());\n },\n },\n },\n });\n}\n\nexport const syncTextTracks = defineBehavior({\n stateKeys: ['presentation', 'selectedTextTrackId', 'userTextTrackSelection'],\n contextKeys: ['mediaElement', 'textTracksActor'],\n setup: syncTextTracksSetup,\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoFA,SAAS,YACP,cACA,cACwB;AACxB,KAAI,CAAC,gBAAgB,CAAC,aAAc,QAAO;AAC3C,QAAO,gBAAgB,cAAc,OAAO,CAAC,SAAS,IAAI,gBAAgB;;;;;;;;AAS5E,SAAS,sBACP,WACA,iBAC4B;AAC5B,KAAI,CAAC,UAAW,QAAO;CACvB,MAAM,WAAW,gBAAgB,MAAM,UAAU,MAAM,OAAO,UAAU,EAAE;AAC1E,QAAO,WAAW,EAAE,UAAU,GAAG,EAAE,IAAI,WAAW;;AAGpD,SAAS,oBAAoB,EAC3B,OACA,SACA,UAe+D;CAC/D,MAAM,EAAE,2BAA2B,mCAAmC,sCAAsC;CAE5G,MAAM,qBAAqB,eAAe,YAAY,MAAM,aAAa,KAAK,EAAE,QAAQ,aAAa,KAAK,CAAC,CAAC;AAE5G,QAAO,qBAA6C;EAClD,SAAS;EACT,eAAe,mBAAmB,KAAK;EACvC,QAAQ;GACN,uBAAuB,EAAE;GAEzB,eAAe;IAOb,aAAa;KACX,MAAM,eAAe,QAAQ,aAAa,KAAK;KAK/C,MAAM,kBAAkB,gBAAgB,MAAM,aAAa,KAAK,EAAG,OAAO;AAK1E,+BAA0B,cAAc,gBAAgB;AASxD,wBAAmB,aAAa,YAAY,MAAM,oBAAoB,KAAK,CAAC;KAO5E,IAAI,mBAAmB;KACvB,MAAM,kBAAkB,iBAAiB;AACvC,yBAAmB;QAClB,EAAE;KAEL,MAAM,iBAAuB;AAC3B,UAAI,kBAAkB;AACpB,0BAAmB,aAAa,YAAY,MAAM,oBAAoB,KAAK,CAAC;AAC5E;;MAKF,MAAM,YAHe,kCAAkC,aAAa,EAGpC,MAAM,KAAA;AAItC,UAAI,cAAc,MAAM,oBAAoB,KAAK,CAAE;AAGnD,YAAM,uBAAuB,IAAI,sBAAsB,WAAW,gBAAgB,CAAC;;KAGrF,MAAM,WAAW,OAAO,aAAa,YAAY,UAAU,SAAS;AAEpE,kBAAa;AACX,gBAAU;AACV,mBAAa,gBAAgB;AAC7B,wCAAkC,aAAa;AAS/C,WAAK,QAAQ,gBAAgB,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;;;IAO1D,eAAe;AAEb,wBADqB,KAAK,QAAQ,aAAa,CACf,YAAY,MAAM,oBAAoB,KAAK,CAAC;;IAE/E;GACF;EACF,CAAC;;AAGJ,MAAa,iBAAiB,eAAe;CAC3C,WAAW;EAAC;EAAgB;EAAuB;EAAyB;CAC5E,aAAa,CAAC,gBAAgB,kBAAkB;CAChD,OAAO;CACR,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"track-current-time.d.ts","names":[],"sources":["../../../../../src/playback/behaviors/dom/track-current-time.ts"],"mappings":";;;UAoBiB,gBAAA;EACf,WAAA;AAAA;AAAA,UAGe,kBAAA;EACf,YAAA,GAAe,gBAAA;AAAA;AAAA,UAGA,sBAAA;EAJA;;;;EASf,kBAAA;AAAA;AAAA,cAgCW,gBAAA;;;;;mBAxBW,MAAA,CAAO,gBAAA;IAAA;EAAA;;oBACJ,cAAA,CAAe,kBAAA;IAAA;EAAA"}
1
+ {"version":3,"file":"track-current-time.d.ts","names":[],"sources":["../../../../../src/playback/behaviors/dom/track-current-time.ts"],"mappings":";;;UAwBiB,gBAAA;EACf,WAAA;AAAA;AAAA,UAGe,kBAAA;EACf,YAAA,GAAe,gBAAA;AAAA;AAAA,UAGA,sBAAA;EAJA;;;;EASf,kBAAA;AAAA;AAAA,cAkCW,gBAAA;;;;;mBA1BW,MAAA,CAAO,gBAAA;IAAA;EAAA;;oBACJ,cAAA,CAAe,kBAAA;IAAA;EAAA"}
@@ -9,6 +9,10 @@ import { listen } from "@videojs/utils/dom";
9
9
  * at the new position when this event dispatches, so buffer management can
10
10
  * react immediately rather than waiting for `timeupdate`, which does not
11
11
  * fire while paused.
12
+ * - `emptied` — fires when the resource selection algorithm tears down the
13
+ * current media (e.g. a new `src` is set on the same element). Re-syncs so
14
+ * downstream state doesn't retain the stale playback position from the
15
+ * previous source when the engine is reused across src changes.
12
16
  *
13
17
  * Also syncs immediately when a media element becomes available.
14
18
  *
@@ -27,9 +31,11 @@ function trackCurrentTimeSetup({ state, context, config }) {
27
31
  }
28
32
  const sync = () => state.currentTime.set(mediaElement.currentTime);
29
33
  sync();
34
+ const removeEmptied = listen(mediaElement, "emptied", sync);
30
35
  const removeTimeupdate = listen(mediaElement, "timeupdate", sync);
31
36
  const removeSeeking = listen(mediaElement, "seeking", sync);
32
37
  return () => {
38
+ removeEmptied();
33
39
  removeTimeupdate();
34
40
  removeSeeking();
35
41
  };
@@ -1 +1 @@
1
- {"version":3,"file":"track-current-time.js","names":[],"sources":["../../../../../src/playback/behaviors/dom/track-current-time.ts"],"sourcesContent":["/**\n * Mirror `mediaElement.currentTime` into reactive state. Listens for:\n * - `timeupdate` — fires during playback (~4 Hz)\n * - `seeking` — fires when a seek begins; per spec, `currentTime` is already\n * at the new position when this event dispatches, so buffer management can\n * react immediately rather than waiting for `timeupdate`, which does not\n * fire while paused.\n *\n * Also syncs immediately when a media element becomes available.\n *\n * When no media element is attached, writes `config.defaultCurrentTime`\n * (default-default `0`, matching the HTMLMediaElement spec) so consumers\n * always see a defined position. Read-only mirror; does not push\n * `state.currentTime` back to the element.\n */\nimport { listen } from '@videojs/utils/dom';\nimport { defineBehavior } from '../../../core/composition/create-composition';\nimport { effect } from '../../../core/signals/effect';\nimport type { ReadonlySignal, Signal } from '../../../core/signals/primitives';\n\nexport interface CurrentTimeState {\n currentTime?: number;\n}\n\nexport interface CurrentTimeContext {\n mediaElement?: HTMLMediaElement | undefined;\n}\n\nexport interface TrackCurrentTimeConfig {\n /**\n * Value written to `state.currentTime` when no media element is attached.\n * Defaults to `0` — the HTMLMediaElement spec default.\n */\n defaultCurrentTime?: number;\n}\n\nfunction trackCurrentTimeSetup({\n state,\n context,\n config,\n}: {\n state: { currentTime: Signal<CurrentTimeState['currentTime']> };\n context: { mediaElement: ReadonlySignal<CurrentTimeContext['mediaElement']> };\n config?: TrackCurrentTimeConfig;\n}): () => void {\n const defaultCurrentTime = config?.defaultCurrentTime ?? 0;\n\n return effect(() => {\n const mediaElement = context.mediaElement.get();\n if (!mediaElement) {\n state.currentTime.set(defaultCurrentTime);\n return;\n }\n\n const sync = () => state.currentTime.set(mediaElement.currentTime);\n sync();\n const removeTimeupdate = listen(mediaElement, 'timeupdate', sync);\n const removeSeeking = listen(mediaElement, 'seeking', sync);\n return () => {\n removeTimeupdate();\n removeSeeking();\n };\n });\n}\n\nexport const trackCurrentTime = defineBehavior({\n stateKeys: ['currentTime'],\n contextKeys: ['mediaElement'],\n setup: trackCurrentTimeSetup,\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAoCA,SAAS,sBAAsB,EAC7B,OACA,SACA,UAKa;CACb,MAAM,qBAAqB,QAAQ,sBAAsB;AAEzD,QAAO,aAAa;EAClB,MAAM,eAAe,QAAQ,aAAa,KAAK;AAC/C,MAAI,CAAC,cAAc;AACjB,SAAM,YAAY,IAAI,mBAAmB;AACzC;;EAGF,MAAM,aAAa,MAAM,YAAY,IAAI,aAAa,YAAY;AAClE,QAAM;EACN,MAAM,mBAAmB,OAAO,cAAc,cAAc,KAAK;EACjE,MAAM,gBAAgB,OAAO,cAAc,WAAW,KAAK;AAC3D,eAAa;AACX,qBAAkB;AAClB,kBAAe;;GAEjB;;AAGJ,MAAa,mBAAmB,eAAe;CAC7C,WAAW,CAAC,cAAc;CAC1B,aAAa,CAAC,eAAe;CAC7B,OAAO;CACR,CAAC"}
1
+ {"version":3,"file":"track-current-time.js","names":[],"sources":["../../../../../src/playback/behaviors/dom/track-current-time.ts"],"sourcesContent":["/**\n * Mirror `mediaElement.currentTime` into reactive state. Listens for:\n * - `timeupdate` — fires during playback (~4 Hz)\n * - `seeking` — fires when a seek begins; per spec, `currentTime` is already\n * at the new position when this event dispatches, so buffer management can\n * react immediately rather than waiting for `timeupdate`, which does not\n * fire while paused.\n * - `emptied` — fires when the resource selection algorithm tears down the\n * current media (e.g. a new `src` is set on the same element). Re-syncs so\n * downstream state doesn't retain the stale playback position from the\n * previous source when the engine is reused across src changes.\n *\n * Also syncs immediately when a media element becomes available.\n *\n * When no media element is attached, writes `config.defaultCurrentTime`\n * (default-default `0`, matching the HTMLMediaElement spec) so consumers\n * always see a defined position. Read-only mirror; does not push\n * `state.currentTime` back to the element.\n */\nimport { listen } from '@videojs/utils/dom';\nimport { defineBehavior } from '../../../core/composition/create-composition';\nimport { effect } from '../../../core/signals/effect';\nimport type { ReadonlySignal, Signal } from '../../../core/signals/primitives';\n\nexport interface CurrentTimeState {\n currentTime?: number;\n}\n\nexport interface CurrentTimeContext {\n mediaElement?: HTMLMediaElement | undefined;\n}\n\nexport interface TrackCurrentTimeConfig {\n /**\n * Value written to `state.currentTime` when no media element is attached.\n * Defaults to `0` — the HTMLMediaElement spec default.\n */\n defaultCurrentTime?: number;\n}\n\nfunction trackCurrentTimeSetup({\n state,\n context,\n config,\n}: {\n state: { currentTime: Signal<CurrentTimeState['currentTime']> };\n context: { mediaElement: ReadonlySignal<CurrentTimeContext['mediaElement']> };\n config?: TrackCurrentTimeConfig;\n}): () => void {\n const defaultCurrentTime = config?.defaultCurrentTime ?? 0;\n\n return effect(() => {\n const mediaElement = context.mediaElement.get();\n if (!mediaElement) {\n state.currentTime.set(defaultCurrentTime);\n return;\n }\n\n const sync = () => state.currentTime.set(mediaElement.currentTime);\n sync();\n const removeEmptied = listen(mediaElement, 'emptied', sync);\n const removeTimeupdate = listen(mediaElement, 'timeupdate', sync);\n const removeSeeking = listen(mediaElement, 'seeking', sync);\n return () => {\n removeEmptied();\n removeTimeupdate();\n removeSeeking();\n };\n });\n}\n\nexport const trackCurrentTime = defineBehavior({\n stateKeys: ['currentTime'],\n contextKeys: ['mediaElement'],\n setup: trackCurrentTimeSetup,\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAwCA,SAAS,sBAAsB,EAC7B,OACA,SACA,UAKa;CACb,MAAM,qBAAqB,QAAQ,sBAAsB;AAEzD,QAAO,aAAa;EAClB,MAAM,eAAe,QAAQ,aAAa,KAAK;AAC/C,MAAI,CAAC,cAAc;AACjB,SAAM,YAAY,IAAI,mBAAmB;AACzC;;EAGF,MAAM,aAAa,MAAM,YAAY,IAAI,aAAa,YAAY;AAClE,QAAM;EACN,MAAM,gBAAgB,OAAO,cAAc,WAAW,KAAK;EAC3D,MAAM,mBAAmB,OAAO,cAAc,cAAc,KAAK;EACjE,MAAM,gBAAgB,OAAO,cAAc,WAAW,KAAK;AAC3D,eAAa;AACX,kBAAe;AACf,qBAAkB;AAClB,kBAAe;;GAEjB;;AAGJ,MAAa,mBAAmB,eAAe;CAC7C,WAAW,CAAC,cAAc;CAC1B,aAAa,CAAC,eAAe;CAC7B,OAAO;CACR,CAAC"}
@@ -1,8 +1,8 @@
1
1
  import { computed } from "../../../core/signals/primitives.js";
2
2
  import { defineBehavior } from "../../../core/composition/create-composition.js";
3
3
  import { createMachineReactor } from "../../../core/reactors/create-machine-reactor.js";
4
- import { getMaxBufferedEnd, shouldUpdateDuration, waitForSourceBuffersReady } from "../../../media/dom/mse/duration.js";
5
4
  import { waitForMediaSourceOpen } from "../../../media/dom/mse/mediasource-setup.js";
5
+ import { getMaxBufferedEnd, shouldUpdateDuration, waitForSourceBuffersReady } from "../../../media/dom/mse/duration.js";
6
6
  //#region src/playback/behaviors/dom/update-mediasource-duration.ts
7
7
  /**
8
8
  * **Propagate `presentation.duration` to `mediaSource.duration` — exactly
@@ -3,12 +3,13 @@ import { defineBehavior } from "../../core/composition/create-composition.js";
3
3
  import { ConcurrentRunner, Task } from "../../core/tasks/task.js";
4
4
  import { createMachineReactor } from "../../core/reactors/create-machine-reactor.js";
5
5
  import { isResolvedPresentation, isResolvedTrack } from "../../media/types/index.js";
6
- import { findTrack, updateTrackInPresentation } from "../../media/utils/tracks.js";
7
- import { AUDIO_TYPE_CONFIG, TEXT_TYPE_CONFIG, VIDEO_TYPE_CONFIG } from "./track-types.js";
8
- import { fetchResolvable, getResponseText } from "../../network/fetch.js";
9
- import { parseMediaPlaylist } from "../../media/hls/parse-media-playlist.js";
6
+ import { applyContainerMimeType, findTrack, updateTrackInPresentation } from "../../media/utils/tracks.js";
7
+ import { AUDIO_TYPE_CONFIG, TEXT_TYPE_CONFIG, VIDEO_TYPE_CONFIG } from "../primitives/track-types.js";
8
+ import { NON_FMP4_CONTAINER_MIMES, parseMediaPlaylist } from "../../media/hls/parse-media-playlist.js";
9
+ import { fetchResolvableText } from "../../network/fetch.js";
10
+ import { failoverFetch } from "../primitives/failover-fetch.js";
10
11
  //#region src/playback/behaviors/resolve-track.ts
11
- function setupTrackResolution({ state, config: { selectedKey, findTrackToResolve } }) {
12
+ function setupTrackResolution({ state, config: { selectedKey, findTrackToResolve, fetchResolvableText: fetchResolvableText$1 = fetchResolvableText } }) {
12
13
  const runner = new ConcurrentRunner();
13
14
  const derivedStateSignal = computed(() => isResolvedPresentation(state.presentation.get()) ? "presentation-resolved" : "presentation-unresolved");
14
15
  return createMachineReactor({
@@ -25,8 +26,12 @@ function setupTrackResolution({ state, config: { selectedKey, findTrackToResolve
25
26
  const track = findTrackToResolve(presentation, trackId);
26
27
  if (!track || isResolvedTrack(track)) return;
27
28
  runner.schedule(new Task(async (signal) => {
28
- const mediaTrack = parseMediaPlaylist(await getResponseText(await fetchResolvable(track, { signal })), track);
29
- update(state.presentation, (current) => isResolvedPresentation(current) ? updateTrackInPresentation(current, mediaTrack) : current);
29
+ const mediaTrack = parseMediaPlaylist(await fetchResolvableText$1(track, { signal }), track);
30
+ update(state.presentation, (current) => {
31
+ if (!isResolvedPresentation(current)) return current;
32
+ const patched = updateTrackInPresentation(current, mediaTrack);
33
+ return NON_FMP4_CONTAINER_MIMES.has(mediaTrack.mimeType) ? applyContainerMimeType(patched, mediaTrack.type, mediaTrack.mimeType) : patched;
34
+ });
30
35
  }, { id: track.id }));
31
36
  }]
32
37
  }
@@ -53,13 +58,19 @@ const TEXT_TRACK_RESOLUTION_CONFIG = {
53
58
  const resolveVideoTrack = defineBehavior({
54
59
  stateKeys: ["presentation", "selectedVideoTrackId"],
55
60
  contextKeys: [],
56
- setup: ({ state, config = {} }) => setupTrackResolution({
57
- state,
58
- config: {
61
+ setup: ({ state, config = {} }) => {
62
+ const trackConfig = {
59
63
  ...VIDEO_TRACK_RESOLUTION_CONFIG,
60
64
  ...config
61
- }
62
- })
65
+ };
66
+ return setupTrackResolution({
67
+ state,
68
+ config: {
69
+ ...trackConfig,
70
+ fetchResolvableText: failoverFetch(fetchResolvableText, state, trackConfig)
71
+ }
72
+ });
73
+ }
63
74
  });
64
75
  /**
65
76
  * Resolve unresolved audio tracks. Same shape as `resolveVideoTrack`,
@@ -68,13 +79,19 @@ const resolveVideoTrack = defineBehavior({
68
79
  const resolveAudioTrack = defineBehavior({
69
80
  stateKeys: ["presentation", "selectedAudioTrackId"],
70
81
  contextKeys: [],
71
- setup: ({ state, config = {} }) => setupTrackResolution({
72
- state,
73
- config: {
82
+ setup: ({ state, config = {} }) => {
83
+ const trackConfig = {
74
84
  ...AUDIO_TRACK_RESOLUTION_CONFIG,
75
85
  ...config
76
- }
77
- })
86
+ };
87
+ return setupTrackResolution({
88
+ state,
89
+ config: {
90
+ ...trackConfig,
91
+ fetchResolvableText: failoverFetch(fetchResolvableText, state, trackConfig)
92
+ }
93
+ });
94
+ }
78
95
  });
79
96
  /**
80
97
  * Resolve unresolved text tracks. Same shape as `resolveVideoTrack`,
@@ -83,13 +100,19 @@ const resolveAudioTrack = defineBehavior({
83
100
  const resolveTextTrack = defineBehavior({
84
101
  stateKeys: ["presentation", "selectedTextTrackId"],
85
102
  contextKeys: [],
86
- setup: ({ state, config = {} }) => setupTrackResolution({
87
- state,
88
- config: {
103
+ setup: ({ state, config = {} }) => {
104
+ const trackConfig = {
89
105
  ...TEXT_TRACK_RESOLUTION_CONFIG,
90
106
  ...config
91
- }
92
- })
107
+ };
108
+ return setupTrackResolution({
109
+ state,
110
+ config: {
111
+ ...trackConfig,
112
+ fetchResolvableText: failoverFetch(fetchResolvableText, state, trackConfig)
113
+ }
114
+ });
115
+ }
93
116
  });
94
117
  //#endregion
95
118
  export { resolveAudioTrack, resolveTextTrack, resolveVideoTrack };
@@ -1 +1 @@
1
- {"version":3,"file":"resolve-track.js","names":[],"sources":["../../../../src/playback/behaviors/resolve-track.ts"],"sourcesContent":["import { defineBehavior } from '../../core/composition/create-composition';\nimport { createMachineReactor } from '../../core/reactors/create-machine-reactor';\nimport { computed, peek, type ReadonlySignal, type Signal, update } from '../../core/signals/primitives';\nimport { ConcurrentRunner, Task } from '../../core/tasks/task';\nimport { parseMediaPlaylist } from '../../media/hls/parse-media-playlist';\nimport type { MaybeResolvedPresentation, PartiallyResolvedTrack, ResolvedTrack } from '../../media/types';\nimport { isResolvedPresentation, isResolvedTrack } from '../../media/types';\nimport { findTrack, updateTrackInPresentation } from '../../media/utils/tracks';\nimport { fetchResolvable, getResponseText } from '../../network/fetch';\nimport { AUDIO_TYPE_CONFIG, TEXT_TYPE_CONFIG, VIDEO_TYPE_CONFIG } from './track-types';\n\n// ============================================================================\n// Specialization helper\n//\n// `setupTrackResolution` has the same shape as a Behavior `setup` function:\n// `({ state, config }) => cleanup`. Each `resolveXTrack` export below calls it\n// from inside its own `defineBehavior` setup, supplying its per-type config\n// inline. The orchestration — gate on a selection, short-circuit when the\n// track is already resolved or missing, schedule the fetch+parse, and patch\n// the resolved track back into `state.presentation` — is shared.\n// ============================================================================\n\n/**\n * State shape for track resolution. Uses `MaybeResolvedPresentation` so it\n * matches the engine's slot type; resolution narrows internally.\n */\nexport interface ResolveTrackState {\n presentation?: MaybeResolvedPresentation;\n selectedVideoTrackId?: string;\n selectedAudioTrackId?: string;\n selectedTextTrackId?: string;\n}\n\ntype SelectedTrackKey = 'selectedVideoTrackId' | 'selectedAudioTrackId' | 'selectedTextTrackId';\n\ntype ResolveTrackStateMap<K extends SelectedTrackKey> = {\n presentation: Signal<ResolveTrackState['presentation']>;\n} & { [P in K]: ReadonlySignal<ResolveTrackState[P]> };\n\ninterface TrackResolutionConfig<K extends SelectedTrackKey> {\n selectedKey: K;\n findTrackToResolve: (\n presentation: MaybeResolvedPresentation,\n trackId: string\n ) => PartiallyResolvedTrack | ResolvedTrack | undefined;\n}\n\nfunction setupTrackResolution<K extends SelectedTrackKey>({\n state,\n config: { selectedKey, findTrackToResolve },\n}: {\n state: ResolveTrackStateMap<K>;\n config: TrackResolutionConfig<K>;\n}) {\n // NOTE: This can/maybe will be pulled into a per-use case factory (e.g. something like createTaskRunner() with args TBD),\n // likely eventually passed down via config or a new \"definitions\" argument. This will allow us to decide if we want our task runner/scheduler\n // to e.g. run concurrently (like we currently are), serially with a queue, or abort the previous task and replace it with the newly scheduled one. (CJP).\n const runner = new ConcurrentRunner();\n\n // Reactor states model the FSM the previous effect-based body was\n // hand-rolling. 'presentation-resolved' is entered when the\n // presentation is fully parsed (has a Ham id + selectionSets); leaving\n // it (presentation cleared or reset to an unresolved value) aborts all\n // in-flight tasks via the entry-cleanup. Most URL changes go through\n // 'presentation-unresolved' naturally (set undefined → set new partial\n // → re-parse), so the common case is covered by state-exit alone; the\n // task body's commit-time id check covers the pathological\n // resolved→resolved-without-unresolved transition.\n const derivedStateSignal = computed(() =>\n isResolvedPresentation(state.presentation.get())\n ? ('presentation-resolved' as const)\n : ('presentation-unresolved' as const)\n );\n\n return createMachineReactor({\n initial: 'presentation-unresolved',\n monitor: () => derivedStateSignal.get(),\n states: {\n 'presentation-unresolved': {},\n 'presentation-resolved': {\n // `entry` runs on state entry; the function it returns is the\n // state-exit cleanup. Returning `() => runner.abortAll()` binds\n // abort-of-in-flight-resolutions to leaving 'presentation-resolved'\n // (presentation cleared/reset, or behavior destroyed) —\n // source-change cancellation expressed structurally through the\n // state machine.\n entry: () => () => runner.abortAll(),\n effects: [\n () => {\n // The reactor's state transitions handle relevant presentation\n // changes (presentation-resolved ↔ presentation-unresolved);\n // within 'presentation-resolved' we peek (untracked read) so\n // internal updates (segments added by sibling tasks) don't\n // re-fire the effect.\n const presentation = peek(state.presentation);\n const trackId = state[selectedKey].get();\n if (!presentation || !trackId) return;\n\n const track = findTrackToResolve(presentation, trackId);\n if (!track || isResolvedTrack(track)) return;\n\n runner.schedule(\n // NOTE: This can/maybe will be pulled into a per-use case factory (e.g. something like createResolveTrackTask(track, context, config)),\n // likely eventually passed down via config or a new \"definitions\" argument (CJP).\n new Task(\n async (signal) => {\n const response = await fetchResolvable(track, { signal });\n const text = await getResponseText(response);\n const mediaTrack = parseMediaPlaylist(text, track);\n\n // Updater handles undefined inputs by returning current\n // unchanged; isResolvedPresentation narrows for the patch.\n // State-exit on resolving→unresolved fires runner.abortAll\n // before any URL change settles, and per the Fetch spec the\n // signal abort cancels in-flight body reads — so by the\n // time we reach this point the presentation we resolved\n // against is the live one.\n update(state.presentation, (current) =>\n isResolvedPresentation(current) ? updateTrackInPresentation(current, mediaTrack) : current\n );\n },\n { id: track.id }\n )\n );\n },\n ],\n },\n },\n });\n}\n\n// ============================================================================\n// Per-helper-per-type configs — defaults that variants spread engine config over\n// ============================================================================\n\nconst VIDEO_TRACK_RESOLUTION_CONFIG = {\n ...VIDEO_TYPE_CONFIG,\n findTrackToResolve: (presentation: MaybeResolvedPresentation, trackId: string) =>\n findTrack(presentation, 'video', trackId),\n} as const;\n\nconst AUDIO_TRACK_RESOLUTION_CONFIG = {\n ...AUDIO_TYPE_CONFIG,\n findTrackToResolve: (presentation: MaybeResolvedPresentation, trackId: string) =>\n findTrack(presentation, 'audio', trackId),\n} as const;\n\nconst TEXT_TRACK_RESOLUTION_CONFIG = {\n ...TEXT_TYPE_CONFIG,\n findTrackToResolve: (presentation: MaybeResolvedPresentation, trackId: string) =>\n findTrack(presentation, 'text', trackId),\n} as const;\n\n// ============================================================================\n// Specialized exports — one per track type\n// ============================================================================\n\n/**\n * Resolve unresolved video tracks. Schedules a fetch task whenever the\n * selected video track is partially resolved, parses the manifest, and\n * writes the resolved track back into `state.presentation`.\n */\nexport const resolveVideoTrack = defineBehavior({\n stateKeys: ['presentation', 'selectedVideoTrackId'],\n contextKeys: [],\n setup: ({ state, config = {} }: { state: ResolveTrackStateMap<'selectedVideoTrackId'>; config?: object }) =>\n setupTrackResolution({\n state,\n config: { ...VIDEO_TRACK_RESOLUTION_CONFIG, ...config },\n }),\n});\n\n/**\n * Resolve unresolved audio tracks. Same shape as `resolveVideoTrack`,\n * narrowed to audio.\n */\nexport const resolveAudioTrack = defineBehavior({\n stateKeys: ['presentation', 'selectedAudioTrackId'],\n contextKeys: [],\n setup: ({ state, config = {} }: { state: ResolveTrackStateMap<'selectedAudioTrackId'>; config?: object }) =>\n setupTrackResolution({\n state,\n config: { ...AUDIO_TRACK_RESOLUTION_CONFIG, ...config },\n }),\n});\n\n/**\n * Resolve unresolved text tracks. Same shape as `resolveVideoTrack`,\n * narrowed to text.\n */\nexport const resolveTextTrack = defineBehavior({\n stateKeys: ['presentation', 'selectedTextTrackId'],\n contextKeys: [],\n setup: ({ state, config = {} }: { state: ResolveTrackStateMap<'selectedTextTrackId'>; config?: object }) =>\n setupTrackResolution({\n state,\n config: { ...TEXT_TRACK_RESOLUTION_CONFIG, ...config },\n }),\n});\n"],"mappings":";;;;;;;;;;AA+CA,SAAS,qBAAiD,EACxD,OACA,QAAQ,EAAE,aAAa,wBAItB;CAID,MAAM,SAAS,IAAI,kBAAkB;CAWrC,MAAM,qBAAqB,eACzB,uBAAuB,MAAM,aAAa,KAAK,CAAC,GAC3C,0BACA,0BACN;AAED,QAAO,qBAAqB;EAC1B,SAAS;EACT,eAAe,mBAAmB,KAAK;EACvC,QAAQ;GACN,2BAA2B,EAAE;GAC7B,yBAAyB;IAOvB,mBAAmB,OAAO,UAAU;IACpC,SAAS,OACD;KAMJ,MAAM,eAAe,KAAK,MAAM,aAAa;KAC7C,MAAM,UAAU,MAAM,aAAa,KAAK;AACxC,SAAI,CAAC,gBAAgB,CAAC,QAAS;KAE/B,MAAM,QAAQ,mBAAmB,cAAc,QAAQ;AACvD,SAAI,CAAC,SAAS,gBAAgB,MAAM,CAAE;AAEtC,YAAO,SAGL,IAAI,KACF,OAAO,WAAW;MAGhB,MAAM,aAAa,mBADN,MAAM,gBADF,MAAM,gBAAgB,OAAO,EAAE,QAAQ,CAAC,CACb,EACA,MAAM;AASlD,aAAO,MAAM,eAAe,YAC1B,uBAAuB,QAAQ,GAAG,0BAA0B,SAAS,WAAW,GAAG,QACpF;QAEH,EAAE,IAAI,MAAM,IAAI,CACjB,CACF;MAEJ;IACF;GACF;EACF,CAAC;;AAOJ,MAAM,gCAAgC;CACpC,GAAG;CACH,qBAAqB,cAAyC,YAC5D,UAAU,cAAc,SAAS,QAAQ;CAC5C;AAED,MAAM,gCAAgC;CACpC,GAAG;CACH,qBAAqB,cAAyC,YAC5D,UAAU,cAAc,SAAS,QAAQ;CAC5C;AAED,MAAM,+BAA+B;CACnC,GAAG;CACH,qBAAqB,cAAyC,YAC5D,UAAU,cAAc,QAAQ,QAAQ;CAC3C;;;;;;AAWD,MAAa,oBAAoB,eAAe;CAC9C,WAAW,CAAC,gBAAgB,uBAAuB;CACnD,aAAa,EAAE;CACf,QAAQ,EAAE,OAAO,SAAS,EAAE,OAC1B,qBAAqB;EACnB;EACA,QAAQ;GAAE,GAAG;GAA+B,GAAG;GAAQ;EACxD,CAAC;CACL,CAAC;;;;;AAMF,MAAa,oBAAoB,eAAe;CAC9C,WAAW,CAAC,gBAAgB,uBAAuB;CACnD,aAAa,EAAE;CACf,QAAQ,EAAE,OAAO,SAAS,EAAE,OAC1B,qBAAqB;EACnB;EACA,QAAQ;GAAE,GAAG;GAA+B,GAAG;GAAQ;EACxD,CAAC;CACL,CAAC;;;;;AAMF,MAAa,mBAAmB,eAAe;CAC7C,WAAW,CAAC,gBAAgB,sBAAsB;CAClD,aAAa,EAAE;CACf,QAAQ,EAAE,OAAO,SAAS,EAAE,OAC1B,qBAAqB;EACnB;EACA,QAAQ;GAAE,GAAG;GAA8B,GAAG;GAAQ;EACvD,CAAC;CACL,CAAC"}
1
+ {"version":3,"file":"resolve-track.js","names":["defaultFetchResolvableText","fetchResolvableText"],"sources":["../../../../src/playback/behaviors/resolve-track.ts"],"sourcesContent":["import { defineBehavior } from '../../core/composition/create-composition';\nimport { createMachineReactor } from '../../core/reactors/create-machine-reactor';\nimport { computed, peek, type ReadonlySignal, type Signal, update } from '../../core/signals/primitives';\nimport { ConcurrentRunner, Task } from '../../core/tasks/task';\nimport { NON_FMP4_CONTAINER_MIMES, parseMediaPlaylist } from '../../media/hls/parse-media-playlist';\nimport type { MaybeResolvedPresentation, PartiallyResolvedTrack, ResolvedTrack } from '../../media/types';\nimport { isResolvedPresentation, isResolvedTrack } from '../../media/types';\nimport type { GetCdnId } from '../../media/utils/cdn';\nimport { applyContainerMimeType, findTrack, updateTrackInPresentation } from '../../media/utils/tracks';\nimport { fetchResolvableText as defaultFetchResolvableText, type FetchText } from '../../network/fetch';\nimport { failoverFetch } from '../primitives/failover-fetch';\nimport { AUDIO_TYPE_CONFIG, TEXT_TYPE_CONFIG, VIDEO_TYPE_CONFIG } from '../primitives/track-types';\n\n// ============================================================================\n// Specialization helper\n//\n// `setupTrackResolution` has the same shape as a Behavior `setup` function:\n// `({ state, config }) => cleanup`. Each `resolveXTrack` export below calls it\n// from inside its own `defineBehavior` setup, supplying its per-type config\n// inline. The orchestration — gate on a selection, short-circuit when the\n// track is already resolved or missing, schedule the fetch+parse, and patch\n// the resolved track back into `state.presentation` — is shared.\n// ============================================================================\n\n/**\n * State shape for track resolution. Uses `MaybeResolvedPresentation` so it\n * matches the engine's slot type; resolution narrows internally.\n */\nexport interface ResolveTrackState {\n presentation?: MaybeResolvedPresentation;\n selectedVideoTrackId?: string;\n selectedAudioTrackId?: string;\n selectedTextTrackId?: string;\n failedCdns?: string[];\n}\n\ntype SelectedTrackKey = 'selectedVideoTrackId' | 'selectedAudioTrackId' | 'selectedTextTrackId';\n\ntype ResolveTrackStateMap<K extends SelectedTrackKey> = {\n presentation: Signal<ResolveTrackState['presentation']>;\n} & { [P in K]: ReadonlySignal<ResolveTrackState[P]> };\n\ninterface TrackResolutionConfig<K extends SelectedTrackKey> {\n selectedKey: K;\n findTrackToResolve: (\n presentation: MaybeResolvedPresentation,\n trackId: string\n ) => PartiallyResolvedTrack | ResolvedTrack | undefined;\n /** Fetch a track's media-playlist text — already failover-decorated by the behavior. */\n fetchResolvableText?: FetchText;\n}\n\n/**\n * Engine-config slice each `resolve*` behavior reads to build its failover-\n * decorated playlist fetch.\n */\ninterface ResolveTrackConfig {\n /** CDN-id derivation for the failover trip; defaults to origin-based `getCdnId`. */\n getCdnId?: GetCdnId;\n}\n\nfunction setupTrackResolution<K extends SelectedTrackKey>({\n state,\n config: { selectedKey, findTrackToResolve, fetchResolvableText = defaultFetchResolvableText },\n}: {\n state: ResolveTrackStateMap<K>;\n config: TrackResolutionConfig<K>;\n}) {\n // NOTE: This can/maybe will be pulled into a per-use case factory (e.g. something like createTaskRunner() with args TBD),\n // likely eventually passed down via config or a new \"definitions\" argument. This will allow us to decide if we want our task runner/scheduler\n // to e.g. run concurrently (like we currently are), serially with a queue, or abort the previous task and replace it with the newly scheduled one. (CJP).\n const runner = new ConcurrentRunner();\n\n // Reactor states model the FSM the previous effect-based body was\n // hand-rolling. 'presentation-resolved' is entered when the\n // presentation is fully parsed (has a Ham id + selectionSets); leaving\n // it (presentation cleared or reset to an unresolved value) aborts all\n // in-flight tasks via the entry-cleanup. Most URL changes go through\n // 'presentation-unresolved' naturally (set undefined → set new partial\n // → re-parse), so the common case is covered by state-exit alone; the\n // task body's commit-time id check covers the pathological\n // resolved→resolved-without-unresolved transition.\n const derivedStateSignal = computed(() =>\n isResolvedPresentation(state.presentation.get())\n ? ('presentation-resolved' as const)\n : ('presentation-unresolved' as const)\n );\n\n return createMachineReactor({\n initial: 'presentation-unresolved',\n monitor: () => derivedStateSignal.get(),\n states: {\n 'presentation-unresolved': {},\n 'presentation-resolved': {\n // `entry` runs on state entry; the function it returns is the\n // state-exit cleanup. Returning `() => runner.abortAll()` binds\n // abort-of-in-flight-resolutions to leaving 'presentation-resolved'\n // (presentation cleared/reset, or behavior destroyed) —\n // source-change cancellation expressed structurally through the\n // state machine.\n entry: () => () => runner.abortAll(),\n effects: [\n () => {\n // The reactor's state transitions handle relevant presentation\n // changes (presentation-resolved ↔ presentation-unresolved);\n // within 'presentation-resolved' we peek (untracked read) so\n // internal updates (segments added by sibling tasks) don't\n // re-fire the effect.\n const presentation = peek(state.presentation);\n const trackId = state[selectedKey].get();\n if (!presentation || !trackId) return;\n\n const track = findTrackToResolve(presentation, trackId);\n if (!track || isResolvedTrack(track)) return;\n\n runner.schedule(\n // NOTE: This can/maybe will be pulled into a per-use case factory (e.g. something like createResolveTrackTask(track, context, config)),\n // likely eventually passed down via config or a new \"definitions\" argument (CJP).\n new Task(\n async (signal) => {\n // `fetchResolvableText` is the behavior's failover-decorated\n // fetch: it trips the CDN on a failed fetch (network error or\n // non-OK status). A parse failure is a content issue, not a\n // CDN-availability one, so it doesn't trip.\n const text = await fetchResolvableText(track, { signal });\n const mediaTrack = parseMediaPlaylist(text, track);\n\n // Updater handles undefined inputs by returning current\n // unchanged; isResolvedPresentation narrows for the patch.\n // State-exit on resolving→unresolved fires runner.abortAll\n // before any URL change settles, and per the Fetch spec the\n // signal abort cancels in-flight body reads — so by the\n // time we reach this point the presentation we resolved\n // against is the live one.\n update(state.presentation, (current) => {\n if (!isResolvedPresentation(current)) return current;\n const patched = updateTrackInPresentation(current, mediaTrack);\n // Container is uniform within a type (an ABR ladder shares\n // its container), so a detected non-fMP4 rendition (TS,\n // raw AAC) implies every rendition of *this* type matches —\n // relabel them all from one resolved playlist instead of\n // fetching each. Scoped to this track's own type: never cross\n // audio↔video (mixed-container sources exist, e.g. muxed-TS\n // video + raw-.aac audio), which also keeps per-type\n // resolutions' writes disjoint (no race).\n return NON_FMP4_CONTAINER_MIMES.has(mediaTrack.mimeType)\n ? applyContainerMimeType(patched, mediaTrack.type, mediaTrack.mimeType)\n : patched;\n });\n },\n { id: track.id }\n )\n );\n },\n ],\n },\n },\n });\n}\n\n// ============================================================================\n// Per-helper-per-type configs — defaults that variants spread engine config over\n// ============================================================================\n\nconst VIDEO_TRACK_RESOLUTION_CONFIG = {\n ...VIDEO_TYPE_CONFIG,\n findTrackToResolve: (presentation: MaybeResolvedPresentation, trackId: string) =>\n findTrack(presentation, 'video', trackId),\n} as const;\n\nconst AUDIO_TRACK_RESOLUTION_CONFIG = {\n ...AUDIO_TYPE_CONFIG,\n findTrackToResolve: (presentation: MaybeResolvedPresentation, trackId: string) =>\n findTrack(presentation, 'audio', trackId),\n} as const;\n\nconst TEXT_TRACK_RESOLUTION_CONFIG = {\n ...TEXT_TYPE_CONFIG,\n findTrackToResolve: (presentation: MaybeResolvedPresentation, trackId: string) =>\n findTrack(presentation, 'text', trackId),\n} as const;\n\n// ============================================================================\n// Specialized exports — one per track type\n// ============================================================================\n\n/**\n * Resolve unresolved video tracks. Schedules a fetch task whenever the\n * selected video track is partially resolved, parses the manifest, and\n * writes the resolved track back into `state.presentation`.\n */\nexport const resolveVideoTrack = defineBehavior({\n stateKeys: ['presentation', 'selectedVideoTrackId'],\n contextKeys: [],\n setup: ({\n state,\n config = {},\n }: {\n state: ResolveTrackStateMap<'selectedVideoTrackId'>;\n config?: ResolveTrackConfig;\n }) => {\n // Engine `config` layers over the per-type defaults (mirrors the other\n // per-type variants, see track-types.ts); `failoverFetch` reads its\n // `selectedKey` + `getCdnId` from the merged result. `fetchResolvableText`\n // is then placed AFTER the spread so the failover-decorated fetch wins —\n // unlike segments, playlists expose no overridable per-type fetch.\n const trackConfig = { ...VIDEO_TRACK_RESOLUTION_CONFIG, ...config };\n return setupTrackResolution({\n state,\n config: { ...trackConfig, fetchResolvableText: failoverFetch(defaultFetchResolvableText, state, trackConfig) },\n });\n },\n});\n\n/**\n * Resolve unresolved audio tracks. Same shape as `resolveVideoTrack`,\n * narrowed to audio.\n */\nexport const resolveAudioTrack = defineBehavior({\n stateKeys: ['presentation', 'selectedAudioTrackId'],\n contextKeys: [],\n setup: ({\n state,\n config = {},\n }: {\n state: ResolveTrackStateMap<'selectedAudioTrackId'>;\n config?: ResolveTrackConfig;\n }) => {\n // Key order is load-bearing — see resolveVideoTrack.\n const trackConfig = { ...AUDIO_TRACK_RESOLUTION_CONFIG, ...config };\n return setupTrackResolution({\n state,\n config: { ...trackConfig, fetchResolvableText: failoverFetch(defaultFetchResolvableText, state, trackConfig) },\n });\n },\n});\n\n/**\n * Resolve unresolved text tracks. Same shape as `resolveVideoTrack`,\n * narrowed to text.\n */\nexport const resolveTextTrack = defineBehavior({\n stateKeys: ['presentation', 'selectedTextTrackId'],\n contextKeys: [],\n setup: ({\n state,\n config = {},\n }: {\n state: ResolveTrackStateMap<'selectedTextTrackId'>;\n config?: ResolveTrackConfig;\n }) => {\n // Key order is load-bearing — see resolveVideoTrack.\n const trackConfig = { ...TEXT_TRACK_RESOLUTION_CONFIG, ...config };\n return setupTrackResolution({\n state,\n config: { ...trackConfig, fetchResolvableText: failoverFetch(defaultFetchResolvableText, state, trackConfig) },\n });\n },\n});\n"],"mappings":";;;;;;;;;;;AA6DA,SAAS,qBAAiD,EACxD,OACA,QAAQ,EAAE,aAAa,oBAAoB,qBAAA,wBAAsBA,yBAIhE;CAID,MAAM,SAAS,IAAI,kBAAkB;CAWrC,MAAM,qBAAqB,eACzB,uBAAuB,MAAM,aAAa,KAAK,CAAC,GAC3C,0BACA,0BACN;AAED,QAAO,qBAAqB;EAC1B,SAAS;EACT,eAAe,mBAAmB,KAAK;EACvC,QAAQ;GACN,2BAA2B,EAAE;GAC7B,yBAAyB;IAOvB,mBAAmB,OAAO,UAAU;IACpC,SAAS,OACD;KAMJ,MAAM,eAAe,KAAK,MAAM,aAAa;KAC7C,MAAM,UAAU,MAAM,aAAa,KAAK;AACxC,SAAI,CAAC,gBAAgB,CAAC,QAAS;KAE/B,MAAM,QAAQ,mBAAmB,cAAc,QAAQ;AACvD,SAAI,CAAC,SAAS,gBAAgB,MAAM,CAAE;AAEtC,YAAO,SAGL,IAAI,KACF,OAAO,WAAW;MAMhB,MAAM,aAAa,mBADN,MAAMC,sBAAoB,OAAO,EAAE,QAAQ,CAAC,EACb,MAAM;AASlD,aAAO,MAAM,eAAe,YAAY;AACtC,WAAI,CAAC,uBAAuB,QAAQ,CAAE,QAAO;OAC7C,MAAM,UAAU,0BAA0B,SAAS,WAAW;AAS9D,cAAO,yBAAyB,IAAI,WAAW,SAAS,GACpD,uBAAuB,SAAS,WAAW,MAAM,WAAW,SAAS,GACrE;QACJ;QAEJ,EAAE,IAAI,MAAM,IAAI,CACjB,CACF;MAEJ;IACF;GACF;EACF,CAAC;;AAOJ,MAAM,gCAAgC;CACpC,GAAG;CACH,qBAAqB,cAAyC,YAC5D,UAAU,cAAc,SAAS,QAAQ;CAC5C;AAED,MAAM,gCAAgC;CACpC,GAAG;CACH,qBAAqB,cAAyC,YAC5D,UAAU,cAAc,SAAS,QAAQ;CAC5C;AAED,MAAM,+BAA+B;CACnC,GAAG;CACH,qBAAqB,cAAyC,YAC5D,UAAU,cAAc,QAAQ,QAAQ;CAC3C;;;;;;AAWD,MAAa,oBAAoB,eAAe;CAC9C,WAAW,CAAC,gBAAgB,uBAAuB;CACnD,aAAa,EAAE;CACf,QAAQ,EACN,OACA,SAAS,EAAE,OAIP;EAMJ,MAAM,cAAc;GAAE,GAAG;GAA+B,GAAG;GAAQ;AACnE,SAAO,qBAAqB;GAC1B;GACA,QAAQ;IAAE,GAAG;IAAa,qBAAqB,cAAcD,qBAA4B,OAAO,YAAY;IAAE;GAC/G,CAAC;;CAEL,CAAC;;;;;AAMF,MAAa,oBAAoB,eAAe;CAC9C,WAAW,CAAC,gBAAgB,uBAAuB;CACnD,aAAa,EAAE;CACf,QAAQ,EACN,OACA,SAAS,EAAE,OAIP;EAEJ,MAAM,cAAc;GAAE,GAAG;GAA+B,GAAG;GAAQ;AACnE,SAAO,qBAAqB;GAC1B;GACA,QAAQ;IAAE,GAAG;IAAa,qBAAqB,cAAcA,qBAA4B,OAAO,YAAY;IAAE;GAC/G,CAAC;;CAEL,CAAC;;;;;AAMF,MAAa,mBAAmB,eAAe;CAC7C,WAAW,CAAC,gBAAgB,sBAAsB;CAClD,aAAa,EAAE;CACf,QAAQ,EACN,OACA,SAAS,EAAE,OAIP;EAEJ,MAAM,cAAc;GAAE,GAAG;GAA8B,GAAG;GAAQ;AAClE,SAAO,qBAAqB;GAC1B;GACA,QAAQ;IAAE,GAAG;IAAa,qBAAqB,cAAcA,qBAA4B,OAAO,YAAY;IAAE;GAC/G,CAAC;;CAEL,CAAC"}
@@ -0,0 +1,13 @@
1
+ import { TrackPicker, VideoSelectionConfig } from "../../media/primitives/select-tracks.js";
2
+
3
+ //#region src/playback/behaviors/select-tracks.d.ts
4
+ /**
5
+ * Config for `selectVideoTrack`. Pass `picker` to fully override selection
6
+ * logic; otherwise the default `pickFirstTrackId` is used.
7
+ */
8
+ interface SelectVideoTrackConfig extends VideoSelectionConfig {
9
+ picker?: TrackPicker<SelectVideoTrackConfig>;
10
+ }
11
+ //#endregion
12
+ export { SelectVideoTrackConfig };
13
+ //# sourceMappingURL=select-tracks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"select-tracks.d.ts","names":[],"sources":["../../../../src/playback/behaviors/select-tracks.ts"],"mappings":";;;;;;;UA0IiB,sBAAA,SAA+B,oBAAA;EAC9C,MAAA,GAAS,WAAA,CAAY,sBAAA;AAAA"}