@videojs/spf 10.0.0-beta.26 → 10.0.0-beta.27

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 (238) hide show
  1. package/dist/default/core/signals/when.js +38 -0
  2. package/dist/default/core/signals/when.js.map +1 -0
  3. package/dist/default/core/tasks/delayed-reschedule.js +31 -0
  4. package/dist/default/core/tasks/delayed-reschedule.js.map +1 -0
  5. package/dist/default/core/tasks/task.js +133 -3
  6. package/dist/default/core/tasks/task.js.map +1 -1
  7. package/dist/default/hls-audio.js +3 -0
  8. package/dist/default/hls-background-video.js +3 -0
  9. package/dist/default/hls-video.js +4 -0
  10. package/dist/default/hls.js +6 -5
  11. package/dist/default/media/dom/capabilities.js +2 -0
  12. package/dist/default/media/dom/capabilities.js.map +1 -1
  13. package/dist/default/media/errors.js +62 -0
  14. package/dist/default/media/errors.js.map +1 -0
  15. package/dist/default/media/hls/parse-media-playlist.js +167 -16
  16. package/dist/default/media/hls/parse-media-playlist.js.map +1 -1
  17. package/dist/default/media/hls/parse-multivariant.js +14 -3
  18. package/dist/default/media/hls/parse-multivariant.js.map +1 -1
  19. package/dist/default/media/hls/reload-policy.js +71 -0
  20. package/dist/default/media/hls/reload-policy.js.map +1 -0
  21. package/dist/default/media/live-window.js +36 -0
  22. package/dist/default/media/live-window.js.map +1 -0
  23. package/dist/default/media/primitives/select-tracks.js +1 -24
  24. package/dist/default/media/primitives/select-tracks.js.map +1 -1
  25. package/dist/default/media/types/index.js +15 -1
  26. package/dist/default/media/types/index.js.map +1 -1
  27. package/dist/default/media/utils/tracks.js +14 -0
  28. package/dist/default/media/utils/tracks.js.map +1 -1
  29. package/dist/default/mux-audio.js +3 -0
  30. package/dist/default/mux-background-video.js +3 -0
  31. package/dist/default/mux-video.js +3 -0
  32. package/dist/default/playback/adapters/hls-audio/adapter.js +186 -0
  33. package/dist/default/playback/adapters/hls-audio/adapter.js.map +1 -0
  34. package/dist/default/playback/adapters/hls-audio/media.js +9 -0
  35. package/dist/default/playback/adapters/hls-audio/media.js.map +1 -0
  36. package/dist/default/playback/adapters/hls-background-video/adapter.js +123 -0
  37. package/dist/default/playback/adapters/hls-background-video/adapter.js.map +1 -0
  38. package/dist/default/playback/adapters/hls-background-video/host.js +60 -0
  39. package/dist/default/playback/adapters/hls-background-video/host.js.map +1 -0
  40. package/dist/default/playback/adapters/hls-background-video/media.js +21 -0
  41. package/dist/default/playback/adapters/hls-background-video/media.js.map +1 -0
  42. package/dist/default/playback/adapters/hls-video/adapter.js +316 -0
  43. package/dist/default/playback/adapters/hls-video/adapter.js.map +1 -0
  44. package/dist/default/playback/adapters/hls-video/error-surface.js +51 -0
  45. package/dist/default/playback/adapters/hls-video/error-surface.js.map +1 -0
  46. package/dist/default/playback/adapters/hls-video/media-tracks.js +126 -0
  47. package/dist/default/playback/adapters/hls-video/media-tracks.js.map +1 -0
  48. package/dist/default/playback/adapters/hls-video/media.js +11 -0
  49. package/dist/default/playback/adapters/hls-video/media.js.map +1 -0
  50. package/dist/default/playback/adapters/mux-audio/media.js +26 -0
  51. package/dist/default/playback/adapters/mux-audio/media.js.map +1 -0
  52. package/dist/default/playback/adapters/mux-video/adapter.js +92 -0
  53. package/dist/default/playback/adapters/mux-video/adapter.js.map +1 -0
  54. package/dist/default/playback/adapters/mux-video/media.js +23 -0
  55. package/dist/default/playback/adapters/mux-video/media.js.map +1 -0
  56. package/dist/default/playback/behaviors/collect-errors.js +77 -0
  57. package/dist/default/playback/behaviors/collect-errors.js.map +1 -0
  58. package/dist/default/playback/behaviors/dom/end-of-stream.js +1 -0
  59. package/dist/default/playback/behaviors/dom/end-of-stream.js.map +1 -1
  60. package/dist/default/playback/behaviors/dom/seek-to-live-edge.js +132 -0
  61. package/dist/default/playback/behaviors/dom/seek-to-live-edge.js.map +1 -0
  62. package/dist/default/playback/behaviors/dom/sync-live-seekable-range.js +26 -0
  63. package/dist/default/playback/behaviors/dom/sync-live-seekable-range.js.map +1 -0
  64. package/dist/default/playback/behaviors/dom/update-mediasource-duration.js +29 -7
  65. package/dist/default/playback/behaviors/dom/update-mediasource-duration.js.map +1 -1
  66. package/dist/default/playback/behaviors/establish-start-media-time.js +78 -4
  67. package/dist/default/playback/behaviors/establish-start-media-time.js.map +1 -1
  68. package/dist/default/playback/behaviors/resolve-track.js +29 -8
  69. package/dist/default/playback/behaviors/resolve-track.js.map +1 -1
  70. package/dist/default/playback/behaviors/track-switching.js +18 -9
  71. package/dist/default/playback/behaviors/track-switching.js.map +1 -1
  72. package/dist/default/playback/engines/hls/engine-audio-only.js +9 -5
  73. package/dist/default/playback/engines/hls/engine-audio-only.js.map +1 -1
  74. package/dist/{dev/playback/engines/background-video/engine.js → default/playback/engines/hls/engine-background-video.js} +3 -3
  75. package/dist/default/playback/engines/hls/engine-background-video.js.map +1 -0
  76. package/dist/default/playback/engines/hls/engine.js +19 -6
  77. package/dist/default/playback/engines/hls/engine.js.map +1 -1
  78. package/dist/default/playback/primitives/error-messages.js +39 -0
  79. package/dist/default/playback/primitives/error-messages.js.map +1 -0
  80. package/dist/default/playback/primitives/live-window.js +66 -0
  81. package/dist/default/playback/primitives/live-window.js.map +1 -0
  82. package/dist/default/playback/primitives/report-track-conditions.js +86 -0
  83. package/dist/default/playback/primitives/report-track-conditions.js.map +1 -0
  84. package/dist/dev/core/signals/when.js +38 -0
  85. package/dist/dev/core/signals/when.js.map +1 -0
  86. package/dist/dev/core/tasks/delayed-reschedule.js +31 -0
  87. package/dist/dev/core/tasks/delayed-reschedule.js.map +1 -0
  88. package/dist/dev/core/tasks/task.d.ts +52 -1
  89. package/dist/dev/core/tasks/task.d.ts.map +1 -1
  90. package/dist/dev/core/tasks/task.js +133 -3
  91. package/dist/dev/core/tasks/task.js.map +1 -1
  92. package/dist/dev/hls-audio.d.ts +3 -0
  93. package/dist/dev/hls-audio.js +3 -0
  94. package/dist/dev/hls-background-video.d.ts +3 -0
  95. package/dist/dev/hls-background-video.js +3 -0
  96. package/dist/dev/hls-video.d.ts +5 -0
  97. package/dist/dev/hls-video.js +4 -0
  98. package/dist/dev/hls.d.ts +6 -5
  99. package/dist/dev/hls.js +6 -5
  100. package/dist/dev/media/dom/capabilities.js +2 -0
  101. package/dist/dev/media/dom/capabilities.js.map +1 -1
  102. package/dist/dev/media/errors.d.ts +82 -0
  103. package/dist/dev/media/errors.d.ts.map +1 -0
  104. package/dist/dev/media/errors.js +62 -0
  105. package/dist/dev/media/errors.js.map +1 -0
  106. package/dist/dev/media/hls/parse-media-playlist.js +167 -16
  107. package/dist/dev/media/hls/parse-media-playlist.js.map +1 -1
  108. package/dist/dev/media/hls/parse-multivariant.js +14 -3
  109. package/dist/dev/media/hls/parse-multivariant.js.map +1 -1
  110. package/dist/dev/media/hls/reload-policy.js +71 -0
  111. package/dist/dev/media/hls/reload-policy.js.map +1 -0
  112. package/dist/dev/media/live-window.js +36 -0
  113. package/dist/dev/media/live-window.js.map +1 -0
  114. package/dist/dev/media/primitives/select-tracks.js +1 -24
  115. package/dist/dev/media/primitives/select-tracks.js.map +1 -1
  116. package/dist/dev/media/types/index.d.ts +125 -9
  117. package/dist/dev/media/types/index.d.ts.map +1 -1
  118. package/dist/dev/media/types/index.js +15 -1
  119. package/dist/dev/media/types/index.js.map +1 -1
  120. package/dist/dev/media/utils/tracks.js +14 -0
  121. package/dist/dev/media/utils/tracks.js.map +1 -1
  122. package/dist/dev/mux-audio.d.ts +4 -0
  123. package/dist/dev/mux-audio.js +3 -0
  124. package/dist/dev/mux-background-video.d.ts +3 -0
  125. package/dist/dev/mux-background-video.js +3 -0
  126. package/dist/dev/mux-video.d.ts +4 -0
  127. package/dist/dev/mux-video.js +3 -0
  128. package/dist/dev/playback/adapters/hls-audio/adapter.d.ts +54 -0
  129. package/dist/dev/playback/adapters/hls-audio/adapter.d.ts.map +1 -0
  130. package/dist/dev/playback/adapters/hls-audio/adapter.js +186 -0
  131. package/dist/dev/playback/adapters/hls-audio/adapter.js.map +1 -0
  132. package/dist/dev/playback/adapters/hls-audio/media.d.ts +10 -0
  133. package/dist/dev/playback/adapters/hls-audio/media.d.ts.map +1 -0
  134. package/dist/dev/playback/adapters/hls-audio/media.js +9 -0
  135. package/dist/dev/playback/adapters/hls-audio/media.js.map +1 -0
  136. package/dist/dev/playback/adapters/hls-background-video/adapter.d.ts +59 -0
  137. package/dist/dev/playback/adapters/hls-background-video/adapter.d.ts.map +1 -0
  138. package/dist/dev/playback/adapters/hls-background-video/adapter.js +123 -0
  139. package/dist/dev/playback/adapters/hls-background-video/adapter.js.map +1 -0
  140. package/dist/dev/playback/adapters/hls-background-video/host.d.ts +38 -0
  141. package/dist/dev/playback/adapters/hls-background-video/host.d.ts.map +1 -0
  142. package/dist/dev/playback/adapters/hls-background-video/host.js +60 -0
  143. package/dist/dev/playback/adapters/hls-background-video/host.js.map +1 -0
  144. package/dist/dev/playback/adapters/hls-background-video/media.d.ts +20 -0
  145. package/dist/dev/playback/adapters/hls-background-video/media.d.ts.map +1 -0
  146. package/dist/dev/playback/adapters/hls-background-video/media.js +21 -0
  147. package/dist/dev/playback/adapters/hls-background-video/media.js.map +1 -0
  148. package/dist/dev/playback/adapters/hls-video/adapter.d.ts +63 -0
  149. package/dist/dev/playback/adapters/hls-video/adapter.d.ts.map +1 -0
  150. package/dist/dev/playback/adapters/hls-video/adapter.js +316 -0
  151. package/dist/dev/playback/adapters/hls-video/adapter.js.map +1 -0
  152. package/dist/dev/playback/adapters/hls-video/error-surface.d.ts +18 -0
  153. package/dist/dev/playback/adapters/hls-video/error-surface.d.ts.map +1 -0
  154. package/dist/dev/playback/adapters/hls-video/error-surface.js +51 -0
  155. package/dist/dev/playback/adapters/hls-video/error-surface.js.map +1 -0
  156. package/dist/dev/playback/adapters/hls-video/media-tracks.d.ts +22 -0
  157. package/dist/dev/playback/adapters/hls-video/media-tracks.d.ts.map +1 -0
  158. package/dist/dev/playback/adapters/hls-video/media-tracks.js +126 -0
  159. package/dist/dev/playback/adapters/hls-video/media-tracks.js.map +1 -0
  160. package/dist/dev/playback/adapters/hls-video/media.d.ts +10 -0
  161. package/dist/dev/playback/adapters/hls-video/media.d.ts.map +1 -0
  162. package/dist/dev/playback/adapters/hls-video/media.js +11 -0
  163. package/dist/dev/playback/adapters/hls-video/media.js.map +1 -0
  164. package/dist/dev/playback/adapters/mux-audio/media.d.ts +28 -0
  165. package/dist/dev/playback/adapters/mux-audio/media.d.ts.map +1 -0
  166. package/dist/dev/playback/adapters/mux-audio/media.js +26 -0
  167. package/dist/dev/playback/adapters/mux-audio/media.js.map +1 -0
  168. package/dist/dev/playback/adapters/mux-video/adapter.d.ts +33 -0
  169. package/dist/dev/playback/adapters/mux-video/adapter.d.ts.map +1 -0
  170. package/dist/dev/playback/adapters/mux-video/adapter.js +92 -0
  171. package/dist/dev/playback/adapters/mux-video/adapter.js.map +1 -0
  172. package/dist/dev/playback/adapters/mux-video/media.d.ts +24 -0
  173. package/dist/dev/playback/adapters/mux-video/media.d.ts.map +1 -0
  174. package/dist/dev/playback/adapters/mux-video/media.js +23 -0
  175. package/dist/dev/playback/adapters/mux-video/media.js.map +1 -0
  176. package/dist/dev/playback/behaviors/collect-errors.js +77 -0
  177. package/dist/dev/playback/behaviors/collect-errors.js.map +1 -0
  178. package/dist/dev/playback/behaviors/dom/end-of-stream.js +1 -0
  179. package/dist/dev/playback/behaviors/dom/end-of-stream.js.map +1 -1
  180. package/dist/dev/playback/behaviors/dom/seek-to-live-edge.js +132 -0
  181. package/dist/dev/playback/behaviors/dom/seek-to-live-edge.js.map +1 -0
  182. package/dist/dev/playback/behaviors/dom/sync-live-seekable-range.js +26 -0
  183. package/dist/dev/playback/behaviors/dom/sync-live-seekable-range.js.map +1 -0
  184. package/dist/dev/playback/behaviors/dom/update-mediasource-duration.js +29 -7
  185. package/dist/dev/playback/behaviors/dom/update-mediasource-duration.js.map +1 -1
  186. package/dist/dev/playback/behaviors/establish-start-media-time.d.ts +5 -0
  187. package/dist/dev/playback/behaviors/establish-start-media-time.d.ts.map +1 -1
  188. package/dist/dev/playback/behaviors/establish-start-media-time.js +78 -4
  189. package/dist/dev/playback/behaviors/establish-start-media-time.js.map +1 -1
  190. package/dist/dev/playback/behaviors/resolve-track.js +29 -8
  191. package/dist/dev/playback/behaviors/resolve-track.js.map +1 -1
  192. package/dist/dev/playback/behaviors/track-switching.js +18 -9
  193. package/dist/dev/playback/behaviors/track-switching.js.map +1 -1
  194. package/dist/dev/playback/engines/hls/engine-audio-only.d.ts +34 -17
  195. package/dist/dev/playback/engines/hls/engine-audio-only.d.ts.map +1 -1
  196. package/dist/dev/playback/engines/hls/engine-audio-only.js +9 -5
  197. package/dist/dev/playback/engines/hls/engine-audio-only.js.map +1 -1
  198. package/dist/dev/playback/engines/{background-video/engine.d.ts → hls/engine-background-video.d.ts} +7 -7
  199. package/dist/dev/playback/engines/hls/engine-background-video.d.ts.map +1 -0
  200. package/dist/{default/playback/engines/background-video/engine.js → dev/playback/engines/hls/engine-background-video.js} +3 -3
  201. package/dist/dev/playback/engines/hls/engine-background-video.js.map +1 -0
  202. package/dist/dev/playback/engines/hls/engine.d.ts +43 -15
  203. package/dist/dev/playback/engines/hls/engine.d.ts.map +1 -1
  204. package/dist/dev/playback/engines/hls/engine.js +19 -6
  205. package/dist/dev/playback/engines/hls/engine.js.map +1 -1
  206. package/dist/dev/playback/primitives/error-messages.js +39 -0
  207. package/dist/dev/playback/primitives/error-messages.js.map +1 -0
  208. package/dist/dev/playback/primitives/live-window.js +66 -0
  209. package/dist/dev/playback/primitives/live-window.js.map +1 -0
  210. package/dist/dev/playback/primitives/report-track-conditions.d.ts +11 -0
  211. package/dist/dev/playback/primitives/report-track-conditions.d.ts.map +1 -0
  212. package/dist/dev/playback/primitives/report-track-conditions.js +86 -0
  213. package/dist/dev/playback/primitives/report-track-conditions.js.map +1 -0
  214. package/package.json +32 -6
  215. package/dist/default/background-video.js +0 -3
  216. package/dist/default/playback/engines/background-video/adapter.js +0 -158
  217. package/dist/default/playback/engines/background-video/adapter.js.map +0 -1
  218. package/dist/default/playback/engines/background-video/engine.js.map +0 -1
  219. package/dist/default/playback/engines/hls/adapter-audio-only.js +0 -124
  220. package/dist/default/playback/engines/hls/adapter-audio-only.js.map +0 -1
  221. package/dist/default/playback/engines/hls/adapter.js +0 -120
  222. package/dist/default/playback/engines/hls/adapter.js.map +0 -1
  223. package/dist/dev/background-video.d.ts +0 -3
  224. package/dist/dev/background-video.js +0 -3
  225. package/dist/dev/playback/engines/background-video/adapter.d.ts +0 -59
  226. package/dist/dev/playback/engines/background-video/adapter.d.ts.map +0 -1
  227. package/dist/dev/playback/engines/background-video/adapter.js +0 -158
  228. package/dist/dev/playback/engines/background-video/adapter.js.map +0 -1
  229. package/dist/dev/playback/engines/background-video/engine.d.ts.map +0 -1
  230. package/dist/dev/playback/engines/background-video/engine.js.map +0 -1
  231. package/dist/dev/playback/engines/hls/adapter-audio-only.d.ts +0 -46
  232. package/dist/dev/playback/engines/hls/adapter-audio-only.d.ts.map +0 -1
  233. package/dist/dev/playback/engines/hls/adapter-audio-only.js +0 -124
  234. package/dist/dev/playback/engines/hls/adapter-audio-only.js.map +0 -1
  235. package/dist/dev/playback/engines/hls/adapter.d.ts +0 -42
  236. package/dist/dev/playback/engines/hls/adapter.d.ts.map +0 -1
  237. package/dist/dev/playback/engines/hls/adapter.js +0 -120
  238. package/dist/dev/playback/engines/hls/adapter.js.map +0 -1
