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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. package/dist/default/background-video.js +3 -0
  2. package/dist/default/core/composition/create-composition.js +1 -1
  3. package/dist/default/core/composition/create-composition.js.map +1 -1
  4. package/dist/default/core/composition/share-signals.js +13 -11
  5. package/dist/default/core/composition/share-signals.js.map +1 -1
  6. package/dist/default/hls.js +3 -1
  7. package/dist/default/media/abr/quality-selection.js +6 -68
  8. package/dist/default/media/abr/quality-selection.js.map +1 -1
  9. package/dist/default/media/dom/capabilities.js +70 -0
  10. package/dist/default/media/dom/capabilities.js.map +1 -0
  11. package/dist/default/media/dom/mse/mediasource-setup.js +1 -1
  12. package/dist/default/media/dom/text/text-track-slots.js +3 -3
  13. package/dist/default/media/dom/text/text-track-slots.js.map +1 -1
  14. package/dist/default/media/hls/parse-attributes.js +16 -2
  15. package/dist/default/media/hls/parse-attributes.js.map +1 -1
  16. package/dist/default/media/hls/parse-media-playlist.js +25 -2
  17. package/dist/default/media/hls/parse-media-playlist.js.map +1 -1
  18. package/dist/default/media/hls/parse-multivariant.js +15 -6
  19. package/dist/default/media/hls/parse-multivariant.js.map +1 -1
  20. package/dist/default/media/primitives/select-tracks.js +98 -13
  21. package/dist/default/media/primitives/select-tracks.js.map +1 -1
  22. package/dist/default/media/types/index.js.map +1 -1
  23. package/dist/default/media/utils/cdn.js +56 -0
  24. package/dist/default/media/utils/cdn.js.map +1 -0
  25. package/dist/default/media/utils/tracks.js +28 -1
  26. package/dist/default/media/utils/tracks.js.map +1 -1
  27. package/dist/default/network/fetch.js +7 -1
  28. package/dist/default/network/fetch.js.map +1 -1
  29. package/dist/default/playback/actors/dom/segment-loader.js +42 -11
  30. package/dist/default/playback/actors/dom/segment-loader.js.map +1 -1
  31. package/dist/default/playback/actors/dom/source-buffer.js +2 -1
  32. package/dist/default/playback/actors/dom/source-buffer.js.map +1 -1
  33. package/dist/default/playback/behaviors/calculate-presentation-duration.js +21 -43
  34. package/dist/default/playback/behaviors/calculate-presentation-duration.js.map +1 -1
  35. package/dist/default/playback/behaviors/derive-cdn-priority.js +68 -0
  36. package/dist/default/playback/behaviors/derive-cdn-priority.js.map +1 -0
  37. package/dist/default/playback/behaviors/dom/end-of-stream.js +15 -103
  38. package/dist/default/playback/behaviors/dom/end-of-stream.js.map +1 -1
  39. package/dist/default/playback/behaviors/dom/load-segments.js +1 -1
  40. package/dist/default/playback/behaviors/dom/load-segments.js.map +1 -1
  41. package/dist/default/playback/behaviors/dom/setup-buffer-actors.js +33 -14
  42. package/dist/default/playback/behaviors/dom/setup-buffer-actors.js.map +1 -1
  43. package/dist/default/playback/behaviors/dom/sync-text-tracks.js +43 -18
  44. package/dist/default/playback/behaviors/dom/sync-text-tracks.js.map +1 -1
  45. package/dist/default/playback/behaviors/dom/track-current-time.js +6 -0
  46. package/dist/default/playback/behaviors/dom/track-current-time.js.map +1 -1
  47. package/dist/default/playback/behaviors/dom/update-mediasource-duration.js +1 -1
  48. package/dist/default/playback/behaviors/resolve-track.js +45 -22
  49. package/dist/default/playback/behaviors/resolve-track.js.map +1 -1
  50. package/dist/default/playback/behaviors/select-tracks.js +46 -77
  51. package/dist/default/playback/behaviors/select-tracks.js.map +1 -1
  52. package/dist/default/playback/behaviors/setup-failover-monitor.js +66 -0
  53. package/dist/default/playback/behaviors/setup-failover-monitor.js.map +1 -0
  54. package/dist/default/playback/behaviors/track-switching.js +341 -0
  55. package/dist/default/playback/behaviors/track-switching.js.map +1 -0
  56. package/dist/default/playback/engines/background-video/adapter.js +158 -0
  57. package/dist/default/playback/engines/background-video/adapter.js.map +1 -0
  58. package/dist/default/playback/engines/background-video/engine.js +75 -0
  59. package/dist/default/playback/engines/background-video/engine.js.map +1 -0
  60. package/dist/default/playback/engines/hls/adapter-audio-only.js +119 -0
  61. package/dist/default/playback/engines/hls/adapter-audio-only.js.map +1 -0
  62. package/dist/default/playback/engines/hls/adapter.js +7 -0
  63. package/dist/default/playback/engines/hls/adapter.js.map +1 -1
  64. package/dist/default/playback/engines/hls/engine-audio-only.js +80 -0
  65. package/dist/default/playback/engines/hls/engine-audio-only.js.map +1 -0
  66. package/dist/default/playback/engines/hls/engine.js +20 -7
  67. package/dist/default/playback/engines/hls/engine.js.map +1 -1
  68. package/dist/default/playback/primitives/failover-fetch.js +42 -0
  69. package/dist/default/playback/primitives/failover-fetch.js.map +1 -0
  70. package/dist/{dev/playback/behaviors → default/playback/primitives}/track-types.js +1 -1
  71. package/dist/default/playback/primitives/track-types.js.map +1 -0
  72. package/dist/dev/background-video.d.ts +3 -0
  73. package/dist/dev/background-video.js +3 -0
  74. package/dist/dev/core/composition/create-composition.js +1 -1
  75. package/dist/dev/core/composition/create-composition.js.map +1 -1
  76. package/dist/dev/core/composition/share-signals.d.ts +11 -9
  77. package/dist/dev/core/composition/share-signals.d.ts.map +1 -1
  78. package/dist/dev/core/composition/share-signals.js +13 -11
  79. package/dist/dev/core/composition/share-signals.js.map +1 -1
  80. package/dist/dev/hls.d.ts +3 -1
  81. package/dist/dev/hls.js +3 -1
  82. package/dist/dev/media/abr/quality-selection.js +6 -68
  83. package/dist/dev/media/abr/quality-selection.js.map +1 -1
  84. package/dist/dev/media/dom/capabilities.js +70 -0
  85. package/dist/dev/media/dom/capabilities.js.map +1 -0
  86. package/dist/dev/media/dom/mse/mediasource-setup.js +1 -1
  87. package/dist/dev/media/dom/text/text-track-slots.d.ts.map +1 -1
  88. package/dist/dev/media/dom/text/text-track-slots.js +3 -3
  89. package/dist/dev/media/dom/text/text-track-slots.js.map +1 -1
  90. package/dist/dev/media/hls/parse-attributes.js +16 -2
  91. package/dist/dev/media/hls/parse-attributes.js.map +1 -1
  92. package/dist/dev/media/hls/parse-media-playlist.js +25 -2
  93. package/dist/dev/media/hls/parse-media-playlist.js.map +1 -1
  94. package/dist/dev/media/hls/parse-multivariant.js +15 -6
  95. package/dist/dev/media/hls/parse-multivariant.js.map +1 -1
  96. package/dist/dev/media/primitives/select-tracks.d.ts +36 -0
  97. package/dist/dev/media/primitives/select-tracks.d.ts.map +1 -0
  98. package/dist/dev/media/primitives/select-tracks.js +98 -13
  99. package/dist/dev/media/primitives/select-tracks.js.map +1 -1
  100. package/dist/dev/media/types/index.d.ts +26 -2
  101. package/dist/dev/media/types/index.d.ts.map +1 -1
  102. package/dist/dev/media/types/index.js.map +1 -1
  103. package/dist/dev/media/utils/cdn.d.ts +13 -0
  104. package/dist/dev/media/utils/cdn.d.ts.map +1 -0
  105. package/dist/dev/media/utils/cdn.js +56 -0
  106. package/dist/dev/media/utils/cdn.js.map +1 -0
  107. package/dist/dev/media/utils/tracks.js +28 -1
  108. package/dist/dev/media/utils/tracks.js.map +1 -1
  109. package/dist/dev/network/fetch.js +7 -1
  110. package/dist/dev/network/fetch.js.map +1 -1
  111. package/dist/dev/playback/actors/dom/segment-loader.js +42 -11
  112. package/dist/dev/playback/actors/dom/segment-loader.js.map +1 -1
  113. package/dist/dev/playback/actors/dom/source-buffer.d.ts +15 -0
  114. package/dist/dev/playback/actors/dom/source-buffer.d.ts.map +1 -1
  115. package/dist/dev/playback/actors/dom/source-buffer.js +2 -1
  116. package/dist/dev/playback/actors/dom/source-buffer.js.map +1 -1
  117. package/dist/dev/playback/behaviors/calculate-presentation-duration.d.ts +8 -0
  118. package/dist/dev/playback/behaviors/calculate-presentation-duration.d.ts.map +1 -1
  119. package/dist/dev/playback/behaviors/calculate-presentation-duration.js +21 -43
  120. package/dist/dev/playback/behaviors/calculate-presentation-duration.js.map +1 -1
  121. package/dist/dev/playback/behaviors/derive-cdn-priority.js +68 -0
  122. package/dist/dev/playback/behaviors/derive-cdn-priority.js.map +1 -0
  123. package/dist/dev/playback/behaviors/dom/end-of-stream.js +15 -103
  124. package/dist/dev/playback/behaviors/dom/end-of-stream.js.map +1 -1
  125. package/dist/dev/playback/behaviors/dom/load-segments.js +1 -1
  126. package/dist/dev/playback/behaviors/dom/load-segments.js.map +1 -1
  127. package/dist/dev/playback/behaviors/dom/setup-buffer-actors.js +33 -14
  128. package/dist/dev/playback/behaviors/dom/setup-buffer-actors.js.map +1 -1
  129. package/dist/dev/playback/behaviors/dom/sync-text-tracks.js +43 -18
  130. package/dist/dev/playback/behaviors/dom/sync-text-tracks.js.map +1 -1
  131. package/dist/dev/playback/behaviors/dom/track-current-time.d.ts.map +1 -1
  132. package/dist/dev/playback/behaviors/dom/track-current-time.js +6 -0
  133. package/dist/dev/playback/behaviors/dom/track-current-time.js.map +1 -1
  134. package/dist/dev/playback/behaviors/dom/update-mediasource-duration.js +1 -1
  135. package/dist/dev/playback/behaviors/resolve-track.js +45 -22
  136. package/dist/dev/playback/behaviors/resolve-track.js.map +1 -1
  137. package/dist/dev/playback/behaviors/select-tracks.d.ts +13 -0
  138. package/dist/dev/playback/behaviors/select-tracks.d.ts.map +1 -0
  139. package/dist/dev/playback/behaviors/select-tracks.js +46 -77
  140. package/dist/dev/playback/behaviors/select-tracks.js.map +1 -1
  141. package/dist/dev/playback/behaviors/setup-failover-monitor.d.ts +12 -0
  142. package/dist/dev/playback/behaviors/setup-failover-monitor.d.ts.map +1 -0
  143. package/dist/dev/playback/behaviors/setup-failover-monitor.js +66 -0
  144. package/dist/dev/playback/behaviors/setup-failover-monitor.js.map +1 -0
  145. package/dist/dev/playback/behaviors/track-switching.js +341 -0
  146. package/dist/dev/playback/behaviors/track-switching.js.map +1 -0
  147. package/dist/dev/playback/engines/background-video/adapter.d.ts +60 -0
  148. package/dist/dev/playback/engines/background-video/adapter.d.ts.map +1 -0
  149. package/dist/dev/playback/engines/background-video/adapter.js +158 -0
  150. package/dist/dev/playback/engines/background-video/adapter.js.map +1 -0
  151. package/dist/dev/playback/engines/background-video/engine.d.ts +107 -0
  152. package/dist/dev/playback/engines/background-video/engine.d.ts.map +1 -0
  153. package/dist/dev/playback/engines/background-video/engine.js +75 -0
  154. package/dist/dev/playback/engines/background-video/engine.js.map +1 -0
  155. package/dist/dev/playback/engines/hls/adapter-audio-only.d.ts +45 -0
  156. package/dist/dev/playback/engines/hls/adapter-audio-only.d.ts.map +1 -0
  157. package/dist/dev/playback/engines/hls/adapter-audio-only.js +119 -0
  158. package/dist/dev/playback/engines/hls/adapter-audio-only.js.map +1 -0
  159. package/dist/dev/playback/engines/hls/adapter.d.ts.map +1 -1
  160. package/dist/dev/playback/engines/hls/adapter.js +7 -0
  161. package/dist/dev/playback/engines/hls/adapter.js.map +1 -1
  162. package/dist/dev/playback/engines/hls/engine-audio-only.d.ts +127 -0
  163. package/dist/dev/playback/engines/hls/engine-audio-only.d.ts.map +1 -0
  164. package/dist/dev/playback/engines/hls/engine-audio-only.js +80 -0
  165. package/dist/dev/playback/engines/hls/engine-audio-only.js.map +1 -0
  166. package/dist/dev/playback/engines/hls/engine.d.ts +61 -2
  167. package/dist/dev/playback/engines/hls/engine.d.ts.map +1 -1
  168. package/dist/dev/playback/engines/hls/engine.js +20 -7
  169. package/dist/dev/playback/engines/hls/engine.js.map +1 -1
  170. package/dist/dev/playback/primitives/failover-fetch.js +42 -0
  171. package/dist/dev/playback/primitives/failover-fetch.js.map +1 -0
  172. package/dist/{default/playback/behaviors → dev/playback/primitives}/track-types.js +1 -1
  173. package/dist/dev/playback/primitives/track-types.js.map +1 -0
  174. package/package.json +7 -2
  175. package/dist/default/playback/behaviors/quality-switching.js +0 -96
  176. package/dist/default/playback/behaviors/quality-switching.js.map +0 -1
  177. package/dist/default/playback/behaviors/track-types.js.map +0 -1
  178. package/dist/dev/playback/behaviors/quality-switching.js +0 -96
  179. package/dist/dev/playback/behaviors/quality-switching.js.map +0 -1
  180. package/dist/dev/playback/behaviors/track-types.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"source-buffer.js","names":[],"sources":["../../../../../src/playback/actors/dom/source-buffer.ts"],"sourcesContent":["import { createMachineActor, type HandlerContext, type MessageActor } from '../../../core/actors/create-machine-actor';\nimport { SerialRunner, Task } from '../../../core/tasks/task';\nimport { type AppendData, appendSegment } from '../../../media/dom/mse/append-segment';\nimport { flushBuffer } from '../../../media/dom/mse/buffer-flusher';\nimport { SEGMENT_TIME_EPSILON, type Segment, type Track } from '../../../media/types';\n\n// =============================================================================\n// Types\n// =============================================================================\n\nexport interface BufferedRange {\n start: number;\n end: number;\n}\n\nexport type AppendSegmentMeta = Pick<Segment, 'id' | 'startTime' | 'duration'> & {\n trackId: Track['id'];\n /** Declared track bandwidth in bps (from playlist BANDWIDTH attribute). */\n trackBandwidth?: number;\n};\n\nexport type { AppendData };\n\nexport type AppendInitMessage = { type: 'append-init'; data: AppendData; meta: { trackId: Track['id'] } };\nexport type AppendSegmentMessage = { type: 'append-segment'; data: AppendData; meta: AppendSegmentMeta };\nexport type RemoveMessage = { type: 'remove'; start: number; end: number };\nexport type IndividualSourceBufferMessage = AppendInitMessage | AppendSegmentMessage | RemoveMessage;\nexport type BatchMessage = { type: 'batch'; messages: IndividualSourceBufferMessage[] };\nexport type CancelMessage = { type: 'cancel' };\n\n/** All messages accepted by a SourceBufferActor. */\nexport type SourceBufferMessage = IndividualSourceBufferMessage | BatchMessage | CancelMessage;\n\n/** Finite states of the actor. */\nexport type SourceBufferActorState = 'idle' | 'updating' | 'destroyed';\n\n/** Non-finite (extended) data managed by the actor — the XState \"context\". */\nexport interface SourceBufferActorContext {\n initTrackId?: string | undefined;\n segments: Array<\n Pick<Segment, 'id' | 'startTime' | 'duration'> & {\n trackId: Track['id'];\n trackBandwidth?: number;\n /**\n * True while a streaming append is in progress for this segment.\n * The segment's data is partially present in the SourceBuffer.\n * Downstream code must not treat a partial segment as fully buffered.\n */\n partial?: boolean;\n }\n >;\n bufferedRanges: BufferedRange[];\n}\n\n/** SourceBuffer actor: queues operations, owns its snapshot. */\nexport type SourceBufferActor = MessageActor<SourceBufferActorState, SourceBufferActorContext, SourceBufferMessage>;\n\n// =============================================================================\n// Helpers\n// =============================================================================\n\nfunction snapshotBuffered(buffered: TimeRanges): BufferedRange[] {\n const ranges: BufferedRange[] = [];\n for (let i = 0; i < buffered.length; i++) {\n ranges.push({ start: buffered.start(i), end: buffered.end(i) });\n }\n return ranges;\n}\n\n// =============================================================================\n// Message task factories\n// =============================================================================\n\n// Context is read lazily via getContext at task execution time — not at creation\n// time — so each task always operates on the most recent context regardless of\n// when it was scheduled.\n\ninterface MessageTaskOptions {\n getContext: () => SourceBufferActorContext;\n sourceBuffer: SourceBuffer;\n setContext: (ctx: SourceBufferActorContext) => void;\n}\n\nfunction appendInitTask(\n message: AppendInitMessage,\n { getContext, sourceBuffer }: MessageTaskOptions\n): Task<SourceBufferActorContext> {\n return new Task(async (taskSignal) => {\n const ctx = getContext();\n if (taskSignal.aborted) return ctx;\n await appendSegment(sourceBuffer, message.data);\n // No abort check here: the physical SourceBuffer has been modified, so\n // the model must be updated to match regardless of signal state.\n return { ...ctx, initTrackId: message.meta.trackId };\n });\n}\n\nfunction appendSegmentTask(\n message: AppendSegmentMessage,\n { getContext, sourceBuffer, setContext }: MessageTaskOptions\n): Task<SourceBufferActorContext> {\n return new Task(async (taskSignal) => {\n const ctx = getContext();\n if (taskSignal.aborted) return ctx;\n\n const { meta } = message;\n // Remove any existing entry at the same start time (same \"slot\" in the\n // timeline), then record the new segment. Assumes time-aligned segments\n // across playlists. `SEGMENT_TIME_EPSILON` guards against floating-point\n // drift in parsed timestamps (shared with the segment-loader quality\n // filter — single source of truth).\n const filtered = ctx.segments.filter((s) => Math.abs(s.startTime - meta.startTime) >= SEGMENT_TIME_EPSILON);\n\n // For streaming data: emit partial state before the first chunk so\n // downstream code can see the in-progress segment and treat it as\n // incomplete. ArrayBuffer appends are atomic so no partial state is\n // needed — context is updated once at task completion.\n if (!(message.data instanceof ArrayBuffer)) {\n setContext({\n ...ctx,\n segments: [\n ...filtered,\n {\n id: meta.id,\n startTime: meta.startTime,\n duration: meta.duration,\n trackId: meta.trackId,\n ...(meta.trackBandwidth !== undefined && { trackBandwidth: meta.trackBandwidth }),\n partial: true,\n },\n ],\n bufferedRanges: ctx.bufferedRanges,\n });\n }\n\n await appendSegment(sourceBuffer, message.data, taskSignal);\n // No abort check here: the physical SourceBuffer has been modified, so\n // the model must be updated to match regardless of signal state.\n return {\n ...ctx,\n segments: [\n ...filtered,\n {\n id: meta.id,\n startTime: meta.startTime,\n duration: meta.duration,\n trackId: meta.trackId,\n ...(meta.trackBandwidth !== undefined && { trackBandwidth: meta.trackBandwidth }),\n },\n ],\n bufferedRanges: snapshotBuffered(sourceBuffer.buffered),\n };\n });\n}\n\nfunction removeTask(\n message: RemoveMessage,\n { getContext, sourceBuffer }: MessageTaskOptions\n): Task<SourceBufferActorContext> {\n return new Task(async (taskSignal) => {\n const ctx = getContext();\n if (taskSignal.aborted) return ctx;\n await flushBuffer(sourceBuffer, message.start, message.end);\n // No abort check here: the physical SourceBuffer has been modified, so\n // the model must be updated to match regardless of signal state.\n //\n // Use the post-flush buffered ranges as ground truth. A segment is kept\n // in the model only if its midpoint falls within a buffered range.\n // Midpoint-based membership handles flush boundaries that don't align\n // exactly with segment edges without over-removing adjacent segments.\n const bufferedRanges = snapshotBuffered(sourceBuffer.buffered);\n const filtered = ctx.segments.filter((s) => {\n const midpoint = s.startTime + s.duration / 2;\n return bufferedRanges.some((r) => midpoint >= r.start && midpoint < r.end);\n });\n return { ...ctx, segments: filtered, bufferedRanges };\n });\n}\n\ntype MessageTaskFactory<T extends IndividualSourceBufferMessage> = (\n message: T,\n options: MessageTaskOptions\n) => Task<SourceBufferActorContext>;\n\nconst messageTaskFactories = {\n 'append-init': appendInitTask,\n 'append-segment': appendSegmentTask,\n remove: removeTask,\n} satisfies {\n [K in IndividualSourceBufferMessage['type']]: MessageTaskFactory<Extract<IndividualSourceBufferMessage, { type: K }>>;\n};\n\nfunction messageToTask(\n message: IndividualSourceBufferMessage,\n options: MessageTaskOptions\n): Task<SourceBufferActorContext> {\n const factory = messageTaskFactories[message.type] as MessageTaskFactory<typeof message>;\n return factory(message, options);\n}\n\n// =============================================================================\n// Implementation\n// =============================================================================\n\nexport function createSourceBufferActor(\n sourceBuffer: SourceBuffer,\n initialContext?: Partial<SourceBufferActorContext>\n): SourceBufferActor {\n type UserState = Exclude<SourceBufferActorState, 'destroyed'>;\n\n const handleError = (e: unknown): void => {\n if (!(e instanceof Error && e.name === 'AbortError')) {\n console.error('SourceBuffer operation failed:', e);\n }\n };\n\n type Ctx = HandlerContext<UserState, SourceBufferActorContext, () => SerialRunner>;\n\n const onMessage = (msg: IndividualSourceBufferMessage, { transition, setContext, getContext, runner }: Ctx): void => {\n transition('updating');\n const task = messageToTask(msg, { getContext, sourceBuffer, setContext });\n runner.schedule(task).then(setContext, handleError);\n };\n\n return createMachineActor<UserState, SourceBufferActorContext, SourceBufferMessage, () => SerialRunner>({\n runner: () => new SerialRunner(),\n initial: 'idle',\n context: { segments: [], bufferedRanges: [], initTrackId: undefined, ...initialContext },\n states: {\n idle: {\n on: {\n 'append-init': onMessage,\n 'append-segment': onMessage,\n remove: onMessage,\n batch: (msg, { transition, setContext, getContext, runner }) => {\n const { messages } = msg;\n if (messages.length === 0) return;\n\n transition('updating');\n messages.forEach((msg) => {\n const task = messageToTask(msg, { getContext, sourceBuffer, setContext });\n runner.schedule(task).then(setContext, handleError);\n });\n },\n },\n },\n updating: {\n // Automatically return to idle once all scheduled tasks settle.\n onSettled: 'idle',\n on: {\n // Abort all in-progress and pending tasks. onSettled handles → 'idle'\n // once the aborted tasks drain.\n cancel: (_, { runner }) => {\n runner.abortAll();\n },\n },\n },\n },\n });\n}\n"],"mappings":";;;;;;AA6DA,SAAS,iBAAiB,UAAuC;CAC/D,MAAM,SAA0B,EAAE;AAClC,MAAK,IAAI,IAAI,GAAG,IAAI,SAAS,QAAQ,IACnC,QAAO,KAAK;EAAE,OAAO,SAAS,MAAM,EAAE;EAAE,KAAK,SAAS,IAAI,EAAE;EAAE,CAAC;AAEjE,QAAO;;AAiBT,SAAS,eACP,SACA,EAAE,YAAY,gBACkB;AAChC,QAAO,IAAI,KAAK,OAAO,eAAe;EACpC,MAAM,MAAM,YAAY;AACxB,MAAI,WAAW,QAAS,QAAO;AAC/B,QAAM,cAAc,cAAc,QAAQ,KAAK;AAG/C,SAAO;GAAE,GAAG;GAAK,aAAa,QAAQ,KAAK;GAAS;GACpD;;AAGJ,SAAS,kBACP,SACA,EAAE,YAAY,cAAc,cACI;AAChC,QAAO,IAAI,KAAK,OAAO,eAAe;EACpC,MAAM,MAAM,YAAY;AACxB,MAAI,WAAW,QAAS,QAAO;EAE/B,MAAM,EAAE,SAAS;EAMjB,MAAM,WAAW,IAAI,SAAS,QAAQ,MAAM,KAAK,IAAI,EAAE,YAAY,KAAK,UAAU,IAAI,qBAAqB;AAM3G,MAAI,EAAE,QAAQ,gBAAgB,aAC5B,YAAW;GACT,GAAG;GACH,UAAU,CACR,GAAG,UACH;IACE,IAAI,KAAK;IACT,WAAW,KAAK;IAChB,UAAU,KAAK;IACf,SAAS,KAAK;IACd,GAAI,KAAK,mBAAmB,KAAA,KAAa,EAAE,gBAAgB,KAAK,gBAAgB;IAChF,SAAS;IACV,CACF;GACD,gBAAgB,IAAI;GACrB,CAAC;AAGJ,QAAM,cAAc,cAAc,QAAQ,MAAM,WAAW;AAG3D,SAAO;GACL,GAAG;GACH,UAAU,CACR,GAAG,UACH;IACE,IAAI,KAAK;IACT,WAAW,KAAK;IAChB,UAAU,KAAK;IACf,SAAS,KAAK;IACd,GAAI,KAAK,mBAAmB,KAAA,KAAa,EAAE,gBAAgB,KAAK,gBAAgB;IACjF,CACF;GACD,gBAAgB,iBAAiB,aAAa,SAAS;GACxD;GACD;;AAGJ,SAAS,WACP,SACA,EAAE,YAAY,gBACkB;AAChC,QAAO,IAAI,KAAK,OAAO,eAAe;EACpC,MAAM,MAAM,YAAY;AACxB,MAAI,WAAW,QAAS,QAAO;AAC/B,QAAM,YAAY,cAAc,QAAQ,OAAO,QAAQ,IAAI;EAQ3D,MAAM,iBAAiB,iBAAiB,aAAa,SAAS;EAC9D,MAAM,WAAW,IAAI,SAAS,QAAQ,MAAM;GAC1C,MAAM,WAAW,EAAE,YAAY,EAAE,WAAW;AAC5C,UAAO,eAAe,MAAM,MAAM,YAAY,EAAE,SAAS,WAAW,EAAE,IAAI;IAC1E;AACF,SAAO;GAAE,GAAG;GAAK,UAAU;GAAU;GAAgB;GACrD;;AAQJ,MAAM,uBAAuB;CAC3B,eAAe;CACf,kBAAkB;CAClB,QAAQ;CACT;AAID,SAAS,cACP,SACA,SACgC;CAChC,MAAM,UAAU,qBAAqB,QAAQ;AAC7C,QAAO,QAAQ,SAAS,QAAQ;;AAOlC,SAAgB,wBACd,cACA,gBACmB;CAGnB,MAAM,eAAe,MAAqB;AACxC,MAAI,EAAE,aAAa,SAAS,EAAE,SAAS,cACrC,SAAQ,MAAM,kCAAkC,EAAE;;CAMtD,MAAM,aAAa,KAAoC,EAAE,YAAY,YAAY,YAAY,aAAwB;AACnH,aAAW,WAAW;EACtB,MAAM,OAAO,cAAc,KAAK;GAAE;GAAY;GAAc;GAAY,CAAC;AACzE,SAAO,SAAS,KAAK,CAAC,KAAK,YAAY,YAAY;;AAGrD,QAAO,mBAAiG;EACtG,cAAc,IAAI,cAAc;EAChC,SAAS;EACT,SAAS;GAAE,UAAU,EAAE;GAAE,gBAAgB,EAAE;GAAE,aAAa,KAAA;GAAW,GAAG;GAAgB;EACxF,QAAQ;GACN,MAAM,EACJ,IAAI;IACF,eAAe;IACf,kBAAkB;IAClB,QAAQ;IACR,QAAQ,KAAK,EAAE,YAAY,YAAY,YAAY,aAAa;KAC9D,MAAM,EAAE,aAAa;AACrB,SAAI,SAAS,WAAW,EAAG;AAE3B,gBAAW,WAAW;AACtB,cAAS,SAAS,QAAQ;MACxB,MAAM,OAAO,cAAc,KAAK;OAAE;OAAY;OAAc;OAAY,CAAC;AACzE,aAAO,SAAS,KAAK,CAAC,KAAK,YAAY,YAAY;OACnD;;IAEL,EACF;GACD,UAAU;IAER,WAAW;IACX,IAAI,EAGF,SAAS,GAAG,EAAE,aAAa;AACzB,YAAO,UAAU;OAEpB;IACF;GACF;EACF,CAAC"}
