@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,55 +1,53 @@
1
+ import { effect } from "../../core/signals/effect.js";
2
+ import { computed, signal, update } from "../../core/signals/primitives.js";
1
3
  import { listen } from "@videojs/utils/dom";
2
-
3
4
  //#region src/dom/features/track-playback-initiated.ts
4
5
  /**
5
- * Track whether playback has been initiated by the user.
6
- *
7
- * Sets `state.playbackInitiated = true` when the media element fires a `play`
8
- * event (via `element.play()`, native controls, or autoplay) and simultaneously
9
- * dispatches `{ type: 'play' }` to the event stream so `resolvePresentation`
10
- * can react.
6
+ * Track whether playback has been initiated for the current presentation URL.
11
7
  *
12
- * Resets `state.playbackInitiated = false` when `presentation.url` changes,
13
- * so a new source with `preload="none"` won't load segments until play is
14
- * triggered again.
8
+ * Uses a local intermediate signal written by two effect streams:
9
+ * - false stream: resets on URL change
10
+ * - true stream: sets on play event
15
11
  *
16
- * This flag is used by `shouldLoadSegments` to allow segment loading after
17
- * play is initiated regardless of the initial `preload` setting `preload`
18
- * is a startup hint, not a runtime gate.
12
+ * A third merge effect reads the local signal and writes to state, reading
13
+ * `state.get()` at merge time so the spread uses the up-to-date value after
14
+ * the async forward bridge has run.
19
15
  *
20
16
  * @example
21
17
  * const cleanup = trackPlaybackInitiated({ state, owners, events });
22
18
  */