@@ -0,0 +1,38 @@
1
+ import { effect } from "./effect.js";
2
+ //#region src/core/signals/when.ts
3
+ /**
4
+ * Resolve once `condition` returns true. The condition is a tracked read — it
5
+ * re-evaluates whenever a signal it read changes — so this is the promise
6
+ * bridge from signal space into async task bodies (await a state condition
7
+ * mid-task without polling).
8
+ *
9
+ * Settles synchronously when the condition already holds. Rejects with the
10
+ * abort reason when `signal` aborts first (an already-aborted signal rejects
11
+ * without evaluating the condition), so a task awaiting a condition dies with
12
+ * its runner instead of leaking the subscription.
13
+ */
14
+ function when(condition, options = {}) {
15
+ const { signal } = options;
16
+ if (signal?.aborted) return Promise.reject(signal.reason);
17
+ return new Promise((resolve, reject) => {
18
+ let settled = false;
19
+ const settle = (complete) => {
20
+ if (settled) return;
21
+ settled = true;
22
+ complete();
23
+ queueMicrotask(() => {
24
+ signal?.removeEventListener("abort", onAbort);
25
+ stop();
26
+ });
27
+ };
28
+ const onAbort = () => settle(() => reject(signal?.reason));
29
+ signal?.addEventListener("abort", onAbort, { once: true });
30
+ const stop = effect(() => {
31
+ if (!settled && condition()) settle(resolve);
32
+ });
33
+ });
34
+ }
35
+ //#endregion
36
+ export { when };
37
+
38
+ //# sourceMappingURL=when.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"when.js","names":[],"sources":["../../../../src/core/signals/when.ts"],"sourcesContent":["import { effect } from './effect';\n\n/**\n * Resolve once `condition` returns true. The condition is a tracked read — it\n * re-evaluates whenever a signal it read changes — so this is the promise\n * bridge from signal space into async task bodies (await a state condition\n * mid-task without polling).\n *\n * Settles synchronously when the condition already holds. Rejects with the\n * abort reason when `signal` aborts first (an already-aborted signal rejects\n * without evaluating the condition), so a task awaiting a condition dies with\n * its runner instead of leaking the subscription.\n */\nexport function when(condition: () => boolean, options: { signal?: AbortSignal } = {}): Promise<void> {\n const { signal } = options;\n if (signal?.aborted) return Promise.reject(signal.reason);\n\n return new Promise<void>((resolve, reject) => {\n let settled = false;\n const settle = (complete: () => void) => {\n if (settled) return;\n settled = true;\n complete();\n // Deferred: on the effect's synchronous initial run `stop` isn't\n // assigned yet, and stopping an effect from inside its own body is\n // undefined behavior for the scheduler either way.\n queueMicrotask(() => {\n signal?.removeEventListener('abort', onAbort);\n stop();\n });\n };\n const onAbort = () => settle(() => reject(signal?.reason));\n signal?.addEventListener('abort', onAbort, { once: true });\n const stop = effect(() => {\n if (!settled && condition()) settle(resolve);\n });\n });\n}\n"],"mappings":";;;;;;;;;;;;;AAaA,SAAgB,KAAK,WAA0B,UAAoC,CAAC,GAAkB;CACpG,MAAM,EAAE,WAAW;CACnB,IAAI,QAAQ,SAAS,OAAO,QAAQ,OAAO,OAAO,MAAM;CAExD,OAAO,IAAI,SAAe,SAAS,WAAW;EAC5C,IAAI,UAAU;EACd,MAAM,UAAU,aAAyB;GACvC,IAAI,SAAS;GACb,UAAU;GACV,SAAS;GAIT,qBAAqB;IACnB,QAAQ,oBAAoB,SAAS,OAAO;IAC5C,KAAK;GACP,CAAC;EACH;EACA,MAAM,gBAAgB,aAAa,OAAO,QAAQ,MAAM,CAAC;EACzD,QAAQ,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAK,CAAC;EACzD,MAAM,OAAO,aAAa;GACxB,IAAI,CAAC,WAAW,UAAU,GAAG,OAAO,OAAO;EAC7C,CAAC;CACH,CAAC;AACH"}
@@ -0,0 +1,31 @@
1
+ import { sleep } from "@videojs/utils/time";
2
+ //#region src/core/tasks/delayed-reschedule.ts
3
+ /**
4
+ * Build a {@link Reschedule} from a pure cadence function — the common
5
+ * timer-based, *start-anchored* implementation.
6
+ *
7
+ * Invoked concurrently with the run, it observes the result, then waits
8
+ * `cadence(current, previous)` milliseconds **measured from when it was invoked**
9
+ * (≈ the run's start): it subtracts the run's own elapsed time, so consecutive
10
+ * runs begin one cadence apart regardless of how long each run takes (per
11
+ * RFC 8216 §6.3.4's "measured from the last time the client began loading"). If
12
+ * the run takes longer than the cadence, the next run starts immediately.
13
+ *
14
+ * A `null` cadence stops the recurrence. A rejected run rejects this reschedule,
15
+ * which the `RecurringRunner` propagates as the recurrence's failure — error
16
+ * recovery (e.g. retrying transient fetch failures) belongs below, at the fetch
17
+ * layer, not in the cadence.
18
+ */
19
+ function delayedReschedule(cadence) {
20
+ return async (task) => {
21
+ const startedAt = Date.now();
22
+ const ms = cadence(await task.run(), task.previous);
23
+ if (ms === null) return false;
24
+ await sleep(Math.max(0, ms - (Date.now() - startedAt)), task.signal);
25
+ return true;
26
+ };
27
+ }
28
+ //#endregion
29
+ export { delayedReschedule };
30
+
31
+ //# sourceMappingURL=delayed-reschedule.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"delayed-reschedule.js","names":[],"sources":["../../../../src/core/tasks/delayed-reschedule.ts"],"sourcesContent":["import { sleep } from '@videojs/utils/time';\nimport type { Reschedule } from './task';\n\n/**\n * Build a {@link Reschedule} from a pure cadence function — the common\n * timer-based, *start-anchored* implementation.\n *\n * Invoked concurrently with the run, it observes the result, then waits\n * `cadence(current, previous)` milliseconds **measured from when it was invoked**\n * (≈ the run's start): it subtracts the run's own elapsed time, so consecutive\n * runs begin one cadence apart regardless of how long each run takes (per\n * RFC 8216 §6.3.4's \"measured from the last time the client began loading\"). If\n * the run takes longer than the cadence, the next run starts immediately.\n *\n * A `null` cadence stops the recurrence. A rejected run rejects this reschedule,\n * which the `RecurringRunner` propagates as the recurrence's failure — error\n * recovery (e.g. retrying transient fetch failures) belongs below, at the fetch\n * layer, not in the cadence.\n */\nexport function delayedReschedule<TValue>(\n cadence: (current: TValue, previous: TValue | undefined) => number | null\n): Reschedule<TValue> {\n return async (task) => {\n const startedAt = Date.now();\n const current = await task.run();\n // `task.previous` is the prior successful value (carried by the runner's\n // clone); read-only for the cadence, hence the cast off `DeepReadonly`.\n const ms = cadence(current, task.previous as TValue | undefined);\n if (ms === null) return false;\n await sleep(Math.max(0, ms - (Date.now() - startedAt)), task.signal);\n return true;\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAmBA,SAAgB,kBACd,SACoB;CACpB,OAAO,OAAO,SAAS;EACrB,MAAM,YAAY,KAAK,IAAI;EAI3B,MAAM,KAAK,QAAQ,MAHG,KAAK,IAAI,GAGH,KAAK,QAA8B;EAC/D,IAAI,OAAO,MAAM,OAAO;EACxB,MAAM,MAAM,KAAK,IAAI,GAAG,MAAM,KAAK,IAAI,IAAI,UAAU,GAAG,KAAK,MAAM;EACnE,OAAO;CACT;AACF"}
@@ -9,22 +9,31 @@ import { generateId } from "@videojs/utils/string";
9
9
  * propagates into the task's work without requiring the caller to track the