1
+ {"version":3,"file":"source-buffer.js","names":[],"sources":["../../../../../src/playback/actors/dom/source-buffer.ts"],"sourcesContent":["import { createMachineActor, type HandlerContext, type MessageActor } from '../../../core/actors/create-machine-actor';\nimport { SerialRunner, Task } from '../../../core/tasks/task';\nimport { type AppendData, appendSegment } from '../../../media/dom/mse/append-segment';\nimport { flushBuffer } from '../../../media/dom/mse/buffer-flusher';\nimport { SEGMENT_TIME_EPSILON, type Segment, type Track } from '../../../media/types';\n\n// =============================================================================\n// Types\n// =============================================================================\n\nexport interface BufferedRange {\n start: number;\n end: number;\n}\n\nexport type AppendSegmentMeta = Pick<Segment, 'id' | 'startTime' | 'duration'> & {\n trackId: Track['id'];\n /** Declared track bandwidth in bps (from playlist BANDWIDTH attribute). */\n trackBandwidth?: number;\n};\n\nexport type { AppendData };\n\nexport type AppendInitMessage = {\n type: 'append-init';\n data: AppendData;\n /**\n * `language` is captured alongside `trackId` so downstream loaders can\n * compare the buffered track's language to the newly-selected track's\n * language and decide whether ahead-buffer flush is warranted on track\n * switch (see `segment-loader`'s `planTasks`). Undefined for video and\n * for audio without explicit `LANGUAGE` attribute.\n */\n meta: { trackId: Track['id']; language?: string };\n};\nexport type AppendSegmentMessage = { type: 'append-segment'; data: AppendData; meta: AppendSegmentMeta };\nexport type RemoveMessage = { type: 'remove'; start: number; end: number };\nexport type IndividualSourceBufferMessage = AppendInitMessage | AppendSegmentMessage | RemoveMessage;\nexport type BatchMessage = { type: 'batch'; messages: IndividualSourceBufferMessage[] };\nexport type CancelMessage = { type: 'cancel' };\n\n/** All messages accepted by a SourceBufferActor. */\nexport type SourceBufferMessage = IndividualSourceBufferMessage | BatchMessage | CancelMessage;\n\n/** Finite states of the actor. */\nexport type SourceBufferActorState = 'idle' | 'updating' | 'destroyed';\n\n/** Non-finite (extended) data managed by the actor — the XState \"context\". */\nexport interface SourceBufferActorContext {\n initTrackId?: string | undefined;\n /**\n * Language of the most recently appended init segment's track (when\n * present on the playlist). Used by the segment-loader's `planTasks`\n * to detect cross-language switches and schedule ahead-buffer flush.\n * Undefined for video and for language-less audio.\n */\n initTrackLanguage?: string | undefined;\n segments: Array<\n Pick<Segment, 'id' | 'startTime' | 'duration'> & {\n trackId: Track['id'];\n trackBandwidth?: number;\n /**\n * True while a streaming append is in progress for this segment.\n * The segment's data is partially present in the SourceBuffer.\n * Downstream code must not treat a partial segment as fully buffered.\n */\n partial?: boolean;\n }\n >;\n bufferedRanges: BufferedRange[];\n}\n\n/** SourceBuffer actor: queues operations, owns its snapshot. */\nexport type SourceBufferActor = MessageActor<SourceBufferActorState, SourceBufferActorContext, SourceBufferMessage>;\n\n// =============================================================================\n// Helpers\n// =============================================================================\n\nfunction snapshotBuffered(buffered: TimeRanges): BufferedRange[] {\n const ranges: BufferedRange[] = [];\n for (let i = 0; i < buffered.length; i++) {\n ranges.push({ start: buffered.start(i), end: buffered.end(i) });\n }\n return ranges;\n}\n\n// =============================================================================\n// Message task factories\n// =============================================================================\n\n// Context is read lazily via getContext at task execution time — not at creation\n// time — so each task always operates on the most recent context regardless of\n// when it was scheduled.\n\ninterface MessageTaskOptions {\n getContext: () => SourceBufferActorContext;\n sourceBuffer: SourceBuffer;\n setContext: (ctx: SourceBufferActorContext) => void;\n}\n\nfunction appendInitTask(\n message: AppendInitMessage,\n { getContext, sourceBuffer }: MessageTaskOptions\n): Task<SourceBufferActorContext> {\n return new Task(async (taskSignal) => {\n const ctx = getContext();\n if (taskSignal.aborted) return ctx;\n await appendSegment(sourceBuffer, message.data);\n // No abort check here: the physical SourceBuffer has been modified, so\n // the model must be updated to match regardless of signal state.\n return { ...ctx, initTrackId: message.meta.trackId, initTrackLanguage: message.meta.language };\n });\n}\n\nfunction appendSegmentTask(\n message: AppendSegmentMessage,\n { getContext, sourceBuffer, setContext }: MessageTaskOptions\n): Task<SourceBufferActorContext> {\n return new Task(async (taskSignal) => {\n const ctx = getContext();\n if (taskSignal.aborted) return ctx;\n\n const { meta } = message;\n // Remove any existing entry at the same start time (same \"slot\" in the\n // timeline), then record the new segment. Assumes time-aligned segments\n // across playlists. `SEGMENT_TIME_EPSILON` guards against floating-point\n // drift in parsed timestamps (shared with the segment-loader quality\n // filter — single source of truth).\n const filtered = ctx.segments.filter((s) => Math.abs(s.startTime - meta.startTime) >= SEGMENT_TIME_EPSILON);\n\n // For streaming data: emit partial state before the first chunk so\n // downstream code can see the in-progress segment and treat it as\n // incomplete. ArrayBuffer appends are atomic so no partial state is\n // needed — context is updated once at task completion.\n if (!(message.data instanceof ArrayBuffer)) {\n setContext({\n ...ctx,\n segments: [\n ...filtered,\n {\n id: meta.id,\n startTime: meta.startTime,\n duration: meta.duration,\n trackId: meta.trackId,\n ...(meta.trackBandwidth !== undefined && { trackBandwidth: meta.trackBandwidth }),\n partial: true,\n },\n ],\n bufferedRanges: ctx.bufferedRanges,\n });\n }\n\n await appendSegment(sourceBuffer, message.data, taskSignal);\n // No abort check here: the physical SourceBuffer has been modified, so\n // the model must be updated to match regardless of signal state.\n return {\n ...ctx,\n segments: [\n ...filtered,\n {\n id: meta.id,\n startTime: meta.startTime,\n duration: meta.duration,\n trackId: meta.trackId,\n ...(meta.trackBandwidth !== undefined && { trackBandwidth: meta.trackBandwidth }),\n },\n ],\n bufferedRanges: snapshotBuffered(sourceBuffer.buffered),\n };\n });\n}\n\nfunction removeTask(\n message: RemoveMessage,\n { getContext, sourceBuffer }: MessageTaskOptions\n): Task<SourceBufferActorContext> {\n return new Task(async (taskSignal) => {\n const ctx = getContext();\n if (taskSignal.aborted) return ctx;\n await flushBuffer(sourceBuffer, message.start, message.end);\n // No abort check here: the physical SourceBuffer has been modified, so\n // the model must be updated to match regardless of signal state.\n //\n // Use the post-flush buffered ranges as ground truth. A segment is kept\n // in the model only if its midpoint falls within a buffered range.\n // Midpoint-based membership handles flush boundaries that don't align\n // exactly with segment edges without over-removing adjacent segments.\n const bufferedRanges = snapshotBuffered(sourceBuffer.buffered);\n const filtered = ctx.segments.filter((s) => {\n const midpoint = s.startTime + s.duration / 2;\n return bufferedRanges.some((r) => midpoint >= r.start && midpoint < r.end);\n });\n return { ...ctx, segments: filtered, bufferedRanges };\n });\n}\n\ntype MessageTaskFactory<T extends IndividualSourceBufferMessage> = (\n message: T,\n options: MessageTaskOptions\n) => Task<SourceBufferActorContext>;\n\nconst messageTaskFactories = {\n 'append-init': appendInitTask,\n 'append-segment': appendSegmentTask,\n remove: removeTask,\n} satisfies {\n [K in IndividualSourceBufferMessage['type']]: MessageTaskFactory<Extract<IndividualSourceBufferMessage, { type: K }>>;\n};\n\nfunction messageToTask(\n message: IndividualSourceBufferMessage,\n options: MessageTaskOptions\n): Task<SourceBufferActorContext> {\n const factory = messageTaskFactories[message.type] as MessageTaskFactory<typeof message>;\n return factory(message, options);\n}\n\n// =============================================================================\n// Implementation\n// =============================================================================\n\nexport function createSourceBufferActor(\n sourceBuffer: SourceBuffer,\n initialContext?: Partial<SourceBufferActorContext>\n): SourceBufferActor {\n type UserState = Exclude<SourceBufferActorState, 'destroyed'>;\n\n const handleError = (e: unknown): void => {\n if (!(e instanceof Error && e.name === 'AbortError')) {\n console.error('SourceBuffer operation failed:', e);\n }\n };\n\n type Ctx = HandlerContext<UserState, SourceBufferActorContext, () => SerialRunner>;\n\n const onMessage = (msg: IndividualSourceBufferMessage, { transition, setContext, getContext, runner }: Ctx): void => {\n transition('updating');\n const task = messageToTask(msg, { getContext, sourceBuffer, setContext });\n runner.schedule(task).then(setContext, handleError);\n };\n\n return createMachineActor<UserState, SourceBufferActorContext, SourceBufferMessage, () => SerialRunner>({\n runner: () => new SerialRunner(),\n initial: 'idle',\n context: { segments: [], bufferedRanges: [], initTrackId: undefined, ...initialContext },\n states: {\n idle: {\n on: {\n 'append-init': onMessage,\n 'append-segment': onMessage,\n remove: onMessage,\n batch: (msg, { transition, setContext, getContext, runner }) => {\n const { messages } = msg;\n if (messages.length === 0) return;\n\n transition('updating');\n messages.forEach((msg) => {\n const task = messageToTask(msg, { getContext, sourceBuffer, setContext });\n runner.schedule(task).then(setContext, handleError);\n });\n },\n },\n },\n updating: {\n // Automatically return to idle once all scheduled tasks settle.\n onSettled: 'idle',\n on: {\n // Abort all in-progress and pending tasks. onSettled handles → 'idle'\n // once the aborted tasks drain.\n cancel: (_, { runner }) => {\n runner.abortAll();\n },\n },\n },\n },\n });\n}\n"],"mappings":";;;;;;AA+EA,SAAS,iBAAiB,UAAuC;CAC/D,MAAM,SAA0B,EAAE;AAClC,MAAK,IAAI,IAAI,GAAG,IAAI,SAAS,QAAQ,IACnC,QAAO,KAAK;EAAE,OAAO,SAAS,MAAM,EAAE;EAAE,KAAK,SAAS,IAAI,EAAE;EAAE,CAAC;AAEjE,QAAO;;AAiBT,SAAS,eACP,SACA,EAAE,YAAY,gBACkB;AAChC,QAAO,IAAI,KAAK,OAAO,eAAe;EACpC,MAAM,MAAM,YAAY;AACxB,MAAI,WAAW,QAAS,QAAO;AAC/B,QAAM,cAAc,cAAc,QAAQ,KAAK;AAG/C,SAAO;GAAE,GAAG;GAAK,aAAa,QAAQ,KAAK;GAAS,mBAAmB,QAAQ,KAAK;GAAU;GAC9F;;AAGJ,SAAS,kBACP,SACA,EAAE,YAAY,cAAc,cACI;AAChC,QAAO,IAAI,KAAK,OAAO,eAAe;EACpC,MAAM,MAAM,YAAY;AACxB,MAAI,WAAW,QAAS,QAAO;EAE/B,MAAM,EAAE,SAAS;EAMjB,MAAM,WAAW,IAAI,SAAS,QAAQ,MAAM,KAAK,IAAI,EAAE,YAAY,KAAK,UAAU,IAAI,qBAAqB;AAM3G,MAAI,EAAE,QAAQ,gBAAgB,aAC5B,YAAW;GACT,GAAG;GACH,UAAU,CACR,GAAG,UACH;IACE,IAAI,KAAK;IACT,WAAW,KAAK;IAChB,UAAU,KAAK;IACf,SAAS,KAAK;IACd,GAAI,KAAK,mBAAmB,KAAA,KAAa,EAAE,gBAAgB,KAAK,gBAAgB;IAChF,SAAS;IACV,CACF;GACD,gBAAgB,IAAI;GACrB,CAAC;AAGJ,QAAM,cAAc,cAAc,QAAQ,MAAM,WAAW;AAG3D,SAAO;GACL,GAAG;GACH,UAAU,CACR,GAAG,UACH;IACE,IAAI,KAAK;IACT,WAAW,KAAK;IAChB,UAAU,KAAK;IACf,SAAS,KAAK;IACd,GAAI,KAAK,mBAAmB,KAAA,KAAa,EAAE,gBAAgB,KAAK,gBAAgB;IACjF,CACF;GACD,gBAAgB,iBAAiB,aAAa,SAAS;GACxD;GACD;;AAGJ,SAAS,WACP,SACA,EAAE,YAAY,gBACkB;AAChC,QAAO,IAAI,KAAK,OAAO,eAAe;EACpC,MAAM,MAAM,YAAY;AACxB,MAAI,WAAW,QAAS,QAAO;AAC/B,QAAM,YAAY,cAAc,QAAQ,OAAO,QAAQ,IAAI;EAQ3D,MAAM,iBAAiB,iBAAiB,aAAa,SAAS;EAC9D,MAAM,WAAW,IAAI,SAAS,QAAQ,MAAM;GAC1C,MAAM,WAAW,EAAE,YAAY,EAAE,WAAW;AAC5C,UAAO,eAAe,MAAM,MAAM,YAAY,EAAE,SAAS,WAAW,EAAE,IAAI;IAC1E;AACF,SAAO;GAAE,GAAG;GAAK,UAAU;GAAU;GAAgB;GACrD;;AAQJ,MAAM,uBAAuB;CAC3B,eAAe;CACf,kBAAkB;CAClB,QAAQ;CACT;AAID,SAAS,cACP,SACA,SACgC;CAChC,MAAM,UAAU,qBAAqB,QAAQ;AAC7C,QAAO,QAAQ,SAAS,QAAQ;;AAOlC,SAAgB,wBACd,cACA,gBACmB;CAGnB,MAAM,eAAe,MAAqB;AACxC,MAAI,EAAE,aAAa,SAAS,EAAE,SAAS,cACrC,SAAQ,MAAM,kCAAkC,EAAE;;CAMtD,MAAM,aAAa,KAAoC,EAAE,YAAY,YAAY,YAAY,aAAwB;AACnH,aAAW,WAAW;EACtB,MAAM,OAAO,cAAc,KAAK;GAAE;GAAY;GAAc;GAAY,CAAC;AACzE,SAAO,SAAS,KAAK,CAAC,KAAK,YAAY,YAAY;;AAGrD,QAAO,mBAAiG;EACtG,cAAc,IAAI,cAAc;EAChC,SAAS;EACT,SAAS;GAAE,UAAU,EAAE;GAAE,gBAAgB,EAAE;GAAE,aAAa,KAAA;GAAW,GAAG;GAAgB;EACxF,QAAQ;GACN,MAAM,EACJ,IAAI;IACF,eAAe;IACf,kBAAkB;IAClB,QAAQ;IACR,QAAQ,KAAK,EAAE,YAAY,YAAY,YAAY,aAAa;KAC9D,MAAM,EAAE,aAAa;AACrB,SAAI,SAAS,WAAW,EAAG;AAE3B,gBAAW,WAAW;AACtB,cAAS,SAAS,QAAQ;MACxB,MAAM,OAAO,cAAc,KAAK;OAAE;OAAY;OAAc;OAAY,CAAC;AACzE,aAAO,SAAS,KAAK,CAAC,KAAK,YAAY,YAAY;OACnD;;IAEL,EACF;GACD,UAAU;IAER,WAAW;IACX,IAAI,EAGF,SAAS,GAAG,EAAE,aAAa;AACzB,YAAO,UAAU;OAEpB;IACF;GACF;EACF,CAAC"}
@@ -1,6 +1,14 @@
1
1
  import { MaybeResolvedPresentation } from "../../media/types/index.js";
