mattebox 0.1.0

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 (471) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +106 -0
  3. package/dist/cdn/mattebox.dash-drm.min.js +2 -0
  4. package/dist/cdn/mattebox.dash.min.js +2 -0
  5. package/dist/cdn/mattebox.dual-drm.min.js +2 -0
  6. package/dist/cdn/mattebox.dual-ts-drm.min.js +3 -0
  7. package/dist/cdn/mattebox.dual-ts.min.js +3 -0
  8. package/dist/cdn/mattebox.dual.min.js +2 -0
  9. package/dist/cdn/mattebox.hls-drm.min.js +2 -0
  10. package/dist/cdn/mattebox.hls-ts-drm.min.js +3 -0
  11. package/dist/cdn/mattebox.hls-ts.min.js +3 -0
  12. package/dist/cdn/mattebox.hls.min.js +2 -0
  13. package/dist/cdn/mattebox.kernel.min.js +1 -0
  14. package/dist/cdn/mattebox.min.js +6 -0
  15. package/dist/cdn/transmux.worker.js +1 -0
  16. package/dist/containers/adts.d.ts +33 -0
  17. package/dist/containers/adts.d.ts.map +1 -0
  18. package/dist/containers/adts.js +66 -0
  19. package/dist/containers/adts.js.map +1 -0
  20. package/dist/containers/captions.d.ts +27 -0
  21. package/dist/containers/captions.d.ts.map +1 -0
  22. package/dist/containers/captions.js +36 -0
  23. package/dist/containers/captions.js.map +1 -0
  24. package/dist/containers/codec-probe/index.d.ts +35 -0
  25. package/dist/containers/codec-probe/index.d.ts.map +1 -0
  26. package/dist/containers/codec-probe/index.js +222 -0
  27. package/dist/containers/codec-probe/index.js.map +1 -0
  28. package/dist/containers/fmp4/writer.d.ts +62 -0
  29. package/dist/containers/fmp4/writer.d.ts.map +1 -0
  30. package/dist/containers/fmp4/writer.js +201 -0
  31. package/dist/containers/fmp4/writer.js.map +1 -0
  32. package/dist/containers/id3.d.ts +25 -0
  33. package/dist/containers/id3.d.ts.map +1 -0
  34. package/dist/containers/id3.js +75 -0
  35. package/dist/containers/id3.js.map +1 -0
  36. package/dist/containers/mp4-box/index.d.ts +79 -0
  37. package/dist/containers/mp4-box/index.d.ts.map +1 -0
  38. package/dist/containers/mp4-box/index.js +231 -0
  39. package/dist/containers/mp4-box/index.js.map +1 -0
  40. package/dist/containers/packed-audio/index.d.ts +7 -0
  41. package/dist/containers/packed-audio/index.d.ts.map +1 -0
  42. package/dist/containers/packed-audio/index.js +63 -0
  43. package/dist/containers/packed-audio/index.js.map +1 -0
  44. package/dist/containers/sei.d.ts +26 -0
  45. package/dist/containers/sei.d.ts.map +1 -0
  46. package/dist/containers/sei.js +100 -0
  47. package/dist/containers/sei.js.map +1 -0
  48. package/dist/containers/ts-transmux/demux.d.ts +49 -0
  49. package/dist/containers/ts-transmux/demux.d.ts.map +1 -0
  50. package/dist/containers/ts-transmux/demux.js +222 -0
  51. package/dist/containers/ts-transmux/demux.js.map +1 -0
  52. package/dist/containers/ts-transmux/h264.d.ts +48 -0
  53. package/dist/containers/ts-transmux/h264.d.ts.map +1 -0
  54. package/dist/containers/ts-transmux/h264.js +211 -0
  55. package/dist/containers/ts-transmux/h264.js.map +1 -0
  56. package/dist/containers/ts-transmux/index.d.ts +17 -0
  57. package/dist/containers/ts-transmux/index.d.ts.map +1 -0
  58. package/dist/containers/ts-transmux/index.js +34 -0
  59. package/dist/containers/ts-transmux/index.js.map +1 -0
  60. package/dist/containers/ts-transmux/runner.d.ts +23 -0
  61. package/dist/containers/ts-transmux/runner.d.ts.map +1 -0
  62. package/dist/containers/ts-transmux/runner.js +99 -0
  63. package/dist/containers/ts-transmux/runner.js.map +1 -0
  64. package/dist/containers/ts-transmux/transmux.d.ts +24 -0
  65. package/dist/containers/ts-transmux/transmux.d.ts.map +1 -0
  66. package/dist/containers/ts-transmux/transmux.js +212 -0
  67. package/dist/containers/ts-transmux/transmux.js.map +1 -0
  68. package/dist/containers/ts-transmux/transmux.worker.d.ts +2 -0
  69. package/dist/containers/ts-transmux/transmux.worker.d.ts.map +1 -0
  70. package/dist/containers/ts-transmux/transmux.worker.js +19 -0
  71. package/dist/containers/ts-transmux/transmux.worker.js.map +1 -0
  72. package/dist/es2015/_virtual/_@oxc-project_runtime@0.147.0/helpers/esm/asyncToGenerator.js +27 -0
  73. package/dist/es2015/_virtual/_@oxc-project_runtime@0.147.0/helpers/esm/defineProperty.js +12 -0
  74. package/dist/es2015/_virtual/_@oxc-project_runtime@0.147.0/helpers/esm/objectSpread2.js +25 -0
  75. package/dist/es2015/_virtual/_@oxc-project_runtime@0.147.0/helpers/esm/objectWithoutProperties.js +13 -0
  76. package/dist/es2015/_virtual/_@oxc-project_runtime@0.147.0/helpers/esm/objectWithoutPropertiesLoose.js +12 -0
  77. package/dist/es2015/_virtual/_@oxc-project_runtime@0.147.0/helpers/esm/toPrimitive.js +14 -0
  78. package/dist/es2015/_virtual/_@oxc-project_runtime@0.147.0/helpers/esm/toPropertyKey.js +9 -0
  79. package/dist/es2015/_virtual/_@oxc-project_runtime@0.147.0/helpers/esm/typeof.js +11 -0
  80. package/dist/es2015/containers/adts.js +77 -0
  81. package/dist/es2015/containers/captions.js +36 -0
  82. package/dist/es2015/containers/codec-probe/index.js +220 -0
  83. package/dist/es2015/containers/fmp4/writer.js +165 -0
  84. package/dist/es2015/containers/id3.js +72 -0
  85. package/dist/es2015/containers/mp4-box/index.js +209 -0
  86. package/dist/es2015/containers/packed-audio/index.js +67 -0
  87. package/dist/es2015/containers/sei.js +79 -0
  88. package/dist/es2015/containers/ts-transmux/demux.js +211 -0
  89. package/dist/es2015/containers/ts-transmux/h264.js +204 -0
  90. package/dist/es2015/containers/ts-transmux/index.js +37 -0
  91. package/dist/es2015/containers/ts-transmux/runner.js +95 -0
  92. package/dist/es2015/containers/ts-transmux/transmux.js +210 -0
  93. package/dist/es2015/containers/ts-transmux/transmux.worker.js +26 -0
  94. package/dist/es2015/index.js +356 -0
  95. package/dist/es2015/kernel/append-queue.js +148 -0
  96. package/dist/es2015/kernel/bus.js +134 -0
  97. package/dist/es2015/kernel/context.js +92 -0
  98. package/dist/es2015/kernel/effects.js +62 -0
  99. package/dist/es2015/kernel/evictor.js +67 -0
  100. package/dist/es2015/kernel/lifecycle.js +122 -0
  101. package/dist/es2015/kernel/loader.js +108 -0
  102. package/dist/es2015/kernel/mime.js +20 -0
  103. package/dist/es2015/kernel/mse.js +426 -0
  104. package/dist/es2015/kernel/reducer.js +1051 -0
  105. package/dist/es2015/kernel/rendition-select.js +263 -0
  106. package/dist/es2015/kernel/scheduler.js +82 -0
  107. package/dist/es2015/kernel/sinks/metadata-sink.js +7 -0
  108. package/dist/es2015/kernel/sinks/mse-sink.js +31 -0
  109. package/dist/es2015/kernel/sinks/text-track-sink.js +167 -0
  110. package/dist/es2015/kernel/timeline.js +127 -0
  111. package/dist/es2015/kernel/trace.js +134 -0
  112. package/dist/es2015/kernel/track-registry.js +47 -0
  113. package/dist/es2015/kernel/transport.js +242 -0
  114. package/dist/es2015/kernel/watchdog.js +38 -0
  115. package/dist/es2015/presets/dash/index.js +9 -0
  116. package/dist/es2015/presets/dash-drm/index.js +13 -0
  117. package/dist/es2015/presets/define.js +45 -0
  118. package/dist/es2015/presets/dual/index.js +14 -0
  119. package/dist/es2015/presets/dual-drm/index.js +14 -0
  120. package/dist/es2015/presets/dual-ts/index.js +14 -0
  121. package/dist/es2015/presets/dual-ts-drm/index.js +15 -0
  122. package/dist/es2015/presets/full/index.js +28 -0
  123. package/dist/es2015/presets/hls/index.js +9 -0
  124. package/dist/es2015/presets/hls-drm/index.js +13 -0
  125. package/dist/es2015/presets/hls-ts/index.js +13 -0
  126. package/dist/es2015/presets/hls-ts-drm/index.js +14 -0
  127. package/dist/es2015/presets/kernel/index.js +10 -0
  128. package/dist/es2015/presets/storage.js +24 -0
  129. package/dist/es2015/presets/tiers.js +93 -0
  130. package/dist/es2015/protocols/dash-cmaf/index.js +161 -0
  131. package/dist/es2015/protocols/dash-cmaf/parse.js +491 -0
  132. package/dist/es2015/protocols/dash-live/index.js +192 -0
  133. package/dist/es2015/protocols/hls-cmaf/index.js +168 -0
  134. package/dist/es2015/protocols/hls-cmaf/lexer.js +72 -0
  135. package/dist/es2015/protocols/hls-cmaf/parse.js +457 -0
  136. package/dist/es2015/protocols/hls-live/index.js +296 -0
  137. package/dist/es2015/protocols/live-shared.js +29 -0
  138. package/dist/es2015/stages/abr/index.js +126 -0
  139. package/dist/es2015/stages/abr-cap-size/index.js +22 -0
  140. package/dist/es2015/stages/abr-persist/index.js +52 -0
  141. package/dist/es2015/stages/aes-128/index.js +76 -0
  142. package/dist/es2015/stages/alt-audio/index.js +80 -0
  143. package/dist/es2015/stages/cmaf-timing/index.js +55 -0
  144. package/dist/es2015/stages/cmcd/index.js +70 -0
  145. package/dist/es2015/stages/codec-probe/index.js +65 -0
  146. package/dist/es2015/stages/codec-switch/index.js +48 -0
  147. package/dist/es2015/stages/content-steering/index.js +179 -0
  148. package/dist/es2015/stages/drm-shared.js +18 -0
  149. package/dist/es2015/stages/eme-cenc/index.js +46 -0
  150. package/dist/es2015/stages/eme-core/index.js +237 -0
  151. package/dist/es2015/stages/eme-fairplay/index.js +52 -0
  152. package/dist/es2015/stages/meta-id3/index.js +37 -0
  153. package/dist/es2015/stages/mp4-box/index.js +27 -0
  154. package/dist/es2015/stages/nal-scan/index.js +121 -0
  155. package/dist/es2015/stages/pdt/index.js +34 -0
  156. package/dist/es2015/stages/recovery/index.js +283 -0
  157. package/dist/es2015/stages/text-cea608/decode.js +201 -0
  158. package/dist/es2015/stages/text-cea608/index.js +71 -0
  159. package/dist/es2015/stages/text-webvtt/index.js +82 -0
  160. package/dist/es2015/stages/text-webvtt/parse.js +60 -0
  161. package/dist/es2015/stages/text-webvtt-segmented/index.js +57 -0
  162. package/dist/es2015/stages/thumbnails/index.js +73 -0
  163. package/dist/index.d.ts +32 -0
  164. package/dist/index.d.ts.map +1 -0
  165. package/dist/index.js +423 -0
  166. package/dist/index.js.map +1 -0
  167. package/dist/kernel/append-queue.d.ts +47 -0
  168. package/dist/kernel/append-queue.d.ts.map +1 -0
  169. package/dist/kernel/append-queue.js +180 -0
  170. package/dist/kernel/append-queue.js.map +1 -0
  171. package/dist/kernel/bus.d.ts +50 -0
  172. package/dist/kernel/bus.d.ts.map +1 -0
  173. package/dist/kernel/bus.js +147 -0
  174. package/dist/kernel/bus.js.map +1 -0
  175. package/dist/kernel/context.d.ts +45 -0
  176. package/dist/kernel/context.d.ts.map +1 -0
  177. package/dist/kernel/context.js +116 -0
  178. package/dist/kernel/context.js.map +1 -0
  179. package/dist/kernel/effects.d.ts +33 -0
  180. package/dist/kernel/effects.d.ts.map +1 -0
  181. package/dist/kernel/effects.js +65 -0
  182. package/dist/kernel/effects.js.map +1 -0
  183. package/dist/kernel/evictor.d.ts +46 -0
  184. package/dist/kernel/evictor.d.ts.map +1 -0
  185. package/dist/kernel/evictor.js +92 -0
  186. package/dist/kernel/evictor.js.map +1 -0
  187. package/dist/kernel/lifecycle.d.ts +41 -0
  188. package/dist/kernel/lifecycle.d.ts.map +1 -0
  189. package/dist/kernel/lifecycle.js +129 -0
  190. package/dist/kernel/lifecycle.js.map +1 -0
  191. package/dist/kernel/loader.d.ts +28 -0
  192. package/dist/kernel/loader.d.ts.map +1 -0
  193. package/dist/kernel/loader.js +126 -0
  194. package/dist/kernel/loader.js.map +1 -0
  195. package/dist/kernel/mime.d.ts +13 -0
  196. package/dist/kernel/mime.d.ts.map +1 -0
  197. package/dist/kernel/mime.js +18 -0
  198. package/dist/kernel/mime.js.map +1 -0
  199. package/dist/kernel/mse.d.ts +62 -0
  200. package/dist/kernel/mse.d.ts.map +1 -0
  201. package/dist/kernel/mse.js +490 -0
  202. package/dist/kernel/mse.js.map +1 -0
  203. package/dist/kernel/reducer.d.ts +39 -0
  204. package/dist/kernel/reducer.d.ts.map +1 -0
  205. package/dist/kernel/reducer.js +1316 -0
  206. package/dist/kernel/reducer.js.map +1 -0
  207. package/dist/kernel/rendition-select.d.ts +102 -0
  208. package/dist/kernel/rendition-select.d.ts.map +1 -0
  209. package/dist/kernel/rendition-select.js +295 -0
  210. package/dist/kernel/rendition-select.js.map +1 -0
  211. package/dist/kernel/scheduler.d.ts +57 -0
  212. package/dist/kernel/scheduler.d.ts.map +1 -0
  213. package/dist/kernel/scheduler.js +98 -0
  214. package/dist/kernel/scheduler.js.map +1 -0
  215. package/dist/kernel/sinks/metadata-sink.d.ts +7 -0
  216. package/dist/kernel/sinks/metadata-sink.d.ts.map +1 -0
  217. package/dist/kernel/sinks/metadata-sink.js +5 -0
  218. package/dist/kernel/sinks/metadata-sink.js.map +1 -0
  219. package/dist/kernel/sinks/mse-sink.d.ts +17 -0
  220. package/dist/kernel/sinks/mse-sink.d.ts.map +1 -0
  221. package/dist/kernel/sinks/mse-sink.js +20 -0
  222. package/dist/kernel/sinks/mse-sink.js.map +1 -0
  223. package/dist/kernel/sinks/text-track-sink.d.ts +45 -0
  224. package/dist/kernel/sinks/text-track-sink.d.ts.map +1 -0
  225. package/dist/kernel/sinks/text-track-sink.js +184 -0
  226. package/dist/kernel/sinks/text-track-sink.js.map +1 -0
  227. package/dist/kernel/timeline.d.ts +57 -0
  228. package/dist/kernel/timeline.d.ts.map +1 -0
  229. package/dist/kernel/timeline.js +203 -0
  230. package/dist/kernel/timeline.js.map +1 -0
  231. package/dist/kernel/trace.d.ts +42 -0
  232. package/dist/kernel/trace.d.ts.map +1 -0
  233. package/dist/kernel/trace.js +165 -0
  234. package/dist/kernel/trace.js.map +1 -0
  235. package/dist/kernel/track-registry.d.ts +22 -0
  236. package/dist/kernel/track-registry.d.ts.map +1 -0
  237. package/dist/kernel/track-registry.js +42 -0
  238. package/dist/kernel/track-registry.js.map +1 -0
  239. package/dist/kernel/transport.d.ts +90 -0
  240. package/dist/kernel/transport.d.ts.map +1 -0
  241. package/dist/kernel/transport.js +256 -0
  242. package/dist/kernel/transport.js.map +1 -0
  243. package/dist/kernel/watchdog.d.ts +35 -0
  244. package/dist/kernel/watchdog.d.ts.map +1 -0
  245. package/dist/kernel/watchdog.js +43 -0
  246. package/dist/kernel/watchdog.js.map +1 -0
  247. package/dist/presets/dash/index.d.ts +4 -0
  248. package/dist/presets/dash/index.d.ts.map +1 -0
  249. package/dist/presets/dash/index.js +8 -0
  250. package/dist/presets/dash/index.js.map +1 -0
  251. package/dist/presets/dash-drm/index.d.ts +4 -0
  252. package/dist/presets/dash-drm/index.d.ts.map +1 -0
  253. package/dist/presets/dash-drm/index.js +8 -0
  254. package/dist/presets/dash-drm/index.js.map +1 -0
  255. package/dist/presets/define.d.ts +36 -0
  256. package/dist/presets/define.d.ts.map +1 -0
  257. package/dist/presets/define.js +39 -0
  258. package/dist/presets/define.js.map +1 -0
  259. package/dist/presets/dual/index.d.ts +4 -0
  260. package/dist/presets/dual/index.d.ts.map +1 -0
  261. package/dist/presets/dual/index.js +9 -0
  262. package/dist/presets/dual/index.js.map +1 -0
  263. package/dist/presets/dual-drm/index.d.ts +4 -0
  264. package/dist/presets/dual-drm/index.d.ts.map +1 -0
  265. package/dist/presets/dual-drm/index.js +13 -0
  266. package/dist/presets/dual-drm/index.js.map +1 -0
  267. package/dist/presets/dual-ts/index.d.ts +4 -0
  268. package/dist/presets/dual-ts/index.d.ts.map +1 -0
  269. package/dist/presets/dual-ts/index.js +8 -0
  270. package/dist/presets/dual-ts/index.js.map +1 -0
  271. package/dist/presets/dual-ts-drm/index.d.ts +4 -0
  272. package/dist/presets/dual-ts-drm/index.d.ts.map +1 -0
  273. package/dist/presets/dual-ts-drm/index.js +14 -0
  274. package/dist/presets/dual-ts-drm/index.js.map +1 -0
  275. package/dist/presets/full/index.d.ts +4 -0
  276. package/dist/presets/full/index.d.ts.map +1 -0
  277. package/dist/presets/full/index.js +27 -0
  278. package/dist/presets/full/index.js.map +1 -0
  279. package/dist/presets/hls/index.d.ts +4 -0
  280. package/dist/presets/hls/index.d.ts.map +1 -0
  281. package/dist/presets/hls/index.js +8 -0
  282. package/dist/presets/hls/index.js.map +1 -0
  283. package/dist/presets/hls-drm/index.d.ts +4 -0
  284. package/dist/presets/hls-drm/index.d.ts.map +1 -0
  285. package/dist/presets/hls-drm/index.js +8 -0
  286. package/dist/presets/hls-drm/index.js.map +1 -0
  287. package/dist/presets/hls-ts/index.d.ts +4 -0
  288. package/dist/presets/hls-ts/index.d.ts.map +1 -0
  289. package/dist/presets/hls-ts/index.js +8 -0
  290. package/dist/presets/hls-ts/index.js.map +1 -0
  291. package/dist/presets/hls-ts-drm/index.d.ts +4 -0
  292. package/dist/presets/hls-ts-drm/index.d.ts.map +1 -0
  293. package/dist/presets/hls-ts-drm/index.js +13 -0
  294. package/dist/presets/hls-ts-drm/index.js.map +1 -0
  295. package/dist/presets/kernel/index.d.ts +4 -0
  296. package/dist/presets/kernel/index.d.ts.map +1 -0
  297. package/dist/presets/kernel/index.js +9 -0
  298. package/dist/presets/kernel/index.js.map +1 -0
  299. package/dist/presets/storage.d.ts +9 -0
  300. package/dist/presets/storage.d.ts.map +1 -0
  301. package/dist/presets/storage.js +24 -0
  302. package/dist/presets/storage.js.map +1 -0
  303. package/dist/presets/tiers.d.ts +22 -0
  304. package/dist/presets/tiers.d.ts.map +1 -0
  305. package/dist/presets/tiers.js +78 -0
  306. package/dist/presets/tiers.js.map +1 -0
  307. package/dist/protocols/dash-cmaf/index.d.ts +20 -0
  308. package/dist/protocols/dash-cmaf/index.d.ts.map +1 -0
  309. package/dist/protocols/dash-cmaf/index.js +176 -0
  310. package/dist/protocols/dash-cmaf/index.js.map +1 -0
  311. package/dist/protocols/dash-cmaf/parse.d.ts +24 -0
  312. package/dist/protocols/dash-cmaf/parse.d.ts.map +1 -0
  313. package/dist/protocols/dash-cmaf/parse.js +586 -0
  314. package/dist/protocols/dash-cmaf/parse.js.map +1 -0
  315. package/dist/protocols/dash-live/index.d.ts +4 -0
  316. package/dist/protocols/dash-live/index.d.ts.map +1 -0
  317. package/dist/protocols/dash-live/index.js +188 -0
  318. package/dist/protocols/dash-live/index.js.map +1 -0
  319. package/dist/protocols/hls-cmaf/index.d.ts +17 -0
  320. package/dist/protocols/hls-cmaf/index.d.ts.map +1 -0
  321. package/dist/protocols/hls-cmaf/index.js +189 -0
  322. package/dist/protocols/hls-cmaf/index.js.map +1 -0
  323. package/dist/protocols/hls-cmaf/lexer.d.ts +25 -0
  324. package/dist/protocols/hls-cmaf/lexer.d.ts.map +1 -0
  325. package/dist/protocols/hls-cmaf/lexer.js +81 -0
  326. package/dist/protocols/hls-cmaf/lexer.js.map +1 -0
  327. package/dist/protocols/hls-cmaf/parse.d.ts +57 -0
  328. package/dist/protocols/hls-cmaf/parse.d.ts.map +1 -0
  329. package/dist/protocols/hls-cmaf/parse.js +572 -0
  330. package/dist/protocols/hls-cmaf/parse.js.map +1 -0
  331. package/dist/protocols/hls-live/index.d.ts +4 -0
  332. package/dist/protocols/hls-live/index.d.ts.map +1 -0
  333. package/dist/protocols/hls-live/index.js +309 -0
  334. package/dist/protocols/hls-live/index.js.map +1 -0
  335. package/dist/protocols/live-shared.d.ts +21 -0
  336. package/dist/protocols/live-shared.d.ts.map +1 -0
  337. package/dist/protocols/live-shared.js +32 -0
  338. package/dist/protocols/live-shared.js.map +1 -0
  339. package/dist/stages/abr/index.d.ts +16 -0
  340. package/dist/stages/abr/index.d.ts.map +1 -0
  341. package/dist/stages/abr/index.js +176 -0
  342. package/dist/stages/abr/index.js.map +1 -0
  343. package/dist/stages/abr-cap-size/index.d.ts +8 -0
  344. package/dist/stages/abr-cap-size/index.d.ts.map +1 -0
  345. package/dist/stages/abr-cap-size/index.js +21 -0
  346. package/dist/stages/abr-cap-size/index.js.map +1 -0
  347. package/dist/stages/abr-persist/index.d.ts +8 -0
  348. package/dist/stages/abr-persist/index.d.ts.map +1 -0
  349. package/dist/stages/abr-persist/index.js +44 -0
  350. package/dist/stages/abr-persist/index.js.map +1 -0
  351. package/dist/stages/aes-128/index.d.ts +15 -0
  352. package/dist/stages/aes-128/index.d.ts.map +1 -0
  353. package/dist/stages/aes-128/index.js +78 -0
  354. package/dist/stages/aes-128/index.js.map +1 -0
  355. package/dist/stages/alt-audio/index.d.ts +4 -0
  356. package/dist/stages/alt-audio/index.d.ts.map +1 -0
  357. package/dist/stages/alt-audio/index.js +90 -0
  358. package/dist/stages/alt-audio/index.js.map +1 -0
  359. package/dist/stages/cmaf-timing/index.d.ts +3 -0
  360. package/dist/stages/cmaf-timing/index.d.ts.map +1 -0
  361. package/dist/stages/cmaf-timing/index.js +56 -0
  362. package/dist/stages/cmaf-timing/index.js.map +1 -0
  363. package/dist/stages/cmcd/index.d.ts +22 -0
  364. package/dist/stages/cmcd/index.d.ts.map +1 -0
  365. package/dist/stages/cmcd/index.js +92 -0
  366. package/dist/stages/cmcd/index.js.map +1 -0
  367. package/dist/stages/codec-probe/index.d.ts +9 -0
  368. package/dist/stages/codec-probe/index.d.ts.map +1 -0
  369. package/dist/stages/codec-probe/index.js +68 -0
  370. package/dist/stages/codec-probe/index.js.map +1 -0
  371. package/dist/stages/codec-switch/index.d.ts +6 -0
  372. package/dist/stages/codec-switch/index.d.ts.map +1 -0
  373. package/dist/stages/codec-switch/index.js +59 -0
  374. package/dist/stages/codec-switch/index.js.map +1 -0
  375. package/dist/stages/content-steering/index.d.ts +4 -0
  376. package/dist/stages/content-steering/index.d.ts.map +1 -0
  377. package/dist/stages/content-steering/index.js +174 -0
  378. package/dist/stages/content-steering/index.js.map +1 -0
  379. package/dist/stages/drm-shared.d.ts +46 -0
  380. package/dist/stages/drm-shared.d.ts.map +1 -0
  381. package/dist/stages/drm-shared.js +28 -0
  382. package/dist/stages/drm-shared.js.map +1 -0
  383. package/dist/stages/eme-cenc/index.d.ts +12 -0
  384. package/dist/stages/eme-cenc/index.d.ts.map +1 -0
  385. package/dist/stages/eme-cenc/index.js +53 -0
  386. package/dist/stages/eme-cenc/index.js.map +1 -0
  387. package/dist/stages/eme-core/index.d.ts +29 -0
  388. package/dist/stages/eme-core/index.d.ts.map +1 -0
  389. package/dist/stages/eme-core/index.js +244 -0
  390. package/dist/stages/eme-core/index.js.map +1 -0
  391. package/dist/stages/eme-fairplay/index.d.ts +20 -0
  392. package/dist/stages/eme-fairplay/index.d.ts.map +1 -0
  393. package/dist/stages/eme-fairplay/index.js +59 -0
  394. package/dist/stages/eme-fairplay/index.js.map +1 -0
  395. package/dist/stages/meta-id3/index.d.ts +14 -0
  396. package/dist/stages/meta-id3/index.d.ts.map +1 -0
  397. package/dist/stages/meta-id3/index.js +30 -0
  398. package/dist/stages/meta-id3/index.js.map +1 -0
  399. package/dist/stages/mp4-box/index.d.ts +3 -0
  400. package/dist/stages/mp4-box/index.d.ts.map +1 -0
  401. package/dist/stages/mp4-box/index.js +19 -0
  402. package/dist/stages/mp4-box/index.js.map +1 -0
  403. package/dist/stages/nal-scan/index.d.ts +3 -0
  404. package/dist/stages/nal-scan/index.d.ts.map +1 -0
  405. package/dist/stages/nal-scan/index.js +123 -0
  406. package/dist/stages/nal-scan/index.js.map +1 -0
  407. package/dist/stages/pdt/index.d.ts +14 -0
  408. package/dist/stages/pdt/index.d.ts.map +1 -0
  409. package/dist/stages/pdt/index.js +34 -0
  410. package/dist/stages/pdt/index.js.map +1 -0
  411. package/dist/stages/recovery/index.d.ts +16 -0
  412. package/dist/stages/recovery/index.d.ts.map +1 -0
  413. package/dist/stages/recovery/index.js +270 -0
  414. package/dist/stages/recovery/index.js.map +1 -0
  415. package/dist/stages/text-cea608/decode.d.ts +44 -0
  416. package/dist/stages/text-cea608/decode.d.ts.map +1 -0
  417. package/dist/stages/text-cea608/decode.js +225 -0
  418. package/dist/stages/text-cea608/decode.js.map +1 -0
  419. package/dist/stages/text-cea608/index.d.ts +3 -0
  420. package/dist/stages/text-cea608/index.d.ts.map +1 -0
  421. package/dist/stages/text-cea608/index.js +84 -0
  422. package/dist/stages/text-cea608/index.js.map +1 -0
  423. package/dist/stages/text-webvtt/index.d.ts +3 -0
  424. package/dist/stages/text-webvtt/index.d.ts.map +1 -0
  425. package/dist/stages/text-webvtt/index.js +82 -0
  426. package/dist/stages/text-webvtt/index.js.map +1 -0
  427. package/dist/stages/text-webvtt/parse.d.ts +19 -0
  428. package/dist/stages/text-webvtt/parse.d.ts.map +1 -0
  429. package/dist/stages/text-webvtt/parse.js +62 -0
  430. package/dist/stages/text-webvtt/parse.js.map +1 -0
  431. package/dist/stages/text-webvtt-segmented/index.d.ts +5 -0
  432. package/dist/stages/text-webvtt-segmented/index.d.ts.map +1 -0
  433. package/dist/stages/text-webvtt-segmented/index.js +63 -0
  434. package/dist/stages/text-webvtt-segmented/index.js.map +1 -0
  435. package/dist/stages/thumbnails/index.d.ts +34 -0
  436. package/dist/stages/thumbnails/index.d.ts.map +1 -0
  437. package/dist/stages/thumbnails/index.js +58 -0
  438. package/dist/stages/thumbnails/index.js.map +1 -0
  439. package/dist/types/error.d.ts +33 -0
  440. package/dist/types/error.d.ts.map +1 -0
  441. package/dist/types/error.js +7 -0
  442. package/dist/types/error.js.map +1 -0
  443. package/dist/types/facade.d.ts +104 -0
  444. package/dist/types/facade.d.ts.map +1 -0
  445. package/dist/types/facade.js +2 -0
  446. package/dist/types/facade.js.map +1 -0
  447. package/dist/types/ir.d.ts +238 -0
  448. package/dist/types/ir.d.ts.map +1 -0
  449. package/dist/types/ir.js +7 -0
  450. package/dist/types/ir.js.map +1 -0
  451. package/dist/types/kernel.d.ts +217 -0
  452. package/dist/types/kernel.d.ts.map +1 -0
  453. package/dist/types/kernel.js +2 -0
  454. package/dist/types/kernel.js.map +1 -0
  455. package/dist/types/messages.d.ts +288 -0
  456. package/dist/types/messages.d.ts.map +1 -0
  457. package/dist/types/messages.js +2 -0
  458. package/dist/types/messages.js.map +1 -0
  459. package/dist/types/quality.d.ts +108 -0
  460. package/dist/types/quality.d.ts.map +1 -0
  461. package/dist/types/quality.js +2 -0
  462. package/dist/types/quality.js.map +1 -0
  463. package/dist/types/sink.d.ts +27 -0
  464. package/dist/types/sink.d.ts.map +1 -0
  465. package/dist/types/sink.js +2 -0
  466. package/dist/types/sink.js.map +1 -0
  467. package/dist/types/stage.d.ts +139 -0
  468. package/dist/types/stage.d.ts.map +1 -0
  469. package/dist/types/stage.js +2 -0
  470. package/dist/types/stage.js.map +1 -0
  471. package/package.json +132 -0