23
- function trackPlaybackInitiated({ state, owners, events }) {
24
- let lastMediaElement;
25
- let removeListener = null;
19
+ function trackPlaybackInitiated({ state, owners }) {
20
+ const presentationUrl = computed(() => state.get().presentation?.url);
21
+ const mediaElement = computed(() => owners.get().mediaElement);
22
+ const playbackInitiated = signal(void 0);
26
23
  let lastPresentationUrl;
27
- const unsubscribeState = state.subscribe((currentState) => {
28
- const url = currentState.presentation?.url;
29
- if (url !== lastPresentationUrl) {
30
- if (lastPresentationUrl !== void 0) state.patch({ playbackInitiated: false });
31
- lastPresentationUrl = url;
32
- }
24
+ let lastMediaElement;
25
+ const cleanupResetEffect = effect(() => {
26
+ const url = presentationUrl.get();
27
+ const el = mediaElement.get();
28
+ if (url !== lastPresentationUrl && lastPresentationUrl !== void 0 || el !== lastMediaElement && lastMediaElement !== void 0) playbackInitiated.set(false);
29
+ lastPresentationUrl = url;
30
+ lastMediaElement = el;
33
31
  });
34
- const unsubscribeOwners = owners.subscribe((currentOwners) => {
35
- const { mediaElement } = currentOwners;
36
- if (mediaElement === lastMediaElement) return;
37
- removeListener?.();
38
- removeListener = null;
39
- lastMediaElement = mediaElement;
40
- if (!mediaElement) return;
41
- removeListener = listen(mediaElement, "play", () => {
42
- state.patch({ playbackInitiated: true });
43
- events.dispatch({ type: "play" });
32
+ const cleanupPlayEffect = effect(() => {
33
+ const el = mediaElement.get();
34
+ if (!el) return;
35
+ return listen(el, "play", () => {
36
+ playbackInitiated.set(true);
44
37
  });
45
38
  });
39
+ const cleanupMergeEffect = effect(() => {
40
+ const pi = playbackInitiated.get();
41
+ if (pi === void 0) return;
42
+ if (state.get().playbackInitiated !== pi) update(state, { playbackInitiated: pi });
43
+ });
46
44
  return () => {
47
- removeListener?.();
48
- unsubscribeState();
49
- unsubscribeOwners();
45
+ cleanupResetEffect();
46
+ cleanupPlayEffect();
47
+ cleanupMergeEffect();
50
48
  };
51
49
  }
52
-
53
50
  //#endregion
54
51
  export { trackPlaybackInitiated };
52
+
55
53
  //# sourceMappingURL=track-playback-initiated.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"track-playback-initiated.js","names":[],"sources":["../../../../src/dom/features/track-playback-initiated.ts"],"sourcesContent":["import { listen } from '@videojs/utils/dom';\nimport type { EventStream } from '../../core/events/create-event-stream';\nimport type { PresentationAction } from '../../core/features/resolve-presentation';\nimport type { WritableState } from '../../core/state/create-state';\n\n/**\n * State shape for playback initiation tracking.\n */\nexport interface PlaybackInitiatedState {\n /** True once the user has initiated playback (play event fired). */\n playbackInitiated?: boolean;\n /** Current presentation — watched for URL changes to reset playbackInitiated. */\n presentation?: { url?: string };\n}\n\n/**\n * Owners shape for playback initiation tracking.\n */\nexport interface PlaybackInitiatedOwners {\n mediaElement?: HTMLMediaElement | undefined;\n}\n\n/**\n * Track whether playback has been initiated by the user.\n *\n * Sets `state.playbackInitiated = true` when the media element fires a `play`\n * event (via `element.play()`, native controls, or autoplay) and simultaneously\n * dispatches `{ type: 'play' }` to the event stream so `resolvePresentation`\n * can react.\n *\n * Resets `state.playbackInitiated = false` when `presentation.url` changes,\n * so a new source with `preload=\"none\"` won't load segments until play is\n * triggered again.\n *\n * This flag is used by `shouldLoadSegments` to allow segment loading after\n * play is initiated regardless of the initial `preload` setting `preload`\n * is a startup hint, not a runtime gate.\n *\n * @example\n * const cleanup = trackPlaybackInitiated({ state, owners, events });\n */\nexport function trackPlaybackInitiated({\n state,\n owners,\n events,\n}: {\n state: WritableState<PlaybackInitiatedState>;\n owners: WritableState<PlaybackInitiatedOwners>;\n events: EventStream<PresentationAction>;\n}): () => void {\n let lastMediaElement: HTMLMediaElement | undefined;\n let removeListener: (() => void) | null = null;\n let lastPresentationUrl: string | undefined;\n\n // Watch for presentation URL changes to reset playbackInitiated\n const unsubscribeState = state.subscribe((currentState) => {\n const url = currentState.presentation?.url;\n if (url !== lastPresentationUrl) {\n if (lastPresentationUrl !== undefined) {\n // URL changed to a new value — reset so new source requires play again\n state.patch({ playbackInitiated: false });\n }\n lastPresentationUrl = url;\n }\n });\n\n // Bridge media element play event state flag + event stream\n const unsubscribeOwners = owners.subscribe((currentOwners) => {\n const { mediaElement } = currentOwners;\n\n if (mediaElement === lastMediaElement) return;\n\n removeListener?.();\n removeListener = null;\n lastMediaElement = mediaElement;\n\n if (!mediaElement) return;\n\n removeListener = listen(mediaElement, 'play', () => {\n state.patch({ playbackInitiated: true });\n events.dispatch({ type: 'play' });\n });\n });\n\n return () => {\n removeListener?.();\n unsubscribeState();\n unsubscribeOwners();\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAyCA,SAAgB,uBAAuB,EACrC,OACA,QACA,UAKa;CACb,IAAI;CACJ,IAAI,iBAAsC;CAC1C,IAAI;CAGJ,MAAM,mBAAmB,MAAM,WAAW,iBAAiB;EACzD,MAAM,MAAM,aAAa,cAAc;AACvC,MAAI,QAAQ,qBAAqB;AAC/B,OAAI,wBAAwB,OAE1B,OAAM,MAAM,EAAE,mBAAmB,OAAO,CAAC;AAE3C,yBAAsB;;GAExB;CAGF,MAAM,oBAAoB,OAAO,WAAW,kBAAkB;EAC5D,MAAM,EAAE,iBAAiB;AAEzB,MAAI,iBAAiB,iBAAkB;AAEvC,oBAAkB;AAClB,mBAAiB;AACjB,qBAAmB;AAEnB,MAAI,CAAC,aAAc;AAEnB,mBAAiB,OAAO,cAAc,cAAc;AAClD,SAAM,MAAM,EAAE,mBAAmB,MAAM,CAAC;AACxC,UAAO,SAAS,EAAE,MAAM,QAAQ,CAAC;IACjC;GACF;AAEF,cAAa;AACX,oBAAkB;AAClB,oBAAkB;AAClB,qBAAmB"}
1
+ {"version":3,"file":"track-playback-initiated.js","names":[],"sources":["../../../../src/dom/features/track-playback-initiated.ts"],"sourcesContent":["import { listen } from '@videojs/utils/dom';\nimport { effect } from '../../core/signals/effect';\nimport { computed, type Signal, signal, update } from '../../core/signals/primitives';\n\n/**\n * State shape for playback initiation tracking.\n */\nexport interface PlaybackInitiatedState {\n /** True once play has been requested for the current presentation URL. */\n playbackInitiated?: boolean;\n /** Current presentation — URL is used to detect source changes. */\n presentation?: { url?: string };\n}\n\n/**\n * Owners shape for playback initiation tracking.\n */\nexport interface PlaybackInitiatedOwners {\n mediaElement?: HTMLMediaElement | undefined;\n}\n\n/**\n * Track whether playback has been initiated for the current presentation URL.\n *\n * Uses a local intermediate signal written by two effect streams:\n * - false stream: resets on URL change\n * - true stream: sets on play event\n *\n * A third merge effect reads the local signal and writes to state, reading\n * `state.get()` at merge time so the spread uses the up-to-date value after\n * the async forward bridge has run.\n *\n * @example\n * const cleanup = trackPlaybackInitiated({ state, owners, events });\n */\nexport function trackPlaybackInitiated<S extends PlaybackInitiatedState, O extends PlaybackInitiatedOwners>({\n state,\n owners,\n}: {\n state: Signal<S>;\n owners: Signal<O>;\n}): () => void {\n const presentationUrl = computed(() => state.get().presentation?.url);\n const mediaElement = computed(() => owners.get().mediaElement);\n\n // Local signal: written by the URL effect (false) and the play listener (true).\n // undefined = not yet initialized (suppresses the merge effect on startup).\n const playbackInitiated = signal<boolean | undefined>(undefined);\n\n let lastPresentationUrl: string | undefined;\n let lastMediaElement: HTMLMediaElement | undefined;\n\n // False stream: reset on URL change or element swap.\n const cleanupResetEffect = effect(() => {\n const url = presentationUrl.get();\n const el = mediaElement.get();\n\n const urlChanged = url !== lastPresentationUrl;\n const elChanged = el !== lastMediaElement;\n\n if ((urlChanged && lastPresentationUrl !== undefined) || (elChanged && lastMediaElement !== undefined)) {\n playbackInitiated.set(false);\n }\n\n lastPresentationUrl = url;\n lastMediaElement = el;\n });\n\n // True stream: set on play event. Cleanup return removes the listener on element swap.\n const cleanupPlayEffect = effect(() => {\n const el = mediaElement.get();\n if (!el) return;\n return listen(el, 'play', () => {\n playbackInitiated.set(true);\n });\n });\n\n // Merge effect: bridge local signal → state.\n // Reads state.get() at merge time (after the async forward bridge has flushed)\n // so the spread captures the current value rather than a stale event-time snapshot.\n // The guard makes the write idempotent.\n const cleanupMergeEffect = effect(() => {\n const pi = playbackInitiated.get();\n if (pi === undefined) return;\n const current = state.get();\n if (current.playbackInitiated !== pi) {\n const patch: Partial<PlaybackInitiatedState> = { playbackInitiated: pi };\n update(state, patch);\n }\n });\n\n return () => {\n cleanupResetEffect();\n cleanupPlayEffect();\n cleanupMergeEffect();\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAmCA,SAAgB,uBAA4F,EAC1G,OACA,UAIa;CACb,MAAM,kBAAkB,eAAe,MAAM,KAAK,CAAC,cAAc,IAAI;CACrE,MAAM,eAAe,eAAe,OAAO,KAAK,CAAC,aAAa;CAI9D,MAAM,oBAAoB,OAA4B,KAAA,EAAU;CAEhE,IAAI;CACJ,IAAI;CAGJ,MAAM,qBAAqB,aAAa;EACtC,MAAM,MAAM,gBAAgB,KAAK;EACjC,MAAM,KAAK,aAAa,KAAK;AAK7B,MAHmB,QAAQ,uBAGR,wBAAwB,KAAA,KAFzB,OAAO,oBAE8C,qBAAqB,KAAA,EAC1F,mBAAkB,IAAI,MAAM;AAG9B,wBAAsB;AACtB,qBAAmB;GACnB;CAGF,MAAM,oBAAoB,aAAa;EACrC,MAAM,KAAK,aAAa,KAAK;AAC7B,MAAI,CAAC,GAAI;AACT,SAAO,OAAO,IAAI,cAAc;AAC9B,qBAAkB,IAAI,KAAK;IAC3B;GACF;CAMF,MAAM,qBAAqB,aAAa;EACtC,MAAM,KAAK,kBAAkB,KAAK;AAClC,MAAI,OAAO,KAAA,EAAW;AAEtB,MADgB,MAAM,KAAK,CACf,sBAAsB,GAEhC,QAAO,OADwC,EAAE,mBAAmB,IAAI,CACpD;GAEtB;AAEF,cAAa;AACX,sBAAoB;AACpB,qBAAmB;AACnB,sBAAoB"}
@@ -1,4 +1,4 @@
1
- import { WritableState } from "../../core/state/create-state.js";
1
+ import { Signal } from "../../core/signals/primitives.js";
2
2
 
3
3
  //#region src/dom/features/track-playback-rate.d.ts
4
4
  /**
@@ -22,12 +22,12 @@ interface PlaybackRateOwners {
22
22
  * @example
23
23
  * const cleanup = trackPlaybackRate({ state, owners });
24
24
  */
25
- declare function trackPlaybackRate({
25
+ declare function trackPlaybackRate<O extends PlaybackRateOwners>({
26
26
  state,
27
27
  owners
28
28
  }: {
29
- state: WritableState<PlaybackRateState>;
30
- owners: WritableState<PlaybackRateOwners>;
29
+ state: Signal<PlaybackRateState>;
30
+ owners: Signal<O>;
31
31
  }): () => void;
32
32
  //#endregion
33
33
  export { PlaybackRateOwners, PlaybackRateState, trackPlaybackRate };
@@ -1 +1 @@
1
- {"version":3,"file":"track-playback-rate.d.ts","names":[],"sources":["../../../../src/dom/features/track-playback-rate.ts"],"mappings":";;;;;AAMA;UAAiB,iBAAA;EACf,YAAA;AAAA;;AAMF;;UAAiB,kBAAA;EACf,YAAA,GAAe,gBAAA;AAAA;;;;;;;;;;iBAsBD,iBAAA,CAAA;EACd,KAAA;EACA;AAAA;EAEA,KAAA,EAAO,aAAA,CAAc,iBAAA;EACrB,MAAA,EAAQ,aAAA,CAAc,kBAAA;AAAA"}
1
+ {"version":3,"file":"track-playback-rate.d.ts","names":[],"sources":["../../../../src/dom/features/track-playback-rate.ts"],"mappings":";;;;;AAOA;UAAiB,iBAAA;EACf,YAAA;AAAA;;AAMF;;UAAiB,kBAAA;EACf,YAAA,GAAe,gBAAA;AAAA;AAYjB;;;;;;;;;AAAA,iBAAgB,iBAAA,WAA4B,kBAAA,CAAA,CAAA;EAC1C,KAAA;EACA;AAAA;EAEA,KAAA,EAAO,MAAA,CAAO,iBAAA;EACd,MAAA,EAAQ,MAAA,CAAO,CAAA;AAAA"}
@@ -1,5 +1,6 @@
1
+ import { effect } from "../../core/signals/effect.js";
2
+ import { computed, update } from "../../core/signals/primitives.js";
1
3
  import { listen } from "@videojs/utils/dom";
2
-
3
4
  //#region src/dom/features/track-playback-rate.ts
4
5
  /**
5
6
  * Track playback rate from the media element.
@@ -11,26 +12,21 @@ import { listen } from "@videojs/utils/dom";
11
12
  * const cleanup = trackPlaybackRate({ state, owners });
12
13
  */
13
14
  function trackPlaybackRate({ state, owners }) {
14
- let lastMediaElement;
15
- let removeListener = null;
16
- const unsubscribe = owners.subscribe((currentOwners) => {
17
- const { mediaElement } = currentOwners;
18
- if (mediaElement === lastMediaElement) return;
19
- removeListener?.();
20
- removeListener = null;
21
- lastMediaElement = mediaElement;
22
- if (!mediaElement) return;
23
- state.patch({ playbackRate: mediaElement.playbackRate });
24
- removeListener = listen(mediaElement, "ratechange", () => {
25
- state.patch({ playbackRate: mediaElement.playbackRate });
15
+ const mediaElementSignal = computed(() => owners.get().mediaElement);
16
+ const canTrackPlaybackRate = computed(() => !!mediaElementSignal.get());
17
+ const cleanupEffect = effect(() => {
18
+ if (!canTrackPlaybackRate.get()) return;
19
+ const mediaElement = mediaElementSignal.get();
20
+ update(state, { playbackRate: mediaElement.playbackRate });
21
+ return listen(mediaElement, "ratechange", () => {
22
+ update(state, { playbackRate: mediaElement.playbackRate });
26
23
  });
27
24
  });
28
25
  return () => {
29
- removeListener?.();
30
- unsubscribe();
26
+ cleanupEffect();
31
27
  };
32
28
  }
33
-
34
29
  //#endregion
35
30
  export { trackPlaybackRate };
31
+
36
32
  //# sourceMappingURL=track-playback-rate.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"track-playback-rate.js","names":[],"sources":["../../../../src/dom/features/track-playback-rate.ts"],"sourcesContent":["import { listen } from '@videojs/utils/dom';\nimport type { WritableState } from '../../core/state/create-state';\n\n/**\n * State shape for playback rate tracking.\n */\nexport interface PlaybackRateState {\n playbackRate?: number;\n}\n\n/**\n * Owners shape for playback rate tracking.\n */\nexport interface PlaybackRateOwners {\n mediaElement?: HTMLMediaElement | undefined;\n}\n\n/**\n * Check if we can track playback rate.\n *\n * Requires:\n * - mediaElement exists in owners\n */\nexport function canTrackPlaybackRate(owners: PlaybackRateOwners): boolean {\n return !!owners.mediaElement;\n}\n\n/**\n * Track playback rate from the media element.\n *\n * Mirrors `mediaElement.playbackRate` into reactive state on each `ratechange`\n * event. Also syncs immediately when a media element becomes available.\n *\n * @example\n * const cleanup = trackPlaybackRate({ state, owners });\n */\nexport function trackPlaybackRate({\n state,\n owners,\n}: {\n state: WritableState<PlaybackRateState>;\n owners: WritableState<PlaybackRateOwners>;\n}): () => void {\n let lastMediaElement: HTMLMediaElement | undefined;\n let removeListener: (() => void) | null = null;\n\n const unsubscribe = owners.subscribe((currentOwners) => {\n const { mediaElement } = currentOwners;\n\n if (mediaElement === lastMediaElement) return;\n\n removeListener?.();\n removeListener = null;\n lastMediaElement = mediaElement;\n\n if (!mediaElement) return;\n\n state.patch({ playbackRate: mediaElement.playbackRate });\n\n removeListener = listen(mediaElement, 'ratechange', () => {\n state.patch({ playbackRate: mediaElement.playbackRate });\n });\n });\n\n return () => {\n removeListener?.();\n unsubscribe();\n };\n}\n"],"mappings":";;;;;;;;;;;;AAoCA,SAAgB,kBAAkB,EAChC,OACA,UAIa;CACb,IAAI;CACJ,IAAI,iBAAsC;CAE1C,MAAM,cAAc,OAAO,WAAW,kBAAkB;EACtD,MAAM,EAAE,iBAAiB;AAEzB,MAAI,iBAAiB,iBAAkB;AAEvC,oBAAkB;AAClB,mBAAiB;AACjB,qBAAmB;AAEnB,MAAI,CAAC,aAAc;AAEnB,QAAM,MAAM,EAAE,cAAc,aAAa,cAAc,CAAC;AAExD,mBAAiB,OAAO,cAAc,oBAAoB;AACxD,SAAM,MAAM,EAAE,cAAc,aAAa,cAAc,CAAC;IACxD;GACF;AAEF,cAAa;AACX,oBAAkB;AAClB,eAAa"}
1
+ {"version":3,"file":"track-playback-rate.js","names":[],"sources":["../../../../src/dom/features/track-playback-rate.ts"],"sourcesContent":["import { listen } from '@videojs/utils/dom';\nimport { effect } from '../../core/signals/effect';\nimport { computed, type Signal, update } from '../../core/signals/primitives';\n\n/**\n * State shape for playback rate tracking.\n */\nexport interface PlaybackRateState {\n playbackRate?: number;\n}\n\n/**\n * Owners shape for playback rate tracking.\n */\nexport interface PlaybackRateOwners {\n mediaElement?: HTMLMediaElement | undefined;\n}\n\n/**\n * Track playback rate from the media element.\n *\n * Mirrors `mediaElement.playbackRate` into reactive state on each `ratechange`\n * event. Also syncs immediately when a media element becomes available.\n *\n * @example\n * const cleanup = trackPlaybackRate({ state, owners });\n */\nexport function trackPlaybackRate<O extends PlaybackRateOwners>({\n state,\n owners,\n}: {\n state: Signal<PlaybackRateState>;\n owners: Signal<O>;\n}): () => void {\n const mediaElementSignal = computed(() => owners.get().mediaElement);\n const canTrackPlaybackRate = computed(() => !!mediaElementSignal.get());\n\n const cleanupEffect = effect(() => {\n if (!canTrackPlaybackRate.get()) return;\n const mediaElement = mediaElementSignal.get() as HTMLMediaElement;\n\n update(state, { playbackRate: mediaElement.playbackRate });\n\n return listen(mediaElement, 'ratechange', () => {\n update(state, { playbackRate: mediaElement.playbackRate });\n });\n });\n\n return () => {\n cleanupEffect();\n };\n}\n"],"mappings":";;;;;;;;;;;;;AA2BA,SAAgB,kBAAgD,EAC9D,OACA,UAIa;CACb,MAAM,qBAAqB,eAAe,OAAO,KAAK,CAAC,aAAa;CACpE,MAAM,uBAAuB,eAAe,CAAC,CAAC,mBAAmB,KAAK,CAAC;CAEvE,MAAM,gBAAgB,aAAa;AACjC,MAAI,CAAC,qBAAqB,KAAK,CAAE;EACjC,MAAM,eAAe,mBAAmB,KAAK;AAE7C,SAAO,OAAO,EAAE,cAAc,aAAa,cAAc,CAAC;AAE1D,SAAO,OAAO,cAAc,oBAAoB;AAC9C,UAAO,OAAO,EAAE,cAAc,aAAa,cAAc,CAAC;IAC1D;GACF;AAEF,cAAa;AACX,iBAAe"}
@@ -1,6 +1,5 @@
1
+ import { effect } from "../../core/signals/effect.js";
1
2
  import { hasPresentationDuration } from "../../core/types/index.js";
2
- import { combineLatest } from "../../core/reactive/combine-latest.js";
3
-
4
3
  //#region src/dom/features/update-duration.ts
5
4
  /**
6
5
  * Check if we can update MediaSource duration (have required data).
@@ -30,7 +29,7 @@ function shouldUpdateDuration(state, owners) {
30
29
  if (!canUpdateDuration(state, owners)) return false;
31
30
  const { mediaSource } = owners;
32
31
  const { presentation } = state;
33
- if (mediaSource.readyState !== "open") return false;
32
+ if ((owners.mediaSourceReadyState?.get() ?? owners.mediaSource?.readyState) !== "open") return false;
34
33
  const duration = presentation.duration;
35
34
  if (!Number.isFinite(duration) || Number.isNaN(duration) || duration <= 0) return false;
36
35
  return Number.isNaN(mediaSource.duration);
@@ -51,22 +50,29 @@ function waitForSourceBuffersReady(owners) {
51
50
  */
52
51
  function updateDuration({ state, owners }) {
53
52
  let destroyed = false;
54
- const unsubscribe = combineLatest([state, owners]).subscribe(async ([currentState, currentOwners]) => {
55
- if (!shouldUpdateDuration(currentState, currentOwners)) return;
53
+ let running = false;
54
+ const cleanupEffect = effect(() => {
55
+ const currentState = state.get();
56
+ const currentOwners = owners.get();
57
+ if (!shouldUpdateDuration(currentState, currentOwners) || running) return;
56
58
  const { mediaSource } = currentOwners;
57
- await waitForSourceBuffersReady(currentOwners);
58
- if (destroyed || mediaSource.readyState !== "open") return;
59
- let duration = currentState.presentation.duration;
60
- const maxBufferedEnd = getMaxBufferedEnd(currentOwners);
61
- if (maxBufferedEnd > duration) duration = maxBufferedEnd;
62
- mediaSource.duration = duration;
59
+ running = true;
60
+ waitForSourceBuffersReady(currentOwners).then(() => {
61
+ if (destroyed || mediaSource.readyState !== "open") return;
62
+ let duration = currentState.presentation.duration;
63
+ const maxBufferedEnd = getMaxBufferedEnd(currentOwners);
64
+ if (maxBufferedEnd > duration) duration = maxBufferedEnd;
65
+ mediaSource.duration = duration;
66
+ }).finally(() => {
67
+ running = false;
68
+ });
63
69
  });
64
70
  return () => {
65
71
  destroyed = true;
66
- unsubscribe();
72
+ cleanupEffect();
67
73
  };
68
74
  }
69
-
70
75
  //#endregion
71
76
  export { updateDuration };
77
+
72
78
  //# sourceMappingURL=update-duration.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"update-duration.js","names":[],"sources":["../../../../src/dom/features/update-duration.ts"],"sourcesContent":["import { combineLatest } from '../../core/reactive/combine-latest';\nimport type { WritableState } from '../../core/state/create-state';\nimport type { Presentation } from '../../core/types';\nimport { hasPresentationDuration } from '../../core/types';\n\nexport interface DurationUpdateState {\n presentation?: Presentation;\n}\n\nexport interface DurationUpdateOwners {\n mediaSource?: MediaSource;\n videoSourceBuffer?: SourceBuffer;\n audioSourceBuffer?: SourceBuffer;\n}\n\n/**\n * Check if we can update MediaSource duration (have required data).\n */\nexport function canUpdateDuration(state: DurationUpdateState, owners: DurationUpdateOwners): boolean {\n return !!(owners.mediaSource && state.presentation && hasPresentationDuration(state.presentation));\n}\n\n/**\n * Get the maximum buffered end time across all SourceBuffers.\n */\nexport function getMaxBufferedEnd(owners: DurationUpdateOwners): number {\n let maxEnd = 0;\n\n const buffers = [owners.videoSourceBuffer, owners.audioSourceBuffer].filter(\n (buf): buf is SourceBuffer => buf !== undefined\n );\n\n for (const buffer of buffers) {\n const { buffered } = buffer;\n if (buffered.length > 0) {\n const end = buffered.end(buffered.length - 1);\n if (end > maxEnd) {\n maxEnd = end;\n }\n }\n }\n\n return maxEnd;\n}\n\n/**\n * Check if we should update MediaSource duration (conditions met).\n */\nexport function shouldUpdateDuration(state: DurationUpdateState, owners: DurationUpdateOwners): boolean {\n if (!canUpdateDuration(state, owners)) return false;\n\n const { mediaSource } = owners;\n const { presentation } = state;\n\n // MediaSource must be open\n if (mediaSource!.readyState !== 'open') return false;\n\n const duration = presentation!.duration!;\n\n // Validate duration: finite, positive, not NaN\n if (!Number.isFinite(duration) || Number.isNaN(duration) || duration <= 0) return false;\n\n // Only set duration on initial MediaSource setup, when it hasn't been set yet.\n // A freshly opened MediaSource has duration === NaN. Once set (either by this\n // task or by endOfStreamTask from buffered.end()), we leave it alone — attempting\n // to re-sync a slight drift between mediaSource.duration and presentation.duration\n // races with concurrent appendBuffer() calls from loadSegmentsTask.\n return Number.isNaN(mediaSource!.duration);\n}\n\n/**\n * Wait for all currently-updating SourceBuffers to finish.\n *\n * The MSE spec forbids setting MediaSource.duration while any attached\n * SourceBuffer has updating === true. This defers until all are idle.\n */\nfunction waitForSourceBuffersReady(owners: DurationUpdateOwners): Promise<void> {\n const updating = [owners.videoSourceBuffer, owners.audioSourceBuffer].filter(\n (buf): buf is SourceBuffer => buf?.updating === true\n );\n\n if (updating.length === 0) return Promise.resolve();\n\n return Promise.all(\n updating.map(\n (buf) => new Promise<void>((resolve) => buf.addEventListener('updateend', () => resolve(), { once: true }))\n )\n ).then(() => undefined);\n}\n\n/**\n * Update MediaSource duration when presentation duration becomes available.\n */\nexport function updateDuration({\n state,\n owners,\n}: {\n state: WritableState<DurationUpdateState>;\n owners: WritableState<DurationUpdateOwners>;\n}): () => void {\n let destroyed = false;\n\n const unsubscribe = combineLatest([state, owners]).subscribe(\n async ([currentState, currentOwners]: [DurationUpdateState, DurationUpdateOwners]) => {\n if (!shouldUpdateDuration(currentState, currentOwners)) return;\n\n const { mediaSource } = currentOwners;\n\n // MSE spec: duration cannot be set while any SourceBuffer is updating\n await waitForSourceBuffersReady(currentOwners);\n\n // Re-check after async wait: destroyed, or readyState changed (e.g. endOfStream\n // already called endOfStream(), transitioning 'open' → 'ended').\n if (destroyed || mediaSource!.readyState !== 'open') return;\n\n let duration = currentState.presentation!.duration!;\n\n // Get max buffered end time across all SourceBuffers\n const maxBufferedEnd = getMaxBufferedEnd(currentOwners);\n\n // MSE spec: duration cannot be less than any buffered range\n // If buffered ranges exceed calculated duration, extend to match\n if (maxBufferedEnd > duration) {\n duration = maxBufferedEnd;\n }\n\n // Set duration on MediaSource\n mediaSource!.duration = duration;\n }\n );\n\n return () => {\n destroyed = true;\n unsubscribe();\n };\n}\n"],"mappings":";;;;;;;AAkBA,SAAgB,kBAAkB,OAA4B,QAAuC;AACnG,QAAO,CAAC,EAAE,OAAO,eAAe,MAAM,gBAAgB,wBAAwB,MAAM,aAAa;;;;;AAMnG,SAAgB,kBAAkB,QAAsC;CACtE,IAAI,SAAS;CAEb,MAAM,UAAU,CAAC,OAAO,mBAAmB,OAAO,kBAAkB,CAAC,QAClE,QAA6B,QAAQ,OACvC;AAED,MAAK,MAAM,UAAU,SAAS;EAC5B,MAAM,EAAE,aAAa;AACrB,MAAI,SAAS,SAAS,GAAG;GACvB,MAAM,MAAM,SAAS,IAAI,SAAS,SAAS,EAAE;AAC7C,OAAI,MAAM,OACR,UAAS;;;AAKf,QAAO;;;;;AAMT,SAAgB,qBAAqB,OAA4B,QAAuC;AACtG,KAAI,CAAC,kBAAkB,OAAO,OAAO,CAAE,QAAO;CAE9C,MAAM,EAAE,gBAAgB;CACxB,MAAM,EAAE,iBAAiB;AAGzB,KAAI,YAAa,eAAe,OAAQ,QAAO;CAE/C,MAAM,WAAW,aAAc;AAG/B,KAAI,CAAC,OAAO,SAAS,SAAS,IAAI,OAAO,MAAM,SAAS,IAAI,YAAY,EAAG,QAAO;AAOlF,QAAO,OAAO,MAAM,YAAa,SAAS;;;;;;;;AAS5C,SAAS,0BAA0B,QAA6C;CAC9E,MAAM,WAAW,CAAC,OAAO,mBAAmB,OAAO,kBAAkB,CAAC,QACnE,QAA6B,KAAK,aAAa,KACjD;AAED,KAAI,SAAS,WAAW,EAAG,QAAO,QAAQ,SAAS;AAEnD,QAAO,QAAQ,IACb,SAAS,KACN,QAAQ,IAAI,SAAe,YAAY,IAAI,iBAAiB,mBAAmB,SAAS,EAAE,EAAE,MAAM,MAAM,CAAC,CAAC,CAC5G,CACF,CAAC,WAAW,OAAU;;;;;AAMzB,SAAgB,eAAe,EAC7B,OACA,UAIa;CACb,IAAI,YAAY;CAEhB,MAAM,cAAc,cAAc,CAAC,OAAO,OAAO,CAAC,CAAC,UACjD,OAAO,CAAC,cAAc,mBAAgE;AACpF,MAAI,CAAC,qBAAqB,cAAc,cAAc,CAAE;EAExD,MAAM,EAAE,gBAAgB;AAGxB,QAAM,0BAA0B,cAAc;AAI9C,MAAI,aAAa,YAAa,eAAe,OAAQ;EAErD,IAAI,WAAW,aAAa,aAAc;EAG1C,MAAM,iBAAiB,kBAAkB,cAAc;AAIvD,MAAI,iBAAiB,SACnB,YAAW;AAIb,cAAa,WAAW;GAE3B;AAED,cAAa;AACX,cAAY;AACZ,eAAa"}
1
+ {"version":3,"file":"update-duration.js","names":[],"sources":["../../../../src/dom/features/update-duration.ts"],"sourcesContent":["import { effect } from '../../core/signals/effect';\nimport type { ReadonlySignal, Signal } from '../../core/signals/primitives';\nimport type { Presentation } from '../../core/types';\nimport { hasPresentationDuration } from '../../core/types';\n\nexport interface DurationUpdateState {\n presentation?: Presentation;\n}\n\nexport interface DurationUpdateOwners {\n mediaSource?: MediaSource;\n /** Reactive mirror of `mediaSource.readyState` — updated via DOM events. */\n mediaSourceReadyState?: ReadonlySignal<MediaSource['readyState']>;\n videoSourceBuffer?: SourceBuffer;\n audioSourceBuffer?: SourceBuffer;\n}\n\n/**\n * Check if we can update MediaSource duration (have required data).\n */\nexport function canUpdateDuration(state: DurationUpdateState, owners: DurationUpdateOwners): boolean {\n return !!(owners.mediaSource && state.presentation && hasPresentationDuration(state.presentation));\n}\n\n/**\n * Get the maximum buffered end time across all SourceBuffers.\n */\nexport function getMaxBufferedEnd(owners: DurationUpdateOwners): number {\n let maxEnd = 0;\n\n const buffers = [owners.videoSourceBuffer, owners.audioSourceBuffer].filter(\n (buf): buf is SourceBuffer => buf !== undefined\n );\n\n for (const buffer of buffers) {\n const { buffered } = buffer;\n if (buffered.length > 0) {\n const end = buffered.end(buffered.length - 1);\n if (end > maxEnd) {\n maxEnd = end;\n }\n }\n }\n\n return maxEnd;\n}\n\n/**\n * Check if we should update MediaSource duration (conditions met).\n */\nexport function shouldUpdateDuration(state: DurationUpdateState, owners: DurationUpdateOwners): boolean {\n if (!canUpdateDuration(state, owners)) return false;\n\n const { mediaSource } = owners;\n const { presentation } = state;\n\n // MediaSource must be open — use reactive readyState signal when available\n // so the effect re-evaluates when readyState changes from 'closed' to 'open'.\n const readyState = owners.mediaSourceReadyState?.get() ?? owners.mediaSource?.readyState;\n if (readyState !== 'open') return false;\n\n const duration = presentation!.duration!;\n\n // Validate duration: finite, positive, not NaN\n if (!Number.isFinite(duration) || Number.isNaN(duration) || duration <= 0) return false;\n\n // Only set duration on initial MediaSource setup, when it hasn't been set yet.\n // A freshly opened MediaSource has duration === NaN. Once set (either by this\n // task or by endOfStreamTask from buffered.end()), we leave it alone — attempting\n // to re-sync a slight drift between mediaSource.duration and presentation.duration\n // races with concurrent appendBuffer() calls from loadSegmentsTask.\n return Number.isNaN(mediaSource!.duration);\n}\n\n/**\n * Wait for all currently-updating SourceBuffers to finish.\n *\n * The MSE spec forbids setting MediaSource.duration while any attached\n * SourceBuffer has updating === true. This defers until all are idle.\n */\nfunction waitForSourceBuffersReady(owners: DurationUpdateOwners): Promise<void> {\n const updating = [owners.videoSourceBuffer, owners.audioSourceBuffer].filter(\n (buf): buf is SourceBuffer => buf?.updating === true\n );\n\n if (updating.length === 0) return Promise.resolve();\n\n return Promise.all(\n updating.map(\n (buf) => new Promise<void>((resolve) => buf.addEventListener('updateend', () => resolve(), { once: true }))\n )\n ).then(() => undefined);\n}\n\n/**\n * Update MediaSource duration when presentation duration becomes available.\n */\nexport function updateDuration<S extends DurationUpdateState, O extends DurationUpdateOwners>({\n state,\n owners,\n}: {\n state: Signal<S>;\n owners: Signal<O>;\n}): () => void {\n let destroyed = false;\n let running = false;\n\n const cleanupEffect = effect(() => {\n const currentState = state.get();\n const currentOwners = owners.get();\n\n if (!shouldUpdateDuration(currentState, currentOwners) || running) return;\n\n const { mediaSource } = currentOwners;\n running = true;\n\n // MSE spec: duration cannot be set while any SourceBuffer is updating.\n // Capture the snapshot; the async helper does not read signals.\n waitForSourceBuffersReady(currentOwners)\n .then(() => {\n // Re-check after async wait: destroyed, or readyState changed (e.g. endOfStream\n // already called endOfStream(), transitioning 'open' → 'ended').\n if (destroyed || mediaSource!.readyState !== 'open') return;\n\n let duration = currentState.presentation!.duration!;\n\n // MSE spec: duration cannot be less than any buffered range.\n // If buffered ranges exceed calculated duration, extend to match.\n const maxBufferedEnd = getMaxBufferedEnd(currentOwners);\n if (maxBufferedEnd > duration) {\n duration = maxBufferedEnd;\n }\n\n mediaSource!.duration = duration;\n })\n .finally(() => {\n running = false;\n });\n });\n\n return () => {\n destroyed = true;\n cleanupEffect();\n };\n}\n"],"mappings":";;;;;;AAoBA,SAAgB,kBAAkB,OAA4B,QAAuC;AACnG,QAAO,CAAC,EAAE,OAAO,eAAe,MAAM,gBAAgB,wBAAwB,MAAM,aAAa;;;;;AAMnG,SAAgB,kBAAkB,QAAsC;CACtE,IAAI,SAAS;CAEb,MAAM,UAAU,CAAC,OAAO,mBAAmB,OAAO,kBAAkB,CAAC,QAClE,QAA6B,QAAQ,KAAA,EACvC;AAED,MAAK,MAAM,UAAU,SAAS;EAC5B,MAAM,EAAE,aAAa;AACrB,MAAI,SAAS,SAAS,GAAG;GACvB,MAAM,MAAM,SAAS,IAAI,SAAS,SAAS,EAAE;AAC7C,OAAI,MAAM,OACR,UAAS;;;AAKf,QAAO;;;;;AAMT,SAAgB,qBAAqB,OAA4B,QAAuC;AACtG,KAAI,CAAC,kBAAkB,OAAO,OAAO,CAAE,QAAO;CAE9C,MAAM,EAAE,gBAAgB;CACxB,MAAM,EAAE,iBAAiB;AAKzB,MADmB,OAAO,uBAAuB,KAAK,IAAI,OAAO,aAAa,gBAC3D,OAAQ,QAAO;CAElC,MAAM,WAAW,aAAc;AAG/B,KAAI,CAAC,OAAO,SAAS,SAAS,IAAI,OAAO,MAAM,SAAS,IAAI,YAAY,EAAG,QAAO;AAOlF,QAAO,OAAO,MAAM,YAAa,SAAS;;;;;;;;AAS5C,SAAS,0BAA0B,QAA6C;CAC9E,MAAM,WAAW,CAAC,OAAO,mBAAmB,OAAO,kBAAkB,CAAC,QACnE,QAA6B,KAAK,aAAa,KACjD;AAED,KAAI,SAAS,WAAW,EAAG,QAAO,QAAQ,SAAS;AAEnD,QAAO,QAAQ,IACb,SAAS,KACN,QAAQ,IAAI,SAAe,YAAY,IAAI,iBAAiB,mBAAmB,SAAS,EAAE,EAAE,MAAM,MAAM,CAAC,CAAC,CAC5G,CACF,CAAC,WAAW,KAAA,EAAU;;;;;AAMzB,SAAgB,eAA8E,EAC5F,OACA,UAIa;CACb,IAAI,YAAY;CAChB,IAAI,UAAU;CAEd,MAAM,gBAAgB,aAAa;EACjC,MAAM,eAAe,MAAM,KAAK;EAChC,MAAM,gBAAgB,OAAO,KAAK;AAElC,MAAI,CAAC,qBAAqB,cAAc,cAAc,IAAI,QAAS;EAEnE,MAAM,EAAE,gBAAgB;AACxB,YAAU;AAIV,4BAA0B,cAAc,CACrC,WAAW;AAGV,OAAI,aAAa,YAAa,eAAe,OAAQ;GAErD,IAAI,WAAW,aAAa,aAAc;GAI1C,MAAM,iBAAiB,kBAAkB,cAAc;AACvD,OAAI,iBAAiB,SACnB,YAAW;AAGb,eAAa,WAAW;IACxB,CACD,cAAc;AACb,aAAU;IACV;GACJ;AAEF,cAAa;AACX,cAAY;AACZ,iBAAe"}
@@ -54,7 +54,7 @@ async function appendChunk(sourceBuffer, data) {
54
54
  }
55
55
  });
56
56
  }
57
-
58
57
  //#endregion
59
58
  export { appendSegment };
59
+
60
60
  //# sourceMappingURL=append-segment.js.map
@@ -49,7 +49,7 @@ async function flushBuffer(sourceBuffer, start, end) {
49
49
  }
50
50
  });
51
51
  }
52
-
53
52
  //#endregion
54
53
  export { flushBuffer };
54
+
55
55
  //# sourceMappingURL=buffer-flusher.js.map
@@ -1,3 +1,4 @@
1
+ import { signal } from "../../core/signals/primitives.js";
1
2
  //#region src/dom/media/mediasource-setup.ts
2
3
  /**
3
4
  * MediaSource Setup
@@ -50,7 +51,6 @@ function createMediaSource(options = {}) {
50
51
  * @example
51
52
  * const mediaSource = createMediaSource();
52
53
  * const { detach } = attachMediaSource(mediaSource, videoElement);
53
- * await waitForSourceOpen(mediaSource);
54
54
  * // Use mediaSource...
55
55
  * // Later, to clean up:
56
56
  * detach();
@@ -81,42 +81,6 @@ function attachMediaSource(mediaSource, mediaElement) {
81
81
  };
82
82
  }
83
83
  /**
84
- * Wait for a MediaSource to reach the 'open' state.
85
- * Resolves immediately if already open.
86
- *
87
- * @param mediaSource - The MediaSource to wait for
88
- * @param signal - Optional AbortSignal for cancellation
89
- * @returns Promise that resolves when the MediaSource is open
90
- *
91
- * @example
92
- * const mediaSource = createMediaSource();
93
- * attachMediaSource(mediaSource, videoElement);
94
- * await waitForSourceOpen(mediaSource);
95
- * // MediaSource is now ready for SourceBuffer creation
96
- */
97
- function waitForSourceOpen(mediaSource, signal) {
98
- return new Promise((resolve, reject) => {
99
- if (mediaSource.readyState === "open") {
100
- resolve();
101
- return;
102
- }
103
- if (signal?.aborted) {
104
- reject(new DOMException("Aborted", "AbortError"));
105
- return;
106
- }
107
- const controller = new AbortController();
108
- const options = { signal: controller.signal };
109
- mediaSource.addEventListener("sourceopen", () => {
110
- controller.abort();
111
- resolve();
112
- }, options);
113
- signal?.addEventListener("abort", () => {
114
- controller.abort();
115
- reject(new DOMException("Aborted", "AbortError"));
116
- }, options);
117
- });
118
- }
119
- /**
120
84
  * Create a SourceBuffer on a MediaSource.
121
85
  *
122
86
  * @param mediaSource - The MediaSource (must be in 'open' state)
@@ -125,7 +89,6 @@ function waitForSourceOpen(mediaSource, signal) {
125
89
  * @throws Error if MediaSource is not open or codec is unsupported
126
90
  *
127
91
  * @example
128
- * await waitForSourceOpen(mediaSource);
129
92
  * const buffer = createSourceBuffer(mediaSource, 'video/mp4; codecs="avc1.42E01E"');
130
93
  */
131
94
  function createSourceBuffer(mediaSource, mimeCodec) {
@@ -148,7 +111,36 @@ function isCodecSupported(mimeCodec) {
148
111
  if (!supportsMediaSource()) return false;
149
112
  return MediaSource.isTypeSupported(mimeCodec);
150
113
  }
151
-
114
+ /**
115
+ * Create a reactive signal that mirrors `mediaSource.readyState`.
116
+ *
117
+ * Listens to `sourceopen`, `sourceended`, and `sourceclose` events and updates
118
+ * the signal accordingly, making readyState visible to the TC39 signal graph.
119
+ * Listeners are automatically removed when `signal` is aborted.
120
+ *
121
+ * @param mediaSource - The MediaSource to observe
122
+ * @param signal - AbortSignal that controls listener lifetime
123
+ * @returns A `Signal.ReadonlyState` that reflects the current `readyState`
124
+ *
125
+ * @example
126
+ * const controller = new AbortController();
127
+ * const readyState = observeMediaSourceReadyState(mediaSource, controller.signal);
128
+ * effect(() => {
129
+ * if (readyState.get() === 'open') { ... }
130
+ * });
131
+ * // Later:
132
+ * controller.abort();
133
+ */
134
+ function observeMediaSourceReadyState(mediaSource, abortSignal) {
135
+ const readyState = signal(mediaSource.readyState);
136
+ const update = () => readyState.set(mediaSource.readyState);
137
+ const options = { signal: abortSignal };
138
+ mediaSource.addEventListener("sourceopen", update, options);
139
+ mediaSource.addEventListener("sourceended", update, options);
140
+ mediaSource.addEventListener("sourceclose", update, options);
141
+ return readyState;
142
+ }
152
143
  //#endregion
153
- export { attachMediaSource, createMediaSource, createSourceBuffer, isCodecSupported, supportsManagedMediaSource, supportsMediaSource, waitForSourceOpen };
144
+ export { attachMediaSource, createMediaSource, createSourceBuffer, observeMediaSourceReadyState };
145
+
154
146
  //# sourceMappingURL=mediasource-setup.js.map
@@ -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,5 +1,5 @@
1
1
  import { Segment, Track } from "../../core/types/index.js";
2
- import { Actor } from "../../core/actor.js";
2
+ import { SignalActor } from "../../core/actor.js";
3
3
  import { AppendData } from "./append-segment.js";
4
4
 
5
5
  //#region src/dom/media/source-buffer-actor.d.ts
@@ -47,7 +47,7 @@ interface SourceBufferActorContext {
47
47
  bufferedRanges: BufferedRange[];
48
48
  }
49
49
  /** SourceBuffer actor: queues operations, owns its snapshot. */
50
- interface SourceBufferActor extends Actor<SourceBufferActorStatus, SourceBufferActorContext> {
50
+ interface SourceBufferActor extends SignalActor<SourceBufferActorStatus, SourceBufferActorContext> {
51
51
  send(message: SourceBufferMessage, signal: AbortSignal): Promise<void>;
52
52
  batch(messages: SourceBufferMessage[], signal: AbortSignal): Promise<void>;
53
53
  }
@@ -1 +1 @@
1
- {"version":3,"file":"source-buffer-actor.d.ts","names":[],"sources":["../../../../src/dom/media/source-buffer-actor.ts"],"mappings":";;;;;UAWiB,aAAA;EACf,KAAA;EACA,GAAA;AAAA;AAAA,KAGU,iBAAA,GAAoB,IAAA,CAAK,OAAA;EACnC,OAAA,EAAS,KAAA,QAJN;EAMH,cAAA;AAAA;AAAA,KAKU,iBAAA;EAAsB,IAAA;EAAqB,IAAA,EAAM,UAAA;EAAY,IAAA;IAAQ,OAAA,EAAS,KAAA;EAAA;AAAA;AAAA,KAC9E,oBAAA;EAAyB,IAAA;EAAwB,IAAA,EAAM,UAAA;EAAY,IAAA,EAAM,iBAAA;AAAA;AAAA,KACzE,aAAA;EAAkB,IAAA;EAAgB,KAAA;EAAe,GAAA;AAAA;AAAA,KACjD,mBAAA,GAAsB,iBAAA,GAAoB,oBAAA,GAAuB,aAAA;;KAGjE,uBAAA;;UAGK,wBAAA;EACf,WAAA;EACA,QAAA,EAAU,KAAA,CACR,IAAA,CAAK,OAAA;IACH,OAAA,EAAS,KAAA;IACT,cAAA;IAb6D;;;;;IAmB7D,OAAA;EAAA;EAGJ,cAAA,EAAgB,aAAA;AAAA;AApBlB;AAAA,UAsCiB,iBAAA,SAA0B,KAAA,CAAM,uBAAA,EAAyB,wBAAA;EACxE,IAAA,CAAK,OAAA,EAAS,mBAAA,EAAqB,MAAA,EAAQ,WAAA,GAAc,OAAA;EACzD,KAAA,CAAM,QAAA,EAAU,mBAAA,IAAuB,MAAA,EAAQ,WAAA,GAAc,OAAA;AAAA"}
1
+ {"version":3,"file":"source-buffer-actor.d.ts","names":[],"sources":["../../../../src/dom/media/source-buffer-actor.ts"],"mappings":";;;;;UAWiB,aAAA;EACf,KAAA;EACA,GAAA;AAAA;AAAA,KAGU,iBAAA,GAAoB,IAAA,CAAK,OAAA;EACnC,OAAA,EAAS,KAAA,QAJN;EAMH,cAAA;AAAA;AAAA,KAKU,iBAAA;EAAsB,IAAA;EAAqB,IAAA,EAAM,UAAA;EAAY,IAAA;IAAQ,OAAA,EAAS,KAAA;EAAA;AAAA;AAAA,KAC9E,oBAAA;EAAyB,IAAA;EAAwB,IAAA,EAAM,UAAA;EAAY,IAAA,EAAM,iBAAA;AAAA;AAAA,KACzE,aAAA;EAAkB,IAAA;EAAgB,KAAA;EAAe,GAAA;AAAA;AAAA,KACjD,mBAAA,GAAsB,iBAAA,GAAoB,oBAAA,GAAuB,aAAA;;KAGjE,uBAAA;;UAGK,wBAAA;EACf,WAAA;EACA,QAAA,EAAU,KAAA,CACR,IAAA,CAAK,OAAA;IACH,OAAA,EAAS,KAAA;IACT,cAAA;IAb6D;;;;;IAmB7D,OAAA;EAAA;EAGJ,cAAA,EAAgB,aAAA;AAAA;AApBlB;AAAA,UAsCiB,iBAAA,SAA0B,WAAA,CAAY,uBAAA,EAAyB,wBAAA;EAC9E,IAAA,CAAK,OAAA,EAAS,mBAAA,EAAqB,MAAA,EAAQ,WAAA,GAAc,OAAA;EACzD,KAAA,CAAM,QAAA,EAAU,mBAAA,IAAuB,MAAA,EAAQ,WAAA,GAAc,OAAA;AAAA"}
@@ -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