2
2
 
3
3
  //#region src/playback/behaviors/calculate-presentation-duration.d.ts
4
+ /**
5
+ * Input shape passed to the duration resolver. Represents the union of
6
+ * track-selection slots the default `getResolvedSelectedTrackDuration`
7
+ * resolver inspects to pick a representative resolved track. Variants
8
+ * that compose neither audio nor video selection still satisfy this
9
+ * shape — the missing fields read as `undefined` and the resolver
10
+ * falls through.
11
+ */
4
12
  interface PresentationDurationState {
5
13
  presentation?: MaybeResolvedPresentation;
6
14
  selectedVideoTrackId?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"calculate-presentation-duration.d.ts","names":[],"sources":["../../../../src/playback/behaviors/calculate-presentation-duration.ts"],"mappings":";;;UAkCiB,yBAAA;EACf,YAAA,GAAe,yBAAA;EACf,oBAAA;EACA,oBAAA;AAAA;;;;;;KAQU,4BAAA,IAAgC,KAAA,EAAO,yBAAA"}
1
+ {"version":3,"file":"calculate-presentation-duration.d.ts","names":[],"sources":["../../../../src/playback/behaviors/calculate-presentation-duration.ts"],"mappings":";;;;;;;;;;;UA0CiB,yBAAA;EACf,YAAA,GAAe,yBAAA;EACf,oBAAA;EACA,oBAAA;AAAA;;;;;;KAQU,4BAAA,IAAgC,KAAA,EAAO,yBAAA"}
@@ -1,57 +1,35 @@
1
- import { snapshot, untrack, update } from "../../core/signals/primitives.js";
2
- import { defineBehavior } from "../../core/composition/create-composition.js";
1
+ import { untrack, update } from "../../core/signals/primitives.js";
3
2
  import { effect } from "../../core/signals/effect.js";
4
3
  //#region src/playback/behaviors/calculate-presentation-duration.ts
5
- /**
6
- * **Populate presentation duration via a config-supplied resolver.**
7
- *
8
- * Once a presentation is in place and its `duration` is still undefined,
9
- * calls `config.resolveDuration(state)` whenever a tracked slot changes. The
10
- * resolver decides what counts as "duration is now derivable" for the variant
11
- * in play; the behavior itself stays variant-agnostic:
12
- *
13
- * - **VoD** — derive from the first resolved selected track's `duration`
14
- * (video preferred, audio fallback). The HLS engine wires
15
- * `getResolvedSelectedTrackDuration` from `media/utils/track-selection.ts`
16
- * as the default. Audio-only falls out of the same resolver naturally
17
- * (no video selected → audio is the first resolved track).
18
- * - **Live** — return `Number.POSITIVE_INFINITY` once the presentation is
19
- * established as live. This is the MSE-spec value for `mediaSource.duration`
20
- * under live playback; downstream `updateMediaSourceDuration` propagates it through.
21
- *
22
- * Validation: writes whatever the resolver returns as long as it's a positive
23
- * number, including `Infinity`. `undefined` / `NaN` / `<= 0` are skipped.
24
- *
25
- * Fires at most once per presentation — an already-set `duration` is never
26
- * overwritten, and the next reset arrives structurally when a new
27
- * (unresolved) presentation replaces the current one. The resolver may
28
- * return `undefined` while duration is still indeterminate; subsequent
29
- * tracked-slot changes re-run the effect until the resolver commits a value.
30
- *
31
- * Downstream of `resolveVideoTrack` / `resolveAudioTrack`; upstream of
32
- * `updateMediaSourceDuration` (which writes the value through to `mediaSource.duration`).
33
- */
34
4
  function calculatePresentationDurationSetup({ state, config }) {
35
5
  return effect(() => {
36
6
  const presentation = state.presentation.get();
37
7
  if (!presentation || presentation.duration !== void 0) return;
38
- const duration = config.resolveDuration(untrack(() => snapshot(state)));
8
+ const resolverInput = untrack(() => ({
9
+ presentation,
10
+ selectedVideoTrackId: state.selectedVideoTrackId?.get(),
11
+ selectedAudioTrackId: state.selectedAudioTrackId?.get()
12
+ }));
13
+ const duration = config.resolveDuration(resolverInput);
39
14
  if (duration === void 0 || Number.isNaN(duration) || duration <= 0) return;
40
- update(state.presentation, (current) => current ? {
41
- ...current,
42
- duration
43
- } : current);
15
+ update(state.presentation, { duration });
44
16
  });
45
17
  }
46
- const calculatePresentationDuration = defineBehavior({
47
- stateKeys: [
48
- "presentation",
49
- "selectedVideoTrackId",
50
- "selectedAudioTrackId"
51
- ],
18
+ /**
19
+ * `calculatePresentationDuration` uses a manual `Behavior<>` literal
20
+ * (rather than `defineBehavior`) so it can declare just `presentation`
21
+ * in its stateKeys while the typed setup-param shape includes the
22
+ * optional `selectedVideoTrackId` / `selectedAudioTrackId` reads used
23
+ * at runtime. Mirrors the pattern in `endOfStream` for the same
24
+ * reason: the behavior is uniform-across-tracks and reads slots
25
+ * contributed by other behaviors, so it shouldn't leak those slot
26
+ * declarations into variants that don't compose the contributors.
27
+ */
28
+ const calculatePresentationDuration = {
29
+ stateKeys: ["presentation"],
52
30
  contextKeys: [],
53
31
  setup: calculatePresentationDurationSetup
54
- });
32
+ };
55
33
  //#endregion
56
34
  export { calculatePresentationDuration };
57
35
 
