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

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 (242) hide show
  1. package/README.md +16 -40
  2. package/dist/default/all.js +16 -0
  3. package/dist/default/core/abr/bandwidth-estimator.js +96 -0
  4. package/dist/default/core/abr/bandwidth-estimator.js.map +1 -0
  5. package/dist/default/core/abr/ewma.js +71 -0
  6. package/dist/default/core/abr/ewma.js.map +1 -0
  7. package/dist/default/core/abr/quality-selection.js +53 -0
  8. package/dist/default/core/abr/quality-selection.js.map +1 -0
  9. package/dist/default/core/buffer/back-buffer.js +46 -0
  10. package/dist/default/core/buffer/back-buffer.js.map +1 -0
  11. package/dist/default/core/buffer/forward-buffer.js +73 -0
  12. package/dist/default/core/buffer/forward-buffer.js.map +1 -0
  13. package/dist/default/core/events/create-event-stream.js +48 -0
  14. package/dist/default/core/events/create-event-stream.js.map +1 -0
  15. package/dist/default/core/features/calculate-presentation-duration.js +51 -0
  16. package/dist/default/core/features/calculate-presentation-duration.js.map +1 -0
  17. package/dist/default/core/features/quality-switching.js +68 -0
  18. package/dist/default/core/features/quality-switching.js.map +1 -0
  19. package/dist/default/core/features/resolve-presentation.js +99 -0
  20. package/dist/default/core/features/resolve-presentation.js.map +1 -0
  21. package/dist/default/core/features/resolve-track.js +77 -0
  22. package/dist/default/core/features/resolve-track.js.map +1 -0
  23. package/dist/default/core/features/select-tracks.js +147 -0
  24. package/dist/default/core/features/select-tracks.js.map +1 -0
  25. package/dist/default/core/hls/parse-attributes.js +134 -0
  26. package/dist/default/core/hls/parse-attributes.js.map +1 -0
  27. package/dist/default/core/hls/parse-media-playlist.js +83 -0
  28. package/dist/default/core/hls/parse-media-playlist.js.map +1 -0
  29. package/dist/default/core/hls/parse-multivariant.js +215 -0
  30. package/dist/default/core/hls/parse-multivariant.js.map +1 -0
  31. package/dist/default/core/hls/resolve-url.js +11 -0
  32. package/dist/default/core/hls/resolve-url.js.map +1 -0
  33. package/dist/default/core/reactive/combine-latest.js +72 -0
  34. package/dist/default/core/reactive/combine-latest.js.map +1 -0
  35. package/dist/default/core/state/create-state.js +143 -0
  36. package/dist/default/core/state/create-state.js.map +1 -0
  37. package/dist/default/core/task.js +122 -0
  38. package/dist/default/core/task.js.map +1 -0
  39. package/dist/default/core/types/index.js +15 -0
  40. package/dist/default/core/types/index.js.map +1 -0
  41. package/dist/default/core/utils/generate-id.js +21 -0
  42. package/dist/default/core/utils/generate-id.js.map +1 -0
  43. package/dist/default/core/utils/track-selection.js +31 -0
  44. package/dist/default/core/utils/track-selection.js.map +1 -0
  45. package/dist/default/dom/features/end-of-stream.js +169 -0
  46. package/dist/default/dom/features/end-of-stream.js.map +1 -0
  47. package/dist/default/dom/features/load-segments.js +188 -0
  48. package/dist/default/dom/features/load-segments.js.map +1 -0
  49. package/dist/default/dom/features/load-text-track-cues.js +151 -0
  50. package/dist/default/dom/features/load-text-track-cues.js.map +1 -0
  51. package/dist/default/dom/features/segment-loader-actor.js +197 -0
  52. package/dist/default/dom/features/segment-loader-actor.js.map +1 -0
  53. package/dist/default/dom/features/setup-mediasource.js +55 -0
  54. package/dist/default/dom/features/setup-mediasource.js.map +1 -0
  55. package/dist/default/dom/features/setup-sourcebuffer.js +70 -0
  56. package/dist/default/dom/features/setup-sourcebuffer.js.map +1 -0
  57. package/dist/default/dom/features/setup-text-tracks.js +88 -0
  58. package/dist/default/dom/features/setup-text-tracks.js.map +1 -0
  59. package/dist/default/dom/features/sync-selected-text-track-from-dom.js +63 -0
  60. package/dist/default/dom/features/sync-selected-text-track-from-dom.js.map +1 -0
  61. package/dist/default/dom/features/sync-text-track-modes.js +38 -0
  62. package/dist/default/dom/features/sync-text-track-modes.js.map +1 -0
  63. package/dist/default/dom/features/track-current-time.js +46 -0
  64. package/dist/default/dom/features/track-current-time.js.map +1 -0
  65. package/dist/default/dom/features/track-playback-initiated.js +55 -0
  66. package/dist/default/dom/features/track-playback-initiated.js.map +1 -0
  67. package/dist/{dom/index.js → default/dom/features/track-playback-rate.js} +2 -4
  68. package/dist/default/dom/features/track-playback-rate.js.map +1 -0
  69. package/dist/default/dom/features/update-duration.js +72 -0
  70. package/dist/default/dom/features/update-duration.js.map +1 -0
  71. package/dist/default/dom/media/append-segment.js +60 -0
  72. package/dist/default/dom/media/append-segment.js.map +1 -0
  73. package/dist/default/dom/media/buffer-flusher.js +55 -0
  74. package/dist/default/dom/media/buffer-flusher.js.map +1 -0
  75. package/dist/default/dom/media/mediasource-setup.js +154 -0
  76. package/dist/default/dom/media/mediasource-setup.js.map +1 -0
  77. package/dist/default/dom/media/source-buffer-actor.js +186 -0
  78. package/dist/default/dom/media/source-buffer-actor.js.map +1 -0
  79. package/dist/default/dom/network/chunked-stream-iterable.js +49 -0
  80. package/dist/default/dom/network/chunked-stream-iterable.js.map +1 -0
  81. package/dist/default/dom/network/fetch.js +56 -0
  82. package/dist/default/dom/network/fetch.js.map +1 -0
  83. package/dist/default/dom/playback-engine/adapter.js +95 -0
  84. package/dist/default/dom/playback-engine/adapter.js.map +1 -0
  85. package/dist/default/dom/playback-engine/engine.js +176 -0
  86. package/dist/default/dom/playback-engine/engine.js.map +1 -0
  87. package/dist/default/dom/playback-engine.js +4 -0
  88. package/dist/default/dom/text/parse-vtt-segment.js +56 -0
  89. package/dist/default/dom/text/parse-vtt-segment.js.map +1 -0
  90. package/dist/default/dom.js +11 -0
  91. package/dist/{src-DRsS_H80.js → default/index.js} +2 -2
  92. package/dist/default/index.js.map +1 -0
  93. package/dist/dev/all.js +16 -0
  94. package/dist/dev/core/abr/bandwidth-estimator.d.ts +35 -0
  95. package/dist/dev/core/abr/bandwidth-estimator.d.ts.map +1 -0
  96. package/dist/dev/core/abr/bandwidth-estimator.js +96 -0
  97. package/dist/dev/core/abr/bandwidth-estimator.js.map +1 -0
  98. package/dist/dev/core/abr/ewma.js +71 -0
  99. package/dist/dev/core/abr/ewma.js.map +1 -0
  100. package/dist/dev/core/abr/quality-selection.js +53 -0
  101. package/dist/dev/core/abr/quality-selection.js.map +1 -0
  102. package/dist/dev/core/actor.d.ts +31 -0
  103. package/dist/dev/core/actor.d.ts.map +1 -0
  104. package/dist/dev/core/buffer/back-buffer.js +46 -0
  105. package/dist/dev/core/buffer/back-buffer.js.map +1 -0
  106. package/dist/dev/core/buffer/forward-buffer.js +73 -0
  107. package/dist/dev/core/buffer/forward-buffer.js.map +1 -0
  108. package/dist/dev/core/events/create-event-stream.d.ts +48 -0
  109. package/dist/dev/core/events/create-event-stream.d.ts.map +1 -0
  110. package/dist/dev/core/events/create-event-stream.js +48 -0
  111. package/dist/dev/core/events/create-event-stream.js.map +1 -0
  112. package/dist/dev/core/features/calculate-presentation-duration.js +51 -0
  113. package/dist/dev/core/features/calculate-presentation-duration.js.map +1 -0
  114. package/dist/dev/core/features/quality-switching.js +68 -0
  115. package/dist/dev/core/features/quality-switching.js.map +1 -0
  116. package/dist/dev/core/features/resolve-presentation.d.ts +16 -0
  117. package/dist/dev/core/features/resolve-presentation.d.ts.map +1 -0
  118. package/dist/dev/core/features/resolve-presentation.js +99 -0
  119. package/dist/dev/core/features/resolve-presentation.js.map +1 -0
  120. package/dist/dev/core/features/resolve-track.d.ts +13 -0
  121. package/dist/dev/core/features/resolve-track.d.ts.map +1 -0
  122. package/dist/dev/core/features/resolve-track.js +77 -0
  123. package/dist/dev/core/features/resolve-track.js.map +1 -0
  124. package/dist/dev/core/features/select-tracks.d.ts +11 -0
  125. package/dist/dev/core/features/select-tracks.d.ts.map +1 -0
  126. package/dist/dev/core/features/select-tracks.js +147 -0
  127. package/dist/dev/core/features/select-tracks.js.map +1 -0
  128. package/dist/dev/core/hls/parse-attributes.js +134 -0
  129. package/dist/dev/core/hls/parse-attributes.js.map +1 -0
  130. package/dist/dev/core/hls/parse-media-playlist.js +83 -0
  131. package/dist/dev/core/hls/parse-media-playlist.js.map +1 -0
  132. package/dist/dev/core/hls/parse-multivariant.js +215 -0
  133. package/dist/dev/core/hls/parse-multivariant.js.map +1 -0
  134. package/dist/dev/core/hls/resolve-url.js +11 -0
  135. package/dist/dev/core/hls/resolve-url.js.map +1 -0
  136. package/dist/dev/core/reactive/combine-latest.js +72 -0
  137. package/dist/dev/core/reactive/combine-latest.js.map +1 -0
  138. package/dist/dev/core/state/create-state.d.ts +104 -0
  139. package/dist/dev/core/state/create-state.d.ts.map +1 -0
  140. package/dist/dev/core/state/create-state.js +143 -0
  141. package/dist/dev/core/state/create-state.js.map +1 -0
  142. package/dist/dev/core/task.js +122 -0
  143. package/dist/dev/core/task.js.map +1 -0
  144. package/dist/dev/core/types/index.d.ts +173 -0
  145. package/dist/dev/core/types/index.d.ts.map +1 -0
  146. package/dist/dev/core/types/index.js +15 -0
  147. package/dist/dev/core/types/index.js.map +1 -0
  148. package/dist/dev/core/utils/generate-id.js +21 -0
  149. package/dist/dev/core/utils/generate-id.js.map +1 -0
  150. package/dist/dev/core/utils/track-selection.d.ts +16 -0
  151. package/dist/dev/core/utils/track-selection.d.ts.map +1 -0
  152. package/dist/dev/core/utils/track-selection.js +31 -0
  153. package/dist/dev/core/utils/track-selection.js.map +1 -0
  154. package/dist/dev/dom/features/end-of-stream.js +169 -0
  155. package/dist/dev/dom/features/end-of-stream.js.map +1 -0
  156. package/dist/dev/dom/features/load-segments.d.ts +72 -0
  157. package/dist/dev/dom/features/load-segments.d.ts.map +1 -0
  158. package/dist/dev/dom/features/load-segments.js +188 -0
  159. package/dist/dev/dom/features/load-segments.js.map +1 -0
  160. package/dist/dev/dom/features/load-text-track-cues.d.ts +61 -0
  161. package/dist/dev/dom/features/load-text-track-cues.d.ts.map +1 -0
  162. package/dist/dev/dom/features/load-text-track-cues.js +151 -0
  163. package/dist/dev/dom/features/load-text-track-cues.js.map +1 -0
  164. package/dist/dev/dom/features/segment-loader-actor.js +197 -0
  165. package/dist/dev/dom/features/segment-loader-actor.js.map +1 -0
  166. package/dist/dev/dom/features/setup-mediasource.js +55 -0
  167. package/dist/dev/dom/features/setup-mediasource.js.map +1 -0
  168. package/dist/dev/dom/features/setup-sourcebuffer.d.ts +9 -0
  169. package/dist/dev/dom/features/setup-sourcebuffer.d.ts.map +1 -0
  170. package/dist/dev/dom/features/setup-sourcebuffer.js +70 -0
  171. package/dist/dev/dom/features/setup-sourcebuffer.js.map +1 -0
  172. package/dist/dev/dom/features/setup-text-tracks.js +88 -0
  173. package/dist/dev/dom/features/setup-text-tracks.js.map +1 -0
  174. package/dist/dev/dom/features/sync-selected-text-track-from-dom.js +63 -0
  175. package/dist/dev/dom/features/sync-selected-text-track-from-dom.js.map +1 -0
  176. package/dist/dev/dom/features/sync-text-track-modes.js +38 -0
  177. package/dist/dev/dom/features/sync-text-track-modes.js.map +1 -0
  178. package/dist/dev/dom/features/track-current-time.d.ts +40 -0
  179. package/dist/dev/dom/features/track-current-time.d.ts.map +1 -0
  180. package/dist/dev/dom/features/track-current-time.js +46 -0
  181. package/dist/dev/dom/features/track-current-time.js.map +1 -0
  182. package/dist/dev/dom/features/track-playback-initiated.d.ts +53 -0
  183. package/dist/dev/dom/features/track-playback-initiated.d.ts.map +1 -0
  184. package/dist/dev/dom/features/track-playback-initiated.js +55 -0
  185. package/dist/dev/dom/features/track-playback-initiated.js.map +1 -0
  186. package/dist/dev/dom/features/track-playback-rate.d.ts +34 -0
  187. package/dist/dev/dom/features/track-playback-rate.d.ts.map +1 -0
  188. package/dist/dev/dom/features/track-playback-rate.js +36 -0
  189. package/dist/dev/dom/features/track-playback-rate.js.map +1 -0
  190. package/dist/dev/dom/features/update-duration.js +72 -0
  191. package/dist/dev/dom/features/update-duration.js.map +1 -0
  192. package/dist/dev/dom/media/append-segment.d.ts +24 -0
  193. package/dist/dev/dom/media/append-segment.d.ts.map +1 -0
  194. package/dist/dev/dom/media/append-segment.js +60 -0
  195. package/dist/dev/dom/media/append-segment.js.map +1 -0
  196. package/dist/dev/dom/media/buffer-flusher.d.ts +24 -0
  197. package/dist/dev/dom/media/buffer-flusher.d.ts.map +1 -0
  198. package/dist/dev/dom/media/buffer-flusher.js +55 -0
  199. package/dist/dev/dom/media/buffer-flusher.js.map +1 -0
  200. package/dist/dev/dom/media/mediasource-setup.js +154 -0
  201. package/dist/dev/dom/media/mediasource-setup.js.map +1 -0
  202. package/dist/dev/dom/media/source-buffer-actor.d.ts +56 -0
  203. package/dist/dev/dom/media/source-buffer-actor.d.ts.map +1 -0
  204. package/dist/dev/dom/media/source-buffer-actor.js +186 -0
  205. package/dist/dev/dom/media/source-buffer-actor.js.map +1 -0
  206. package/dist/dev/dom/network/chunked-stream-iterable.js +49 -0
  207. package/dist/dev/dom/network/chunked-stream-iterable.js.map +1 -0
  208. package/dist/dev/dom/network/fetch.js +56 -0
  209. package/dist/dev/dom/network/fetch.js.map +1 -0
  210. package/dist/dev/dom/playback-engine/adapter.d.ts +41 -0
  211. package/dist/dev/dom/playback-engine/adapter.d.ts.map +1 -0
  212. package/dist/dev/dom/playback-engine/adapter.js +95 -0
  213. package/dist/dev/dom/playback-engine/adapter.js.map +1 -0
  214. package/dist/dev/dom/playback-engine/engine.d.ts +135 -0
  215. package/dist/dev/dom/playback-engine/engine.d.ts.map +1 -0
  216. package/dist/dev/dom/playback-engine/engine.js +176 -0
  217. package/dist/dev/dom/playback-engine/engine.js.map +1 -0
  218. package/dist/dev/dom/playback-engine.d.ts +3 -0
  219. package/dist/dev/dom/playback-engine.js +4 -0
  220. package/dist/dev/dom/text/parse-vtt-segment.d.ts +12 -0
  221. package/dist/dev/dom/text/parse-vtt-segment.d.ts.map +1 -0
  222. package/dist/dev/dom/text/parse-vtt-segment.js +56 -0
  223. package/dist/dev/dom/text/parse-vtt-segment.js.map +1 -0
  224. package/dist/dev/dom.d.ts +10 -0
  225. package/dist/dev/dom.js +11 -0
  226. package/dist/dev/index.d.ts.map +1 -0
  227. package/dist/dev/index.js +14 -0
  228. package/dist/dev/index.js.map +1 -0
  229. package/package.json +22 -22
  230. package/dist/adapter-CMw-rvbk.js +0 -3512
  231. package/dist/adapter-CMw-rvbk.js.map +0 -1
  232. package/dist/adapter-l8iJN_GI.d.ts +0 -689
  233. package/dist/adapter-l8iJN_GI.d.ts.map +0 -1
  234. package/dist/dom/index.d.ts +0 -243
  235. package/dist/dom/index.d.ts.map +0 -1
  236. package/dist/dom/index.js.map +0 -1
  237. package/dist/dom/playback-engine/index.d.ts +0 -2
  238. package/dist/dom/playback-engine/index.js +0 -4
  239. package/dist/index.d.ts.map +0 -1
  240. package/dist/index.js +0 -3
  241. package/dist/src-DRsS_H80.js.map +0 -1
  242. /package/dist/{index.d.ts → dev/index.d.ts} +0 -0