10
10
  * task separately.
11
11
  *
12
+ * `run()` is memoized: the work runs at most once per instance, and every call
13
+ * returns the same promise (so observers can `await run()` to read the result
14
+ * without re-triggering the work). To re-run the *same* work, take a `clone()` —
15
+ * a fresh instance with its own AbortController and a pending state.
16
+ *
12
17
  * Ordering guarantee: `value` is written before `status` transitions to `'done'`;
13
18
  * `error` is written before `status` transitions to `'error'`. Any reader
14
19
  * observing `status === 'done'` is guaranteed `value` is already present.
15
20
  */
16
- var Task = class {
21
+ var Task = class Task {
17
22
  id;
18
23
  #runFn;
24
+ #externalSignal;
19
25
  #abortController = new AbortController();
20
26
  #signal;
21
27
  #status = "pending";
22
28
  #value = void 0;
23
29
  #error = void 0;
30
+ #previous = void 0;
31
+ #promise = void 0;
24
32
  constructor(runFn, config) {
25
33
  this.#runFn = runFn;
26
34
  const rawId = config?.id;
27
35
  this.id = typeof rawId === "function" ? rawId() : rawId ?? generateId();
36
+ this.#externalSignal = config?.signal;
28
37
  this.#signal = config?.signal ? anyAbortSignal([this.#abortController.signal, config.signal]) : this.#abortController.signal;
29
38
  }
30
39
  get status() {
@@ -36,7 +45,17 @@ var Task = class {
36
45
  get error() {
37
46
  return this.#error;
38
47
  }
39
- async run() {
48
+ get previous() {
49
+ return this.#previous;
50
+ }
51
+ get signal() {
52
+ return this.#signal;
53
+ }
54
+ run() {
55
+ this.#promise ??= this.#execute();
56
+ return this.#promise;
57
+ }
58
+ async #execute() {
40
59
  this.#status = "running";
41
60
  try {
42
61
  const result = await this.#runFn(this.#signal);
@@ -52,6 +71,24 @@ var Task = class {
52
71
  abort() {
53
72
  this.#abortController.abort();
54
73
  }
74
+ /**
75
+ * A fresh task with the same work, id, and external signal, in a pending state
76
+ * (its own AbortController, no memoized result) — so it can be run again. Used
77
+ * to re-run structurally identical work (e.g. `RecurringRunner` reloads).
78
+ *
79
+ * The clone inherits this run's value as its `previous` (or this run's own
80
+ * `previous` if it never produced one — e.g. it errored), so a recurrence's
81
+ * `previous` always tracks the last *successful* value across the lineage with
82
+ * no bookkeeping in the runner.
83
+ */
84
+ clone() {
85
+ const cloned = new Task(this.#runFn, {
86
+ id: this.id,
87
+ signal: this.#externalSignal
88
+ });
89
+ cloned.#previous = this.#value ?? this.#previous;
90
+ return cloned;
91
+ }
55
92
  };
56
93
  /**
57
94
  * Runs tasks concurrently, deduplicated by task id.
@@ -185,7 +222,100 @@ var SerialRunner = class {
185
222
  this.abortAll();
186
223
  }
187
224
  };
225
+ /**
226
+ * A {@link Reschedule} that never recurs — the task runs exactly once. Pass it to
227
+ * a {@link RecurringRunner} for non-recurring, run-once work (e.g. resolving a
228
+ * complete VoD playlist that can never go stale).
229
+ */
230
+ const runOnce = () => Promise.resolve(false);
231
+ /**
232
+ * Runs a task, then re-runs it whenever a {@link Reschedule} function says to,
233
+ * until it says stop (or it's aborted) — the recurring sibling of
234
+ * {@link ConcurrentRunner} / {@link SerialRunner}, and like them it's handed a
235
+ * {@link TaskLike} to run.
236
+ *
237
+ * The runner has no notion of time: it just awaits whatever `reschedule`
238
+ * returns (resolves `true` → re-run; `false` → stop). A `reschedule` is required;
239
+ * pass {@link runOnce} for non-recurring, run-exactly-once work.
240
+ *
241
+ * Single-slot, keyed by task **id**: there is always at most one identified
242
+ * active task for re-running. Scheduling a task whose id matches the active one
243
+ * is a no-op — the existing recurrence keeps running (dedup by id). Scheduling a
244
+ * task with a *different* id aborts the prior task's in-flight run and pending
245
+ * reschedule, then takes over the slot (abort-and-replace) — the right shape
246
+ * when there's one logical unit of recurring work (e.g. reloading the *selected*
247
+ * track's media playlist).
248
+ *
249
+ * Each re-run is a fresh `clone()` of the task (since `Task.run()` is memoized —
250
+ * the same instance won't re-execute), carrying the same id so the slot's
251
+ * identity is stable across cycles. The clone also carries the prior cycle's
252
+ * value forward as `task.previous`. The run function should read any inputs that
253
+ * change between cycles at call time rather than capturing them once.
254
+ *
255
+ * The task is the sole cancellation channel: `abortAll()` aborts the active task,
256
+ * which fires `task.signal` — cancelling both its in-flight run and any pending
257
+ * reschedule delay waiting on that signal. An aborted (or stopped) recurrence
258
+ * frees the slot, so a later schedule of the same id starts fresh.
259
+ */
260
+ var RecurringRunner = class {
261
+ #reschedule;
262
+ #active = null;
263
+ #destroyed = false;
264
+ constructor(reschedule) {
265
+ this.#reschedule = reschedule;
266
+ }
267
+ /**
268
+ * Run `task` and recur per the `reschedule` verdict, as a single promise.
269
+ * Resolves with the *final* cycle's value when the recurrence stops; **rejects**
270
+ * if a run (or reschedule) genuinely fails — the rejection propagates to the
271
+ * caller, who owns error handling; the runner only frees its slot (no
272
+ * swallowing). The runner's *own* cancellation (abort/supersede/destroy) is not
273
+ * a failure, so an aborted recurrence settles quietly rather than rejecting —
274
+ * callers don't have to `.catch` routine teardown.
275
+ *
276
+ * Each cycle runs the task and consults `reschedule` concurrently (so the delay
277
+ * can be measured from the run's start); when both settle and this cycle still
278
+ * owns the slot, a `true` verdict re-schedules a `clone()` whose promise is
279
+ * *returned* — so the recurrence is the method calling itself, threaded into one
280
+ * promise, no separate loop. The clone shares the id, so the slot's identity is
281
+ * stable across cycles; it's released just before the re-schedule so the call
282
+ * advances rather than dedup-returning.
283
+ *
284
+ * Note: because each cycle's promise adopts the next, the chain retains every
285
+ * prior cycle for the life of the recurrence — bounded for finite recurrences,
286
+ * an unbounded (small per-cycle) cost for a long-lived one (e.g. live reload).
287
+ */
288
+ schedule(task) {
289
+ if (this.#destroyed) return Promise.resolve();
290
+ if (this.#active?.id === task.id) return this.#active.run();
291
+ this.#cancel();
292
+ this.#active = task;
293
+ return Promise.all([task.run(), this.#reschedule(task)]).then(([value, again]) => {
294
+ if (this.#active === task && again && !task.signal.aborted) {
295
+ this.#active = null;
296
+ return this.schedule(task.clone());
297
+ }
298
+ if (this.#active === task) this.#active = null;
299
+ return value;
300
+ }, (error) => {
301
+ if (this.#active === task) this.#active = null;
302
+ if (task.signal.aborted) return void 0;
303
+ throw error;
304
+ });
305
+ }
306
+ #cancel() {
307
+ this.#active?.abort();
308
+ this.#active = null;
309
+ }
310
+ abortAll() {
311
+ this.#cancel();
312
+ }
313
+ destroy() {
314
+ this.#destroyed = true;
315
+ this.abortAll();
316
+ }
317
+ };
188
318
  //#endregion
189
- export { ConcurrentRunner, SerialRunner, Task };
319
+ export { ConcurrentRunner, RecurringRunner, SerialRunner, Task, runOnce };
190
320
 
191
321
  //# sourceMappingURL=task.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"task.js","names":["#runFn","#abortController","#signal","#status","#value","#error","#pending","#destroyed","#settled","#resolveSettled","#chain","#current"],"sources":["../../../../src/core/tasks/task.ts"],"sourcesContent":["import { anyAbortSignal } from '@videojs/utils/events';\nimport { generateId } from '@videojs/utils/string';\n\n// =============================================================================\n// DeepReadonly\n// =============================================================================\n\n/** Recursively marks all properties as readonly. */\nexport type DeepReadonly<T> = T extends (infer U)[]\n ? ReadonlyArray<DeepReadonly<U>>\n : T extends object\n ? { readonly [K in keyof T]: DeepReadonly<T[K]> }\n : T;\n\n// =============================================================================\n// Task\n// =============================================================================\n\nexport type TaskStatus = 'pending' | 'running' | 'done' | 'error';\n\n/**\n * Configuration for a Task.\n */\nexport interface TaskConfig {\n /**\n * Identifier for this task.\n * - string: used as-is\n * - () => string: called once at construction time\n * - undefined: a unique ID is generated via generateId()\n */\n id?: string | (() => string);\n\n /**\n * Optional external AbortSignal to compose with the task's internal one.\n * The task's work is aborted when either the internal controller (via abort())\n * or this external signal fires — whichever comes first.\n */\n signal?: AbortSignal;\n}\n\n/**\n * Minimal contract for a schedulable unit of async work.\n */\nexport interface TaskLike<TValue = void, TError = unknown> {\n readonly id: string;\n readonly status: TaskStatus;\n readonly value: DeepReadonly<TValue> | undefined;\n readonly error: DeepReadonly<TError> | undefined;\n run(): Promise<TValue>;\n abort(): void;\n}\n\n/**\n * Generic reusable task that wraps an async run function.\n *\n * Owns its own AbortController so it can always be aborted independently.\n * Optionally composes an external AbortSignal so that a parent's cancellation\n * propagates into the task's work without requiring the caller to track the\n * task separately.\n *\n * Ordering guarantee: `value` is written before `status` transitions to `'done'`;\n * `error` is written before `status` transitions to `'error'`. Any reader\n * observing `status === 'done'` is guaranteed `value` is already present.\n */\nexport class Task<TValue = void, TError = unknown> implements TaskLike<TValue, TError> {\n readonly id: string;\n readonly #runFn: (signal: AbortSignal) => Promise<TValue>;\n readonly #abortController = new AbortController();\n readonly #signal: AbortSignal;\n\n #status: TaskStatus = 'pending';\n #value: TValue | undefined = undefined;\n #error: TError | undefined = undefined;\n\n constructor(runFn: (signal: AbortSignal) => Promise<TValue>, config?: TaskConfig) {\n this.#runFn = runFn;\n const rawId = config?.id;\n this.id = typeof rawId === 'function' ? rawId() : (rawId ?? generateId());\n this.#signal = config?.signal\n ? anyAbortSignal([this.#abortController.signal, config.signal])\n : this.#abortController.signal;\n }\n\n get status(): TaskStatus {\n return this.#status;\n }\n\n get value(): DeepReadonly<TValue> | undefined {\n return this.#value as DeepReadonly<TValue> | undefined;\n }\n\n get error(): DeepReadonly<TError> | undefined {\n return this.#error as DeepReadonly<TError> | undefined;\n }\n\n async run(): Promise<TValue> {\n this.#status = 'running';\n try {\n const result = await this.#runFn(this.#signal);\n this.#value = result; // value before status — ordering guarantee\n this.#status = 'done';\n return result;\n } catch (e) {\n this.#error = e as TError; // error before status — ordering guarantee\n this.#status = 'error';\n throw e;\n }\n }\n\n abort(): void {\n this.#abortController.abort();\n }\n}\n\n// =============================================================================\n// ConcurrentRunner\n// =============================================================================\n\n/**\n * Runs tasks concurrently, deduplicated by task id.\n *\n * If a task with a given id is already in flight, subsequent schedule() calls\n * for that id are silently ignored until the first completes. Tasks are stored\n * so abortAll() can cancel any in-flight work (e.g. on engine cleanup).\n */\nexport class ConcurrentRunner {\n readonly #pending = new Map<string, { task: TaskLike<unknown, unknown>; promise: Promise<unknown> }>();\n #settled: Promise<void> = Promise.resolve();\n #resolveSettled: (() => void) | null = null;\n #destroyed = false;\n\n schedule<TValue = void, TError = unknown>(task: TaskLike<TValue, TError>): Promise<TValue> {\n if (this.#destroyed) return Promise.resolve() as Promise<TValue>;\n const existing = this.#pending.get(task.id);\n if (existing) return existing.promise as Promise<TValue>;\n\n if (this.#pending.size === 0) {\n this.#settled = new Promise((resolve) => {\n this.#resolveSettled = resolve;\n });\n }\n\n const promise = task.run();\n // Suppress unhandled rejection for callers that ignore the return value.\n promise.catch(() => {});\n // Cleanup: update pending and resolve settled regardless of outcome.\n const cleanup = () => {\n this.#pending.delete(task.id);\n if (this.#pending.size === 0) {\n this.#resolveSettled?.();\n this.#resolveSettled = null;\n }\n };\n promise.then(cleanup, cleanup);\n\n this.#pending.set(task.id, { task: task as TaskLike<unknown, unknown>, promise: promise as Promise<unknown> });\n return promise;\n }\n\n /**\n * Registers a callback to fire when all currently in-flight tasks settle.\n * If the runner is already idle, the callback is never called. If abortAll()\n * is called before the batch settles, the callback is superseded and silently\n * dropped — no stale callbacks, no generation token required by the caller.\n */\n whenSettled(callback: () => void): void {\n if (this.#pending.size === 0) return;\n const captured = this.#settled;\n captured.then(\n () => {\n if (this.#settled !== captured) return;\n callback();\n },\n () => {}\n );\n }\n\n abortAll(): void {\n for (const { task } of this.#pending.values()) task.abort();\n this.#pending.clear();\n // Resolve the current settled promise so any .then() handlers are queued,\n // then replace the reference — whenSettled callbacks that captured the old\n // reference will see the identity mismatch and be dropped.\n this.#resolveSettled?.();\n this.#resolveSettled = null;\n this.#settled = Promise.resolve();\n }\n\n destroy(): void {\n this.#destroyed = true;\n this.abortAll();\n }\n}\n\n// =============================================================================\n// SerialRunner\n// =============================================================================\n\n/**\n * Runs tasks one at a time in submission order.\n *\n * Each schedule() call returns a Promise that resolves or rejects with the\n * task's result when it is eventually executed. Tasks wait in queue until the\n * prior task completes.\n *\n * Serialization is achieved by chaining each task's run() onto the tail of a\n * shared promise chain — no explicit queue or drain loop needed.\n *\n * abortAll() aborts all pending (not yet started) tasks and the currently\n * in-flight task. Pending tasks still run briefly but receive an aborted\n * signal and are expected to exit early.\n */\nexport class SerialRunner {\n #chain: Promise<unknown> = Promise.resolve();\n readonly #pending = new Set<TaskLike<unknown, unknown>>();\n #current: TaskLike<unknown, unknown> | null = null;\n #destroyed = false;\n\n schedule<TValue = void, TError = unknown>(task: TaskLike<TValue, TError>): Promise<TValue> {\n if (this.#destroyed) return Promise.resolve() as Promise<TValue>;\n const t = task as TaskLike<unknown, unknown>;\n this.#pending.add(t);\n\n const result = this.#chain\n .then(() => {\n this.#pending.delete(t);\n this.#current = t;\n return task.run();\n })\n .finally(() => {\n this.#current = null;\n });\n\n // Advance the chain regardless of whether this task succeeds or fails.\n this.#chain = result.then(\n () => {},\n () => {}\n );\n\n return result as Promise<TValue>;\n }\n\n /**\n * A promise that resolves when all currently-scheduled tasks have settled.\n * Use the reference as a generation token: capture it after scheduling a\n * batch, then check identity in the resolution callback to detect whether\n * a subsequent abortAll() + new batch has superseded this one.\n */\n get settled(): Promise<void> {\n return this.#chain as Promise<void>;\n }\n\n /**\n * Registers a callback to fire when all currently-pending tasks settle.\n * If the runner is already idle (no pending or running tasks), the callback\n * is never called. If new tasks are scheduled before the current batch\n * settles, the callback is superseded and silently dropped — no stale\n * callbacks, no generation token required by the caller.\n */\n whenSettled(callback: () => void): void {\n if (this.#pending.size === 0 && this.#current === null) return;\n const currentChain = this.#chain;\n currentChain.then(\n () => {\n if (this.#chain !== currentChain) return;\n callback();\n },\n () => {}\n );\n }\n\n /** Aborts and clears queued tasks without touching the in-flight task. */\n abortPending(): void {\n for (const task of this.#pending) task.abort();\n this.#pending.clear();\n }\n\n abortAll(): void {\n this.abortPending();\n this.#current?.abort();\n }\n\n destroy(): void {\n this.#destroyed = true;\n this.abortAll();\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;AAgEA,IAAa,OAAb,MAAuF;CACrF;CACA;CACA,mBAA4B,IAAI,gBAAgB;CAChD;CAEA,UAAsB;CACtB,SAA6B,KAAA;CAC7B,SAA6B,KAAA;CAE7B,YAAY,OAAiD,QAAqB;EAChF,KAAKA,SAAS;EACd,MAAM,QAAQ,QAAQ;EACtB,KAAK,KAAK,OAAO,UAAU,aAAa,MAAM,IAAK,SAAS,WAAW;EACvE,KAAKE,UAAU,QAAQ,SACnB,eAAe,CAAC,KAAKD,iBAAiB,QAAQ,OAAO,MAAM,CAAC,IAC5D,KAAKA,iBAAiB;CAC5B;CAEA,IAAI,SAAqB;EACvB,OAAO,KAAKE;CACd;CAEA,IAAI,QAA0C;EAC5C,OAAO,KAAKC;CACd;CAEA,IAAI,QAA0C;EAC5C,OAAO,KAAKC;CACd;CAEA,MAAM,MAAuB;EAC3B,KAAKF,UAAU;EACf,IAAI;GACF,MAAM,SAAS,MAAM,KAAKH,OAAO,KAAKE,OAAO;GAC7C,KAAKE,SAAS;GACd,KAAKD,UAAU;GACf,OAAO;EACT,SAAS,GAAG;GACV,KAAKE,SAAS;GACd,KAAKF,UAAU;GACf,MAAM;EACR;CACF;CAEA,QAAc;EACZ,KAAKF,iBAAiB,MAAM;CAC9B;AACF;;;;;;;;AAaA,IAAa,mBAAb,MAA8B;CAC5B,2BAAoB,IAAI,IAA6E;CACrG,WAA0B,QAAQ,QAAQ;CAC1C,kBAAuC;CACvC,aAAa;CAEb,SAA0C,MAAiD;EACzF,IAAI,KAAKM,YAAY,OAAO,QAAQ,QAAQ;EAC5C,MAAM,WAAW,KAAKD,SAAS,IAAI,KAAK,EAAE;EAC1C,IAAI,UAAU,OAAO,SAAS;EAE9B,IAAI,KAAKA,SAAS,SAAS,GACzB,KAAKE,WAAW,IAAI,SAAS,YAAY;GACvC,KAAKC,kBAAkB;EACzB,CAAC;EAGH,MAAM,UAAU,KAAK,IAAI;EAEzB,QAAQ,YAAY,CAAC,CAAC;EAEtB,MAAM,gBAAgB;GACpB,KAAKH,SAAS,OAAO,KAAK,EAAE;GAC5B,IAAI,KAAKA,SAAS,SAAS,GAAG;IAC5B,KAAKG,kBAAkB;IACvB,KAAKA,kBAAkB;GACzB;EACF;EACA,QAAQ,KAAK,SAAS,OAAO;EAE7B,KAAKH,SAAS,IAAI,KAAK,IAAI;GAAQ;GAA6C;EAA4B,CAAC;EAC7G,OAAO;CACT;;;;;;;CAQA,YAAY,UAA4B;EACtC,IAAI,KAAKA,SAAS,SAAS,GAAG;EAC9B,MAAM,WAAW,KAAKE;EACtB,SAAS,WACD;GACJ,IAAI,KAAKA,aAAa,UAAU;GAChC,SAAS;EACX,SACM,CAAC,CACT;CACF;CAEA,WAAiB;EACf,KAAK,MAAM,EAAE,UAAU,KAAKF,SAAS,OAAO,GAAG,KAAK,MAAM;EAC1D,KAAKA,SAAS,MAAM;EAIpB,KAAKG,kBAAkB;EACvB,KAAKA,kBAAkB;EACvB,KAAKD,WAAW,QAAQ,QAAQ;CAClC;CAEA,UAAgB;EACd,KAAKD,aAAa;EAClB,KAAK,SAAS;CAChB;AACF;;;;;;;;;;;;;;;AAoBA,IAAa,eAAb,MAA0B;CACxB,SAA2B,QAAQ,QAAQ;CAC3C,2BAAoB,IAAI,IAAgC;CACxD,WAA8C;CAC9C,aAAa;CAEb,SAA0C,MAAiD;EACzF,IAAI,KAAKA,YAAY,OAAO,QAAQ,QAAQ;EAC5C,MAAM,IAAI;EACV,KAAKD,SAAS,IAAI,CAAC;EAEnB,MAAM,SAAS,KAAKI,OACjB,WAAW;GACV,KAAKJ,SAAS,OAAO,CAAC;GACtB,KAAKK,WAAW;GAChB,OAAO,KAAK,IAAI;EAClB,CAAC,CAAC,CACD,cAAc;GACb,KAAKA,WAAW;EAClB,CAAC;EAGH,KAAKD,SAAS,OAAO,WACb,CAAC,SACD,CAAC,CACT;EAEA,OAAO;CACT;;;;;;;CAQA,IAAI,UAAyB;EAC3B,OAAO,KAAKA;CACd;;;;;;;;CASA,YAAY,UAA4B;EACtC,IAAI,KAAKJ,SAAS,SAAS,KAAK,KAAKK,aAAa,MAAM;EACxD,MAAM,eAAe,KAAKD;EAC1B,aAAa,WACL;GACJ,IAAI,KAAKA,WAAW,cAAc;GAClC,SAAS;EACX,SACM,CAAC,CACT;CACF;;CAGA,eAAqB;EACnB,KAAK,MAAM,QAAQ,KAAKJ,UAAU,KAAK,MAAM;EAC7C,KAAKA,SAAS,MAAM;CACtB;CAEA,WAAiB;EACf,KAAK,aAAa;EAClB,KAAKK,UAAU,MAAM;CACvB;CAEA,UAAgB;EACd,KAAKJ,aAAa;EAClB,KAAK,SAAS;CAChB;AACF"}
1
+ {"version":3,"file":"task.js","names":["#runFn","#externalSignal","#abortController","#signal","#status","#value","#error","#previous","#promise","#execute","#pending","#destroyed","#settled","#resolveSettled","#chain","#current","#reschedule","#active","#cancel"],"sources":["../../../../src/core/tasks/task.ts"],"sourcesContent":["import { anyAbortSignal } from '@videojs/utils/events';\nimport { generateId } from '@videojs/utils/string';\n\n// =============================================================================\n// DeepReadonly\n// =============================================================================\n\n/** Recursively marks all properties as readonly. */\nexport type DeepReadonly<T> = T extends (infer U)[]\n ? ReadonlyArray<DeepReadonly<U>>\n : T extends object\n ? { readonly [K in keyof T]: DeepReadonly<T[K]> }\n : T;\n\n// =============================================================================\n// Task\n// =============================================================================\n\nexport type TaskStatus = 'pending' | 'running' | 'done' | 'error';\n\n/**\n * Configuration for a Task.\n */\nexport interface TaskConfig {\n /**\n * Identifier for this task.\n * - string: used as-is\n * - () => string: called once at construction time\n * - undefined: a unique ID is generated via generateId()\n */\n id?: string | (() => string);\n\n /**\n * Optional external AbortSignal to compose with the task's internal one.\n * The task's work is aborted when either the internal controller (via abort())\n * or this external signal fires — whichever comes first.\n */\n signal?: AbortSignal;\n}\n\n/**\n * Minimal contract for a schedulable unit of async work.\n */\nexport interface TaskLike<TValue = void, TError = unknown> {\n readonly id: string;\n readonly status: TaskStatus;\n readonly value: DeepReadonly<TValue> | undefined;\n readonly error: DeepReadonly<TError> | undefined;\n /**\n * The last successful value carried forward from the run this was `clone()`d\n * from — the prior cycle's result for a `RecurringRunner` (undefined for an\n * original, or until the lineage has produced a success).\n */\n readonly previous: DeepReadonly<TValue> | undefined;\n /**\n * The signal this task's work runs under (its own abort composed with any\n * external one). Aborting the task fires it — so a `reschedule` can wait on it\n * to have its delay cancelled when the recurrence is aborted.\n */\n readonly signal: AbortSignal;\n /** Run the work, memoized: repeated calls share one execution + result. */\n run(): Promise<TValue>;\n abort(): void;\n /** A fresh, structurally identical task (same work + id) in a pending state — for re-running. */\n clone(): TaskLike<TValue, TError>;\n}\n\n/**\n * Generic reusable task that wraps an async run function.\n *\n * Owns its own AbortController so it can always be aborted independently.\n * Optionally composes an external AbortSignal so that a parent's cancellation\n * propagates into the task's work without requiring the caller to track the\n * task separately.\n *\n * `run()` is memoized: the work runs at most once per instance, and every call\n * returns the same promise (so observers can `await run()` to read the result\n * without re-triggering the work). To re-run the *same* work, take a `clone()` —\n * a fresh instance with its own AbortController and a pending state.\n *\n * Ordering guarantee: `value` is written before `status` transitions to `'done'`;\n * `error` is written before `status` transitions to `'error'`. Any reader\n * observing `status === 'done'` is guaranteed `value` is already present.\n */\nexport class Task<TValue = void, TError = unknown> implements TaskLike<TValue, TError> {\n readonly id: string;\n readonly #runFn: (signal: AbortSignal) => Promise<TValue>;\n readonly #externalSignal: AbortSignal | undefined;\n readonly #abortController = new AbortController();\n readonly #signal: AbortSignal;\n\n #status: TaskStatus = 'pending';\n #value: TValue | undefined = undefined;\n #error: TError | undefined = undefined;\n #previous: TValue | undefined = undefined;\n #promise: Promise<TValue> | undefined = undefined;\n\n constructor(runFn: (signal: AbortSignal) => Promise<TValue>, config?: TaskConfig) {\n this.#runFn = runFn;\n const rawId = config?.id;\n this.id = typeof rawId === 'function' ? rawId() : (rawId ?? generateId());\n this.#externalSignal = config?.signal;\n this.#signal = config?.signal\n ? anyAbortSignal([this.#abortController.signal, config.signal])\n : this.#abortController.signal;\n }\n\n get status(): TaskStatus {\n return this.#status;\n }\n\n get value(): DeepReadonly<TValue> | undefined {\n return this.#value as DeepReadonly<TValue> | undefined;\n }\n\n get error(): DeepReadonly<TError> | undefined {\n return this.#error as DeepReadonly<TError> | undefined;\n }\n\n get previous(): DeepReadonly<TValue> | undefined {\n return this.#previous as DeepReadonly<TValue> | undefined;\n }\n\n get signal(): AbortSignal {\n return this.#signal;\n }\n\n run(): Promise<TValue> {\n // Memoized: run the work once, sharing the same promise across calls. The\n // memoized promise IS what callers await, so it's always handled (no orphan\n // `Promise.resolve/reject`); a sync-throwing `#runFn` is captured as a\n // rejection rather than re-run.\n this.#promise ??= this.#execute();\n return this.#promise;\n }\n\n async #execute(): Promise<TValue> {\n this.#status = 'running';\n try {\n const result = await this.#runFn(this.#signal);\n this.#value = result; // value before status — ordering guarantee\n this.#status = 'done';\n return result;\n } catch (e) {\n this.#error = e as TError; // error before status — ordering guarantee\n this.#status = 'error';\n throw e;\n }\n }\n\n abort(): void {\n this.#abortController.abort();\n }\n\n /**\n * A fresh task with the same work, id, and external signal, in a pending state\n * (its own AbortController, no memoized result) — so it can be run again. Used\n * to re-run structurally identical work (e.g. `RecurringRunner` reloads).\n *\n * The clone inherits this run's value as its `previous` (or this run's own\n * `previous` if it never produced one — e.g. it errored), so a recurrence's\n * `previous` always tracks the last *successful* value across the lineage with\n * no bookkeeping in the runner.\n */\n clone(): Task<TValue, TError> {\n const cloned = new Task<TValue, TError>(this.#runFn, { id: this.id, signal: this.#externalSignal });\n cloned.#previous = this.#value ?? this.#previous;\n return cloned;\n }\n}\n\n// =============================================================================\n// ConcurrentRunner\n// =============================================================================\n\n/**\n * Runs tasks concurrently, deduplicated by task id.\n *\n * If a task with a given id is already in flight, subsequent schedule() calls\n * for that id are silently ignored until the first completes. Tasks are stored\n * so abortAll() can cancel any in-flight work (e.g. on engine cleanup).\n */\nexport class ConcurrentRunner {\n readonly #pending = new Map<string, { task: TaskLike<unknown, unknown>; promise: Promise<unknown> }>();\n #settled: Promise<void> = Promise.resolve();\n #resolveSettled: (() => void) | null = null;\n #destroyed = false;\n\n schedule<TValue = void, TError = unknown>(task: TaskLike<TValue, TError>): Promise<TValue> {\n if (this.#destroyed) return Promise.resolve() as Promise<TValue>;\n const existing = this.#pending.get(task.id);\n if (existing) return existing.promise as Promise<TValue>;\n\n if (this.#pending.size === 0) {\n this.#settled = new Promise((resolve) => {\n this.#resolveSettled = resolve;\n });\n }\n\n const promise = task.run();\n // Suppress unhandled rejection for callers that ignore the return value.\n promise.catch(() => {});\n // Cleanup: update pending and resolve settled regardless of outcome.\n const cleanup = () => {\n this.#pending.delete(task.id);\n if (this.#pending.size === 0) {\n this.#resolveSettled?.();\n this.#resolveSettled = null;\n }\n };\n promise.then(cleanup, cleanup);\n\n this.#pending.set(task.id, { task: task as TaskLike<unknown, unknown>, promise: promise as Promise<unknown> });\n return promise;\n }\n\n /**\n * Registers a callback to fire when all currently in-flight tasks settle.\n * If the runner is already idle, the callback is never called. If abortAll()\n * is called before the batch settles, the callback is superseded and silently\n * dropped — no stale callbacks, no generation token required by the caller.\n */\n whenSettled(callback: () => void): void {\n if (this.#pending.size === 0) return;\n const captured = this.#settled;\n captured.then(\n () => {\n if (this.#settled !== captured) return;\n callback();\n },\n () => {}\n );\n }\n\n abortAll(): void {\n for (const { task } of this.#pending.values()) task.abort();\n this.#pending.clear();\n // Resolve the current settled promise so any .then() handlers are queued,\n // then replace the reference — whenSettled callbacks that captured the old\n // reference will see the identity mismatch and be dropped.\n this.#resolveSettled?.();\n this.#resolveSettled = null;\n this.#settled = Promise.resolve();\n }\n\n destroy(): void {\n this.#destroyed = true;\n this.abortAll();\n }\n}\n\n// =============================================================================\n// SerialRunner\n// =============================================================================\n\n/**\n * Runs tasks one at a time in submission order.\n *\n * Each schedule() call returns a Promise that resolves or rejects with the\n * task's result when it is eventually executed. Tasks wait in queue until the\n * prior task completes.\n *\n * Serialization is achieved by chaining each task's run() onto the tail of a\n * shared promise chain — no explicit queue or drain loop needed.\n *\n * abortAll() aborts all pending (not yet started) tasks and the currently\n * in-flight task. Pending tasks still run briefly but receive an aborted\n * signal and are expected to exit early.\n */\nexport class SerialRunner {\n #chain: Promise<unknown> = Promise.resolve();\n readonly #pending = new Set<TaskLike<unknown, unknown>>();\n #current: TaskLike<unknown, unknown> | null = null;\n #destroyed = false;\n\n schedule<TValue = void, TError = unknown>(task: TaskLike<TValue, TError>): Promise<TValue> {\n if (this.#destroyed) return Promise.resolve() as Promise<TValue>;\n const t = task as TaskLike<unknown, unknown>;\n this.#pending.add(t);\n\n const result = this.#chain\n .then(() => {\n this.#pending.delete(t);\n this.#current = t;\n return task.run();\n })\n .finally(() => {\n this.#current = null;\n });\n\n // Advance the chain regardless of whether this task succeeds or fails.\n this.#chain = result.then(\n () => {},\n () => {}\n );\n\n return result as Promise<TValue>;\n }\n\n /**\n * A promise that resolves when all currently-scheduled tasks have settled.\n * Use the reference as a generation token: capture it after scheduling a\n * batch, then check identity in the resolution callback to detect whether\n * a subsequent abortAll() + new batch has superseded this one.\n */\n get settled(): Promise<void> {\n return this.#chain as Promise<void>;\n }\n\n /**\n * Registers a callback to fire when all currently-pending tasks settle.\n * If the runner is already idle (no pending or running tasks), the callback\n * is never called. If new tasks are scheduled before the current batch\n * settles, the callback is superseded and silently dropped — no stale\n * callbacks, no generation token required by the caller.\n */\n whenSettled(callback: () => void): void {\n if (this.#pending.size === 0 && this.#current === null) return;\n const currentChain = this.#chain;\n currentChain.then(\n () => {\n if (this.#chain !== currentChain) return;\n callback();\n },\n () => {}\n );\n }\n\n /** Aborts and clears queued tasks without touching the in-flight task. */\n abortPending(): void {\n for (const task of this.#pending) task.abort();\n this.#pending.clear();\n }\n\n abortAll(): void {\n this.abortPending();\n this.#current?.abort();\n }\n\n destroy(): void {\n this.#destroyed = true;\n this.abortAll();\n }\n}\n\n// =============================================================================\n// RecurringRunner\n// =============================================================================\n\n/**\n * Decides whether — and *when* — a {@link RecurringRunner} re-runs its task.\n * Invoked **concurrently with the run** (so the inter-run interval can be\n * measured from when the run *started*, not when it finished) with the single\n * {@link TaskLike}, which carries everything a decision needs:\n * - `task.run()` — the in-flight run, observable via this memoized call (does\n * not re-trigger work), e.g. to read its result for a cadence/stop decision.\n * - `task.previous` — the prior successful run's value (`undefined` on the\n * first), for decisions that compare consecutive results.\n * - `task.signal` — aborts the wait (and the recurrence) when the task is.\n *\n * Resolves `true` to re-run (after whatever delay it owns) or `false` to stop.\n * The runner deals only in this awaitable verdict — *how* the delay is produced\n * (a timer, a frame, an event) and *when* it's measured from live entirely in\n * the reschedule function, so the runner itself knows nothing about time. See\n * `delayedReschedule` for the common timer-based, start-anchored implementation.\n */\nexport type Reschedule<TValue> = (task: TaskLike<TValue>) => PromiseLike<boolean>;\n\n/**\n * A {@link Reschedule} that never recurs — the task runs exactly once. Pass it to\n * a {@link RecurringRunner} for non-recurring, run-once work (e.g. resolving a\n * complete VoD playlist that can never go stale).\n */\nexport const runOnce: Reschedule<unknown> = () => Promise.resolve(false);\n\n/**\n * Runs a task, then re-runs it whenever a {@link Reschedule} function says to,\n * until it says stop (or it's aborted) — the recurring sibling of\n * {@link ConcurrentRunner} / {@link SerialRunner}, and like them it's handed a\n * {@link TaskLike} to run.\n *\n * The runner has no notion of time: it just awaits whatever `reschedule`\n * returns (resolves `true` → re-run; `false` → stop). A `reschedule` is required;\n * pass {@link runOnce} for non-recurring, run-exactly-once work.\n *\n * Single-slot, keyed by task **id**: there is always at most one identified\n * active task for re-running. Scheduling a task whose id matches the active one\n * is a no-op — the existing recurrence keeps running (dedup by id). Scheduling a\n * task with a *different* id aborts the prior task's in-flight run and pending\n * reschedule, then takes over the slot (abort-and-replace) — the right shape\n * when there's one logical unit of recurring work (e.g. reloading the *selected*\n * track's media playlist).\n *\n * Each re-run is a fresh `clone()` of the task (since `Task.run()` is memoized —\n * the same instance won't re-execute), carrying the same id so the slot's\n * identity is stable across cycles. The clone also carries the prior cycle's\n * value forward as `task.previous`. The run function should read any inputs that\n * change between cycles at call time rather than capturing them once.\n *\n * The task is the sole cancellation channel: `abortAll()` aborts the active task,\n * which fires `task.signal` — cancelling both its in-flight run and any pending\n * reschedule delay waiting on that signal. An aborted (or stopped) recurrence\n * frees the slot, so a later schedule of the same id starts fresh.\n */\nexport class RecurringRunner<TValue = unknown> {\n readonly #reschedule: Reschedule<TValue>;\n // The identified active task — the one being (re)run. Held across cycles\n // (including the inter-cycle wait); null once the recurrence stops/aborts. Also\n // serves as the ownership token: a cycle advances or frees the slot only while\n // `#active` still points at its own task (a supersede/abortAll swaps it).\n #active: TaskLike<TValue, unknown> | null = null;\n #destroyed = false;\n\n constructor(reschedule: Reschedule<TValue>) {\n this.#reschedule = reschedule;\n }\n\n /**\n * Run `task` and recur per the `reschedule` verdict, as a single promise.\n * Resolves with the *final* cycle's value when the recurrence stops; **rejects**\n * if a run (or reschedule) genuinely fails — the rejection propagates to the\n * caller, who owns error handling; the runner only frees its slot (no\n * swallowing). The runner's *own* cancellation (abort/supersede/destroy) is not\n * a failure, so an aborted recurrence settles quietly rather than rejecting —\n * callers don't have to `.catch` routine teardown.\n *\n * Each cycle runs the task and consults `reschedule` concurrently (so the delay\n * can be measured from the run's start); when both settle and this cycle still\n * owns the slot, a `true` verdict re-schedules a `clone()` whose promise is\n * *returned* — so the recurrence is the method calling itself, threaded into one\n * promise, no separate loop. The clone shares the id, so the slot's identity is\n * stable across cycles; it's released just before the re-schedule so the call\n * advances rather than dedup-returning.\n *\n * Note: because each cycle's promise adopts the next, the chain retains every\n * prior cycle for the life of the recurrence — bounded for finite recurrences,\n * an unbounded (small per-cycle) cost for a long-lived one (e.g. live reload).\n */\n schedule(task: TaskLike<TValue, unknown>): Promise<TValue> {\n if (this.#destroyed) return Promise.resolve() as Promise<TValue>;\n // Dedup by id: this id is already the active re-run target, so the existing\n // recurrence continues uninterrupted (don't restart it) — hand back its\n // in-flight run.\n if (this.#active?.id === task.id) return this.#active.run();\n // Different id supersedes: abort the prior recurrence, then take over.\n this.#cancel();\n this.#active = task;\n\n // Drive the run (in case `reschedule` doesn't observe it — e.g. `runOnce`)\n // and the verdict together; a rejected run rejects the whole cycle.\n return Promise.all([task.run(), this.#reschedule(task)]).then(\n ([value, again]) => {\n // Only act while we still own the slot — a supersede/abortAll swapped\n // `#active`, in which case this stale cycle does nothing.\n if (this.#active === task && again && !task.signal.aborted) {\n // Release first so the same-id clone advances (isn't dedup-returned),\n // then chain the next cycle into this promise.\n this.#active = null;\n return this.schedule(task.clone());\n }\n if (this.#active === task) this.#active = null; // natural stop / superseded\n return value;\n },\n (error) => {\n // The recurrence ended on a rejection; free the slot if we still own it.\n if (this.#active === task) this.#active = null;\n // The runner's own cancellation isn't a failure — settle quietly so\n // routine teardown (abort/supersede/destroy) needs no caller `.catch`.\n // A genuine run/reschedule failure propagates to the caller.\n if (task.signal.aborted) return undefined as TValue;\n throw error;\n }\n );\n }\n\n #cancel(): void {\n this.#active?.abort();\n this.#active = null;\n }\n\n abortAll(): void {\n this.#cancel();\n }\n\n destroy(): void {\n this.#destroyed = true;\n this.abortAll();\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import { HlsAudioMediaElement, HlsAudioMediaMixin, hlsAudioMediaDefaultProps } from "./playback/adapters/hls-audio/adapter.js";
2
+ import { HlsAudioMedia } from "./playback/adapters/hls-audio/media.js";
3
+ export { HlsAudioMedia, HlsAudioMediaElement, HlsAudioMediaMixin, hlsAudioMediaDefaultProps };
@@ -0,0 +1,3 @@
1
+ import { HlsBackgroundVideoMediaElement, HlsBackgroundVideoMediaMixin, hlsBackgroundVideoMediaDefaultProps } from "./playback/adapters/hls-background-video/adapter.js";
2
+ import { HlsBackgroundVideoMedia } from "./playback/adapters/hls-background-video/media.js";
3
+ export { HlsBackgroundVideoMedia, HlsBackgroundVideoMediaElement, HlsBackgroundVideoMediaMixin, hlsBackgroundVideoMediaDefaultProps };
@@ -0,0 +1,4 @@
1
+ import { HlsVideoMediaElement, HlsVideoMediaMixin, hlsVideoMediaDefaultProps } from "./playback/adapters/hls-video/adapter.js";
2
+ import { HlsVideoMediaMediaTracksMixin } from "./playback/adapters/hls-video/media-tracks.js";
3
+ import { HlsVideoMedia } from "./playback/adapters/hls-video/media.js";
4
+ export { HlsVideoMedia, HlsVideoMediaElement, HlsVideoMediaMediaTracksMixin, HlsVideoMediaMixin, hlsVideoMediaDefaultProps };
@@ -1,6 +1,7 @@
1
+ import { getMediaPlaylistMetadata } from "./media/types/index.js";
2
+ import { SVTA_UNSUPPORTED_PLAYBACK_FEATURE, svtaCategory, svtaIndex } from "./media/errors.js";
1
3
  import { derivePerTypeStartMediaTime, deriveSharedMinStartMediaTime } from "./playback/behaviors/establish-start-media-time.js";
2
- import { createSimpleHlsEngine } from "./playback/engines/hls/engine.js";
3
- import { SimpleHlsMediaElement, SimpleHlsMediaMixin, simpleHlsMediaDefaultProps } from "./playback/engines/hls/adapter.js";
4
- import { createHlsAudioOnlyEngine } from "./playback/engines/hls/engine-audio-only.js";
5
- import { SimpleHlsAudioOnlyMediaElement, SimpleHlsAudioOnlyMediaMixin, simpleHlsAudioOnlyMediaDefaultProps } from "./playback/engines/hls/adapter-audio-only.js";
6
- export { SimpleHlsAudioOnlyMediaElement, SimpleHlsAudioOnlyMediaMixin, SimpleHlsMediaElement, SimpleHlsMediaMixin, createHlsAudioOnlyEngine, createSimpleHlsEngine, derivePerTypeStartMediaTime, deriveSharedMinStartMediaTime, simpleHlsAudioOnlyMediaDefaultProps, simpleHlsMediaDefaultProps };
4
+ import { createHlsVideoEngine } from "./playback/engines/hls/engine.js";
5
+ import { createHlsAudioEngine } from "./playback/engines/hls/engine-audio-only.js";
6
+ import { createBackgroundVideoEngine } from "./playback/engines/hls/engine-background-video.js";
7
+ export { SVTA_UNSUPPORTED_PLAYBACK_FEATURE, createBackgroundVideoEngine, createHlsAudioEngine, createHlsVideoEngine, derivePerTypeStartMediaTime, deriveSharedMinStartMediaTime, getMediaPlaylistMetadata, svtaCategory, svtaIndex };
@@ -1,3 +1,4 @@
1
+ import { getMediaPlaylistMetadata } from "../types/index.js";
1
2
  import { NON_FMP4_CONTAINER_MIMES } from "../hls/parse-media-playlist.js";
2
3
  import { buildMimeCodec, isCodecSupported } from "./mse/mediasource-setup.js";
3
4
  //#region src/media/dom/capabilities.ts
@@ -53,6 +54,7 @@ const codecSupportCache = /* @__PURE__ */ new Map();
53
54
  */
54
55
  const canPlayTrack = (track) => {
55
56
  if (track.mimeType && NON_FMP4_CONTAINER_MIMES.has(track.mimeType)) return false;
57
+ if (getMediaPlaylistMetadata(track)?.encrypted) return false;
56
58
  if (!track.mimeType || !track.codecs?.length) return true;
57
59
  const mimeCodec = buildMimeCodec({
58
60
  mimeType: track.mimeType,
@@ -1 +1 @@
1
- {"version":3,"file":"capabilities.js","names":[],"sources":["../../../../src/media/dom/capabilities.ts"],"sourcesContent":["/**\n * Capability probing — the engine's foundation for asking the browser what it\n * can actually decode before committing a rendition to the pipeline.\n *\n * Today this is the synchronous codec half: `canPlayTrack` answers \"can this\n * environment play this track?\" by building the track's MIME codec string and\n * passing it to `MediaSource.isTypeSupported` (via `isCodecSupported`). It's the\n * DOM implementation of the DOM-free `CanPlayTrack` predicate the\n * track-switching hard-constraint pre-pass consumes — injected through engine\n * config so the (DOM-free) behavior never imports a DOM API directly.\n *\n * Results are memoized by built MIME string: codec support is a pure function\n * of (codec, environment) and never changes after load, so probing is lazy\n * (per candidate, at constraint-apply time) but each unique MIME is asked once.\n *\n * Future cluster-D phases (async `requestMediaKeySystemAccess` key-system\n * probing, `SourceBuffer.changeType()` availability) extend this surface; the\n * async ones land as a state-slot writer behavior rather than a config\n * predicate, since their verdict resolves asynchronously.\n */\n\nimport { NON_FMP4_CONTAINER_MIMES } from '../hls/parse-media-playlist';\nimport type { CanPlayTrack } from '../types';\nimport { buildMimeCodec, isCodecSupported } from './mse/mediasource-setup';\n\nconst codecSupportCache = new Map<string, boolean>();\n\n/**\n * Whether the environment can decode `track`, by codec. Builds the track's\n * MIME codec string and checks `MediaSource.isTypeSupported`, memoized by MIME.\n * A track without enough to probe — no `mimeType`, or no declared `codecs`\n * (CODECS is optional per the HLS spec) — is unprobeable and passes through as\n * playable (`true`) rather than being dropped; the late `createSourceBuffer`\n * check stays as the backstop for those.\n *\n * Detected non-fMP4 containers (`video/mp2t`, `audio/aac`) are asserted\n * unsupported regardless of the probe, so they're pruned before selection\n * (the type makes no pick) instead of failing/stalling deep in the pipeline.\n * Two different reasons, neither UA-based:\n *\n * - **MPEG-TS** can't be played at all here: `isTypeSupported('video/mp2t…')` is\n * a genuine false positive on Chromium (reports `true` but appends produce no\n * buffered range), and this engine has no TS transmux pipeline.\n * - **Raw ADTS AAC** is a *temporary* limitation. The browser genuinely\n * supports it (Chrome/Safari decode `audio/aac`; Firefox doesn't), so it could\n * be made playable — but our segment actors / loading behaviors / append\n * pipeline assume every rendition has an `EXT-X-MAP` init segment (e.g. an\n * `append-init` task with an empty URL, fMP4-shaped append handling). Until\n * that init-segment assumption is removed, ADTS would fetch but never buffer\n * (a silent stall), so we assert it unplayable for now. FOLLOW-UP: drop the\n * init-required assumption in the pipeline and switch this to a bare-MIME\n * probe (`buildMimeCodec` would project `audio/aac` with no codecs) so it\n * plays where the browser supports it.\n *\n * Override via the engine's `canPlayTrack` config when those pipelines land.\n */\nexport const canPlayTrack: CanPlayTrack = (track) => {\n if (track.mimeType && NON_FMP4_CONTAINER_MIMES.has(track.mimeType)) return false;\n if (!track.mimeType || !track.codecs?.length) return true;\n const mimeCodec = buildMimeCodec({ mimeType: track.mimeType, codecs: track.codecs });\n const cached = codecSupportCache.get(mimeCodec);\n if (cached !== undefined) return cached;\n const supported = isCodecSupported(mimeCodec);\n codecSupportCache.set(mimeCodec, supported);\n return supported;\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAyBA,MAAM,oCAAoB,IAAI,IAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BnD,MAAa,gBAA8B,UAAU;CACnD,IAAI,MAAM,YAAY,yBAAyB,IAAI,MAAM,QAAQ,GAAG,OAAO;CAC3E,IAAI,CAAC,MAAM,YAAY,CAAC,MAAM,QAAQ,QAAQ,OAAO;CACrD,MAAM,YAAY,eAAe;EAAE,UAAU,MAAM;EAAU,QAAQ,MAAM;CAAO,CAAC;CACnF,MAAM,SAAS,kBAAkB,IAAI,SAAS;CAC9C,IAAI,WAAW,KAAA,GAAW,OAAO;CACjC,MAAM,YAAY,iBAAiB,SAAS;CAC5C,kBAAkB,IAAI,WAAW,SAAS;CAC1C,OAAO;AACT"}
1
+ {"version":3,"file":"capabilities.js","names":[],"sources":["../../../../src/media/dom/capabilities.ts"],"sourcesContent":["/**\n * Capability probing — the engine's foundation for asking the browser what it\n * can actually decode before committing a rendition to the pipeline.\n *\n * Today this is the synchronous codec half: `canPlayTrack` answers \"can this\n * environment play this track?\" by building the track's MIME codec string and\n * passing it to `MediaSource.isTypeSupported` (via `isCodecSupported`). It's the\n * DOM implementation of the DOM-free `CanPlayTrack` predicate the\n * track-switching hard-constraint pre-pass consumes — injected through engine\n * config so the (DOM-free) behavior never imports a DOM API directly.\n *\n * Results are memoized by built MIME string: codec support is a pure function\n * of (codec, environment) and never changes after load, so probing is lazy\n * (per candidate, at constraint-apply time) but each unique MIME is asked once.\n *\n * Future cluster-D phases (async `requestMediaKeySystemAccess` key-system\n * probing, `SourceBuffer.changeType()` availability) extend this surface; the\n * async ones land as a state-slot writer behavior rather than a config\n * predicate, since their verdict resolves asynchronously.\n */\n\nimport { NON_FMP4_CONTAINER_MIMES } from '../hls/parse-media-playlist';\nimport { type CanPlayTrack, getMediaPlaylistMetadata } from '../types';\nimport { buildMimeCodec, isCodecSupported } from './mse/mediasource-setup';\n\nconst codecSupportCache = new Map<string, boolean>();\n\n/**\n * Whether the environment can decode `track`, by codec. Builds the track's\n * MIME codec string and checks `MediaSource.isTypeSupported`, memoized by MIME.\n * A track without enough to probe — no `mimeType`, or no declared `codecs`\n * (CODECS is optional per the HLS spec) — is unprobeable and passes through as\n * playable (`true`) rather than being dropped; the late `createSourceBuffer`\n * check stays as the backstop for those.\n *\n * Detected non-fMP4 containers (`video/mp2t`, `audio/aac`) are asserted\n * unsupported regardless of the probe, so they're pruned before selection\n * (the type makes no pick) instead of failing/stalling deep in the pipeline.\n * Two different reasons, neither UA-based:\n *\n * - **MPEG-TS** can't be played at all here: `isTypeSupported('video/mp2t…')` is\n * a genuine false positive on Chromium (reports `true` but appends produce no\n * buffered range), and this engine has no TS transmux pipeline.\n * - **Raw ADTS AAC** is a *temporary* limitation. The browser genuinely\n * supports it (Chrome/Safari decode `audio/aac`; Firefox doesn't), so it could\n * be made playable — but our segment actors / loading behaviors / append\n * pipeline assume every rendition has an `EXT-X-MAP` init segment (e.g. an\n * `append-init` task with an empty URL, fMP4-shaped append handling). Until\n * that init-segment assumption is removed, ADTS would fetch but never buffer\n * (a silent stall), so we assert it unplayable for now. FOLLOW-UP: drop the\n * init-required assumption in the pipeline and switch this to a bare-MIME\n * probe (`buildMimeCodec` would project `audio/aac` with no codecs) so it\n * plays where the browser supports it.\n *\n * Override via the engine's `canPlayTrack` config when those pipelines land.\n */\nexport const canPlayTrack: CanPlayTrack = (track) => {\n if (track.mimeType && NON_FMP4_CONTAINER_MIMES.has(track.mimeType)) return false;\n // Encrypted renditions are unplayable *for now* — this engine has no EME /\n // license pipeline, so appending them would fail to decode with nothing to\n // explain it. Pruning them here means a partially-encrypted source still plays\n // its clear renditions, and a fully-encrypted one empties the candidate set\n // (which `track-switching` reports). Remove this when DRM support lands.\n if (getMediaPlaylistMetadata(track)?.encrypted) return false;\n if (!track.mimeType || !track.codecs?.length) return true;\n const mimeCodec = buildMimeCodec({ mimeType: track.mimeType, codecs: track.codecs });\n const cached = codecSupportCache.get(mimeCodec);\n if (cached !== undefined) return cached;\n const supported = isCodecSupported(mimeCodec);\n codecSupportCache.set(mimeCodec, supported);\n return supported;\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAyBA,MAAM,oCAAoB,IAAI,IAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BnD,MAAa,gBAA8B,UAAU;CACnD,IAAI,MAAM,YAAY,yBAAyB,IAAI,MAAM,QAAQ,GAAG,OAAO;CAM3E,IAAI,yBAAyB,KAAK,CAAC,EAAE,WAAW,OAAO;CACvD,IAAI,CAAC,MAAM,YAAY,CAAC,MAAM,QAAQ,QAAQ,OAAO;CACrD,MAAM,YAAY,eAAe;EAAE,UAAU,MAAM;EAAU,QAAQ,MAAM;CAAO,CAAC;CACnF,MAAM,SAAS,kBAAkB,IAAI,SAAS;CAC9C,IAAI,WAAW,KAAA,GAAW,OAAO;CACjC,MAAM,YAAY,iBAAiB,SAAS;CAC5C,kBAAkB,IAAI,WAAW,SAAS;CAC1C,OAAO;AACT"}
@@ -0,0 +1,62 @@
1
+ //#region src/media/errors.ts
2
+ /** SVTA 1 [Media Content] 004 — the video is in a format we can't play (e.g. an MPEG-TS container). */
3
+ const SVTA_UNSUPPORTED_VIDEO_FORMAT = 1004;
4
+ /** SVTA 1 [Media Content] 005 — the audio counterpart of {@link SVTA_UNSUPPORTED_VIDEO_FORMAT}. */
5
+ const SVTA_UNSUPPORTED_AUDIO_FORMAT = 1005;
6
+ /**
7
+ * SVTA 4 [Content Protection] 008 — unsupported or unavailable DRM system. Used
8
+ * for "this source is encrypted and we have no decryption pipeline," which is
9
+ * detection, not a license failure.
10
+ */
11
+ const SVTA_UNSUPPORTED_DRM_SYSTEM = 4008;
12
+ /**
13
+ * SVTA 2 [Playback] 011 — no video track the environment can play. For a source
14
+ * that *has* video renditions where every one was excluded as unplayable.
15
+ */
16
+ const SVTA_NO_SUPPORTED_VIDEO_TRACK = 2011;
17
+ /** SVTA 2 [Playback] 012 — the audio counterpart of {@link SVTA_NO_SUPPORTED_VIDEO_TRACK}. */
18
+ const SVTA_NO_SUPPORTED_AUDIO_TRACK = 2012;
19
+ /**
20
+ * SVTA 99 [Custom] 001 — this engine has no pipeline for something the source
21
+ * requires, so the source is unplayable *here* rather than broken.
22
+ *
23
+ * Custom rather than standard because the standard codes available describe
24
+ * either narrower or wider things. The causes (1004/1005 unsupported format,
25
+ * 4008 unsupported DRM) say what one rendition hit; the verdicts (2011/2012 no
26
+ * supported track) say a type emptied without saying why it's unfixable. And
27
+ * 2039 "Manifest feature unsupported" covers features that are unsupported but
28
+ * still *playable* — LL-HLS degrading to standard live is a 2039 — so
29
+ * overloading it for a fatal condition would make it useless for the notices it
30
+ * belongs on.
31
+ *
32
+ * Index `001`: the spec defines only `99000` (Unknown) for the custom category
33
+ * and leaves the rest to the publisher, so this is the first code we define.
34
+ *
35
+ * Five digits, and deliberately not special-cased anywhere: {@link svtaCategory}
36
+ * and {@link svtaIndex} decompose it correctly by arithmetic alone, because
37
+ * every standard category is below `8000` and custom starts at `99000`.
38
+ */
39
+ const SVTA_UNSUPPORTED_PLAYBACK_FEATURE = 99001;
40
+ /**
41
+ * The error's domain — `code / 1000`, per the spec's "divide by one thousand to
42
+ * obtain the error category". Works uniformly across the four-digit native form
43
+ * and the five-digit form embedding an external standard: `"03404"` is
44
+ * numerically 3404, which decomposes identically. That also makes a numeric code
45
+ * immune to the spec's inconsistent zero-padding (§Approach writes a
46
+ * category-unknown network error as `"0300"` where the error index implies
47
+ * category 3 / index 000).
48
+ */
49
+ function svtaCategory(code) {
50
+ return Math.floor(code / 1e3);
51
+ }
52
+ /**
53
+ * The specific error within its category — `code % 1000`. For a five-digit code
54
+ * this is the embedded external value (an HTTP status, a VAST code).
55
+ */
56
+ function svtaIndex(code) {
57
+ return code % 1e3;
58
+ }
59
+ //#endregion
60
+ export { SVTA_NO_SUPPORTED_AUDIO_TRACK, SVTA_NO_SUPPORTED_VIDEO_TRACK, SVTA_UNSUPPORTED_AUDIO_FORMAT, SVTA_UNSUPPORTED_DRM_SYSTEM, SVTA_UNSUPPORTED_PLAYBACK_FEATURE, SVTA_UNSUPPORTED_VIDEO_FORMAT, svtaCategory, svtaIndex };
61
+
62
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","names":[],"sources":["../../../src/media/errors.ts"],"sourcesContent":["/**\n * SVTA 2070 (Standardized Error Codes) — the vocabulary for identifying a\n * playback failure or notice, independent of how it's transported or who\n * decides what to do about it.\n *\n * A code is a single integer: the leading digit(s) are the **category** (the\n * error's domain) and the trailing three are the **index** (the specific error\n * within it). Four digits for natively-defined errors, five when an external\n * standard is embedded — `\"03404\"` is an HTTP 404 under the network category.\n * Categories are `0` unknown, `1` media content, `2` playback, `3` network,\n * `4` content protection, `5` accessibility, `6` remote play, `7` advertising,\n * `99` custom.\n *\n * Two properties of the spec shape the types here:\n *\n * - **Severity is deliberately not part of a code.** Per §Approach, \"impact\n * varies with player implementation, breaking the consistency of a specific\n * error mapping to single code.\" So an error carries no fatal flag — whether a\n * condition is fatal depends on the composition observing it, and is decided\n * downstream.\n * - **Reporting is partial and stacked** (Principles 5–6). Errors are reported\n * as encountered, most of them non-fatal, and the *sequence* carries causation\n * a single value can't.\n *\n * See `internal/design/spf/features/errors.md`.\n */\n\n/**\n * A reported condition, identified by its SVTA code.\n *\n * Named for the spec rather than the engine because the vocabulary is\n * format- and player-neutral. Not `MediaError` — that name belongs to the\n * `@videojs/media` DOM-facing class this eventually maps *onto*, and the mapping\n * is the point at which severity and user-facing text get decided.\n */\nexport interface SvtaError {\n /** The SVTA code — see {@link svtaCategory} / {@link svtaIndex}. */\n code: number;\n /** Engineer-facing detail. Optional; the code is the identity. */\n message?: string;\n /** Reporter-specific context (track type, url, the constraint that fired). */\n data?: unknown;\n}\n\n/** SVTA 1 [Media Content] 004 — the video is in a format we can't play (e.g. an MPEG-TS container). */\nexport const SVTA_UNSUPPORTED_VIDEO_FORMAT = 1004;\n\n/** SVTA 1 [Media Content] 005 — the audio counterpart of {@link SVTA_UNSUPPORTED_VIDEO_FORMAT}. */\nexport const SVTA_UNSUPPORTED_AUDIO_FORMAT = 1005;\n\n/**\n * SVTA 4 [Content Protection] 008 — unsupported or unavailable DRM system. Used\n * for \"this source is encrypted and we have no decryption pipeline,\" which is\n * detection, not a license failure.\n */\nexport const SVTA_UNSUPPORTED_DRM_SYSTEM = 4008;\n\n/**\n * SVTA 2 [Playback] 011 — no video track the environment can play. For a source\n * that *has* video renditions where every one was excluded as unplayable.\n */\nexport const SVTA_NO_SUPPORTED_VIDEO_TRACK = 2011;\n\n/** SVTA 2 [Playback] 012 — the audio counterpart of {@link SVTA_NO_SUPPORTED_VIDEO_TRACK}. */\nexport const SVTA_NO_SUPPORTED_AUDIO_TRACK = 2012;\n\n/**\n * SVTA 99 [Custom] 001 — this engine has no pipeline for something the source\n * requires, so the source is unplayable *here* rather than broken.\n *\n * Custom rather than standard because the standard codes available describe\n * either narrower or wider things. The causes (1004/1005 unsupported format,\n * 4008 unsupported DRM) say what one rendition hit; the verdicts (2011/2012 no\n * supported track) say a type emptied without saying why it's unfixable. And\n * 2039 \"Manifest feature unsupported\" covers features that are unsupported but\n * still *playable* — LL-HLS degrading to standard live is a 2039 — so\n * overloading it for a fatal condition would make it useless for the notices it\n * belongs on.\n *\n * Index `001`: the spec defines only `99000` (Unknown) for the custom category\n * and leaves the rest to the publisher, so this is the first code we define.\n *\n * Five digits, and deliberately not special-cased anywhere: {@link svtaCategory}\n * and {@link svtaIndex} decompose it correctly by arithmetic alone, because\n * every standard category is below `8000` and custom starts at `99000`.\n */\nexport const SVTA_UNSUPPORTED_PLAYBACK_FEATURE = 99001;\n\n/**\n * The error's domain — `code / 1000`, per the spec's \"divide by one thousand to\n * obtain the error category\". Works uniformly across the four-digit native form\n * and the five-digit form embedding an external standard: `\"03404\"` is\n * numerically 3404, which decomposes identically. That also makes a numeric code\n * immune to the spec's inconsistent zero-padding (§Approach writes a\n * category-unknown network error as `\"0300\"` where the error index implies\n * category 3 / index 000).\n */\nexport function svtaCategory(code: number): number {\n return Math.floor(code / 1000);\n}\n\n/**\n * The specific error within its category — `code % 1000`. For a five-digit code\n * this is the embedded external value (an HTTP status, a VAST code).\n */\nexport function svtaIndex(code: number): number {\n return code % 1000;\n}\n"],"mappings":";;AA6CA,MAAa,gCAAgC;;AAG7C,MAAa,gCAAgC;;;;;;AAO7C,MAAa,8BAA8B;;;;;AAM3C,MAAa,gCAAgC;;AAG7C,MAAa,gCAAgC;;;;;;;;;;;;;;;;;;;;;AAsB7C,MAAa,oCAAoC;;;;;;;;;;AAWjD,SAAgB,aAAa,MAAsB;CACjD,OAAO,KAAK,MAAM,OAAO,GAAI;AAC/B;;;;;AAMA,SAAgB,UAAU,MAAsB;CAC9C,OAAO,OAAO;AAChB"}