@@ -1 +1 @@
1
- {"version":3,"file":"calculate-presentation-duration.js","names":[],"sources":["../../../../src/playback/behaviors/calculate-presentation-duration.ts"],"sourcesContent":["/**\n * **Populate presentation duration via a config-supplied resolver.**\n *\n * Once a presentation is in place and its `duration` is still undefined,\n * calls `config.resolveDuration(state)` whenever a tracked slot changes. The\n * resolver decides what counts as \"duration is now derivable\" for the variant\n * in play; the behavior itself stays variant-agnostic:\n *\n * - **VoD** — derive from the first resolved selected track's `duration`\n * (video preferred, audio fallback). The HLS engine wires\n * `getResolvedSelectedTrackDuration` from `media/utils/track-selection.ts`\n * as the default. Audio-only falls out of the same resolver naturally\n * (no video selected → audio is the first resolved track).\n * - **Live** — return `Number.POSITIVE_INFINITY` once the presentation is\n * established as live. This is the MSE-spec value for `mediaSource.duration`\n * under live playback; downstream `updateMediaSourceDuration` propagates it through.\n *\n * Validation: writes whatever the resolver returns as long as it's a positive\n * number, including `Infinity`. `undefined` / `NaN` / `<= 0` are skipped.\n *\n * Fires at most once per presentation — an already-set `duration` is never\n * overwritten, and the next reset arrives structurally when a new\n * (unresolved) presentation replaces the current one. The resolver may\n * return `undefined` while duration is still indeterminate; subsequent\n * tracked-slot changes re-run the effect until the resolver commits a value.\n *\n * Downstream of `resolveVideoTrack` / `resolveAudioTrack`; upstream of\n * `updateMediaSourceDuration` (which writes the value through to `mediaSource.duration`).\n */\nimport { defineBehavior } from '../../core/composition/create-composition';\nimport { effect } from '../../core/signals/effect';\nimport { type ReadonlySignal, type Signal, snapshot, untrack, update } from '../../core/signals/primitives';\nimport type { MaybeResolvedPresentation } from '../../media/types';\n\nexport interface PresentationDurationState {\n presentation?: MaybeResolvedPresentation;\n selectedVideoTrackId?: string;\n selectedAudioTrackId?: string;\n}\n\n/**\n * Resolver supplied via `config.resolveDuration`. Returns the duration to\n * write to `presentation.duration`, or `undefined` when it isn't yet\n * derivable. Positive `Infinity` is the canonical live value.\n */\nexport type PresentationDurationResolver = (state: PresentationDurationState) => number | undefined;\n\nexport interface PresentationDurationConfig {\n resolveDuration: PresentationDurationResolver;\n}\n\nfunction calculatePresentationDurationSetup({\n state,\n config,\n}: {\n state: {\n presentation: Signal<PresentationDurationState['presentation']>;\n selectedVideoTrackId: ReadonlySignal<PresentationDurationState['selectedVideoTrackId']>;\n selectedAudioTrackId: ReadonlySignal<PresentationDurationState['selectedAudioTrackId']>;\n };\n config: PresentationDurationConfig;\n}): () => void {\n return effect(() => {\n const presentation = state.presentation.get();\n if (!presentation || presentation.duration !== undefined) return;\n\n // presentation drives the effect; selection-only changes can't yield a\n // writable duration on their own (resolver returns undefined until a\n // track resolves, at which point resolve-track writes back through\n // state.presentation anyway), so we read the rest untracked.\n const duration = config.resolveDuration(untrack(() => snapshot(state)));\n if (duration === undefined || Number.isNaN(duration) || duration <= 0) return;\n\n update(state.presentation, (current) => (current ? { ...current, duration } : current));\n });\n}\n\nexport const calculatePresentationDuration = defineBehavior({\n stateKeys: ['presentation', 'selectedVideoTrackId', 'selectedAudioTrackId'],\n contextKeys: [],\n setup: calculatePresentationDurationSetup,\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmDA,SAAS,mCAAmC,EAC1C,OACA,UAQa;AACb,QAAO,aAAa;EAClB,MAAM,eAAe,MAAM,aAAa,KAAK;AAC7C,MAAI,CAAC,gBAAgB,aAAa,aAAa,KAAA,EAAW;EAM1D,MAAM,WAAW,OAAO,gBAAgB,cAAc,SAAS,MAAM,CAAC,CAAC;AACvE,MAAI,aAAa,KAAA,KAAa,OAAO,MAAM,SAAS,IAAI,YAAY,EAAG;AAEvE,SAAO,MAAM,eAAe,YAAa,UAAU;GAAE,GAAG;GAAS;GAAU,GAAG,QAAS;GACvF;;AAGJ,MAAa,gCAAgC,eAAe;CAC1D,WAAW;EAAC;EAAgB;EAAwB;EAAuB;CAC3E,aAAa,EAAE;CACf,OAAO;CACR,CAAC"}
1
+ {"version":3,"file":"calculate-presentation-duration.js","names":[],"sources":["../../../../src/playback/behaviors/calculate-presentation-duration.ts"],"sourcesContent":["/**\n * **Populate presentation duration via a config-supplied resolver.**\n *\n * Once a presentation is in place and its `duration` is still undefined,\n * calls `config.resolveDuration(state)` whenever a tracked slot changes. The\n * resolver decides what counts as \"duration is now derivable\" for the variant\n * in play; the behavior itself stays variant-agnostic:\n *\n * - **VoD** — derive from the first resolved selected track's `duration`\n * (video preferred, audio fallback). The HLS engine wires\n * `getResolvedSelectedTrackDuration` from `media/utils/track-selection.ts`\n * as the default. Audio-only falls out of the same resolver naturally\n * (no video selected → audio is the first resolved track).\n * - **Live** — return `Number.POSITIVE_INFINITY` once the presentation is\n * established as live. This is the MSE-spec value for `mediaSource.duration`\n * under live playback; downstream `updateMediaSourceDuration` propagates it through.\n *\n * Validation: writes whatever the resolver returns as long as it's a positive\n * number, including `Infinity`. `undefined` / `NaN` / `<= 0` are skipped.\n *\n * Fires at most once per presentation — an already-set `duration` is never\n * overwritten, and the next reset arrives structurally when a new\n * (unresolved) presentation replaces the current one. The resolver may\n * return `undefined` while duration is still indeterminate; subsequent\n * tracked-slot changes re-run the effect until the resolver commits a value.\n *\n * Downstream of `resolveVideoTrack` / `resolveAudioTrack`; upstream of\n * `updateMediaSourceDuration` (which writes the value through to `mediaSource.duration`).\n */\nimport type { Behavior } from '../../core/composition/create-composition';\nimport { effect } from '../../core/signals/effect';\nimport { type ReadonlySignal, type Signal, untrack, update } from '../../core/signals/primitives';\nimport type { MaybeResolvedPresentation } from '../../media/types';\n\n/**\n * Input shape passed to the duration resolver. Represents the union of\n * track-selection slots the default `getResolvedSelectedTrackDuration`\n * resolver inspects to pick a representative resolved track. Variants\n * that compose neither audio nor video selection still satisfy this\n * shape — the missing fields read as `undefined` and the resolver\n * falls through.\n */\nexport interface PresentationDurationState {\n presentation?: MaybeResolvedPresentation;\n selectedVideoTrackId?: string;\n selectedAudioTrackId?: string;\n}\n\n/**\n * Resolver supplied via `config.resolveDuration`. Returns the duration to\n * write to `presentation.duration`, or `undefined` when it isn't yet\n * derivable. Positive `Infinity` is the canonical live value.\n */\nexport type PresentationDurationResolver = (state: PresentationDurationState) => number | undefined;\n\nexport interface PresentationDurationConfig {\n resolveDuration: PresentationDurationResolver;\n}\n\nfunction calculatePresentationDurationSetup({\n state,\n config,\n}: {\n state: {\n presentation: Signal<PresentationDurationState['presentation']>;\n // selectedVideoTrackId / selectedAudioTrackId are read defensively —\n // they are *not* declared in this behavior's stateKeys. The slots are\n // contributed by other behaviors (`switchVideoTrack` writes the\n // video slot in the default engine; `selectAudioTrack` writes the\n // audio slot) which compose conditionally per engine variant. Treating\n // each signal as optional lets calculatePresentationDuration stay\n // variant-agnostic — it feeds whatever's present to the resolver and\n // lets the resolver decide.\n selectedVideoTrackId?: ReadonlySignal<PresentationDurationState['selectedVideoTrackId']>;\n selectedAudioTrackId?: ReadonlySignal<PresentationDurationState['selectedAudioTrackId']>;\n };\n config: PresentationDurationConfig;\n}): () => void {\n return effect(() => {\n const presentation = state.presentation.get();\n if (!presentation || presentation.duration !== undefined) return;\n\n // presentation drives the effect; selection-only changes can't yield a\n // writable duration on their own (resolver returns undefined until a\n // track resolves, at which point resolve-track writes back through\n // state.presentation anyway), so we read the rest untracked.\n const resolverInput: PresentationDurationState = untrack(() => ({\n presentation,\n selectedVideoTrackId: state.selectedVideoTrackId?.get(),\n selectedAudioTrackId: state.selectedAudioTrackId?.get(),\n }));\n const duration = config.resolveDuration(resolverInput);\n if (duration === undefined || Number.isNaN(duration) || duration <= 0) return;\n\n // Patch-object form requires `T extends object`; `state.presentation`'s\n // T is `MaybeResolvedPresentation | undefined`, which doesn't satisfy\n // the constraint. The guard at the top of the effect already\n // established that the slot holds a defined value, so cast to narrow\n // the signal type and use the cleaner merge form.\n update(state.presentation as Signal<MaybeResolvedPresentation>, { duration });\n });\n}\n\n/**\n * `calculatePresentationDuration` uses a manual `Behavior<>` literal\n * (rather than `defineBehavior`) so it can declare just `presentation`\n * in its stateKeys while the typed setup-param shape includes the\n * optional `selectedVideoTrackId` / `selectedAudioTrackId` reads used\n * at runtime. Mirrors the pattern in `endOfStream` for the same\n * reason: the behavior is uniform-across-tracks and reads slots\n * contributed by other behaviors, so it shouldn't leak those slot\n * declarations into variants that don't compose the contributors.\n */\nexport const calculatePresentationDuration: Behavior<\n { presentation: Signal<PresentationDurationState['presentation']> },\n Record<string, never>,\n PresentationDurationConfig\n> = {\n stateKeys: ['presentation'],\n contextKeys: [],\n setup: calculatePresentationDurationSetup,\n};\n"],"mappings":";;;AA2DA,SAAS,mCAAmC,EAC1C,OACA,UAgBa;AACb,QAAO,aAAa;EAClB,MAAM,eAAe,MAAM,aAAa,KAAK;AAC7C,MAAI,CAAC,gBAAgB,aAAa,aAAa,KAAA,EAAW;EAM1D,MAAM,gBAA2C,eAAe;GAC9D;GACA,sBAAsB,MAAM,sBAAsB,KAAK;GACvD,sBAAsB,MAAM,sBAAsB,KAAK;GACxD,EAAE;EACH,MAAM,WAAW,OAAO,gBAAgB,cAAc;AACtD,MAAI,aAAa,KAAA,KAAa,OAAO,MAAM,SAAS,IAAI,YAAY,EAAG;AAOvE,SAAO,MAAM,cAAmD,EAAE,UAAU,CAAC;GAC7E;;;;;;;;;;;;AAaJ,MAAa,gCAIT;CACF,WAAW,CAAC,eAAe;CAC3B,aAAa,EAAE;CACf,OAAO;CACR"}
@@ -0,0 +1,68 @@
1
+ import { computed, peek } from "../../core/signals/primitives.js";
2
+ import { defineBehavior } from "../../core/composition/create-composition.js";
3
+ import { createMachineReactor } from "../../core/reactors/create-machine-reactor.js";
4
+ import { isResolvedPresentation } from "../../media/types/index.js";
5
+ import { getCdnId, getOrderedCdnIds } from "../../media/utils/cdn.js";
6
+ //#region src/playback/behaviors/derive-cdn-priority.ts
7
+ /**
8
+ * **Session-level CDN priority.** While a presentation is resolved, owns the
9
+ * `cdnPriority` signal: the distinct CDNs the source is served from (origin of
10
+ * each track's URL), in manifest priority order — most-preferred first. Cleared
11
+ * on src unload. The name mirrors HLS content steering's `PATHWAY-PRIORITY`.
12
+ *
13
+ * Redundant-stream sources (e.g. Mux's `?redundant_streams=true`) list the same
14
+ * content on multiple hosts, so the candidate tracks already include one variant
15
+ * per CDN. This behavior publishes *which* CDNs exist and their priority; the
16
+ * `preferActiveCdn` scope rule in `track-switching` reads `cdnPriority` and
17
+ * narrows each type's candidates to the first CDN with surviving tracks — so
18
+ * video / audio / text all resolve from one host (the shared list is the
19
+ * per-presentation coherence guarantee).
20
+ *
21
+ * The "active" CDN is not stored — it's derived by the scope as the
22
+ * highest-priority entry in `cdnPriority` that still has tracks after the
23
+ * constraints pre-pass. That makes failover a pure consequence of the (future)
24
+ * failed-CDN constraint: when the primary's tracks are pruned during cooldown,
25
+ * the scope falls to the next CDN; when the primary recovers, it snaps back.
26
+ * Content steering, when it lands, reorders `cdnPriority` (pathway priority as a
27
+ * sort key).
28
+ *
29
+ * Lifecycle: `'presentation-unresolved'` ↔ `'presentation-resolved'`, mirroring
30
+ * `setupTrackSwitching`. The resolved state owns the signal; its entry-returned
31
+ * cleanup clears it on exit (canonical cleanup-binds-to-setup per `reactors.md`).
32
+ */
33
+ const samePriority = (a, b) => !!a && a.length === b.length && a.every((cdn, i) => cdn === b[i]);
34
+ /**
35
+ * Manage `cdnPriority`: publish the manifest-ordered CDN list on src load, clear
36
+ * on src unload.
37
+ *
38
+ * @example
39
+ * const reactor = deriveCdnPriority.setup({ state });
40
+ */
41
+ const deriveCdnPriority = defineBehavior({
42
+ stateKeys: ["presentation", "cdnPriority"],
43
+ contextKeys: [],
44
+ setup: ({ state, config = {} }) => {
45
+ const getCdnId$1 = config.getCdnId ?? getCdnId;
46
+ const derivedStateSignal = computed(() => isResolvedPresentation(state.presentation.get()) ? "presentation-resolved" : "presentation-unresolved");
47
+ return createMachineReactor({
48
+ initial: "presentation-unresolved",
49
+ monitor: () => derivedStateSignal.get(),
50
+ states: {
51
+ "presentation-unresolved": {},
52
+ "presentation-resolved": {
53
+ entry: () => () => state.cdnPriority.set(void 0),
54
+ effects: [() => {
55
+ const presentation = state.presentation.get();
56
+ if (!isResolvedPresentation(presentation)) return;
57
+ const next = getOrderedCdnIds(presentation, getCdnId$1);
58
+ if (!samePriority(peek(state.cdnPriority), next)) state.cdnPriority.set(next);
59
+ }]
60
+ }
61
+ }
62
+ });
63
+ }
64
+ });
65
+ //#endregion
66
+ export { deriveCdnPriority };
67
+
68
+ //# sourceMappingURL=derive-cdn-priority.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"derive-cdn-priority.js","names":["getCdnId","defaultGetCdnId"],"sources":["../../../../src/playback/behaviors/derive-cdn-priority.ts"],"sourcesContent":["/**\n * **Session-level CDN priority.** While a presentation is resolved, owns the\n * `cdnPriority` signal: the distinct CDNs the source is served from (origin of\n * each track's URL), in manifest priority order — most-preferred first. Cleared\n * on src unload. The name mirrors HLS content steering's `PATHWAY-PRIORITY`.\n *\n * Redundant-stream sources (e.g. Mux's `?redundant_streams=true`) list the same\n * content on multiple hosts, so the candidate tracks already include one variant\n * per CDN. This behavior publishes *which* CDNs exist and their priority; the\n * `preferActiveCdn` scope rule in `track-switching` reads `cdnPriority` and\n * narrows each type's candidates to the first CDN with surviving tracks — so\n * video / audio / text all resolve from one host (the shared list is the\n * per-presentation coherence guarantee).\n *\n * The \"active\" CDN is not stored — it's derived by the scope as the\n * highest-priority entry in `cdnPriority` that still has tracks after the\n * constraints pre-pass. That makes failover a pure consequence of the (future)\n * failed-CDN constraint: when the primary's tracks are pruned during cooldown,\n * the scope falls to the next CDN; when the primary recovers, it snaps back.\n * Content steering, when it lands, reorders `cdnPriority` (pathway priority as a\n * sort key).\n *\n * Lifecycle: `'presentation-unresolved'` ↔ `'presentation-resolved'`, mirroring\n * `setupTrackSwitching`. The resolved state owns the signal; its entry-returned\n * cleanup clears it on exit (canonical cleanup-binds-to-setup per `reactors.md`).\n */\n\nimport { defineBehavior } from '../../core/composition/create-composition';\nimport { createMachineReactor } from '../../core/reactors/create-machine-reactor';\nimport { computed, peek, type ReadonlySignal, type Signal } from '../../core/signals/primitives';\nimport { isResolvedPresentation, type MaybeResolvedPresentation } from '../../media/types';\nimport { getCdnId as defaultGetCdnId, type GetCdnId, getOrderedCdnIds } from '../../media/utils/cdn';\n\nexport interface DeriveCdnPriorityState {\n presentation?: MaybeResolvedPresentation;\n cdnPriority?: string[];\n}\n\nconst samePriority = (a: string[] | undefined, b: string[]): boolean =>\n !!a && a.length === b.length && a.every((cdn, i) => cdn === b[i]);\n\n/**\n * Manage `cdnPriority`: publish the manifest-ordered CDN list on src load, clear\n * on src unload.\n *\n * @example\n * const reactor = deriveCdnPriority.setup({ state });\n */\nexport const deriveCdnPriority = defineBehavior({\n stateKeys: ['presentation', 'cdnPriority'],\n contextKeys: [],\n setup: ({\n state,\n config = {},\n }: {\n state: {\n presentation: ReadonlySignal<DeriveCdnPriorityState['presentation']>;\n cdnPriority: Signal<DeriveCdnPriorityState['cdnPriority']>;\n };\n config?: { getCdnId?: GetCdnId };\n }) => {\n const getCdnId = config.getCdnId ?? defaultGetCdnId;\n const derivedStateSignal = computed(() =>\n isResolvedPresentation(state.presentation.get())\n ? ('presentation-resolved' as const)\n : ('presentation-unresolved' as const)\n );\n\n return createMachineReactor({\n initial: 'presentation-unresolved',\n monitor: () => derivedStateSignal.get(),\n states: {\n 'presentation-unresolved': {},\n 'presentation-resolved': {\n // Cleanup-binds-to-setup: the CDN priority list is valid for exactly\n // 'presentation-resolved'. Clear fires on exit (src unload + destroy).\n entry: () => () => state.cdnPriority.set(undefined),\n effects: [\n () => {\n const presentation = state.presentation.get();\n if (!isResolvedPresentation(presentation)) return;\n const next = getOrderedCdnIds(presentation, getCdnId);\n // Skip the write when the CDN set is unchanged — a live reload swaps\n // in a new presentation object with the same hosts, and re-setting a\n // fresh array would re-fire the scope for an identical result.\n // `peek` reads without subscribing, so this never self-triggers.\n if (!samePriority(peek(state.cdnPriority), next)) state.cdnPriority.set(next);\n },\n ],\n },\n },\n });\n },\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsCA,MAAM,gBAAgB,GAAyB,MAC7C,CAAC,CAAC,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,OAAO,KAAK,MAAM,QAAQ,EAAE,GAAG;;;;;;;;AASnE,MAAa,oBAAoB,eAAe;CAC9C,WAAW,CAAC,gBAAgB,cAAc;CAC1C,aAAa,EAAE;CACf,QAAQ,EACN,OACA,SAAS,EAAE,OAOP;EACJ,MAAMA,aAAW,OAAO,YAAYC;EACpC,MAAM,qBAAqB,eACzB,uBAAuB,MAAM,aAAa,KAAK,CAAC,GAC3C,0BACA,0BACN;AAED,SAAO,qBAAqB;GAC1B,SAAS;GACT,eAAe,mBAAmB,KAAK;GACvC,QAAQ;IACN,2BAA2B,EAAE;IAC7B,yBAAyB;KAGvB,mBAAmB,MAAM,YAAY,IAAI,KAAA,EAAU;KACnD,SAAS,OACD;MACJ,MAAM,eAAe,MAAM,aAAa,KAAK;AAC7C,UAAI,CAAC,uBAAuB,aAAa,CAAE;MAC3C,MAAM,OAAO,iBAAiB,cAAcD,WAAS;AAKrD,UAAI,CAAC,aAAa,KAAK,MAAM,YAAY,EAAE,KAAK,CAAE,OAAM,YAAY,IAAI,KAAK;OAEhF;KACF;IACF;GACF,CAAC;;CAEL,CAAC"}
@@ -1,106 +1,12 @@
1
1
  import { computed, signal } from "../../../core/signals/primitives.js";
2
- import { defineBehavior } from "../../../core/composition/create-composition.js";
3
2
  import { effect } from "../../../core/signals/effect.js";
4
3
  import { createMachineReactor } from "../../../core/reactors/create-machine-reactor.js";
5
4
  import { isResolvedTrack } from "../../../media/types/index.js";
6
5
  import { findTrackById } from "../../../media/utils/tracks.js";
6
+ import { onMediaSourceReadyStateChange } from "../../../media/dom/mse/mediasource-setup.js";
7
7
  import { getMaxBufferedEnd, waitForSourceBuffersReady } from "../../../media/dom/mse/duration.js";
8
8
  import { isLastSegmentAppended } from "../../../media/dom/mse/end-of-stream.js";
9
- import { onMediaSourceReadyStateChange } from "../../../media/dom/mse/mediasource-setup.js";
10
9
  //#region src/playback/behaviors/dom/end-of-stream.ts
11
- /**
12
- * **Drive each `open → ended` transition of the MediaSource.** Calls
13
- * `MediaSource.endOfStream()` once the temporally last segments of every
14
- * active buffer actor's currently-loading track are fully appended and
15
- * the user has reached them — letting the browser finalize duration and
16
- * fire `ended` on the media element.
17
- *
18
- * Re-fires on every subsequent `open → ended → open` cycle. Per the MSE
19
- * spec, `appendBuffer()` after `endOfStream()` transitions the MediaSource
20
- * back to `'open'`; seek-back replays and back-buffer refills that re-load
21
- * earlier segments take this path, so the behavior must call
22
- * `endOfStream()` again once the last segments are reappended. The reactor's
23
- * `'preconditions-unmet'` ↔ `'eos-ready'` cycle *is* the re-arm mechanism:
24
- * a successful `endOfStream()` flips `mediaSource.readyState` to `'ended'`,
25
- * which (via the local `msIsOpen` signal) exits `'eos-ready'`; the next
26
- * `'open'` re-evaluates preconditions and may re-enter.
27
- *
28
- * # Tracking what's in the buffer
29
- *
30
- * Each `SourceBufferActor` knows which track it's currently loading via
31
- * `initTrackId` (set on the most recent `append-init` message) and which
32
- * segments it has appended. `deriveState` iterates over the available
33
- * buffer actors (`[videoBufferActor, audioBufferActor].filter(Boolean)`),
34
- * resolves each to its track via `findTrackById`, and checks that track's
35
- * last segment is appended. This means audio-only / video-only / mixed
36
- * configurations compose uniformly — the body iterates whatever's in
37
- * scope. No reliance on `selectedTrackId` slots: the actor's view IS the
38
- * source of truth for "what's being loaded into this buffer."
39
- *
40
- * **Two-fire on mid-end ABR switches**: when a quality switch occurs near
41
- * end-of-stream, the actor's `initTrackId` still reflects the *old* track
42
- * until the new init segment is appended. The reactor may fire
43
- * `endOfStream()` against the old track's last-segment-appended state,
44
- * then the new init's `appendBuffer()` re-opens the MS, which re-arms us
45
- * to fire again once the new track's last segment lands. Functionally
46
- * correct (the browser re-fires `ended` after the re-arm), at the cost of
47
- * one extra call. Accepted as the price of dropping `selectedTrackId`
48
- * dependence.
49
- *
50
- * # Buffer-actor idle gate
51
- *
52
- * Each actor must be `'idle'` (no queued or in-flight tasks) before we
53
- * fire. `buffer.updating === false` alone is insufficient: for chunked
54
- * fMP4 streaming, `updateend` fires after each chunk while the actor's
55
- * for-await loop synchronously enqueues the next `appendBuffer()` — so
56
- * the buffer flips `!updating → updating` across a microtask boundary.
57
- * The actor's `'updating'` state spans the entire multi-chunk append, so
58
- * `actor.snapshot.value === 'idle'` is the canonical "no more pending or
59
- * in-flight work" oracle.
60
- *
61
- * This bet (actor models the pipeline; `mediaSource.sourceBuffers` models
62
- * DOM state) is durable as long as the segment loader is the sole writer
63
- * of `appendBuffer()` calls. A future loop-mode that auto-fetches earlier
64
- * segments mid-`ended` would require a broader coordination story
65
- * between SourceBuffers and the MediaSource — possibly via a
66
- * `MediaSourceActor` — and that's where this gate would want
67
- * re-evaluating.
68
- *
69
- * # currentTime gate
70
- *
71
- * `currentTime` must have reached at least one active track's last
72
- * segment startTime. Prevents `'eos-ready'` entry when a back-buffer
73
- * `remove()` / `appendBuffer()` briefly re-opens the MediaSource while
74
- * the user is mid-stream. HLS rendition time-alignment means any active
75
- * track works as the reference.
76
- *
77
- * # MS readyState — local subscription
78
- *
79
- * The behavior subscribes to `mediaSource`'s readyState changes inside
80
- * its setup (via `onMediaSourceReadyStateChange`) and mirrors `'open'` to
81
- * a behavior-local `msIsOpen` signal that `deriveState` reads. No shared
82
- * `mediaSourceReadyState` slot dependency. Anticipates a future
83
- * `MediaSourceActor` whose snapshot would expose the same signal — the
84
- * consumer code wouldn't change.
85
- *
86
- * # `endOfStream` entry sequence
87
- *
88
- * 1. Wait for all SourceBuffers to be idle (DOM-level — defensive,
89
- * should already hold given the actor-idle gate in `deriveState`).
90
- * 2. Set `mediaSource.duration` from `getMaxBufferedEnd` to match actual
91
- * container timestamps (`endOfStream()` only clamps up implicitly;
92
- * setting it explicitly here keeps the final value deterministic).
93
- * 3. Call `mediaSource.endOfStream()`.
94
- *
95
- * State-exit cleanup (`controller.abort()`) cancels any in-flight wait
96
- * on source unload, presentation replace, or behavior destroy.
97
- *
98
- * # Coordination with `updateMediaSourceDuration`
99
- *
100
- * `updateMediaSourceDuration` writes the initial `mediaSource.duration`
101
- * from `presentation.duration` once per source; this behavior writes the
102
- * final value from the buffered end. Decision domains don't overlap.
103
- */
104
10
  function deriveState(presentation, mediaSource, msIsOpen, videoBufferActor, audioBufferActor, currentTime) {
105
11
  if (!mediaSource || !presentation || !msIsOpen) return "preconditions-unmet";
106
12
  const actors = [videoBufferActor, audioBufferActor].filter((a) => a !== void 0);
@@ -137,7 +43,7 @@ function endOfStreamSetup({ state, context }) {
137
43
  });
138
44
  return () => controller.abort();
139
45
  });