@@ -0,0 +1,197 @@
1
+ import { calculateBackBufferFlushPoint } from "../../core/buffer/back-buffer.js";
2
+ import { calculateForwardFlushPoint, getSegmentsToLoad } from "../../core/buffer/forward-buffer.js";
3
+
4
+ //#region src/dom/features/segment-loader-actor.ts
5
+ /**
6
+ * Creates a SegmentLoaderActor for one track type (video or audio).
7
+ *
8
+ * Receives load assignments via `send()` and owns all execution: planning,
9
+ * removes, fetches, and appends. Coordinates with the SourceBufferActor for
10
+ * all physical SourceBuffer operations.
11
+ *
12
+ * Planning (Cases 1–3) happens in `send()` on every incoming message, producing
13
+ * an ordered LoadTask list. The runner drains that list sequentially. When a new
14
+ * message arrives mid-run, send() replans and either continues the in-flight
15
+ * operation (if still needed) or preempts it.
16
+ *
17
+ * @param sourceBufferActor - Shared SourceBufferActor reference (not owned)
18
+ * @param fetchBytes - Tracked fetch closure (owns throughput sampling for segments).
19
+ * Accepts an optional `minChunkSize` in options; init segments pass `Infinity`
20
+ * so the entire body accumulates as one chunk before appending.
21
+ */
22
+ function createSegmentLoaderActor(sourceBufferActor, fetchBytes) {
23
+ let pendingTasks = null;
24
+ let inFlightInitTrackId = null;
25
+ let inFlightSegmentId = null;
26
+ let abortController = null;
27
+ let running = false;
28
+ let destroyed = false;
29
+ const getBufferedSegments = (allSegments) => {
30
+ const bufferedIds = new Set(sourceBufferActor.snapshot.context.segments.filter((s) => !s.partial).map((s) => s.id));
31
+ return allSegments.filter((s) => bufferedIds.has(s.id));
32
+ };
33
+ /**
34
+ * Translate a load message into an ordered LoadTask list based on committed
35
+ * actor state. In-flight awareness is handled separately in send().
36
+ *
37
+ * @todo Rename alongside LoadTask (e.g. planOps).
38
+ *
39
+ * Case 1 — Removes: forward and back buffer flush points, segment-aligned.
40
+ * No flush on track switch: appending new content overwrites existing buffer
41
+ * ranges, and the actor's time-aligned deduplication keeps the segment model
42
+ * accurate as new segments arrive.
43
+ *
44
+ * Case 2 — Init: schedule if not yet committed for this track.
45
+ *
46
+ * Case 3 — Segments: all segments in the load window not yet committed.
47
+ */
48
+ const planTasks = (message) => {
49
+ const { track, range } = message;
50
+ const actorCtx = sourceBufferActor.snapshot.context;
51
+ const bufferedSegments = getBufferedSegments(track.segments);
52
+ const currentTime = range?.start ?? 0;
53
+ const tasks = [];
54
+ if (range) {
55
+ const forwardFlushStart = calculateForwardFlushPoint(bufferedSegments, currentTime);
56
+ if (forwardFlushStart < Infinity) tasks.push({
57
+ type: "remove",
58
+ start: forwardFlushStart,
59
+ end: Infinity
60
+ });
61
+ const backFlushEnd = calculateBackBufferFlushPoint(bufferedSegments, currentTime);
62
+ if (backFlushEnd > 0) tasks.push({
63
+ type: "remove",
64
+ start: 0,
65
+ end: backFlushEnd
66
+ });
67
+ }
68
+ if (actorCtx.initTrackId !== track.id) tasks.push({
69
+ type: "append-init",
70
+ meta: { trackId: track.id },
71
+ url: track.initialization.url,
72
+ ...track.initialization.byteRange !== void 0 && { byteRange: track.initialization.byteRange }
73
+ });
74
+ if (range) {
75
+ const EPSILON = 1e-4;
76
+ const segmentsToLoad = getSegmentsToLoad(track.segments, bufferedSegments, currentTime).filter((seg) => {
77
+ const existing = actorCtx.segments.find((s) => Math.abs(s.startTime - seg.startTime) < EPSILON);
78
+ if (existing?.partial) return true;
79
+ if (!existing?.trackBandwidth || !track.bandwidth) return true;
80
+ return track.bandwidth > existing.trackBandwidth;
81
+ });
82
+ for (const segment of segmentsToLoad) tasks.push({
83
+ type: "append-segment",
84
+ meta: {
85
+ id: segment.id,
86
+ startTime: segment.startTime,
87
+ duration: segment.duration,
88
+ trackId: track.id,
89
+ trackBandwidth: track.bandwidth
90
+ },
91
+ url: segment.url,
92
+ ...segment.byteRange !== void 0 && { byteRange: segment.byteRange }
93
+ });
94
+ }
95
+ return tasks;
96
+ };
97
+ /**
98
+ * Execute a single LoadTask: fetch (if needed) then forward to SourceBufferActor.
99
+ * Sets/clears in-flight tracking around async operations so send() can make
100
+ * accurate continue/preempt decisions at any point during execution.
101
+ *
102
+ * @todo Rename alongside LoadTask (e.g. executeOp).
103
+ */
104
+ const executeLoadTask = async (task) => {
105
+ const signal = abortController.signal;
106
+ try {
107
+ if (task.type === "remove") {
108
+ await sourceBufferActor.send(task, signal);
109
+ return;
110
+ }
111
+ if (task.type === "append-init") {
112
+ inFlightInitTrackId = task.meta.trackId;
113
+ if (!signal.aborted) {
114
+ const data = await fetchBytes(task, {
115
+ signal,
116
+ minChunkSize: Infinity
117
+ });
118
+ const isTrackSwitch = pendingTasks?.some((t) => t.type === "append-init" && t.meta.trackId !== task.meta.trackId);
119
+ if (!signal.aborted || !isTrackSwitch) {
120
+ const appendSignal = signal.aborted ? new AbortController().signal : signal;
121
+ await sourceBufferActor.send({
122
+ type: "append-init",
123
+ data,
124
+ meta: task.meta
125
+ }, appendSignal);
126
+ }
127
+ }
128
+ return;
129
+ }
130
+ inFlightSegmentId = task.meta.id;
131
+ if (!signal.aborted) {
132
+ const stream = await fetchBytes(task, { signal });
133
+ if (!signal.aborted) await sourceBufferActor.send({
134
+ type: "append-segment",
135
+ data: stream,
136
+ meta: task.meta
137
+ }, signal);
138
+ }
139
+ } finally {
140
+ inFlightInitTrackId = null;
141
+ inFlightSegmentId = null;
142
+ }
143
+ };
144
+ /**
145
+ * Drain the scheduled task list sequentially.
146
+ * After each task completes, checks for a pending replacement plan from send().
147
+ * If the signal was aborted and no new plan arrived, stops immediately.
148
+ */
149
+ const runScheduled = async (initialTasks) => {
150
+ running = true;
151
+ abortController = new AbortController();
152
+ let scheduled = initialTasks;
153
+ while (scheduled.length > 0 && !destroyed) {
154
+ const task = scheduled[0];
155
+ scheduled = scheduled.slice(1);
156
+ try {
157
+ await executeLoadTask(task);
158
+ } catch (error) {
159
+ if (error instanceof Error && error.name === "AbortError") {} else {
160
+ console.error("Unexpected error in segment loader:", error);
161
+ scheduled = [];
162
+ }
163
+ }
164
+ if (pendingTasks !== null) {
165
+ scheduled = pendingTasks;
166
+ pendingTasks = null;
167
+ abortController = new AbortController();
168
+ } else if (abortController.signal.aborted) break;
169
+ }
170
+ abortController = null;
171
+ running = false;
172
+ };
173
+ return {
174
+ send(message) {
175
+ if (destroyed) return;
176
+ const allTasks = planTasks(message);
177
+ if (!running) {
178
+ if (allTasks.length === 0) return;
179
+ runScheduled(allTasks);
180
+ return;
181
+ }
182
+ if (inFlightSegmentId !== null && allTasks.some((t) => t.type === "append-segment" && t.meta.id === inFlightSegmentId) || inFlightInitTrackId !== null && allTasks.some((t) => t.type === "append-init" && t.meta.trackId === inFlightInitTrackId)) pendingTasks = allTasks.filter((t) => !(t.type === "append-segment" && t.meta.id === inFlightSegmentId) && !(t.type === "append-init" && t.meta.trackId === inFlightInitTrackId));
183
+ else {
184
+ pendingTasks = allTasks;
185
+ abortController?.abort();
186
+ }
187
+ },
188
+ destroy() {
189
+ destroyed = true;
190
+ abortController?.abort();
191
+ }
192
+ };
193
+ }
194
+
195
+ //#endregion
196
+ export { createSegmentLoaderActor };
197
+ //# sourceMappingURL=segment-loader-actor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"segment-loader-actor.js","names":[],"sources":["../../../../src/dom/features/segment-loader-actor.ts"],"sourcesContent":["import { calculateBackBufferFlushPoint } from '../../core/buffer/back-buffer';\nimport { calculateForwardFlushPoint, getSegmentsToLoad } from '../../core/buffer/forward-buffer';\nimport type { AddressableObject, AudioTrack, Segment, VideoTrack } from '../../core/types';\nimport type {\n AppendInitMessage,\n AppendSegmentMessage,\n RemoveMessage,\n SourceBufferActor,\n} from '../media/source-buffer-actor';\n\n// ============================================================================\n// BUFFER STATE TYPES\n// ============================================================================\n\n/**\n * Buffer state for a single SourceBuffer.\n * Tracks which init segment and media segments are loaded.\n */\nexport interface SourceBufferState {\n /** Track ID of the loaded init segment */\n initTrackId?: string;\n /** Loaded media segments (unordered - selectors derive ordering) */\n segments: Array<{ id: string; trackId: string }>;\n}\n\n/**\n * Buffer state for all SourceBuffers.\n */\nexport interface BufferState {\n video?: SourceBufferState;\n audio?: SourceBufferState;\n}\n\n// ============================================================================\n// MESSAGE PROTOCOL\n// ============================================================================\n\n/** Track types that have SourceBuffers (video and audio only). */\nexport type SegmentLoaderTrack = VideoTrack | AudioTrack;\n\n/**\n * Message sent to a SegmentLoaderActor.\n *\n * `range` is optional to distinguish loading modes:\n * - No range: load init segment only (metadata preload mode)\n * - With range: load init + all segments overlapping [start, end]\n *\n * `start` and `end` are raw time values — no segment snapping.\n * The actor maps them onto segment boundaries internally.\n */\nexport type SegmentLoaderMessage = {\n type: 'load';\n track: SegmentLoaderTrack;\n range?: { start: number; end: number };\n};\n\n// ============================================================================\n// LOAD TASK\n// ============================================================================\n\n/**\n * A LoadTask is the intent to perform one unit of SegmentLoader work.\n * Unlike SourceBufferMessage, fetch-based tasks carry a URL rather than\n * pre-fetched data — the runner fetches and appends them in sequence.\n *\n * Derived from SourceBufferMessage types by removing `data` and adding\n * a fetch URL via AddressableObject.\n *\n * @todo Rename — \"LoadTask\" risks confusion with the `Task` class used for\n * SourceBufferActor scheduling. These are closer to operation descriptors or\n * messages than tasks in that sense.\n */\nexport type LoadTask =\n | (Omit<AppendInitMessage, 'data'> & AddressableObject)\n | (Omit<AppendSegmentMessage, 'data'> & AddressableObject)\n | RemoveMessage;\n\n// ============================================================================\n// ACTOR INTERFACE\n// ============================================================================\n\nexport interface SegmentLoaderActor {\n send(message: SegmentLoaderMessage): void;\n destroy(): void;\n}\n\n// ============================================================================\n// IMPLEMENTATION\n// ============================================================================\n\n/**\n * Creates a SegmentLoaderActor for one track type (video or audio).\n *\n * Receives load assignments via `send()` and owns all execution: planning,\n * removes, fetches, and appends. Coordinates with the SourceBufferActor for\n * all physical SourceBuffer operations.\n *\n * Planning (Cases 1–3) happens in `send()` on every incoming message, producing\n * an ordered LoadTask list. The runner drains that list sequentially. When a new\n * message arrives mid-run, send() replans and either continues the in-flight\n * operation (if still needed) or preempts it.\n *\n * @param sourceBufferActor - Shared SourceBufferActor reference (not owned)\n * @param fetchBytes - Tracked fetch closure (owns throughput sampling for segments).\n * Accepts an optional `minChunkSize` in options; init segments pass `Infinity`\n * so the entire body accumulates as one chunk before appending.\n */\nexport function createSegmentLoaderActor(\n sourceBufferActor: SourceBufferActor,\n fetchBytes: (\n addressable: AddressableObject,\n options?: RequestInit & { minChunkSize?: number }\n ) => Promise<AsyncIterable<Uint8Array>>\n): SegmentLoaderActor {\n let pendingTasks: LoadTask[] | null = null;\n let inFlightInitTrackId: string | null = null;\n let inFlightSegmentId: string | null = null;\n let abortController: AbortController | null = null;\n let running = false;\n let destroyed = false;\n\n const getBufferedSegments = (allSegments: readonly Segment[]): Segment[] => {\n // Exclude partial segments — they are still being streamed and must not be\n // treated as fully buffered for load planning or buffer window calculations.\n const bufferedIds = new Set(sourceBufferActor.snapshot.context.segments.filter((s) => !s.partial).map((s) => s.id));\n return allSegments.filter((s) => bufferedIds.has(s.id));\n };\n\n /**\n * Translate a load message into an ordered LoadTask list based on committed\n * actor state. In-flight awareness is handled separately in send().\n *\n * @todo Rename alongside LoadTask (e.g. planOps).\n *\n * Case 1 — Removes: forward and back buffer flush points, segment-aligned.\n * No flush on track switch: appending new content overwrites existing buffer\n * ranges, and the actor's time-aligned deduplication keeps the segment model\n * accurate as new segments arrive.\n *\n * Case 2 — Init: schedule if not yet committed for this track.\n *\n * Case 3 — Segments: all segments in the load window not yet committed.\n */\n const planTasks = (message: SegmentLoaderMessage): LoadTask[] => {\n const { track, range } = message;\n const actorCtx = sourceBufferActor.snapshot.context;\n const bufferedSegments = getBufferedSegments(track.segments);\n const currentTime = range?.start ?? 0;\n const tasks: LoadTask[] = [];\n\n // Case 1: Removes\n if (range) {\n const forwardFlushStart = calculateForwardFlushPoint(bufferedSegments, currentTime);\n if (forwardFlushStart < Infinity) {\n tasks.push({ type: 'remove', start: forwardFlushStart, end: Infinity });\n }\n const backFlushEnd = calculateBackBufferFlushPoint(bufferedSegments, currentTime);\n if (backFlushEnd > 0) {\n tasks.push({ type: 'remove', start: 0, end: backFlushEnd });\n }\n }\n\n // Case 2: Init\n if (actorCtx.initTrackId !== track.id) {\n tasks.push({\n type: 'append-init',\n meta: { trackId: track.id },\n url: track.initialization.url,\n ...(track.initialization.byteRange !== undefined && { byteRange: track.initialization.byteRange }),\n });\n }\n\n // Case 3: Segments\n if (range) {\n const EPSILON = 0.0001;\n const segmentsToLoad = getSegmentsToLoad(track.segments, bufferedSegments, currentTime).filter((seg) => {\n // Quality-aware filter: skip segments already covered by equal-or-higher-quality\n // content in the actor context. Preserves buffered high-quality content during\n // ABR downgrades; loads during upgrades and for uncovered positions.\n const existing = actorCtx.segments.find((s) => Math.abs(s.startTime - seg.startTime) < EPSILON);\n // Partial segments are still streaming — treat as not buffered so they\n // are always re-planned (avoids relying on incomplete data).\n if (existing?.partial) return true;\n if (!existing?.trackBandwidth || !track.bandwidth) return true;\n return track.bandwidth > existing.trackBandwidth;\n });\n for (const segment of segmentsToLoad) {\n tasks.push({\n type: 'append-segment',\n meta: {\n id: segment.id,\n startTime: segment.startTime,\n duration: segment.duration,\n trackId: track.id,\n trackBandwidth: track.bandwidth,\n },\n url: segment.url,\n ...(segment.byteRange !== undefined && { byteRange: segment.byteRange }),\n });\n }\n }\n\n return tasks;\n };\n\n /**\n * Execute a single LoadTask: fetch (if needed) then forward to SourceBufferActor.\n * Sets/clears in-flight tracking around async operations so send() can make\n * accurate continue/preempt decisions at any point during execution.\n *\n * @todo Rename alongside LoadTask (e.g. executeOp).\n */\n const executeLoadTask = async (task: LoadTask): Promise<void> => {\n const signal = abortController!.signal;\n try {\n if (task.type === 'remove') {\n await sourceBufferActor.send(task, signal);\n return;\n }\n\n if (task.type === 'append-init') {\n inFlightInitTrackId = task.meta.trackId;\n if (!signal.aborted) {\n // Init segments are small and need the full body before the\n // same-track-seek vs track-switch commit decision can be made.\n // minChunkSize: Infinity causes ChunkedStreamIterable to accumulate\n // all chunks and yield exactly one — equivalent to arrayBuffer() but\n // through the same streaming path as media segments.\n const data = await fetchBytes(task, { signal, minChunkSize: Infinity });\n // For seeks on the same track: commit even if aborted — avoids re-fetching the\n // same init next time. For track switches: don't commit the old track's init;\n // the new track's init follows in pendingTasks.\n const isTrackSwitch = pendingTasks?.some(\n (t) => t.type === 'append-init' && t.meta.trackId !== task.meta.trackId\n );\n if (!signal.aborted || !isTrackSwitch) {\n const appendSignal = signal.aborted ? new AbortController().signal : signal;\n await sourceBufferActor.send({ type: 'append-init', data, meta: task.meta }, appendSignal);\n }\n }\n return;\n }\n\n // append-segment: await headers eagerly (starts the HTTP connection and\n // records the fetch in observers like tests), then pass the body stream\n // directly to the actor so chunks are appended as they arrive.\n inFlightSegmentId = task.meta.id;\n if (!signal.aborted) {\n const stream = await fetchBytes(task, { signal });\n if (!signal.aborted) {\n await sourceBufferActor.send({ type: 'append-segment', data: stream, meta: task.meta }, signal);\n }\n }\n } finally {\n inFlightInitTrackId = null;\n inFlightSegmentId = null;\n }\n };\n\n /**\n * Drain the scheduled task list sequentially.\n * After each task completes, checks for a pending replacement plan from send().\n * If the signal was aborted and no new plan arrived, stops immediately.\n */\n const runScheduled = async (initialTasks: LoadTask[]): Promise<void> => {\n running = true;\n abortController = new AbortController();\n let scheduled = initialTasks;\n\n while (scheduled.length > 0 && !destroyed) {\n const task = scheduled[0]!;\n scheduled = scheduled.slice(1);\n\n try {\n await executeLoadTask(task);\n } catch (error) {\n if (error instanceof Error && error.name === 'AbortError') {\n // Abort is handled in the post-task check below.\n } else {\n console.error('Unexpected error in segment loader:', error);\n // Non-abort error (e.g. network failure on init): don't continue with\n // remaining tasks in this sequence — they depend on the failed step.\n // A pending replacement plan (if any) will still be picked up below.\n scheduled = [];\n }\n }\n\n if (pendingTasks !== null) {\n // A new plan arrived while this task was running — switch to it.\n scheduled = pendingTasks;\n pendingTasks = null;\n abortController = new AbortController();\n } else if (abortController.signal.aborted) {\n // Aborted with no replacement plan — stop.\n break;\n }\n }\n\n abortController = null;\n running = false;\n };\n\n return {\n send(message: SegmentLoaderMessage) {\n if (destroyed) return;\n\n const allTasks = planTasks(message);\n\n if (!running) {\n if (allTasks.length === 0) return;\n runScheduled(allTasks);\n return;\n }\n\n // Determine whether the in-flight operation is still needed for the new plan.\n const inFlightStillNeeded =\n (inFlightSegmentId !== null &&\n allTasks.some((t) => t.type === 'append-segment' && t.meta.id === inFlightSegmentId)) ||\n (inFlightInitTrackId !== null &&\n allTasks.some((t) => t.type === 'append-init' && t.meta.trackId === inFlightInitTrackId));\n\n if (inFlightStillNeeded) {\n // Continue: the in-flight operation covers something the new plan needs.\n // Queue everything except the in-flight item — it will complete on its own.\n pendingTasks = allTasks.filter(\n (t) =>\n !(t.type === 'append-segment' && t.meta.id === inFlightSegmentId) &&\n !(t.type === 'append-init' && t.meta.trackId === inFlightInitTrackId)\n );\n } else {\n // Preempt: in-flight work is not needed for the new plan. Abort and replan.\n pendingTasks = allTasks;\n abortController?.abort();\n }\n },\n\n destroy() {\n destroyed = true;\n abortController?.abort();\n },\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,55 @@
1
+ import { attachMediaSource, createMediaSource, waitForSourceOpen } from "../media/mediasource-setup.js";
2
+ import { combineLatest } from "../../core/reactive/combine-latest.js";
3
+ import { isNil } from "@videojs/utils/predicate";
4
+
5
+ //#region src/dom/features/setup-mediasource.ts
6
+ /**
7
+ * Check if we have the minimum requirements to create MediaSource.
8
+ */
9
+ function canSetup(state, owners) {
10
+ return !isNil(owners.mediaElement) && !isNil(state.presentation?.url);
11
+ }
12
+ /**
13
+ * Check if we should proceed with MediaSource creation.
14
+ * Placeholder for future conditions (e.g., checking if already created).
15
+ */
16
+ function shouldSetup(_state, owners) {
17
+ return isNil(owners.mediaSource);
18
+ }
19
+ /**
20
+ * Setup MediaSource orchestration.
21
+ *
22
+ * Creates and attaches MediaSource when:
23
+ * - mediaElement exists in owners
24
+ * - presentation.url exists in state
25
+ *
26
+ * Updates owners.mediaSource after successful setup.
27
+ */
28
+ function setupMediaSource({ state, owners }) {
29
+ let settingUp = false;
30
+ let abortController = null;
31
+ const unsubscribe = combineLatest([state, owners]).subscribe(async ([currentState, currentOwners]) => {
32
+ if (!canSetup(currentState, currentOwners) || !shouldSetup(currentState, currentOwners) || settingUp) return;
33
+ try {
34
+ settingUp = true;
35
+ abortController = new AbortController();
36
+ const mediaSource = createMediaSource({ preferManaged: true });
37
+ attachMediaSource(mediaSource, currentOwners.mediaElement);
38
+ await waitForSourceOpen(mediaSource, abortController.signal);
39
+ owners.patch({ mediaSource });
40
+ } catch (error) {
41
+ if (error instanceof DOMException && error.name === "AbortError") return;
42
+ throw error;
43
+ } finally {
44
+ settingUp = false;
45
+ }
46
+ });
47
+ return () => {
48
+ abortController?.abort();
49
+ unsubscribe();
50
+ };
51
+ }
52
+
53
+ //#endregion
54
+ export { setupMediaSource };
55
+ //# sourceMappingURL=setup-mediasource.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"setup-mediasource.js","names":[],"sources":["../../../../src/dom/features/setup-mediasource.ts"],"sourcesContent":["import { isNil } from '@videojs/utils/predicate';\nimport { combineLatest } from '../../core/reactive/combine-latest';\nimport type { WritableState } from '../../core/state/create-state';\nimport type { Presentation } from '../../core/types';\nimport { attachMediaSource, createMediaSource, waitForSourceOpen } from '../media/mediasource-setup';\n\n/**\n * State shape required for MediaSource setup.\n */\nexport interface MediaSourceState {\n presentation?: Presentation;\n}\n\n/**\n * Owners shape for MediaSource setup.\n */\nexport interface MediaSourceOwners {\n mediaElement?: HTMLMediaElement | undefined;\n mediaSource?: MediaSource;\n}\n\n/**\n * Check if we have the minimum requirements to create MediaSource.\n */\nexport function canSetup(state: MediaSourceState, owners: MediaSourceOwners): boolean {\n return !isNil(owners.mediaElement) && !isNil(state.presentation?.url);\n}\n\n/**\n * Check if we should proceed with MediaSource creation.\n * Placeholder for future conditions (e.g., checking if already created).\n */\nexport function shouldSetup(_state: MediaSourceState, owners: MediaSourceOwners): boolean {\n // Don't create if we already have one\n return isNil(owners.mediaSource);\n}\n\n/**\n * Setup MediaSource orchestration.\n *\n * Creates and attaches MediaSource when:\n * - mediaElement exists in owners\n * - presentation.url exists in state\n *\n * Updates owners.mediaSource after successful setup.\n */\nexport function setupMediaSource({\n state,\n owners,\n}: {\n state: WritableState<MediaSourceState>;\n owners: WritableState<MediaSourceOwners>;\n}): () => void {\n let settingUp = false;\n let abortController: AbortController | null = null;\n\n const unsubscribe = combineLatest([state, owners]).subscribe(\n async ([currentState, currentOwners]: [MediaSourceState, MediaSourceOwners]) => {\n if (!canSetup(currentState, currentOwners) || !shouldSetup(currentState, currentOwners) || settingUp) return;\n\n try {\n settingUp = true;\n abortController = new AbortController();\n\n // Prefer ManagedMediaSource when available (Safari 17+). attachMediaSource\n // handles the srcObject path and sets disableRemotePlayback automatically.\n const mediaSource = createMediaSource({ preferManaged: true });\n\n // Attach to element\n attachMediaSource(mediaSource, currentOwners.mediaElement!);\n\n // Wait for sourceopen — aborted if destroy() is called before it fires\n await waitForSourceOpen(mediaSource, abortController.signal);\n\n owners.patch({ mediaSource });\n } catch (error) {\n if (error instanceof DOMException && error.name === 'AbortError') return;\n throw error;\n } finally {\n settingUp = false;\n }\n }\n );\n\n return () => {\n abortController?.abort();\n unsubscribe();\n };\n}\n"],"mappings":";;;;;;;;AAwBA,SAAgB,SAAS,OAAyB,QAAoC;AACpF,QAAO,CAAC,MAAM,OAAO,aAAa,IAAI,CAAC,MAAM,MAAM,cAAc,IAAI;;;;;;AAOvE,SAAgB,YAAY,QAA0B,QAAoC;AAExF,QAAO,MAAM,OAAO,YAAY;;;;;;;;;;;AAYlC,SAAgB,iBAAiB,EAC/B,OACA,UAIa;CACb,IAAI,YAAY;CAChB,IAAI,kBAA0C;CAE9C,MAAM,cAAc,cAAc,CAAC,OAAO,OAAO,CAAC,CAAC,UACjD,OAAO,CAAC,cAAc,mBAA0D;AAC9E,MAAI,CAAC,SAAS,cAAc,cAAc,IAAI,CAAC,YAAY,cAAc,cAAc,IAAI,UAAW;AAEtG,MAAI;AACF,eAAY;AACZ,qBAAkB,IAAI,iBAAiB;GAIvC,MAAM,cAAc,kBAAkB,EAAE,eAAe,MAAM,CAAC;AAG9D,qBAAkB,aAAa,cAAc,aAAc;AAG3D,SAAM,kBAAkB,aAAa,gBAAgB,OAAO;AAE5D,UAAO,MAAM,EAAE,aAAa,CAAC;WACtB,OAAO;AACd,OAAI,iBAAiB,gBAAgB,MAAM,SAAS,aAAc;AAClE,SAAM;YACE;AACR,eAAY;;GAGjB;AAED,cAAa;AACX,mBAAiB,OAAO;AACxB,eAAa"}
@@ -0,0 +1,9 @@
1
+ //#region src/dom/features/setup-sourcebuffer.d.ts
2
+ /**
3
+ * Media track type for SourceBuffer setup.
4
+ * Text tracks are excluded as they don't use MSE SourceBuffers.
5
+ */
6
+ type MediaTrackType = 'video' | 'audio';
7
+ //#endregion
8
+ export { MediaTrackType };
9
+ //# sourceMappingURL=setup-sourcebuffer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"setup-sourcebuffer.d.ts","names":[],"sources":["../../../../src/dom/features/setup-sourcebuffer.ts"],"mappings":";;;;;KAaY,cAAA"}
@@ -0,0 +1,70 @@
1
+ import { isResolvedTrack } from "../../core/types/index.js";
2
+ import { createSourceBuffer } from "../media/mediasource-setup.js";
3
+ import { combineLatest } from "../../core/reactive/combine-latest.js";
4
+ import { getSelectedTrack } from "../../core/utils/track-selection.js";
5
+ import { createSourceBufferActor } from "../media/source-buffer-actor.js";
6
+ import "@videojs/utils/predicate";
7
+
8
+ //#region src/dom/features/setup-sourcebuffer.ts
9
+ /**
10
+ * Build MIME codec string from track metadata.
11
+ *
12
+ * @param track - Resolved track with mimeType and codecs
13
+ * @returns MIME codec string (e.g., 'video/mp4; codecs="avc1.42E01E,mp4a.40.2"')
14
+ *
15
+ * @example
16
+ * buildMimeCodec({ mimeType: 'video/mp4', codecs: ['avc1.42E01E'] })
17
+ * // => 'video/mp4; codecs="avc1.42E01E"'
18
+ */
19
+ function buildMimeCodec(track) {
20
+ const codecString = track.codecs?.join(",") ?? "";
21
+ return `${track.mimeType}; codecs="${codecString}"`;
22
+ }
23
+ /**
24
+ * Setup all needed SourceBuffers as a single coordinated operation.
25
+ *
26
+ * Waits until ALL selected tracks (video and/or audio) are resolved with
27
+ * codecs, then creates every SourceBuffer in one synchronous block before
28
+ * patching owners. This guarantees that downstream consumers (e.g.
29
+ * loadSegments) never see a partial set of SourceBuffers — preventing the
30
+ * Firefox bug where appending to a video SourceBuffer before the audio
31
+ * SourceBuffer exists causes mozHasAudio to be permanently false.
32
+ *
33
+ * Handles video-only, audio-only, and combined presentations correctly:
34
+ * only the tracks that are actually selected are waited on and created.
35
+ *
36
+ * @example
37
+ * const cleanup = setupSourceBuffers({ state, owners });
38
+ */
39
+ function setupSourceBuffers({ state, owners }) {
40
+ let setupDone = false;
41
+ return combineLatest([state, owners]).subscribe(async ([currentState, currentOwners]) => {
42
+ if (setupDone) return;
43
+ if (!currentOwners.mediaSource) return;
44
+ const videoSelected = !!currentState.selectedVideoTrackId;
45
+ const audioSelected = !!currentState.selectedAudioTrackId;
46
+ if (!videoSelected && !audioSelected) return;
47
+ const videoTrack = videoSelected ? getSelectedTrack(currentState, "video") : null;
48
+ const audioTrack = audioSelected ? getSelectedTrack(currentState, "audio") : null;
49
+ if (videoSelected && (!videoTrack || !isResolvedTrack(videoTrack) || !videoTrack.codecs?.length)) return;
50
+ if (audioSelected && (!audioTrack || !isResolvedTrack(audioTrack) || !audioTrack.codecs?.length)) return;
51
+ setupDone = true;
52
+ const patch = {};
53
+ if (videoSelected && videoTrack && isResolvedTrack(videoTrack)) {
54
+ const buffer = createSourceBuffer(currentOwners.mediaSource, buildMimeCodec(videoTrack));
55
+ patch.videoBuffer = buffer;
56
+ patch.videoBufferActor = createSourceBufferActor(buffer);
57
+ }
58
+ if (audioSelected && audioTrack && isResolvedTrack(audioTrack)) {
59
+ const buffer = createSourceBuffer(currentOwners.mediaSource, buildMimeCodec(audioTrack));
60
+ patch.audioBuffer = buffer;
61
+ patch.audioBufferActor = createSourceBufferActor(buffer);
62
+ }
63
+ owners.patch(patch);
64
+ await new Promise((resolve) => requestAnimationFrame(resolve));
65
+ });
66
+ }
67
+
68
+ //#endregion
69
+ export { setupSourceBuffers };
70
+ //# sourceMappingURL=setup-sourcebuffer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"setup-sourcebuffer.js","names":[],"sources":["../../../../src/dom/features/setup-sourcebuffer.ts"],"sourcesContent":["import { isUndefined } from '@videojs/utils/predicate';\nimport { combineLatest } from '../../core/reactive/combine-latest';\nimport type { WritableState } from '../../core/state/create-state';\nimport type { Presentation, ResolvedTrack } from '../../core/types';\nimport { isResolvedTrack } from '../../core/types';\nimport { BufferKeyByType, getSelectedTrack, type TrackSelectionState } from '../../core/utils/track-selection';\nimport { createSourceBuffer } from '../media/mediasource-setup';\nimport { createSourceBufferActor, type SourceBufferActor } from '../media/source-buffer-actor';\n\n/**\n * Media track type for SourceBuffer setup.\n * Text tracks are excluded as they don't use MSE SourceBuffers.\n */\nexport type MediaTrackType = 'video' | 'audio';\n\n/**\n * State shape for SourceBuffer setup.\n */\nexport interface SourceBufferState extends TrackSelectionState {\n presentation?: Presentation;\n selectedVideoTrackId?: string;\n selectedAudioTrackId?: string;\n}\n\n/**\n * Owners shape for SourceBuffer setup.\n */\nexport interface SourceBufferOwners {\n mediaSource?: MediaSource;\n videoBuffer?: SourceBuffer;\n audioBuffer?: SourceBuffer;\n videoBufferActor?: SourceBufferActor;\n audioBufferActor?: SourceBufferActor;\n}\n\n/**\n * Build MIME codec string from track metadata.\n *\n * @param track - Resolved track with mimeType and codecs\n * @returns MIME codec string (e.g., 'video/mp4; codecs=\"avc1.42E01E,mp4a.40.2\"')\n *\n * @example\n * buildMimeCodec({ mimeType: 'video/mp4', codecs: ['avc1.42E01E'] })\n * // => 'video/mp4; codecs=\"avc1.42E01E\"'\n */\nexport function buildMimeCodec(track: ResolvedTrack): string {\n const codecString = track.codecs?.join(',') ?? '';\n return `${track.mimeType}; codecs=\"${codecString}\"`;\n}\n\n/**\n * Check if we can setup SourceBuffer for track type.\n *\n * Requires:\n * - MediaSource exists in owners\n * - Track is selected\n *\n * Note: We don't check mediaSource.readyState because owners holds references\n * to mutable objects. Changes to properties on those objects won't trigger\n * observations. Instead, setupMediaSource only patches owners.mediaSource after\n * it's already open, so if it exists in owners, it's ready to use.\n *\n * Note: Track does not need to be resolved yet. The orchestration will wait\n * for the track to be resolved (via resolveTrack) before creating the SourceBuffer.\n */\nexport function canSetupBuffer(state: SourceBufferState, owners: SourceBufferOwners, type: MediaTrackType): boolean {\n // MediaSource exists (already open - see note above)\n if (!owners.mediaSource) {\n return false;\n }\n\n // Need selected track (doesn't need to be resolved yet)\n const track = getSelectedTrack(state, type);\n if (!track) {\n return false;\n }\n\n return true;\n}\n\n/**\n * Check if we should create SourceBuffer (not already created).\n */\nexport function shouldSetupBuffer(owners: SourceBufferOwners, type: MediaTrackType): boolean {\n const bufferKey = BufferKeyByType[type];\n return isUndefined(owners[bufferKey]);\n}\n\n/**\n * Setup all needed SourceBuffers as a single coordinated operation.\n *\n * Waits until ALL selected tracks (video and/or audio) are resolved with\n * codecs, then creates every SourceBuffer in one synchronous block before\n * patching owners. This guarantees that downstream consumers (e.g.\n * loadSegments) never see a partial set of SourceBuffers — preventing the\n * Firefox bug where appending to a video SourceBuffer before the audio\n * SourceBuffer exists causes mozHasAudio to be permanently false.\n *\n * Handles video-only, audio-only, and combined presentations correctly:\n * only the tracks that are actually selected are waited on and created.\n *\n * @example\n * const cleanup = setupSourceBuffers({ state, owners });\n */\nexport function setupSourceBuffers({\n state,\n owners,\n}: {\n state: WritableState<SourceBufferState>;\n owners: WritableState<SourceBufferOwners>;\n}): () => void {\n let setupDone = false;\n\n const cleanup = combineLatest([state, owners]).subscribe(\n async ([currentState, currentOwners]: [SourceBufferState, SourceBufferOwners]) => {\n if (setupDone) return;\n if (!currentOwners.mediaSource) return;\n\n const videoSelected = !!currentState.selectedVideoTrackId;\n const audioSelected = !!currentState.selectedAudioTrackId;\n\n if (!videoSelected && !audioSelected) return;\n\n const videoTrack = videoSelected ? getSelectedTrack(currentState, 'video') : null;\n const audioTrack = audioSelected ? getSelectedTrack(currentState, 'audio') : null;\n\n // Wait until every selected track is resolved with codecs before\n // creating any SourceBuffer. This is the coordination guarantee.\n if (videoSelected && (!videoTrack || !isResolvedTrack(videoTrack) || !videoTrack.codecs?.length)) return;\n if (audioSelected && (!audioTrack || !isResolvedTrack(audioTrack) || !audioTrack.codecs?.length)) return;\n\n setupDone = true;\n\n // Create all SourceBuffers synchronously — no await between addSourceBuffer\n // calls — then patch owners once so subscribers see all buffers simultaneously.\n const patch: Partial<SourceBufferOwners> = {};\n\n if (videoSelected && videoTrack && isResolvedTrack(videoTrack)) {\n const buffer = createSourceBuffer(currentOwners.mediaSource!, buildMimeCodec(videoTrack));\n patch.videoBuffer = buffer;\n patch.videoBufferActor = createSourceBufferActor(buffer);\n }\n\n if (audioSelected && audioTrack && isResolvedTrack(audioTrack)) {\n const buffer = createSourceBuffer(currentOwners.mediaSource!, buildMimeCodec(audioTrack));\n patch.audioBuffer = buffer;\n patch.audioBufferActor = createSourceBufferActor(buffer);\n }\n\n owners.patch(patch);\n\n // Wait a frame to allow async state updates to flush before downstream\n // orchestrations (loadSegments) begin reacting to the new owners.\n await new Promise((resolve) => requestAnimationFrame(resolve));\n }\n );\n\n return cleanup;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AA6CA,SAAgB,eAAe,OAA8B;CAC3D,MAAM,cAAc,MAAM,QAAQ,KAAK,IAAI,IAAI;AAC/C,QAAO,GAAG,MAAM,SAAS,YAAY,YAAY;;;;;;;;;;;;;;;;;;AAyDnD,SAAgB,mBAAmB,EACjC,OACA,UAIa;CACb,IAAI,YAAY;AA8ChB,QA5CgB,cAAc,CAAC,OAAO,OAAO,CAAC,CAAC,UAC7C,OAAO,CAAC,cAAc,mBAA4D;AAChF,MAAI,UAAW;AACf,MAAI,CAAC,cAAc,YAAa;EAEhC,MAAM,gBAAgB,CAAC,CAAC,aAAa;EACrC,MAAM,gBAAgB,CAAC,CAAC,aAAa;AAErC,MAAI,CAAC,iBAAiB,CAAC,cAAe;EAEtC,MAAM,aAAa,gBAAgB,iBAAiB,cAAc,QAAQ,GAAG;EAC7E,MAAM,aAAa,gBAAgB,iBAAiB,cAAc,QAAQ,GAAG;AAI7E,MAAI,kBAAkB,CAAC,cAAc,CAAC,gBAAgB,WAAW,IAAI,CAAC,WAAW,QAAQ,QAAS;AAClG,MAAI,kBAAkB,CAAC,cAAc,CAAC,gBAAgB,WAAW,IAAI,CAAC,WAAW,QAAQ,QAAS;AAElG,cAAY;EAIZ,MAAM,QAAqC,EAAE;AAE7C,MAAI,iBAAiB,cAAc,gBAAgB,WAAW,EAAE;GAC9D,MAAM,SAAS,mBAAmB,cAAc,aAAc,eAAe,WAAW,CAAC;AACzF,SAAM,cAAc;AACpB,SAAM,mBAAmB,wBAAwB,OAAO;;AAG1D,MAAI,iBAAiB,cAAc,gBAAgB,WAAW,EAAE;GAC9D,MAAM,SAAS,mBAAmB,cAAc,aAAc,eAAe,WAAW,CAAC;AACzF,SAAM,cAAc;AACpB,SAAM,mBAAmB,wBAAwB,OAAO;;AAG1D,SAAO,MAAM,MAAM;AAInB,QAAM,IAAI,SAAS,YAAY,sBAAsB,QAAQ,CAAC;GAEjE"}
@@ -0,0 +1,88 @@
1
+ import { combineLatest } from "../../core/reactive/combine-latest.js";
2
+
3
+ //#region src/dom/features/setup-text-tracks.ts
4
+ /**
5
+ * Get all text tracks from presentation.
6
+ */
7
+ function getTextTracks(presentation) {
8
+ if (!presentation?.selectionSets) return [];
9
+ const textSet = presentation.selectionSets.find((set) => set.type === "text");
10
+ if (!textSet?.switchingSets?.[0]?.tracks) return [];
11
+ return textSet.switchingSets[0].tracks;
12
+ }
13
+ /**
14
+ * Check if we can setup text tracks.
15
+ *
16
+ * Requires:
17
+ * - mediaElement exists
18
+ * - presentation has text tracks to setup
19
+ */
20
+ function canSetupTextTracks(state, owners) {
21
+ return !!owners.mediaElement && !!getTextTracks(state.presentation).length;
22
+ }
23
+ /**
24
+ * Check if we should setup text tracks (not already set up).
25
+ */
26
+ function shouldSetupTextTracks(owners) {
27
+ return !owners.textTracks;
28
+ }
29
+ /**
30
+ * Create a track element for a text track.
31
+ *
32
+ * Note: We use DOM <track> elements instead of the TextTrack JS API
33
+ * because there's no way to remove TextTracks added via addTextTrack().
34
+ */
35
+ function createTrackElement(track) {
36
+ const trackElement = document.createElement("track");
37
+ trackElement.id = track.id;
38
+ trackElement.kind = track.kind;
39
+ trackElement.label = track.label;
40
+ if (track.language) trackElement.srclang = track.language;
41
+ if (track.default) trackElement.default = true;
42
+ trackElement.src = track.url;
43
+ return trackElement;
44
+ }
45
+ /**
46
+ * Setup text tracks orchestration.
47
+ *
48
+ * Triggers when:
49
+ * - mediaElement exists
50
+ * - presentation is resolved (has text tracks)
51
+ *
52
+ * Creates <track> elements for all text tracks and adds them as children
53
+ * to the media element. This allows the browser's native text track rendering.
54
+ *
55
+ * Note: Uses DOM track elements instead of TextTrack API because tracks
56
+ * added via addTextTrack() cannot be removed.
57
+ *
58
+ * @example
59
+ * const cleanup = setupTextTracks({ state, owners });
60
+ */
61
+ function setupTextTracks({ state, owners }) {
62
+ let hasSetup = false;
63
+ let createdTracks = [];
64
+ const unsubscribe = combineLatest([state, owners]).subscribe(([s, o]) => {
65
+ if (hasSetup) return;
66
+ if (!canSetupTextTracks(s, o) || !shouldSetupTextTracks(o)) return;
67
+ hasSetup = true;
68
+ const textTracks = getTextTracks(s.presentation);
69
+ if (textTracks.length === 0) return;
70
+ const trackMap = /* @__PURE__ */ new Map();
71
+ for (const track of textTracks) {
72
+ const trackElement = createTrackElement(track);
73
+ o.mediaElement.appendChild(trackElement);
74
+ trackMap.set(track.id, trackElement);
75
+ createdTracks.push(trackElement);
76
+ }
77
+ owners.patch({ textTracks: trackMap });
78
+ });
79
+ return () => {
80
+ for (const trackElement of createdTracks) trackElement.remove();
81
+ createdTracks = [];
82
+ unsubscribe();
83
+ };
84
+ }
85
+
86
+ //#endregion
87
+ export { setupTextTracks };
88
+ //# sourceMappingURL=setup-text-tracks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"setup-text-tracks.js","names":[],"sources":["../../../../src/dom/features/setup-text-tracks.ts"],"sourcesContent":["import { combineLatest } from '../../core/reactive/combine-latest';\nimport type { WritableState } from '../../core/state/create-state';\nimport type { PartiallyResolvedTextTrack, Presentation } from '../../core/types';\n\n/**\n * State shape for text track setup.\n */\nexport interface TextTrackState {\n presentation?: Presentation | undefined;\n selectedTextTrackId?: string | undefined;\n}\n\n/**\n * Owners shape for text track setup.\n */\nexport interface TextTrackOwners {\n mediaElement?: HTMLMediaElement | undefined;\n textTracks?: Map<string, HTMLTrackElement>;\n}\n\n/**\n * Get all text tracks from presentation.\n */\nfunction getTextTracks(presentation?: Presentation): PartiallyResolvedTextTrack[] {\n if (!presentation?.selectionSets) return [];\n\n const textSet = presentation.selectionSets.find((set) => set.type === 'text');\n if (!textSet?.switchingSets?.[0]?.tracks) return [];\n\n return textSet.switchingSets[0].tracks as PartiallyResolvedTextTrack[];\n}\n\n/**\n * Check if we can setup text tracks.\n *\n * Requires:\n * - mediaElement exists\n * - presentation has text tracks to setup\n */\nexport function canSetupTextTracks(state: TextTrackState, owners: TextTrackOwners): boolean {\n return !!owners.mediaElement && !!getTextTracks(state.presentation).length;\n}\n\n/**\n * Check if we should setup text tracks (not already set up).\n */\nexport function shouldSetupTextTracks(owners: TextTrackOwners): boolean {\n return !owners.textTracks;\n}\n\n/**\n * Create a track element for a text track.\n *\n * Note: We use DOM <track> elements instead of the TextTrack JS API\n * because there's no way to remove TextTracks added via addTextTrack().\n */\nfunction createTrackElement(track: PartiallyResolvedTextTrack): HTMLTrackElement {\n const trackElement = document.createElement('track');\n\n trackElement.id = track.id;\n trackElement.kind = track.kind;\n trackElement.label = track.label;\n\n if (track.language) {\n trackElement.srclang = track.language;\n }\n\n if (track.default) {\n trackElement.default = true;\n }\n\n // Set src to track URL\n trackElement.src = track.url;\n\n return trackElement;\n}\n\n/**\n * Setup text tracks orchestration.\n *\n * Triggers when:\n * - mediaElement exists\n * - presentation is resolved (has text tracks)\n *\n * Creates <track> elements for all text tracks and adds them as children\n * to the media element. This allows the browser's native text track rendering.\n *\n * Note: Uses DOM track elements instead of TextTrack API because tracks\n * added via addTextTrack() cannot be removed.\n *\n * @example\n * const cleanup = setupTextTracks({ state, owners });\n */\nexport function setupTextTracks({\n state,\n owners,\n}: {\n state: WritableState<TextTrackState>;\n owners: WritableState<TextTrackOwners>;\n}): () => void {\n let hasSetup = false;\n let createdTracks: HTMLTrackElement[] = [];\n\n const unsubscribe = combineLatest([state, owners]).subscribe(([s, o]: [TextTrackState, TextTrackOwners]) => {\n // Check orchestration conditions\n if (hasSetup) return; // Only run once\n if (!canSetupTextTracks(s, o) || !shouldSetupTextTracks(o)) return;\n\n // Mark as setup before doing any work to prevent re-entry\n hasSetup = true;\n\n const textTracks = getTextTracks(s.presentation);\n if (textTracks.length === 0) return;\n\n const trackMap = new Map<string, HTMLTrackElement>();\n\n // Create track elements for all text tracks\n for (const track of textTracks) {\n const trackElement = createTrackElement(track);\n o.mediaElement!.appendChild(trackElement);\n trackMap.set(track.id, trackElement);\n createdTracks.push(trackElement);\n }\n\n // Update owners with track element map\n owners.patch({ textTracks: trackMap });\n });\n\n // Return cleanup function\n return () => {\n // Remove all created track elements\n for (const trackElement of createdTracks) {\n trackElement.remove();\n }\n createdTracks = [];\n unsubscribe();\n };\n}\n"],"mappings":";;;;;;AAuBA,SAAS,cAAc,cAA2D;AAChF,KAAI,CAAC,cAAc,cAAe,QAAO,EAAE;CAE3C,MAAM,UAAU,aAAa,cAAc,MAAM,QAAQ,IAAI,SAAS,OAAO;AAC7E,KAAI,CAAC,SAAS,gBAAgB,IAAI,OAAQ,QAAO,EAAE;AAEnD,QAAO,QAAQ,cAAc,GAAG;;;;;;;;;AAUlC,SAAgB,mBAAmB,OAAuB,QAAkC;AAC1F,QAAO,CAAC,CAAC,OAAO,gBAAgB,CAAC,CAAC,cAAc,MAAM,aAAa,CAAC;;;;;AAMtE,SAAgB,sBAAsB,QAAkC;AACtE,QAAO,CAAC,OAAO;;;;;;;;AASjB,SAAS,mBAAmB,OAAqD;CAC/E,MAAM,eAAe,SAAS,cAAc,QAAQ;AAEpD,cAAa,KAAK,MAAM;AACxB,cAAa,OAAO,MAAM;AAC1B,cAAa,QAAQ,MAAM;AAE3B,KAAI,MAAM,SACR,cAAa,UAAU,MAAM;AAG/B,KAAI,MAAM,QACR,cAAa,UAAU;AAIzB,cAAa,MAAM,MAAM;AAEzB,QAAO;;;;;;;;;;;;;;;;;;AAmBT,SAAgB,gBAAgB,EAC9B,OACA,UAIa;CACb,IAAI,WAAW;CACf,IAAI,gBAAoC,EAAE;CAE1C,MAAM,cAAc,cAAc,CAAC,OAAO,OAAO,CAAC,CAAC,WAAW,CAAC,GAAG,OAA0C;AAE1G,MAAI,SAAU;AACd,MAAI,CAAC,mBAAmB,GAAG,EAAE,IAAI,CAAC,sBAAsB,EAAE,CAAE;AAG5D,aAAW;EAEX,MAAM,aAAa,cAAc,EAAE,aAAa;AAChD,MAAI,WAAW,WAAW,EAAG;EAE7B,MAAM,2BAAW,IAAI,KAA+B;AAGpD,OAAK,MAAM,SAAS,YAAY;GAC9B,MAAM,eAAe,mBAAmB,MAAM;AAC9C,KAAE,aAAc,YAAY,aAAa;AACzC,YAAS,IAAI,MAAM,IAAI,aAAa;AACpC,iBAAc,KAAK,aAAa;;AAIlC,SAAO,MAAM,EAAE,YAAY,UAAU,CAAC;GACtC;AAGF,cAAa;AAEX,OAAK,MAAM,gBAAgB,cACzB,cAAa,QAAQ;AAEvB,kBAAgB,EAAE;AAClB,eAAa"}
@@ -0,0 +1,63 @@
1
+ import { listen } from "@videojs/utils/dom";
2
+
3
+ //#region src/dom/features/sync-selected-text-track-from-dom.ts
4
+ /**
5
+ * Sync selectedTextTrackId from DOM text track mode changes.
6
+ *
7
+ * Listens to the `change` event on `media.textTracks` and updates
8
+ * `selectedTextTrackId` when external code (e.g. the captions button via
9
+ * `toggleSubtitles()`) changes a subtitle/caption track mode to 'showing'.
10
+ *
11
+ * This bridges the core store's `toggleSubtitles()` with SPF's reactive text
12
+ * track pipeline (`syncTextTrackModes`, `loadTextTrackCues`). Without this
13
+ * bridge, direct DOM mode changes would be immediately overridden by
14
+ * `syncTextTrackModes` on the next SPF state update.
15
+ *
16
+ * When a subtitle/caption track's mode is 'showing', its DOM `id` — which
17
+ * matches the SPF track ID set by `setupTextTracks` — is written to
18
+ * `selectedTextTrackId`. When no subtitle/caption track is 'showing',
19
+ * `selectedTextTrackId` is cleared along with the deselected track's
20
+ * `textBufferState` entry — setting mode to 'disabled' clears native cues from
21
+ * the track element, so the buffer must be reset to re-fetch cues on re-enable.
22
+ *
23
+ * @example
24
+ * const cleanup = syncSelectedTextTrackFromDom({ state, owners });
25
+ */
26
+ 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;
39
+ if (current.selectedTextTrackId === newId) return;
40
+ if (newId) state.patch({ selectedTextTrackId: newId });
41
+ else {
42
+ const prevId = current.selectedTextTrackId;
43
+ if (prevId && current.textBufferState?.[prevId]) {
44
+ const next = { ...current.textBufferState };
45
+ delete next[prevId];
46
+ state.patch({
47
+ selectedTextTrackId: void 0,
48
+ textBufferState: next
49
+ });
50
+ } else state.patch({ selectedTextTrackId: void 0 });
51
+ }
52
+ };
53
+ removeListener = listen(mediaElement.textTracks, "change", sync);
54
+ });
55
+ return () => {
56
+ removeListener?.();
57
+ unsubscribe();
58
+ };
59
+ }
60
+
61
+ //#endregion
62
+ export { syncSelectedTextTrackFromDom };
63
+ //# sourceMappingURL=sync-selected-text-track-from-dom.js.map
@@ -0,0 +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"}
@@ -0,0 +1,38 @@
1
+ import { combineLatest } from "../../core/reactive/combine-latest.js";
2
+
3
+ //#region src/dom/features/sync-text-track-modes.ts
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
+ * Sync text track modes orchestration.
15
+ *
16
+ * Manages track element modes based on selectedTextTrackId:
17
+ * - Selected track: mode = "showing"
18
+ * - Other tracks: mode = "hidden"
19
+ * - No selection: all tracks mode = "hidden"
20
+ *
21
+ * Note: Uses "hidden" instead of "disabled" for non-selected tracks
22
+ * so they remain available in the browser's track menu.
23
+ *
24
+ * @example
25
+ * const cleanup = syncTextTrackModes({ state, owners });
26
+ */
27
+ 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";
33
+ });
34
+ }
35
+
36
+ //#endregion
37
+ export { syncTextTrackModes };
38
+ //# sourceMappingURL=sync-text-track-modes.js.map
@@ -0,0 +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"}