@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,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/sync-selected-text-track-from-dom.ts
4
5
  /**
5
6
  * Sync selectedTextTrackId from DOM text track mode changes.
@@ -24,40 +25,30 @@ import { listen } from "@videojs/utils/dom";
24
25
  * const cleanup = syncSelectedTextTrackFromDom({ state, owners });
25
26
  */
26
27
  function syncSelectedTextTrackFromDom({ state, owners }) {
27
- let lastMediaElement;
28
- let removeListener = null;
29
- const unsubscribe = owners.subscribe((currentOwners) => {
30
- const { mediaElement } = currentOwners;
31
- if (mediaElement === lastMediaElement) return;
32
- removeListener?.();
33
- removeListener = null;
34
- lastMediaElement = mediaElement;
35
- if (!mediaElement) return;
36
- const sync = () => {
37
- const newId = Array.from(mediaElement.textTracks).find((t) => t.mode === "showing" && (t.kind === "subtitles" || t.kind === "captions"))?.id || void 0;
38
- const current = state.current;
28
+ const mediaElement = computed(() => owners.get().mediaElement);
29
+ return effect(() => {
30
+ const el = mediaElement.get();
31
+ if (!el) return;
32
+ return listen(el.textTracks, "change", () => {
33
+ const newId = Array.from(el.textTracks).find((t) => t.mode === "showing" && (t.kind === "subtitles" || t.kind === "captions"))?.id || void 0;
34
+ const current = state.get();
39
35
  if (current.selectedTextTrackId === newId) return;
40
- if (newId) state.patch({ selectedTextTrackId: newId });
36
+ if (newId) update(state, { selectedTextTrackId: newId });
41
37
  else {
42
38
  const prevId = current.selectedTextTrackId;
43
39
  if (prevId && current.textBufferState?.[prevId]) {
44
40
  const next = { ...current.textBufferState };
45
41
  delete next[prevId];
46
- state.patch({
42
+ update(state, {
47
43
  selectedTextTrackId: void 0,
48
44
  textBufferState: next
49
45
  });
50
- } else state.patch({ selectedTextTrackId: void 0 });
46
+ } else update(state, { selectedTextTrackId: void 0 });
51
47
  }
52
- };
53
- removeListener = listen(mediaElement.textTracks, "change", sync);
48
+ });
54
49
  });
55
- return () => {
56
- removeListener?.();
57
- unsubscribe();
58
- };
59
50
  }
60
-
61
51
  //#endregion
62
52
  export { syncSelectedTextTrackFromDom };
53
+
63
54
  //# sourceMappingURL=sync-selected-text-track-from-dom.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"sync-selected-text-track-from-dom.js","names":[],"sources":["../../../../src/dom/features/sync-selected-text-track-from-dom.ts"],"sourcesContent":["import { listen } from '@videojs/utils/dom';\nimport type { WritableState } from '../../core/state/create-state';\nimport type { TextTrackBufferState } from './load-text-track-cues';\n\n/**\n * State shape for DOM-driven text track selection.\n */\nexport interface SelectedTextTrackFromDomState {\n selectedTextTrackId?: string | undefined;\n textBufferState?: TextTrackBufferState | undefined;\n}\n\n/**\n * Owners shape for DOM-driven text track selection.\n */\nexport interface SelectedTextTrackFromDomOwners {\n mediaElement?: HTMLMediaElement | undefined;\n}\n\n/**\n * Sync selectedTextTrackId from DOM text track mode changes.\n *\n * Listens to the `change` event on `media.textTracks` and updates\n * `selectedTextTrackId` when external code (e.g. the captions button via\n * `toggleSubtitles()`) changes a subtitle/caption track mode to 'showing'.\n *\n * This bridges the core store's `toggleSubtitles()` with SPF's reactive text\n * track pipeline (`syncTextTrackModes`, `loadTextTrackCues`). Without this\n * bridge, direct DOM mode changes would be immediately overridden by\n * `syncTextTrackModes` on the next SPF state update.\n *\n * When a subtitle/caption track's mode is 'showing', its DOM `id` — which\n * matches the SPF track ID set by `setupTextTracks` — is written to\n * `selectedTextTrackId`. When no subtitle/caption track is 'showing',\n * `selectedTextTrackId` is cleared along with the deselected track's\n * `textBufferState` entry — setting mode to 'disabled' clears native cues from\n * the track element, so the buffer must be reset to re-fetch cues on re-enable.\n *\n * @example\n * const cleanup = syncSelectedTextTrackFromDom({ state, owners });\n */\nexport function syncSelectedTextTrackFromDom({\n state,\n owners,\n}: {\n state: WritableState<SelectedTextTrackFromDomState>;\n owners: WritableState<SelectedTextTrackFromDomOwners>;\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 const sync = () => {\n const showingTrack = Array.from(mediaElement.textTracks).find(\n (t) => t.mode === 'showing' && (t.kind === 'subtitles' || t.kind === 'captions')\n );\n\n // showingTrack.id is set from the SPF presentation track ID by setupTextTracks.\n // Fall back to undefined for empty-string IDs (non-SPF-managed tracks).\n const newId = showingTrack?.id || undefined;\n const current = state.current;\n\n // Guard against redundant patches — e.g. syncTextTrackModes confirming the\n // current selection, which would otherwise create a feedback loop.\n if (current.selectedTextTrackId === newId) return;\n\n if (newId) {\n state.patch({ selectedTextTrackId: newId });\n } else {\n // When deselecting, clear the textBufferState entry for the previous track.\n // Setting mode to 'disabled' (as toggleSubtitles() does) clears native cues\n // from the track element, so the buffer must be reset to allow re-fetching\n // on re-enable.\n const prevId = current.selectedTextTrackId;\n if (prevId && current.textBufferState?.[prevId]) {\n const next = { ...current.textBufferState };\n delete next[prevId];\n state.patch({ selectedTextTrackId: undefined, textBufferState: next });\n } else {\n state.patch({ selectedTextTrackId: undefined });\n }\n }\n };\n\n removeListener = listen(mediaElement.textTracks, 'change', sync);\n });\n\n return () => {\n removeListener?.();\n unsubscribe();\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AAyCA,SAAgB,6BAA6B,EAC3C,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;EAEnB,MAAM,aAAa;GAOjB,MAAM,QANe,MAAM,KAAK,aAAa,WAAW,CAAC,MACtD,MAAM,EAAE,SAAS,cAAc,EAAE,SAAS,eAAe,EAAE,SAAS,YACtE,EAI2B,MAAM;GAClC,MAAM,UAAU,MAAM;AAItB,OAAI,QAAQ,wBAAwB,MAAO;AAE3C,OAAI,MACF,OAAM,MAAM,EAAE,qBAAqB,OAAO,CAAC;QACtC;IAKL,MAAM,SAAS,QAAQ;AACvB,QAAI,UAAU,QAAQ,kBAAkB,SAAS;KAC/C,MAAM,OAAO,EAAE,GAAG,QAAQ,iBAAiB;AAC3C,YAAO,KAAK;AACZ,WAAM,MAAM;MAAE,qBAAqB;MAAW,iBAAiB;MAAM,CAAC;UAEtE,OAAM,MAAM,EAAE,qBAAqB,QAAW,CAAC;;;AAKrD,mBAAiB,OAAO,aAAa,YAAY,UAAU,KAAK;GAChE;AAEF,cAAa;AACX,oBAAkB;AAClB,eAAa"}
1
+ {"version":3,"file":"sync-selected-text-track-from-dom.js","names":[],"sources":["../../../../src/dom/features/sync-selected-text-track-from-dom.ts"],"sourcesContent":["import { listen } from '@videojs/utils/dom';\nimport { effect } from '../../core/signals/effect';\nimport { computed, type Signal, update } from '../../core/signals/primitives';\nimport type { TextTrackBufferState } from './load-text-track-cues';\n\n/**\n * State shape for DOM-driven text track selection.\n */\nexport interface SelectedTextTrackFromDomState {\n selectedTextTrackId?: string | undefined;\n textBufferState?: TextTrackBufferState | undefined;\n}\n\n/**\n * Owners shape for DOM-driven text track selection.\n */\nexport interface SelectedTextTrackFromDomOwners {\n mediaElement?: HTMLMediaElement | undefined;\n}\n\n/**\n * Sync selectedTextTrackId from DOM text track mode changes.\n *\n * Listens to the `change` event on `media.textTracks` and updates\n * `selectedTextTrackId` when external code (e.g. the captions button via\n * `toggleSubtitles()`) changes a subtitle/caption track mode to 'showing'.\n *\n * This bridges the core store's `toggleSubtitles()` with SPF's reactive text\n * track pipeline (`syncTextTrackModes`, `loadTextTrackCues`). Without this\n * bridge, direct DOM mode changes would be immediately overridden by\n * `syncTextTrackModes` on the next SPF state update.\n *\n * When a subtitle/caption track's mode is 'showing', its DOM `id` — which\n * matches the SPF track ID set by `setupTextTracks` — is written to\n * `selectedTextTrackId`. When no subtitle/caption track is 'showing',\n * `selectedTextTrackId` is cleared along with the deselected track's\n * `textBufferState` entry — setting mode to 'disabled' clears native cues from\n * the track element, so the buffer must be reset to re-fetch cues on re-enable.\n *\n * @example\n * const cleanup = syncSelectedTextTrackFromDom({ state, owners });\n */\nexport function syncSelectedTextTrackFromDom<\n S extends SelectedTextTrackFromDomState,\n O extends SelectedTextTrackFromDomOwners,\n>({ state, owners }: { state: Signal<S>; owners: Signal<O> }): () => void {\n const mediaElement = computed(() => owners.get().mediaElement);\n\n return effect(() => {\n const el = mediaElement.get();\n if (!el) return;\n\n return listen(el.textTracks, 'change', () => {\n const showingTrack = Array.from(el.textTracks).find(\n (t) => t.mode === 'showing' && (t.kind === 'subtitles' || t.kind === 'captions')\n );\n\n // showingTrack.id is set from the SPF presentation track ID by setupTextTracks.\n // Fall back to undefined for empty-string IDs (non-SPF-managed tracks).\n const newId = showingTrack?.id || undefined;\n const current = state.get();\n\n // Guard against redundant writes — e.g. syncTextTrackModes confirming the\n // current selection, which would otherwise create a feedback loop.\n if (current.selectedTextTrackId === newId) return;\n\n if (newId) {\n const patch: Partial<SelectedTextTrackFromDomState> = { selectedTextTrackId: newId };\n update(state, patch);\n } else {\n // When deselecting, clear the textBufferState entry for the previous track.\n // Setting mode to 'disabled' (as toggleSubtitles() does) clears native cues\n // from the track element, so the buffer must be reset to allow re-fetching\n // on re-enable.\n const prevId = current.selectedTextTrackId;\n if (prevId && current.textBufferState?.[prevId]) {\n const next = { ...current.textBufferState };\n delete next[prevId];\n const patch: Partial<SelectedTextTrackFromDomState> = {\n selectedTextTrackId: undefined,\n textBufferState: next,\n };\n update(state, patch);\n } else {\n const patch: Partial<SelectedTextTrackFromDomState> = { selectedTextTrackId: undefined };\n update(state, patch);\n }\n }\n });\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AA0CA,SAAgB,6BAGd,EAAE,OAAO,UAA+D;CACxE,MAAM,eAAe,eAAe,OAAO,KAAK,CAAC,aAAa;AAE9D,QAAO,aAAa;EAClB,MAAM,KAAK,aAAa,KAAK;AAC7B,MAAI,CAAC,GAAI;AAET,SAAO,OAAO,GAAG,YAAY,gBAAgB;GAO3C,MAAM,QANe,MAAM,KAAK,GAAG,WAAW,CAAC,MAC5C,MAAM,EAAE,SAAS,cAAc,EAAE,SAAS,eAAe,EAAE,SAAS,YACtE,EAI2B,MAAM,KAAA;GAClC,MAAM,UAAU,MAAM,KAAK;AAI3B,OAAI,QAAQ,wBAAwB,MAAO;AAE3C,OAAI,MAEF,QAAO,OAD+C,EAAE,qBAAqB,OAAO,CAChE;QACf;IAKL,MAAM,SAAS,QAAQ;AACvB,QAAI,UAAU,QAAQ,kBAAkB,SAAS;KAC/C,MAAM,OAAO,EAAE,GAAG,QAAQ,iBAAiB;AAC3C,YAAO,KAAK;AAKZ,YAAO,OAJ+C;MACpD,qBAAqB,KAAA;MACrB,iBAAiB;MAClB,CACmB;UAGpB,QAAO,OAD+C,EAAE,qBAAqB,KAAA,GAAW,CACpE;;IAGxB;GACF"}
@@ -1,16 +1,7 @@
1
- import { combineLatest } from "../../core/reactive/combine-latest.js";
2
-
1
+ import { effect } from "../../core/signals/effect.js";
2
+ import { computed } from "../../core/signals/primitives.js";
3
3
  //#region src/dom/features/sync-text-track-modes.ts
4
4
  /**
5
- * Check if we can sync text track modes.
6
- *
7
- * Requires:
8
- * - textTracks map exists (track elements created)
9
- */
10
- function canSyncTextTrackModes(owners) {
11
- return !!owners.textTracks && owners.textTracks.size > 0;
12
- }
13
- /**
14
5
  * Sync text track modes orchestration.
15
6
  *
16
7
  * Manages track element modes based on selectedTextTrackId:
@@ -25,14 +16,18 @@ function canSyncTextTrackModes(owners) {
25
16
  * const cleanup = syncTextTrackModes({ state, owners });
26
17
  */
27
18
  function syncTextTrackModes({ state, owners }) {
28
- return combineLatest([state, owners]).subscribe(([s, o]) => {
29
- if (!canSyncTextTrackModes(o)) return;
30
- const selectedId = s.selectedTextTrackId;
31
- for (const [trackId, trackElement] of o.textTracks) if (trackId === selectedId) trackElement.track.mode = "showing";
32
- else trackElement.track.mode = "hidden";
19
+ const textTracksSignal = computed(() => owners.get().textTracks);
20
+ const selectedTextTrackIdSignal = computed(() => state.get().selectedTextTrackId);
21
+ const canSyncTextTrackModes = computed(() => !!textTracksSignal.get()?.size);
22
+ return effect(() => {
23
+ if (!canSyncTextTrackModes.get()) return;
24
+ /** @TODO refactor TextTracks owners model. Should simply use id. Also should use corresponding TextTrack (JS) element if possible (CJP) */
25
+ const textTracks = textTracksSignal.get();
26
+ const selectedTextTrackId = selectedTextTrackIdSignal.get();
27
+ for (const [trackId, trackElement] of textTracks) trackElement.track.mode = trackId === selectedTextTrackId ? "showing" : "hidden";
33
28
  });
34
29
  }
35
-
36
30
  //#endregion
37
31
  export { syncTextTrackModes };
32
+
38
33
  //# sourceMappingURL=sync-text-track-modes.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"sync-text-track-modes.js","names":[],"sources":["../../../../src/dom/features/sync-text-track-modes.ts"],"sourcesContent":["import { combineLatest } from '../../core/reactive/combine-latest';\nimport type { WritableState } from '../../core/state/create-state';\n\n/**\n * State shape for text track mode synchronization.\n */\nexport interface TextTrackModeState {\n selectedTextTrackId?: string | undefined;\n}\n\n/**\n * Owners shape for text track mode synchronization.\n */\nexport interface TextTrackModeOwners {\n textTracks?: Map<string, HTMLTrackElement>;\n}\n\n/**\n * Check if we can sync text track modes.\n *\n * Requires:\n * - textTracks map exists (track elements created)\n */\nexport function canSyncTextTrackModes(owners: TextTrackModeOwners): boolean {\n return !!owners.textTracks && owners.textTracks.size > 0;\n}\n\n/**\n * Sync text track modes orchestration.\n *\n * Manages track element modes based on selectedTextTrackId:\n * - Selected track: mode = \"showing\"\n * - Other tracks: mode = \"hidden\"\n * - No selection: all tracks mode = \"hidden\"\n *\n * Note: Uses \"hidden\" instead of \"disabled\" for non-selected tracks\n * so they remain available in the browser's track menu.\n *\n * @example\n * const cleanup = syncTextTrackModes({ state, owners });\n */\nexport function syncTextTrackModes({\n state,\n owners,\n}: {\n state: WritableState<TextTrackModeState>;\n owners: WritableState<TextTrackModeOwners>;\n}): () => void {\n return combineLatest([state, owners]).subscribe(([s, o]: [TextTrackModeState, TextTrackModeOwners]) => {\n // Check orchestration conditions\n if (!canSyncTextTrackModes(o)) return;\n\n const selectedId = s.selectedTextTrackId;\n\n // Update all track element modes\n for (const [trackId, trackElement] of o.textTracks!) {\n if (trackId === selectedId) {\n // Selected track: show it\n trackElement.track.mode = 'showing';\n } else {\n // Other tracks: hide them (but keep available in menu)\n trackElement.track.mode = 'hidden';\n }\n }\n });\n}\n"],"mappings":";;;;;;;;;AAuBA,SAAgB,sBAAsB,QAAsC;AAC1E,QAAO,CAAC,CAAC,OAAO,cAAc,OAAO,WAAW,OAAO;;;;;;;;;;;;;;;;AAiBzD,SAAgB,mBAAmB,EACjC,OACA,UAIa;AACb,QAAO,cAAc,CAAC,OAAO,OAAO,CAAC,CAAC,WAAW,CAAC,GAAG,OAAkD;AAErG,MAAI,CAAC,sBAAsB,EAAE,CAAE;EAE/B,MAAM,aAAa,EAAE;AAGrB,OAAK,MAAM,CAAC,SAAS,iBAAiB,EAAE,WACtC,KAAI,YAAY,WAEd,cAAa,MAAM,OAAO;MAG1B,cAAa,MAAM,OAAO;GAG9B"}
1
+ {"version":3,"file":"sync-text-track-modes.js","names":[],"sources":["../../../../src/dom/features/sync-text-track-modes.ts"],"sourcesContent":["import { effect } from '../../core/signals/effect';\nimport { computed, type Signal } from '../../core/signals/primitives';\n\n/**\n * State shape for text track mode synchronization.\n */\nexport interface TextTrackModeState {\n selectedTextTrackId?: string | undefined;\n}\n\n/**\n * Owners shape for text track mode synchronization.\n */\nexport interface TextTrackModeOwners {\n textTracks?: Map<string, HTMLTrackElement>;\n}\n\n/**\n * Sync text track modes orchestration.\n *\n * Manages track element modes based on selectedTextTrackId:\n * - Selected track: mode = \"showing\"\n * - Other tracks: mode = \"hidden\"\n * - No selection: all tracks mode = \"hidden\"\n *\n * Note: Uses \"hidden\" instead of \"disabled\" for non-selected tracks\n * so they remain available in the browser's track menu.\n *\n * @example\n * const cleanup = syncTextTrackModes({ state, owners });\n */\nexport function syncTextTrackModes<S extends TextTrackModeState, O extends TextTrackModeOwners>({\n state,\n owners,\n}: {\n state: Signal<S>;\n owners: Signal<O>;\n}): () => void {\n const textTracksSignal = computed(() => owners.get().textTracks);\n const selectedTextTrackIdSignal = computed(() => state.get().selectedTextTrackId);\n\n const canSyncTextTrackModes = computed(() => !!textTracksSignal.get()?.size);\n\n return effect(() => {\n if (!canSyncTextTrackModes.get()) return;\n /** @TODO refactor TextTracks owners model. Should simply use id. Also should use corresponding TextTrack (JS) element if possible (CJP) */\n const textTracks = textTracksSignal.get() as Map<string, HTMLTrackElement>;\n const selectedTextTrackId = selectedTextTrackIdSignal.get() as string;\n\n for (const [trackId, trackElement] of textTracks) {\n trackElement.track.mode = trackId === selectedTextTrackId ? 'showing' : 'hidden';\n }\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AA+BA,SAAgB,mBAAgF,EAC9F,OACA,UAIa;CACb,MAAM,mBAAmB,eAAe,OAAO,KAAK,CAAC,WAAW;CAChE,MAAM,4BAA4B,eAAe,MAAM,KAAK,CAAC,oBAAoB;CAEjF,MAAM,wBAAwB,eAAe,CAAC,CAAC,iBAAiB,KAAK,EAAE,KAAK;AAE5E,QAAO,aAAa;AAClB,MAAI,CAAC,sBAAsB,KAAK,CAAE;;EAElC,MAAM,aAAa,iBAAiB,KAAK;EACzC,MAAM,sBAAsB,0BAA0B,KAAK;AAE3D,OAAK,MAAM,CAAC,SAAS,iBAAiB,WACpC,cAAa,MAAM,OAAO,YAAY,sBAAsB,YAAY;GAE1E"}
@@ -1,5 +1,6 @@
1
+ import { effect } from "../../core/signals/effect.js";
2
+ import { update } from "../../core/signals/primitives.js";
1
3
  import { listen } from "@videojs/utils/dom";
2
-
3
4
  //#region src/dom/features/track-current-time.ts
4
5
  /**
5
6
  * Track current playback position from the media element.
@@ -19,15 +20,17 @@ import { listen } from "@videojs/utils/dom";
19
20
  function trackCurrentTime({ state, owners }) {
20
21
  let lastMediaElement;
21
22
  let removeListeners = null;
22
- const unsubscribe = owners.subscribe((currentOwners) => {
23
- const { mediaElement } = currentOwners;
23
+ const cleanupEffect = effect(() => {
24
+ const { mediaElement } = owners.get();
24
25
  if (mediaElement === lastMediaElement) return;
25
26
  removeListeners?.();
26
27
  removeListeners = null;
27
28
  lastMediaElement = mediaElement;
28
29
  if (!mediaElement) return;
29
- state.patch({ currentTime: mediaElement.currentTime });
30
- const sync = () => state.patch({ currentTime: mediaElement.currentTime });
30
+ const sync = () => {
31
+ update(state, { currentTime: mediaElement.currentTime });
32
+ };
33
+ sync();
31
34
  const removeTimeupdate = listen(mediaElement, "timeupdate", sync);
32
35
  const removeSeeking = listen(mediaElement, "seeking", sync);
33
36
  removeListeners = () => {
@@ -37,10 +40,10 @@ function trackCurrentTime({ state, owners }) {
37
40
  });
38
41
  return () => {
39
42
  removeListeners?.();
40
- unsubscribe();
43
+ cleanupEffect();
41
44
  };
42
45
  }
43
-
44
46
  //#endregion
45
47
  export { trackCurrentTime };
48
+
46
49
  //# sourceMappingURL=track-current-time.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"track-current-time.js","names":[],"sources":["../../../../src/dom/features/track-current-time.ts"],"sourcesContent":["import { listen } from '@videojs/utils/dom';\nimport type { WritableState } from '../../core/state/create-state';\n\n/**\n * State shape for current time tracking.\n */\nexport interface CurrentTimeState {\n currentTime?: number;\n}\n\n/**\n * Owners shape for current time tracking.\n */\nexport interface CurrentTimeOwners {\n mediaElement?: HTMLMediaElement | undefined;\n}\n\n/**\n * Check if we can track current time.\n *\n * Requires:\n * - mediaElement exists in owners\n */\nexport function canTrackCurrentTime(owners: CurrentTimeOwners): boolean {\n return !!owners.mediaElement;\n}\n\n/**\n * Track current playback position from the media element.\n *\n * Mirrors `mediaElement.currentTime` into reactive state on:\n * - `timeupdate` — fires during playback (~4 Hz)\n * - `seeking` — fires when a seek begins; per spec, `currentTime` is\n * already at the new position when this event dispatches, so buffer\n * management can react immediately rather than waiting for `timeupdate`,\n * which does not fire while paused.\n *\n * Also syncs immediately when a media element becomes available.\n *\n * @example\n * const cleanup = trackCurrentTime({ state, owners });\n */\nexport function trackCurrentTime({\n state,\n owners,\n}: {\n state: WritableState<CurrentTimeState>;\n owners: WritableState<CurrentTimeOwners>;\n}): () => void {\n let lastMediaElement: HTMLMediaElement | undefined;\n let removeListeners: (() => void) | null = null;\n\n const unsubscribe = owners.subscribe((currentOwners) => {\n const { mediaElement } = currentOwners;\n\n if (mediaElement === lastMediaElement) return;\n\n removeListeners?.();\n removeListeners = null;\n lastMediaElement = mediaElement;\n\n if (!mediaElement) return;\n\n // Sync immediately so consumers don't wait for the first event\n state.patch({ currentTime: mediaElement.currentTime });\n\n const sync = () => state.patch({ currentTime: mediaElement.currentTime });\n const removeTimeupdate = listen(mediaElement, 'timeupdate', sync);\n const removeSeeking = listen(mediaElement, 'seeking', sync);\n removeListeners = () => {\n removeTimeupdate();\n removeSeeking();\n };\n });\n\n return () => {\n removeListeners?.();\n unsubscribe();\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AA0CA,SAAgB,iBAAiB,EAC/B,OACA,UAIa;CACb,IAAI;CACJ,IAAI,kBAAuC;CAE3C,MAAM,cAAc,OAAO,WAAW,kBAAkB;EACtD,MAAM,EAAE,iBAAiB;AAEzB,MAAI,iBAAiB,iBAAkB;AAEvC,qBAAmB;AACnB,oBAAkB;AAClB,qBAAmB;AAEnB,MAAI,CAAC,aAAc;AAGnB,QAAM,MAAM,EAAE,aAAa,aAAa,aAAa,CAAC;EAEtD,MAAM,aAAa,MAAM,MAAM,EAAE,aAAa,aAAa,aAAa,CAAC;EACzE,MAAM,mBAAmB,OAAO,cAAc,cAAc,KAAK;EACjE,MAAM,gBAAgB,OAAO,cAAc,WAAW,KAAK;AAC3D,0BAAwB;AACtB,qBAAkB;AAClB,kBAAe;;GAEjB;AAEF,cAAa;AACX,qBAAmB;AACnB,eAAa"}
1
+ {"version":3,"file":"track-current-time.js","names":[],"sources":["../../../../src/dom/features/track-current-time.ts"],"sourcesContent":["import { listen } from '@videojs/utils/dom';\nimport { effect } from '../../core/signals/effect';\nimport { type Signal, update } from '../../core/signals/primitives';\n\n/**\n * State shape for current time tracking.\n */\nexport interface CurrentTimeState {\n currentTime?: number;\n}\n\n/**\n * Owners shape for current time tracking.\n */\nexport interface CurrentTimeOwners {\n mediaElement?: HTMLMediaElement | undefined;\n}\n\n/**\n * Check if we can track current time.\n *\n * Requires:\n * - mediaElement exists in owners\n */\nexport function canTrackCurrentTime(owners: CurrentTimeOwners): boolean {\n return !!owners.mediaElement;\n}\n\n/**\n * Track current playback position from the media element.\n *\n * Mirrors `mediaElement.currentTime` into reactive state on:\n * - `timeupdate` — fires during playback (~4 Hz)\n * - `seeking` — fires when a seek begins; per spec, `currentTime` is\n * already at the new position when this event dispatches, so buffer\n * management can react immediately rather than waiting for `timeupdate`,\n * which does not fire while paused.\n *\n * Also syncs immediately when a media element becomes available.\n *\n * @example\n * const cleanup = trackCurrentTime({ state, owners });\n */\nexport function trackCurrentTime<S extends CurrentTimeState, O extends CurrentTimeOwners>({\n state,\n owners,\n}: {\n state: Signal<S>;\n owners: Signal<O>;\n}): () => void {\n let lastMediaElement: HTMLMediaElement | undefined;\n let removeListeners: (() => void) | null = null;\n\n const cleanupEffect = effect(() => {\n const { mediaElement } = owners.get();\n\n if (mediaElement === lastMediaElement) return;\n\n removeListeners?.();\n removeListeners = null;\n lastMediaElement = mediaElement;\n\n if (!mediaElement) return;\n\n const sync = () => {\n const patch: Partial<CurrentTimeState> = { currentTime: mediaElement.currentTime };\n update(state, patch);\n };\n\n // Sync immediately so consumers don't wait for the first event\n sync();\n const removeTimeupdate = listen(mediaElement, 'timeupdate', sync);\n const removeSeeking = listen(mediaElement, 'seeking', sync);\n removeListeners = () => {\n removeTimeupdate();\n removeSeeking();\n };\n });\n\n return () => {\n removeListeners?.();\n cleanupEffect();\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AA2CA,SAAgB,iBAA0E,EACxF,OACA,UAIa;CACb,IAAI;CACJ,IAAI,kBAAuC;CAE3C,MAAM,gBAAgB,aAAa;EACjC,MAAM,EAAE,iBAAiB,OAAO,KAAK;AAErC,MAAI,iBAAiB,iBAAkB;AAEvC,qBAAmB;AACnB,oBAAkB;AAClB,qBAAmB;AAEnB,MAAI,CAAC,aAAc;EAEnB,MAAM,aAAa;AAEjB,UAAO,OADkC,EAAE,aAAa,aAAa,aAAa,CAC9D;;AAItB,QAAM;EACN,MAAM,mBAAmB,OAAO,cAAc,cAAc,KAAK;EACjE,MAAM,gBAAgB,OAAO,cAAc,WAAW,KAAK;AAC3D,0BAAwB;AACtB,qBAAkB;AAClB,kBAAe;;GAEjB;AAEF,cAAa;AACX,qBAAmB;AACnB,iBAAe"}
@@ -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,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