@@ -0,0 +1,1316 @@
1
+ import { normalizeMimeType } from './mime.js';
2
+ import { canSwitchTo, codecFamily, createArbiter, planPinApply } from './rendition-select.js';
3
+ import { bufferedEndFrom, schedule } from './scheduler.js';
4
+ import { sbIdFor } from './sinks/mse-sink.js';
5
+ import { DEFAULT_TRACE_CAPACITY } from './trace.js';
6
+ /**
7
+ * The default tuning. Every value is overridable through the `config`
8
+ * parameters of `initialState` and `createReducer`; these are starting
9
+ * points, not policy.
10
+ */
11
+ export const DEFAULT_KERNEL_CONFIG = {
12
+ // Small enough to ride out one odd sample.
13
+ ewmaAlpha: 0.2,
14
+ // Large enough that a collapse shows within two or three samples.
15
+ ewmaFastAlpha: 0.6,
16
+ // An init segment or playlist measures connection jitter, not bandwidth.
17
+ ewmaMinSampleBytes: 10_000,
18
+ // Three strikes: enough to ride out one transient, few enough that a
19
+ // structurally unplayable track halts before the loop burns the network.
20
+ bufferErrorLimit: 3,
21
+ // Looser than the error breaker: flushes and seeks legitimately repeat a
22
+ // decision once or twice; six identical ones in a row never happen.
23
+ repeatFetchLimit: 5,
24
+ backBufferSeconds: 30,
25
+ bufferGoalSeconds: 30,
26
+ manifestTimeoutMs: 10_000,
27
+ traceCapacity: DEFAULT_TRACE_CAPACITY,
28
+ // Long enough that a recovery stage's zero-delay commands apply first,
29
+ // short enough that a retry without recovery still feels prompt.
30
+ baseRetryDelayMs: 400,
31
+ mediaTimeNormalized: false,
32
+ };
33
+ export function resolveConfig(config) {
34
+ return { ...DEFAULT_KERNEL_CONFIG, ...config };
35
+ }
36
+ export function initialState(config) {
37
+ const cfg = resolveConfig(config);
38
+ return {
39
+ lifecycle: { phase: 'idle' },
40
+ presentation: null,
41
+ timeline: { periodOffsets: new Map(), discontinuitySeq: 0 },
42
+ buffers: new Map(),
43
+ bufferErrors: new Map(),
44
+ cues: new Map(),
45
+ live: null,
46
+ scheduling: { inflight: new Map(), bufferGoal: cfg.bufferGoalSeconds, tokenSeq: 0 },
47
+ tracks: { active: new Map(), available: [] },
48
+ quality: { version: 0, constraints: new Map(), pinned: null, active: null, appendLog: [] },
49
+ stats: { throughputEwma: 0, throughputFastEwma: 0 },
50
+ playback: { currentTime: 0, buffered: [], seeking: false },
51
+ };
52
+ }
53
+ /** Gap width treated as continuous when measuring buffered spans. */
54
+ const GAP_TOLERANCE = 0.25;
55
+ /** The codecs part of an MSE type string ('video/mp4; codecs="avc1.42c00d"' -> 'avc1.42c00d'). */
56
+ function bufferCodecString(type) {
57
+ const match = /codecs="?([^"]+)"?/.exec(type);
58
+ return match?.[1] ?? null;
59
+ }
60
+ function reject(state, command, reason) {
61
+ return [state, [{ kind: 'emit', event: 'command:rejected', payload: { command, reason } }]];
62
+ }
63
+ /**
64
+ * A manifest that will never yield a presentation. Every path lands here:
65
+ * an adapter's parse failure (MANIFEST_FAILED), a fetch failure on the
66
+ * manifest request, a `mimeType` no adapter accepts, and bytes no adapter
67
+ * claims. One phase transition and one error shape, so an integrator
68
+ * handles a single code per cause.
69
+ */
70
+ function failManifest(state, error, extra = {}) {
71
+ const phase = error.fatal ? 'error' : state.lifecycle.phase;
72
+ return [
73
+ { ...state, lifecycle: { phase } },
74
+ [
75
+ {
76
+ kind: 'emit',
77
+ event: 'error',
78
+ payload: {
79
+ category: error.category,
80
+ code: error.code,
81
+ fatal: error.fatal,
82
+ recoverable: error.recoverable,
83
+ ...extra,
84
+ },
85
+ },
86
+ ],
87
+ ];
88
+ }
89
+ /** The in-flight manifest request a SEGMENT_LOADED fact answers, or null. At most one is ever in flight. */
90
+ function manifestRequestFor(state, msg) {
91
+ if (msg.trackId !== 'manifest')
92
+ return null;
93
+ for (const request of state.scheduling.inflight.values()) {
94
+ if (request.trackId === 'manifest' &&
95
+ (msg.token === undefined || msg.token === request.token)) {
96
+ return request;
97
+ }
98
+ }
99
+ return null;
100
+ }
101
+ function bumped(quality) {
102
+ return { ...quality, version: quality.version + 1 };
103
+ }
104
+ /**
105
+ * Forgets the last fetch decision. A pin or a seek legitimately makes the
106
+ * scheduler repeat itself; only unprompted repetition means a stuck loop.
107
+ */
108
+ function clearRepeat(scheduling) {
109
+ if (scheduling.repeat === undefined)
110
+ return scheduling;
111
+ const { repeat: _repeat, ...rest } = scheduling;
112
+ return rest;
113
+ }
114
+ function ewma(previous, sample, alpha) {
115
+ return previous === 0 ? sample : alpha * sample + (1 - alpha) * previous;
116
+ }
117
+ /** Companion groups present, as `contentType:groupId`, for the coupling filter. */
118
+ function availableGroups(state) {
119
+ const groups = new Set();
120
+ for (const period of state.presentation?.periods ?? []) {
121
+ for (const track of period.tracks) {
122
+ if (track.contentType !== 'audio' && track.contentType !== 'text')
123
+ continue;
124
+ const colon = track.id.indexOf(':');
125
+ const group = colon === -1 ? track.id : track.id.slice(0, colon);
126
+ groups.add(`${track.contentType}:${group}`);
127
+ }
128
+ }
129
+ return groups;
130
+ }
131
+ /** Merges one span into a sorted coverage list, coalescing overlaps. */
132
+ function mergeCoverage(coverage, added) {
133
+ const merged = [];
134
+ let pending = added;
135
+ for (const range of coverage) {
136
+ if (range.end < pending.start || range.start > pending.end) {
137
+ merged.push(range);
138
+ }
139
+ else {
140
+ pending = {
141
+ start: Math.min(range.start, pending.start),
142
+ end: Math.max(range.end, pending.end),
143
+ };
144
+ }
145
+ }
146
+ merged.push(pending);
147
+ return merged.sort((a, b) => a.start - b.start);
148
+ }
149
+ function findTrack(presentation, trackId) {
150
+ if (presentation === null)
151
+ return null;
152
+ for (const period of presentation.periods) {
153
+ for (const track of period.tracks) {
154
+ if (track.id === trackId)
155
+ return track;
156
+ }
157
+ }
158
+ return null;
159
+ }
160
+ function findTrackSite(presentation, trackId) {
161
+ for (const period of presentation.periods) {
162
+ for (const track of period.tracks) {
163
+ if (track.id === trackId)
164
+ return { track, period };
165
+ }
166
+ }
167
+ return null;
168
+ }
169
+ function findRendition(presentation, renditionId) {
170
+ if (presentation === null)
171
+ return null;
172
+ for (const period of presentation.periods) {
173
+ for (const track of period.tracks) {
174
+ for (const rendition of track.renditions) {
175
+ if (rendition.id === renditionId)
176
+ return { rendition, track, period };
177
+ }
178
+ }
179
+ }
180
+ return null;
181
+ }
182
+ /**
183
+ * Whether a completed request feeds the throughput estimate: only media of
184
+ * the lead track (video where there is one, else audio). An audio segment
185
+ * beside video, a text cue, or a playlist is a small transfer whose time
186
+ * is latency, not bandwidth.
187
+ */
188
+ function samplesThroughput(state, request) {
189
+ const lead = state.tracks.active.get('video') ?? state.tracks.active.get('audio');
190
+ return lead === undefined ? request.renditionId === undefined : request.trackId === lead;
191
+ }
192
+ /** Abort effects for every matching in-flight request, plus the state with them removed. */
193
+ function abortInflight(state, trackId) {
194
+ const effects = [];
195
+ const inflight = new Map(state.scheduling.inflight);
196
+ for (const [token, request] of state.scheduling.inflight) {
197
+ if (trackId !== undefined && request.trackId !== trackId)
198
+ continue;
199
+ effects.push({ kind: 'abort', token });
200
+ inflight.delete(token);
201
+ }
202
+ if (effects.length === 0)
203
+ return [state, []];
204
+ return [{ ...state, scheduling: { ...state.scheduling, inflight } }, effects];
205
+ }
206
+ function reduceCommand(state, msg, cfg, hooks) {
207
+ switch (msg.type) {
208
+ case 'ATTACH': {
209
+ if (state.lifecycle.phase !== 'idle') {
210
+ return reject(state, msg.type, 'already attached');
211
+ }
212
+ return [{ ...state, lifecycle: { phase: 'attaching' } }, []];
213
+ }
214
+ case 'DETACH': {
215
+ // Idempotent and safe from any phase, including error.
216
+ const [, aborts] = abortInflight(state);
217
+ return [initialState(cfg), aborts];
218
+ }
219
+ case 'LOAD': {
220
+ if (state.lifecycle.phase === 'idle') {
221
+ return reject(state, msg.type, 'not attached');
222
+ }
223
+ if (state.lifecycle.phase !== 'attaching') {
224
+ return reject(state, msg.type, 'already loaded');
225
+ }
226
+ // An explicit mimeType is authoritative: when no composed adapter
227
+ // declares it, the load fails here, before any bytes move. An
228
+ // integrator's source resolver relies on this being cheap.
229
+ if (msg.mimeType !== undefined && hooks.manifestTypes !== undefined) {
230
+ const mimeType = normalizeMimeType(msg.mimeType);
231
+ if (!hooks.manifestTypes.has(mimeType)) {
232
+ return failManifest(state, { category: 'manifest', code: 'MANIFEST_UNSUPPORTED', fatal: true, recoverable: false }, { mimeType });
233
+ }
234
+ }
235
+ const tokenSeq = state.scheduling.tokenSeq + 1;
236
+ const token = `t${tokenSeq}:manifest`;
237
+ const inflight = new Map(state.scheduling.inflight);
238
+ inflight.set(token, { token, trackId: 'manifest', seq: 0, url: msg.url });
239
+ return [
240
+ {
241
+ ...state,
242
+ lifecycle: { phase: 'loading' },
243
+ scheduling: { ...state.scheduling, inflight, tokenSeq },
244
+ },
245
+ [{ kind: 'fetch', token, url: msg.url, timeout: cfg.manifestTimeoutMs }],
246
+ ];
247
+ }
248
+ case 'UNLOAD': {
249
+ const [, aborts] = abortInflight(state);
250
+ const fresh = initialState(cfg);
251
+ const phase = state.lifecycle.phase === 'idle' ? 'idle' : 'attaching';
252
+ // The source goes, so do its cues: a native track keeps rendering
253
+ // what it holds, and the next source must not start under the last
254
+ // subtitle of the previous one.
255
+ const clears = [...state.cues.keys()].map((trackId) => ({
256
+ kind: 'clearCues',
257
+ trackId,
258
+ start: 0,
259
+ end: Number.POSITIVE_INFINITY,
260
+ }));
261
+ return [
262
+ {
263
+ ...fresh,
264
+ lifecycle: { phase },
265
+ scheduling: { ...fresh.scheduling, tokenSeq: state.scheduling.tokenSeq },
266
+ },
267
+ [...aborts, ...clears],
268
+ ];
269
+ }
270
+ case 'SEEK': {
271
+ if (state.presentation === null) {
272
+ return reject(state, msg.type, 'no source');
273
+ }
274
+ const [next, aborts] = abortInflight(state);
275
+ // A live span bounds the seek: behind it nothing is available any
276
+ // more, ahead of it nothing exists yet. The edge, not the span end,
277
+ // is the far bound, so a seek to "now" lands where playback can start.
278
+ const to = state.live === null
279
+ ? msg.to
280
+ : Math.min(Math.max(msg.to, state.live.span.start), state.live.edge);
281
+ return [
282
+ { ...next, scheduling: clearRepeat(next.scheduling) },
283
+ [...aborts, { kind: 'seekElement', to }],
284
+ ];
285
+ }
286
+ case 'SEEK_TO_LIVE_EDGE': {
287
+ if (state.presentation === null || !state.presentation.isLive) {
288
+ return reject(state, msg.type, 'not live');
289
+ }
290
+ if (state.live === null) {
291
+ return reject(state, msg.type, 'no live support loaded');
292
+ }
293
+ const [next, aborts] = abortInflight(state);
294
+ return [
295
+ { ...next, scheduling: clearRepeat(next.scheduling) },
296
+ [...aborts, { kind: 'seekElement', to: state.live.edge }],
297
+ ];
298
+ }
299
+ case 'SELECT_TRACK': {
300
+ const track = findTrack(state.presentation, msg.trackId);
301
+ if (track === null) {
302
+ return reject(state, msg.type, `unknown track: ${msg.trackId}`);
303
+ }
304
+ const previous = state.tracks.active.get(track.contentType);
305
+ const active = new Map(state.tracks.active);
306
+ active.set(track.contentType, track.id);
307
+ const effects = [];
308
+ let buffers = state.buffers;
309
+ let inflight = state.scheduling.inflight;
310
+ // A media track change mid-stream must clear the old track's buffer,
311
+ // or the new track's segment (which re-covers time the old track
312
+ // already buffered) overlaps stale content and strict decoders
313
+ // (WebKit) reject the append. The whole buffer goes; the new track
314
+ // refills from the playhead's segment with a brief gap.
315
+ if (previous !== undefined &&
316
+ previous !== track.id &&
317
+ (track.contentType === 'audio' || track.contentType === 'video')) {
318
+ const sbId = sbIdFor(track.contentType);
319
+ if (state.buffers.has(sbId)) {
320
+ effects.push({
321
+ kind: 'remove',
322
+ sbId,
323
+ start: 0,
324
+ end: Number.POSITIVE_INFINITY,
325
+ });
326
+ const buffer = state.buffers.get(sbId);
327
+ if (buffer !== undefined) {
328
+ // Force an init re-fetch for the new track: its initFor no
329
+ // longer matches, so scheduling fetches init before media.
330
+ const next = new Map(state.buffers);
331
+ const { initFor: _initFor, ...rest } = buffer;
332
+ next.set(sbId, rest);
333
+ buffers = next;
334
+ }
335
+ // Drop the old track's in-flight fetches.
336
+ const pruned = new Map(state.scheduling.inflight);
337
+ for (const [token, request] of state.scheduling.inflight) {
338
+ if (request.trackId === previous) {
339
+ effects.push({ kind: 'abort', token });
340
+ pruned.delete(token);
341
+ }
342
+ }
343
+ inflight = pruned;
344
+ }
345
+ }
346
+ if (previous !== track.id) {
347
+ effects.push({
348
+ kind: 'emit',
349
+ event: 'tracks:selected',
350
+ payload: { contentType: track.contentType, trackId: track.id },
351
+ });
352
+ }
353
+ return [
354
+ {
355
+ ...state,
356
+ tracks: { ...state.tracks, active },
357
+ buffers,
358
+ scheduling: clearRepeat({ ...state.scheduling, inflight }),
359
+ quality: bumped(state.quality),
360
+ },
361
+ effects,
362
+ ];
363
+ }
364
+ case 'DESELECT_TRACK': {
365
+ if (msg.contentType !== 'text' && msg.contentType !== 'metadata') {
366
+ return reject(state, msg.type, 'video and audio always keep a selection');
367
+ }
368
+ const trackId = state.tracks.active.get(msg.contentType);
369
+ if (trackId === undefined)
370
+ return [state, []];
371
+ const active = new Map(state.tracks.active);
372
+ active.delete(msg.contentType);
373
+ const cues = new Map(state.cues);
374
+ cues.delete(trackId);
375
+ const [aborted, abortEffects] = abortInflight(state, trackId);
376
+ return [
377
+ {
378
+ ...aborted,
379
+ tracks: { ...state.tracks, active },
380
+ cues,
381
+ quality: bumped(state.quality),
382
+ },
383
+ [
384
+ ...abortEffects,
385
+ { kind: 'clearCues', trackId, start: 0, end: Number.POSITIVE_INFINITY },
386
+ {
387
+ kind: 'emit',
388
+ event: 'tracks:selected',
389
+ payload: { contentType: msg.contentType, trackId: null },
390
+ },
391
+ ],
392
+ ];
393
+ }
394
+ case 'PIN_RENDITION': {
395
+ const site = findRendition(state.presentation, msg.renditionId);
396
+ if (site === null) {
397
+ return reject(state, msg.type, `unknown rendition: ${msg.renditionId}`);
398
+ }
399
+ const pinnedState = {
400
+ ...state,
401
+ quality: bumped({ ...state.quality, pinned: site.rendition.id, active: site.rendition.id }),
402
+ };
403
+ // Apply planning needs a buffer to flush; text and metadata pins
404
+ // change future fetches only.
405
+ if (site.track.contentType !== 'video' && site.track.contentType !== 'audio') {
406
+ return [pinnedState, []];
407
+ }
408
+ const sbId = sbIdFor(site.track.contentType);
409
+ const inflightTokens = [];
410
+ for (const request of state.scheduling.inflight.values()) {
411
+ if (request.trackId === site.track.id)
412
+ inflightTokens.push(request.token);
413
+ }
414
+ const plan = planPinApply({
415
+ strategy: msg.apply,
416
+ currentTime: state.playback.currentTime,
417
+ ranges: state.buffers.get(sbId)?.ranges ?? [],
418
+ sbId,
419
+ trackId: site.track.id,
420
+ inflightTokens,
421
+ period: site.period,
422
+ rendition: site.rendition,
423
+ tokenSeq: state.scheduling.tokenSeq,
424
+ });
425
+ const inflight = new Map(state.scheduling.inflight);
426
+ for (const token of inflightTokens)
427
+ inflight.delete(token);
428
+ for (const request of plan.requests)
429
+ inflight.set(request.token, request);
430
+ return [
431
+ {
432
+ ...pinnedState,
433
+ scheduling: clearRepeat({ ...state.scheduling, inflight, tokenSeq: plan.tokenSeq }),
434
+ },
435
+ plan.effects,
436
+ ];
437
+ }
438
+ case 'RELEASE_PIN': {
439
+ return [{ ...state, quality: bumped({ ...state.quality, pinned: null }) }, []];
440
+ }
441
+ case 'CONSTRAIN': {
442
+ const constraints = new Map(state.quality.constraints);
443
+ constraints.set(msg.source, msg.constraint);
444
+ // A changed allowed set means the last fetch decision is no longer
445
+ // the same decision; reset the repeat breaker so recovery's exclusion
446
+ // gets a chance to switch renditions before failures re-trip it.
447
+ return [
448
+ {
449
+ ...state,
450
+ quality: bumped({ ...state.quality, constraints }),
451
+ scheduling: clearRepeat(state.scheduling),
452
+ },
453
+ [],
454
+ ];
455
+ }
456
+ case 'RELEASE_CONSTRAINT': {
457
+ if (!state.quality.constraints.has(msg.source))
458
+ return [state, []];
459
+ const constraints = new Map(state.quality.constraints);
460
+ constraints.delete(msg.source);
461
+ return [
462
+ {
463
+ ...state,
464
+ quality: bumped({ ...state.quality, constraints }),
465
+ scheduling: clearRepeat(state.scheduling),
466
+ },
467
+ [],
468
+ ];
469
+ }
470
+ case 'SET_BUFFER_GOAL': {
471
+ if (!Number.isFinite(msg.seconds) || msg.seconds <= 0) {
472
+ return reject(state, msg.type, 'invalid buffer goal');
473
+ }
474
+ return [{ ...state, scheduling: { ...state.scheduling, bufferGoal: msg.seconds } }, []];
475
+ }
476
+ case 'ABORT_INFLIGHT': {
477
+ return abortInflight(state, msg.trackId);
478
+ }
479
+ }
480
+ }
481
+ function reduceFact(state, msg, cfg, hooks) {
482
+ switch (msg.type) {
483
+ case 'ELEMENT_ATTACHED':
484
+ case 'MEDIASOURCE_OPEN':
485
+ case 'MEDIASOURCE_CLOSED':
486
+ // Lifecycle detail owned by the mse module. Absorbed for the trace.
487
+ return [state, []];
488
+ case 'MANIFEST_LOADED': {
489
+ const available = [];
490
+ for (const period of msg.presentation.periods) {
491
+ for (const track of period.tracks)
492
+ available.push(track.id);
493
+ }
494
+ const inflight = new Map(state.scheduling.inflight);
495
+ for (const [token, request] of inflight) {
496
+ if (request.trackId === 'manifest')
497
+ inflight.delete(token);
498
+ }
499
+ const phase = state.lifecycle.phase === 'loading' ? 'ready' : state.lifecycle.phase;
500
+ // Default activation: the first video and audio track, so a manifest
501
+ // alone yields a playable composition without a SELECT_TRACK.
502
+ const active = new Map(state.tracks.active);
503
+ for (const period of msg.presentation.periods) {
504
+ for (const track of period.tracks) {
505
+ if ((track.contentType === 'video' || track.contentType === 'audio') &&
506
+ !active.has(track.contentType)) {
507
+ active.set(track.contentType, track.id);
508
+ }
509
+ }
510
+ }
511
+ const loaded = {
512
+ ...state,
513
+ lifecycle: { phase },
514
+ presentation: msg.presentation,
515
+ scheduling: { ...state.scheduling, inflight },
516
+ tracks: { active, available },
517
+ quality: bumped(state.quality),
518
+ };
519
+ // A live reload feeds a MANIFEST_LOADED per playlist; the track list
520
+ // only changed if the ids did.
521
+ const sameTracks = available.length === state.tracks.available.length &&
522
+ available.every((id, index) => id === state.tracks.available[index]);
523
+ const manifestEffects = sameTracks
524
+ ? []
525
+ : [{ kind: 'emit', event: 'tracks:changed', payload: { available } }];
526
+ // The manifest DRM route: emit every track's protection schemes so
527
+ // eme-core (if loaded) can open sessions. Serializable init data
528
+ // rides the event; nothing DRM-specific enters the reducer.
529
+ const schemes = msg.presentation.periods.flatMap((period) => period.tracks.flatMap((track) => track.protection?.schemes ?? []));
530
+ if (schemes.length > 0) {
531
+ manifestEffects.push({
532
+ kind: 'emit',
533
+ event: 'presentation:protection',
534
+ payload: schemes,
535
+ });
536
+ }
537
+ if (!msg.presentation.isLive && msg.presentation.duration !== undefined) {
538
+ manifestEffects.push({ kind: 'setDuration', seconds: msg.presentation.duration });
539
+ }
540
+ return [loaded, manifestEffects];
541
+ }
542
+ case 'MANIFEST_FAILED':
543
+ return failManifest(state, msg.error);
544
+ case 'PLAYLIST_REFRESHED':
545
+ // Merging a refreshed live playlist into the presentation belongs to
546
+ // the protocol stages. Absorbed for the trace until they exist.
547
+ return [state, []];
548
+ case 'SEGMENT_LOADED': {
549
+ let matched = null;
550
+ if (msg.token !== undefined) {
551
+ // The token names one request. Track and sequence alone would also
552
+ // match the refetch that replaced an aborted request.
553
+ matched = state.scheduling.inflight.get(msg.token) ?? null;
554
+ }
555
+ else {
556
+ for (const request of state.scheduling.inflight.values()) {
557
+ if (request.trackId === msg.trackId && request.seq === msg.seq) {
558
+ matched = request;
559
+ break;
560
+ }
561
+ }
562
+ }
563
+ if (matched === null) {
564
+ // Late arrival for an aborted or superseded request. The bytes
565
+ // already crossed the network; absorb and drop them.
566
+ return [state, []];
567
+ }
568
+ const inflight = new Map(state.scheduling.inflight);
569
+ inflight.delete(matched.token);
570
+ // The throughput estimate follows the lead track: a 96 KB audio
571
+ // segment beside a 2 MB video segment measures latency, not the link,
572
+ // and would drag the estimate down to a fraction of what video sees.
573
+ const sampleBps = msg.rtt > 0 && msg.size >= cfg.ewmaMinSampleBytes && samplesThroughput(state, matched)
574
+ ? (msg.size * 8000) / msg.rtt
575
+ : null;
576
+ const stats = sampleBps === null
577
+ ? state.stats
578
+ : {
579
+ throughputEwma: ewma(state.stats.throughputEwma, sampleBps, cfg.ewmaAlpha),
580
+ throughputFastEwma: ewma(state.stats.throughputFastEwma, sampleBps, cfg.ewmaFastAlpha),
581
+ };
582
+ let buffers = state.buffers;
583
+ let timeline = state.timeline;
584
+ let quality = state.quality;
585
+ let cues = state.cues;
586
+ const effects = [];
587
+ // No SourceBuffer destination: a cue track's segment routes to its
588
+ // sink through the deliver effect. Coverage merges here, from the
589
+ // request's own timing, so a parse failure still counts as covered
590
+ // and the scheduler never refetch-loops a bad segment.
591
+ const cueTrack = matched.sbId === undefined ? findTrack(state.presentation, matched.trackId) : null;
592
+ if (cueTrack !== null &&
593
+ (cueTrack.contentType === 'text' || cueTrack.contentType === 'metadata') &&
594
+ matched.segmentStart !== undefined) {
595
+ const span = {
596
+ start: matched.segmentStart,
597
+ end: matched.segmentStart + (matched.segmentDuration ?? 0),
598
+ };
599
+ const next = new Map(cues);
600
+ next.set(matched.trackId, mergeCoverage(cues.get(matched.trackId) ?? [], span));
601
+ cues = next;
602
+ effects.push({
603
+ kind: 'deliver',
604
+ trackId: matched.trackId,
605
+ contentType: cueTrack.contentType,
606
+ data: msg.bytes,
607
+ meta: {
608
+ trackId: matched.trackId,
609
+ renditionId: matched.renditionId ?? '',
610
+ contentType: cueTrack.contentType,
611
+ seq: matched.seq,
612
+ start: matched.segmentStart,
613
+ duration: matched.segmentDuration ?? 0,
614
+ isInit: matched.seq < 0,
615
+ },
616
+ });
617
+ }
618
+ if (matched.sbId !== undefined) {
619
+ // seq below zero is the init-segment convention: mark the buffer
620
+ // initialized for this rendition so media scheduling can proceed.
621
+ if (matched.seq < 0 && matched.renditionId !== undefined) {
622
+ const buffer = state.buffers.get(matched.sbId);
623
+ if (buffer !== undefined) {
624
+ // A switch that changes the codec family needs a changeType
625
+ // before the new init, or the append fails. In-family profile
626
+ // changes append the new init bare: browsers accept those in
627
+ // practice, and forcing changeType there breaks WebKit. The
628
+ // reducer emits the mechanism; codec-switch's policy governed
629
+ // whether abr proposed the switch at all.
630
+ const targetSite = findRendition(state.presentation, matched.renditionId);
631
+ const targetCodecs = targetSite === null
632
+ ? buffer.codecs
633
+ : targetSite.rendition.codecs === null
634
+ ? targetSite.rendition.mimeType
635
+ : `${targetSite.rendition.mimeType}; codecs="${targetSite.rendition.codecs}"`;
636
+ const familyChanged = codecFamily(targetSite?.rendition.codecs ?? null) !==
637
+ codecFamily(bufferCodecString(buffer.codecs));
638
+ const nextBuffers = new Map(state.buffers);
639
+ if (targetCodecs !== buffer.codecs && familyChanged) {
640
+ effects.push({ kind: 'changeType', sbId: matched.sbId, codecs: targetCodecs });
641
+ }
642
+ nextBuffers.set(matched.sbId, {
643
+ ...buffer,
644
+ initFor: matched.renditionId,
645
+ codecs: targetCodecs,
646
+ });
647
+ buffers = nextBuffers;
648
+ }
649
+ }
650
+ // A media segment decodes only under its own rendition's init
651
+ // segment. A constraint or pin can move the buffer to another
652
+ // rendition while this request was in flight, and its bytes would
653
+ // then meet the wrong parameter sets: a hardware decoder rejects
654
+ // them as malformed. Drop them; scheduling below refetches the
655
+ // span from the rendition the buffer is set up for.
656
+ const initFor = buffers.get(matched.sbId)?.initFor;
657
+ if (matched.seq >= 0 &&
658
+ matched.renditionId !== undefined &&
659
+ initFor !== undefined &&
660
+ initFor !== matched.renditionId) {
661
+ const next = {
662
+ ...state,
663
+ scheduling: { ...state.scheduling, inflight },
664
+ stats,
665
+ buffers,
666
+ cues,
667
+ };
668
+ // SEGMENT_LOADED drives scheduling on the way out, so the span is
669
+ // refetched from the rendition the buffer holds the init for.
670
+ return [
671
+ next,
672
+ [
673
+ ...effects,
674
+ {
675
+ kind: 'emit',
676
+ event: 'quality:stale-segment',
677
+ payload: { renditionId: matched.renditionId, seq: matched.seq, initFor },
678
+ },
679
+ ],
680
+ ];
681
+ }
682
+ // The scheduler computed the epoch's offset at request time; apply
683
+ // it ahead of the append when it differs from what the buffer has.
684
+ // Playlist discontinuities and period boundaries both land here.
685
+ if (matched.timestampOffset !== undefined &&
686
+ state.timeline.periodOffsets.get(matched.sbId) !== matched.timestampOffset) {
687
+ effects.push({
688
+ kind: 'setTimestampOffset',
689
+ sbId: matched.sbId,
690
+ offset: matched.timestampOffset,
691
+ });
692
+ const periodOffsets = new Map(state.timeline.periodOffsets);
693
+ periodOffsets.set(matched.sbId, matched.timestampOffset);
694
+ timeline = { ...state.timeline, periodOffsets };
695
+ }
696
+ effects.push({
697
+ kind: 'append',
698
+ sbId: matched.sbId,
699
+ data: msg.bytes,
700
+ ...(matched.segmentStart !== undefined ? { start: matched.segmentStart } : {}),
701
+ ...(matched.renditionId !== undefined ? { renditionId: matched.renditionId } : {}),
702
+ seq: matched.seq,
703
+ });
704
+ const buffer = buffers.get(matched.sbId);
705
+ if (buffer !== undefined) {
706
+ const nextBuffers = new Map(buffers);
707
+ nextBuffers.set(matched.sbId, { ...buffer, pendingAppends: buffer.pendingAppends + 1 });
708
+ buffers = nextBuffers;
709
+ }
710
+ if (matched.sbId === sbIdFor('video') &&
711
+ matched.renditionId !== undefined &&
712
+ matched.segmentStart !== undefined) {
713
+ // The append log behind quality.playing: which video rendition
714
+ // occupies which span of the buffer. Video only: an audio append
715
+ // carries its track id here, and logging it would answer
716
+ // `playing` with an id no video rendition has. Pruned behind the
717
+ // eviction watermark, so its size is bounded by the buffered span
718
+ // over the segment duration.
719
+ const watermark = state.playback.currentTime - cfg.backBufferSeconds;
720
+ const appendLog = [
721
+ ...state.quality.appendLog.filter(([range]) => range.end > watermark),
722
+ [
723
+ {
724
+ start: matched.segmentStart,
725
+ end: matched.segmentStart + (matched.segmentDuration ?? 0),
726
+ },
727
+ matched.renditionId,
728
+ ],
729
+ ];
730
+ quality = { ...quality, appendLog };
731
+ }
732
+ }
733
+ return [
734
+ {
735
+ ...state,
736
+ buffers,
737
+ timeline,
738
+ quality,
739
+ cues,
740
+ scheduling: { ...state.scheduling, inflight },
741
+ stats,
742
+ },
743
+ effects,
744
+ ];
745
+ }
746
+ case 'SEGMENT_FAILED': {
747
+ const inflight = new Map(state.scheduling.inflight);
748
+ for (const [token, request] of inflight) {
749
+ if (request.trackId === msg.trackId && request.seq === msg.seq)
750
+ inflight.delete(token);
751
+ }
752
+ // The manifest request has no rendition to exclude and no segment to
753
+ // skip: the transport already retried per policy, so its failure is
754
+ // the load's failure. Without this the engine sat in `loading` with
755
+ // a non-fatal error and a retry tick that had nothing to schedule.
756
+ if (msg.trackId === 'manifest') {
757
+ const contentType = msg.error.context?.contentType;
758
+ return failManifest({ ...state, scheduling: { ...state.scheduling, inflight } }, { ...msg.error, fatal: true, recoverable: false }, {
759
+ status: msg.status ?? null,
760
+ ...(typeof contentType === 'string' ? { contentType } : {}),
761
+ });
762
+ }
763
+ // A cue pipeline degrades, never kills playback: subtitles stop,
764
+ // video continues. The span counts as covered so the scheduler moves
765
+ // on instead of hammering the same missing segment.
766
+ const failedTrack = findTrack(state.presentation, msg.trackId);
767
+ const isCueTrack = failedTrack !== null &&
768
+ (failedTrack.contentType === 'text' || failedTrack.contentType === 'metadata');
769
+ let cues = state.cues;
770
+ if (isCueTrack) {
771
+ const request = [...state.scheduling.inflight.values()].find((r) => r.trackId === msg.trackId && r.seq === msg.seq);
772
+ if (request?.segmentStart !== undefined) {
773
+ const next = new Map(cues);
774
+ next.set(msg.trackId, mergeCoverage(cues.get(msg.trackId) ?? [], {
775
+ start: request.segmentStart,
776
+ end: request.segmentStart + (request.segmentDuration ?? 0),
777
+ }));
778
+ cues = next;
779
+ }
780
+ }
781
+ const failEffects = [
782
+ {
783
+ kind: 'emit',
784
+ event: 'error',
785
+ payload: {
786
+ category: msg.error.category,
787
+ code: msg.error.code,
788
+ fatal: isCueTrack ? false : msg.error.fatal,
789
+ recoverable: msg.error.recoverable,
790
+ trackId: msg.trackId,
791
+ seq: msg.seq,
792
+ status: msg.status ?? null,
793
+ },
794
+ },
795
+ ];
796
+ // A failed media fetch must re-drive, but not synchronously: a 404
797
+ // resolves as a microtask, and an immediate re-fetch of the same dead
798
+ // segment would flood the loop before a recovery stage's macrotask
799
+ // commands could intervene. A short backoff schedules the re-drive,
800
+ // giving exclusion and skip time to change the decision. Without
801
+ // recovery, the same segment retries until the breaker ends it.
802
+ if (!isCueTrack) {
803
+ failEffects.push({
804
+ kind: 'schedule',
805
+ token: 'kernel:retry',
806
+ delayMs: cfg.baseRetryDelayMs,
807
+ // biome-ignore lint/suspicious/noThenProperty: `then` is the schedule effect's field name from the message taxonomy
808
+ then: { type: 'TICK', token: 'kernel:retry' },
809
+ });
810
+ }
811
+ return [{ ...state, cues, scheduling: { ...state.scheduling, inflight } }, failEffects];
812
+ }
813
+ case 'SOURCEBUFFER_CREATED': {
814
+ const buffers = new Map(state.buffers);
815
+ buffers.set(msg.sbId, { codecs: msg.codecs, ranges: [], pendingAppends: 0 });
816
+ return [{ ...state, buffers }, []];
817
+ }
818
+ case 'SOURCEBUFFER_UPDATEEND': {
819
+ const buffer = state.buffers.get(msg.sbId);
820
+ if (buffer === undefined) {
821
+ // The buffer was removed by a concurrent detach. Absorb and ignore.
822
+ return [state, []];
823
+ }
824
+ const buffers = new Map(state.buffers);
825
+ buffers.set(msg.sbId, {
826
+ ...buffer,
827
+ ...(msg.ranges !== undefined ? { ranges: msg.ranges } : {}),
828
+ pendingAppends: Math.max(0, buffer.pendingAppends - 1),
829
+ });
830
+ // A successful append proves the buffer works: the breaker resets.
831
+ let bufferErrors = state.bufferErrors;
832
+ if (bufferErrors.has(msg.sbId)) {
833
+ bufferErrors = new Map(bufferErrors);
834
+ bufferErrors.delete(msg.sbId);
835
+ }
836
+ // An append completing is the natural moment to decide the next fetch,
837
+ // so the buffer keeps filling toward the goal without waiting for the
838
+ // next time update.
839
+ return driveScheduling({ ...state, buffers, bufferErrors }, hooks, cfg);
840
+ }
841
+ case 'SOURCEBUFFER_ERROR': {
842
+ // The circuit breaker: repeated failures on one buffer can never make
843
+ // progress, and every failure loops back into a refetch of the same
844
+ // segment. At the limit the failure turns fatal and the loop dies.
845
+ const count = (state.bufferErrors.get(msg.sbId) ?? 0) + 1;
846
+ const bufferErrors = new Map(state.bufferErrors);
847
+ bufferErrors.set(msg.sbId, count);
848
+ const fatal = msg.error.fatal || count >= cfg.bufferErrorLimit;
849
+ const phase = fatal ? 'error' : state.lifecycle.phase;
850
+ const effects = [
851
+ {
852
+ kind: 'emit',
853
+ event: 'error',
854
+ payload: {
855
+ category: msg.error.category,
856
+ code: msg.error.code,
857
+ fatal,
858
+ recoverable: msg.error.recoverable,
859
+ sbId: msg.sbId,
860
+ ...(fatal && !msg.error.fatal ? { consecutiveFailures: count } : {}),
861
+ },
862
+ },
863
+ ];
864
+ let inflight = state.scheduling.inflight;
865
+ if (fatal) {
866
+ // Stop the world for this buffer: whatever is in flight will only
867
+ // feed the same failure again.
868
+ const pruned = new Map(state.scheduling.inflight);
869
+ for (const [token, request] of state.scheduling.inflight) {
870
+ if (request.sbId === msg.sbId) {
871
+ effects.push({ kind: 'abort', token });
872
+ pruned.delete(token);
873
+ }
874
+ }
875
+ inflight = pruned;
876
+ }
877
+ return [
878
+ {
879
+ ...state,
880
+ bufferErrors,
881
+ lifecycle: { phase },
882
+ scheduling: { ...state.scheduling, inflight },
883
+ },
884
+ effects,
885
+ ];
886
+ }
887
+ case 'QUOTA_EXCEEDED': {
888
+ const boundary = Math.max(0, state.playback.currentTime - cfg.backBufferSeconds);
889
+ if (boundary <= 0) {
890
+ // Nothing behind the playhead to evict. Report; recovery is a stage.
891
+ return [state, [{ kind: 'emit', event: 'quota:exhausted', payload: { sbId: msg.sbId } }]];
892
+ }
893
+ return [state, [{ kind: 'remove', sbId: msg.sbId, start: 0, end: boundary }]];
894
+ }
895
+ case 'TIME_UPDATE': {
896
+ const moved = {
897
+ ...state,
898
+ playback: { ...state.playback, currentTime: msg.currentTime, buffered: msg.buffered },
899
+ };
900
+ return driveScheduling(moved, hooks, cfg);
901
+ }
902
+ case 'SEEKING': {
903
+ // Seeking out of the ended phase resumes an ordinary ready state.
904
+ const phase = state.lifecycle.phase === 'ended' ? 'ready' : state.lifecycle.phase;
905
+ return [
906
+ {
907
+ ...state,
908
+ lifecycle: { phase },
909
+ playback: { ...state.playback, currentTime: msg.to, seeking: true },
910
+ },
911
+ [],
912
+ ];
913
+ }
914
+ case 'SEEKED': {
915
+ return [
916
+ { ...state, playback: { ...state.playback, currentTime: msg.at, seeking: false } },
917
+ [],
918
+ ];
919
+ }
920
+ case 'STALLED': {
921
+ return [state, [{ kind: 'emit', event: 'playback:stalled', payload: { at: msg.at } }]];
922
+ }
923
+ case 'ENCRYPTED': {
924
+ return [
925
+ state,
926
+ [
927
+ {
928
+ kind: 'emit',
929
+ event: 'drm:encrypted',
930
+ payload: { initDataType: msg.initDataType, initData: msg.initData },
931
+ },
932
+ ],
933
+ ];
934
+ }
935
+ case 'LIVE_WINDOW_CHANGED': {
936
+ const firstWindow = state.live === null;
937
+ const withLive = {
938
+ ...state,
939
+ live: { span: { start: msg.start, end: msg.end }, edge: msg.edge },
940
+ };
941
+ // The availability span is what the element may seek within. MSE
942
+ // derives `seekable` from the duration unless told otherwise, so a
943
+ // live presentation without a declared duration gets an infinite one
944
+ // on its first span and the span itself as the seekable range on
945
+ // every update. Native controls then scrub the DVR span and clamp
946
+ // seeks into it, instead of offering 0 to the buffered end.
947
+ const spanEffects = [];
948
+ if (firstWindow && state.presentation?.duration === undefined) {
949
+ spanEffects.push({ kind: 'setDuration', seconds: Number.POSITIVE_INFINITY });
950
+ }
951
+ spanEffects.push({ kind: 'setLiveSeekableRange', start: msg.start, end: msg.end });
952
+ // On the first availability update of a live stream, start at the live
953
+ // edge instead of the beginning of what can be a long DVR span (hours
954
+ // behind). The playhead moves to the edge, the element seeks there, and
955
+ // scheduling then fills the buffer at the edge, not at the span start.
956
+ if (firstWindow && msg.edge > state.playback.currentTime + 1) {
957
+ const seeked = {
958
+ ...withLive,
959
+ playback: { ...withLive.playback, currentTime: msg.edge },
960
+ };
961
+ const [scheduled, effects] = driveScheduling(seeked, hooks, cfg);
962
+ return [scheduled, [...spanEffects, { kind: 'seekElement', to: msg.edge }, ...effects]];
963
+ }
964
+ const [scheduled, effects] = driveScheduling(withLive, hooks, cfg);
965
+ return [scheduled, [...spanEffects, ...effects]];
966
+ }
967
+ case 'TICK': {
968
+ // Slices own their tokens; the kernel has nothing to do.
969
+ return [state, []];
970
+ }
971
+ case 'ENDED': {
972
+ if (state.lifecycle.phase !== 'ready')
973
+ return [state, []];
974
+ return [
975
+ { ...state, lifecycle: { phase: 'ended' } },
976
+ [{ kind: 'emit', event: 'playback:ended', payload: { at: msg.at } }],
977
+ ];
978
+ }
979
+ case 'THROUGHPUT_SAMPLE': {
980
+ return [
981
+ {
982
+ ...state,
983
+ stats: {
984
+ throughputEwma: ewma(state.stats.throughputEwma, msg.bps, cfg.ewmaAlpha),
985
+ throughputFastEwma: ewma(state.stats.throughputFastEwma, msg.bps, cfg.ewmaFastAlpha),
986
+ },
987
+ },
988
+ [],
989
+ ];
990
+ }
991
+ }
992
+ }
993
+ /**
994
+ * The buffer-goal loop, run on TIME_UPDATE. Pure: rendition choice comes
995
+ * from arbitration over state, buffer knowledge from the ranges snapshots
996
+ * the updateend facts carried into state. Live presentations wait for
997
+ * the live stages to supply availability bounds; until then nothing is
998
+ * scheduled.
999
+ */
1000
+ function driveScheduling(state, hooks, cfg) {
1001
+ if (state.lifecycle.phase !== 'ready' || state.presentation === null)
1002
+ return [state, []];
1003
+ // Live schedules only once a live stage has reported availability
1004
+ // bounds; the kernel never computes an edge itself.
1005
+ if (state.presentation.isLive && state.live === null)
1006
+ return [state, []];
1007
+ const effects = [];
1008
+ const tracks = [];
1009
+ const initFetches = [];
1010
+ let quality = state.quality;
1011
+ for (const contentType of ['video', 'audio']) {
1012
+ const trackId = state.tracks.active.get(contentType);
1013
+ if (trackId === undefined)
1014
+ continue;
1015
+ const found = findTrackSite(state.presentation, trackId);
1016
+ if (found === null || found.track.renditions.length === 0)
1017
+ continue;
1018
+ const sbId = sbIdFor(contentType);
1019
+ // An append in flight means the ranges snapshot is stale: deciding on
1020
+ // it refetches the very segment being appended. Its updateend is a
1021
+ // driving fact, so waiting costs nothing.
1022
+ if ((state.buffers.get(sbId)?.pendingAppends ?? 0) > 0)
1023
+ continue;
1024
+ const bufferAhead = bufferedEndFrom(state.buffers.get(sbId)?.ranges ?? [], state.playback.currentTime, GAP_TOLERANCE) - state.playback.currentTime;
1025
+ // Arbitrate the rendition for this track. Memoized on quality.version:
1026
+ // TIME_UPDATE at 60 Hz reuses the last outcome until something changes.
1027
+ // With an abr chooser loaded the key also carries coarse telemetry
1028
+ // buckets, so a real throughput or buffer change re-arbitrates without
1029
+ // recomputing at frame rate.
1030
+ const memoKey = hooks.abr == null
1031
+ ? state.quality.version
1032
+ : `${state.quality.version}:${Math.round(state.stats.throughputEwma / 25_000)}:${Math.round(state.stats.throughputFastEwma / 25_000)}:${Math.round(bufferAhead)}:${state.quality.active}`;
1033
+ const outcome = arbiterFor(hooks, contentType).run({
1034
+ renditions: found.track.renditions,
1035
+ constraints: state.quality.constraints,
1036
+ pinned: state.quality.pinned,
1037
+ current: state.quality.active,
1038
+ couplings: state.presentation.couplings,
1039
+ activeTracks: state.tracks.active,
1040
+ availableGroups: availableGroups(state),
1041
+ abr: hooks.abr ?? null,
1042
+ telemetry: {
1043
+ throughputEwma: state.stats.throughputEwma,
1044
+ throughputFastEwma: state.stats.throughputFastEwma,
1045
+ bufferAhead,
1046
+ current: state.quality.active,
1047
+ currentTime: state.playback.currentTime,
1048
+ canSwitchTo: hooks.switchPolicy ?? canSwitchTo,
1049
+ },
1050
+ }, memoKey);
1051
+ for (const event of outcome.events)
1052
+ effects.push(event);
1053
+ const rendition = found.track.renditions.find((r) => r.id === outcome.result.selected);
1054
+ if (rendition === undefined)
1055
+ continue;
1056
+ if (contentType === 'video' && quality.active !== rendition.id) {
1057
+ quality = { ...quality, active: rendition.id };
1058
+ }
1059
+ const inflight = [];
1060
+ for (const request of state.scheduling.inflight.values()) {
1061
+ if (request.trackId === trackId)
1062
+ inflight.push(request);
1063
+ }
1064
+ // The buffer request rides with the first fetch decision; while that
1065
+ // fetch is in flight the SOURCEBUFFER_CREATED fact is on its way, so
1066
+ // re-requesting every tick would spam the trace.
1067
+ if (!state.buffers.has(sbId) && inflight.length === 0) {
1068
+ const codecs = rendition.codecs === null
1069
+ ? rendition.mimeType
1070
+ : `${rendition.mimeType}; codecs="${rendition.codecs}"`;
1071
+ effects.push({ kind: 'createSourceBuffer', sbId, codecs });
1072
+ }
1073
+ // Init before media, always: while the buffer's init is not this
1074
+ // rendition's, the only fetch this track may make is the init segment.
1075
+ // One request per track makes the ordering free.
1076
+ if (rendition.init !== undefined &&
1077
+ state.buffers.get(sbId)?.initFor !== rendition.id &&
1078
+ inflight.length === 0) {
1079
+ initFetches.push({ trackId, sbId, rendition: rendition.id, init: rendition.init });
1080
+ continue;
1081
+ }
1082
+ tracks.push({
1083
+ trackId,
1084
+ period: found.period,
1085
+ rendition,
1086
+ ranges: state.buffers.get(sbId)?.ranges ?? [],
1087
+ sbId,
1088
+ inflight,
1089
+ });
1090
+ }
1091
+ // Cue pipelines schedule like media, minus everything SourceBuffer:
1092
+ // ranges come from delivered coverage, there is no init and no
1093
+ // destination id, and delivery routes through the sink instead.
1094
+ for (const contentType of ['text', 'metadata']) {
1095
+ const trackId = state.tracks.active.get(contentType);
1096
+ if (trackId === undefined)
1097
+ continue;
1098
+ const found = findTrackSite(state.presentation, trackId);
1099
+ const rendition = found?.track.renditions[0];
1100
+ if (found === null || rendition === undefined)
1101
+ continue;
1102
+ const inflight = [];
1103
+ for (const request of state.scheduling.inflight.values()) {
1104
+ if (request.trackId === trackId)
1105
+ inflight.push(request);
1106
+ }
1107
+ tracks.push({
1108
+ trackId,
1109
+ period: found.period,
1110
+ rendition,
1111
+ ranges: state.cues.get(trackId) ?? [],
1112
+ inflight,
1113
+ });
1114
+ }
1115
+ const result = schedule({
1116
+ currentTime: state.playback.currentTime,
1117
+ bufferGoal: state.scheduling.bufferGoal,
1118
+ tokenSeq: state.scheduling.tokenSeq + initFetches.length,
1119
+ tracks,
1120
+ liveWindow: state.presentation.isLive ? (state.live?.span ?? null) : null,
1121
+ mediaTimeNormalized: cfg.mediaTimeNormalized,
1122
+ });
1123
+ // The scheduling breaker: an identical decision repeated past the limit
1124
+ // means appends report success but nothing ever progresses, and the
1125
+ // engine would fetch the same bytes forever. Halt fatally instead.
1126
+ const decisionKey = [
1127
+ ...initFetches.map((p) => `${p.trackId}:init:${p.rendition}`),
1128
+ ...result.requests.map((r) => `${r.trackId}:${r.seq}:${r.renditionId ?? ''}`),
1129
+ ].join('|');
1130
+ let repeat = state.scheduling.repeat;
1131
+ if (decisionKey !== '') {
1132
+ repeat =
1133
+ repeat !== undefined && repeat.key === decisionKey
1134
+ ? { key: decisionKey, count: repeat.count + 1 }
1135
+ : { key: decisionKey, count: 1 };
1136
+ if (repeat.count > cfg.repeatFetchLimit) {
1137
+ effects.push({
1138
+ kind: 'emit',
1139
+ event: 'error',
1140
+ payload: {
1141
+ category: 'internal',
1142
+ code: 'INTERNAL_ASSERTION',
1143
+ fatal: true,
1144
+ recoverable: false,
1145
+ context: { reason: 'the same fetch decision repeated without progress', decisionKey },
1146
+ },
1147
+ });
1148
+ return [
1149
+ {
1150
+ ...state,
1151
+ quality,
1152
+ lifecycle: { phase: 'error' },
1153
+ scheduling: { ...state.scheduling, repeat },
1154
+ },
1155
+ effects,
1156
+ ];
1157
+ }
1158
+ }
1159
+ let tokenSeq = state.scheduling.tokenSeq;
1160
+ const inflight = new Map(state.scheduling.inflight);
1161
+ for (const pending of initFetches) {
1162
+ tokenSeq += 1;
1163
+ const token = `t${tokenSeq}:${pending.trackId}:init`;
1164
+ effects.push({
1165
+ kind: 'fetch',
1166
+ token,
1167
+ url: pending.init.url,
1168
+ ...(pending.init.byteRange !== undefined ? { range: pending.init.byteRange } : {}),
1169
+ });
1170
+ inflight.set(token, {
1171
+ token,
1172
+ trackId: pending.trackId,
1173
+ seq: -1,
1174
+ url: pending.init.url,
1175
+ sbId: pending.sbId,
1176
+ renditionId: pending.rendition,
1177
+ });
1178
+ }
1179
+ for (const effect of result.effects)
1180
+ effects.push(effect);
1181
+ for (const request of result.requests) {
1182
+ inflight.set(request.token, request);
1183
+ }
1184
+ // VOD end: every track has nothing left to fetch and the buffers reach
1185
+ // the announced duration. endOfStream is safe to re-emit; the handler
1186
+ // no-ops once the source has left 'open'.
1187
+ // Only SourceBuffer tracks gate the end: a subtitle playlist shorter
1188
+ // than the video must not hold the stream open.
1189
+ const mediaTracks = tracks.filter((track) => track.sbId !== undefined);
1190
+ if (state.presentation.duration !== undefined &&
1191
+ initFetches.length === 0 &&
1192
+ result.effects.length === 0 &&
1193
+ mediaTracks.length > 0 &&
1194
+ mediaTracks.every((track) => {
1195
+ const end = bufferedEndFrom(track.ranges, state.playback.currentTime, 0.25);
1196
+ return end >= (state.presentation?.duration ?? 0) - 0.5;
1197
+ })) {
1198
+ effects.push({ kind: 'endOfStream' });
1199
+ }
1200
+ if (effects.length === 0 && quality === state.quality)
1201
+ return [state, []];
1202
+ return [
1203
+ {
1204
+ ...state,
1205
+ quality,
1206
+ scheduling: {
1207
+ ...state.scheduling,
1208
+ inflight,
1209
+ tokenSeq: result.tokenSeq,
1210
+ ...(repeat !== undefined ? { repeat } : {}),
1211
+ },
1212
+ },
1213
+ effects,
1214
+ ];
1215
+ }
1216
+ const arbiters = new WeakMap();
1217
+ function arbiterFor(hooks, contentType) {
1218
+ let pair = arbiters.get(hooks);
1219
+ if (pair === undefined) {
1220
+ pair = { video: createArbiter(), audio: createArbiter() };
1221
+ arbiters.set(hooks, pair);
1222
+ }
1223
+ return pair[contentType];
1224
+ }
1225
+ const COMMAND_TYPES = {
1226
+ ATTACH: true,
1227
+ DETACH: true,
1228
+ LOAD: true,
1229
+ UNLOAD: true,
1230
+ SEEK: true,
1231
+ SEEK_TO_LIVE_EDGE: true,
1232
+ DESELECT_TRACK: true,
1233
+ SELECT_TRACK: true,
1234
+ PIN_RENDITION: true,
1235
+ RELEASE_PIN: true,
1236
+ CONSTRAIN: true,
1237
+ RELEASE_CONSTRAINT: true,
1238
+ SET_BUFFER_GOAL: true,
1239
+ ABORT_INFLIGHT: true,
1240
+ };
1241
+ export function isCommand(msg) {
1242
+ return msg.type in COMMAND_TYPES;
1243
+ }
1244
+ /**
1245
+ * Builds the root reducer from the built-in kernel logic plus stage slices.
1246
+ * A slice reducer receives its own slice (undefined on first run) and a
1247
+ * read-only view of kernel state. A throwing slice reducer is contained: its
1248
+ * slice keeps the previous value and an error event effect is emitted, so
1249
+ * one broken stage cannot corrupt kernel state.
1250
+ */
1251
+ export function createReducer(slices, config, hooks = {}) {
1252
+ const sliceList = slices ? [...slices] : [];
1253
+ const cfg = resolveConfig(config);
1254
+ const DRIVING_FACTS = new Set([
1255
+ 'MANIFEST_LOADED',
1256
+ 'SEGMENT_LOADED',
1257
+ 'SOURCEBUFFER_UPDATEEND',
1258
+ 'SEEKING',
1259
+ 'SEEKED',
1260
+ 'LIVE_WINDOW_CHANGED',
1261
+ // A failed fetch re-drives on a scheduled backoff TICK, not synchronously,
1262
+ // so recovery's commands change the decision before the retry fires.
1263
+ 'TICK',
1264
+ ]);
1265
+ return (state, msg) => {
1266
+ // Manifest bytes answer exactly one request; the kernel notes which so
1267
+ // it can tell, after the slices ran, whether any adapter claimed them.
1268
+ const manifestRequest = !isCommand(msg) && msg.type === 'SEGMENT_LOADED' ? manifestRequestFor(state, msg) : null;
1269
+ let [next, effects] = isCommand(msg)
1270
+ ? reduceCommand(state, msg, cfg, hooks)
1271
+ : reduceFact(state, msg, cfg, hooks);
1272
+ // The buffer-goal loop runs on every fact that can change what to
1273
+ // fetch next. TIME_UPDATE drives inside its own reduction; the others
1274
+ // drive here, which is what makes startup work on a paused element
1275
+ // that fires no timeupdate.
1276
+ if (!isCommand(msg) && DRIVING_FACTS.has(msg.type)) {
1277
+ const [driven, driveEffects] = driveScheduling(next, hooks, cfg);
1278
+ next = driven;
1279
+ if (driveEffects.length > 0)
1280
+ effects = [...effects, ...driveEffects];
1281
+ }
1282
+ // An adapter claims manifest bytes by acting on them: a loop-back feed
1283
+ // of MANIFEST_LOADED or MANIFEST_FAILED, or a further fetch. Events are
1284
+ // observation, not a claim. Bytes nobody acts on would otherwise leave
1285
+ // the engine in `loading` forever, or misreport as a parse failure.
1286
+ let claimed = manifestRequest === null;
1287
+ for (const [name, slice] of sliceList) {
1288
+ try {
1289
+ const [sliceState, sliceEffects] = slice(next[name], msg, next);
1290
+ next = { ...next, [name]: sliceState };
1291
+ if (sliceEffects.length > 0) {
1292
+ effects = [...effects, ...sliceEffects];
1293
+ if (sliceEffects.some((effect) => effect.kind !== 'emit'))
1294
+ claimed = true;
1295
+ }
1296
+ }
1297
+ catch (err) {
1298
+ effects = [
1299
+ ...effects,
1300
+ {
1301
+ kind: 'emit',
1302
+ event: 'kernel:slice-error',
1303
+ payload: { slice: name, message: String(err) },
1304
+ },
1305
+ ];
1306
+ }
1307
+ }
1308
+ if (!claimed && manifestRequest !== null && next.lifecycle.phase === 'loading') {
1309
+ const [failed, failEffects] = failManifest(next, { category: 'manifest', code: 'MANIFEST_UNSUPPORTED', fatal: true, recoverable: false }, { url: manifestRequest.url });
1310
+ next = failed;
1311
+ effects = [...effects, ...failEffects];
1312
+ }
1313
+ return [next, effects];
1314
+ };
1315
+ }
1316
+ //# sourceMappingURL=reducer.js.map