140
- const derivedStateSignal = computed(() => deriveState(state.presentation.get(), context.mediaSource.get(), msIsOpen.get(), context.videoBufferActor.get(), context.audioBufferActor.get(), state.currentTime.get()));
46
+ const derivedStateSignal = computed(() => deriveState(state.presentation.get(), context.mediaSource.get(), msIsOpen.get(), context.videoBufferActor?.get(), context.audioBufferActor?.get(), state.currentTime.get()));
141
47
  const reactor = createMachineReactor({
142
48
  initial: "preconditions-unmet",
143
49
  monitor: () => derivedStateSignal.get(),
@@ -163,15 +69,21 @@ function endOfStreamSetup({ state, context }) {
163
69
  reactor.destroy();
164
70
  };
165
71
  }
166
- const endOfStream = defineBehavior({
72
+ /**
73
+ * `endOfStream` uses a manual `Behavior<>` literal (rather than
74
+ * `defineBehavior`) because it reads `videoBufferActor` /
75
+ * `audioBufferActor` defensively without declaring them in its
76
+ * contextKeys — those slots are contributed by other behaviors and
77
+ * compose conditionally per engine variant. The `Behavior<>` literal
78
+ * opts out of the exhaustiveness check so the typed context shape can
79
+ * include the optional fields used at runtime. See the comment on
80
+ * `endOfStreamSetup`'s context param for the discipline.
81
+ */
82
+ const endOfStream = {
167
83
  stateKeys: ["presentation", "currentTime"],
168
- contextKeys: [
169
- "mediaSource",
170
- "videoBufferActor",
171
- "audioBufferActor"
172
- ],
84
+ contextKeys: ["mediaSource"],
173
85
  setup: endOfStreamSetup
174
- });
86
+ };
175
87
  //#endregion
176
88
  export { endOfStream };
177
89
 
@@ -1 +1 @@
1
- {"version":3,"file":"end-of-stream.js","names":[],"sources":["../../../../../src/playback/behaviors/dom/end-of-stream.ts"],"sourcesContent":["/**\n * **Drive each `open → ended` transition of the MediaSource.** Calls\n * `MediaSource.endOfStream()` once the temporally last segments of every\n * active buffer actor's currently-loading track are fully appended and\n * the user has reached them — letting the browser finalize duration and\n * fire `ended` on the media element.\n *\n * Re-fires on every subsequent `open → ended → open` cycle. Per the MSE\n * spec, `appendBuffer()` after `endOfStream()` transitions the MediaSource\n * back to `'open'`; seek-back replays and back-buffer refills that re-load\n * earlier segments take this path, so the behavior must call\n * `endOfStream()` again once the last segments are reappended. The reactor's\n * `'preconditions-unmet'` ↔ `'eos-ready'` cycle *is* the re-arm mechanism:\n * a successful `endOfStream()` flips `mediaSource.readyState` to `'ended'`,\n * which (via the local `msIsOpen` signal) exits `'eos-ready'`; the next\n * `'open'` re-evaluates preconditions and may re-enter.\n *\n * # Tracking what's in the buffer\n *\n * Each `SourceBufferActor` knows which track it's currently loading via\n * `initTrackId` (set on the most recent `append-init` message) and which\n * segments it has appended. `deriveState` iterates over the available\n * buffer actors (`[videoBufferActor, audioBufferActor].filter(Boolean)`),\n * resolves each to its track via `findTrackById`, and checks that track's\n * last segment is appended. This means audio-only / video-only / mixed\n * configurations compose uniformly — the body iterates whatever's in\n * scope. No reliance on `selectedTrackId` slots: the actor's view IS the\n * source of truth for \"what's being loaded into this buffer.\"\n *\n * **Two-fire on mid-end ABR switches**: when a quality switch occurs near\n * end-of-stream, the actor's `initTrackId` still reflects the *old* track\n * until the new init segment is appended. The reactor may fire\n * `endOfStream()` against the old track's last-segment-appended state,\n * then the new init's `appendBuffer()` re-opens the MS, which re-arms us\n * to fire again once the new track's last segment lands. Functionally\n * correct (the browser re-fires `ended` after the re-arm), at the cost of\n * one extra call. Accepted as the price of dropping `selectedTrackId`\n * dependence.\n *\n * # Buffer-actor idle gate\n *\n * Each actor must be `'idle'` (no queued or in-flight tasks) before we\n * fire. `buffer.updating === false` alone is insufficient: for chunked\n * fMP4 streaming, `updateend` fires after each chunk while the actor's\n * for-await loop synchronously enqueues the next `appendBuffer()` — so\n * the buffer flips `!updating → updating` across a microtask boundary.\n * The actor's `'updating'` state spans the entire multi-chunk append, so\n * `actor.snapshot.value === 'idle'` is the canonical \"no more pending or\n * in-flight work\" oracle.\n *\n * This bet (actor models the pipeline; `mediaSource.sourceBuffers` models\n * DOM state) is durable as long as the segment loader is the sole writer\n * of `appendBuffer()` calls. A future loop-mode that auto-fetches earlier\n * segments mid-`ended` would require a broader coordination story\n * between SourceBuffers and the MediaSource — possibly via a\n * `MediaSourceActor` — and that's where this gate would want\n * re-evaluating.\n *\n * # currentTime gate\n *\n * `currentTime` must have reached at least one active track's last\n * segment startTime. Prevents `'eos-ready'` entry when a back-buffer\n * `remove()` / `appendBuffer()` briefly re-opens the MediaSource while\n * the user is mid-stream. HLS rendition time-alignment means any active\n * track works as the reference.\n *\n * # MS readyState — local subscription\n *\n * The behavior subscribes to `mediaSource`'s readyState changes inside\n * its setup (via `onMediaSourceReadyStateChange`) and mirrors `'open'` to\n * a behavior-local `msIsOpen` signal that `deriveState` reads. No shared\n * `mediaSourceReadyState` slot dependency. Anticipates a future\n * `MediaSourceActor` whose snapshot would expose the same signal — the\n * consumer code wouldn't change.\n *\n * # `endOfStream` entry sequence\n *\n * 1. Wait for all SourceBuffers to be idle (DOM-level — defensive,\n * should already hold given the actor-idle gate in `deriveState`).\n * 2. Set `mediaSource.duration` from `getMaxBufferedEnd` to match actual\n * container timestamps (`endOfStream()` only clamps up implicitly;\n * setting it explicitly here keeps the final value deterministic).\n * 3. Call `mediaSource.endOfStream()`.\n *\n * State-exit cleanup (`controller.abort()`) cancels any in-flight wait\n * on source unload, presentation replace, or behavior destroy.\n *\n * # Coordination with `updateMediaSourceDuration`\n *\n * `updateMediaSourceDuration` writes the initial `mediaSource.duration`\n * from `presentation.duration` once per source; this behavior writes the\n * final value from the buffered end. Decision domains don't overlap.\n */\nimport { defineBehavior } from '../../../core/composition/create-composition';\nimport { createMachineReactor } from '../../../core/reactors/create-machine-reactor';\nimport { effect } from '../../../core/signals/effect';\nimport { computed, type ReadonlySignal, signal } from '../../../core/signals/primitives';\nimport { getMaxBufferedEnd, waitForSourceBuffersReady } from '../../../media/dom/mse/duration';\nimport { isLastSegmentAppended } from '../../../media/dom/mse/end-of-stream';\nimport { onMediaSourceReadyStateChange } from '../../../media/dom/mse/mediasource-setup';\nimport type { MaybeResolvedPresentation } from '../../../media/types';\nimport { isResolvedTrack } from '../../../media/types';\nimport { findTrackById } from '../../../media/utils/tracks';\nimport type { SourceBufferActor } from '../../actors/dom/source-buffer';\n\nexport interface EndOfStreamState {\n presentation?: MaybeResolvedPresentation;\n currentTime?: number;\n}\n\nexport interface EndOfStreamContext {\n mediaSource?: MediaSource;\n videoBufferActor?: SourceBufferActor;\n audioBufferActor?: SourceBufferActor;\n}\n\ntype EndOfStreamFsmState = 'preconditions-unmet' | 'eos-ready';\n\nfunction deriveState(\n presentation: MaybeResolvedPresentation | undefined,\n mediaSource: MediaSource | undefined,\n msIsOpen: boolean,\n videoBufferActor: SourceBufferActor | undefined,\n audioBufferActor: SourceBufferActor | undefined,\n currentTime: number | undefined\n): EndOfStreamFsmState {\n if (!mediaSource || !presentation || !msIsOpen) return 'preconditions-unmet';\n\n const actors = [videoBufferActor, audioBufferActor].filter((a): a is SourceBufferActor => a !== undefined);\n // No active buffer actors means setup hasn't completed wiring yet.\n if (actors.length === 0) return 'preconditions-unmet';\n\n // Track the latest last-segment startTime across actors as the\n // currentTime reference. HLS rendition time-alignment makes any active\n // track usable; using the max is safest when timings drift slightly.\n let lastSegStart: number | undefined;\n\n for (const actor of actors) {\n const snapshot = actor.snapshot.get();\n if (snapshot.value !== 'idle') return 'preconditions-unmet';\n\n const { initTrackId, segments: appended } = snapshot.context;\n if (!initTrackId) return 'preconditions-unmet';\n\n const track = findTrackById(presentation, initTrackId);\n if (!track || !isResolvedTrack(track)) return 'preconditions-unmet';\n if (!isLastSegmentAppended(track.segments, appended)) return 'preconditions-unmet';\n\n if (track.segments.length > 0) {\n const start = track.segments[track.segments.length - 1]!.startTime;\n if (lastSegStart === undefined || start > lastSegStart) lastSegStart = start;\n }\n }\n\n if (lastSegStart !== undefined && (currentTime ?? 0) < lastSegStart) {\n return 'preconditions-unmet';\n }\n\n return 'eos-ready';\n}\n\nfunction endOfStreamSetup({\n state,\n context,\n}: {\n state: {\n presentation: ReadonlySignal<EndOfStreamState['presentation']>;\n currentTime: ReadonlySignal<EndOfStreamState['currentTime']>;\n };\n context: {\n mediaSource: ReadonlySignal<EndOfStreamContext['mediaSource']>;\n videoBufferActor: ReadonlySignal<EndOfStreamContext['videoBufferActor']>;\n audioBufferActor: ReadonlySignal<EndOfStreamContext['audioBufferActor']>;\n };\n}): () => void {\n // Behavior-local mirror of `mediaSource.readyState === 'open'`. Subscribes\n // to MS events whenever a MediaSource is in scope; tears down when the\n // MS is swapped or the behavior destroys. Anticipates a future\n // `MediaSourceActor` whose snapshot would expose the same value — the\n // consumer code (deriveState) wouldn't change.\n const msIsOpen = signal(false);\n const cleanupMsListener = effect(() => {\n const mediaSource = context.mediaSource.get();\n if (!mediaSource) {\n msIsOpen.set(false);\n return;\n }\n msIsOpen.set(mediaSource.readyState === 'open');\n const controller = new AbortController();\n onMediaSourceReadyStateChange(mediaSource, controller.signal, (rs) => {\n msIsOpen.set(rs === 'open');\n });\n return () => controller.abort();\n });\n\n const derivedStateSignal = computed(() =>\n deriveState(\n state.presentation.get(),\n context.mediaSource.get(),\n msIsOpen.get(),\n context.videoBufferActor.get(),\n context.audioBufferActor.get(),\n state.currentTime.get()\n )\n );\n\n const reactor = createMachineReactor<EndOfStreamFsmState>({\n initial: 'preconditions-unmet',\n monitor: () => derivedStateSignal.get(),\n states: {\n 'preconditions-unmet': {},\n\n 'eos-ready': {\n // entry body is auto-untracked. deriveState handles source resets\n // and the MS readyState flip from 'open' to 'ended' that\n // endOfStream() produces; this entry resolves the MSE-spec\n // preconditions and binds its cleanup (abort) to state exit +\n // destroy.\n entry: () => {\n const mediaSource = context.mediaSource.get()!;\n const controller = new AbortController();\n\n const endStreamWhenReady = async () => {\n // Defensive: the actor-idle gate in deriveState should mean\n // buffers are already settled, but a microtask boundary\n // between deriveState's read and entry firing leaves a thin\n // gap. The DOM-level wait closes it.\n await waitForSourceBuffersReady(mediaSource.sourceBuffers, controller.signal);\n if (controller.signal.aborted) return;\n\n // MSE spec: duration cannot be less than any buffered range, and\n // endOfStream() will only clamp up implicitly. Setting it\n // explicitly here from actual container timestamps keeps the\n // final value deterministic for assets whose declared duration\n // disagrees with the buffered end (common with CMAF).\n const bufferedEnd = getMaxBufferedEnd(mediaSource.sourceBuffers);\n if (bufferedEnd > 0) mediaSource.duration = bufferedEnd;\n\n mediaSource.endOfStream();\n };\n\n endStreamWhenReady().catch((err) => console.error('Failed to call endOfStream:', err));\n\n return () => controller.abort();\n },\n },\n },\n });\n\n return () => {\n cleanupMsListener();\n reactor.destroy();\n };\n}\n\nexport const endOfStream = defineBehavior({\n stateKeys: ['presentation', 'currentTime'],\n contextKeys: ['mediaSource', 'videoBufferActor', 'audioBufferActor'],\n setup: endOfStreamSetup,\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsHA,SAAS,YACP,cACA,aACA,UACA,kBACA,kBACA,aACqB;AACrB,KAAI,CAAC,eAAe,CAAC,gBAAgB,CAAC,SAAU,QAAO;CAEvD,MAAM,SAAS,CAAC,kBAAkB,iBAAiB,CAAC,QAAQ,MAA8B,MAAM,KAAA,EAAU;AAE1G,KAAI,OAAO,WAAW,EAAG,QAAO;CAKhC,IAAI;AAEJ,MAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,WAAW,MAAM,SAAS,KAAK;AACrC,MAAI,SAAS,UAAU,OAAQ,QAAO;EAEtC,MAAM,EAAE,aAAa,UAAU,aAAa,SAAS;AACrD,MAAI,CAAC,YAAa,QAAO;EAEzB,MAAM,QAAQ,cAAc,cAAc,YAAY;AACtD,MAAI,CAAC,SAAS,CAAC,gBAAgB,MAAM,CAAE,QAAO;AAC9C,MAAI,CAAC,sBAAsB,MAAM,UAAU,SAAS,CAAE,QAAO;AAE7D,MAAI,MAAM,SAAS,SAAS,GAAG;GAC7B,MAAM,QAAQ,MAAM,SAAS,MAAM,SAAS,SAAS,GAAI;AACzD,OAAI,iBAAiB,KAAA,KAAa,QAAQ,aAAc,gBAAe;;;AAI3E,KAAI,iBAAiB,KAAA,MAAc,eAAe,KAAK,aACrD,QAAO;AAGT,QAAO;;AAGT,SAAS,iBAAiB,EACxB,OACA,WAWa;CAMb,MAAM,WAAW,OAAO,MAAM;CAC9B,MAAM,oBAAoB,aAAa;EACrC,MAAM,cAAc,QAAQ,YAAY,KAAK;AAC7C,MAAI,CAAC,aAAa;AAChB,YAAS,IAAI,MAAM;AACnB;;AAEF,WAAS,IAAI,YAAY,eAAe,OAAO;EAC/C,MAAM,aAAa,IAAI,iBAAiB;AACxC,gCAA8B,aAAa,WAAW,SAAS,OAAO;AACpE,YAAS,IAAI,OAAO,OAAO;IAC3B;AACF,eAAa,WAAW,OAAO;GAC/B;CAEF,MAAM,qBAAqB,eACzB,YACE,MAAM,aAAa,KAAK,EACxB,QAAQ,YAAY,KAAK,EACzB,SAAS,KAAK,EACd,QAAQ,iBAAiB,KAAK,EAC9B,QAAQ,iBAAiB,KAAK,EAC9B,MAAM,YAAY,KAAK,CACxB,CACF;CAED,MAAM,UAAU,qBAA0C;EACxD,SAAS;EACT,eAAe,mBAAmB,KAAK;EACvC,QAAQ;GACN,uBAAuB,EAAE;GAEzB,aAAa,EAMX,aAAa;IACX,MAAM,cAAc,QAAQ,YAAY,KAAK;IAC7C,MAAM,aAAa,IAAI,iBAAiB;IAExC,MAAM,qBAAqB,YAAY;AAKrC,WAAM,0BAA0B,YAAY,eAAe,WAAW,OAAO;AAC7E,SAAI,WAAW,OAAO,QAAS;KAO/B,MAAM,cAAc,kBAAkB,YAAY,cAAc;AAChE,SAAI,cAAc,EAAG,aAAY,WAAW;AAE5C,iBAAY,aAAa;;AAG3B,wBAAoB,CAAC,OAAO,QAAQ,QAAQ,MAAM,+BAA+B,IAAI,CAAC;AAEtF,iBAAa,WAAW,OAAO;MAElC;GACF;EACF,CAAC;AAEF,cAAa;AACX,qBAAmB;AACnB,UAAQ,SAAS;;;AAIrB,MAAa,cAAc,eAAe;CACxC,WAAW,CAAC,gBAAgB,cAAc;CAC1C,aAAa;EAAC;EAAe;EAAoB;EAAmB;CACpE,OAAO;CACR,CAAC"}
1
+ {"version":3,"file":"end-of-stream.js","names":[],"sources":["../../../../../src/playback/behaviors/dom/end-of-stream.ts"],"sourcesContent":["/**\n * **Drive each `open → ended` transition of the MediaSource.** Calls\n * `MediaSource.endOfStream()` once the temporally last segments of every\n * active buffer actor's currently-loading track are fully appended and\n * the user has reached them — letting the browser finalize duration and\n * fire `ended` on the media element.\n *\n * Re-fires on every subsequent `open → ended → open` cycle. Per the MSE\n * spec, `appendBuffer()` after `endOfStream()` transitions the MediaSource\n * back to `'open'`; seek-back replays and back-buffer refills that re-load\n * earlier segments take this path, so the behavior must call\n * `endOfStream()` again once the last segments are reappended. The reactor's\n * `'preconditions-unmet'` ↔ `'eos-ready'` cycle *is* the re-arm mechanism:\n * a successful `endOfStream()` flips `mediaSource.readyState` to `'ended'`,\n * which (via the local `msIsOpen` signal) exits `'eos-ready'`; the next\n * `'open'` re-evaluates preconditions and may re-enter.\n *\n * # Tracking what's in the buffer\n *\n * Each `SourceBufferActor` knows which track it's currently loading via\n * `initTrackId` (set on the most recent `append-init` message) and which\n * segments it has appended. `deriveState` iterates over the available\n * buffer actors (`[videoBufferActor, audioBufferActor].filter(Boolean)`),\n * resolves each to its track via `findTrackById`, and checks that track's\n * last segment is appended. This means audio-only / video-only / mixed\n * configurations compose uniformly — the body iterates whatever's in\n * scope. No reliance on `selectedTrackId` slots: the actor's view IS the\n * source of truth for \"what's being loaded into this buffer.\"\n *\n * **Two-fire on mid-end ABR switches**: when a quality switch occurs near\n * end-of-stream, the actor's `initTrackId` still reflects the *old* track\n * until the new init segment is appended. The reactor may fire\n * `endOfStream()` against the old track's last-segment-appended state,\n * then the new init's `appendBuffer()` re-opens the MS, which re-arms us\n * to fire again once the new track's last segment lands. Functionally\n * correct (the browser re-fires `ended` after the re-arm), at the cost of\n * one extra call. Accepted as the price of dropping `selectedTrackId`\n * dependence.\n *\n * # Buffer-actor idle gate\n *\n * Each actor must be `'idle'` (no queued or in-flight tasks) before we\n * fire. `buffer.updating === false` alone is insufficient: for chunked\n * fMP4 streaming, `updateend` fires after each chunk while the actor's\n * for-await loop synchronously enqueues the next `appendBuffer()` — so\n * the buffer flips `!updating → updating` across a microtask boundary.\n * The actor's `'updating'` state spans the entire multi-chunk append, so\n * `actor.snapshot.value === 'idle'` is the canonical \"no more pending or\n * in-flight work\" oracle.\n *\n * This bet (actor models the pipeline; `mediaSource.sourceBuffers` models\n * DOM state) is durable as long as the segment loader is the sole writer\n * of `appendBuffer()` calls. A future loop-mode that auto-fetches earlier\n * segments mid-`ended` would require a broader coordination story\n * between SourceBuffers and the MediaSource — possibly via a\n * `MediaSourceActor` — and that's where this gate would want\n * re-evaluating.\n *\n * # currentTime gate\n *\n * `currentTime` must have reached at least one active track's last\n * segment startTime. Prevents `'eos-ready'` entry when a back-buffer\n * `remove()` / `appendBuffer()` briefly re-opens the MediaSource while\n * the user is mid-stream. HLS rendition time-alignment means any active\n * track works as the reference.\n *\n * # MS readyState — local subscription\n *\n * The behavior subscribes to `mediaSource`'s readyState changes inside\n * its setup (via `onMediaSourceReadyStateChange`) and mirrors `'open'` to\n * a behavior-local `msIsOpen` signal that `deriveState` reads. No shared\n * `mediaSourceReadyState` slot dependency. Anticipates a future\n * `MediaSourceActor` whose snapshot would expose the same signal — the\n * consumer code wouldn't change.\n *\n * # `endOfStream` entry sequence\n *\n * 1. Wait for all SourceBuffers to be idle (DOM-level — defensive,\n * should already hold given the actor-idle gate in `deriveState`).\n * 2. Set `mediaSource.duration` from `getMaxBufferedEnd` to match actual\n * container timestamps (`endOfStream()` only clamps up implicitly;\n * setting it explicitly here keeps the final value deterministic).\n * 3. Call `mediaSource.endOfStream()`.\n *\n * State-exit cleanup (`controller.abort()`) cancels any in-flight wait\n * on source unload, presentation replace, or behavior destroy.\n *\n * # Coordination with `updateMediaSourceDuration`\n *\n * `updateMediaSourceDuration` writes the initial `mediaSource.duration`\n * from `presentation.duration` once per source; this behavior writes the\n * final value from the buffered end. Decision domains don't overlap.\n */\nimport type { Behavior } from '../../../core/composition/create-composition';\nimport { createMachineReactor } from '../../../core/reactors/create-machine-reactor';\nimport { effect } from '../../../core/signals/effect';\nimport { computed, type ReadonlySignal, signal } from '../../../core/signals/primitives';\nimport { getMaxBufferedEnd, waitForSourceBuffersReady } from '../../../media/dom/mse/duration';\nimport { isLastSegmentAppended } from '../../../media/dom/mse/end-of-stream';\nimport { onMediaSourceReadyStateChange } from '../../../media/dom/mse/mediasource-setup';\nimport type { MaybeResolvedPresentation } from '../../../media/types';\nimport { isResolvedTrack } from '../../../media/types';\nimport { findTrackById } from '../../../media/utils/tracks';\nimport type { SourceBufferActor } from '../../actors/dom/source-buffer';\n\nexport interface EndOfStreamState {\n presentation?: MaybeResolvedPresentation;\n currentTime?: number;\n}\n\nexport interface EndOfStreamContext {\n mediaSource?: MediaSource;\n videoBufferActor?: SourceBufferActor;\n audioBufferActor?: SourceBufferActor;\n}\n\ntype EndOfStreamFsmState = 'preconditions-unmet' | 'eos-ready';\n\nfunction deriveState(\n presentation: MaybeResolvedPresentation | undefined,\n mediaSource: MediaSource | undefined,\n msIsOpen: boolean,\n videoBufferActor: SourceBufferActor | undefined,\n audioBufferActor: SourceBufferActor | undefined,\n currentTime: number | undefined\n): EndOfStreamFsmState {\n if (!mediaSource || !presentation || !msIsOpen) return 'preconditions-unmet';\n\n const actors = [videoBufferActor, audioBufferActor].filter((a): a is SourceBufferActor => a !== undefined);\n // No active buffer actors means setup hasn't completed wiring yet.\n if (actors.length === 0) return 'preconditions-unmet';\n\n // Track the latest last-segment startTime across actors as the\n // currentTime reference. HLS rendition time-alignment makes any active\n // track usable; using the max is safest when timings drift slightly.\n let lastSegStart: number | undefined;\n\n for (const actor of actors) {\n const snapshot = actor.snapshot.get();\n if (snapshot.value !== 'idle') return 'preconditions-unmet';\n\n const { initTrackId, segments: appended } = snapshot.context;\n if (!initTrackId) return 'preconditions-unmet';\n\n const track = findTrackById(presentation, initTrackId);\n if (!track || !isResolvedTrack(track)) return 'preconditions-unmet';\n if (!isLastSegmentAppended(track.segments, appended)) return 'preconditions-unmet';\n\n if (track.segments.length > 0) {\n const start = track.segments[track.segments.length - 1]!.startTime;\n if (lastSegStart === undefined || start > lastSegStart) lastSegStart = start;\n }\n }\n\n if (lastSegStart !== undefined && (currentTime ?? 0) < lastSegStart) {\n return 'preconditions-unmet';\n }\n\n return 'eos-ready';\n}\n\nfunction endOfStreamSetup({\n state,\n context,\n}: {\n state: {\n presentation: ReadonlySignal<EndOfStreamState['presentation']>;\n currentTime: ReadonlySignal<EndOfStreamState['currentTime']>;\n };\n context: {\n mediaSource: ReadonlySignal<EndOfStreamContext['mediaSource']>;\n // See behavior definition for details on these optional context signals.\n videoBufferActor?: ReadonlySignal<SourceBufferActor | undefined>;\n audioBufferActor?: ReadonlySignal<SourceBufferActor | undefined>;\n };\n}): () => void {\n // Behavior-local mirror of `mediaSource.readyState === 'open'`. Subscribes\n // to MS events whenever a MediaSource is in scope; tears down when the\n // MS is swapped or the behavior destroys. Anticipates a future\n // `MediaSourceActor` whose snapshot would expose the same value — the\n // consumer code (deriveState) wouldn't change.\n const msIsOpen = signal(false);\n const cleanupMsListener = effect(() => {\n const mediaSource = context.mediaSource.get();\n if (!mediaSource) {\n msIsOpen.set(false);\n return;\n }\n msIsOpen.set(mediaSource.readyState === 'open');\n const controller = new AbortController();\n onMediaSourceReadyStateChange(mediaSource, controller.signal, (rs) => {\n msIsOpen.set(rs === 'open');\n });\n return () => controller.abort();\n });\n\n const derivedStateSignal = computed(() =>\n deriveState(\n state.presentation.get(),\n context.mediaSource.get(),\n msIsOpen.get(),\n context.videoBufferActor?.get(),\n context.audioBufferActor?.get(),\n state.currentTime.get()\n )\n );\n\n const reactor = createMachineReactor<EndOfStreamFsmState>({\n initial: 'preconditions-unmet',\n monitor: () => derivedStateSignal.get(),\n states: {\n 'preconditions-unmet': {},\n\n 'eos-ready': {\n // entry body is auto-untracked. deriveState handles source resets\n // and the MS readyState flip from 'open' to 'ended' that\n // endOfStream() produces; this entry resolves the MSE-spec\n // preconditions and binds its cleanup (abort) to state exit +\n // destroy.\n entry: () => {\n const mediaSource = context.mediaSource.get()!;\n const controller = new AbortController();\n\n const endStreamWhenReady = async () => {\n // Defensive: the actor-idle gate in deriveState should mean\n // buffers are already settled, but a microtask boundary\n // between deriveState's read and entry firing leaves a thin\n // gap. The DOM-level wait closes it.\n await waitForSourceBuffersReady(mediaSource.sourceBuffers, controller.signal);\n if (controller.signal.aborted) return;\n\n // MSE spec: duration cannot be less than any buffered range, and\n // endOfStream() will only clamp up implicitly. Setting it\n // explicitly here from actual container timestamps keeps the\n // final value deterministic for assets whose declared duration\n // disagrees with the buffered end (common with CMAF).\n const bufferedEnd = getMaxBufferedEnd(mediaSource.sourceBuffers);\n if (bufferedEnd > 0) mediaSource.duration = bufferedEnd;\n\n mediaSource.endOfStream();\n };\n\n endStreamWhenReady().catch((err) => console.error('Failed to call endOfStream:', err));\n\n return () => controller.abort();\n },\n },\n },\n });\n\n return () => {\n cleanupMsListener();\n reactor.destroy();\n };\n}\n\n/**\n * `endOfStream` uses a manual `Behavior<>` literal (rather than\n * `defineBehavior`) because it reads `videoBufferActor` /\n * `audioBufferActor` defensively without declaring them in its\n * contextKeys — those slots are contributed by other behaviors and\n * compose conditionally per engine variant. The `Behavior<>` literal\n * opts out of the exhaustiveness check so the typed context shape can\n * include the optional fields used at runtime. See the comment on\n * `endOfStreamSetup`'s context param for the discipline.\n */\nexport const endOfStream: Behavior<\n {\n presentation: ReadonlySignal<EndOfStreamState['presentation']>;\n currentTime: ReadonlySignal<EndOfStreamState['currentTime']>;\n },\n { mediaSource: ReadonlySignal<EndOfStreamContext['mediaSource']> }\n> = {\n stateKeys: ['presentation', 'currentTime'],\n contextKeys: ['mediaSource'],\n setup: endOfStreamSetup,\n};\n"],"mappings":";;;;;;;;;AAsHA,SAAS,YACP,cACA,aACA,UACA,kBACA,kBACA,aACqB;AACrB,KAAI,CAAC,eAAe,CAAC,gBAAgB,CAAC,SAAU,QAAO;CAEvD,MAAM,SAAS,CAAC,kBAAkB,iBAAiB,CAAC,QAAQ,MAA8B,MAAM,KAAA,EAAU;AAE1G,KAAI,OAAO,WAAW,EAAG,QAAO;CAKhC,IAAI;AAEJ,MAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,WAAW,MAAM,SAAS,KAAK;AACrC,MAAI,SAAS,UAAU,OAAQ,QAAO;EAEtC,MAAM,EAAE,aAAa,UAAU,aAAa,SAAS;AACrD,MAAI,CAAC,YAAa,QAAO;EAEzB,MAAM,QAAQ,cAAc,cAAc,YAAY;AACtD,MAAI,CAAC,SAAS,CAAC,gBAAgB,MAAM,CAAE,QAAO;AAC9C,MAAI,CAAC,sBAAsB,MAAM,UAAU,SAAS,CAAE,QAAO;AAE7D,MAAI,MAAM,SAAS,SAAS,GAAG;GAC7B,MAAM,QAAQ,MAAM,SAAS,MAAM,SAAS,SAAS,GAAI;AACzD,OAAI,iBAAiB,KAAA,KAAa,QAAQ,aAAc,gBAAe;;;AAI3E,KAAI,iBAAiB,KAAA,MAAc,eAAe,KAAK,aACrD,QAAO;AAGT,QAAO;;AAGT,SAAS,iBAAiB,EACxB,OACA,WAYa;CAMb,MAAM,WAAW,OAAO,MAAM;CAC9B,MAAM,oBAAoB,aAAa;EACrC,MAAM,cAAc,QAAQ,YAAY,KAAK;AAC7C,MAAI,CAAC,aAAa;AAChB,YAAS,IAAI,MAAM;AACnB;;AAEF,WAAS,IAAI,YAAY,eAAe,OAAO;EAC/C,MAAM,aAAa,IAAI,iBAAiB;AACxC,gCAA8B,aAAa,WAAW,SAAS,OAAO;AACpE,YAAS,IAAI,OAAO,OAAO;IAC3B;AACF,eAAa,WAAW,OAAO;GAC/B;CAEF,MAAM,qBAAqB,eACzB,YACE,MAAM,aAAa,KAAK,EACxB,QAAQ,YAAY,KAAK,EACzB,SAAS,KAAK,EACd,QAAQ,kBAAkB,KAAK,EAC/B,QAAQ,kBAAkB,KAAK,EAC/B,MAAM,YAAY,KAAK,CACxB,CACF;CAED,MAAM,UAAU,qBAA0C;EACxD,SAAS;EACT,eAAe,mBAAmB,KAAK;EACvC,QAAQ;GACN,uBAAuB,EAAE;GAEzB,aAAa,EAMX,aAAa;IACX,MAAM,cAAc,QAAQ,YAAY,KAAK;IAC7C,MAAM,aAAa,IAAI,iBAAiB;IAExC,MAAM,qBAAqB,YAAY;AAKrC,WAAM,0BAA0B,YAAY,eAAe,WAAW,OAAO;AAC7E,SAAI,WAAW,OAAO,QAAS;KAO/B,MAAM,cAAc,kBAAkB,YAAY,cAAc;AAChE,SAAI,cAAc,EAAG,aAAY,WAAW;AAE5C,iBAAY,aAAa;;AAG3B,wBAAoB,CAAC,OAAO,QAAQ,QAAQ,MAAM,+BAA+B,IAAI,CAAC;AAEtF,iBAAa,WAAW,OAAO;MAElC;GACF;EACF,CAAC;AAEF,cAAa;AACX,qBAAmB;AACnB,UAAQ,SAAS;;;;;;;;;;;;;AAcrB,MAAa,cAMT;CACF,WAAW,CAAC,gBAAgB,cAAc;CAC1C,aAAa,CAAC,cAAc;CAC5B,OAAO;CACR"}
@@ -3,7 +3,7 @@ import { defineBehavior } from "../../../core/composition/create-composition.js"
3
3
  import { createMachineReactor } from "../../../core/reactors/create-machine-reactor.js";
4
4
  import { DEFAULT_FORWARD_BUFFER_CONFIG, segmentStartForTime } from "../../../media/buffer/forward-buffer.js";
5
5
  import { findResolvedAudioTrack, findResolvedTextTrack, findResolvedVideoTrack } from "../../../media/utils/tracks.js";
6
- import { AUDIO_TYPE_CONFIG, TEXT_TYPE_CONFIG, VIDEO_TYPE_CONFIG } from "../track-types.js";
6
+ import { AUDIO_TYPE_CONFIG, TEXT_TYPE_CONFIG, VIDEO_TYPE_CONFIG } from "../../primitives/track-types.js";
7
7
  //#region src/playback/behaviors/dom/load-segments.ts
8
8
  /**
9
9
  * **Per-type segment loading dispatch.** Per available track type (video /
@@ -1 +1 @@
1
- {"version":3,"file":"load-segments.js","names":[],"sources":["../../../../../src/playback/behaviors/dom/load-segments.ts"],"sourcesContent":["/**\n * **Per-type segment loading dispatch.** Per available track type (video /\n * audio / text), reads the per-type segment-loader actor from context and\n * sends typed `'load'` messages whenever a meaningful loading condition\n * changes (selected track, current time crossing a segment boundary).\n *\n * Loader-actor lifecycle is owned upstream:\n * - Video: `setupVideoBufferActors`\n * - Audio: `setupAudioBufferActors`\n * - Text: `setupTextTrackActors`\n *\n * This behavior is pure-consumer: it reads `context[loaderKey]` and\n * dispatches typed messages via the variant's per-type `findResolvedTrack`\n * resolver.\n *\n * # Load modes as reactor states\n *\n * Four states encode the load-gating policy directly:\n *\n * - `'preconditions-unmet'` — no loader actor in context, or the selected\n * track hasn't resolved.\n * - `'dormant'` — `preload === 'none' && !loadActivated`. Nothing fires.\n * - `'metadata-only'` — `!loadActivated && preload !== 'auto' && preload !== 'none'`.\n * Fires an init-segment-only `load` message **once on entry**. The\n * variant's loader actor decides what to do — v/a's actor fetches the\n * init segment; text's actor no-ops (no init concept).\n * - `'full-range'` — `loadActivated || preload === 'auto'`. Effect re-fires\n * on selected-track change and on segment-boundary crossing.\n *\n * # Per-type parameterization (inference-driven)\n *\n * The helper is generic over `Track` — the resolved-track type. Each\n * variant supplies its own `findResolvedTrack` resolver via config; TS\n * infers `Track` from the resolver's return type. The loader signal's\n * value type is constrained to `SegmentLoaderLike<Track>` — anything with\n * a `send` method accepting the `Track`-parameterized message. Concrete\n * actor types (`SegmentLoaderActor`, `TextTrackSegmentLoaderActor`)\n * satisfy this via function-parameter contravariance: an actor whose\n * `.send` accepts a wider track type is assignable to a slot expecting a\n * narrower track type.\n *\n * No widening of actor message types is needed; no casts inside the\n * helper. Per-variant wiring (right loader paired with right resolver)\n * is enforced at the variant call site.\n */\nimport { defineBehavior } from '../../../core/composition/create-composition';\nimport type { Reactor } from '../../../core/reactors/create-machine-reactor';\nimport { createMachineReactor } from '../../../core/reactors/create-machine-reactor';\nimport { computed, peek, type ReadonlySignal } from '../../../core/signals/primitives';\nimport {\n DEFAULT_FORWARD_BUFFER_CONFIG,\n type ForwardBufferConfig,\n segmentStartForTime,\n} from '../../../media/buffer/forward-buffer';\nimport type { MaybeResolvedPresentation, Segment } from '../../../media/types';\nimport { findResolvedAudioTrack, findResolvedTextTrack, findResolvedVideoTrack } from '../../../media/utils/tracks';\nimport type { BufferState, SegmentLoaderActor, SourceBufferState } from '../../actors/dom/segment-loader';\nimport type { TextTrackSegmentLoaderActor } from '../../actors/text-track-segment-loader';\nimport { AUDIO_TYPE_CONFIG, TEXT_TYPE_CONFIG, VIDEO_TYPE_CONFIG } from '../track-types';\n\n// Re-export buffer state types for consumers that import them from this module.\nexport type { BufferState, SourceBufferState };\n\n// ============================================================================\n// STATE & CONTEXT\n// ============================================================================\n\n/** State shape for segment loading. */\nexport interface SegmentLoadingState {\n presentation?: MaybeResolvedPresentation;\n preload?: string;\n /** Current playback position in seconds. Defaults to 0 when undefined. */\n currentTime?: number;\n /** True once a preload-overriding event has fired for the current source. */\n loadActivated?: boolean;\n selectedVideoTrackId?: string;\n selectedAudioTrackId?: string;\n selectedTextTrackId?: string;\n}\n\n/** Context shape for segment loading. Each variant only consumes its own type's loader actor. */\nexport interface SegmentLoadingContext {\n videoSegmentLoaderActor?: SegmentLoaderActor;\n audioSegmentLoaderActor?: SegmentLoaderActor;\n textTrackSegmentLoaderActor?: TextTrackSegmentLoaderActor;\n}\n\n// ============================================================================\n// REACTOR\n// ============================================================================\n\ntype SegmentLoadingFsmState = 'preconditions-unmet' | 'dormant' | 'metadata-only' | 'full-range';\n\ntype SelectedTrackKey = 'selectedVideoTrackId' | 'selectedAudioTrackId' | 'selectedTextTrackId';\ntype SegmentLoaderActorKey = 'videoSegmentLoaderActor' | 'audioSegmentLoaderActor' | 'textTrackSegmentLoaderActor';\n\ntype SegmentLoadingStateMap<K extends SelectedTrackKey> = {\n presentation: ReadonlySignal<SegmentLoadingState['presentation']>;\n preload: ReadonlySignal<SegmentLoadingState['preload']>;\n currentTime: ReadonlySignal<SegmentLoadingState['currentTime']>;\n loadActivated: ReadonlySignal<SegmentLoadingState['loadActivated']>;\n} & { [P in K]: ReadonlySignal<SegmentLoadingState[P]> };\n\n/** Shared `'load'` message shape parameterized over the resolved track type. */\ninterface LoadMessage<Track> {\n type: 'load';\n track: Track;\n range?: { start: number; end: number };\n}\n\n/** Structural constraint for any segment-loader-style actor. */\ninterface SegmentLoaderLike<Track> {\n send: (message: LoadMessage<Track>) => void;\n}\n\n/**\n * Specialization helper. Generic over `Track` (inferred from\n * `findResolvedTrack`'s return type). The loader value type is\n * constrained to `SegmentLoaderLike<Track>` — concrete actor types\n * (whose `.send` accepts a wider track union) satisfy this via\n * function-parameter contravariance.\n *\n * Tracks that the helper handles must have a `segments` field — used by\n * `segmentBoundarySignal` to compute the load-anchor boundary. Each\n * variant's resolver narrows to the right resolved-track shape.\n */\nfunction setupSegmentLoading<\n K extends SelectedTrackKey,\n L extends SegmentLoaderActorKey,\n Track extends { segments: readonly Segment[] },\n>({\n state,\n context,\n config,\n}: {\n state: SegmentLoadingStateMap<K>;\n context: { [P in L]: ReadonlySignal<SegmentLoaderLike<Track> | undefined> };\n config: {\n selectedKey: K;\n loaderKey: L;\n findResolvedTrack: (\n presentation: MaybeResolvedPresentation | undefined,\n trackId: string | undefined\n ) => Track | undefined;\n forwardBuffer?: Partial<ForwardBufferConfig>;\n };\n}): Reactor<SegmentLoadingFsmState | 'destroying' | 'destroyed'> {\n const { selectedKey, loaderKey, findResolvedTrack } = config;\n const bufferDuration = config.forwardBuffer?.bufferDuration ?? DEFAULT_FORWARD_BUFFER_CONFIG.bufferDuration;\n\n const selectedTrack = computed<Track | undefined>(() =>\n findResolvedTrack(state.presentation.get(), state[selectedKey].get())\n );\n\n // Segment-boundary signal — `segmentStartForTime` returns the same number\n // while `currentTime` stays inside one segment, so signal-polyfill's\n // `Object.is` equality on this computed dedups within-segment ticks.\n const segmentBoundarySignal = computed(() => {\n const track = selectedTrack.get();\n if (!track) return undefined;\n return segmentStartForTime(state.currentTime.get() ?? 0, track.segments);\n });\n\n const derivedStateSignal = computed<SegmentLoadingFsmState>(() => {\n if (!context[loaderKey].get() || !selectedTrack.get()) return 'preconditions-unmet';\n if (state.loadActivated.get() || state.preload.get() === 'auto') return 'full-range';\n if (state.preload.get() === 'none') return 'dormant';\n return 'metadata-only';\n });\n\n return createMachineReactor<SegmentLoadingFsmState>({\n initial: 'preconditions-unmet',\n monitor: () => derivedStateSignal.get(),\n states: {\n 'preconditions-unmet': {},\n dormant: {},\n 'metadata-only': {\n // Fires once on entry. Matches HTMLMediaElement's preload='metadata'\n // semantics — load enough to surface metadata for the entry-time\n // selection. Each variant's loader decides what to do: v/a's actor\n // fetches the init segment; text's actor treats no-range as a\n // no-op. Selection changes while still in `'metadata-only'` are\n // intentionally not followed.\n entry: () => {\n const track = selectedTrack.get()!;\n context[loaderKey].get()!.send({ type: 'load', track });\n },\n },\n 'full-range': {\n // Re-fires on selected-track changes and on segment-boundary\n // crossings. The loader and `currentTime` are peeked — the\n // boundary-dedup signal already handles re-firing policy.\n effects: () => {\n const track = selectedTrack.get()!;\n segmentBoundarySignal.get();\n const currentTime = peek(state.currentTime) ?? 0;\n peek(context[loaderKey])!.send({\n type: 'load',\n track,\n range: { start: currentTime, end: currentTime + bufferDuration },\n });\n },\n },\n },\n });\n}\n\n// ============================================================================\n// Per-helper-per-type configs — defaults that variants spread engine config over\n// ============================================================================\n\nconst VIDEO_SEGMENT_LOADING_CONFIG = {\n ...VIDEO_TYPE_CONFIG,\n findResolvedTrack: findResolvedVideoTrack,\n} as const;\n\nconst AUDIO_SEGMENT_LOADING_CONFIG = {\n ...AUDIO_TYPE_CONFIG,\n findResolvedTrack: findResolvedAudioTrack,\n} as const;\n\nconst TEXT_SEGMENT_LOADING_CONFIG = {\n ...TEXT_TYPE_CONFIG,\n findResolvedTrack: findResolvedTextTrack,\n} as const;\n\n// ============================================================================\n// Specialized exports — one per media type\n// ============================================================================\n\nexport const loadVideoSegments = defineBehavior({\n stateKeys: ['presentation', 'preload', 'currentTime', 'loadActivated', 'selectedVideoTrackId'],\n contextKeys: ['videoSegmentLoaderActor'],\n setup: ({\n state,\n context,\n config = {},\n }: {\n state: SegmentLoadingStateMap<'selectedVideoTrackId'>;\n context: {\n videoSegmentLoaderActor: ReadonlySignal<SegmentLoadingContext['videoSegmentLoaderActor']>;\n };\n config?: object;\n }) =>\n setupSegmentLoading({\n state,\n context,\n config: { ...VIDEO_SEGMENT_LOADING_CONFIG, ...config },\n }),\n});\n\nexport const loadAudioSegments = defineBehavior({\n stateKeys: ['presentation', 'preload', 'currentTime', 'loadActivated', 'selectedAudioTrackId'],\n contextKeys: ['audioSegmentLoaderActor'],\n setup: ({\n state,\n context,\n config = {},\n }: {\n state: SegmentLoadingStateMap<'selectedAudioTrackId'>;\n context: {\n audioSegmentLoaderActor: ReadonlySignal<SegmentLoadingContext['audioSegmentLoaderActor']>;\n };\n config?: object;\n }) =>\n setupSegmentLoading({\n state,\n context,\n config: { ...AUDIO_SEGMENT_LOADING_CONFIG, ...config },\n }),\n});\n\nexport const loadTextTrackSegments = defineBehavior({\n stateKeys: ['presentation', 'preload', 'currentTime', 'loadActivated', 'selectedTextTrackId'],\n contextKeys: ['textTrackSegmentLoaderActor'],\n setup: ({\n state,\n context,\n config = {},\n }: {\n state: SegmentLoadingStateMap<'selectedTextTrackId'>;\n context: {\n textTrackSegmentLoaderActor: ReadonlySignal<SegmentLoadingContext['textTrackSegmentLoaderActor']>;\n };\n config?: object;\n }) =>\n setupSegmentLoading({\n state,\n context,\n config: { ...TEXT_SEGMENT_LOADING_CONFIG, ...config },\n }),\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8HA,SAAS,oBAIP,EACA,OACA,SACA,UAa+D;CAC/D,MAAM,EAAE,aAAa,WAAW,sBAAsB;CACtD,MAAM,iBAAiB,OAAO,eAAe,kBAAkB,8BAA8B;CAE7F,MAAM,gBAAgB,eACpB,kBAAkB,MAAM,aAAa,KAAK,EAAE,MAAM,aAAa,KAAK,CAAC,CACtE;CAKD,MAAM,wBAAwB,eAAe;EAC3C,MAAM,QAAQ,cAAc,KAAK;AACjC,MAAI,CAAC,MAAO,QAAO,KAAA;AACnB,SAAO,oBAAoB,MAAM,YAAY,KAAK,IAAI,GAAG,MAAM,SAAS;GACxE;CAEF,MAAM,qBAAqB,eAAuC;AAChE,MAAI,CAAC,QAAQ,WAAW,KAAK,IAAI,CAAC,cAAc,KAAK,CAAE,QAAO;AAC9D,MAAI,MAAM,cAAc,KAAK,IAAI,MAAM,QAAQ,KAAK,KAAK,OAAQ,QAAO;AACxE,MAAI,MAAM,QAAQ,KAAK,KAAK,OAAQ,QAAO;AAC3C,SAAO;GACP;AAEF,QAAO,qBAA6C;EAClD,SAAS;EACT,eAAe,mBAAmB,KAAK;EACvC,QAAQ;GACN,uBAAuB,EAAE;GACzB,SAAS,EAAE;GACX,iBAAiB,EAOf,aAAa;IACX,MAAM,QAAQ,cAAc,KAAK;AACjC,YAAQ,WAAW,KAAK,CAAE,KAAK;KAAE,MAAM;KAAQ;KAAO,CAAC;MAE1D;GACD,cAAc,EAIZ,eAAe;IACb,MAAM,QAAQ,cAAc,KAAK;AACjC,0BAAsB,KAAK;IAC3B,MAAM,cAAc,KAAK,MAAM,YAAY,IAAI;AAC/C,SAAK,QAAQ,WAAW,CAAE,KAAK;KAC7B,MAAM;KACN;KACA,OAAO;MAAE,OAAO;MAAa,KAAK,cAAc;MAAgB;KACjE,CAAC;MAEL;GACF;EACF,CAAC;;AAOJ,MAAM,+BAA+B;CACnC,GAAG;CACH,mBAAmB;CACpB;AAED,MAAM,+BAA+B;CACnC,GAAG;CACH,mBAAmB;CACpB;AAED,MAAM,8BAA8B;CAClC,GAAG;CACH,mBAAmB;CACpB;AAMD,MAAa,oBAAoB,eAAe;CAC9C,WAAW;EAAC;EAAgB;EAAW;EAAe;EAAiB;EAAuB;CAC9F,aAAa,CAAC,0BAA0B;CACxC,QAAQ,EACN,OACA,SACA,SAAS,EAAE,OAQX,oBAAoB;EAClB;EACA;EACA,QAAQ;GAAE,GAAG;GAA8B,GAAG;GAAQ;EACvD,CAAC;CACL,CAAC;AAEF,MAAa,oBAAoB,eAAe;CAC9C,WAAW;EAAC;EAAgB;EAAW;EAAe;EAAiB;EAAuB;CAC9F,aAAa,CAAC,0BAA0B;CACxC,QAAQ,EACN,OACA,SACA,SAAS,EAAE,OAQX,oBAAoB;EAClB;EACA;EACA,QAAQ;GAAE,GAAG;GAA8B,GAAG;GAAQ;EACvD,CAAC;CACL,CAAC;AAEF,MAAa,wBAAwB,eAAe;CAClD,WAAW;EAAC;EAAgB;EAAW;EAAe;EAAiB;EAAsB;CAC7F,aAAa,CAAC,8BAA8B;CAC5C,QAAQ,EACN,OACA,SACA,SAAS,EAAE,OAQX,oBAAoB;EAClB;EACA;EACA,QAAQ;GAAE,GAAG;GAA6B,GAAG;GAAQ;EACtD,CAAC;CACL,CAAC"}
1
+ {"version":3,"file":"load-segments.js","names":[],"sources":["../../../../../src/playback/behaviors/dom/load-segments.ts"],"sourcesContent":["/**\n * **Per-type segment loading dispatch.** Per available track type (video /\n * audio / text), reads the per-type segment-loader actor from context and\n * sends typed `'load'` messages whenever a meaningful loading condition\n * changes (selected track, current time crossing a segment boundary).\n *\n * Loader-actor lifecycle is owned upstream:\n * - Video: `setupVideoBufferActors`\n * - Audio: `setupAudioBufferActors`\n * - Text: `setupTextTrackActors`\n *\n * This behavior is pure-consumer: it reads `context[loaderKey]` and\n * dispatches typed messages via the variant's per-type `findResolvedTrack`\n * resolver.\n *\n * # Load modes as reactor states\n *\n * Four states encode the load-gating policy directly:\n *\n * - `'preconditions-unmet'` — no loader actor in context, or the selected\n * track hasn't resolved.\n * - `'dormant'` — `preload === 'none' && !loadActivated`. Nothing fires.\n * - `'metadata-only'` — `!loadActivated && preload !== 'auto' && preload !== 'none'`.\n * Fires an init-segment-only `load` message **once on entry**. The\n * variant's loader actor decides what to do — v/a's actor fetches the\n * init segment; text's actor no-ops (no init concept).\n * - `'full-range'` — `loadActivated || preload === 'auto'`. Effect re-fires\n * on selected-track change and on segment-boundary crossing.\n *\n * # Per-type parameterization (inference-driven)\n *\n * The helper is generic over `Track` — the resolved-track type. Each\n * variant supplies its own `findResolvedTrack` resolver via config; TS\n * infers `Track` from the resolver's return type. The loader signal's\n * value type is constrained to `SegmentLoaderLike<Track>` — anything with\n * a `send` method accepting the `Track`-parameterized message. Concrete\n * actor types (`SegmentLoaderActor`, `TextTrackSegmentLoaderActor`)\n * satisfy this via function-parameter contravariance: an actor whose\n * `.send` accepts a wider track type is assignable to a slot expecting a\n * narrower track type.\n *\n * No widening of actor message types is needed; no casts inside the\n * helper. Per-variant wiring (right loader paired with right resolver)\n * is enforced at the variant call site.\n */\nimport { defineBehavior } from '../../../core/composition/create-composition';\nimport type { Reactor } from '../../../core/reactors/create-machine-reactor';\nimport { createMachineReactor } from '../../../core/reactors/create-machine-reactor';\nimport { computed, peek, type ReadonlySignal } from '../../../core/signals/primitives';\nimport {\n DEFAULT_FORWARD_BUFFER_CONFIG,\n type ForwardBufferConfig,\n segmentStartForTime,\n} from '../../../media/buffer/forward-buffer';\nimport type { MaybeResolvedPresentation, Segment } from '../../../media/types';\nimport { findResolvedAudioTrack, findResolvedTextTrack, findResolvedVideoTrack } from '../../../media/utils/tracks';\nimport type { BufferState, SegmentLoaderActor, SourceBufferState } from '../../actors/dom/segment-loader';\nimport type { TextTrackSegmentLoaderActor } from '../../actors/text-track-segment-loader';\nimport { AUDIO_TYPE_CONFIG, TEXT_TYPE_CONFIG, VIDEO_TYPE_CONFIG } from '../../primitives/track-types';\n\n// Re-export buffer state types for consumers that import them from this module.\nexport type { BufferState, SourceBufferState };\n\n// ============================================================================\n// STATE & CONTEXT\n// ============================================================================\n\n/** State shape for segment loading. */\nexport interface SegmentLoadingState {\n presentation?: MaybeResolvedPresentation;\n preload?: string;\n /** Current playback position in seconds. Defaults to 0 when undefined. */\n currentTime?: number;\n /** True once a preload-overriding event has fired for the current source. */\n loadActivated?: boolean;\n selectedVideoTrackId?: string;\n selectedAudioTrackId?: string;\n selectedTextTrackId?: string;\n}\n\n/** Context shape for segment loading. Each variant only consumes its own type's loader actor. */\nexport interface SegmentLoadingContext {\n videoSegmentLoaderActor?: SegmentLoaderActor;\n audioSegmentLoaderActor?: SegmentLoaderActor;\n textTrackSegmentLoaderActor?: TextTrackSegmentLoaderActor;\n}\n\n// ============================================================================\n// REACTOR\n// ============================================================================\n\ntype SegmentLoadingFsmState = 'preconditions-unmet' | 'dormant' | 'metadata-only' | 'full-range';\n\ntype SelectedTrackKey = 'selectedVideoTrackId' | 'selectedAudioTrackId' | 'selectedTextTrackId';\ntype SegmentLoaderActorKey = 'videoSegmentLoaderActor' | 'audioSegmentLoaderActor' | 'textTrackSegmentLoaderActor';\n\ntype SegmentLoadingStateMap<K extends SelectedTrackKey> = {\n presentation: ReadonlySignal<SegmentLoadingState['presentation']>;\n preload: ReadonlySignal<SegmentLoadingState['preload']>;\n currentTime: ReadonlySignal<SegmentLoadingState['currentTime']>;\n loadActivated: ReadonlySignal<SegmentLoadingState['loadActivated']>;\n} & { [P in K]: ReadonlySignal<SegmentLoadingState[P]> };\n\n/** Shared `'load'` message shape parameterized over the resolved track type. */\ninterface LoadMessage<Track> {\n type: 'load';\n track: Track;\n range?: { start: number; end: number };\n}\n\n/** Structural constraint for any segment-loader-style actor. */\ninterface SegmentLoaderLike<Track> {\n send: (message: LoadMessage<Track>) => void;\n}\n\n/**\n * Specialization helper. Generic over `Track` (inferred from\n * `findResolvedTrack`'s return type). The loader value type is\n * constrained to `SegmentLoaderLike<Track>` — concrete actor types\n * (whose `.send` accepts a wider track union) satisfy this via\n * function-parameter contravariance.\n *\n * Tracks that the helper handles must have a `segments` field — used by\n * `segmentBoundarySignal` to compute the load-anchor boundary. Each\n * variant's resolver narrows to the right resolved-track shape.\n */\nfunction setupSegmentLoading<\n K extends SelectedTrackKey,\n L extends SegmentLoaderActorKey,\n Track extends { segments: readonly Segment[] },\n>({\n state,\n context,\n config,\n}: {\n state: SegmentLoadingStateMap<K>;\n context: { [P in L]: ReadonlySignal<SegmentLoaderLike<Track> | undefined> };\n config: {\n selectedKey: K;\n loaderKey: L;\n findResolvedTrack: (\n presentation: MaybeResolvedPresentation | undefined,\n trackId: string | undefined\n ) => Track | undefined;\n forwardBuffer?: Partial<ForwardBufferConfig>;\n };\n}): Reactor<SegmentLoadingFsmState | 'destroying' | 'destroyed'> {\n const { selectedKey, loaderKey, findResolvedTrack } = config;\n const bufferDuration = config.forwardBuffer?.bufferDuration ?? DEFAULT_FORWARD_BUFFER_CONFIG.bufferDuration;\n\n const selectedTrack = computed<Track | undefined>(() =>\n findResolvedTrack(state.presentation.get(), state[selectedKey].get())\n );\n\n // Segment-boundary signal — `segmentStartForTime` returns the same number\n // while `currentTime` stays inside one segment, so signal-polyfill's\n // `Object.is` equality on this computed dedups within-segment ticks.\n const segmentBoundarySignal = computed(() => {\n const track = selectedTrack.get();\n if (!track) return undefined;\n return segmentStartForTime(state.currentTime.get() ?? 0, track.segments);\n });\n\n const derivedStateSignal = computed<SegmentLoadingFsmState>(() => {\n if (!context[loaderKey].get() || !selectedTrack.get()) return 'preconditions-unmet';\n if (state.loadActivated.get() || state.preload.get() === 'auto') return 'full-range';\n if (state.preload.get() === 'none') return 'dormant';\n return 'metadata-only';\n });\n\n return createMachineReactor<SegmentLoadingFsmState>({\n initial: 'preconditions-unmet',\n monitor: () => derivedStateSignal.get(),\n states: {\n 'preconditions-unmet': {},\n dormant: {},\n 'metadata-only': {\n // Fires once on entry. Matches HTMLMediaElement's preload='metadata'\n // semantics — load enough to surface metadata for the entry-time\n // selection. Each variant's loader decides what to do: v/a's actor\n // fetches the init segment; text's actor treats no-range as a\n // no-op. Selection changes while still in `'metadata-only'` are\n // intentionally not followed.\n entry: () => {\n const track = selectedTrack.get()!;\n context[loaderKey].get()!.send({ type: 'load', track });\n },\n },\n 'full-range': {\n // Re-fires on selected-track changes and on segment-boundary\n // crossings. The loader and `currentTime` are peeked — the\n // boundary-dedup signal already handles re-firing policy.\n effects: () => {\n const track = selectedTrack.get()!;\n segmentBoundarySignal.get();\n const currentTime = peek(state.currentTime) ?? 0;\n peek(context[loaderKey])!.send({\n type: 'load',\n track,\n range: { start: currentTime, end: currentTime + bufferDuration },\n });\n },\n },\n },\n });\n}\n\n// ============================================================================\n// Per-helper-per-type configs — defaults that variants spread engine config over\n// ============================================================================\n\nconst VIDEO_SEGMENT_LOADING_CONFIG = {\n ...VIDEO_TYPE_CONFIG,\n findResolvedTrack: findResolvedVideoTrack,\n} as const;\n\nconst AUDIO_SEGMENT_LOADING_CONFIG = {\n ...AUDIO_TYPE_CONFIG,\n findResolvedTrack: findResolvedAudioTrack,\n} as const;\n\nconst TEXT_SEGMENT_LOADING_CONFIG = {\n ...TEXT_TYPE_CONFIG,\n findResolvedTrack: findResolvedTextTrack,\n} as const;\n\n// ============================================================================\n// Specialized exports — one per media type\n// ============================================================================\n\nexport const loadVideoSegments = defineBehavior({\n stateKeys: ['presentation', 'preload', 'currentTime', 'loadActivated', 'selectedVideoTrackId'],\n contextKeys: ['videoSegmentLoaderActor'],\n setup: ({\n state,\n context,\n config = {},\n }: {\n state: SegmentLoadingStateMap<'selectedVideoTrackId'>;\n context: {\n videoSegmentLoaderActor: ReadonlySignal<SegmentLoadingContext['videoSegmentLoaderActor']>;\n };\n config?: object;\n }) =>\n setupSegmentLoading({\n state,\n context,\n config: { ...VIDEO_SEGMENT_LOADING_CONFIG, ...config },\n }),\n});\n\nexport const loadAudioSegments = defineBehavior({\n stateKeys: ['presentation', 'preload', 'currentTime', 'loadActivated', 'selectedAudioTrackId'],\n contextKeys: ['audioSegmentLoaderActor'],\n setup: ({\n state,\n context,\n config = {},\n }: {\n state: SegmentLoadingStateMap<'selectedAudioTrackId'>;\n context: {\n audioSegmentLoaderActor: ReadonlySignal<SegmentLoadingContext['audioSegmentLoaderActor']>;\n };\n config?: object;\n }) =>\n setupSegmentLoading({\n state,\n context,\n config: { ...AUDIO_SEGMENT_LOADING_CONFIG, ...config },\n }),\n});\n\nexport const loadTextTrackSegments = defineBehavior({\n stateKeys: ['presentation', 'preload', 'currentTime', 'loadActivated', 'selectedTextTrackId'],\n contextKeys: ['textTrackSegmentLoaderActor'],\n setup: ({\n state,\n context,\n config = {},\n }: {\n state: SegmentLoadingStateMap<'selectedTextTrackId'>;\n context: {\n textTrackSegmentLoaderActor: ReadonlySignal<SegmentLoadingContext['textTrackSegmentLoaderActor']>;\n };\n config?: object;\n }) =>\n setupSegmentLoading({\n state,\n context,\n config: { ...TEXT_SEGMENT_LOADING_CONFIG, ...config },\n }),\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8HA,SAAS,oBAIP,EACA,OACA,SACA,UAa+D;CAC/D,MAAM,EAAE,aAAa,WAAW,sBAAsB;CACtD,MAAM,iBAAiB,OAAO,eAAe,kBAAkB,8BAA8B;CAE7F,MAAM,gBAAgB,eACpB,kBAAkB,MAAM,aAAa,KAAK,EAAE,MAAM,aAAa,KAAK,CAAC,CACtE;CAKD,MAAM,wBAAwB,eAAe;EAC3C,MAAM,QAAQ,cAAc,KAAK;AACjC,MAAI,CAAC,MAAO,QAAO,KAAA;AACnB,SAAO,oBAAoB,MAAM,YAAY,KAAK,IAAI,GAAG,MAAM,SAAS;GACxE;CAEF,MAAM,qBAAqB,eAAuC;AAChE,MAAI,CAAC,QAAQ,WAAW,KAAK,IAAI,CAAC,cAAc,KAAK,CAAE,QAAO;AAC9D,MAAI,MAAM,cAAc,KAAK,IAAI,MAAM,QAAQ,KAAK,KAAK,OAAQ,QAAO;AACxE,MAAI,MAAM,QAAQ,KAAK,KAAK,OAAQ,QAAO;AAC3C,SAAO;GACP;AAEF,QAAO,qBAA6C;EAClD,SAAS;EACT,eAAe,mBAAmB,KAAK;EACvC,QAAQ;GACN,uBAAuB,EAAE;GACzB,SAAS,EAAE;GACX,iBAAiB,EAOf,aAAa;IACX,MAAM,QAAQ,cAAc,KAAK;AACjC,YAAQ,WAAW,KAAK,CAAE,KAAK;KAAE,MAAM;KAAQ;KAAO,CAAC;MAE1D;GACD,cAAc,EAIZ,eAAe;IACb,MAAM,QAAQ,cAAc,KAAK;AACjC,0BAAsB,KAAK;IAC3B,MAAM,cAAc,KAAK,MAAM,YAAY,IAAI;AAC/C,SAAK,QAAQ,WAAW,CAAE,KAAK;KAC7B,MAAM;KACN;KACA,OAAO;MAAE,OAAO;MAAa,KAAK,cAAc;MAAgB;KACjE,CAAC;MAEL;GACF;EACF,CAAC;;AAOJ,MAAM,+BAA+B;CACnC,GAAG;CACH,mBAAmB;CACpB;AAED,MAAM,+BAA+B;CACnC,GAAG;CACH,mBAAmB;CACpB;AAED,MAAM,8BAA8B;CAClC,GAAG;CACH,mBAAmB;CACpB;AAMD,MAAa,oBAAoB,eAAe;CAC9C,WAAW;EAAC;EAAgB;EAAW;EAAe;EAAiB;EAAuB;CAC9F,aAAa,CAAC,0BAA0B;CACxC,QAAQ,EACN,OACA,SACA,SAAS,EAAE,OAQX,oBAAoB;EAClB;EACA;EACA,QAAQ;GAAE,GAAG;GAA8B,GAAG;GAAQ;EACvD,CAAC;CACL,CAAC;AAEF,MAAa,oBAAoB,eAAe;CAC9C,WAAW;EAAC;EAAgB;EAAW;EAAe;EAAiB;EAAuB;CAC9F,aAAa,CAAC,0BAA0B;CACxC,QAAQ,EACN,OACA,SACA,SAAS,EAAE,OAQX,oBAAoB;EAClB;EACA;EACA,QAAQ;GAAE,GAAG;GAA8B,GAAG;GAAQ;EACvD,CAAC;CACL,CAAC;AAEF,MAAa,wBAAwB,eAAe;CAClD,WAAW;EAAC;EAAgB;EAAW;EAAe;EAAiB;EAAsB;CAC7F,aAAa,CAAC,8BAA8B;CAC5C,QAAQ,EACN,OACA,SACA,SAAS,EAAE,OAQX,oBAAoB;EAClB;EACA;EACA,QAAQ;GAAE,GAAG;GAA6B,GAAG;GAAQ;EACtD,CAAC;CACL,CAAC"}