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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (211) hide show
  1. package/dist/default/core/abr/bandwidth-estimator.js +2 -3
  2. package/dist/default/core/abr/bandwidth-estimator.js.map +1 -1
  3. package/dist/default/core/abr/ewma.js +1 -1
  4. package/dist/default/core/abr/quality-selection.js +2 -2
  5. package/dist/default/core/abr/quality-selection.js.map +1 -1
  6. package/dist/default/core/buffer/back-buffer.js +2 -2
  7. package/dist/default/core/buffer/forward-buffer.js +1 -1
  8. package/dist/default/core/features/calculate-presentation-duration.js +11 -9
  9. package/dist/default/core/features/calculate-presentation-duration.js.map +1 -1
  10. package/dist/default/core/features/quality-switching.js +7 -6
  11. package/dist/default/core/features/quality-switching.js.map +1 -1
  12. package/dist/default/core/features/resolve-presentation.js +30 -52
  13. package/dist/default/core/features/resolve-presentation.js.map +1 -1
  14. package/dist/default/core/features/resolve-track.js +17 -28
  15. package/dist/default/core/features/resolve-track.js.map +1 -1
  16. package/dist/default/core/features/select-tracks.js +18 -35
  17. package/dist/default/core/features/select-tracks.js.map +1 -1
  18. package/dist/default/core/features/sync-preload-attribute.js +26 -0
  19. package/dist/default/core/features/sync-preload-attribute.js.map +1 -0
  20. package/dist/default/core/hls/parse-attributes.js +1 -1
  21. package/dist/default/core/hls/parse-attributes.js.map +1 -1
  22. package/dist/default/core/hls/parse-media-playlist.js +1 -2
  23. package/dist/default/core/hls/parse-media-playlist.js.map +1 -1
  24. package/dist/default/core/hls/parse-multivariant.js +2 -3
  25. package/dist/default/core/hls/parse-multivariant.js.map +1 -1
  26. package/dist/default/core/hls/resolve-url.js +1 -1
  27. package/dist/default/core/signals/effect.js +41 -0
  28. package/dist/default/core/signals/effect.js.map +1 -0
  29. package/dist/default/core/signals/primitives.js +32 -0
  30. package/dist/default/core/signals/primitives.js.map +1 -0
  31. package/dist/default/core/task.js +3 -3
  32. package/dist/default/core/task.js.map +1 -1
  33. package/dist/default/core/types/index.js +1 -1
  34. package/dist/default/core/types/index.js.map +1 -1
  35. package/dist/default/core/utils/generate-id.js +1 -1
  36. package/dist/default/core/utils/track-selection.js +10 -2
  37. package/dist/default/core/utils/track-selection.js.map +1 -1
  38. package/dist/default/dom/features/end-of-stream.js +24 -48
  39. package/dist/default/dom/features/end-of-stream.js.map +1 -1
  40. package/dist/default/dom/features/load-segments.js +40 -36
  41. package/dist/default/dom/features/load-segments.js.map +1 -1
  42. package/dist/default/dom/features/load-text-track-cues.js +36 -26
  43. package/dist/default/dom/features/load-text-track-cues.js.map +1 -1
  44. package/dist/default/dom/features/segment-loader-actor.js +4 -5
  45. package/dist/default/dom/features/segment-loader-actor.js.map +1 -1
  46. package/dist/default/dom/features/setup-mediasource.js +29 -36
  47. package/dist/default/dom/features/setup-mediasource.js.map +1 -1
  48. package/dist/default/dom/features/setup-sourcebuffer.js +43 -35
  49. package/dist/default/dom/features/setup-sourcebuffer.js.map +1 -1
  50. package/dist/default/dom/features/setup-text-tracks.js +25 -46
  51. package/dist/default/dom/features/setup-text-tracks.js.map +1 -1
  52. package/dist/default/dom/features/sync-selected-text-track-from-dom.js +14 -23
  53. package/dist/default/dom/features/sync-selected-text-track-from-dom.js.map +1 -1
  54. package/dist/default/dom/features/sync-text-track-modes.js +12 -17
  55. package/dist/default/dom/features/sync-text-track-modes.js.map +1 -1
  56. package/dist/default/dom/features/track-current-time.js +10 -7
  57. package/dist/default/dom/features/track-current-time.js.map +1 -1
  58. package/dist/default/dom/features/track-playback-initiated.js +34 -36
  59. package/dist/default/dom/features/track-playback-initiated.js.map +1 -1
  60. package/dist/default/dom/features/track-playback-rate.js +12 -16
  61. package/dist/default/dom/features/track-playback-rate.js.map +1 -1
  62. package/dist/default/dom/features/update-duration.js +19 -13
  63. package/dist/default/dom/features/update-duration.js.map +1 -1
  64. package/dist/default/dom/media/append-segment.js +1 -1
  65. package/dist/default/dom/media/buffer-flusher.js +1 -1
  66. package/dist/default/dom/media/mediasource-setup.js +32 -40
  67. package/dist/default/dom/media/mediasource-setup.js.map +1 -1
  68. package/dist/default/dom/media/source-buffer-actor.js +16 -26
  69. package/dist/default/dom/media/source-buffer-actor.js.map +1 -1
  70. package/dist/default/dom/network/chunked-stream-iterable.js +1 -1
  71. package/dist/default/dom/network/chunked-stream-iterable.js.map +1 -1
  72. package/dist/default/dom/network/fetch.js +1 -2
  73. package/dist/default/dom/network/fetch.js.map +1 -1
  74. package/dist/default/dom/playback-engine/adapter.js +13 -13
  75. package/dist/default/dom/playback-engine/adapter.js.map +1 -1
  76. package/dist/default/dom/playback-engine/engine.js +24 -49
  77. package/dist/default/dom/playback-engine/engine.js.map +1 -1
  78. package/dist/default/dom/playback-engine.js +2 -2
  79. package/dist/default/dom/text/parse-vtt-segment.js +1 -1
  80. package/dist/default/dom.js +1 -2
  81. package/dist/default/index.js +1 -1
  82. package/dist/dev/core/abr/bandwidth-estimator.js +2 -3
  83. package/dist/dev/core/abr/bandwidth-estimator.js.map +1 -1
  84. package/dist/dev/core/abr/ewma.js +1 -1
  85. package/dist/dev/core/abr/quality-selection.js +2 -2
  86. package/dist/dev/core/abr/quality-selection.js.map +1 -1
  87. package/dist/dev/core/actor.d.ts +6 -19
  88. package/dist/dev/core/actor.d.ts.map +1 -1
  89. package/dist/dev/core/buffer/back-buffer.js +2 -2
  90. package/dist/dev/core/buffer/forward-buffer.js +1 -1
  91. package/dist/dev/core/features/calculate-presentation-duration.js +11 -9
  92. package/dist/dev/core/features/calculate-presentation-duration.js.map +1 -1
  93. package/dist/dev/core/features/quality-switching.js +7 -6
  94. package/dist/dev/core/features/quality-switching.js.map +1 -1
  95. package/dist/dev/core/features/resolve-presentation.js +30 -52
  96. package/dist/dev/core/features/resolve-presentation.js.map +1 -1
  97. package/dist/dev/core/features/resolve-track.js +17 -28
  98. package/dist/dev/core/features/resolve-track.js.map +1 -1
  99. package/dist/dev/core/features/select-tracks.js +18 -35
  100. package/dist/dev/core/features/select-tracks.js.map +1 -1
  101. package/dist/dev/core/features/sync-preload-attribute.js +26 -0
  102. package/dist/dev/core/features/sync-preload-attribute.js.map +1 -0
  103. package/dist/dev/core/hls/parse-attributes.js +1 -1
  104. package/dist/dev/core/hls/parse-attributes.js.map +1 -1
  105. package/dist/dev/core/hls/parse-media-playlist.js +1 -2
  106. package/dist/dev/core/hls/parse-media-playlist.js.map +1 -1
  107. package/dist/dev/core/hls/parse-multivariant.js +2 -3
  108. package/dist/dev/core/hls/parse-multivariant.js.map +1 -1
  109. package/dist/dev/core/hls/resolve-url.js +1 -1
  110. package/dist/dev/core/signals/effect.d.ts +15 -0
  111. package/dist/dev/core/signals/effect.d.ts.map +1 -0
  112. package/dist/dev/core/signals/effect.js +41 -0
  113. package/dist/dev/core/signals/effect.js.map +1 -0
  114. package/dist/dev/core/signals/primitives.d.ts +10 -0
  115. package/dist/dev/core/signals/primitives.d.ts.map +1 -0
  116. package/dist/dev/core/signals/primitives.js +32 -0
  117. package/dist/dev/core/signals/primitives.js.map +1 -0
  118. package/dist/dev/core/task.js +3 -3
  119. package/dist/dev/core/task.js.map +1 -1
  120. package/dist/dev/core/types/index.js +1 -1
  121. package/dist/dev/core/types/index.js.map +1 -1
  122. package/dist/dev/core/utils/generate-id.js +1 -1
  123. package/dist/dev/core/utils/track-selection.js +10 -2
  124. package/dist/dev/core/utils/track-selection.js.map +1 -1
  125. package/dist/dev/dom/features/end-of-stream.js +24 -48
  126. package/dist/dev/dom/features/end-of-stream.js.map +1 -1
  127. package/dist/dev/dom/features/load-segments.d.ts +4 -4
  128. package/dist/dev/dom/features/load-segments.d.ts.map +1 -1
  129. package/dist/dev/dom/features/load-segments.js +40 -36
  130. package/dist/dev/dom/features/load-segments.js.map +1 -1
  131. package/dist/dev/dom/features/load-text-track-cues.d.ts +4 -4
  132. package/dist/dev/dom/features/load-text-track-cues.d.ts.map +1 -1
  133. package/dist/dev/dom/features/load-text-track-cues.js +36 -26
  134. package/dist/dev/dom/features/load-text-track-cues.js.map +1 -1
  135. package/dist/dev/dom/features/segment-loader-actor.d.ts +1 -0
  136. package/dist/dev/dom/features/segment-loader-actor.js +4 -5
  137. package/dist/dev/dom/features/segment-loader-actor.js.map +1 -1
  138. package/dist/dev/dom/features/setup-mediasource.js +29 -36
  139. package/dist/dev/dom/features/setup-mediasource.js.map +1 -1
  140. package/dist/dev/dom/features/setup-sourcebuffer.d.ts.map +1 -1
  141. package/dist/dev/dom/features/setup-sourcebuffer.js +43 -35
  142. package/dist/dev/dom/features/setup-sourcebuffer.js.map +1 -1
  143. package/dist/dev/dom/features/setup-text-tracks.js +25 -46
  144. package/dist/dev/dom/features/setup-text-tracks.js.map +1 -1
  145. package/dist/dev/dom/features/sync-selected-text-track-from-dom.js +14 -23
  146. package/dist/dev/dom/features/sync-selected-text-track-from-dom.js.map +1 -1
  147. package/dist/dev/dom/features/sync-text-track-modes.js +12 -17
  148. package/dist/dev/dom/features/sync-text-track-modes.js.map +1 -1
  149. package/dist/dev/dom/features/track-current-time.d.ts +4 -4
  150. package/dist/dev/dom/features/track-current-time.d.ts.map +1 -1
  151. package/dist/dev/dom/features/track-current-time.js +10 -7
  152. package/dist/dev/dom/features/track-current-time.js.map +1 -1
  153. package/dist/dev/dom/features/track-playback-initiated.d.ts +14 -23
  154. package/dist/dev/dom/features/track-playback-initiated.d.ts.map +1 -1
  155. package/dist/dev/dom/features/track-playback-initiated.js +34 -36
  156. package/dist/dev/dom/features/track-playback-initiated.js.map +1 -1
  157. package/dist/dev/dom/features/track-playback-rate.d.ts +4 -4
  158. package/dist/dev/dom/features/track-playback-rate.d.ts.map +1 -1
  159. package/dist/dev/dom/features/track-playback-rate.js +12 -16
  160. package/dist/dev/dom/features/track-playback-rate.js.map +1 -1
  161. package/dist/dev/dom/features/update-duration.js +19 -13
  162. package/dist/dev/dom/features/update-duration.js.map +1 -1
  163. package/dist/dev/dom/media/append-segment.js +1 -1
  164. package/dist/dev/dom/media/buffer-flusher.js +1 -1
  165. package/dist/dev/dom/media/mediasource-setup.js +32 -40
  166. package/dist/dev/dom/media/mediasource-setup.js.map +1 -1
  167. package/dist/dev/dom/media/source-buffer-actor.d.ts +2 -2
  168. package/dist/dev/dom/media/source-buffer-actor.d.ts.map +1 -1
  169. package/dist/dev/dom/media/source-buffer-actor.js +16 -26
  170. package/dist/dev/dom/media/source-buffer-actor.js.map +1 -1
  171. package/dist/dev/dom/network/chunked-stream-iterable.js +1 -1
  172. package/dist/dev/dom/network/chunked-stream-iterable.js.map +1 -1
  173. package/dist/dev/dom/network/fetch.js +1 -2
  174. package/dist/dev/dom/network/fetch.js.map +1 -1
  175. package/dist/dev/dom/playback-engine/adapter.d.ts.map +1 -1
  176. package/dist/dev/dom/playback-engine/adapter.js +13 -13
  177. package/dist/dev/dom/playback-engine/adapter.js.map +1 -1
  178. package/dist/dev/dom/playback-engine/engine.d.ts +14 -26
  179. package/dist/dev/dom/playback-engine/engine.d.ts.map +1 -1
  180. package/dist/dev/dom/playback-engine/engine.js +24 -49
  181. package/dist/dev/dom/playback-engine/engine.js.map +1 -1
  182. package/dist/dev/dom/playback-engine.d.ts +3 -2
  183. package/dist/dev/dom/playback-engine.js +2 -2
  184. package/dist/dev/dom/text/parse-vtt-segment.js +1 -1
  185. package/dist/dev/dom.js +1 -2
  186. package/dist/dev/index.js +1 -1
  187. package/package.json +5 -4
  188. package/dist/default/all.js +0 -16
  189. package/dist/default/core/events/create-event-stream.js +0 -48
  190. package/dist/default/core/events/create-event-stream.js.map +0 -1
  191. package/dist/default/core/reactive/combine-latest.js +0 -72
  192. package/dist/default/core/reactive/combine-latest.js.map +0 -1
  193. package/dist/default/core/state/create-state.js +0 -143
  194. package/dist/default/core/state/create-state.js.map +0 -1
  195. package/dist/dev/all.js +0 -16
  196. package/dist/dev/core/events/create-event-stream.d.ts +0 -48
  197. package/dist/dev/core/events/create-event-stream.d.ts.map +0 -1
  198. package/dist/dev/core/events/create-event-stream.js +0 -48
  199. package/dist/dev/core/events/create-event-stream.js.map +0 -1
  200. package/dist/dev/core/features/resolve-presentation.d.ts +0 -16
  201. package/dist/dev/core/features/resolve-presentation.d.ts.map +0 -1
  202. package/dist/dev/core/features/resolve-track.d.ts +0 -13
  203. package/dist/dev/core/features/resolve-track.d.ts.map +0 -1
  204. package/dist/dev/core/features/select-tracks.d.ts +0 -11
  205. package/dist/dev/core/features/select-tracks.d.ts.map +0 -1
  206. package/dist/dev/core/reactive/combine-latest.js +0 -72
  207. package/dist/dev/core/reactive/combine-latest.js.map +0 -1
  208. package/dist/dev/core/state/create-state.d.ts +0 -104
  209. package/dist/dev/core/state/create-state.d.ts.map +0 -1
  210. package/dist/dev/core/state/create-state.js +0 -143
  211. package/dist/dev/core/state/create-state.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"mediasource-setup.js","names":[],"sources":["../../../../src/dom/media/mediasource-setup.ts"],"sourcesContent":["/**\n * MediaSource Setup\n *\n * Utilities for creating and configuring MediaSource/ManagedMediaSource\n * for MSE (Media Source Extensions) playback.\n *\n * Global ManagedMediaSource types are defined in ./mediasource.d.ts\n */\n\n/**\n * Check if MediaSource API is supported.\n */\nexport function supportsMediaSource(): boolean {\n return typeof MediaSource !== 'undefined';\n}\n\n/**\n * Check if ManagedMediaSource API is supported.\n * ManagedMediaSource is a newer Safari API with better lifecycle management.\n */\nexport function supportsManagedMediaSource(): boolean {\n return typeof ManagedMediaSource !== 'undefined';\n}\n\n/**\n * Options for creating a MediaSource.\n */\nexport interface CreateMediaSourceOptions {\n /** Prefer ManagedMediaSource when available (default: false for broader compatibility). */\n preferManaged?: boolean;\n}\n\n/**\n * Create a MediaSource or ManagedMediaSource instance.\n *\n * @param options - Creation options\n * @returns A MediaSource or ManagedMediaSource instance\n * @throws Error if no MediaSource API is available\n *\n * @example\n * const mediaSource = createMediaSource();\n * const mediaElement = document.querySelector('video');\n * attachMediaSource(mediaSource, mediaElement);\n */\nexport function createMediaSource(options: CreateMediaSourceOptions = {}): MediaSource {\n const { preferManaged = false } = options;\n\n if (preferManaged && supportsManagedMediaSource()) {\n return new ManagedMediaSource!();\n }\n\n if (supportsMediaSource()) {\n return new MediaSource();\n }\n\n throw new Error('MediaSource API is not supported');\n}\n\n/**\n * Result of attaching a MediaSource to a media element.\n */\nexport interface AttachMediaSourceResult {\n /** The object URL created for the MediaSource (empty string for ManagedMediaSource). */\n url: string;\n /** Detach the MediaSource and clean up resources. */\n detach: () => void;\n}\n\n/**\n * Attach a MediaSource to an HTMLMediaElement.\n *\n * Uses srcObject for ManagedMediaSource (Safari), or createObjectURL for regular MediaSource.\n *\n * @param mediaSource - The MediaSource to attach\n * @param mediaElement - The media element to attach to\n * @returns Object with URL and detach function\n *\n * @example\n * const mediaSource = createMediaSource();\n * const { detach } = attachMediaSource(mediaSource, videoElement);\n * await waitForSourceOpen(mediaSource);\n * // Use mediaSource...\n * // Later, to clean up:\n * detach();\n */\nexport function attachMediaSource(mediaSource: MediaSource, mediaElement: HTMLMediaElement): AttachMediaSourceResult {\n // ManagedMediaSource requires srcObject instead of createObjectURL\n const isManagedMediaSource = supportsManagedMediaSource() && mediaSource instanceof ManagedMediaSource!;\n\n if (isManagedMediaSource) {\n // ManagedMediaSource requires disableRemotePlayback — without it Safari\n // will not fire sourceopen.\n (mediaElement as HTMLMediaElement & { disableRemotePlayback: boolean }).disableRemotePlayback = true;\n\n // Use srcObject for ManagedMediaSource\n (mediaElement as HTMLMediaElement & { srcObject: MediaSource | null }).srcObject = mediaSource;\n\n const detach = (): void => {\n (mediaElement as HTMLMediaElement & { srcObject: MediaSource | null }).srcObject = null;\n mediaElement.load(); // Reset the element\n };\n\n return { url: '', detach };\n }\n\n // Use createObjectURL for regular MediaSource\n const url = URL.createObjectURL(mediaSource);\n mediaElement.src = url;\n\n const detach = (): void => {\n mediaElement.removeAttribute('src');\n mediaElement.load(); // Reset the element\n URL.revokeObjectURL(url);\n };\n\n return { url, detach };\n}\n\n/**\n * Wait for a MediaSource to reach the 'open' state.\n * Resolves immediately if already open.\n *\n * @param mediaSource - The MediaSource to wait for\n * @param signal - Optional AbortSignal for cancellation\n * @returns Promise that resolves when the MediaSource is open\n *\n * @example\n * const mediaSource = createMediaSource();\n * attachMediaSource(mediaSource, videoElement);\n * await waitForSourceOpen(mediaSource);\n * // MediaSource is now ready for SourceBuffer creation\n */\nexport function waitForSourceOpen(mediaSource: MediaSource, signal?: AbortSignal): Promise<void> {\n return new Promise((resolve, reject) => {\n if (mediaSource.readyState === 'open') {\n resolve();\n return;\n }\n\n // Check if already aborted\n if (signal?.aborted) {\n reject(new DOMException('Aborted', 'AbortError'));\n return;\n }\n\n // Use internal AbortController for cleanup coordination\n const controller = new AbortController();\n const options = { signal: controller.signal };\n\n mediaSource.addEventListener(\n 'sourceopen',\n () => {\n controller.abort();\n resolve();\n },\n options\n );\n\n signal?.addEventListener(\n 'abort',\n () => {\n controller.abort();\n reject(new DOMException('Aborted', 'AbortError'));\n },\n options\n );\n });\n}\n\n/**\n * Create a SourceBuffer on a MediaSource.\n *\n * @param mediaSource - The MediaSource (must be in 'open' state)\n * @param mimeCodec - MIME type with codecs (e.g., 'video/mp4; codecs=\"avc1.42E01E\"')\n * @returns The created SourceBuffer\n * @throws Error if MediaSource is not open or codec is unsupported\n *\n * @example\n * await waitForSourceOpen(mediaSource);\n * const buffer = createSourceBuffer(mediaSource, 'video/mp4; codecs=\"avc1.42E01E\"');\n */\nexport function createSourceBuffer(mediaSource: MediaSource, mimeCodec: string): SourceBuffer {\n if (mediaSource.readyState !== 'open') {\n throw new Error('MediaSource is not open');\n }\n\n if (!isCodecSupported(mimeCodec)) {\n throw new Error(`Codec not supported: ${mimeCodec}`);\n }\n\n return mediaSource.addSourceBuffer(mimeCodec);\n}\n\n/**\n * Check if a codec is supported.\n *\n * @param mimeCodec - MIME type with codecs string\n * @returns True if the codec is supported\n *\n * @example\n * if (isCodecSupported('video/mp4; codecs=\"avc1.42E01E\"')) {\n * // Create source buffer\n * }\n */\nexport function isCodecSupported(mimeCodec: string): boolean {\n if (!supportsMediaSource()) {\n return false;\n }\n\n return MediaSource.isTypeSupported(mimeCodec);\n}\n"],"mappings":";;;;;;;;;;;;AAYA,SAAgB,sBAA+B;AAC7C,QAAO,OAAO,gBAAgB;;;;;;AAOhC,SAAgB,6BAAsC;AACpD,QAAO,OAAO,uBAAuB;;;;;;;;;;;;;;AAuBvC,SAAgB,kBAAkB,UAAoC,EAAE,EAAe;CACrF,MAAM,EAAE,gBAAgB,UAAU;AAElC,KAAI,iBAAiB,4BAA4B,CAC/C,QAAO,IAAI,oBAAqB;AAGlC,KAAI,qBAAqB,CACvB,QAAO,IAAI,aAAa;AAG1B,OAAM,IAAI,MAAM,mCAAmC;;;;;;;;;;;;;;;;;;;AA8BrD,SAAgB,kBAAkB,aAA0B,cAAyD;AAInH,KAF6B,4BAA4B,IAAI,uBAAuB,oBAE1D;AAGxB,EAAC,aAAuE,wBAAwB;AAGhG,EAAC,aAAsE,YAAY;EAEnF,MAAM,eAAqB;AACzB,GAAC,aAAsE,YAAY;AACnF,gBAAa,MAAM;;AAGrB,SAAO;GAAE,KAAK;GAAI;GAAQ;;CAI5B,MAAM,MAAM,IAAI,gBAAgB,YAAY;AAC5C,cAAa,MAAM;CAEnB,MAAM,eAAqB;AACzB,eAAa,gBAAgB,MAAM;AACnC,eAAa,MAAM;AACnB,MAAI,gBAAgB,IAAI;;AAG1B,QAAO;EAAE;EAAK;EAAQ;;;;;;;;;;;;;;;;AAiBxB,SAAgB,kBAAkB,aAA0B,QAAqC;AAC/F,QAAO,IAAI,SAAS,SAAS,WAAW;AACtC,MAAI,YAAY,eAAe,QAAQ;AACrC,YAAS;AACT;;AAIF,MAAI,QAAQ,SAAS;AACnB,UAAO,IAAI,aAAa,WAAW,aAAa,CAAC;AACjD;;EAIF,MAAM,aAAa,IAAI,iBAAiB;EACxC,MAAM,UAAU,EAAE,QAAQ,WAAW,QAAQ;AAE7C,cAAY,iBACV,oBACM;AACJ,cAAW,OAAO;AAClB,YAAS;KAEX,QACD;AAED,UAAQ,iBACN,eACM;AACJ,cAAW,OAAO;AAClB,UAAO,IAAI,aAAa,WAAW,aAAa,CAAC;KAEnD,QACD;GACD;;;;;;;;;;;;;;AAeJ,SAAgB,mBAAmB,aAA0B,WAAiC;AAC5F,KAAI,YAAY,eAAe,OAC7B,OAAM,IAAI,MAAM,0BAA0B;AAG5C,KAAI,CAAC,iBAAiB,UAAU,CAC9B,OAAM,IAAI,MAAM,wBAAwB,YAAY;AAGtD,QAAO,YAAY,gBAAgB,UAAU;;;;;;;;;;;;;AAc/C,SAAgB,iBAAiB,WAA4B;AAC3D,KAAI,CAAC,qBAAqB,CACxB,QAAO;AAGT,QAAO,YAAY,gBAAgB,UAAU"}
