@videojs/spf 10.0.0-beta.11 → 10.0.0-beta.13

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 (211) hide show
  1. package/dist/default/core/abr/bandwidth-estimator.js +2 -3
  2. package/dist/default/core/abr/bandwidth-estimator.js.map +1 -1
  3. package/dist/default/core/abr/ewma.js +1 -1
  4. package/dist/default/core/abr/quality-selection.js +2 -2
  5. package/dist/default/core/abr/quality-selection.js.map +1 -1
  6. package/dist/default/core/buffer/back-buffer.js +2 -2
  7. package/dist/default/core/buffer/forward-buffer.js +1 -1
  8. package/dist/default/core/features/calculate-presentation-duration.js +11 -9
  9. package/dist/default/core/features/calculate-presentation-duration.js.map +1 -1
  10. package/dist/default/core/features/quality-switching.js +7 -6
  11. package/dist/default/core/features/quality-switching.js.map +1 -1
  12. package/dist/default/core/features/resolve-presentation.js +30 -52
  13. package/dist/default/core/features/resolve-presentation.js.map +1 -1
  14. package/dist/default/core/features/resolve-track.js +17 -28
  15. package/dist/default/core/features/resolve-track.js.map +1 -1
  16. package/dist/default/core/features/select-tracks.js +18 -35
  17. package/dist/default/core/features/select-tracks.js.map +1 -1
  18. package/dist/default/core/features/sync-preload-attribute.js +26 -0
  19. package/dist/default/core/features/sync-preload-attribute.js.map +1 -0
  20. package/dist/default/core/hls/parse-attributes.js +1 -1
  21. package/dist/default/core/hls/parse-attributes.js.map +1 -1
  22. package/dist/default/core/hls/parse-media-playlist.js +1 -2
  23. package/dist/default/core/hls/parse-media-playlist.js.map +1 -1
  24. package/dist/default/core/hls/parse-multivariant.js +2 -3
  25. package/dist/default/core/hls/parse-multivariant.js.map +1 -1
  26. package/dist/default/core/hls/resolve-url.js +1 -1
  27. package/dist/default/core/signals/effect.js +41 -0
  28. package/dist/default/core/signals/effect.js.map +1 -0
  29. package/dist/default/core/signals/primitives.js +32 -0
  30. package/dist/default/core/signals/primitives.js.map +1 -0
  31. package/dist/default/core/task.js +3 -3
  32. package/dist/default/core/task.js.map +1 -1
  33. package/dist/default/core/types/index.js +1 -1
  34. package/dist/default/core/types/index.js.map +1 -1
  35. package/dist/default/core/utils/generate-id.js +1 -1
  36. package/dist/default/core/utils/track-selection.js +10 -2
  37. package/dist/default/core/utils/track-selection.js.map +1 -1
  38. package/dist/default/dom/features/end-of-stream.js +24 -48
  39. package/dist/default/dom/features/end-of-stream.js.map +1 -1
  40. package/dist/default/dom/features/load-segments.js +40 -36
  41. package/dist/default/dom/features/load-segments.js.map +1 -1
  42. package/dist/default/dom/features/load-text-track-cues.js +36 -26
  43. package/dist/default/dom/features/load-text-track-cues.js.map +1 -1
  44. package/dist/default/dom/features/segment-loader-actor.js +4 -5
  45. package/dist/default/dom/features/segment-loader-actor.js.map +1 -1
  46. package/dist/default/dom/features/setup-mediasource.js +29 -36
  47. package/dist/default/dom/features/setup-mediasource.js.map +1 -1
  48. package/dist/default/dom/features/setup-sourcebuffer.js +43 -35
  49. package/dist/default/dom/features/setup-sourcebuffer.js.map +1 -1
  50. package/dist/default/dom/features/setup-text-tracks.js +25 -46
  51. package/dist/default/dom/features/setup-text-tracks.js.map +1 -1
  52. package/dist/default/dom/features/sync-selected-text-track-from-dom.js +14 -23
  53. package/dist/default/dom/features/sync-selected-text-track-from-dom.js.map +1 -1
  54. package/dist/default/dom/features/sync-text-track-modes.js +12 -17
  55. package/dist/default/dom/features/sync-text-track-modes.js.map +1 -1
  56. package/dist/default/dom/features/track-current-time.js +10 -7
  57. package/dist/default/dom/features/track-current-time.js.map +1 -1
  58. package/dist/default/dom/features/track-playback-initiated.js +34 -36
  59. package/dist/default/dom/features/track-playback-initiated.js.map +1 -1
  60. package/dist/default/dom/features/track-playback-rate.js +12 -16
  61. package/dist/default/dom/features/track-playback-rate.js.map +1 -1
  62. package/dist/default/dom/features/update-duration.js +19 -13
  63. package/dist/default/dom/features/update-duration.js.map +1 -1
  64. package/dist/default/dom/media/append-segment.js +1 -1
  65. package/dist/default/dom/media/buffer-flusher.js +1 -1
  66. package/dist/default/dom/media/mediasource-setup.js +32 -40
  67. package/dist/default/dom/media/mediasource-setup.js.map +1 -1
  68. package/dist/default/dom/media/source-buffer-actor.js +16 -26
  69. package/dist/default/dom/media/source-buffer-actor.js.map +1 -1
  70. package/dist/default/dom/network/chunked-stream-iterable.js +1 -1
  71. package/dist/default/dom/network/chunked-stream-iterable.js.map +1 -1
  72. package/dist/default/dom/network/fetch.js +1 -2
  73. package/dist/default/dom/network/fetch.js.map +1 -1
  74. package/dist/default/dom/playback-engine/adapter.js +13 -13
  75. package/dist/default/dom/playback-engine/adapter.js.map +1 -1
  76. package/dist/default/dom/playback-engine/engine.js +24 -49
  77. package/dist/default/dom/playback-engine/engine.js.map +1 -1
  78. package/dist/default/dom/playback-engine.js +2 -2
  79. package/dist/default/dom/text/parse-vtt-segment.js +1 -1
  80. package/dist/default/dom.js +1 -2
  81. package/dist/default/index.js +1 -1
  82. package/dist/dev/core/abr/bandwidth-estimator.js +2 -3
  83. package/dist/dev/core/abr/bandwidth-estimator.js.map +1 -1
  84. package/dist/dev/core/abr/ewma.js +1 -1
  85. package/dist/dev/core/abr/quality-selection.js +2 -2
  86. package/dist/dev/core/abr/quality-selection.js.map +1 -1
  87. package/dist/dev/core/actor.d.ts +6 -19
  88. package/dist/dev/core/actor.d.ts.map +1 -1
  89. package/dist/dev/core/buffer/back-buffer.js +2 -2
  90. package/dist/dev/core/buffer/forward-buffer.js +1 -1
  91. package/dist/dev/core/features/calculate-presentation-duration.js +11 -9
  92. package/dist/dev/core/features/calculate-presentation-duration.js.map +1 -1
  93. package/dist/dev/core/features/quality-switching.js +7 -6
  94. package/dist/dev/core/features/quality-switching.js.map +1 -1
  95. package/dist/dev/core/features/resolve-presentation.js +30 -52
  96. package/dist/dev/core/features/resolve-presentation.js.map +1 -1
  97. package/dist/dev/core/features/resolve-track.js +17 -28
  98. package/dist/dev/core/features/resolve-track.js.map +1 -1
  99. package/dist/dev/core/features/select-tracks.js +18 -35
  100. package/dist/dev/core/features/select-tracks.js.map +1 -1
  101. package/dist/dev/core/features/sync-preload-attribute.js +26 -0
  102. package/dist/dev/core/features/sync-preload-attribute.js.map +1 -0
  103. package/dist/dev/core/hls/parse-attributes.js +1 -1
  104. package/dist/dev/core/hls/parse-attributes.js.map +1 -1
  105. package/dist/dev/core/hls/parse-media-playlist.js +1 -2
  106. package/dist/dev/core/hls/parse-media-playlist.js.map +1 -1
  107. package/dist/dev/core/hls/parse-multivariant.js +2 -3
  108. package/dist/dev/core/hls/parse-multivariant.js.map +1 -1
  109. package/dist/dev/core/hls/resolve-url.js +1 -1
  110. package/dist/dev/core/signals/effect.d.ts +15 -0
  111. package/dist/dev/core/signals/effect.d.ts.map +1 -0
  112. package/dist/dev/core/signals/effect.js +41 -0
  113. package/dist/dev/core/signals/effect.js.map +1 -0
  114. package/dist/dev/core/signals/primitives.d.ts +10 -0
  115. package/dist/dev/core/signals/primitives.d.ts.map +1 -0
  116. package/dist/dev/core/signals/primitives.js +32 -0
  117. package/dist/dev/core/signals/primitives.js.map +1 -0
  118. package/dist/dev/core/task.js +3 -3
  119. package/dist/dev/core/task.js.map +1 -1
  120. package/dist/dev/core/types/index.js +1 -1
  121. package/dist/dev/core/types/index.js.map +1 -1
  122. package/dist/dev/core/utils/generate-id.js +1 -1
  123. package/dist/dev/core/utils/track-selection.js +10 -2
  124. package/dist/dev/core/utils/track-selection.js.map +1 -1
  125. package/dist/dev/dom/features/end-of-stream.js +24 -48
  126. package/dist/dev/dom/features/end-of-stream.js.map +1 -1
  127. package/dist/dev/dom/features/load-segments.d.ts +4 -4
  128. package/dist/dev/dom/features/load-segments.d.ts.map +1 -1
  129. package/dist/dev/dom/features/load-segments.js +40 -36
  130. package/dist/dev/dom/features/load-segments.js.map +1 -1
  131. package/dist/dev/dom/features/load-text-track-cues.d.ts +4 -4
  132. package/dist/dev/dom/features/load-text-track-cues.d.ts.map +1 -1
  133. package/dist/dev/dom/features/load-text-track-cues.js +36 -26
  134. package/dist/dev/dom/features/load-text-track-cues.js.map +1 -1
  135. package/dist/dev/dom/features/segment-loader-actor.d.ts +1 -0
  136. package/dist/dev/dom/features/segment-loader-actor.js +4 -5
  137. package/dist/dev/dom/features/segment-loader-actor.js.map +1 -1
  138. package/dist/dev/dom/features/setup-mediasource.js +29 -36
  139. package/dist/dev/dom/features/setup-mediasource.js.map +1 -1
  140. package/dist/dev/dom/features/setup-sourcebuffer.d.ts.map +1 -1
  141. package/dist/dev/dom/features/setup-sourcebuffer.js +43 -35
  142. package/dist/dev/dom/features/setup-sourcebuffer.js.map +1 -1
  143. package/dist/dev/dom/features/setup-text-tracks.js +25 -46
  144. package/dist/dev/dom/features/setup-text-tracks.js.map +1 -1
  145. package/dist/dev/dom/features/sync-selected-text-track-from-dom.js +14 -23
  146. package/dist/dev/dom/features/sync-selected-text-track-from-dom.js.map +1 -1
  147. package/dist/dev/dom/features/sync-text-track-modes.js +12 -17
  148. package/dist/dev/dom/features/sync-text-track-modes.js.map +1 -1
  149. package/dist/dev/dom/features/track-current-time.d.ts +4 -4
  150. package/dist/dev/dom/features/track-current-time.d.ts.map +1 -1
  151. package/dist/dev/dom/features/track-current-time.js +10 -7
  152. package/dist/dev/dom/features/track-current-time.js.map +1 -1
  153. package/dist/dev/dom/features/track-playback-initiated.d.ts +14 -23
  154. package/dist/dev/dom/features/track-playback-initiated.d.ts.map +1 -1
  155. package/dist/dev/dom/features/track-playback-initiated.js +34 -36
  156. package/dist/dev/dom/features/track-playback-initiated.js.map +1 -1
  157. package/dist/dev/dom/features/track-playback-rate.d.ts +4 -4
  158. package/dist/dev/dom/features/track-playback-rate.d.ts.map +1 -1
  159. package/dist/dev/dom/features/track-playback-rate.js +12 -16
  160. package/dist/dev/dom/features/track-playback-rate.js.map +1 -1
  161. package/dist/dev/dom/features/update-duration.js +19 -13
  162. package/dist/dev/dom/features/update-duration.js.map +1 -1
  163. package/dist/dev/dom/media/append-segment.js +1 -1
  164. package/dist/dev/dom/media/buffer-flusher.js +1 -1
  165. package/dist/dev/dom/media/mediasource-setup.js +32 -40
  166. package/dist/dev/dom/media/mediasource-setup.js.map +1 -1
  167. package/dist/dev/dom/media/source-buffer-actor.d.ts +2 -2
  168. package/dist/dev/dom/media/source-buffer-actor.d.ts.map +1 -1
  169. package/dist/dev/dom/media/source-buffer-actor.js +16 -26
  170. package/dist/dev/dom/media/source-buffer-actor.js.map +1 -1
  171. package/dist/dev/dom/network/chunked-stream-iterable.js +1 -1
  172. package/dist/dev/dom/network/chunked-stream-iterable.js.map +1 -1
  173. package/dist/dev/dom/network/fetch.js +1 -2
  174. package/dist/dev/dom/network/fetch.js.map +1 -1
  175. package/dist/dev/dom/playback-engine/adapter.d.ts.map +1 -1
  176. package/dist/dev/dom/playback-engine/adapter.js +13 -13
  177. package/dist/dev/dom/playback-engine/adapter.js.map +1 -1
  178. package/dist/dev/dom/playback-engine/engine.d.ts +14 -26
  179. package/dist/dev/dom/playback-engine/engine.d.ts.map +1 -1
  180. package/dist/dev/dom/playback-engine/engine.js +24 -49
  181. package/dist/dev/dom/playback-engine/engine.js.map +1 -1
  182. package/dist/dev/dom/playback-engine.d.ts +3 -2
  183. package/dist/dev/dom/playback-engine.js +2 -2
  184. package/dist/dev/dom/text/parse-vtt-segment.js +1 -1
  185. package/dist/dev/dom.js +1 -2
  186. package/dist/dev/index.js +1 -1
  187. package/package.json +5 -4
  188. package/dist/default/all.js +0 -16
  189. package/dist/default/core/events/create-event-stream.js +0 -48
  190. package/dist/default/core/events/create-event-stream.js.map +0 -1
  191. package/dist/default/core/reactive/combine-latest.js +0 -72
  192. package/dist/default/core/reactive/combine-latest.js.map +0 -1
  193. package/dist/default/core/state/create-state.js +0 -143
  194. package/dist/default/core/state/create-state.js.map +0 -1
  195. package/dist/dev/all.js +0 -16
  196. package/dist/dev/core/events/create-event-stream.d.ts +0 -48
  197. package/dist/dev/core/events/create-event-stream.d.ts.map +0 -1
  198. package/dist/dev/core/events/create-event-stream.js +0 -48
  199. package/dist/dev/core/events/create-event-stream.js.map +0 -1
  200. package/dist/dev/core/features/resolve-presentation.d.ts +0 -16
  201. package/dist/dev/core/features/resolve-presentation.d.ts.map +0 -1
  202. package/dist/dev/core/features/resolve-track.d.ts +0 -13
  203. package/dist/dev/core/features/resolve-track.d.ts.map +0 -1
  204. package/dist/dev/core/features/select-tracks.d.ts +0 -11
  205. package/dist/dev/core/features/select-tracks.d.ts.map +0 -1
  206. package/dist/dev/core/reactive/combine-latest.js +0 -72
  207. package/dist/dev/core/reactive/combine-latest.js.map +0 -1
  208. package/dist/dev/core/state/create-state.d.ts +0 -104
  209. package/dist/dev/core/state/create-state.d.ts.map +0 -1
  210. package/dist/dev/core/state/create-state.js +0 -143
  211. package/dist/dev/core/state/create-state.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"load-segments.js","names":[],"sources":["../../../../src/dom/features/load-segments.ts"],"sourcesContent":["import { combineLatest } from '../../all';\nimport { type BandwidthState, sampleBandwidth } from '../../core/abr/bandwidth-estimator';\nimport { DEFAULT_FORWARD_BUFFER_CONFIG } from '../../core/buffer/forward-buffer';\nimport { createState, type WritableState } from '../../core/state/create-state';\nimport type { AddressableObject, Presentation, ResolvedTrack } from '../../core/types';\nimport { isResolvedTrack } from '../../core/types';\nimport { getSelectedTrack, type TrackSelectionState } from '../../core/utils/track-selection';\nimport type { SourceBufferActor } from '../media/source-buffer-actor';\nimport { ChunkedStreamIterable, type ChunkedStreamIterableOptions } from '../network/chunked-stream-iterable';\nimport { fetchResolvable } from '../network/fetch';\nimport {\n type BufferState,\n createSegmentLoaderActor,\n type SegmentLoaderActor,\n type SourceBufferState,\n} from './segment-loader-actor';\nimport type { MediaTrackType } from './setup-sourcebuffer';\n\n// Re-export buffer state types for consumers that import them from this module.\nexport type { BufferState, SourceBufferState };\n\n// Map track type to SourceBufferActor owner property key.\nconst ActorKeyByType = {\n video: 'videoBufferActor',\n audio: 'audioBufferActor',\n} as const;\n\n// ============================================================================\n// TRACKED FETCH\n// ============================================================================\n\n/**\n * Creates a fetch function that eagerly starts the HTTP request (TTFB is\n * awaited), then returns a lazy iterable over the response body that\n * transparently samples bandwidth per chunk.\n *\n * Separating connection start from body reading ensures `fetch()` is called\n * as soon as the task begins — not deferred until the actor's append loop\n * first pulls a chunk. This makes fetch timing predictable and observable\n * (e.g. in tests that record fetched URLs) regardless of downstream consumers.\n *\n * `onSample` bridges throughput state outward; see Phase 2 comment for detail.\n */\ntype FetchOptions = RequestInit & ChunkedStreamIterableOptions;\n\nfunction createTrackedFetch(\n throughput: WritableState<BandwidthState>,\n onSample?: (next: BandwidthState) => void\n): (addressable: AddressableObject, options?: FetchOptions) => Promise<AsyncIterable<Uint8Array>> {\n return async (addressable, options) => {\n const { minChunkSize, ...fetchOptions } = options ?? {};\n const response = await fetchResolvable(addressable, fetchOptions);\n if (!response.body) throw new Error('Response has no body');\n const body = response.body;\n return {\n [Symbol.asyncIterator]: async function* () {\n let chunkStart = performance.now();\n for await (const chunk of new ChunkedStreamIterable(\n body,\n ...(minChunkSize !== undefined ? [{ minChunkSize }] : [])\n )) {\n const elapsed = performance.now() - chunkStart;\n const next = sampleBandwidth(throughput.current, elapsed, chunk.byteLength);\n throughput.patch(next);\n throughput.flush();\n onSample?.(next);\n yield chunk;\n chunkStart = performance.now();\n }\n },\n };\n };\n}\n\n/**\n * Non-tracking fetch: eagerly starts the request and returns the response body\n * as a lazy chunk iterable. Used for audio tracks which don't sample bandwidth.\n * Pass `minChunkSize: Infinity` to accumulate the full body as a single chunk\n * (equivalent to arrayBuffer() but through the same streaming path).\n */\nasync function fetchStream(addressable: AddressableObject, options?: FetchOptions): Promise<AsyncIterable<Uint8Array>> {\n const { minChunkSize, ...fetchOptions } = options ?? {};\n const response = await fetchResolvable(addressable, fetchOptions);\n if (!response.body) throw new Error('Response has no body');\n return new ChunkedStreamIterable(response.body, ...(minChunkSize !== undefined ? [{ minChunkSize }] : []));\n}\n\n// ============================================================================\n// STATE & OWNERS\n// ============================================================================\n\n/**\n * State shape for segment loading.\n */\nexport interface SegmentLoadingState extends TrackSelectionState {\n presentation?: Presentation;\n preload?: string;\n bandwidthState?: BandwidthState;\n /** Current playback position in seconds. Defaults to 0 when undefined. */\n currentTime?: number;\n /** True once the user has initiated playback. Allows segment loading regardless of preload setting. */\n playbackInitiated?: boolean;\n}\n\n/**\n * Owners shape for segment loading.\n */\nexport interface SegmentLoadingOwners {\n videoBuffer?: SourceBuffer;\n audioBuffer?: SourceBuffer;\n videoBufferActor?: SourceBufferActor;\n audioBufferActor?: SourceBufferActor;\n}\n\n// ============================================================================\n// STATE SELECTOR\n// ============================================================================\n\n/**\n * The subset of state that drives segment loading decisions.\n * A plain \"pick\" with one derived field (resolved track ID).\n */\ntype LoadingInputs = {\n playbackInitiated: boolean | undefined;\n preload: string | undefined;\n currentTime: number | undefined;\n /** @TODO cleanup type precision via inference+generics */\n track: ReturnType<typeof getSelectedTrack>;\n segmentsCanLoad: boolean;\n};\n\nfunction selectLoadingInputs(\n [segmentsCanLoad, state]: [boolean, SegmentLoadingState],\n type: MediaTrackType\n): LoadingInputs {\n const { playbackInitiated, preload, currentTime } = state;\n const track = getSelectedTrack(state, type);\n return {\n playbackInitiated,\n preload,\n currentTime,\n track,\n segmentsCanLoad,\n };\n}\n\n/**\n * Equality function encoding the condition hierarchy for relevant changes.\n *\n * Pre-play (!playbackInitiated):\n * Only preload changes matter. currentTime and resolvedTrackId are ignored\n * (track changes not supported pre-play; currentTime value is used at\n * trigger time but changes don't re-trigger).\n *\n * playbackInitiated transition:\n * Always fires (handled in the subscriber; preload='auto' suppression\n * applied there since equality functions have no memory of prior values).\n *\n * Post-play (playbackInitiated):\n * resolvedTrackId changes (track switch or previously-unresolved track\n * resolving) and currentTime changes both trigger. preload is irrelevant.\n */\nconst segmentStartFor = (currentTime: number | undefined, track: ResolvedTrack | undefined) => {\n if (currentTime == null) return undefined;\n return track?.segments.find(\n ({ startTime, duration }, i, segments) =>\n currentTime >= startTime && (currentTime < startTime + duration || i === segments.length - 1)\n )?.startTime;\n};\n\n/**\n * Returns true when the inputs are equal (no meaningful change — don't fire).\n * Returns false when the inputs differ in a way that requires a new message.\n *\n * This IS the shouldLoadSegments logic, expressed as an equality function.\n */\nfunction loadingInputsEq(prevState: LoadingInputs, curState: LoadingInputs): boolean {\n if (!curState.segmentsCanLoad) return true;\n // Haven't started playback. Only care about preload changes.\n if (!curState.playbackInitiated) {\n if (curState.preload === 'none') return true; // blocked — equal, don't fire\n return curState.preload === prevState.preload; // equal if preload unchanged\n }\n // Transition: !playbackInitiated → playbackInitiated\n if (!prevState.playbackInitiated && curState.playbackInitiated) {\n if (prevState.preload !== 'auto') return false; // fire — message shape changes\n // preload was 'auto': fall through to segment comparison (suppress if same position)\n }\n\n // Don't treat as a change if we don't (yet) have a currently resolved track.\n if (!curState.track || !isResolvedTrack(curState.track)) return true;\n\n // If we *do* have a currently resolved track, treat as a change if the track ids have changed\n if (prevState.track?.id !== curState.track.id && isResolvedTrack(curState.track)) return false;\n\n // Finally, if playback has initiated *and* we have a resolved track, check if currentTime\n // has changed \"significantly\" (based on segment time range boundaries) and treat as a change\n // if so (regardless of whether or not the track has changed)\n return (\n segmentStartFor(prevState.currentTime, curState.track as ResolvedTrack) ===\n segmentStartFor(curState.currentTime, curState.track as ResolvedTrack)\n );\n}\n\n// ============================================================================\n// REACTOR\n// ============================================================================\n\n/**\n * Load segments orchestration — Reactor layer.\n *\n * Sends typed load messages to a SegmentLoaderActor when relevant conditions\n * change. Uses targeted subscriptions rather than broad combineLatest so only\n * meaningful state changes trigger evaluation.\n *\n * Condition hierarchy (see SegmentLoadingKey for detail):\n *\n * !playbackInitiated\n * preload==='none' (or unset) → dormant; no trigger\n * preload==='metadata' → trigger on transition to 'metadata'\n * preload==='auto' → trigger on transition to 'auto'\n *\n * !playbackInitiated → playbackInitiated\n * preload !== 'auto' → trigger (message shape changes)\n * preload === 'auto' → suppressed (was already full-range mode;\n * let segmentStart take over post-play)\n * KNOWN LIMITATION: seek-before-play with\n * preload='auto' is not supported — if the\n * user seeks before pressing play, the\n * first re-send is delayed until the next\n * segment boundary crossing post-play.\n *\n * playbackInitiated\n * resolvedTrackId changes → trigger\n * segmentStart(currentTime) changes → trigger (segment boundary only)\n *\n * @example\n * const cleanup = loadSegments({ state, owners }, { type: 'video' });\n */\nexport function loadSegments(\n {\n state,\n owners,\n }: {\n state: WritableState<SegmentLoadingState>;\n owners: WritableState<SegmentLoadingOwners>;\n },\n config: { type: MediaTrackType }\n): () => void {\n const { type } = config;\n const actorKey = ActorKeyByType[type];\n\n // Local throughput state — owns BandwidthState for this track's fetch loop.\n // Sampling is handled transparently inside fetchBytes; callers never touch it.\n //\n // MIGRATION BRIDGE: the onSample callback below keeps global state.bandwidthState\n // in sync so ABR (selectVideoTrack) continues to work unchanged. Remove this\n // bridge once ABR reads from throughput directly.\n const initialBandwidth = state.current.bandwidthState;\n const throughput = createState<BandwidthState>(\n initialBandwidth ?? {\n fastEstimate: 0,\n fastTotalWeight: 0,\n slowEstimate: 0,\n slowTotalWeight: 0,\n bytesSampled: 0,\n }\n );\n\n const fetchBytes =\n type === 'video'\n ? createTrackedFetch(\n throughput,\n initialBandwidth !== undefined\n ? (next) => {\n state.patch({ bandwidthState: next });\n // Flush immediately so switchQuality sees the new estimate before the\n // next segment fetch starts, rather than waiting for the microtask queue.\n state.flush();\n }\n : undefined\n )\n : fetchStream;\n\n const segmentLoader = createState<SegmentLoaderActor | undefined>(undefined);\n\n // SegmentLoaderActor lifecycle — watch only the actor key in owners so this\n // subscription does not fire on unrelated owners changes.\n const unsubActorLifecycle = owners.subscribe(\n (o) => o[actorKey],\n (actor) => {\n if (actor) {\n segmentLoader.patch(createSegmentLoaderActor(actor, fetchBytes));\n } else if (!actor && segmentLoader.current) {\n segmentLoader.current.destroy();\n segmentLoader.patch(undefined);\n }\n return () => {\n segmentLoader.current?.destroy();\n segmentLoader.patch(undefined);\n };\n }\n );\n\n const segmentsCanLoad = createState<boolean>(false);\n const unsubscribeCanLoadSegments = combineLatest([state, segmentLoader]).subscribe(\n ([currentState, currentSegmentLoader]) => {\n const track = getSelectedTrack(currentState, type);\n const trackResolved = !!track && isResolvedTrack(track);\n const segmentLoaderActorExists = !!currentSegmentLoader;\n segmentsCanLoad.patch(trackResolved && segmentLoaderActorExists);\n }\n );\n\n const unsubscribeShouldLoadSegments = combineLatest([segmentsCanLoad, state]).subscribe(\n ([segmentsCanLoad, state]) => selectLoadingInputs([segmentsCanLoad, state], type),\n ({ preload, playbackInitiated, currentTime, track }) => {\n const fullMode = preload === 'auto' || !!playbackInitiated;\n\n if (!fullMode) {\n // Metadata mode: init only, no range.\n /** @ts-expect-error */\n segmentLoader.current?.send({ type: 'load', track });\n } else {\n segmentLoader.current?.send({\n type: 'load',\n /** @ts-expect-error */\n track,\n range: {\n start: currentTime as number,\n end: (currentTime as number) + DEFAULT_FORWARD_BUFFER_CONFIG.bufferDuration,\n },\n });\n }\n },\n { equalityFn: loadingInputsEq }\n );\n\n return () => {\n unsubscribeCanLoadSegments();\n unsubscribeShouldLoadSegments();\n unsubActorLifecycle();\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"load-segments.js","names":[],"sources":["../../../../src/dom/features/load-segments.ts"],"sourcesContent":["import { type BandwidthState, sampleBandwidth } from '../../core/abr/bandwidth-estimator';\nimport { DEFAULT_FORWARD_BUFFER_CONFIG } from '../../core/buffer/forward-buffer';\nimport { effect } from '../../core/signals/effect';\nimport { computed, type Signal, signal } from '../../core/signals/primitives';\nimport type { AddressableObject, Presentation, ResolvedTrack } from '../../core/types';\nimport { isResolvedTrack } from '../../core/types';\nimport { getSelectedTrack, type TrackSelectionState } from '../../core/utils/track-selection';\nimport type { SourceBufferActor } from '../media/source-buffer-actor';\nimport { ChunkedStreamIterable, type ChunkedStreamIterableOptions } from '../network/chunked-stream-iterable';\nimport { fetchResolvable } from '../network/fetch';\nimport {\n type BufferState,\n createSegmentLoaderActor,\n type SegmentLoaderActor,\n type SourceBufferState,\n} from './segment-loader-actor';\nimport type { MediaTrackType } from './setup-sourcebuffer';\n\n// Re-export buffer state types for consumers that import them from this module.\nexport type { BufferState, SourceBufferState };\n\n// Map track type to SourceBufferActor owner property key.\nconst ActorKeyByType = {\n video: 'videoBufferActor',\n audio: 'audioBufferActor',\n} as const;\n\n// ============================================================================\n// TRACKED FETCH\n// ============================================================================\n\n/**\n * Creates a fetch function that eagerly starts the HTTP request (TTFB is\n * awaited), then returns a lazy iterable over the response body that\n * transparently samples bandwidth per chunk.\n *\n * Separating connection start from body reading ensures `fetch()` is called\n * as soon as the task begins — not deferred until the actor's append loop\n * first pulls a chunk. This makes fetch timing predictable and observable\n * (e.g. in tests that record fetched URLs) regardless of downstream consumers.\n *\n * `onSample` bridges throughput state outward; see Phase 2 comment for detail.\n */\ntype FetchOptions = RequestInit & ChunkedStreamIterableOptions;\n\nfunction createTrackedFetch(\n throughput: Signal<BandwidthState>,\n onSample?: (next: BandwidthState) => void\n): (addressable: AddressableObject, options?: FetchOptions) => Promise<AsyncIterable<Uint8Array>> {\n return async (addressable, options) => {\n const { minChunkSize, ...fetchOptions } = options ?? {};\n const response = await fetchResolvable(addressable, fetchOptions);\n if (!response.body) throw new Error('Response has no body');\n const body = response.body;\n return {\n [Symbol.asyncIterator]: async function* () {\n let chunkStart = performance.now();\n for await (const chunk of new ChunkedStreamIterable(\n body,\n ...(minChunkSize !== undefined ? [{ minChunkSize }] : [])\n )) {\n const elapsed = performance.now() - chunkStart;\n const next = sampleBandwidth(throughput.get(), elapsed, chunk.byteLength);\n throughput.set(next);\n onSample?.(next);\n yield chunk;\n chunkStart = performance.now();\n }\n },\n };\n };\n}\n\n/**\n * Non-tracking fetch: eagerly starts the request and returns the response body\n * as a lazy chunk iterable. Used for audio tracks which don't sample bandwidth.\n * Pass `minChunkSize: Infinity` to accumulate the full body as a single chunk\n * (equivalent to arrayBuffer() but through the same streaming path).\n */\nasync function fetchStream(addressable: AddressableObject, options?: FetchOptions): Promise<AsyncIterable<Uint8Array>> {\n const { minChunkSize, ...fetchOptions } = options ?? {};\n const response = await fetchResolvable(addressable, fetchOptions);\n if (!response.body) throw new Error('Response has no body');\n return new ChunkedStreamIterable(response.body, ...(minChunkSize !== undefined ? [{ minChunkSize }] : []));\n}\n\n// ============================================================================\n// STATE & OWNERS\n// ============================================================================\n\n/**\n * State shape for segment loading.\n */\nexport interface SegmentLoadingState extends TrackSelectionState {\n presentation?: Presentation;\n preload?: string;\n bandwidthState?: BandwidthState;\n /** Current playback position in seconds. Defaults to 0 when undefined. */\n currentTime?: number;\n /** True once the user has initiated playback. Allows segment loading regardless of preload setting. */\n playbackInitiated?: boolean;\n}\n\n/**\n * Owners shape for segment loading.\n */\nexport interface SegmentLoadingOwners {\n videoBuffer?: SourceBuffer;\n audioBuffer?: SourceBuffer;\n videoBufferActor?: SourceBufferActor;\n audioBufferActor?: SourceBufferActor;\n}\n\n// ============================================================================\n// STATE SELECTOR\n// ============================================================================\n\n/**\n * The subset of state that drives segment loading decisions.\n * A plain \"pick\" with one derived field (resolved track ID).\n */\ntype LoadingInputs = {\n playbackInitiated: boolean | undefined;\n preload: string | undefined;\n currentTime: number | undefined;\n /** @TODO cleanup type precision via inference+generics */\n track: ReturnType<typeof getSelectedTrack>;\n segmentsCanLoad: boolean;\n};\n\nfunction selectLoadingInputs(\n [segmentsCanLoad, state]: [boolean, SegmentLoadingState],\n type: MediaTrackType\n): LoadingInputs {\n const { playbackInitiated, preload, currentTime } = state;\n const track = getSelectedTrack(state, type);\n return {\n playbackInitiated,\n preload,\n currentTime,\n track,\n segmentsCanLoad,\n };\n}\n\n/**\n * Equality function encoding the condition hierarchy for relevant changes.\n *\n * Pre-play (!playbackInitiated):\n * Only preload changes matter. currentTime and resolvedTrackId are ignored\n * (track changes not supported pre-play; currentTime value is used at\n * trigger time but changes don't re-trigger).\n *\n * playbackInitiated transition:\n * Always fires (handled in the subscriber; preload='auto' suppression\n * applied there since equality functions have no memory of prior values).\n *\n * Post-play (playbackInitiated):\n * resolvedTrackId changes (track switch or previously-unresolved track\n * resolving) and currentTime changes both trigger. preload is irrelevant.\n */\nconst segmentStartFor = (currentTime: number | undefined, track: ResolvedTrack | undefined) => {\n if (currentTime == null) return undefined;\n return track?.segments.find(\n ({ startTime, duration }, i, segments) =>\n currentTime >= startTime && (currentTime < startTime + duration || i === segments.length - 1)\n )?.startTime;\n};\n\n/**\n * Returns true when the inputs are equal (no meaningful change — don't fire).\n * Returns false when the inputs differ in a way that requires a new message.\n *\n * This IS the shouldLoadSegments logic, expressed as an equality function.\n */\nfunction loadingInputsEq(prevState: LoadingInputs, curState: LoadingInputs): boolean {\n if (!curState.segmentsCanLoad) return true;\n // Haven't started playback. Only care about preload changes.\n if (!curState.playbackInitiated) {\n if (curState.preload === 'none') return true; // blocked — equal, don't fire\n return curState.preload === prevState.preload; // equal if preload unchanged\n }\n // Transition: !playbackInitiated → playbackInitiated\n if (!prevState.playbackInitiated && curState.playbackInitiated) {\n if (prevState.preload !== 'auto') return false; // fire — message shape changes\n // preload was 'auto': fall through to segment comparison (suppress if same position)\n }\n\n // Don't treat as a change if we don't (yet) have a currently resolved track.\n if (!curState.track || !isResolvedTrack(curState.track)) return true;\n\n // If we *do* have a currently resolved track, treat as a change if the track ids have changed\n if (prevState.track?.id !== curState.track.id && isResolvedTrack(curState.track)) return false;\n\n // Finally, if playback has initiated *and* we have a resolved track, check if currentTime\n // has changed \"significantly\" (based on segment time range boundaries) and treat as a change\n // if so (regardless of whether or not the track has changed)\n return (\n segmentStartFor(prevState.currentTime, curState.track as ResolvedTrack) ===\n segmentStartFor(curState.currentTime, curState.track as ResolvedTrack)\n );\n}\n\n// ============================================================================\n// REACTOR\n// ============================================================================\n\n/**\n * Load segments orchestration — Reactor layer.\n *\n * Sends typed load messages to a SegmentLoaderActor when relevant conditions\n * change. Uses targeted subscriptions rather than broad combineLatest so only\n * meaningful state changes trigger evaluation.\n *\n * Condition hierarchy (see SegmentLoadingKey for detail):\n *\n * !playbackInitiated\n * preload==='none' (or unset) → dormant; no trigger\n * preload==='metadata' → trigger on transition to 'metadata'\n * preload==='auto' → trigger on transition to 'auto'\n *\n * !playbackInitiated → playbackInitiated\n * preload !== 'auto' → trigger (message shape changes)\n * preload === 'auto' → suppressed (was already full-range mode;\n * let segmentStart take over post-play)\n * KNOWN LIMITATION: seek-before-play with\n * preload='auto' is not supported — if the\n * user seeks before pressing play, the\n * first re-send is delayed until the next\n * segment boundary crossing post-play.\n *\n * playbackInitiated\n * resolvedTrackId changes → trigger\n * segmentStart(currentTime) changes → trigger (segment boundary only)\n *\n * @example\n * const cleanup = loadSegments({ state, owners }, { type: 'video' });\n */\nexport function loadSegments<S extends SegmentLoadingState, O extends SegmentLoadingOwners>(\n { state, owners }: { state: Signal<S>; owners: Signal<O> },\n config: { type: MediaTrackType }\n): () => void {\n const { type } = config;\n const actorKey = ActorKeyByType[type];\n\n // Local throughput signal — used by createTrackedFetch to sample bandwidth\n // per chunk and propagate back to the shared state for ABR.\n const initialBandwidth = state.get().bandwidthState;\n const throughput = signal<BandwidthState>(\n initialBandwidth ?? {\n fastEstimate: 0,\n fastTotalWeight: 0,\n slowEstimate: 0,\n slowTotalWeight: 0,\n bytesSampled: 0,\n }\n );\n\n const fetchBytes =\n type === 'video'\n ? createTrackedFetch(\n throughput,\n initialBandwidth !== undefined\n ? (next) => {\n state.set(Object.assign({}, state.get(), { bandwidthState: next }) as S);\n }\n : undefined\n )\n : fetchStream;\n\n // Local segment loader — signal so segmentsCanLoad can track it reactively\n const segmentLoader = signal<SegmentLoaderActor | undefined>(undefined);\n\n // Computed: isolates the specific actor key so the lifecycle effect only\n // re-runs when that actor reference changes, not on unrelated owners updates.\n const actorSource = computed<SourceBufferActor | undefined>(() => owners.get()[actorKey]);\n\n // Actor lifecycle — destroy and recreate SegmentLoaderActor when the\n // upstream SourceBufferActor is replaced (quality switch) or removed.\n let currentLoader: SegmentLoaderActor | undefined;\n const cleanupActorLifecycle = effect(() => {\n const actor = actorSource.get();\n\n if (currentLoader) {\n currentLoader.destroy();\n segmentLoader.set(undefined);\n currentLoader = undefined;\n }\n\n if (actor) {\n const loader = createSegmentLoaderActor(actor, fetchBytes);\n currentLoader = loader;\n segmentLoader.set(loader);\n }\n });\n\n // Derived: true only when we have both a resolved track and a ready loader\n const segmentsCanLoad = computed(() => {\n const track = getSelectedTrack(state.get(), type);\n return !!track && isResolvedTrack(track) && !!segmentLoader.get();\n });\n\n // Loading inputs — selectLoadingInputs is cheap; let the effect body apply\n // loadingInputsEq rather than the computed, so the first run always fires\n // regardless of the initial state.\n const loadingInputs = computed(() => selectLoadingInputs([segmentsCanLoad.get(), state.get()], type));\n\n // Load effect — prevInputs guards against redundant messages using the same\n // equality semantics as the former combineLatest selector subscription.\n // prevInputs is updated only when we actually send a message so that the\n // transition segmentsCanLoad: false → true always fires (loadingInputsEq\n // returns true for that transition, so we must not skip it here).\n let prevInputs: LoadingInputs | undefined;\n const cleanupLoadEffect = effect(() => {\n const inputs = loadingInputs.get();\n if (prevInputs !== undefined && loadingInputsEq(prevInputs, inputs)) return;\n\n const { preload, playbackInitiated, currentTime, track, segmentsCanLoad: canLoad } = inputs;\n if (!canLoad) return;\n\n prevInputs = inputs;\n\n const fullMode = preload === 'auto' || !!playbackInitiated;\n if (!fullMode) {\n // Metadata mode: init only, no range.\n /** @ts-expect-error */\n segmentLoader.get()?.send({ type: 'load', track });\n } else {\n segmentLoader.get()?.send({\n type: 'load',\n /** @ts-expect-error */\n track,\n range: {\n start: currentTime as number,\n end: (currentTime as number) + DEFAULT_FORWARD_BUFFER_CONFIG.bufferDuration,\n },\n });\n }\n });\n\n return () => {\n cleanupActorLifecycle();\n cleanupLoadEffect();\n currentLoader?.destroy();\n };\n}\n"],"mappings":";;;;;;;;;;AAsBA,MAAM,iBAAiB;CACrB,OAAO;CACP,OAAO;CACR;AAoBD,SAAS,mBACP,YACA,UACgG;AAChG,QAAO,OAAO,aAAa,YAAY;EACrC,MAAM,EAAE,cAAc,GAAG,iBAAiB,WAAW,EAAE;EACvD,MAAM,WAAW,MAAM,gBAAgB,aAAa,aAAa;AACjE,MAAI,CAAC,SAAS,KAAM,OAAM,IAAI,MAAM,uBAAuB;EAC3D,MAAM,OAAO,SAAS;AACtB,SAAO,EACL,CAAC,OAAO,gBAAgB,mBAAmB;GACzC,IAAI,aAAa,YAAY,KAAK;AAClC,cAAW,MAAM,SAAS,IAAI,sBAC5B,MACA,GAAI,iBAAiB,KAAA,IAAY,CAAC,EAAE,cAAc,CAAC,GAAG,EAAE,CACzD,EAAE;IACD,MAAM,UAAU,YAAY,KAAK,GAAG;IACpC,MAAM,OAAO,gBAAgB,WAAW,KAAK,EAAE,SAAS,MAAM,WAAW;AACzE,eAAW,IAAI,KAAK;AACpB,eAAW,KAAK;AAChB,UAAM;AACN,iBAAa,YAAY,KAAK;;KAGnC;;;;;;;;;AAUL,eAAe,YAAY,aAAgC,SAA4D;CACrH,MAAM,EAAE,cAAc,GAAG,iBAAiB,WAAW,EAAE;CACvD,MAAM,WAAW,MAAM,gBAAgB,aAAa,aAAa;AACjE,KAAI,CAAC,SAAS,KAAM,OAAM,IAAI,MAAM,uBAAuB;AAC3D,QAAO,IAAI,sBAAsB,SAAS,MAAM,GAAI,iBAAiB,KAAA,IAAY,CAAC,EAAE,cAAc,CAAC,GAAG,EAAE,CAAE;;AA+C5G,SAAS,oBACP,CAAC,iBAAiB,QAClB,MACe;CACf,MAAM,EAAE,mBAAmB,SAAS,gBAAgB;AAEpD,QAAO;EACL;EACA;EACA;EACA,OALY,iBAAiB,OAAO,KAAK;EAMzC;EACD;;;;;;;;;;;;;;;;;;AAmBH,MAAM,mBAAmB,aAAiC,UAAqC;AAC7F,KAAI,eAAe,KAAM,QAAO,KAAA;AAChC,QAAO,OAAO,SAAS,MACpB,EAAE,WAAW,YAAY,GAAG,aAC3B,eAAe,cAAc,cAAc,YAAY,YAAY,MAAM,SAAS,SAAS,GAC9F,EAAE;;;;;;;;AASL,SAAS,gBAAgB,WAA0B,UAAkC;AACnF,KAAI,CAAC,SAAS,gBAAiB,QAAO;AAEtC,KAAI,CAAC,SAAS,mBAAmB;AAC/B,MAAI,SAAS,YAAY,OAAQ,QAAO;AACxC,SAAO,SAAS,YAAY,UAAU;;AAGxC,KAAI,CAAC,UAAU,qBAAqB,SAAS;MACvC,UAAU,YAAY,OAAQ,QAAO;;AAK3C,KAAI,CAAC,SAAS,SAAS,CAAC,gBAAgB,SAAS,MAAM,CAAE,QAAO;AAGhE,KAAI,UAAU,OAAO,OAAO,SAAS,MAAM,MAAM,gBAAgB,SAAS,MAAM,CAAE,QAAO;AAKzF,QACE,gBAAgB,UAAU,aAAa,SAAS,MAAuB,KACvE,gBAAgB,SAAS,aAAa,SAAS,MAAuB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuC1E,SAAgB,aACd,EAAE,OAAO,UACT,QACY;CACZ,MAAM,EAAE,SAAS;CACjB,MAAM,WAAW,eAAe;CAIhC,MAAM,mBAAmB,MAAM,KAAK,CAAC;CACrC,MAAM,aAAa,OACjB,oBAAoB;EAClB,cAAc;EACd,iBAAiB;EACjB,cAAc;EACd,iBAAiB;EACjB,cAAc;EACf,CACF;CAED,MAAM,aACJ,SAAS,UACL,mBACE,YACA,qBAAqB,KAAA,KAChB,SAAS;AACR,QAAM,IAAI,OAAO,OAAO,EAAE,EAAE,MAAM,KAAK,EAAE,EAAE,gBAAgB,MAAM,CAAC,CAAM;KAE1E,KAAA,EACL,GACD;CAGN,MAAM,gBAAgB,OAAuC,KAAA,EAAU;CAIvE,MAAM,cAAc,eAA8C,OAAO,KAAK,CAAC,UAAU;CAIzF,IAAI;CACJ,MAAM,wBAAwB,aAAa;EACzC,MAAM,QAAQ,YAAY,KAAK;AAE/B,MAAI,eAAe;AACjB,iBAAc,SAAS;AACvB,iBAAc,IAAI,KAAA,EAAU;AAC5B,mBAAgB,KAAA;;AAGlB,MAAI,OAAO;GACT,MAAM,SAAS,yBAAyB,OAAO,WAAW;AAC1D,mBAAgB;AAChB,iBAAc,IAAI,OAAO;;GAE3B;CAGF,MAAM,kBAAkB,eAAe;EACrC,MAAM,QAAQ,iBAAiB,MAAM,KAAK,EAAE,KAAK;AACjD,SAAO,CAAC,CAAC,SAAS,gBAAgB,MAAM,IAAI,CAAC,CAAC,cAAc,KAAK;GACjE;CAKF,MAAM,gBAAgB,eAAe,oBAAoB,CAAC,gBAAgB,KAAK,EAAE,MAAM,KAAK,CAAC,EAAE,KAAK,CAAC;CAOrG,IAAI;CACJ,MAAM,oBAAoB,aAAa;EACrC,MAAM,SAAS,cAAc,KAAK;AAClC,MAAI,eAAe,KAAA,KAAa,gBAAgB,YAAY,OAAO,CAAE;EAErE,MAAM,EAAE,SAAS,mBAAmB,aAAa,OAAO,iBAAiB,YAAY;AACrF,MAAI,CAAC,QAAS;AAEd,eAAa;AAGb,MAAI,EADa,YAAY,UAAU,CAAC,CAAC;;AAIvC,gBAAc,KAAK,EAAE,KAAK;GAAE,MAAM;GAAQ;GAAO,CAAC;MAElD,eAAc,KAAK,EAAE,KAAK;GACxB,MAAM;GAEN;GACA,OAAO;IACL,OAAO;IACP,KAAM,cAAyB,8BAA8B;IAC9D;GACF,CAAC;GAEJ;AAEF,cAAa;AACX,yBAAuB;AACvB,qBAAmB;AACnB,iBAAe,SAAS"}
@@ -1,4 +1,4 @@
1
- import { WritableState } from "../../core/state/create-state.js";
1
+ import { Signal } from "../../core/signals/primitives.js";
2
2
  import { Presentation } from "../../core/types/index.js";
3
3
 
4
4
  //#region src/dom/features/load-text-track-cues.d.ts
@@ -49,12 +49,12 @@ interface TextTrackCueLoadingOwners {
49
49
  * @example
50
50
  * const cleanup = loadTextTrackCues({ state, owners });
51
51
  */
52
- declare function loadTextTrackCues({
52
+ declare function loadTextTrackCues<S extends TextTrackCueLoadingState, O extends TextTrackCueLoadingOwners>({
53
53
  state,
54
54
  owners
55
55
  }: {
56
- state: WritableState<TextTrackCueLoadingState>;
57
- owners: WritableState<TextTrackCueLoadingOwners>;
56
+ state: Signal<S>;
57
+ owners: Signal<O>;
58
58
  }): () => void;
59
59
  //#endregion
60
60
  export { TextTrackBufferState, loadTextTrackCues };
@@ -1 +1 @@
1
- {"version":3,"file":"load-text-track-cues.d.ts","names":[],"sources":["../../../../src/dom/features/load-text-track-cues.ts"],"mappings":";;;;;;AA8GA;UAAiB,qBAAA;EACf,QAAA,EAAU,KAAA;IAAQ,EAAA;EAAA;AAAA;;;;AASpB;;;KAAY,oBAAA,GAAuB,MAAA,SAAe,qBAAA;;AAKlD;;UAAiB,wBAAA;EACf,mBAAA;EACA,YAAA,GAAe,YAAA;EAAf;EAEA,WAAA;EAAA;EAEA,eAAA,GAAkB,oBAAA;AAAA;;;AAMpB;UAAiB,yBAAA;EACf,UAAA,GAAa,GAAA,SAAY,gBAAA;AAAA;;;;;;;;;;;;;;;;iBAoGX,iBAAA,CAAA;EACd,KAAA;EACA;AAAA;EAEA,KAAA,EAAO,aAAA,CAAc,wBAAA;EACrB,MAAA,EAAQ,aAAA,CAAc,yBAAA;AAAA"}
1
+ {"version":3,"file":"load-text-track-cues.d.ts","names":[],"sources":["../../../../src/dom/features/load-text-track-cues.ts"],"mappings":";;;;;;AA0GA;UAAiB,qBAAA;EACf,QAAA,EAAU,KAAA;IAAQ,EAAA;EAAA;AAAA;;;;AASpB;;;KAAY,oBAAA,GAAuB,MAAA,SAAe,qBAAA;;AAKlD;;UAAiB,wBAAA;EACf,mBAAA;EACA,YAAA,GAAe,YAAA;EAAf;EAEA,WAAA;EAAA;EAEA,eAAA,GAAkB,oBAAA;AAAA;;;AAMpB;UAAiB,yBAAA;EACf,UAAA,GAAa,GAAA,SAAY,gBAAA;AAAA;;;;;;;;;;;;;;;;iBAoGX,iBAAA,WAA4B,wBAAA,YAAoC,yBAAA,CAAA,CAAA;EAC9E,KAAA;EACA;AAAA;EAEA,KAAA,EAAO,MAAA,CAAO,CAAA;EACd,MAAA,EAAQ,MAAA,CAAO,CAAA;AAAA"}
@@ -1,21 +1,24 @@
1
1
  import { getSegmentsToLoad } from "../../core/buffer/forward-buffer.js";
2
+ import { effect } from "../../core/signals/effect.js";
3
+ import { computed } from "../../core/signals/primitives.js";
2
4
  import { isResolvedTrack } from "../../core/types/index.js";
3
- import { combineLatest } from "../../core/reactive/combine-latest.js";
4
5
  import { parseVttSegment } from "../text/parse-vtt-segment.js";
5
-
6
6
  //#region src/dom/features/load-text-track-cues.ts
7
- function isDuplicateCue(cue, textTrack) {
8
- const { cues } = textTrack;
9
- if (!cues) return false;
10
- for (let i = 0; i < cues.length; i++) {
11
- const existing = cues[i];
12
- if (existing.startTime === cue.startTime && existing.endTime === cue.endTime && existing.text === cue.text) return true;
13
- }
14
- return false;
7
+ const CueKeys = [
8
+ "startTime",
9
+ "endTime",
10
+ "text"
11
+ ];
12
+ function isDuplicateCue(cue, existingCues) {
13
+ return Array.prototype.some.call(existingCues ?? [], (existingCue) => {
14
+ return CueKeys.every((k) => existingCue[k] === cue[k]);
15
+ });
15
16
  }
16
- const loadVttSegmentTask = async ({ segment }, context) => {
17
- const cues = await parseVttSegment(segment.url);
18
- for (const cue of cues) if (!isDuplicateCue(cue, context.textTrack)) context.textTrack.addCue(cue);
17
+ const loadVttSegmentTask = async ({ segment }, { textTrack }) => {
18
+ (await parseVttSegment(segment.url)).forEach((cue) => {
19
+ if (isDuplicateCue(cue, textTrack.cues)) return;
20
+ textTrack.addCue(cue);
21
+ });
19
22
  };
20
23
  /**
21
24
  * Load text track cues task (composite - orchestrates VTT segment subtasks).
@@ -33,12 +36,16 @@ const loadTextTrackCuesTask = async ({ currentState }, context) => {
33
36
  if (context.signal.aborted) break;
34
37
  try {
35
38
  await loadVttSegmentTask({ segment }, { textTrack: context.textTrack });
36
- const latest = context.state.current.textBufferState ?? {};
39
+ const latestState = context.state.get();
40
+ const latest = latestState.textBufferState ?? {};
37
41
  const trackState = latest[trackId] ?? { segments: [] };
38
- context.state.patch({ textBufferState: {
39
- ...latest,
40
- [trackId]: { segments: [...trackState.segments, { id: segment.id }] }
41
- } });
42
+ context.state.set({
43
+ ...latestState,
44
+ textBufferState: {
45
+ ...latest,
46
+ [trackId]: { segments: [...trackState.segments, { id: segment.id }] }
47
+ }
48
+ });
42
49
  } catch (error) {
43
50
  if (error instanceof Error && error.name === "AbortError") break;
44
51
  console.error("Failed to load VTT segment:", error);
@@ -121,18 +128,21 @@ function loadTextTrackCues({ state, owners }) {
121
128
  let currentTask = null;
122
129
  let abortController = null;
123
130
  let lastTrackId;
124
- const cleanup = combineLatest([state, owners]).subscribe(async ([currentState, currentOwners]) => {
125
- if (currentState.selectedTextTrackId !== lastTrackId) {
126
- lastTrackId = currentState.selectedTextTrackId;
131
+ const selectedTrackId = computed(() => state.get().selectedTextTrackId);
132
+ const cleanupEffect = effect(() => {
133
+ const s = state.get();
134
+ const o = owners.get();
135
+ if (selectedTrackId.get() !== lastTrackId) {
136
+ lastTrackId = selectedTrackId.get();
127
137
  abortController?.abort();
128
138
  currentTask = null;
129
139
  }
130
140
  if (currentTask) return;
131
- if (!shouldLoadTextTrackCues(currentState, currentOwners)) return;
132
- const textTrack = getSelectedTextTrackFromOwners(currentState, currentOwners);
141
+ if (!shouldLoadTextTrackCues(s, o)) return;
142
+ const textTrack = getSelectedTextTrackFromOwners(s, o);
133
143
  if (!textTrack) return;
134
144
  abortController = new AbortController();
135
- currentTask = loadTextTrackCuesTask({ currentState }, {
145
+ currentTask = loadTextTrackCuesTask({ currentState: s }, {
136
146
  signal: abortController.signal,
137
147
  textTrack,
138
148
  state
@@ -142,10 +152,10 @@ function loadTextTrackCues({ state, owners }) {
142
152
  });
143
153
  return () => {
144
154
  abortController?.abort();
145
- cleanup();
155
+ cleanupEffect();
146
156
  };
147
157
  }
148
-
149
158
  //#endregion
150
159
  export { loadTextTrackCues };
160
+
151
161
  //# sourceMappingURL=load-text-track-cues.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"load-text-track-cues.js","names":[],"sources":["../../../../src/dom/features/load-text-track-cues.ts"],"sourcesContent":["import { getSegmentsToLoad } from '../../core/buffer/forward-buffer';\nimport { combineLatest } from '../../core/reactive/combine-latest';\nimport type { WritableState } from '../../core/state/create-state';\nimport type { Presentation, Segment, TextTrack } from '../../core/types';\nimport { isResolvedTrack } from '../../core/types';\nimport { parseVttSegment } from '../text/parse-vtt-segment';\n\nfunction isDuplicateCue(cue: VTTCue, textTrack: globalThis.TextTrack): boolean {\n const { cues } = textTrack;\n if (!cues) return false;\n for (let i = 0; i < cues.length; i++) {\n const existing = cues[i] as VTTCue;\n if (existing.startTime === cue.startTime && existing.endTime === cue.endTime && existing.text === cue.text) {\n return true;\n }\n }\n return false;\n}\n\nconst loadVttSegmentTask = async (\n { segment }: { segment: Segment },\n context: { textTrack: globalThis.TextTrack }\n): Promise<void> => {\n const cues = await parseVttSegment(segment.url);\n for (const cue of cues) {\n if (!isDuplicateCue(cue, context.textTrack)) {\n context.textTrack.addCue(cue);\n }\n }\n};\n\n// ============================================================================\n// MAIN TASK (composite - orchestrates subtasks)\n// ============================================================================\n\n/**\n * Load text track cues task (composite - orchestrates VTT segment subtasks).\n */\nconst loadTextTrackCuesTask = async (\n { currentState }: { currentState: TextTrackCueLoadingState },\n context: {\n signal: AbortSignal;\n textTrack: globalThis.TextTrack;\n state: WritableState<TextTrackCueLoadingState>;\n }\n): Promise<void> => {\n const track = findSelectedTextTrack(currentState);\n if (!track || !isResolvedTrack(track)) return;\n\n const { segments } = track;\n if (segments.length === 0) return;\n\n const trackId = track.id;\n\n // Resolve segments already recorded in the state model for this track.\n // Keyed by track ID so multiple text tracks don't interfere with each other.\n const loadedIds = new Set((currentState.textBufferState?.[trackId]?.segments ?? []).map((s) => s.id));\n const alreadyLoaded = segments.filter((s) => loadedIds.has(s.id));\n\n // Apply the same forward buffer window as audio/video segment loading.\n const currentTime = currentState.currentTime ?? 0;\n const segmentsToLoad = getSegmentsToLoad(segments, alreadyLoaded, currentTime).filter((s) => !loadedIds.has(s.id));\n\n if (segmentsToLoad.length === 0) return;\n\n // Execute subtasks sequentially, recording each loaded segment in state.\n for (const segment of segmentsToLoad) {\n if (context.signal.aborted) break;\n\n try {\n await loadVttSegmentTask({ segment }, { textTrack: context.textTrack });\n\n // Record the loaded segment in shared state — mirrors bufferState for\n // audio/video and supports N text tracks keyed by track ID.\n const latest = context.state.current.textBufferState ?? {};\n const trackState = latest[trackId] ?? { segments: [] };\n context.state.patch({\n textBufferState: {\n ...latest,\n [trackId]: { segments: [...trackState.segments, { id: segment.id }] },\n },\n });\n } catch (error) {\n if (error instanceof Error && error.name === 'AbortError') break;\n console.error('Failed to load VTT segment:', error);\n // Continue to next segment (graceful degradation)\n }\n }\n\n // Chrome bug: after a track goes through mode='disabled' (which clears cues) and back\n // to 'showing', cues added to the track aren't activated. Re-adding all cues forces\n // Chrome to re-process them. Safe no-op in other browsers.\n // Mirrors the workaround in HlsMediaTextTracksMixin (packages/core/src/dom/media/hls/text-tracks.ts).\n if (context.textTrack.mode === 'showing' && context.textTrack.cues) {\n Array.from(context.textTrack.cues).forEach((cue) => {\n context.textTrack.addCue(cue);\n });\n }\n\n // Wait a frame before completing to allow state updates to flush\n await new Promise((resolve) => requestAnimationFrame(resolve));\n};\n\n// ============================================================================\n// STATE & OWNERS\n// ============================================================================\n\n/**\n * Loaded-segment record for a single text track.\n */\nexport interface TextTrackSegmentState {\n segments: Array<{ id: string }>;\n}\n\n/**\n * Buffer model for text track cues — keyed by track ID.\n *\n * Using a per-track-ID map (rather than fixed 'video'/'audio' keys) because\n * there can be N text tracks — one per language/subtitle variant.\n */\nexport type TextTrackBufferState = Record<string, TextTrackSegmentState>;\n\n/**\n * State shape for text track cue loading.\n */\nexport interface TextTrackCueLoadingState {\n selectedTextTrackId?: string;\n presentation?: Presentation;\n /** Current playback position — used to gate VTT segment fetching to the forward buffer window. */\n currentTime?: number;\n /** Loaded-segment model for text tracks, keyed by track ID. */\n textBufferState?: TextTrackBufferState;\n}\n\n/**\n * Owners shape for text track cue loading.\n */\nexport interface TextTrackCueLoadingOwners {\n textTracks?: Map<string, HTMLTrackElement>;\n}\n\n/**\n * Find the selected text track in the presentation.\n */\nfunction findSelectedTextTrack(state: TextTrackCueLoadingState): TextTrack | undefined {\n if (!state.presentation || !state.selectedTextTrackId) {\n return undefined;\n }\n\n const textSet = state.presentation.selectionSets.find((set) => set.type === 'text');\n if (!textSet?.switchingSets?.[0]?.tracks) {\n return undefined;\n }\n\n const track = textSet.switchingSets[0].tracks.find((t) => t.id === state.selectedTextTrackId);\n\n return track as TextTrack | undefined;\n}\n\n/**\n * Get the browser's TextTrack object for the selected text track.\n *\n * Retrieves the live TextTrack interface from the track element in owners,\n * which is used for adding cues, checking mode, and managing track state.\n *\n * Note: Returns the DOM TextTrack interface (HTMLTrackElement.track),\n * not the presentation Track metadata type.\n *\n * @param state - Current playback state (track selection)\n * @param owners - DOM owners containing track elements map\n * @returns DOM TextTrack interface or undefined if not found\n */\nfunction getSelectedTextTrackFromOwners(\n state: TextTrackCueLoadingState,\n owners: TextTrackCueLoadingOwners\n): globalThis.TextTrack | undefined {\n const trackId = state.selectedTextTrackId;\n if (!trackId || !owners.textTracks) {\n return undefined;\n }\n\n const trackElement = owners.textTracks.get(trackId);\n return trackElement?.track;\n}\n\n/**\n * Check if we can load text track cues.\n *\n * Requires:\n * - Selected text track ID exists\n * - Track elements map exists\n * - Track element exists for selected track\n */\nexport function canLoadTextTrackCues(state: TextTrackCueLoadingState, owners: TextTrackCueLoadingOwners): boolean {\n return !!state.selectedTextTrackId && !!owners.textTracks && owners.textTracks.has(state.selectedTextTrackId);\n}\n\n/**\n * Check if we should load text track cues.\n *\n * Only load if:\n * - Track is resolved (has segments)\n * - Track has at least one segment\n * - Track element exists\n */\nexport function shouldLoadTextTrackCues(state: TextTrackCueLoadingState, owners: TextTrackCueLoadingOwners): boolean {\n if (!canLoadTextTrackCues(state, owners)) {\n return false;\n }\n\n const track = findSelectedTextTrack(state);\n if (!track || !isResolvedTrack(track) || track.segments.length === 0) {\n return false;\n }\n\n const textTrack = getSelectedTextTrackFromOwners(state, owners);\n if (!textTrack) {\n return false;\n }\n\n return true;\n}\n\n/**\n * Load text track cues orchestration.\n *\n * Triggers when:\n * - Text track is selected\n * - Track is resolved (has segments)\n * - Track element exists\n *\n * Fetches and parses VTT segments within the forward buffer window, then adds\n * cues to the track incrementally. Continues on segment errors to provide\n * partial subtitles.\n *\n * @example\n * const cleanup = loadTextTrackCues({ state, owners });\n */\nexport function loadTextTrackCues({\n state,\n owners,\n}: {\n state: WritableState<TextTrackCueLoadingState>;\n owners: WritableState<TextTrackCueLoadingOwners>;\n}): () => void {\n let currentTask: Promise<void> | null = null;\n let abortController: AbortController | null = null;\n let lastTrackId: string | undefined;\n\n const cleanup = combineLatest([state, owners]).subscribe(\n async ([currentState, currentOwners]: [TextTrackCueLoadingState, TextTrackCueLoadingOwners]) => {\n // Abort any in-progress task when the selected track changes.\n // The new track's textBufferState entry will be empty, so the task\n // naturally starts fresh without needing any explicit reset.\n if (currentState.selectedTextTrackId !== lastTrackId) {\n lastTrackId = currentState.selectedTextTrackId;\n abortController?.abort();\n currentTask = null;\n }\n\n if (currentTask) return; // Task already in progress\n if (!shouldLoadTextTrackCues(currentState, currentOwners)) return;\n\n const textTrack = getSelectedTextTrackFromOwners(currentState, currentOwners);\n if (!textTrack) return;\n\n // Create abort controller and invoke task\n abortController = new AbortController();\n currentTask = loadTextTrackCuesTask(\n { currentState },\n { signal: abortController.signal, textTrack, state }\n ).finally(() => {\n currentTask = null;\n });\n }\n );\n\n return () => {\n abortController?.abort();\n cleanup();\n };\n}\n"],"mappings":";;;;;;AAOA,SAAS,eAAe,KAAa,WAA0C;CAC7E,MAAM,EAAE,SAAS;AACjB,KAAI,CAAC,KAAM,QAAO;AAClB,MAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;EACpC,MAAM,WAAW,KAAK;AACtB,MAAI,SAAS,cAAc,IAAI,aAAa,SAAS,YAAY,IAAI,WAAW,SAAS,SAAS,IAAI,KACpG,QAAO;;AAGX,QAAO;;AAGT,MAAM,qBAAqB,OACzB,EAAE,WACF,YACkB;CAClB,MAAM,OAAO,MAAM,gBAAgB,QAAQ,IAAI;AAC/C,MAAK,MAAM,OAAO,KAChB,KAAI,CAAC,eAAe,KAAK,QAAQ,UAAU,CACzC,SAAQ,UAAU,OAAO,IAAI;;;;;AAYnC,MAAM,wBAAwB,OAC5B,EAAE,gBACF,YAKkB;CAClB,MAAM,QAAQ,sBAAsB,aAAa;AACjD,KAAI,CAAC,SAAS,CAAC,gBAAgB,MAAM,CAAE;CAEvC,MAAM,EAAE,aAAa;AACrB,KAAI,SAAS,WAAW,EAAG;CAE3B,MAAM,UAAU,MAAM;CAItB,MAAM,YAAY,IAAI,KAAK,aAAa,kBAAkB,UAAU,YAAY,EAAE,EAAE,KAAK,MAAM,EAAE,GAAG,CAAC;CAKrG,MAAM,iBAAiB,kBAAkB,UAJnB,SAAS,QAAQ,MAAM,UAAU,IAAI,EAAE,GAAG,CAAC,EAG7C,aAAa,eAAe,EAC8B,CAAC,QAAQ,MAAM,CAAC,UAAU,IAAI,EAAE,GAAG,CAAC;AAElH,KAAI,eAAe,WAAW,EAAG;AAGjC,MAAK,MAAM,WAAW,gBAAgB;AACpC,MAAI,QAAQ,OAAO,QAAS;AAE5B,MAAI;AACF,SAAM,mBAAmB,EAAE,SAAS,EAAE,EAAE,WAAW,QAAQ,WAAW,CAAC;GAIvE,MAAM,SAAS,QAAQ,MAAM,QAAQ,mBAAmB,EAAE;GAC1D,MAAM,aAAa,OAAO,YAAY,EAAE,UAAU,EAAE,EAAE;AACtD,WAAQ,MAAM,MAAM,EAClB,iBAAiB;IACf,GAAG;KACF,UAAU,EAAE,UAAU,CAAC,GAAG,WAAW,UAAU,EAAE,IAAI,QAAQ,IAAI,CAAC,EAAE;IACtE,EACF,CAAC;WACK,OAAO;AACd,OAAI,iBAAiB,SAAS,MAAM,SAAS,aAAc;AAC3D,WAAQ,MAAM,+BAA+B,MAAM;;;AASvD,KAAI,QAAQ,UAAU,SAAS,aAAa,QAAQ,UAAU,KAC5D,OAAM,KAAK,QAAQ,UAAU,KAAK,CAAC,SAAS,QAAQ;AAClD,UAAQ,UAAU,OAAO,IAAI;GAC7B;AAIJ,OAAM,IAAI,SAAS,YAAY,sBAAsB,QAAQ,CAAC;;;;;AA4ChE,SAAS,sBAAsB,OAAwD;AACrF,KAAI,CAAC,MAAM,gBAAgB,CAAC,MAAM,oBAChC;CAGF,MAAM,UAAU,MAAM,aAAa,cAAc,MAAM,QAAQ,IAAI,SAAS,OAAO;AACnF,KAAI,CAAC,SAAS,gBAAgB,IAAI,OAChC;AAKF,QAFc,QAAQ,cAAc,GAAG,OAAO,MAAM,MAAM,EAAE,OAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;AAkB/F,SAAS,+BACP,OACA,QACkC;CAClC,MAAM,UAAU,MAAM;AACtB,KAAI,CAAC,WAAW,CAAC,OAAO,WACtB;AAIF,QADqB,OAAO,WAAW,IAAI,QAAQ,EAC9B;;;;;;;;;;AAWvB,SAAgB,qBAAqB,OAAiC,QAA4C;AAChH,QAAO,CAAC,CAAC,MAAM,uBAAuB,CAAC,CAAC,OAAO,cAAc,OAAO,WAAW,IAAI,MAAM,oBAAoB;;;;;;;;;;AAW/G,SAAgB,wBAAwB,OAAiC,QAA4C;AACnH,KAAI,CAAC,qBAAqB,OAAO,OAAO,CACtC,QAAO;CAGT,MAAM,QAAQ,sBAAsB,MAAM;AAC1C,KAAI,CAAC,SAAS,CAAC,gBAAgB,MAAM,IAAI,MAAM,SAAS,WAAW,EACjE,QAAO;AAIT,KAAI,CADc,+BAA+B,OAAO,OAAO,CAE7D,QAAO;AAGT,QAAO;;;;;;;;;;;;;;;;;AAkBT,SAAgB,kBAAkB,EAChC,OACA,UAIa;CACb,IAAI,cAAoC;CACxC,IAAI,kBAA0C;CAC9C,IAAI;CAEJ,MAAM,UAAU,cAAc,CAAC,OAAO,OAAO,CAAC,CAAC,UAC7C,OAAO,CAAC,cAAc,mBAA0E;AAI9F,MAAI,aAAa,wBAAwB,aAAa;AACpD,iBAAc,aAAa;AAC3B,oBAAiB,OAAO;AACxB,iBAAc;;AAGhB,MAAI,YAAa;AACjB,MAAI,CAAC,wBAAwB,cAAc,cAAc,CAAE;EAE3D,MAAM,YAAY,+BAA+B,cAAc,cAAc;AAC7E,MAAI,CAAC,UAAW;AAGhB,oBAAkB,IAAI,iBAAiB;AACvC,gBAAc,sBACZ,EAAE,cAAc,EAChB;GAAE,QAAQ,gBAAgB;GAAQ;GAAW;GAAO,CACrD,CAAC,cAAc;AACd,iBAAc;IACd;GAEL;AAED,cAAa;AACX,mBAAiB,OAAO;AACxB,WAAS"}
1
+ {"version":3,"file":"load-text-track-cues.js","names":[],"sources":["../../../../src/dom/features/load-text-track-cues.ts"],"sourcesContent":["import { getSegmentsToLoad } from '../../core/buffer/forward-buffer';\nimport { effect } from '../../core/signals/effect';\nimport { computed, type Signal } from '../../core/signals/primitives';\nimport type { Presentation, Segment, TextTrack } from '../../core/types';\nimport { isResolvedTrack } from '../../core/types';\nimport { parseVttSegment } from '../text/parse-vtt-segment';\n\nconst CueKeys = ['startTime', 'endTime', 'text'] as const;\nfunction isDuplicateCue(cue: VTTCue, existingCues: globalThis.TextTrack['cues']): boolean {\n return Array.prototype.some.call(existingCues ?? [], (existingCue) => {\n return CueKeys.every((k) => existingCue[k] === cue[k]);\n });\n}\n\nconst loadVttSegmentTask = async (\n { segment }: { segment: Segment },\n { textTrack }: { textTrack: globalThis.TextTrack }\n): Promise<void> => {\n const cues = await parseVttSegment(segment.url);\n cues.forEach((cue) => {\n if (isDuplicateCue(cue, textTrack.cues)) return;\n textTrack.addCue(cue);\n });\n};\n\n// ============================================================================\n// MAIN TASK (composite - orchestrates subtasks)\n// ============================================================================\n\n/**\n * Load text track cues task (composite - orchestrates VTT segment subtasks).\n */\nconst loadTextTrackCuesTask = async <S extends TextTrackCueLoadingState>(\n { currentState }: { currentState: S },\n context: {\n signal: AbortSignal;\n textTrack: globalThis.TextTrack;\n state: Signal<S>;\n }\n): Promise<void> => {\n const track = findSelectedTextTrack(currentState);\n if (!track || !isResolvedTrack(track)) return;\n\n const { segments } = track;\n if (segments.length === 0) return;\n\n const trackId = track.id;\n\n // Resolve segments already recorded in the state model for this track.\n // Keyed by track ID so multiple text tracks don't interfere with each other.\n const loadedIds = new Set((currentState.textBufferState?.[trackId]?.segments ?? []).map((s) => s.id));\n const alreadyLoaded = segments.filter((s) => loadedIds.has(s.id));\n\n // Apply the same forward buffer window as audio/video segment loading.\n const currentTime = currentState.currentTime ?? 0;\n const segmentsToLoad = getSegmentsToLoad(segments, alreadyLoaded, currentTime).filter((s) => !loadedIds.has(s.id));\n\n if (segmentsToLoad.length === 0) return;\n\n // Execute subtasks sequentially, recording each loaded segment in state.\n for (const segment of segmentsToLoad) {\n if (context.signal.aborted) break;\n\n try {\n await loadVttSegmentTask({ segment }, { textTrack: context.textTrack });\n\n // Record the loaded segment in shared state — mirrors bufferState for\n // audio/video and supports N text tracks keyed by track ID.\n const latestState = context.state.get();\n const latest = latestState.textBufferState ?? {};\n const trackState = latest[trackId] ?? { segments: [] };\n context.state.set({\n ...latestState,\n textBufferState: {\n ...latest,\n [trackId]: { segments: [...trackState.segments, { id: segment.id }] },\n },\n } as S);\n } catch (error) {\n if (error instanceof Error && error.name === 'AbortError') break;\n console.error('Failed to load VTT segment:', error);\n // Continue to next segment (graceful degradation)\n }\n }\n\n // Chrome bug: after a track goes through mode='disabled' (which clears cues) and back\n // to 'showing', cues added to the track aren't activated. Re-adding all cues forces\n // Chrome to re-process them. Safe no-op in other browsers.\n // Mirrors the workaround in HlsMediaTextTracksMixin (packages/core/src/dom/media/hls/text-tracks.ts).\n if (context.textTrack.mode === 'showing' && context.textTrack.cues) {\n Array.from(context.textTrack.cues).forEach((cue) => {\n context.textTrack.addCue(cue);\n });\n }\n\n // Wait a frame before completing to allow state updates to flush\n await new Promise((resolve) => requestAnimationFrame(resolve));\n};\n\n// ============================================================================\n// STATE & OWNERS\n// ============================================================================\n\n/**\n * Loaded-segment record for a single text track.\n */\nexport interface TextTrackSegmentState {\n segments: Array<{ id: string }>;\n}\n\n/**\n * Buffer model for text track cues — keyed by track ID.\n *\n * Using a per-track-ID map (rather than fixed 'video'/'audio' keys) because\n * there can be N text tracks — one per language/subtitle variant.\n */\nexport type TextTrackBufferState = Record<string, TextTrackSegmentState>;\n\n/**\n * State shape for text track cue loading.\n */\nexport interface TextTrackCueLoadingState {\n selectedTextTrackId?: string;\n presentation?: Presentation;\n /** Current playback position — used to gate VTT segment fetching to the forward buffer window. */\n currentTime?: number;\n /** Loaded-segment model for text tracks, keyed by track ID. */\n textBufferState?: TextTrackBufferState;\n}\n\n/**\n * Owners shape for text track cue loading.\n */\nexport interface TextTrackCueLoadingOwners {\n textTracks?: Map<string, HTMLTrackElement>;\n}\n\n/**\n * Find the selected text track in the presentation.\n */\nfunction findSelectedTextTrack(state: TextTrackCueLoadingState): TextTrack | undefined {\n if (!state.presentation || !state.selectedTextTrackId) {\n return undefined;\n }\n\n const textSet = state.presentation.selectionSets.find((set) => set.type === 'text');\n if (!textSet?.switchingSets?.[0]?.tracks) {\n return undefined;\n }\n\n const track = textSet.switchingSets[0].tracks.find((t) => t.id === state.selectedTextTrackId);\n\n return track as TextTrack | undefined;\n}\n\n/**\n * Get the browser's TextTrack object for the selected text track.\n *\n * Retrieves the live TextTrack interface from the track element in owners,\n * which is used for adding cues, checking mode, and managing track state.\n *\n * Note: Returns the DOM TextTrack interface (HTMLTrackElement.track),\n * not the presentation Track metadata type.\n *\n * @param state - Current playback state (track selection)\n * @param owners - DOM owners containing track elements map\n * @returns DOM TextTrack interface or undefined if not found\n */\nfunction getSelectedTextTrackFromOwners(\n state: TextTrackCueLoadingState,\n owners: TextTrackCueLoadingOwners\n): globalThis.TextTrack | undefined {\n const trackId = state.selectedTextTrackId;\n if (!trackId || !owners.textTracks) {\n return undefined;\n }\n\n const trackElement = owners.textTracks.get(trackId);\n return trackElement?.track;\n}\n\n/**\n * Check if we can load text track cues.\n *\n * Requires:\n * - Selected text track ID exists\n * - Track elements map exists\n * - Track element exists for selected track\n */\nexport function canLoadTextTrackCues(state: TextTrackCueLoadingState, owners: TextTrackCueLoadingOwners): boolean {\n return !!state.selectedTextTrackId && !!owners.textTracks && owners.textTracks.has(state.selectedTextTrackId);\n}\n\n/**\n * Check if we should load text track cues.\n *\n * Only load if:\n * - Track is resolved (has segments)\n * - Track has at least one segment\n * - Track element exists\n */\nexport function shouldLoadTextTrackCues(state: TextTrackCueLoadingState, owners: TextTrackCueLoadingOwners): boolean {\n if (!canLoadTextTrackCues(state, owners)) {\n return false;\n }\n\n const track = findSelectedTextTrack(state);\n if (!track || !isResolvedTrack(track) || track.segments.length === 0) {\n return false;\n }\n\n const textTrack = getSelectedTextTrackFromOwners(state, owners);\n if (!textTrack) {\n return false;\n }\n\n return true;\n}\n\n/**\n * Load text track cues orchestration.\n *\n * Triggers when:\n * - Text track is selected\n * - Track is resolved (has segments)\n * - Track element exists\n *\n * Fetches and parses VTT segments within the forward buffer window, then adds\n * cues to the track incrementally. Continues on segment errors to provide\n * partial subtitles.\n *\n * @example\n * const cleanup = loadTextTrackCues({ state, owners });\n */\nexport function loadTextTrackCues<S extends TextTrackCueLoadingState, O extends TextTrackCueLoadingOwners>({\n state,\n owners,\n}: {\n state: Signal<S>;\n owners: Signal<O>;\n}): () => void {\n let currentTask: Promise<void> | null = null;\n let abortController: AbortController | null = null;\n let lastTrackId: string | undefined;\n\n const selectedTrackId = computed(() => state.get().selectedTextTrackId);\n\n const cleanupEffect = effect(() => {\n const s = state.get();\n const o = owners.get();\n\n // Abort any in-progress task when the selected track changes.\n // The new track's textBufferState entry will be empty, so the task\n // naturally starts fresh without needing any explicit reset.\n if (selectedTrackId.get() !== lastTrackId) {\n lastTrackId = selectedTrackId.get();\n abortController?.abort();\n currentTask = null;\n }\n\n if (currentTask) return;\n if (!shouldLoadTextTrackCues(s, o)) return;\n\n const textTrack = getSelectedTextTrackFromOwners(s, o);\n if (!textTrack) return;\n\n abortController = new AbortController();\n currentTask = loadTextTrackCuesTask(\n { currentState: s },\n { signal: abortController.signal, textTrack, state }\n ).finally(() => {\n currentTask = null;\n });\n });\n\n return () => {\n abortController?.abort();\n cleanupEffect();\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export { };
@@ -1,6 +1,5 @@
1
- import { calculateBackBufferFlushPoint } from "../../core/buffer/back-buffer.js";
2
1
  import { calculateForwardFlushPoint, getSegmentsToLoad } from "../../core/buffer/forward-buffer.js";
3
-
2
+ import { calculateBackBufferFlushPoint } from "../../core/buffer/back-buffer.js";
4
3
  //#region src/dom/features/segment-loader-actor.ts
5
4
  /**
6
5
  * Creates a SegmentLoaderActor for one track type (video or audio).
@@ -27,7 +26,7 @@ function createSegmentLoaderActor(sourceBufferActor, fetchBytes) {
27
26
  let running = false;
28
27
  let destroyed = false;
29
28
  const getBufferedSegments = (allSegments) => {
30
- const bufferedIds = new Set(sourceBufferActor.snapshot.context.segments.filter((s) => !s.partial).map((s) => s.id));
29
+ const bufferedIds = new Set(sourceBufferActor.snapshot.get().context.segments.filter((s) => !s.partial).map((s) => s.id));
31
30
  return allSegments.filter((s) => bufferedIds.has(s.id));
32
31
  };
33
32
  /**
@@ -47,7 +46,7 @@ function createSegmentLoaderActor(sourceBufferActor, fetchBytes) {
47
46
  */
48
47
  const planTasks = (message) => {
49
48
  const { track, range } = message;
50
- const actorCtx = sourceBufferActor.snapshot.context;
49
+ const actorCtx = sourceBufferActor.snapshot.get().context;
51
50
  const bufferedSegments = getBufferedSegments(track.segments);
52
51
  const currentTime = range?.start ?? 0;
53
52
  const tasks = [];
@@ -191,7 +190,7 @@ function createSegmentLoaderActor(sourceBufferActor, fetchBytes) {
191
190
  }
192
191
  };
193
192
  }
194
-
195
193
  //#endregion
196
194
  export { createSegmentLoaderActor };
195
+
197
196
  //# sourceMappingURL=segment-loader-actor.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"segment-loader-actor.js","names":[],"sources":["../../../../src/dom/features/segment-loader-actor.ts"],"sourcesContent":["import { calculateBackBufferFlushPoint } from '../../core/buffer/back-buffer';\nimport { calculateForwardFlushPoint, getSegmentsToLoad } from '../../core/buffer/forward-buffer';\nimport type { AddressableObject, AudioTrack, Segment, VideoTrack } from '../../core/types';\nimport type {\n AppendInitMessage,\n AppendSegmentMessage,\n RemoveMessage,\n SourceBufferActor,\n} from '../media/source-buffer-actor';\n\n// ============================================================================\n// BUFFER STATE TYPES\n// ============================================================================\n\n/**\n * Buffer state for a single SourceBuffer.\n * Tracks which init segment and media segments are loaded.\n */\nexport interface SourceBufferState {\n /** Track ID of the loaded init segment */\n initTrackId?: string;\n /** Loaded media segments (unordered - selectors derive ordering) */\n segments: Array<{ id: string; trackId: string }>;\n}\n\n/**\n * Buffer state for all SourceBuffers.\n */\nexport interface BufferState {\n video?: SourceBufferState;\n audio?: SourceBufferState;\n}\n\n// ============================================================================\n// MESSAGE PROTOCOL\n// ============================================================================\n\n/** Track types that have SourceBuffers (video and audio only). */\nexport type SegmentLoaderTrack = VideoTrack | AudioTrack;\n\n/**\n * Message sent to a SegmentLoaderActor.\n *\n * `range` is optional to distinguish loading modes:\n * - No range: load init segment only (metadata preload mode)\n * - With range: load init + all segments overlapping [start, end]\n *\n * `start` and `end` are raw time values — no segment snapping.\n * The actor maps them onto segment boundaries internally.\n */\nexport type SegmentLoaderMessage = {\n type: 'load';\n track: SegmentLoaderTrack;\n range?: { start: number; end: number };\n};\n\n// ============================================================================\n// LOAD TASK\n// ============================================================================\n\n/**\n * A LoadTask is the intent to perform one unit of SegmentLoader work.\n * Unlike SourceBufferMessage, fetch-based tasks carry a URL rather than\n * pre-fetched data — the runner fetches and appends them in sequence.\n *\n * Derived from SourceBufferMessage types by removing `data` and adding\n * a fetch URL via AddressableObject.\n *\n * @todo Rename — \"LoadTask\" risks confusion with the `Task` class used for\n * SourceBufferActor scheduling. These are closer to operation descriptors or\n * messages than tasks in that sense.\n */\nexport type LoadTask =\n | (Omit<AppendInitMessage, 'data'> & AddressableObject)\n | (Omit<AppendSegmentMessage, 'data'> & AddressableObject)\n | RemoveMessage;\n\n// ============================================================================\n// ACTOR INTERFACE\n// ============================================================================\n\nexport interface SegmentLoaderActor {\n send(message: SegmentLoaderMessage): void;\n destroy(): void;\n}\n\n// ============================================================================\n// IMPLEMENTATION\n// ============================================================================\n\n/**\n * Creates a SegmentLoaderActor for one track type (video or audio).\n *\n * Receives load assignments via `send()` and owns all execution: planning,\n * removes, fetches, and appends. Coordinates with the SourceBufferActor for\n * all physical SourceBuffer operations.\n *\n * Planning (Cases 1–3) happens in `send()` on every incoming message, producing\n * an ordered LoadTask list. The runner drains that list sequentially. When a new\n * message arrives mid-run, send() replans and either continues the in-flight\n * operation (if still needed) or preempts it.\n *\n * @param sourceBufferActor - Shared SourceBufferActor reference (not owned)\n * @param fetchBytes - Tracked fetch closure (owns throughput sampling for segments).\n * Accepts an optional `minChunkSize` in options; init segments pass `Infinity`\n * so the entire body accumulates as one chunk before appending.\n */\nexport function createSegmentLoaderActor(\n sourceBufferActor: SourceBufferActor,\n fetchBytes: (\n addressable: AddressableObject,\n options?: RequestInit & { minChunkSize?: number }\n ) => Promise<AsyncIterable<Uint8Array>>\n): SegmentLoaderActor {\n let pendingTasks: LoadTask[] | null = null;\n let inFlightInitTrackId: string | null = null;\n let inFlightSegmentId: string | null = null;\n let abortController: AbortController | null = null;\n let running = false;\n let destroyed = false;\n\n const getBufferedSegments = (allSegments: readonly Segment[]): Segment[] => {\n // Exclude partial segments — they are still being streamed and must not be\n // treated as fully buffered for load planning or buffer window calculations.\n const bufferedIds = new Set(sourceBufferActor.snapshot.context.segments.filter((s) => !s.partial).map((s) => s.id));\n return allSegments.filter((s) => bufferedIds.has(s.id));\n };\n\n /**\n * Translate a load message into an ordered LoadTask list based on committed\n * actor state. In-flight awareness is handled separately in send().\n *\n * @todo Rename alongside LoadTask (e.g. planOps).\n *\n * Case 1 — Removes: forward and back buffer flush points, segment-aligned.\n * No flush on track switch: appending new content overwrites existing buffer\n * ranges, and the actor's time-aligned deduplication keeps the segment model\n * accurate as new segments arrive.\n *\n * Case 2 — Init: schedule if not yet committed for this track.\n *\n * Case 3 — Segments: all segments in the load window not yet committed.\n */\n const planTasks = (message: SegmentLoaderMessage): LoadTask[] => {\n const { track, range } = message;\n const actorCtx = sourceBufferActor.snapshot.context;\n const bufferedSegments = getBufferedSegments(track.segments);\n const currentTime = range?.start ?? 0;\n const tasks: LoadTask[] = [];\n\n // Case 1: Removes\n if (range) {\n const forwardFlushStart = calculateForwardFlushPoint(bufferedSegments, currentTime);\n if (forwardFlushStart < Infinity) {\n tasks.push({ type: 'remove', start: forwardFlushStart, end: Infinity });\n }\n const backFlushEnd = calculateBackBufferFlushPoint(bufferedSegments, currentTime);\n if (backFlushEnd > 0) {\n tasks.push({ type: 'remove', start: 0, end: backFlushEnd });\n }\n }\n\n // Case 2: Init\n if (actorCtx.initTrackId !== track.id) {\n tasks.push({\n type: 'append-init',\n meta: { trackId: track.id },\n url: track.initialization.url,\n ...(track.initialization.byteRange !== undefined && { byteRange: track.initialization.byteRange }),\n });\n }\n\n // Case 3: Segments\n if (range) {\n const EPSILON = 0.0001;\n const segmentsToLoad = getSegmentsToLoad(track.segments, bufferedSegments, currentTime).filter((seg) => {\n // Quality-aware filter: skip segments already covered by equal-or-higher-quality\n // content in the actor context. Preserves buffered high-quality content during\n // ABR downgrades; loads during upgrades and for uncovered positions.\n const existing = actorCtx.segments.find((s) => Math.abs(s.startTime - seg.startTime) < EPSILON);\n // Partial segments are still streaming — treat as not buffered so they\n // are always re-planned (avoids relying on incomplete data).\n if (existing?.partial) return true;\n if (!existing?.trackBandwidth || !track.bandwidth) return true;\n return track.bandwidth > existing.trackBandwidth;\n });\n for (const segment of segmentsToLoad) {\n tasks.push({\n type: 'append-segment',\n meta: {\n id: segment.id,\n startTime: segment.startTime,\n duration: segment.duration,\n trackId: track.id,\n trackBandwidth: track.bandwidth,\n },\n url: segment.url,\n ...(segment.byteRange !== undefined && { byteRange: segment.byteRange }),\n });\n }\n }\n\n return tasks;\n };\n\n /**\n * Execute a single LoadTask: fetch (if needed) then forward to SourceBufferActor.\n * Sets/clears in-flight tracking around async operations so send() can make\n * accurate continue/preempt decisions at any point during execution.\n *\n * @todo Rename alongside LoadTask (e.g. executeOp).\n */\n const executeLoadTask = async (task: LoadTask): Promise<void> => {\n const signal = abortController!.signal;\n try {\n if (task.type === 'remove') {\n await sourceBufferActor.send(task, signal);\n return;\n }\n\n if (task.type === 'append-init') {\n inFlightInitTrackId = task.meta.trackId;\n if (!signal.aborted) {\n // Init segments are small and need the full body before the\n // same-track-seek vs track-switch commit decision can be made.\n // minChunkSize: Infinity causes ChunkedStreamIterable to accumulate\n // all chunks and yield exactly one — equivalent to arrayBuffer() but\n // through the same streaming path as media segments.\n const data = await fetchBytes(task, { signal, minChunkSize: Infinity });\n // For seeks on the same track: commit even if aborted — avoids re-fetching the\n // same init next time. For track switches: don't commit the old track's init;\n // the new track's init follows in pendingTasks.\n const isTrackSwitch = pendingTasks?.some(\n (t) => t.type === 'append-init' && t.meta.trackId !== task.meta.trackId\n );\n if (!signal.aborted || !isTrackSwitch) {\n const appendSignal = signal.aborted ? new AbortController().signal : signal;\n await sourceBufferActor.send({ type: 'append-init', data, meta: task.meta }, appendSignal);\n }\n }\n return;\n }\n\n // append-segment: await headers eagerly (starts the HTTP connection and\n // records the fetch in observers like tests), then pass the body stream\n // directly to the actor so chunks are appended as they arrive.\n inFlightSegmentId = task.meta.id;\n if (!signal.aborted) {\n const stream = await fetchBytes(task, { signal });\n if (!signal.aborted) {\n await sourceBufferActor.send({ type: 'append-segment', data: stream, meta: task.meta }, signal);\n }\n }\n } finally {\n inFlightInitTrackId = null;\n inFlightSegmentId = null;\n }\n };\n\n /**\n * Drain the scheduled task list sequentially.\n * After each task completes, checks for a pending replacement plan from send().\n * If the signal was aborted and no new plan arrived, stops immediately.\n */\n const runScheduled = async (initialTasks: LoadTask[]): Promise<void> => {\n running = true;\n abortController = new AbortController();\n let scheduled = initialTasks;\n\n while (scheduled.length > 0 && !destroyed) {\n const task = scheduled[0]!;\n scheduled = scheduled.slice(1);\n\n try {\n await executeLoadTask(task);\n } catch (error) {\n if (error instanceof Error && error.name === 'AbortError') {\n // Abort is handled in the post-task check below.\n } else {\n console.error('Unexpected error in segment loader:', error);\n // Non-abort error (e.g. network failure on init): don't continue with\n // remaining tasks in this sequence — they depend on the failed step.\n // A pending replacement plan (if any) will still be picked up below.\n scheduled = [];\n }\n }\n\n if (pendingTasks !== null) {\n // A new plan arrived while this task was running — switch to it.\n scheduled = pendingTasks;\n pendingTasks = null;\n abortController = new AbortController();\n } else if (abortController.signal.aborted) {\n // Aborted with no replacement plan — stop.\n break;\n }\n }\n\n abortController = null;\n running = false;\n };\n\n return {\n send(message: SegmentLoaderMessage) {\n if (destroyed) return;\n\n const allTasks = planTasks(message);\n\n if (!running) {\n if (allTasks.length === 0) return;\n runScheduled(allTasks);\n return;\n }\n\n // Determine whether the in-flight operation is still needed for the new plan.\n const inFlightStillNeeded =\n (inFlightSegmentId !== null &&\n allTasks.some((t) => t.type === 'append-segment' && t.meta.id === inFlightSegmentId)) ||\n (inFlightInitTrackId !== null &&\n allTasks.some((t) => t.type === 'append-init' && t.meta.trackId === inFlightInitTrackId));\n\n if (inFlightStillNeeded) {\n // Continue: the in-flight operation covers something the new plan needs.\n // Queue everything except the in-flight item — it will complete on its own.\n pendingTasks = allTasks.filter(\n (t) =>\n !(t.type === 'append-segment' && t.meta.id === inFlightSegmentId) &&\n !(t.type === 'append-init' && t.meta.trackId === inFlightInitTrackId)\n );\n } else {\n // Preempt: in-flight work is not needed for the new plan. Abort and replan.\n pendingTasks = allTasks;\n abortController?.abort();\n }\n },\n\n destroy() {\n destroyed = true;\n abortController?.abort();\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"segment-loader-actor.js","names":[],"sources":["../../../../src/dom/features/segment-loader-actor.ts"],"sourcesContent":["import { calculateBackBufferFlushPoint } from '../../core/buffer/back-buffer';\nimport { calculateForwardFlushPoint, getSegmentsToLoad } from '../../core/buffer/forward-buffer';\nimport type { AddressableObject, AudioTrack, Segment, VideoTrack } from '../../core/types';\nimport type {\n AppendInitMessage,\n AppendSegmentMessage,\n RemoveMessage,\n SourceBufferActor,\n} from '../media/source-buffer-actor';\n\n// ============================================================================\n// BUFFER STATE TYPES\n// ============================================================================\n\n/**\n * Buffer state for a single SourceBuffer.\n * Tracks which init segment and media segments are loaded.\n */\nexport interface SourceBufferState {\n /** Track ID of the loaded init segment */\n initTrackId?: string;\n /** Loaded media segments (unordered - selectors derive ordering) */\n segments: Array<{ id: string; trackId: string }>;\n}\n\n/**\n * Buffer state for all SourceBuffers.\n */\nexport interface BufferState {\n video?: SourceBufferState;\n audio?: SourceBufferState;\n}\n\n// ============================================================================\n// MESSAGE PROTOCOL\n// ============================================================================\n\n/** Track types that have SourceBuffers (video and audio only). */\nexport type SegmentLoaderTrack = VideoTrack | AudioTrack;\n\n/**\n * Message sent to a SegmentLoaderActor.\n *\n * `range` is optional to distinguish loading modes:\n * - No range: load init segment only (metadata preload mode)\n * - With range: load init + all segments overlapping [start, end]\n *\n * `start` and `end` are raw time values — no segment snapping.\n * The actor maps them onto segment boundaries internally.\n */\nexport type SegmentLoaderMessage = {\n type: 'load';\n track: SegmentLoaderTrack;\n range?: { start: number; end: number };\n};\n\n// ============================================================================\n// LOAD TASK\n// ============================================================================\n\n/**\n * A LoadTask is the intent to perform one unit of SegmentLoader work.\n * Unlike SourceBufferMessage, fetch-based tasks carry a URL rather than\n * pre-fetched data — the runner fetches and appends them in sequence.\n *\n * Derived from SourceBufferMessage types by removing `data` and adding\n * a fetch URL via AddressableObject.\n *\n * @todo Rename — \"LoadTask\" risks confusion with the `Task` class used for\n * SourceBufferActor scheduling. These are closer to operation descriptors or\n * messages than tasks in that sense.\n */\nexport type LoadTask =\n | (Omit<AppendInitMessage, 'data'> & AddressableObject)\n | (Omit<AppendSegmentMessage, 'data'> & AddressableObject)\n | RemoveMessage;\n\n// ============================================================================\n// ACTOR INTERFACE\n// ============================================================================\n\nexport interface SegmentLoaderActor {\n send(message: SegmentLoaderMessage): void;\n destroy(): void;\n}\n\n// ============================================================================\n// IMPLEMENTATION\n// ============================================================================\n\n/**\n * Creates a SegmentLoaderActor for one track type (video or audio).\n *\n * Receives load assignments via `send()` and owns all execution: planning,\n * removes, fetches, and appends. Coordinates with the SourceBufferActor for\n * all physical SourceBuffer operations.\n *\n * Planning (Cases 1–3) happens in `send()` on every incoming message, producing\n * an ordered LoadTask list. The runner drains that list sequentially. When a new\n * message arrives mid-run, send() replans and either continues the in-flight\n * operation (if still needed) or preempts it.\n *\n * @param sourceBufferActor - Shared SourceBufferActor reference (not owned)\n * @param fetchBytes - Tracked fetch closure (owns throughput sampling for segments).\n * Accepts an optional `minChunkSize` in options; init segments pass `Infinity`\n * so the entire body accumulates as one chunk before appending.\n */\nexport function createSegmentLoaderActor(\n sourceBufferActor: SourceBufferActor,\n fetchBytes: (\n addressable: AddressableObject,\n options?: RequestInit & { minChunkSize?: number }\n ) => Promise<AsyncIterable<Uint8Array>>\n): SegmentLoaderActor {\n let pendingTasks: LoadTask[] | null = null;\n let inFlightInitTrackId: string | null = null;\n let inFlightSegmentId: string | null = null;\n let abortController: AbortController | null = null;\n let running = false;\n let destroyed = false;\n\n const getBufferedSegments = (allSegments: readonly Segment[]): Segment[] => {\n // Exclude partial segments — they are still being streamed and must not be\n // treated as fully buffered for load planning or buffer window calculations.\n const bufferedIds = new Set(\n sourceBufferActor.snapshot\n .get()\n .context.segments.filter((s) => !s.partial)\n .map((s) => s.id)\n );\n return allSegments.filter((s) => bufferedIds.has(s.id));\n };\n\n /**\n * Translate a load message into an ordered LoadTask list based on committed\n * actor state. In-flight awareness is handled separately in send().\n *\n * @todo Rename alongside LoadTask (e.g. planOps).\n *\n * Case 1 — Removes: forward and back buffer flush points, segment-aligned.\n * No flush on track switch: appending new content overwrites existing buffer\n * ranges, and the actor's time-aligned deduplication keeps the segment model\n * accurate as new segments arrive.\n *\n * Case 2 — Init: schedule if not yet committed for this track.\n *\n * Case 3 — Segments: all segments in the load window not yet committed.\n */\n const planTasks = (message: SegmentLoaderMessage): LoadTask[] => {\n const { track, range } = message;\n const actorCtx = sourceBufferActor.snapshot.get().context;\n const bufferedSegments = getBufferedSegments(track.segments);\n const currentTime = range?.start ?? 0;\n const tasks: LoadTask[] = [];\n\n // Case 1: Removes\n if (range) {\n const forwardFlushStart = calculateForwardFlushPoint(bufferedSegments, currentTime);\n if (forwardFlushStart < Infinity) {\n tasks.push({ type: 'remove', start: forwardFlushStart, end: Infinity });\n }\n const backFlushEnd = calculateBackBufferFlushPoint(bufferedSegments, currentTime);\n if (backFlushEnd > 0) {\n tasks.push({ type: 'remove', start: 0, end: backFlushEnd });\n }\n }\n\n // Case 2: Init\n if (actorCtx.initTrackId !== track.id) {\n tasks.push({\n type: 'append-init',\n meta: { trackId: track.id },\n url: track.initialization.url,\n ...(track.initialization.byteRange !== undefined && { byteRange: track.initialization.byteRange }),\n });\n }\n\n // Case 3: Segments\n if (range) {\n const EPSILON = 0.0001;\n const segmentsToLoad = getSegmentsToLoad(track.segments, bufferedSegments, currentTime).filter((seg) => {\n // Quality-aware filter: skip segments already covered by equal-or-higher-quality\n // content in the actor context. Preserves buffered high-quality content during\n // ABR downgrades; loads during upgrades and for uncovered positions.\n const existing = actorCtx.segments.find((s) => Math.abs(s.startTime - seg.startTime) < EPSILON);\n // Partial segments are still streaming — treat as not buffered so they\n // are always re-planned (avoids relying on incomplete data).\n if (existing?.partial) return true;\n if (!existing?.trackBandwidth || !track.bandwidth) return true;\n return track.bandwidth > existing.trackBandwidth;\n });\n for (const segment of segmentsToLoad) {\n tasks.push({\n type: 'append-segment',\n meta: {\n id: segment.id,\n startTime: segment.startTime,\n duration: segment.duration,\n trackId: track.id,\n trackBandwidth: track.bandwidth,\n },\n url: segment.url,\n ...(segment.byteRange !== undefined && { byteRange: segment.byteRange }),\n });\n }\n }\n\n return tasks;\n };\n\n /**\n * Execute a single LoadTask: fetch (if needed) then forward to SourceBufferActor.\n * Sets/clears in-flight tracking around async operations so send() can make\n * accurate continue/preempt decisions at any point during execution.\n *\n * @todo Rename alongside LoadTask (e.g. executeOp).\n */\n const executeLoadTask = async (task: LoadTask): Promise<void> => {\n const signal = abortController!.signal;\n try {\n if (task.type === 'remove') {\n await sourceBufferActor.send(task, signal);\n return;\n }\n\n if (task.type === 'append-init') {\n inFlightInitTrackId = task.meta.trackId;\n if (!signal.aborted) {\n // Init segments are small and need the full body before the\n // same-track-seek vs track-switch commit decision can be made.\n // minChunkSize: Infinity causes ChunkedStreamIterable to accumulate\n // all chunks and yield exactly one — equivalent to arrayBuffer() but\n // through the same streaming path as media segments.\n const data = await fetchBytes(task, { signal, minChunkSize: Infinity });\n // For seeks on the same track: commit even if aborted — avoids re-fetching the\n // same init next time. For track switches: don't commit the old track's init;\n // the new track's init follows in pendingTasks.\n const isTrackSwitch = pendingTasks?.some(\n (t) => t.type === 'append-init' && t.meta.trackId !== task.meta.trackId\n );\n if (!signal.aborted || !isTrackSwitch) {\n const appendSignal = signal.aborted ? new AbortController().signal : signal;\n await sourceBufferActor.send({ type: 'append-init', data, meta: task.meta }, appendSignal);\n }\n }\n return;\n }\n\n // append-segment: await headers eagerly (starts the HTTP connection and\n // records the fetch in observers like tests), then pass the body stream\n // directly to the actor so chunks are appended as they arrive.\n inFlightSegmentId = task.meta.id;\n if (!signal.aborted) {\n const stream = await fetchBytes(task, { signal });\n if (!signal.aborted) {\n await sourceBufferActor.send({ type: 'append-segment', data: stream, meta: task.meta }, signal);\n }\n }\n } finally {\n inFlightInitTrackId = null;\n inFlightSegmentId = null;\n }\n };\n\n /**\n * Drain the scheduled task list sequentially.\n * After each task completes, checks for a pending replacement plan from send().\n * If the signal was aborted and no new plan arrived, stops immediately.\n */\n const runScheduled = async (initialTasks: LoadTask[]): Promise<void> => {\n running = true;\n abortController = new AbortController();\n let scheduled = initialTasks;\n\n while (scheduled.length > 0 && !destroyed) {\n const task = scheduled[0]!;\n scheduled = scheduled.slice(1);\n\n try {\n await executeLoadTask(task);\n } catch (error) {\n if (error instanceof Error && error.name === 'AbortError') {\n // Abort is handled in the post-task check below.\n } else {\n console.error('Unexpected error in segment loader:', error);\n // Non-abort error (e.g. network failure on init): don't continue with\n // remaining tasks in this sequence — they depend on the failed step.\n // A pending replacement plan (if any) will still be picked up below.\n scheduled = [];\n }\n }\n\n if (pendingTasks !== null) {\n // A new plan arrived while this task was running — switch to it.\n scheduled = pendingTasks;\n pendingTasks = null;\n abortController = new AbortController();\n } else if (abortController.signal.aborted) {\n // Aborted with no replacement plan — stop.\n break;\n }\n }\n\n abortController = null;\n running = false;\n };\n\n return {\n send(message: SegmentLoaderMessage) {\n if (destroyed) return;\n\n const allTasks = planTasks(message);\n\n if (!running) {\n if (allTasks.length === 0) return;\n runScheduled(allTasks);\n return;\n }\n\n // Determine whether the in-flight operation is still needed for the new plan.\n const inFlightStillNeeded =\n (inFlightSegmentId !== null &&\n allTasks.some((t) => t.type === 'append-segment' && t.meta.id === inFlightSegmentId)) ||\n (inFlightInitTrackId !== null &&\n allTasks.some((t) => t.type === 'append-init' && t.meta.trackId === inFlightInitTrackId));\n\n if (inFlightStillNeeded) {\n // Continue: the in-flight operation covers something the new plan needs.\n // Queue everything except the in-flight item — it will complete on its own.\n pendingTasks = allTasks.filter(\n (t) =>\n !(t.type === 'append-segment' && t.meta.id === inFlightSegmentId) &&\n !(t.type === 'append-init' && t.meta.trackId === inFlightInitTrackId)\n );\n } else {\n // Preempt: in-flight work is not needed for the new plan. Abort and replan.\n pendingTasks = allTasks;\n abortController?.abort();\n }\n },\n\n destroy() {\n destroyed = true;\n abortController?.abort();\n },\n };\n}\n"],"mappings":"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"}
@@ -1,22 +1,8 @@
1
- import { attachMediaSource, createMediaSource, waitForSourceOpen } from "../media/mediasource-setup.js";
2
- import { combineLatest } from "../../core/reactive/combine-latest.js";
3
- import { isNil } from "@videojs/utils/predicate";
4
-
1
+ import { effect } from "../../core/signals/effect.js";
2
+ import { computed } from "../../core/signals/primitives.js";
3
+ import { attachMediaSource, createMediaSource, observeMediaSourceReadyState } from "../media/mediasource-setup.js";
5
4
  //#region src/dom/features/setup-mediasource.ts
6
5
  /**
7
- * Check if we have the minimum requirements to create MediaSource.
8
- */
9
- function canSetup(state, owners) {
10
- return !isNil(owners.mediaElement) && !isNil(state.presentation?.url);
11
- }
12
- /**
13
- * Check if we should proceed with MediaSource creation.
14
- * Placeholder for future conditions (e.g., checking if already created).
15
- */
16
- function shouldSetup(_state, owners) {
17
- return isNil(owners.mediaSource);
18
- }
19
- /**
20
6
  * Setup MediaSource orchestration.
21
7
  *
22
8
  * Creates and attaches MediaSource when:
@@ -26,30 +12,37 @@ function shouldSetup(_state, owners) {
26
12
  * Updates owners.mediaSource after successful setup.
27
13
  */
28
14
  function setupMediaSource({ state, owners }) {
29
- let settingUp = false;
30
- let abortController = null;
31
- const unsubscribe = combineLatest([state, owners]).subscribe(async ([currentState, currentOwners]) => {
32
- if (!canSetup(currentState, currentOwners) || !shouldSetup(currentState, currentOwners) || settingUp) return;
33
- try {
34
- settingUp = true;
35
- abortController = new AbortController();
36
- const mediaSource = createMediaSource({ preferManaged: true });
37
- attachMediaSource(mediaSource, currentOwners.mediaElement);
38
- await waitForSourceOpen(mediaSource, abortController.signal);
39
- owners.patch({ mediaSource });
40
- } catch (error) {
41
- if (error instanceof DOMException && error.name === "AbortError") return;
42
- throw error;
43
- } finally {
44
- settingUp = false;
45
- }
15
+ const abortController = new AbortController();
16
+ const mediaElementSignal = computed(() => owners.get().mediaElement);
17
+ const presentationUrlSignal = computed(() => state.get().presentation?.url);
18
+ const canSetupSignal = computed(() => !!mediaElementSignal.get() && !!presentationUrlSignal.get());
19
+ const mediaElementSrcSignal = computed(() => mediaElementSignal.get()?.src);
20
+ const mediaSourceSignal = computed(() => owners.get().mediaSource);
21
+ const shouldSetupSignal = computed(() => !mediaElementSrcSignal.get());
22
+ const cleanupEffect = effect(() => {
23
+ if (!canSetupSignal.get() || !shouldSetupSignal.get()) return;
24
+ const mediaElement = mediaElementSignal.get();
25
+ const { signal } = abortController;
26
+ const mediaSource = createMediaSource({ preferManaged: true });
27
+ const mediaSourceReadyState = observeMediaSourceReadyState(mediaSource, signal);
28
+ attachMediaSource(mediaSource, mediaElement);
29
+ const cleanupOwnersUpdateEffect = effect(() => {
30
+ if (!!mediaSourceSignal.get() || mediaSourceReadyState.get() !== "open") return;
31
+ owners.set(Object.assign({}, owners.get(), {
32
+ mediaSource,
33
+ mediaSourceReadyState
34
+ }));
35
+ });
36
+ return () => {
37
+ cleanupOwnersUpdateEffect();
38
+ };
46
39
  });
47
40
  return () => {
48
41
  abortController?.abort();
49
- unsubscribe();
42
+ cleanupEffect();
50
43
  };
51
44
  }
52
-
53
45
  //#endregion
54
46
  export { setupMediaSource };
47
+
55
48
  //# sourceMappingURL=setup-mediasource.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"setup-mediasource.js","names":[],"sources":["../../../../src/dom/features/setup-mediasource.ts"],"sourcesContent":["import { isNil } from '@videojs/utils/predicate';\nimport { combineLatest } from '../../core/reactive/combine-latest';\nimport type { WritableState } from '../../core/state/create-state';\nimport type { Presentation } from '../../core/types';\nimport { attachMediaSource, createMediaSource, waitForSourceOpen } from '../media/mediasource-setup';\n\n/**\n * State shape required for MediaSource setup.\n */\nexport interface MediaSourceState {\n presentation?: Presentation;\n}\n\n/**\n * Owners shape for MediaSource setup.\n */\nexport interface MediaSourceOwners {\n mediaElement?: HTMLMediaElement | undefined;\n mediaSource?: MediaSource;\n}\n\n/**\n * Check if we have the minimum requirements to create MediaSource.\n */\nexport function canSetup(state: MediaSourceState, owners: MediaSourceOwners): boolean {\n return !isNil(owners.mediaElement) && !isNil(state.presentation?.url);\n}\n\n/**\n * Check if we should proceed with MediaSource creation.\n * Placeholder for future conditions (e.g., checking if already created).\n */\nexport function shouldSetup(_state: MediaSourceState, owners: MediaSourceOwners): boolean {\n // Don't create if we already have one\n return isNil(owners.mediaSource);\n}\n\n/**\n * Setup MediaSource orchestration.\n *\n * Creates and attaches MediaSource when:\n * - mediaElement exists in owners\n * - presentation.url exists in state\n *\n * Updates owners.mediaSource after successful setup.\n */\nexport function setupMediaSource({\n state,\n owners,\n}: {\n state: WritableState<MediaSourceState>;\n owners: WritableState<MediaSourceOwners>;\n}): () => void {\n let settingUp = false;\n let abortController: AbortController | null = null;\n\n const unsubscribe = combineLatest([state, owners]).subscribe(\n async ([currentState, currentOwners]: [MediaSourceState, MediaSourceOwners]) => {\n if (!canSetup(currentState, currentOwners) || !shouldSetup(currentState, currentOwners) || settingUp) return;\n\n try {\n settingUp = true;\n abortController = new AbortController();\n\n // Prefer ManagedMediaSource when available (Safari 17+). attachMediaSource\n // handles the srcObject path and sets disableRemotePlayback automatically.\n const mediaSource = createMediaSource({ preferManaged: true });\n\n // Attach to element\n attachMediaSource(mediaSource, currentOwners.mediaElement!);\n\n // Wait for sourceopen aborted if destroy() is called before it fires\n await waitForSourceOpen(mediaSource, abortController.signal);\n\n owners.patch({ mediaSource });\n } catch (error) {\n if (error instanceof DOMException && error.name === 'AbortError') return;\n throw error;\n } finally {\n settingUp = false;\n }\n }\n );\n\n return () => {\n abortController?.abort();\n unsubscribe();\n };\n}\n"],"mappings":";;;;;;;;AAwBA,SAAgB,SAAS,OAAyB,QAAoC;AACpF,QAAO,CAAC,MAAM,OAAO,aAAa,IAAI,CAAC,MAAM,MAAM,cAAc,IAAI;;;;;;AAOvE,SAAgB,YAAY,QAA0B,QAAoC;AAExF,QAAO,MAAM,OAAO,YAAY;;;;;;;;;;;AAYlC,SAAgB,iBAAiB,EAC/B,OACA,UAIa;CACb,IAAI,YAAY;CAChB,IAAI,kBAA0C;CAE9C,MAAM,cAAc,cAAc,CAAC,OAAO,OAAO,CAAC,CAAC,UACjD,OAAO,CAAC,cAAc,mBAA0D;AAC9E,MAAI,CAAC,SAAS,cAAc,cAAc,IAAI,CAAC,YAAY,cAAc,cAAc,IAAI,UAAW;AAEtG,MAAI;AACF,eAAY;AACZ,qBAAkB,IAAI,iBAAiB;GAIvC,MAAM,cAAc,kBAAkB,EAAE,eAAe,MAAM,CAAC;AAG9D,qBAAkB,aAAa,cAAc,aAAc;AAG3D,SAAM,kBAAkB,aAAa,gBAAgB,OAAO;AAE5D,UAAO,MAAM,EAAE,aAAa,CAAC;WACtB,OAAO;AACd,OAAI,iBAAiB,gBAAgB,MAAM,SAAS,aAAc;AAClE,SAAM;YACE;AACR,eAAY;;GAGjB;AAED,cAAa;AACX,mBAAiB,OAAO;AACxB,eAAa"}
1
+ {"version":3,"file":"setup-mediasource.js","names":[],"sources":["../../../../src/dom/features/setup-mediasource.ts"],"sourcesContent":["import { effect } from '../../core/signals/effect';\nimport { computed, type ReadonlySignal, type Signal } from '../../core/signals/primitives';\nimport type { Presentation } from '../../core/types';\nimport { attachMediaSource, createMediaSource, observeMediaSourceReadyState } from '../media/mediasource-setup';\n\n/**\n * State shape required for MediaSource setup.\n */\nexport interface MediaSourceState {\n presentation?: Presentation;\n}\n\n/**\n * Owners shape for MediaSource setup.\n */\nexport interface MediaSourceOwners {\n mediaElement?: HTMLMediaElement | undefined;\n mediaSource?: MediaSource;\n /** Reactive mirror of `mediaSource.readyState` updated via DOM events. */\n mediaSourceReadyState?: ReadonlySignal<MediaSource['readyState']>;\n}\n\n/**\n * Setup MediaSource orchestration.\n *\n * Creates and attaches MediaSource when:\n * - mediaElement exists in owners\n * - presentation.url exists in state\n *\n * Updates owners.mediaSource after successful setup.\n */\nexport function setupMediaSource<S extends MediaSourceState, O extends MediaSourceOwners>({\n state,\n owners,\n}: {\n state: Signal<S>;\n owners: Signal<O>;\n}): () => void {\n const abortController = new AbortController();\n\n // Get the latest mediaElement (even if nullish)\n const mediaElementSignal = computed(() => owners.get().mediaElement);\n // Get the latest presentationUrl (even if nullish)\n const presentationUrlSignal = computed(() => state.get().presentation?.url);\n\n const canSetupSignal = computed(() => !!mediaElementSignal.get() && !!presentationUrlSignal.get());\n\n const mediaElementSrcSignal = computed(() => mediaElementSignal.get()?.src);\n const mediaSourceSignal = computed(() => owners.get().mediaSource);\n const shouldSetupSignal = computed(() => !mediaElementSrcSignal.get());\n\n // NOTE: This should be cleaner and less brittle if/when Reactors have their own internal finite state. This is planned as followup work.\n // (here, e.g. something like: \"preconditions_unmet\"|\"pending\"|\"setting_up\"|\"tearing_down\"|\"set_up\")\n // This should also avoid needing nested effect().\n const cleanupEffect = effect(() => {\n if (!canSetupSignal.get() || !shouldSetupSignal.get()) return;\n const mediaElement = mediaElementSignal.get() as HTMLMediaElement;\n const { signal } = abortController;\n\n const mediaSource = createMediaSource({ preferManaged: true });\n // NOTE: Consider making MediaSource an Actor and using this in it.\n const mediaSourceReadyState = observeMediaSourceReadyState(mediaSource, signal);\n attachMediaSource(mediaSource, mediaElement);\n\n const cleanupOwnersUpdateEffect = effect(() => {\n // If we already have a MediaSource or the *internal* mediaSource is not yet fully attached, wait to add it to owners;\n if (!!mediaSourceSignal.get() || mediaSourceReadyState.get() !== 'open') return;\n owners.set(Object.assign({}, owners.get(), { mediaSource, mediaSourceReadyState }) as O);\n });\n\n return () => {\n cleanupOwnersUpdateEffect();\n };\n });\n\n return () => {\n abortController?.abort();\n cleanupEffect();\n };\n}\n"],"mappings":";;;;;;;;;;;;;AA+BA,SAAgB,iBAA0E,EACxF,OACA,UAIa;CACb,MAAM,kBAAkB,IAAI,iBAAiB;CAG7C,MAAM,qBAAqB,eAAe,OAAO,KAAK,CAAC,aAAa;CAEpE,MAAM,wBAAwB,eAAe,MAAM,KAAK,CAAC,cAAc,IAAI;CAE3E,MAAM,iBAAiB,eAAe,CAAC,CAAC,mBAAmB,KAAK,IAAI,CAAC,CAAC,sBAAsB,KAAK,CAAC;CAElG,MAAM,wBAAwB,eAAe,mBAAmB,KAAK,EAAE,IAAI;CAC3E,MAAM,oBAAoB,eAAe,OAAO,KAAK,CAAC,YAAY;CAClE,MAAM,oBAAoB,eAAe,CAAC,sBAAsB,KAAK,CAAC;CAKtE,MAAM,gBAAgB,aAAa;AACjC,MAAI,CAAC,eAAe,KAAK,IAAI,CAAC,kBAAkB,KAAK,CAAE;EACvD,MAAM,eAAe,mBAAmB,KAAK;EAC7C,MAAM,EAAE,WAAW;EAEnB,MAAM,cAAc,kBAAkB,EAAE,eAAe,MAAM,CAAC;EAE9D,MAAM,wBAAwB,6BAA6B,aAAa,OAAO;AAC/E,oBAAkB,aAAa,aAAa;EAE5C,MAAM,4BAA4B,aAAa;AAE7C,OAAI,CAAC,CAAC,kBAAkB,KAAK,IAAI,sBAAsB,KAAK,KAAK,OAAQ;AACzE,UAAO,IAAI,OAAO,OAAO,EAAE,EAAE,OAAO,KAAK,EAAE;IAAE;IAAa;IAAuB,CAAC,CAAM;IACxF;AAEF,eAAa;AACX,8BAA2B;;GAE7B;AAEF,cAAa;AACX,mBAAiB,OAAO;AACxB,iBAAe"}
@@ -1 +1 @@
1
- {"version":3,"file":"setup-sourcebuffer.d.ts","names":[],"sources":["../../../../src/dom/features/setup-sourcebuffer.ts"],"mappings":";;;;;KAaY,cAAA"}
1
+ {"version":3,"file":"setup-sourcebuffer.d.ts","names":[],"sources":["../../../../src/dom/features/setup-sourcebuffer.ts"],"mappings":";AAYA;;;;AAAA,KAAY,cAAA"}