1
+ {"version":3,"file":"mediasource-setup.js","names":[],"sources":["../../../../src/dom/media/mediasource-setup.ts"],"sourcesContent":["/**\n * MediaSource Setup\n *\n * Utilities for creating and configuring MediaSource/ManagedMediaSource\n * for MSE (Media Source Extensions) playback.\n *\n * Global ManagedMediaSource types are defined in ./mediasource.d.ts\n */\n\nimport { type ReadonlySignal, signal } from '../../core/signals/primitives';\n\n/**\n * Check if MediaSource API is supported.\n */\nexport function supportsMediaSource(): boolean {\n return typeof MediaSource !== 'undefined';\n}\n\n/**\n * Check if ManagedMediaSource API is supported.\n * ManagedMediaSource is a newer Safari API with better lifecycle management.\n */\nexport function supportsManagedMediaSource(): boolean {\n return typeof ManagedMediaSource !== 'undefined';\n}\n\n/**\n * Options for creating a MediaSource.\n */\nexport interface CreateMediaSourceOptions {\n /** Prefer ManagedMediaSource when available (default: false for broader compatibility). */\n preferManaged?: boolean;\n}\n\n/**\n * Create a MediaSource or ManagedMediaSource instance.\n *\n * @param options - Creation options\n * @returns A MediaSource or ManagedMediaSource instance\n * @throws Error if no MediaSource API is available\n *\n * @example\n * const mediaSource = createMediaSource();\n * const mediaElement = document.querySelector('video');\n * attachMediaSource(mediaSource, mediaElement);\n */\nexport function createMediaSource(options: CreateMediaSourceOptions = {}): MediaSource {\n const { preferManaged = false } = options;\n\n if (preferManaged && supportsManagedMediaSource()) {\n return new ManagedMediaSource!();\n }\n\n if (supportsMediaSource()) {\n return new MediaSource();\n }\n\n throw new Error('MediaSource API is not supported');\n}\n\n/**\n * Result of attaching a MediaSource to a media element.\n */\nexport interface AttachMediaSourceResult {\n /** The object URL created for the MediaSource (empty string for ManagedMediaSource). */\n url: string;\n /** Detach the MediaSource and clean up resources. */\n detach: () => void;\n}\n\n/**\n * Attach a MediaSource to an HTMLMediaElement.\n *\n * Uses srcObject for ManagedMediaSource (Safari), or createObjectURL for regular MediaSource.\n *\n * @param mediaSource - The MediaSource to attach\n * @param mediaElement - The media element to attach to\n * @returns Object with URL and detach function\n *\n * @example\n * const mediaSource = createMediaSource();\n * const { detach } = attachMediaSource(mediaSource, videoElement);\n * // Use mediaSource...\n * // Later, to clean up:\n * detach();\n */\nexport function attachMediaSource(mediaSource: MediaSource, mediaElement: HTMLMediaElement): AttachMediaSourceResult {\n // ManagedMediaSource requires srcObject instead of createObjectURL\n const isManagedMediaSource = supportsManagedMediaSource() && mediaSource instanceof ManagedMediaSource!;\n\n if (isManagedMediaSource) {\n // ManagedMediaSource requires disableRemotePlayback — without it Safari\n // will not fire sourceopen.\n (mediaElement as HTMLMediaElement & { disableRemotePlayback: boolean }).disableRemotePlayback = true;\n\n // Use srcObject for ManagedMediaSource\n (mediaElement as HTMLMediaElement & { srcObject: MediaSource | null }).srcObject = mediaSource;\n\n const detach = (): void => {\n (mediaElement as HTMLMediaElement & { srcObject: MediaSource | null }).srcObject = null;\n mediaElement.load(); // Reset the element\n };\n\n return { url: '', detach };\n }\n\n // Use createObjectURL for regular MediaSource\n const url = URL.createObjectURL(mediaSource);\n mediaElement.src = url;\n\n const detach = (): void => {\n mediaElement.removeAttribute('src');\n mediaElement.load(); // Reset the element\n URL.revokeObjectURL(url);\n };\n\n return { url, detach };\n}\n\n/**\n * Create a SourceBuffer on a MediaSource.\n *\n * @param mediaSource - The MediaSource (must be in 'open' state)\n * @param mimeCodec - MIME type with codecs (e.g., 'video/mp4; codecs=\"avc1.42E01E\"')\n * @returns The created SourceBuffer\n * @throws Error if MediaSource is not open or codec is unsupported\n *\n * @example\n * const buffer = createSourceBuffer(mediaSource, 'video/mp4; codecs=\"avc1.42E01E\"');\n */\nexport function createSourceBuffer(mediaSource: MediaSource, mimeCodec: string): SourceBuffer {\n if (mediaSource.readyState !== 'open') {\n throw new Error('MediaSource is not open');\n }\n\n if (!isCodecSupported(mimeCodec)) {\n throw new Error(`Codec not supported: ${mimeCodec}`);\n }\n\n return mediaSource.addSourceBuffer(mimeCodec);\n}\n\n/**\n * Check if a codec is supported.\n *\n * @param mimeCodec - MIME type with codecs string\n * @returns True if the codec is supported\n *\n * @example\n * if (isCodecSupported('video/mp4; codecs=\"avc1.42E01E\"')) {\n * // Create source buffer\n * }\n */\nexport function isCodecSupported(mimeCodec: string): boolean {\n if (!supportsMediaSource()) {\n return false;\n }\n\n return MediaSource.isTypeSupported(mimeCodec);\n}\n\n/**\n * Create a reactive signal that mirrors `mediaSource.readyState`.\n *\n * Listens to `sourceopen`, `sourceended`, and `sourceclose` events and updates\n * the signal accordingly, making readyState visible to the TC39 signal graph.\n * Listeners are automatically removed when `signal` is aborted.\n *\n * @param mediaSource - The MediaSource to observe\n * @param signal - AbortSignal that controls listener lifetime\n * @returns A `Signal.ReadonlyState` that reflects the current `readyState`\n *\n * @example\n * const controller = new AbortController();\n * const readyState = observeMediaSourceReadyState(mediaSource, controller.signal);\n * effect(() => {\n * if (readyState.get() === 'open') { ... }\n * });\n * // Later:\n * controller.abort();\n */\nexport function observeMediaSourceReadyState(\n mediaSource: MediaSource,\n abortSignal: AbortSignal\n): ReadonlySignal<MediaSource['readyState']> {\n const readyState = signal<MediaSource['readyState']>(mediaSource.readyState);\n const update = () => readyState.set(mediaSource.readyState);\n const options = { signal: abortSignal };\n mediaSource.addEventListener('sourceopen', update, options);\n mediaSource.addEventListener('sourceended', update, options);\n mediaSource.addEventListener('sourceclose', update, options);\n return readyState;\n}\n"],"mappings":";;;;;;;;;;;;;AAcA,SAAgB,sBAA+B;AAC7C,QAAO,OAAO,gBAAgB;;;;;;AAOhC,SAAgB,6BAAsC;AACpD,QAAO,OAAO,uBAAuB;;;;;;;;;;;;;;AAuBvC,SAAgB,kBAAkB,UAAoC,EAAE,EAAe;CACrF,MAAM,EAAE,gBAAgB,UAAU;AAElC,KAAI,iBAAiB,4BAA4B,CAC/C,QAAO,IAAI,oBAAqB;AAGlC,KAAI,qBAAqB,CACvB,QAAO,IAAI,aAAa;AAG1B,OAAM,IAAI,MAAM,mCAAmC;;;;;;;;;;;;;;;;;;AA6BrD,SAAgB,kBAAkB,aAA0B,cAAyD;AAInH,KAF6B,4BAA4B,IAAI,uBAAuB,oBAE1D;AAGvB,eAAuE,wBAAwB;AAG/F,eAAsE,YAAY;EAEnF,MAAM,eAAqB;AACxB,gBAAsE,YAAY;AACnF,gBAAa,MAAM;;AAGrB,SAAO;GAAE,KAAK;GAAI;GAAQ;;CAI5B,MAAM,MAAM,IAAI,gBAAgB,YAAY;AAC5C,cAAa,MAAM;CAEnB,MAAM,eAAqB;AACzB,eAAa,gBAAgB,MAAM;AACnC,eAAa,MAAM;AACnB,MAAI,gBAAgB,IAAI;;AAG1B,QAAO;EAAE;EAAK;EAAQ;;;;;;;;;;;;;AAcxB,SAAgB,mBAAmB,aAA0B,WAAiC;AAC5F,KAAI,YAAY,eAAe,OAC7B,OAAM,IAAI,MAAM,0BAA0B;AAG5C,KAAI,CAAC,iBAAiB,UAAU,CAC9B,OAAM,IAAI,MAAM,wBAAwB,YAAY;AAGtD,QAAO,YAAY,gBAAgB,UAAU;;;;;;;;;;;;;AAc/C,SAAgB,iBAAiB,WAA4B;AAC3D,KAAI,CAAC,qBAAqB,CACxB,QAAO;AAGT,QAAO,YAAY,gBAAgB,UAAU;;;;;;;;;;;;;;;;;;;;;;AAuB/C,SAAgB,6BACd,aACA,aAC2C;CAC3C,MAAM,aAAa,OAAkC,YAAY,WAAW;CAC5E,MAAM,eAAe,WAAW,IAAI,YAAY,WAAW;CAC3D,MAAM,UAAU,EAAE,QAAQ,aAAa;AACvC,aAAY,iBAAiB,cAAc,QAAQ,QAAQ;AAC3D,aAAY,iBAAiB,eAAe,QAAQ,QAAQ;AAC5D,aAAY,iBAAiB,eAAe,QAAQ,QAAQ;AAC5D,QAAO"}
@@ -1,8 +1,7 @@
1
- import { createState } from "../../core/state/create-state.js";
1
+ import { signal, update } from "../../core/signals/primitives.js";
2
2
  import { appendSegment } from "./append-segment.js";
3
3
  import { flushBuffer } from "./buffer-flusher.js";
4
4
  import { SerialRunner, Task } from "../../core/task.js";
5
-
6
5
  //#region src/dom/media/source-buffer-actor.ts
7
6
  /**
8
7
  * Thrown when a message is sent to the actor in a state that does not
@@ -93,7 +92,7 @@ function messageToTask(message, options) {
93
92
  return factory(message, options);
94
93
  }
95
94
  function createSourceBufferActor(sourceBuffer, initialContext) {
96
- const state = createState({
95
+ const snapshotSignal = signal({
97
96
  status: "idle",
98
97
  context: {
99
98
  segments: [],
@@ -104,55 +103,47 @@ function createSourceBufferActor(sourceBuffer, initialContext) {
104
103
  });
105
104
  const runner = new SerialRunner();
106
105
  function applyResult(newContext) {
107
- const status = state.current.status === "destroyed" ? "destroyed" : "idle";
108
- state.patch({
106
+ const status = snapshotSignal.get().status === "destroyed" ? "destroyed" : "idle";
107
+ snapshotSignal.set({
109
108
  status,
110
109
  context: newContext
111
110
  });
112
- state.flush();
113
111
  }
114
112
  function handleError(e) {
115
- const status = state.current.status === "destroyed" ? "destroyed" : "idle";
116
- state.patch({ status });
117
- state.flush();
113
+ update(snapshotSignal, { status: snapshotSignal.get().status === "destroyed" ? "destroyed" : "idle" });
118
114
  throw e;
119
115
  }
120
116
  return {
121
117
  get snapshot() {
122
- return state.current;
123
- },
124
- subscribe(listener) {
125
- return state.subscribe(listener);
118
+ return snapshotSignal;
126
119
  },
127
120
  send(message, signal) {
128
- if (state.current.status !== "idle") return Promise.reject(new SourceBufferActorError(`send() called while actor is ${state.current.status}`));
129
- state.patch({ status: "updating" });
121
+ if (snapshotSignal.get().status !== "idle") return Promise.reject(new SourceBufferActorError(`send() called while actor is ${snapshotSignal.get().status}`));
122
+ update(snapshotSignal, { status: "updating" });
130
123
  const onPartialContext = (ctx) => {
131
- state.patch({
124
+ snapshotSignal.set({
132
125
  status: "updating",
133
126
  context: ctx
134
127
  });
135
- state.flush();
136
128
  };
137
129
  const task = messageToTask(message, {
138
130
  signal,
139
- getCtx: () => state.current.context,
131
+ getCtx: () => snapshotSignal.get().context,
140
132
  sourceBuffer,
141
133
  onPartialContext
142
134
  });
143
135
  return runner.schedule(task).then(applyResult).catch(handleError);
144
136
  },
145
137
  batch(messages, signal) {
146
- if (state.current.status !== "idle") return Promise.reject(new SourceBufferActorError(`batch() called while actor is ${state.current.status}`));
138
+ if (snapshotSignal.get().status !== "idle") return Promise.reject(new SourceBufferActorError(`batch() called while actor is ${snapshotSignal.get().status}`));
147
139
  if (messages.length === 0) return Promise.resolve();
148
- state.patch({ status: "updating" });
149
- let workingCtx = state.current.context;
140
+ update(snapshotSignal, { status: "updating" });
141
+ let workingCtx = snapshotSignal.get().context;
150
142
  const onPartialContext = (ctx) => {
151
- state.patch({
143
+ snapshotSignal.set({
152
144
  status: "updating",
153
145
  context: ctx
154
146
  });
155
- state.flush();
156
147
  };
157
148
  for (const message of messages.slice(0, -1)) {
158
149
  const task = messageToTask(message, {
@@ -174,13 +165,12 @@ function createSourceBufferActor(sourceBuffer, initialContext) {
174
165
  return runner.schedule(lastTask).then(applyResult).catch(handleError);
175
166
  },
176
167
  destroy() {
177
- state.patch({ status: "destroyed" });
178
- state.flush();
168
+ update(snapshotSignal, { status: "destroyed" });
179
169
  runner.destroy();
180
170
  }
181
171
  };
182
172
  }
183
-
184
173
  //#endregion
185
174
  export { createSourceBufferActor };
175
+
186
176
  //# sourceMappingURL=source-buffer-actor.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"source-buffer-actor.js","names":[],"sources":["../../../../src/dom/media/source-buffer-actor.ts"],"sourcesContent":["import type { Actor, ActorSnapshot } from '../../core/actor';\nimport { createState } from '../../core/state/create-state';\nimport { SerialRunner, Task } from '../../core/task';\nimport type { Segment, Track } from '../../core/types';\nimport { type AppendData, appendSegment } from './append-segment';\nimport { flushBuffer } from './buffer-flusher';\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 SourceBufferMessage = AppendInitMessage | AppendSegmentMessage | RemoveMessage;\n\n/** Finite (bounded) operational modes of the actor. */\nexport type SourceBufferActorStatus = '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/** Complete snapshot of a SourceBufferActor. */\nexport type SourceBufferActorSnapshot = ActorSnapshot<SourceBufferActorStatus, SourceBufferActorContext>;\n\n/**\n * Thrown when a message is sent to the actor in a state that does not\n * accept messages (currently: 'updating').\n */\nexport class SourceBufferActorError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'SourceBufferActorError';\n }\n}\n\n/** SourceBuffer actor: queues operations, owns its snapshot. */\nexport interface SourceBufferActor extends Actor<SourceBufferActorStatus, SourceBufferActorContext> {\n send(message: SourceBufferMessage, signal: AbortSignal): Promise<void>;\n batch(messages: SourceBufferMessage[], signal: AbortSignal): Promise<void>;\n}\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 getCtx 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 signal: AbortSignal;\n getCtx: () => SourceBufferActorContext;\n sourceBuffer: SourceBuffer;\n /**\n * Called when a streaming append transitions to a partial state — i.e.\n * the first chunk of an AsyncIterable has been committed and the segment\n * now has data in the SourceBuffer but is not yet complete. Not called for\n * full ArrayBuffer appends (which are atomic).\n */\n onPartialContext: (ctx: SourceBufferActorContext) => void;\n}\n\nfunction appendInitTask(\n message: AppendInitMessage,\n { signal, getCtx, sourceBuffer }: MessageTaskOptions\n): Task<SourceBufferActorContext> {\n return new Task(\n async (taskSignal) => {\n const ctx = getCtx();\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 { signal }\n );\n}\n\nfunction appendSegmentTask(\n message: AppendSegmentMessage,\n { signal, getCtx, sourceBuffer, onPartialContext }: MessageTaskOptions\n): Task<SourceBufferActorContext> {\n return new Task(\n async (taskSignal) => {\n const ctx = getCtx();\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. The epsilon guards against floating-point drift in\n // parsed timestamps.\n const EPSILON = 0.0001;\n const filtered = ctx.segments.filter((s) => Math.abs(s.startTime - meta.startTime) >= 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 onPartialContext({\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 { signal }\n );\n}\n\nfunction removeTask(\n message: RemoveMessage,\n { signal, getCtx, sourceBuffer }: MessageTaskOptions\n): Task<SourceBufferActorContext> {\n return new Task(\n async (taskSignal) => {\n const ctx = getCtx();\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 { signal }\n );\n}\n\ntype MessageTaskFactory<T extends SourceBufferMessage> = (\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 { [K in SourceBufferMessage['type']]: MessageTaskFactory<Extract<SourceBufferMessage, { type: K }>> };\n\nfunction messageToTask(message: SourceBufferMessage, options: MessageTaskOptions): 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 const state = createState<SourceBufferActorSnapshot>({\n status: 'idle',\n context: { segments: [], bufferedRanges: [], initTrackId: undefined, ...initialContext },\n });\n\n const runner = new SerialRunner();\n\n // Applies the completed context atomically with the idle transition and\n // flushes synchronously so consumers observe the final snapshot when their\n // awaited Promise resolves.\n // If the actor was destroyed while the operation was in flight, preserve\n // 'destroyed' — do not regress to 'idle'.\n function applyResult(newContext: SourceBufferActorContext): void {\n const status = state.current.status === 'destroyed' ? 'destroyed' : 'idle';\n state.patch({ status, context: newContext });\n state.flush();\n }\n\n function handleError(e: unknown): never {\n // TODO: QuotaExceededError and other SourceBuffer errors leave the physical\n // buffer in an unknown partial state while context goes unchanged. A future\n // improvement should detect QuotaExceededError specifically and use total\n // bytes-in-buffer as a heuristic to identify the effective buffer capacity,\n // enabling targeted flush-and-retry rather than silent model drift.\n const status = state.current.status === 'destroyed' ? 'destroyed' : 'idle';\n state.patch({ status });\n state.flush();\n throw e;\n }\n\n return {\n get snapshot(): SourceBufferActorSnapshot {\n return state.current;\n },\n\n subscribe(listener: (snapshot: SourceBufferActorSnapshot) => void): () => void {\n return state.subscribe(listener);\n },\n\n send(message: SourceBufferMessage, signal: AbortSignal): Promise<void> {\n if (state.current.status !== 'idle') {\n return Promise.reject(new SourceBufferActorError(`send() called while actor is ${state.current.status}`));\n }\n\n // Transition synchronously so any subsequent send/batch within the same\n // tick is rejected — the actor is now committed to this operation.\n state.patch({ status: 'updating' });\n\n const onPartialContext = (ctx: SourceBufferActorContext) => {\n state.patch({ status: 'updating', context: ctx });\n state.flush();\n };\n\n const task = messageToTask(message, {\n signal,\n getCtx: () => state.current.context,\n sourceBuffer,\n onPartialContext,\n });\n\n return runner.schedule(task).then(applyResult).catch(handleError);\n },\n\n batch(messages: SourceBufferMessage[], signal: AbortSignal): Promise<void> {\n if (state.current.status !== 'idle') {\n return Promise.reject(new SourceBufferActorError(`batch() called while actor is ${state.current.status}`));\n }\n\n if (messages.length === 0) return Promise.resolve();\n\n // Transition synchronously — the entire batch is one 'updating' period.\n state.patch({ status: 'updating' });\n\n // Each message is its own Task on the runner, executed in submission order.\n // workingCtx threads the result of each task into the next without\n // patching shared state between steps — context is only written to state\n // atomically when the last task completes.\n //\n // workingCtx is captured here (synchronously after status → 'updating'),\n // so it reflects the current context at the moment the batch was accepted.\n // This is correct: status is now 'updating' so no other sender can modify\n // context between this line and the first task executing.\n //\n // NOTE: if an intermediate task fails (e.g. SourceBuffer error event),\n // workingCtx is not updated for that step and subsequent tasks in the\n // batch will operate on a stale context. This is an edge case — happy-path\n // appends do not fail — but worth revisiting if MSE error recovery lands.\n let workingCtx = state.current.context;\n\n // Partial context updates from streaming appends patch state directly so\n // external subscribers see in-progress state, but workingCtx is only\n // advanced on task completion to preserve batch context threading.\n const onPartialContext = (ctx: SourceBufferActorContext) => {\n state.patch({ status: 'updating', context: ctx });\n state.flush();\n };\n\n for (const message of messages.slice(0, -1)) {\n const task = messageToTask(message, { signal, getCtx: () => workingCtx, sourceBuffer, onPartialContext });\n const result = runner.schedule(task);\n result.then((newCtx) => {\n workingCtx = newCtx;\n });\n }\n\n const lastTask = messageToTask(messages[messages.length - 1]!, {\n signal,\n getCtx: () => workingCtx,\n sourceBuffer,\n onPartialContext,\n });\n return runner.schedule(lastTask).then(applyResult).catch(handleError);\n },\n\n destroy(): void {\n state.patch({ status: 'destroyed' });\n state.flush();\n runner.destroy();\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"source-buffer-actor.js","names":[],"sources":["../../../../src/dom/media/source-buffer-actor.ts"],"sourcesContent":["import type { ActorSnapshot, SignalActor } from '../../core/actor';\nimport { type ReadonlySignal, signal, update } from '../../core/signals/primitives';\nimport { SerialRunner, Task } from '../../core/task';\nimport type { Segment, Track } from '../../core/types';\nimport { type AppendData, appendSegment } from './append-segment';\nimport { flushBuffer } from './buffer-flusher';\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 SourceBufferMessage = AppendInitMessage | AppendSegmentMessage | RemoveMessage;\n\n/** Finite (bounded) operational modes of the actor. */\nexport type SourceBufferActorStatus = '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/** Complete snapshot of a SourceBufferActor. */\nexport type SourceBufferActorSnapshot = ActorSnapshot<SourceBufferActorStatus, SourceBufferActorContext>;\n\n/**\n * Thrown when a message is sent to the actor in a state that does not\n * accept messages (currently: 'updating').\n */\nexport class SourceBufferActorError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'SourceBufferActorError';\n }\n}\n\n/** SourceBuffer actor: queues operations, owns its snapshot. */\nexport interface SourceBufferActor extends SignalActor<SourceBufferActorStatus, SourceBufferActorContext> {\n send(message: SourceBufferMessage, signal: AbortSignal): Promise<void>;\n batch(messages: SourceBufferMessage[], signal: AbortSignal): Promise<void>;\n}\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 getCtx 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 signal: AbortSignal;\n getCtx: () => SourceBufferActorContext;\n sourceBuffer: SourceBuffer;\n /**\n * Called when a streaming append transitions to a partial state — i.e.\n * the first chunk of an AsyncIterable has been committed and the segment\n * now has data in the SourceBuffer but is not yet complete. Not called for\n * full ArrayBuffer appends (which are atomic).\n */\n onPartialContext: (ctx: SourceBufferActorContext) => void;\n}\n\nfunction appendInitTask(\n message: AppendInitMessage,\n { signal, getCtx, sourceBuffer }: MessageTaskOptions\n): Task<SourceBufferActorContext> {\n return new Task(\n async (taskSignal) => {\n const ctx = getCtx();\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 { signal }\n );\n}\n\nfunction appendSegmentTask(\n message: AppendSegmentMessage,\n { signal, getCtx, sourceBuffer, onPartialContext }: MessageTaskOptions\n): Task<SourceBufferActorContext> {\n return new Task(\n async (taskSignal) => {\n const ctx = getCtx();\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. The epsilon guards against floating-point drift in\n // parsed timestamps.\n const EPSILON = 0.0001;\n const filtered = ctx.segments.filter((s) => Math.abs(s.startTime - meta.startTime) >= 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 onPartialContext({\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 { signal }\n );\n}\n\nfunction removeTask(\n message: RemoveMessage,\n { signal, getCtx, sourceBuffer }: MessageTaskOptions\n): Task<SourceBufferActorContext> {\n return new Task(\n async (taskSignal) => {\n const ctx = getCtx();\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 { signal }\n );\n}\n\ntype MessageTaskFactory<T extends SourceBufferMessage> = (\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 { [K in SourceBufferMessage['type']]: MessageTaskFactory<Extract<SourceBufferMessage, { type: K }>> };\n\nfunction messageToTask(message: SourceBufferMessage, options: MessageTaskOptions): 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 const snapshotSignal = signal<SourceBufferActorSnapshot>({\n status: 'idle',\n context: { segments: [], bufferedRanges: [], initTrackId: undefined, ...initialContext },\n });\n\n const runner = new SerialRunner();\n\n // Applies the completed context atomically with the idle transition.\n // If the actor was destroyed while the operation was in flight, preserve\n // 'destroyed' — do not regress to 'idle'.\n function applyResult(newContext: SourceBufferActorContext): void {\n const status = snapshotSignal.get().status === 'destroyed' ? 'destroyed' : 'idle';\n snapshotSignal.set({ status, context: newContext });\n }\n\n function handleError(e: unknown): never {\n // TODO: QuotaExceededError and other SourceBuffer errors leave the physical\n // buffer in an unknown partial state while context goes unchanged. A future\n // improvement should detect QuotaExceededError specifically and use total\n // bytes-in-buffer as a heuristic to identify the effective buffer capacity,\n // enabling targeted flush-and-retry rather than silent model drift.\n const status = snapshotSignal.get().status === 'destroyed' ? 'destroyed' : 'idle';\n update(snapshotSignal, { status });\n throw e;\n }\n\n return {\n get snapshot(): ReadonlySignal<SourceBufferActorSnapshot> {\n return snapshotSignal;\n },\n\n send(message: SourceBufferMessage, signal: AbortSignal): Promise<void> {\n if (snapshotSignal.get().status !== 'idle') {\n return Promise.reject(\n new SourceBufferActorError(`send() called while actor is ${snapshotSignal.get().status}`)\n );\n }\n\n // Transition synchronously so any subsequent send/batch within the same\n // tick is rejected — the actor is now committed to this operation.\n update(snapshotSignal, { status: 'updating' });\n\n const onPartialContext = (ctx: SourceBufferActorContext) => {\n snapshotSignal.set({ status: 'updating', context: ctx });\n };\n\n const task = messageToTask(message, {\n signal,\n getCtx: () => snapshotSignal.get().context,\n sourceBuffer,\n onPartialContext,\n });\n\n return runner.schedule(task).then(applyResult).catch(handleError);\n },\n\n batch(messages: SourceBufferMessage[], signal: AbortSignal): Promise<void> {\n if (snapshotSignal.get().status !== 'idle') {\n return Promise.reject(\n new SourceBufferActorError(`batch() called while actor is ${snapshotSignal.get().status}`)\n );\n }\n\n if (messages.length === 0) return Promise.resolve();\n\n // Transition synchronously — the entire batch is one 'updating' period.\n update(snapshotSignal, { status: 'updating' });\n\n // Each message is its own Task on the runner, executed in submission order.\n // workingCtx threads the result of each task into the next without\n // writing to the signal between steps — context is only written atomically\n // when the last task completes.\n //\n // workingCtx is captured here (synchronously after status → 'updating'),\n // so it reflects the current context at the moment the batch was accepted.\n // This is correct: status is now 'updating' so no other sender can modify\n // context between this line and the first task executing.\n //\n // NOTE: if an intermediate task fails (e.g. SourceBuffer error event),\n // workingCtx is not updated for that step and subsequent tasks in the\n // batch will operate on a stale context. This is an edge case — happy-path\n // appends do not fail — but worth revisiting if MSE error recovery lands.\n let workingCtx = snapshotSignal.get().context;\n\n // Partial context updates from streaming appends write to the signal\n // directly so external subscribers see in-progress state, but workingCtx\n // is only advanced on task completion to preserve batch context threading.\n const onPartialContext = (ctx: SourceBufferActorContext) => {\n snapshotSignal.set({ status: 'updating', context: ctx });\n };\n\n for (const message of messages.slice(0, -1)) {\n const task = messageToTask(message, { signal, getCtx: () => workingCtx, sourceBuffer, onPartialContext });\n const result = runner.schedule(task);\n result.then((newCtx) => {\n workingCtx = newCtx;\n });\n }\n\n const lastTask = messageToTask(messages[messages.length - 1]!, {\n signal,\n getCtx: () => workingCtx,\n sourceBuffer,\n onPartialContext,\n });\n return runner.schedule(lastTask).then(applyResult).catch(handleError);\n },\n\n destroy(): void {\n update(snapshotSignal, { status: 'destroyed' });\n runner.destroy();\n },\n };\n}\n"],"mappings":"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"}
@@ -43,7 +43,7 @@ function concat(a, b) {
43
43
  result.set(b, a.length);
44
44
  return result;
45
45
  }
46
-
47
46
  //#endregion
48
47
  export { ChunkedStreamIterable };
48
+
49
49
  //# sourceMappingURL=chunked-stream-iterable.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"chunked-stream-iterable.js","names":["#readableStream"],"sources":["../../../../src/dom/network/chunked-stream-iterable.ts"],"sourcesContent":["const DEFAULT_MIN_CHUNK_SIZE = 2 ** 17; // 128 KB\n\nexport interface ChunkedStreamIterableOptions {\n minChunkSize?: number;\n}\n\n/**\n * Adapts a `ReadableStream<Uint8Array>` (e.g. `response.body`) into an\n * `AsyncIterable<Uint8Array>` that yields chunks no smaller than\n * `minChunkSize` bytes. Smaller network chunks are accumulated and yielded\n * together once the threshold is met. Any remainder is flushed on stream end.\n *\n * Errors from the underlying stream propagate naturally — the reader lock is\n * always released via `finally`.\n */\nexport class ChunkedStreamIterable implements AsyncIterable<Uint8Array> {\n readonly minChunkSize: number;\n #readableStream: ReadableStream<Uint8Array>;\n\n constructor(\n readableStream: ReadableStream<Uint8Array>,\n { minChunkSize = DEFAULT_MIN_CHUNK_SIZE }: ChunkedStreamIterableOptions = {}\n ) {\n this.#readableStream = readableStream;\n this.minChunkSize = minChunkSize;\n }\n\n async *[Symbol.asyncIterator](): AsyncGenerator<Uint8Array> {\n let pending: Uint8Array | undefined;\n const reader = this.#readableStream.getReader();\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) {\n if (pending) yield pending;\n break;\n }\n\n pending = pending ? concat(pending, value) : value;\n\n if (pending.length >= this.minChunkSize) {\n yield pending;\n pending = undefined;\n }\n }\n } finally {\n reader.releaseLock();\n }\n }\n}\n\nfunction concat(a: Uint8Array, b: Uint8Array): Uint8Array {\n const result = new Uint8Array(a.length + b.length);\n result.set(a);\n result.set(b, a.length);\n return result;\n}\n"],"mappings":";AAAA,MAAM,yBAAyB,KAAK;;;;;;;;;;AAepC,IAAa,wBAAb,MAAwE;CACtE,AAAS;CACT;CAEA,YACE,gBACA,EAAE,eAAe,2BAAyD,EAAE,EAC5E;AACA,QAAKA,iBAAkB;AACvB,OAAK,eAAe;;CAGtB,QAAQ,OAAO,iBAA6C;EAC1D,IAAI;EACJ,MAAM,SAAS,MAAKA,eAAgB,WAAW;AAC/C,MAAI;AACF,UAAO,MAAM;IACX,MAAM,EAAE,MAAM,UAAU,MAAM,OAAO,MAAM;AAC3C,QAAI,MAAM;AACR,SAAI,QAAS,OAAM;AACnB;;AAGF,cAAU,UAAU,OAAO,SAAS,MAAM,GAAG;AAE7C,QAAI,QAAQ,UAAU,KAAK,cAAc;AACvC,WAAM;AACN,eAAU;;;YAGN;AACR,UAAO,aAAa;;;;AAK1B,SAAS,OAAO,GAAe,GAA2B;CACxD,MAAM,SAAS,IAAI,WAAW,EAAE,SAAS,EAAE,OAAO;AAClD,QAAO,IAAI,EAAE;AACb,QAAO,IAAI,GAAG,EAAE,OAAO;AACvB,QAAO"}
1
+ {"version":3,"file":"chunked-stream-iterable.js","names":["#readableStream"],"sources":["../../../../src/dom/network/chunked-stream-iterable.ts"],"sourcesContent":["const DEFAULT_MIN_CHUNK_SIZE = 2 ** 17; // 128 KB\n\nexport interface ChunkedStreamIterableOptions {\n minChunkSize?: number;\n}\n\n/**\n * Adapts a `ReadableStream<Uint8Array>` (e.g. `response.body`) into an\n * `AsyncIterable<Uint8Array>` that yields chunks no smaller than\n * `minChunkSize` bytes. Smaller network chunks are accumulated and yielded\n * together once the threshold is met. Any remainder is flushed on stream end.\n *\n * Errors from the underlying stream propagate naturally — the reader lock is\n * always released via `finally`.\n */\nexport class ChunkedStreamIterable implements AsyncIterable<Uint8Array> {\n readonly minChunkSize: number;\n #readableStream: ReadableStream<Uint8Array>;\n\n constructor(\n readableStream: ReadableStream<Uint8Array>,\n { minChunkSize = DEFAULT_MIN_CHUNK_SIZE }: ChunkedStreamIterableOptions = {}\n ) {\n this.#readableStream = readableStream;\n this.minChunkSize = minChunkSize;\n }\n\n async *[Symbol.asyncIterator](): AsyncGenerator<Uint8Array> {\n let pending: Uint8Array | undefined;\n const reader = this.#readableStream.getReader();\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) {\n if (pending) yield pending;\n break;\n }\n\n pending = pending ? concat(pending, value) : value;\n\n if (pending.length >= this.minChunkSize) {\n yield pending;\n pending = undefined;\n }\n }\n } finally {\n reader.releaseLock();\n }\n }\n}\n\nfunction concat(a: Uint8Array, b: Uint8Array): Uint8Array {\n const result = new Uint8Array(a.length + b.length);\n result.set(a);\n result.set(b, a.length);\n return result;\n}\n"],"mappings":";AAAA,MAAM,yBAAyB,KAAK;;;;;;;;;;AAepC,IAAa,wBAAb,MAAwE;CACtE;CACA;CAEA,YACE,gBACA,EAAE,eAAe,2BAAyD,EAAE,EAC5E;AACA,QAAA,iBAAuB;AACvB,OAAK,eAAe;;CAGtB,QAAQ,OAAO,iBAA6C;EAC1D,IAAI;EACJ,MAAM,SAAS,MAAA,eAAqB,WAAW;AAC/C,MAAI;AACF,UAAO,MAAM;IACX,MAAM,EAAE,MAAM,UAAU,MAAM,OAAO,MAAM;AAC3C,QAAI,MAAM;AACR,SAAI,QAAS,OAAM;AACnB;;AAGF,cAAU,UAAU,OAAO,SAAS,MAAM,GAAG;AAE7C,QAAI,QAAQ,UAAU,KAAK,cAAc;AACvC,WAAM;AACN,eAAU,KAAA;;;YAGN;AACR,UAAO,aAAa;;;;AAK1B,SAAS,OAAO,GAAe,GAA2B;CACxD,MAAM,SAAS,IAAI,WAAW,EAAE,SAAS,EAAE,OAAO;AAClD,QAAO,IAAI,EAAE;AACb,QAAO,IAAI,GAAG,EAAE,OAAO;AACvB,QAAO"}
@@ -1,5 +1,4 @@
1
1
  import "./chunked-stream-iterable.js";
2
-
3
2
  //#region src/dom/network/fetch.ts
4
3
  /**
5
4
  * Fetch resolvable from AddressableObject.
@@ -50,7 +49,7 @@ async function fetchResolvable(addressable, options) {
50
49
  function getResponseText(response) {
51
50
  return response.text();
52
51
  }
53
-
54
52
  //#endregion
55
53
  export { fetchResolvable, getResponseText };
54
+
56
55
  //# sourceMappingURL=fetch.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"fetch.js","names":[],"sources":["../../../../src/dom/network/fetch.ts"],"sourcesContent":["/**\n * HTTP Fetch Wrapper\n *\n * Two-function approach for composability:\n * 1. fetchResolvable() - Fetch AddressableObject (handles byte ranges)\n * 2. getResponseText() - Extract text from Response\n * 3. fetchResolvableStream() - Stream body as Uint8Array chunks\n */\n\nimport type { AddressableObject } from '../../core/types';\nimport { ChunkedStreamIterable, type ChunkedStreamIterableOptions } from './chunked-stream-iterable';\n\n/**\n * Minimal Response-like interface for text extraction.\n * Allows testing without full Response object.\n */\nexport interface ResponseLike {\n text(): Promise<string>;\n}\n\n/**\n * Fetch resolvable from AddressableObject.\n *\n * Handles byte range requests if byteRange is present.\n * Returns native fetch Response for composability (can extract text, stream, etc.).\n *\n * @param addressable - Resource to fetch (url + optional byteRange)\n * @returns Promise resolving to Response\n *\n * @example\n * const response = await fetchResolvable({ url: 'https://example.com/segment.m4s' });\n * const text = await getResponseText(response);\n *\n * @example\n * // With byte range\n * const response = await fetchResolvable({\n * url: 'https://example.com/file.mp4',\n * byteRange: { start: 1000, end: 1999 }\n * });\n */\nexport async function fetchResolvable(addressable: AddressableObject, options?: RequestInit): Promise<Response> {\n const headers = new Headers(options?.headers);\n\n // Add Range header for byte range requests\n if (addressable.byteRange) {\n const { start, end } = addressable.byteRange;\n headers.set('Range', `bytes=${start}-${end}`);\n }\n\n const request = new Request(addressable.url, {\n method: 'GET',\n headers,\n ...options,\n });\n\n return fetch(request);\n}\n\n/**\n * Fetch resolvable as bytes.\n *\n * Convenience wrapper around fetchResolvable that resolves the body as an\n * ArrayBuffer. Use this when you need the raw bytes (e.g. segment appends).\n * For text or streaming consumption, use fetchResolvable directly.\n */\nexport async function fetchResolvableBytes(\n addressable: AddressableObject,\n options?: RequestInit\n): Promise<ArrayBuffer> {\n const response = await fetchResolvable(addressable, options);\n return response.arrayBuffer();\n}\n\n/**\n * Fetch resolvable as a stream of Uint8Array chunks.\n *\n * Convenience wrapper around fetchResolvable that yields the body as chunks\n * via ChunkedStreamIterable. Headers are awaited before the first chunk is\n * yielded (TTFB is accounted for before iteration begins).\n *\n * Throws if the response body is null (e.g. non-body HTTP status).\n * Errors from the underlying stream propagate naturally as thrown errors.\n */\nexport async function* fetchResolvableStream(\n addressable: AddressableObject,\n options?: RequestInit & ChunkedStreamIterableOptions\n): AsyncGenerator<Uint8Array> {\n const { minChunkSize, ...fetchOptions } = options ?? {};\n const response = await fetchResolvable(addressable, fetchOptions);\n if (!response.body) throw new Error('Response has no body');\n yield* new ChunkedStreamIterable(response.body, ...(minChunkSize !== undefined ? [{ minChunkSize }] : []));\n}\n\n/**\n * Extract text from Response.\n *\n * Accepts minimal Response-like object (just needs text() method).\n * Returns promise from response.text().\n *\n * @param response - Response-like object with text() method\n * @returns Promise resolving to text content\n *\n * @example\n * const response = await fetchResolvable(addressable);\n * const text = await getResponseText(response);\n */\nexport function getResponseText(response: ResponseLike): Promise<string> {\n return response.text();\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAwCA,eAAsB,gBAAgB,aAAgC,SAA0C;CAC9G,MAAM,UAAU,IAAI,QAAQ,SAAS,QAAQ;AAG7C,KAAI,YAAY,WAAW;EACzB,MAAM,EAAE,OAAO,QAAQ,YAAY;AACnC,UAAQ,IAAI,SAAS,SAAS,MAAM,GAAG,MAAM;;CAG/C,MAAM,UAAU,IAAI,QAAQ,YAAY,KAAK;EAC3C,QAAQ;EACR;EACA,GAAG;EACJ,CAAC;AAEF,QAAO,MAAM,QAAQ;;;;;;;;;;;;;;;AAmDvB,SAAgB,gBAAgB,UAAyC;AACvE,QAAO,SAAS,MAAM"}
1
+ {"version":3,"file":"fetch.js","names":[],"sources":["../../../../src/dom/network/fetch.ts"],"sourcesContent":["/**\n * HTTP Fetch Wrapper\n *\n * Two-function approach for composability:\n * 1. fetchResolvable() - Fetch AddressableObject (handles byte ranges)\n * 2. getResponseText() - Extract text from Response\n * 3. fetchResolvableStream() - Stream body as Uint8Array chunks\n */\n\nimport type { AddressableObject } from '../../core/types';\nimport { ChunkedStreamIterable, type ChunkedStreamIterableOptions } from './chunked-stream-iterable';\n\n/**\n * Minimal Response-like interface for text extraction.\n * Allows testing without full Response object.\n */\nexport interface ResponseLike {\n text(): Promise<string>;\n}\n\n/**\n * Fetch resolvable from AddressableObject.\n *\n * Handles byte range requests if byteRange is present.\n * Returns native fetch Response for composability (can extract text, stream, etc.).\n *\n * @param addressable - Resource to fetch (url + optional byteRange)\n * @returns Promise resolving to Response\n *\n * @example\n * const response = await fetchResolvable({ url: 'https://example.com/segment.m4s' });\n * const text = await getResponseText(response);\n *\n * @example\n * // With byte range\n * const response = await fetchResolvable({\n * url: 'https://example.com/file.mp4',\n * byteRange: { start: 1000, end: 1999 }\n * });\n */\nexport async function fetchResolvable(addressable: AddressableObject, options?: RequestInit): Promise<Response> {\n const headers = new Headers(options?.headers);\n\n // Add Range header for byte range requests\n if (addressable.byteRange) {\n const { start, end } = addressable.byteRange;\n headers.set('Range', `bytes=${start}-${end}`);\n }\n\n const request = new Request(addressable.url, {\n method: 'GET',\n headers,\n ...options,\n });\n\n return fetch(request);\n}\n\n/**\n * Fetch resolvable as bytes.\n *\n * Convenience wrapper around fetchResolvable that resolves the body as an\n * ArrayBuffer. Use this when you need the raw bytes (e.g. segment appends).\n * For text or streaming consumption, use fetchResolvable directly.\n */\nexport async function fetchResolvableBytes(\n addressable: AddressableObject,\n options?: RequestInit\n): Promise<ArrayBuffer> {\n const response = await fetchResolvable(addressable, options);\n return response.arrayBuffer();\n}\n\n/**\n * Fetch resolvable as a stream of Uint8Array chunks.\n *\n * Convenience wrapper around fetchResolvable that yields the body as chunks\n * via ChunkedStreamIterable. Headers are awaited before the first chunk is\n * yielded (TTFB is accounted for before iteration begins).\n *\n * Throws if the response body is null (e.g. non-body HTTP status).\n * Errors from the underlying stream propagate naturally as thrown errors.\n */\nexport async function* fetchResolvableStream(\n addressable: AddressableObject,\n options?: RequestInit & ChunkedStreamIterableOptions\n): AsyncGenerator<Uint8Array> {\n const { minChunkSize, ...fetchOptions } = options ?? {};\n const response = await fetchResolvable(addressable, fetchOptions);\n if (!response.body) throw new Error('Response has no body');\n yield* new ChunkedStreamIterable(response.body, ...(minChunkSize !== undefined ? [{ minChunkSize }] : []));\n}\n\n/**\n * Extract text from Response.\n *\n * Accepts minimal Response-like object (just needs text() method).\n * Returns promise from response.text().\n *\n * @param response - Response-like object with text() method\n * @returns Promise resolving to text content\n *\n * @example\n * const response = await fetchResolvable(addressable);\n * const text = await getResponseText(response);\n */\nexport function getResponseText(response: ResponseLike): Promise<string> {\n return response.text();\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAwCA,eAAsB,gBAAgB,aAAgC,SAA0C;CAC9G,MAAM,UAAU,IAAI,QAAQ,SAAS,QAAQ;AAG7C,KAAI,YAAY,WAAW;EACzB,MAAM,EAAE,OAAO,QAAQ,YAAY;AACnC,UAAQ,IAAI,SAAS,SAAS,MAAM,GAAG,MAAM;;CAG/C,MAAM,UAAU,IAAI,QAAQ,YAAY,KAAK;EAC3C,QAAQ;EACR;EACA,GAAG;EACJ,CAAC;AAEF,QAAO,MAAM,QAAQ;;;;;;;;;;;;;;;AAmDvB,SAAgB,gBAAgB,UAAyC;AACvE,QAAO,SAAS,MAAM"}
@@ -1,5 +1,5 @@
1
+ import { update } from "../../core/signals/primitives.js";
1
2
  import { createPlaybackEngine } from "./engine.js";
2
-
3
3
  //#region src/dom/playback-engine/adapter.ts
4
4
  /**
5
5
  * HTMLMediaElement-compatible adapter for the SPF playback engine.
@@ -37,11 +37,11 @@ var SpfMedia = class {
37
37
  return this.#engine;
38
38
  }
39
39
  attach(mediaElement) {
40
- this.#engine.owners.patch({ mediaElement });
40
+ update(this.#engine.owners, { mediaElement });
41
41
  }
42
42
  detach() {
43
43
  this.#cancelPendingPlay();
44
- this.#engine.owners.patch({ mediaElement: void 0 });
44
+ update(this.#engine.owners, { mediaElement: void 0 });
45
45
  }
46
46
  destroy() {
47
47
  this.#cancelPendingPlay();
@@ -52,24 +52,24 @@ var SpfMedia = class {
52
52
  }
53
53
  set preload(value) {
54
54
  this.#preload = value;
55
- if (value) this.#engine.state.patch({ preload: value });
55
+ if (value) update(this.#engine.state, { preload: value });
56
56
  }
57
57
  get src() {
58
- return this.#engine.state.current.presentation?.url ?? "";
58
+ return this.#engine.state.get().presentation?.url ?? "";
59
59
  }
60
60
  set src(value) {
61
- const prevMediaElement = this.#engine.owners.current.mediaElement;
61
+ const prevMediaElement = this.#engine.owners.get().mediaElement;
62
62
  this.#cancelPendingPlay();
63
63
  this.#engine.destroy();
64
64
  this.#engine = createPlaybackEngine(this.#config);
65
- if (this.#preload) this.#engine.state.patch({ preload: this.#preload });
66
- if (prevMediaElement) this.#engine.owners.patch({ mediaElement: prevMediaElement });
67
- if (value) this.#engine.state.patch({ presentation: { url: value } });
65
+ if (this.#preload) update(this.#engine.state, { preload: this.#preload });
66
+ if (prevMediaElement) update(this.#engine.owners, { mediaElement: prevMediaElement });
67
+ if (value) update(this.#engine.state, { presentation: { url: value } });
68
68
  }
69
69
  play() {
70
- const { mediaElement } = this.#engine.owners.current;
70
+ const { mediaElement } = this.#engine.owners.get();
71
71
  if (!mediaElement) return Promise.reject(/* @__PURE__ */ new Error("SpfMedia: no media element attached"));
72
- this.#engine.state.patch({ playbackInitiated: true });
72
+ update(this.#engine.state, { playbackInitiated: true });
73
73
  return mediaElement.play().catch((err) => {
74
74
  if (this.src) return new Promise((resolve, reject) => {
75
75
  const listener = () => {
@@ -84,12 +84,12 @@ var SpfMedia = class {
84
84
  }
85
85
  #cancelPendingPlay() {
86
86
  if (!this.#loadstartListener) return;
87
- const { mediaElement } = this.#engine.owners.current;
87
+ const { mediaElement } = this.#engine.owners.get();
88
88
  mediaElement?.removeEventListener("loadstart", this.#loadstartListener);
89
89
  this.#loadstartListener = null;
90
90
  }
91
91
  };
92
-
93
92
  //#endregion
94
93
  export { SpfMedia };
94
+
95
95
  //# sourceMappingURL=adapter.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"adapter.js","names":["#config","#engine","#cancelPendingPlay","#preload","#loadstartListener"],"sources":["../../../../src/dom/playback-engine/adapter.ts"],"sourcesContent":["import type { PlaybackEngineConfig } from './engine';\nimport { createPlaybackEngine, type PlaybackEngine } from './engine';\n\n/**\n * HTMLMediaElement-compatible adapter for the SPF playback engine.\n *\n * Implements the src/play() contract per the WHATWG HTML spec so that SPF can\n * be used anywhere a media element API is expected.\n *\n * A new engine is created on every src assignment — this fully tears down all\n * state, SourceBuffers, and in-flight requests from the previous source before\n * the next one begins. The media element reference is preserved across src\n * changes and re-applied to the new engine automatically.\n *\n * @example\n * const media = new SpfMedia({ preferredAudioLanguage: 'en' });\n * media.attach(document.querySelector('video'));\n * media.src = 'https://stream.mux.com/abc123.m3u8';\n *\n * // Change source — old engine is destroyed, new one starts clean:\n * media.src = 'https://stream.mux.com/xyz456.m3u8';\n *\n * // Explicit teardown:\n * media.destroy();\n */\nexport class SpfMedia {\n #engine: PlaybackEngine;\n #config: PlaybackEngineConfig;\n #preload: '' | 'none' | 'metadata' | 'auto' = '';\n\n /** Pending loadstart listener from a deferred play() retry, if any. */\n #loadstartListener: (() => void) | null = null;\n\n constructor(config: PlaybackEngineConfig = {}) {\n this.#config = config;\n this.#engine = createPlaybackEngine(config);\n }\n\n get engine(): PlaybackEngine {\n return this.#engine;\n }\n\n // ---------------------------------------------------------------------------\n // Media element lifecycle\n // ---------------------------------------------------------------------------\n\n attach(mediaElement: HTMLMediaElement): void {\n this.#engine.owners.patch({ mediaElement });\n }\n\n detach(): void {\n this.#cancelPendingPlay();\n this.#engine.owners.patch({ mediaElement: undefined });\n }\n\n destroy(): void {\n this.#cancelPendingPlay();\n this.#engine.destroy();\n }\n\n // ---------------------------------------------------------------------------\n // preload — synchronous IDL attribute (WHATWG §4.8.11.2)\n // ---------------------------------------------------------------------------\n\n get preload(): '' | 'none' | 'metadata' | 'auto' {\n return this.#preload;\n }\n\n set preload(value: '' | 'none' | 'metadata' | 'auto') {\n this.#preload = value;\n if (value) {\n this.#engine.state.patch({ preload: value });\n }\n // value = '' clears #preload (so the next engine recreation won't re-apply\n // an explicit value) but does not patch current state — the existing preload\n // stays in effect until the next src change creates a fresh engine.\n }\n\n // ---------------------------------------------------------------------------\n // src — synchronous IDL attribute (WHATWG §4.8.11.2)\n // Each assignment destroys the current engine and starts a fresh one, exactly\n // as the browser's load algorithm resets all media element state on src change.\n // ---------------------------------------------------------------------------\n\n get src(): string {\n return this.#engine.state.current.presentation?.url ?? '';\n }\n\n set src(value: string) {\n const prevMediaElement = this.#engine.owners.current.mediaElement;\n\n this.#cancelPendingPlay();\n this.#engine.destroy();\n this.#engine = createPlaybackEngine(this.#config);\n\n // Apply explicit preload before owners.patch so syncPreloadAttribute skips\n // element inference and the explicit value is preserved across src changes.\n if (this.#preload) {\n this.#engine.state.patch({ preload: this.#preload });\n }\n\n if (prevMediaElement) {\n this.#engine.owners.patch({ mediaElement: prevMediaElement });\n }\n\n if (value) {\n this.#engine.state.patch({ presentation: { url: value } });\n }\n }\n\n // ---------------------------------------------------------------------------\n // play() — WHATWG §4.8.11.8\n // Delegates to the attached media element's native play().\n // ---------------------------------------------------------------------------\n\n play(): Promise<void> {\n const { mediaElement } = this.#engine.owners.current;\n if (!mediaElement) {\n return Promise.reject(new Error('SpfMedia: no media element attached'));\n }\n\n // Signal play intent — enables loading even with preload=\"none\"\n this.#engine.state.patch({ playbackInitiated: true });\n\n return mediaElement.play().catch((err: unknown) => {\n // If we have a pending HLS source, the rejection may be because MSE\n // hasn't attached a blob URL yet. Wait for loadstart (src assigned\n // by MSE setup) and retry once.\n if (this.src) {\n return new Promise<void>((resolve, reject) => {\n const listener = () => {\n this.#loadstartListener = null;\n mediaElement.play().then(resolve, reject);\n };\n this.#loadstartListener = listener;\n mediaElement.addEventListener('loadstart', listener, { once: true });\n });\n }\n throw err;\n });\n }\n\n // ---------------------------------------------------------------------------\n // Private\n // ---------------------------------------------------------------------------\n\n #cancelPendingPlay(): void {\n if (!this.#loadstartListener) return;\n const { mediaElement } = this.#engine.owners.current;\n mediaElement?.removeEventListener('loadstart', this.#loadstartListener);\n this.#loadstartListener = null;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AAyBA,IAAa,WAAb,MAAsB;CACpB;CACA;CACA,WAA8C;;CAG9C,qBAA0C;CAE1C,YAAY,SAA+B,EAAE,EAAE;AAC7C,QAAKA,SAAU;AACf,QAAKC,SAAU,qBAAqB,OAAO;;CAG7C,IAAI,SAAyB;AAC3B,SAAO,MAAKA;;CAOd,OAAO,cAAsC;AAC3C,QAAKA,OAAQ,OAAO,MAAM,EAAE,cAAc,CAAC;;CAG7C,SAAe;AACb,QAAKC,mBAAoB;AACzB,QAAKD,OAAQ,OAAO,MAAM,EAAE,cAAc,QAAW,CAAC;;CAGxD,UAAgB;AACd,QAAKC,mBAAoB;AACzB,QAAKD,OAAQ,SAAS;;CAOxB,IAAI,UAA6C;AAC/C,SAAO,MAAKE;;CAGd,IAAI,QAAQ,OAA0C;AACpD,QAAKA,UAAW;AAChB,MAAI,MACF,OAAKF,OAAQ,MAAM,MAAM,EAAE,SAAS,OAAO,CAAC;;CAahD,IAAI,MAAc;AAChB,SAAO,MAAKA,OAAQ,MAAM,QAAQ,cAAc,OAAO;;CAGzD,IAAI,IAAI,OAAe;EACrB,MAAM,mBAAmB,MAAKA,OAAQ,OAAO,QAAQ;AAErD,QAAKC,mBAAoB;AACzB,QAAKD,OAAQ,SAAS;AACtB,QAAKA,SAAU,qBAAqB,MAAKD,OAAQ;AAIjD,MAAI,MAAKG,QACP,OAAKF,OAAQ,MAAM,MAAM,EAAE,SAAS,MAAKE,SAAU,CAAC;AAGtD,MAAI,iBACF,OAAKF,OAAQ,OAAO,MAAM,EAAE,cAAc,kBAAkB,CAAC;AAG/D,MAAI,MACF,OAAKA,OAAQ,MAAM,MAAM,EAAE,cAAc,EAAE,KAAK,OAAO,EAAE,CAAC;;CAS9D,OAAsB;EACpB,MAAM,EAAE,iBAAiB,MAAKA,OAAQ,OAAO;AAC7C,MAAI,CAAC,aACH,QAAO,QAAQ,uBAAO,IAAI,MAAM,sCAAsC,CAAC;AAIzE,QAAKA,OAAQ,MAAM,MAAM,EAAE,mBAAmB,MAAM,CAAC;AAErD,SAAO,aAAa,MAAM,CAAC,OAAO,QAAiB;AAIjD,OAAI,KAAK,IACP,QAAO,IAAI,SAAe,SAAS,WAAW;IAC5C,MAAM,iBAAiB;AACrB,WAAKG,oBAAqB;AAC1B,kBAAa,MAAM,CAAC,KAAK,SAAS,OAAO;;AAE3C,UAAKA,oBAAqB;AAC1B,iBAAa,iBAAiB,aAAa,UAAU,EAAE,MAAM,MAAM,CAAC;KACpE;AAEJ,SAAM;IACN;;CAOJ,qBAA2B;AACzB,MAAI,CAAC,MAAKA,kBAAoB;EAC9B,MAAM,EAAE,iBAAiB,MAAKH,OAAQ,OAAO;AAC7C,gBAAc,oBAAoB,aAAa,MAAKG,kBAAmB;AACvE,QAAKA,oBAAqB"}
1
+ {"version":3,"file":"adapter.js","names":["#config","#engine","#cancelPendingPlay","#preload","#loadstartListener"],"sources":["../../../../src/dom/playback-engine/adapter.ts"],"sourcesContent":["import { update } from '../../core/signals/primitives';\nimport type { PlaybackEngineConfig } from './engine';\nimport { createPlaybackEngine, type PlaybackEngine } from './engine';\n\n/**\n * HTMLMediaElement-compatible adapter for the SPF playback engine.\n *\n * Implements the src/play() contract per the WHATWG HTML spec so that SPF can\n * be used anywhere a media element API is expected.\n *\n * A new engine is created on every src assignment — this fully tears down all\n * state, SourceBuffers, and in-flight requests from the previous source before\n * the next one begins. The media element reference is preserved across src\n * changes and re-applied to the new engine automatically.\n *\n * @example\n * const media = new SpfMedia({ preferredAudioLanguage: 'en' });\n * media.attach(document.querySelector('video'));\n * media.src = 'https://stream.mux.com/abc123.m3u8';\n *\n * // Change source — old engine is destroyed, new one starts clean:\n * media.src = 'https://stream.mux.com/xyz456.m3u8';\n *\n * // Explicit teardown:\n * media.destroy();\n */\nexport class SpfMedia {\n #engine: PlaybackEngine;\n #config: PlaybackEngineConfig;\n #preload: '' | 'none' | 'metadata' | 'auto' = '';\n\n /** Pending loadstart listener from a deferred play() retry, if any. */\n #loadstartListener: (() => void) | null = null;\n\n constructor(config: PlaybackEngineConfig = {}) {\n this.#config = config;\n this.#engine = createPlaybackEngine(config);\n }\n\n get engine(): PlaybackEngine {\n return this.#engine;\n }\n\n // ---------------------------------------------------------------------------\n // Media element lifecycle\n // ---------------------------------------------------------------------------\n\n attach(mediaElement: HTMLMediaElement): void {\n update(this.#engine.owners, { mediaElement });\n }\n\n detach(): void {\n this.#cancelPendingPlay();\n update(this.#engine.owners, { mediaElement: undefined });\n }\n\n destroy(): void {\n this.#cancelPendingPlay();\n this.#engine.destroy();\n }\n\n // ---------------------------------------------------------------------------\n // preload — synchronous IDL attribute (WHATWG §4.8.11.2)\n // ---------------------------------------------------------------------------\n\n get preload(): '' | 'none' | 'metadata' | 'auto' {\n return this.#preload;\n }\n\n set preload(value: '' | 'none' | 'metadata' | 'auto') {\n this.#preload = value;\n if (value) {\n update(this.#engine.state, { preload: value });\n }\n // value = '' clears #preload (so the next engine recreation won't re-apply\n // an explicit value) but does not patch current state — the existing preload\n // stays in effect until the next src change creates a fresh engine.\n }\n\n // ---------------------------------------------------------------------------\n // src — synchronous IDL attribute (WHATWG §4.8.11.2)\n // Each assignment destroys the current engine and starts a fresh one, exactly\n // as the browser's load algorithm resets all media element state on src change.\n // ---------------------------------------------------------------------------\n\n get src(): string {\n return this.#engine.state.get().presentation?.url ?? '';\n }\n\n set src(value: string) {\n const prevMediaElement = this.#engine.owners.get().mediaElement;\n\n this.#cancelPendingPlay();\n this.#engine.destroy();\n this.#engine = createPlaybackEngine(this.#config);\n\n // Apply explicit preload before setting owners so syncPreloadAttribute skips\n // element inference and the explicit value is preserved across src changes.\n if (this.#preload) {\n update(this.#engine.state, { preload: this.#preload });\n }\n\n if (prevMediaElement) {\n update(this.#engine.owners, { mediaElement: prevMediaElement });\n }\n\n if (value) {\n update(this.#engine.state, { presentation: { url: value } });\n }\n }\n\n // ---------------------------------------------------------------------------\n // play() — WHATWG §4.8.11.8\n // Delegates to the attached media element's native play().\n // ---------------------------------------------------------------------------\n\n play(): Promise<void> {\n const { mediaElement } = this.#engine.owners.get();\n if (!mediaElement) {\n return Promise.reject(new Error('SpfMedia: no media element attached'));\n }\n\n // Signal play intent — enables loading even with preload=\"none\"\n update(this.#engine.state, { playbackInitiated: true });\n\n return mediaElement.play().catch((err: unknown) => {\n // If we have a pending HLS source, the rejection may be because MSE\n // hasn't attached a blob URL yet. Wait for loadstart (src assigned\n // by MSE setup) and retry once.\n if (this.src) {\n return new Promise<void>((resolve, reject) => {\n const listener = () => {\n this.#loadstartListener = null;\n mediaElement.play().then(resolve, reject);\n };\n this.#loadstartListener = listener;\n mediaElement.addEventListener('loadstart', listener, { once: true });\n });\n }\n throw err;\n });\n }\n\n // ---------------------------------------------------------------------------\n // Private\n // ---------------------------------------------------------------------------\n\n #cancelPendingPlay(): void {\n if (!this.#loadstartListener) return;\n const { mediaElement } = this.#engine.owners.get();\n mediaElement?.removeEventListener('loadstart', this.#loadstartListener);\n this.#loadstartListener = null;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AA0BA,IAAa,WAAb,MAAsB;CACpB;CACA;CACA,WAA8C;;CAG9C,qBAA0C;CAE1C,YAAY,SAA+B,EAAE,EAAE;AAC7C,QAAA,SAAe;AACf,QAAA,SAAe,qBAAqB,OAAO;;CAG7C,IAAI,SAAyB;AAC3B,SAAO,MAAA;;CAOT,OAAO,cAAsC;AAC3C,SAAO,MAAA,OAAa,QAAQ,EAAE,cAAc,CAAC;;CAG/C,SAAe;AACb,QAAA,mBAAyB;AACzB,SAAO,MAAA,OAAa,QAAQ,EAAE,cAAc,KAAA,GAAW,CAAC;;CAG1D,UAAgB;AACd,QAAA,mBAAyB;AACzB,QAAA,OAAa,SAAS;;CAOxB,IAAI,UAA6C;AAC/C,SAAO,MAAA;;CAGT,IAAI,QAAQ,OAA0C;AACpD,QAAA,UAAgB;AAChB,MAAI,MACF,QAAO,MAAA,OAAa,OAAO,EAAE,SAAS,OAAO,CAAC;;CAalD,IAAI,MAAc;AAChB,SAAO,MAAA,OAAa,MAAM,KAAK,CAAC,cAAc,OAAO;;CAGvD,IAAI,IAAI,OAAe;EACrB,MAAM,mBAAmB,MAAA,OAAa,OAAO,KAAK,CAAC;AAEnD,QAAA,mBAAyB;AACzB,QAAA,OAAa,SAAS;AACtB,QAAA,SAAe,qBAAqB,MAAA,OAAa;AAIjD,MAAI,MAAA,QACF,QAAO,MAAA,OAAa,OAAO,EAAE,SAAS,MAAA,SAAe,CAAC;AAGxD,MAAI,iBACF,QAAO,MAAA,OAAa,QAAQ,EAAE,cAAc,kBAAkB,CAAC;AAGjE,MAAI,MACF,QAAO,MAAA,OAAa,OAAO,EAAE,cAAc,EAAE,KAAK,OAAO,EAAE,CAAC;;CAShE,OAAsB;EACpB,MAAM,EAAE,iBAAiB,MAAA,OAAa,OAAO,KAAK;AAClD,MAAI,CAAC,aACH,QAAO,QAAQ,uBAAO,IAAI,MAAM,sCAAsC,CAAC;AAIzE,SAAO,MAAA,OAAa,OAAO,EAAE,mBAAmB,MAAM,CAAC;AAEvD,SAAO,aAAa,MAAM,CAAC,OAAO,QAAiB;AAIjD,OAAI,KAAK,IACP,QAAO,IAAI,SAAe,SAAS,WAAW;IAC5C,MAAM,iBAAiB;AACrB,WAAA,oBAA0B;AAC1B,kBAAa,MAAM,CAAC,KAAK,SAAS,OAAO;;AAE3C,UAAA,oBAA0B;AAC1B,iBAAa,iBAAiB,aAAa,UAAU,EAAE,MAAM,MAAM,CAAC;KACpE;AAEJ,SAAM;IACN;;CAOJ,qBAA2B;AACzB,MAAI,CAAC,MAAA,kBAAyB;EAC9B,MAAM,EAAE,iBAAiB,MAAA,OAAa,OAAO,KAAK;AAClD,gBAAc,oBAAoB,aAAa,MAAA,kBAAwB;AACvE,QAAA,oBAA0B"}
@@ -1,15 +1,15 @@
1
- import { createState } from "../../core/state/create-state.js";
2
- import { createEventStream } from "../../core/events/create-event-stream.js";
3
- import { resolvePresentation, syncPreloadAttribute } from "../../core/features/resolve-presentation.js";
4
- import { switchQuality } from "../../core/features/quality-switching.js";
1
+ import { signal } from "../../core/signals/primitives.js";
5
2
  import { loadSegments } from "../features/load-segments.js";
6
3
  import { destroyVttParser } from "../text/parse-vtt-segment.js";
7
4
  import { loadTextTrackCues } from "../features/load-text-track-cues.js";
8
5
  import { trackCurrentTime } from "../features/track-current-time.js";
9
6
  import { trackPlaybackInitiated } from "../features/track-playback-initiated.js";
10
7
  import { calculatePresentationDuration } from "../../core/features/calculate-presentation-duration.js";
8
+ import { switchQuality } from "../../core/features/quality-switching.js";
9
+ import { resolvePresentation } from "../../core/features/resolve-presentation.js";
11
10
  import { resolveTrack } from "../../core/features/resolve-track.js";
12
11
  import { selectAudioTrack, selectTextTrack, selectVideoTrack } from "../../core/features/select-tracks.js";
12
+ import { syncPreloadAttribute } from "../../core/features/sync-preload-attribute.js";
13
13
  import { endOfStream } from "../features/end-of-stream.js";
14
14
  import { setupMediaSource } from "../features/setup-mediasource.js";
15
15
  import { setupSourceBuffers } from "../features/setup-sourcebuffer.js";
@@ -17,7 +17,6 @@ import { setupTextTracks } from "../features/setup-text-tracks.js";
17
17
  import { syncSelectedTextTrackFromDom } from "../features/sync-selected-text-track-from-dom.js";
18
18
  import { syncTextTrackModes } from "../features/sync-text-track-modes.js";
19
19
  import { updateDuration } from "../features/update-duration.js";
20
-
21
20
  //#region src/dom/playback-engine/engine.ts
22
21
  /**
23
22
  * Create a POC playback engine.
@@ -40,78 +39,56 @@ import { updateDuration } from "../features/update-duration.js";
40
39
  * preferredAudioLanguage: 'en',
41
40
  * });
42
41
  *
43
- * // Initialize by patching state and owners
44
- * engine.owners.patch({ mediaElement: document.querySelector('video') });
45
- * engine.state.patch({
42
+ * // Initialize by setting state and owners
43
+ * engine.owners.set({ ...engine.owners.get(), mediaElement: document.querySelector('video') });
44
+ * engine.state.set({
45
+ * ...engine.state.get(),
46
46
  * presentation: { url: 'https://example.com/playlist.m3u8' },
47
47
  * preload: 'auto',
48
48
  * });
49
49
  *
50
50
  * // Inspect state
51
- * console.log(engine.state.current);
51
+ * console.log(engine.state.get());
52
52
  *
53
53
  * // Cleanup
54
54
  * engine.destroy();
55
55
  */
56
56
  function createPlaybackEngine(config = {}) {
57
- const state = createState({ bandwidthState: {
57
+ const state = signal({ bandwidthState: {
58
58
  fastEstimate: 0,
59
59
  fastTotalWeight: 0,
60
60
  slowEstimate: 0,
61
61
  slowTotalWeight: 0,
62
62
  bytesSampled: 0
63
63
  } });
64
- const owners = createState({});
65
- const events = createEventStream();
64
+ const owners = signal({});
66
65
  const cleanups = [
67
- syncPreloadAttribute(state, owners),
68
- trackPlaybackInitiated({
66
+ syncPreloadAttribute({
69
67
  state,
70
- owners,
71
- events
68
+ owners
72
69
  }),
73
- resolvePresentation({
70
+ trackPlaybackInitiated({
74
71
  state,
75
- events
72
+ owners
76
73
  }),
77
- selectVideoTrack({
78
- state,
79
- owners,
80
- events
81
- }, {
74
+ resolvePresentation({ state }),
75
+ selectVideoTrack({ state }, {
82
76
  type: "video",
83
77
  ...config.initialBandwidth !== void 0 && { initialBandwidth: config.initialBandwidth }
84
78
  }),
85
- selectAudioTrack({
86
- state,
87
- owners,
88
- events
89
- }, {
79
+ selectAudioTrack({ state }, {
90
80
  type: "audio",
91
81
  ...config.preferredAudioLanguage !== void 0 && { preferredAudioLanguage: config.preferredAudioLanguage }
92
82
  }),
93
- selectTextTrack({
94
- state,
95
- owners,
96
- events
97
- }, {
83
+ selectTextTrack({ state }, {
98
84
  type: "text",
99
85
  ...config.preferredSubtitleLanguage !== void 0 && { preferredSubtitleLanguage: config.preferredSubtitleLanguage },
100
86
  ...config.includeForcedTracks !== void 0 && { includeForcedTracks: config.includeForcedTracks },
101
87
  ...config.enableDefaultTrack !== void 0 && { enableDefaultTrack: config.enableDefaultTrack }
102
88
  }),
103
- resolveTrack({
104
- state,
105
- events
106
- }, { type: "video" }),
107
- resolveTrack({
108
- state,
109
- events
110
- }, { type: "audio" }),
111
- resolveTrack({
112
- state,
113
- events
114
- }, { type: "text" }),
89
+ resolveTrack({ state }, { type: "video" }),
90
+ resolveTrack({ state }, { type: "audio" }),
91
+ resolveTrack({ state }, { type: "text" }),
115
92
  calculatePresentationDuration({ state }),
116
93
  setupMediaSource({
117
94
  state,
@@ -129,7 +106,7 @@ function createPlaybackEngine(config = {}) {
129
106
  state,
130
107
  owners
131
108
  }),
132
- switchQuality({ state }),
109
+ switchQuality({ state }, config.initialBandwidth !== void 0 ? { defaultBandwidth: config.initialBandwidth } : {}),
133
110
  loadSegments({
134
111
  state,
135
112
  owners
@@ -159,18 +136,16 @@ function createPlaybackEngine(config = {}) {
159
136
  owners
160
137
  })
161
138
  ];
162
- events.dispatch({ type: "@@INITIALIZE@@" });
163
139
  return {
164
140
  state,
165
141
  owners,
166
- events,
167
142
  destroy: () => {
168
143
  cleanups.forEach((cleanup) => cleanup());
169
144
  destroyVttParser();
170
145
  }
171
146
  };
172
147
  }
173
-
174
148
  //#endregion
175
149
  export { createPlaybackEngine };
150
+
176
151
  //# sourceMappingURL=engine.js.map