@blockcast/mmt-container 0.1.0 → 0.3.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 (799) hide show
  1. package/README.md +524 -0
  2. package/dist/.build-stamp +0 -0
  3. package/dist/__tests__/test-utils.d.ts +71 -0
  4. package/dist/__tests__/test-utils.d.ts.map +1 -0
  5. package/dist/align.d.ts +152 -0
  6. package/dist/align.d.ts.map +1 -0
  7. package/dist/align.js +88 -0
  8. package/dist/align.js.map +7 -0
  9. package/dist/audio-init-parser.d.ts +66 -0
  10. package/dist/audio-init-parser.d.ts.map +1 -0
  11. package/dist/audio-init-parser.js +118 -0
  12. package/dist/audio-init-parser.js.map +7 -0
  13. package/dist/audio-ring-buffer.d.ts +50 -0
  14. package/dist/audio-ring-buffer.d.ts.map +1 -0
  15. package/dist/audio-ring-buffer.js +80 -0
  16. package/dist/audio-ring-buffer.js.map +7 -0
  17. package/dist/authored-timing.d.ts +32 -0
  18. package/dist/authored-timing.d.ts.map +1 -0
  19. package/dist/authored-timing.js +32 -0
  20. package/dist/authored-timing.js.map +7 -0
  21. package/dist/block-policy.d.ts +314 -0
  22. package/dist/block-policy.d.ts.map +1 -0
  23. package/dist/block-policy.js +466 -0
  24. package/dist/block-policy.js.map +7 -0
  25. package/dist/browser-runtime.js +26549 -0
  26. package/dist/browser-runtime.js.map +7 -0
  27. package/dist/carp-catalog.d.ts +429 -0
  28. package/dist/carp-catalog.d.ts.map +1 -0
  29. package/dist/carp-catalog.js +985 -0
  30. package/dist/carp-catalog.js.map +7 -0
  31. package/dist/catalog-fec-params.d.ts +275 -0
  32. package/dist/catalog-fec-params.d.ts.map +1 -0
  33. package/dist/catalog-fec-params.js +702 -0
  34. package/dist/catalog-fec-params.js.map +7 -0
  35. package/dist/catalog-network-source.d.ts +42 -0
  36. package/dist/catalog-network-source.d.ts.map +1 -0
  37. package/dist/catalog-network-source.js +47 -0
  38. package/dist/catalog-network-source.js.map +7 -0
  39. package/dist/catalog-validator.d.ts +18 -0
  40. package/dist/catalog-validator.d.ts.map +1 -0
  41. package/dist/catalog-validator.js +7747 -0
  42. package/dist/catalog-validator.js.map +7 -0
  43. package/dist/cmaf-block-assembler.d.ts +285 -0
  44. package/dist/cmaf-block-assembler.d.ts.map +1 -0
  45. package/dist/cmaf-block-assembler.js +1966 -0
  46. package/dist/cmaf-block-assembler.js.map +7 -0
  47. package/dist/cmaf-group-batcher.d.ts +38 -0
  48. package/dist/cmaf-group-batcher.d.ts.map +1 -0
  49. package/dist/cmaf-group-batcher.js +176 -0
  50. package/dist/cmaf-group-batcher.js.map +7 -0
  51. package/dist/cmcd-producer.d.ts +104 -0
  52. package/dist/cmcd-producer.d.ts.map +1 -0
  53. package/dist/cmcd-producer.js +183 -0
  54. package/dist/cmcd-producer.js.map +7 -0
  55. package/dist/codec-utils.d.ts +384 -0
  56. package/dist/codec-utils.d.ts.map +1 -0
  57. package/dist/codec-utils.js +1546 -0
  58. package/dist/codec-utils.js.map +7 -0
  59. package/dist/conceal-timeline.d.ts +146 -0
  60. package/dist/conceal-timeline.d.ts.map +1 -0
  61. package/dist/conceal-timeline.js +137 -0
  62. package/dist/conceal-timeline.js.map +7 -0
  63. package/dist/delivery-gap.d.ts +75 -0
  64. package/dist/delivery-gap.d.ts.map +1 -0
  65. package/dist/delivery-gap.js +88 -0
  66. package/dist/delivery-gap.js.map +7 -0
  67. package/dist/duplicate-ledger.d.ts +197 -0
  68. package/dist/duplicate-ledger.d.ts.map +1 -0
  69. package/dist/duplicate-ledger.js +226 -0
  70. package/dist/duplicate-ledger.js.map +7 -0
  71. package/dist/fec-codec.d.ts +32 -0
  72. package/dist/fec-codec.d.ts.map +1 -0
  73. package/dist/fec-codec.js +47 -0
  74. package/dist/fec-codec.js.map +7 -0
  75. package/dist/fec-manager.d.ts +583 -0
  76. package/dist/fec-manager.d.ts.map +1 -0
  77. package/dist/fec-manager.js +3899 -0
  78. package/dist/fec-manager.js.map +7 -0
  79. package/dist/fec-starvation-detector.d.ts +59 -0
  80. package/dist/fec-starvation-detector.d.ts.map +1 -0
  81. package/dist/fec-starvation-detector.js +60 -0
  82. package/dist/fec-starvation-detector.js.map +7 -0
  83. package/dist/fec-state.d.ts +131 -0
  84. package/dist/fec-state.d.ts.map +1 -0
  85. package/dist/fec-state.js +87 -0
  86. package/dist/fec-state.js.map +7 -0
  87. package/dist/fec-worker-setup.d.ts +94 -0
  88. package/dist/fec-worker-setup.d.ts.map +1 -0
  89. package/dist/fec-worker-setup.js +673 -0
  90. package/dist/fec-worker-setup.js.map +7 -0
  91. package/dist/fmp4-completion.d.ts +30 -0
  92. package/dist/fmp4-completion.d.ts.map +1 -0
  93. package/dist/fmp4-completion.js +21 -0
  94. package/dist/fmp4-completion.js.map +7 -0
  95. package/dist/fmp4-reader.d.ts +37 -0
  96. package/dist/fmp4-reader.d.ts.map +1 -0
  97. package/dist/fmp4-reader.js +100 -0
  98. package/dist/fmp4-reader.js.map +7 -0
  99. package/dist/index.d.ts +44 -3
  100. package/dist/index.d.ts.map +1 -1
  101. package/dist/index.js +460 -14
  102. package/dist/index.js.map +7 -1
  103. package/dist/iso-block-locator.d.ts +118 -0
  104. package/dist/iso-block-locator.d.ts.map +1 -0
  105. package/dist/iso-block-locator.js +289 -0
  106. package/dist/iso-block-locator.js.map +7 -0
  107. package/dist/iso-bmff-timing.d.ts +78 -0
  108. package/dist/iso-bmff-timing.d.ts.map +1 -0
  109. package/dist/iso-bmff-timing.js +593 -0
  110. package/dist/iso-bmff-timing.js.map +7 -0
  111. package/dist/loc.d.ts +70 -75
  112. package/dist/loc.d.ts.map +1 -1
  113. package/dist/loc.js +106 -192
  114. package/dist/loc.js.map +7 -1
  115. package/dist/manifest-auth-plan.d.ts +30 -0
  116. package/dist/manifest-auth-plan.d.ts.map +1 -0
  117. package/dist/manifest-auth-plan.js +1266 -0
  118. package/dist/manifest-auth-plan.js.map +7 -0
  119. package/dist/media-router.d.ts +262 -0
  120. package/dist/media-router.d.ts.map +1 -0
  121. package/dist/media-router.js +584 -0
  122. package/dist/media-router.js.map +7 -0
  123. package/dist/mfu-reassembler.d.ts +313 -9
  124. package/dist/mfu-reassembler.d.ts.map +1 -1
  125. package/dist/mfu-reassembler.js +3051 -285
  126. package/dist/mfu-reassembler.js.map +7 -1
  127. package/dist/mfu-reassembly-policy.d.ts +24 -0
  128. package/dist/mfu-reassembly-policy.d.ts.map +1 -0
  129. package/dist/mfu-reassembly-policy.js +99 -0
  130. package/dist/mfu-reassembly-policy.js.map +7 -0
  131. package/dist/mmtp-container-parser.d.ts +568 -0
  132. package/dist/mmtp-container-parser.d.ts.map +1 -0
  133. package/dist/mmtp-container-parser.js +6580 -0
  134. package/dist/mmtp-container-parser.js.map +7 -0
  135. package/dist/mmtp-container-parser.no-modules.js +9616 -0
  136. package/dist/mmtp-container-parser.no-modules.js.map +7 -0
  137. package/dist/mmtp-decode-pipeline.d.ts +138 -0
  138. package/dist/mmtp-decode-pipeline.d.ts.map +1 -0
  139. package/dist/mmtp-decode-pipeline.js +6814 -0
  140. package/dist/mmtp-decode-pipeline.js.map +7 -0
  141. package/dist/mmtp-router.d.ts +679 -0
  142. package/dist/mmtp-router.d.ts.map +1 -0
  143. package/dist/mmtp-router.js +11278 -0
  144. package/dist/mmtp-router.js.map +7 -0
  145. package/dist/mmtp-to-cmaf.d.ts +187 -0
  146. package/dist/mmtp-to-cmaf.d.ts.map +1 -0
  147. package/dist/mmtp-to-cmaf.js +716 -0
  148. package/dist/mmtp-to-cmaf.js.map +7 -0
  149. package/dist/multicast-auth-catalog.d.ts +115 -0
  150. package/dist/multicast-auth-catalog.d.ts.map +1 -0
  151. package/dist/multicast-auth-catalog.js +202 -0
  152. package/dist/multicast-auth-catalog.js.map +7 -0
  153. package/dist/multicast-input.d.ts +141 -0
  154. package/dist/multicast-input.d.ts.map +1 -0
  155. package/dist/multicast-input.js +390 -0
  156. package/dist/multicast-input.js.map +7 -0
  157. package/dist/multicast-manager.d.ts +669 -0
  158. package/dist/multicast-manager.d.ts.map +1 -0
  159. package/dist/multicast-manager.js +19101 -0
  160. package/dist/multicast-manager.js.map +7 -0
  161. package/dist/multicast-transport.d.ts +271 -0
  162. package/dist/multicast-transport.d.ts.map +1 -0
  163. package/dist/multicast-transport.js +2264 -0
  164. package/dist/multicast-transport.js.map +7 -0
  165. package/dist/multicast-unwrap.d.ts +57 -0
  166. package/dist/multicast-unwrap.d.ts.map +1 -0
  167. package/dist/multicast-unwrap.js +418 -0
  168. package/dist/multicast-unwrap.js.map +7 -0
  169. package/dist/no-modules.d.ts +10 -0
  170. package/dist/no-modules.d.ts.map +1 -0
  171. package/dist/no-modules.js +9491 -0
  172. package/dist/no-modules.js.map +7 -0
  173. package/dist/phase14-catalog-override.d.ts +126 -0
  174. package/dist/phase14-catalog-override.d.ts.map +1 -0
  175. package/dist/phase14-catalog-override.js +155 -0
  176. package/dist/phase14-catalog-override.js.map +7 -0
  177. package/dist/repair-fetch.d.ts +270 -0
  178. package/dist/repair-fetch.d.ts.map +1 -0
  179. package/dist/repair-fetch.js +381 -0
  180. package/dist/repair-fetch.js.map +7 -0
  181. package/dist/repair-layers.d.ts +158 -0
  182. package/dist/repair-layers.d.ts.map +1 -0
  183. package/dist/repair-layers.js +344 -0
  184. package/dist/repair-layers.js.map +7 -0
  185. package/dist/sample-combiner.d.ts +212 -0
  186. package/dist/sample-combiner.d.ts.map +1 -0
  187. package/dist/sample-combiner.js +595 -0
  188. package/dist/sample-combiner.js.map +7 -0
  189. package/dist/signaling.d.ts +29 -0
  190. package/dist/signaling.d.ts.map +1 -0
  191. package/dist/signaling.js +572 -0
  192. package/dist/signaling.js.map +7 -0
  193. package/dist/source-auth.d.ts +2 -0
  194. package/dist/source-auth.d.ts.map +1 -0
  195. package/dist/source-auth.js +1209 -0
  196. package/dist/source-auth.js.map +7 -0
  197. package/dist/switching-set-alignment.d.ts +61 -0
  198. package/dist/switching-set-alignment.d.ts.map +1 -0
  199. package/dist/switching-set-alignment.js +70 -0
  200. package/dist/switching-set-alignment.js.map +7 -0
  201. package/dist/timestamp-codec.d.ts +34 -0
  202. package/dist/timestamp-codec.d.ts.map +1 -0
  203. package/dist/timestamp-codec.js +473 -0
  204. package/dist/timestamp-codec.js.map +7 -0
  205. package/dist/track-pipeline-setup.d.ts +83 -0
  206. package/dist/track-pipeline-setup.d.ts.map +1 -0
  207. package/dist/track-pipeline-setup.js +291 -0
  208. package/dist/track-pipeline-setup.js.map +7 -0
  209. package/dist/track-pipeline.d.ts +121 -0
  210. package/dist/track-pipeline.d.ts.map +1 -0
  211. package/dist/track-pipeline.js +209 -0
  212. package/dist/track-pipeline.js.map +7 -0
  213. package/dist/track-session.d.ts +414 -0
  214. package/dist/track-session.d.ts.map +1 -0
  215. package/dist/track-session.js +754 -0
  216. package/dist/track-session.js.map +7 -0
  217. package/dist/transport-pool.d.ts +69 -0
  218. package/dist/transport-pool.d.ts.map +1 -0
  219. package/dist/transport-pool.js +74 -0
  220. package/dist/transport-pool.js.map +7 -0
  221. package/dist/wasm-mfu-reassembler.d.ts +97 -0
  222. package/dist/wasm-mfu-reassembler.d.ts.map +1 -0
  223. package/dist/wasm-mfu-reassembler.js +2133 -0
  224. package/dist/wasm-mfu-reassembler.js.map +7 -0
  225. package/dist/wasm-mmtp-container-parser.d.ts +10 -0
  226. package/dist/wasm-mmtp-container-parser.d.ts.map +1 -0
  227. package/dist/wasm-mmtp-container-parser.js +6834 -0
  228. package/dist/wasm-mmtp-container-parser.js.map +7 -0
  229. package/node_modules/@blockcast/fec-worker/README.md +223 -0
  230. package/node_modules/@blockcast/fec-worker/dist/.build-stamp +0 -0
  231. package/node_modules/@blockcast/fec-worker/dist/block-order.d.ts +38 -0
  232. package/node_modules/@blockcast/fec-worker/dist/block-order.d.ts.map +1 -0
  233. package/node_modules/@blockcast/fec-worker/dist/block-order.js +78 -0
  234. package/node_modules/@blockcast/fec-worker/dist/block-order.js.map +1 -0
  235. package/node_modules/@blockcast/fec-worker/dist/fec-worker-client.d.ts +93 -0
  236. package/node_modules/@blockcast/fec-worker/dist/fec-worker-client.d.ts.map +1 -0
  237. package/node_modules/@blockcast/fec-worker/dist/fec-worker-client.js +346 -0
  238. package/node_modules/@blockcast/fec-worker/dist/fec-worker-client.js.map +1 -0
  239. package/node_modules/@blockcast/fec-worker/dist/fec-worker-types.d.ts +380 -0
  240. package/node_modules/@blockcast/fec-worker/dist/fec-worker-types.d.ts.map +1 -0
  241. package/node_modules/@blockcast/fec-worker/dist/fec-worker-types.js +11 -0
  242. package/node_modules/@blockcast/fec-worker/dist/fec-worker-types.js.map +1 -0
  243. package/node_modules/@blockcast/fec-worker/dist/fec-worker.d.ts +14 -0
  244. package/node_modules/@blockcast/fec-worker/dist/fec-worker.d.ts.map +1 -0
  245. package/node_modules/@blockcast/fec-worker/dist/fec-worker.js +859 -0
  246. package/node_modules/@blockcast/fec-worker/dist/fec-worker.js.map +7 -0
  247. package/node_modules/@blockcast/fec-worker/dist/index.d.ts +10 -0
  248. package/node_modules/@blockcast/fec-worker/dist/index.d.ts.map +1 -0
  249. package/node_modules/@blockcast/fec-worker/dist/index.js +8 -0
  250. package/node_modules/@blockcast/fec-worker/dist/index.js.map +1 -0
  251. package/node_modules/@blockcast/fec-worker/dist/shred-fec-worker.d.ts +20 -0
  252. package/node_modules/@blockcast/fec-worker/dist/shred-fec-worker.d.ts.map +1 -0
  253. package/node_modules/@blockcast/fec-worker/dist/shred-fec-worker.js +370 -0
  254. package/node_modules/@blockcast/fec-worker/dist/shred-fec-worker.js.map +7 -0
  255. package/node_modules/@blockcast/fec-worker/dist/shred-worker-client.d.ts +104 -0
  256. package/node_modules/@blockcast/fec-worker/dist/shred-worker-client.d.ts.map +1 -0
  257. package/node_modules/@blockcast/fec-worker/dist/shred-worker-client.js +181 -0
  258. package/node_modules/@blockcast/fec-worker/dist/shred-worker-client.js.map +1 -0
  259. package/node_modules/@blockcast/fec-worker/dist/shred-worker-types.d.ts +194 -0
  260. package/node_modules/@blockcast/fec-worker/dist/shred-worker-types.d.ts.map +1 -0
  261. package/node_modules/@blockcast/fec-worker/dist/shred-worker-types.js +62 -0
  262. package/node_modules/@blockcast/fec-worker/dist/shred-worker-types.js.map +1 -0
  263. package/node_modules/@blockcast/fec-worker/dist/transfer.d.ts +9 -0
  264. package/node_modules/@blockcast/fec-worker/dist/transfer.d.ts.map +1 -0
  265. package/node_modules/@blockcast/fec-worker/dist/transfer.js +13 -0
  266. package/node_modules/@blockcast/fec-worker/dist/transfer.js.map +1 -0
  267. package/node_modules/@blockcast/fec-worker/package.json +59 -0
  268. package/node_modules/@blockcast/fec-worker/src/block-order.ts +93 -0
  269. package/node_modules/@blockcast/fec-worker/src/fec-worker-client.ts +392 -0
  270. package/node_modules/@blockcast/fec-worker/src/fec-worker-types.ts +399 -0
  271. package/node_modules/@blockcast/fec-worker/src/fec-worker.ts +1502 -0
  272. package/node_modules/@blockcast/fec-worker/src/index.ts +35 -0
  273. package/node_modules/@blockcast/fec-worker/src/shred-fec-worker.ts +589 -0
  274. package/node_modules/@blockcast/fec-worker/src/shred-worker-client.ts +222 -0
  275. package/node_modules/@blockcast/fec-worker/src/shred-worker-types.ts +209 -0
  276. package/node_modules/@blockcast/fec-worker/src/transfer.ts +12 -0
  277. package/node_modules/@blockcast/mmt-alta-parse/README.md +181 -0
  278. package/node_modules/@blockcast/mmt-alta-parse/dist/.build-stamp +0 -0
  279. package/node_modules/@blockcast/mmt-alta-parse/dist/index.d.ts +57 -0
  280. package/node_modules/@blockcast/mmt-alta-parse/dist/index.d.ts.map +1 -0
  281. package/node_modules/@blockcast/mmt-alta-parse/dist/index.js +115 -0
  282. package/node_modules/@blockcast/mmt-alta-parse/dist/index.js.map +1 -0
  283. package/node_modules/@blockcast/mmt-alta-parse/package.json +50 -0
  284. package/node_modules/@blockcast/mmt-alta-parse/src/index.ts +136 -0
  285. package/node_modules/@blockcast/mmt-base/README.md +134 -0
  286. package/node_modules/@blockcast/mmt-base/dist/.build-stamp +0 -0
  287. package/node_modules/@blockcast/mmt-base/dist/fec-block-profile.d.ts +55 -0
  288. package/node_modules/@blockcast/mmt-base/dist/fec-block-profile.d.ts.map +1 -0
  289. package/node_modules/@blockcast/mmt-base/dist/fec-block-profile.js +128 -0
  290. package/node_modules/@blockcast/mmt-base/dist/fec-block-profile.js.map +1 -0
  291. package/node_modules/@blockcast/mmt-base/dist/index.d.ts +7 -0
  292. package/node_modules/@blockcast/mmt-base/dist/index.d.ts.map +1 -0
  293. package/node_modules/@blockcast/mmt-base/dist/index.js +7 -0
  294. package/node_modules/@blockcast/mmt-base/dist/index.js.map +1 -0
  295. package/node_modules/@blockcast/mmt-base/dist/mmtp.d.ts +292 -0
  296. package/node_modules/@blockcast/mmt-base/dist/mmtp.d.ts.map +1 -0
  297. package/node_modules/@blockcast/mmt-base/dist/mmtp.js +612 -0
  298. package/node_modules/@blockcast/mmt-base/dist/mmtp.js.map +1 -0
  299. package/node_modules/@blockcast/mmt-base/dist/net-constants.d.ts +18 -0
  300. package/node_modules/@blockcast/mmt-base/dist/net-constants.d.ts.map +1 -0
  301. package/node_modules/@blockcast/mmt-base/dist/net-constants.js +18 -0
  302. package/node_modules/@blockcast/mmt-base/dist/net-constants.js.map +1 -0
  303. package/node_modules/@blockcast/mmt-base/dist/ntp-short.d.ts +47 -0
  304. package/node_modules/@blockcast/mmt-base/dist/ntp-short.d.ts.map +1 -0
  305. package/node_modules/@blockcast/mmt-base/dist/ntp-short.js +54 -0
  306. package/node_modules/@blockcast/mmt-base/dist/ntp-short.js.map +1 -0
  307. package/node_modules/@blockcast/mmt-base/dist/serial-number.d.ts +57 -0
  308. package/node_modules/@blockcast/mmt-base/dist/serial-number.d.ts.map +1 -0
  309. package/node_modules/@blockcast/mmt-base/dist/serial-number.js +98 -0
  310. package/node_modules/@blockcast/mmt-base/dist/serial-number.js.map +1 -0
  311. package/node_modules/@blockcast/mmt-base/dist/signalling.d.ts +312 -0
  312. package/node_modules/@blockcast/mmt-base/dist/signalling.d.ts.map +1 -0
  313. package/node_modules/@blockcast/mmt-base/dist/signalling.js +637 -0
  314. package/node_modules/@blockcast/mmt-base/dist/signalling.js.map +1 -0
  315. package/node_modules/@blockcast/mmt-base/package.json +49 -0
  316. package/node_modules/@blockcast/mmt-base/src/fec-block-profile.ts +145 -0
  317. package/node_modules/@blockcast/mmt-base/src/index.ts +6 -0
  318. package/node_modules/@blockcast/mmt-base/src/mmtp.ts +892 -0
  319. package/node_modules/@blockcast/mmt-base/src/net-constants.ts +17 -0
  320. package/node_modules/@blockcast/mmt-base/src/ntp-short.ts +67 -0
  321. package/node_modules/@blockcast/mmt-base/src/serial-number.ts +107 -0
  322. package/node_modules/@blockcast/mmt-base/src/signalling.ts +862 -0
  323. package/node_modules/@blockcast/mmt-fec/README.md +271 -0
  324. package/node_modules/@blockcast/mmt-fec/dist/.build-stamp +0 -0
  325. package/node_modules/@blockcast/mmt-fec/dist/block-manager.d.ts +82 -0
  326. package/node_modules/@blockcast/mmt-fec/dist/block-manager.d.ts.map +1 -0
  327. package/node_modules/@blockcast/mmt-fec/dist/block-manager.js +296 -0
  328. package/node_modules/@blockcast/mmt-fec/dist/block-manager.js.map +1 -0
  329. package/node_modules/@blockcast/mmt-fec/dist/block-processor.d.ts +106 -0
  330. package/node_modules/@blockcast/mmt-fec/dist/block-processor.d.ts.map +1 -0
  331. package/node_modules/@blockcast/mmt-fec/dist/block-processor.js +288 -0
  332. package/node_modules/@blockcast/mmt-fec/dist/block-processor.js.map +1 -0
  333. package/node_modules/@blockcast/mmt-fec/dist/cmaf-recovery.d.ts +46 -0
  334. package/node_modules/@blockcast/mmt-fec/dist/cmaf-recovery.d.ts.map +1 -0
  335. package/node_modules/@blockcast/mmt-fec/dist/cmaf-recovery.js +96 -0
  336. package/node_modules/@blockcast/mmt-fec/dist/cmaf-recovery.js.map +1 -0
  337. package/node_modules/@blockcast/mmt-fec/dist/cmaf-symbols.d.ts +33 -0
  338. package/node_modules/@blockcast/mmt-fec/dist/cmaf-symbols.d.ts.map +1 -0
  339. package/node_modules/@blockcast/mmt-fec/dist/cmaf-symbols.js +57 -0
  340. package/node_modules/@blockcast/mmt-fec/dist/cmaf-symbols.js.map +1 -0
  341. package/node_modules/@blockcast/mmt-fec/dist/fec-block-mapper.d.ts +205 -0
  342. package/node_modules/@blockcast/mmt-fec/dist/fec-block-mapper.d.ts.map +1 -0
  343. package/node_modules/@blockcast/mmt-fec/dist/fec-block-mapper.js +240 -0
  344. package/node_modules/@blockcast/mmt-fec/dist/fec-block-mapper.js.map +1 -0
  345. package/node_modules/@blockcast/mmt-fec/dist/index.d.ts +28 -0
  346. package/node_modules/@blockcast/mmt-fec/dist/index.d.ts.map +1 -0
  347. package/node_modules/@blockcast/mmt-fec/dist/index.js +28 -0
  348. package/node_modules/@blockcast/mmt-fec/dist/index.js.map +1 -0
  349. package/node_modules/@blockcast/mmt-fec/dist/loc-symbols.d.ts +20 -0
  350. package/node_modules/@blockcast/mmt-fec/dist/loc-symbols.d.ts.map +1 -0
  351. package/node_modules/@blockcast/mmt-fec/dist/loc-symbols.js +28 -0
  352. package/node_modules/@blockcast/mmt-fec/dist/loc-symbols.js.map +1 -0
  353. package/node_modules/@blockcast/mmt-fec/dist/oti.d.ts +258 -0
  354. package/node_modules/@blockcast/mmt-fec/dist/oti.d.ts.map +1 -0
  355. package/node_modules/@blockcast/mmt-fec/dist/oti.js +382 -0
  356. package/node_modules/@blockcast/mmt-fec/dist/oti.js.map +1 -0
  357. package/node_modules/@blockcast/mmt-fec/dist/reorder-buffer.d.ts +41 -0
  358. package/node_modules/@blockcast/mmt-fec/dist/reorder-buffer.d.ts.map +1 -0
  359. package/node_modules/@blockcast/mmt-fec/dist/reorder-buffer.js +81 -0
  360. package/node_modules/@blockcast/mmt-fec/dist/reorder-buffer.js.map +1 -0
  361. package/node_modules/@blockcast/mmt-fec/dist/repair-group.d.ts +82 -0
  362. package/node_modules/@blockcast/mmt-fec/dist/repair-group.d.ts.map +1 -0
  363. package/node_modules/@blockcast/mmt-fec/dist/repair-group.js +170 -0
  364. package/node_modules/@blockcast/mmt-fec/dist/repair-group.js.map +1 -0
  365. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-alta-parse/README.md +181 -0
  366. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-alta-parse/dist/.build-stamp +0 -0
  367. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-alta-parse/dist/index.d.ts +57 -0
  368. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-alta-parse/dist/index.d.ts.map +1 -0
  369. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-alta-parse/dist/index.js +115 -0
  370. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-alta-parse/dist/index.js.map +1 -0
  371. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-alta-parse/package.json +50 -0
  372. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-alta-parse/src/index.ts +136 -0
  373. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/README.md +134 -0
  374. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/.build-stamp +0 -0
  375. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/fec-block-profile.d.ts +55 -0
  376. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/fec-block-profile.d.ts.map +1 -0
  377. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/fec-block-profile.js +128 -0
  378. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/fec-block-profile.js.map +1 -0
  379. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/index.d.ts +7 -0
  380. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/index.d.ts.map +1 -0
  381. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/index.js +7 -0
  382. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/index.js.map +1 -0
  383. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/mmtp.d.ts +292 -0
  384. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/mmtp.d.ts.map +1 -0
  385. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/mmtp.js +612 -0
  386. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/mmtp.js.map +1 -0
  387. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/net-constants.d.ts +18 -0
  388. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/net-constants.d.ts.map +1 -0
  389. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/net-constants.js +18 -0
  390. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/net-constants.js.map +1 -0
  391. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/ntp-short.d.ts +47 -0
  392. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/ntp-short.d.ts.map +1 -0
  393. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/ntp-short.js +54 -0
  394. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/ntp-short.js.map +1 -0
  395. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/serial-number.d.ts +57 -0
  396. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/serial-number.d.ts.map +1 -0
  397. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/serial-number.js +98 -0
  398. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/serial-number.js.map +1 -0
  399. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/signalling.d.ts +312 -0
  400. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/signalling.d.ts.map +1 -0
  401. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/signalling.js +637 -0
  402. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/signalling.js.map +1 -0
  403. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/package.json +49 -0
  404. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/src/fec-block-profile.ts +145 -0
  405. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/src/index.ts +6 -0
  406. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/src/mmtp.ts +892 -0
  407. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/src/net-constants.ts +17 -0
  408. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/src/ntp-short.ts +67 -0
  409. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/src/serial-number.ts +107 -0
  410. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/src/signalling.ts +862 -0
  411. package/node_modules/@blockcast/mmt-fec/package.json +79 -0
  412. package/node_modules/@blockcast/mmt-fec/src/block-manager.ts +371 -0
  413. package/node_modules/@blockcast/mmt-fec/src/block-processor.ts +439 -0
  414. package/node_modules/@blockcast/mmt-fec/src/cmaf-recovery.ts +127 -0
  415. package/node_modules/@blockcast/mmt-fec/src/cmaf-symbols.ts +68 -0
  416. package/node_modules/@blockcast/mmt-fec/src/fec-block-mapper.ts +310 -0
  417. package/node_modules/@blockcast/mmt-fec/src/index.ts +94 -0
  418. package/node_modules/@blockcast/mmt-fec/src/loc-symbols.ts +34 -0
  419. package/node_modules/@blockcast/mmt-fec/src/oti.ts +463 -0
  420. package/node_modules/@blockcast/mmt-fec/src/reorder-buffer.ts +101 -0
  421. package/node_modules/@blockcast/mmt-fec/src/repair-group.ts +195 -0
  422. package/node_modules/@blockcast/mmt-manifest-verify/dist/.build-stamp +0 -0
  423. package/node_modules/@blockcast/mmt-manifest-verify/dist/admission.d.ts +28 -0
  424. package/node_modules/@blockcast/mmt-manifest-verify/dist/admission.d.ts.map +1 -0
  425. package/node_modules/@blockcast/mmt-manifest-verify/dist/admission.js +128 -0
  426. package/node_modules/@blockcast/mmt-manifest-verify/dist/admission.js.map +1 -0
  427. package/node_modules/@blockcast/mmt-manifest-verify/dist/index.d.ts +4 -0
  428. package/node_modules/@blockcast/mmt-manifest-verify/dist/index.d.ts.map +1 -0
  429. package/node_modules/@blockcast/mmt-manifest-verify/dist/index.js +4 -0
  430. package/node_modules/@blockcast/mmt-manifest-verify/dist/index.js.map +1 -0
  431. package/node_modules/@blockcast/mmt-manifest-verify/dist/source-auth.d.ts +37 -0
  432. package/node_modules/@blockcast/mmt-manifest-verify/dist/source-auth.d.ts.map +1 -0
  433. package/node_modules/@blockcast/mmt-manifest-verify/dist/source-auth.js +179 -0
  434. package/node_modules/@blockcast/mmt-manifest-verify/dist/source-auth.js.map +1 -0
  435. package/node_modules/@blockcast/mmt-manifest-verify/dist/verifier.d.ts +99 -0
  436. package/node_modules/@blockcast/mmt-manifest-verify/dist/verifier.d.ts.map +1 -0
  437. package/node_modules/@blockcast/mmt-manifest-verify/dist/verifier.js +492 -0
  438. package/node_modules/@blockcast/mmt-manifest-verify/dist/verifier.js.map +1 -0
  439. package/node_modules/@blockcast/mmt-manifest-verify/package.json +45 -0
  440. package/node_modules/@blockcast/mmt-manifest-verify/src/admission.ts +171 -0
  441. package/node_modules/@blockcast/mmt-manifest-verify/src/index.ts +3 -0
  442. package/node_modules/@blockcast/mmt-manifest-verify/src/source-auth.ts +230 -0
  443. package/node_modules/@blockcast/mmt-manifest-verify/src/verifier.ts +669 -0
  444. package/node_modules/@noble/hashes/LICENSE +21 -0
  445. package/node_modules/@noble/hashes/README.md +521 -0
  446. package/node_modules/@noble/hashes/_assert.d.ts +17 -0
  447. package/node_modules/@noble/hashes/_assert.d.ts.map +1 -0
  448. package/node_modules/@noble/hashes/_assert.js +18 -0
  449. package/node_modules/@noble/hashes/_assert.js.map +1 -0
  450. package/node_modules/@noble/hashes/_blake.d.ts +14 -0
  451. package/node_modules/@noble/hashes/_blake.d.ts.map +1 -0
  452. package/node_modules/@noble/hashes/_blake.js +50 -0
  453. package/node_modules/@noble/hashes/_blake.js.map +1 -0
  454. package/node_modules/@noble/hashes/_md.d.ts +51 -0
  455. package/node_modules/@noble/hashes/_md.d.ts.map +1 -0
  456. package/node_modules/@noble/hashes/_md.js +162 -0
  457. package/node_modules/@noble/hashes/_md.js.map +1 -0
  458. package/node_modules/@noble/hashes/_u64.d.ts +55 -0
  459. package/node_modules/@noble/hashes/_u64.d.ts.map +1 -0
  460. package/node_modules/@noble/hashes/_u64.js +90 -0
  461. package/node_modules/@noble/hashes/_u64.js.map +1 -0
  462. package/node_modules/@noble/hashes/argon2.d.ts +32 -0
  463. package/node_modules/@noble/hashes/argon2.d.ts.map +1 -0
  464. package/node_modules/@noble/hashes/argon2.js +401 -0
  465. package/node_modules/@noble/hashes/argon2.js.map +1 -0
  466. package/node_modules/@noble/hashes/blake1.d.ts +106 -0
  467. package/node_modules/@noble/hashes/blake1.d.ts.map +1 -0
  468. package/node_modules/@noble/hashes/blake1.js +459 -0
  469. package/node_modules/@noble/hashes/blake1.js.map +1 -0
  470. package/node_modules/@noble/hashes/blake2.d.ts +116 -0
  471. package/node_modules/@noble/hashes/blake2.d.ts.map +1 -0
  472. package/node_modules/@noble/hashes/blake2.js +420 -0
  473. package/node_modules/@noble/hashes/blake2.js.map +1 -0
  474. package/node_modules/@noble/hashes/blake2b.d.ts +11 -0
  475. package/node_modules/@noble/hashes/blake2b.d.ts.map +1 -0
  476. package/node_modules/@noble/hashes/blake2b.js +14 -0
  477. package/node_modules/@noble/hashes/blake2b.js.map +1 -0
  478. package/node_modules/@noble/hashes/blake2s.d.ts +20 -0
  479. package/node_modules/@noble/hashes/blake2s.d.ts.map +1 -0
  480. package/node_modules/@noble/hashes/blake2s.js +24 -0
  481. package/node_modules/@noble/hashes/blake2s.js.map +1 -0
  482. package/node_modules/@noble/hashes/blake3.d.ts +54 -0
  483. package/node_modules/@noble/hashes/blake3.d.ts.map +1 -0
  484. package/node_modules/@noble/hashes/blake3.js +255 -0
  485. package/node_modules/@noble/hashes/blake3.js.map +1 -0
  486. package/node_modules/@noble/hashes/crypto.d.ts +2 -0
  487. package/node_modules/@noble/hashes/crypto.d.ts.map +1 -0
  488. package/node_modules/@noble/hashes/crypto.js +5 -0
  489. package/node_modules/@noble/hashes/crypto.js.map +1 -0
  490. package/node_modules/@noble/hashes/cryptoNode.d.ts +2 -0
  491. package/node_modules/@noble/hashes/cryptoNode.d.ts.map +1 -0
  492. package/node_modules/@noble/hashes/cryptoNode.js +18 -0
  493. package/node_modules/@noble/hashes/cryptoNode.js.map +1 -0
  494. package/node_modules/@noble/hashes/eskdf.d.ts +47 -0
  495. package/node_modules/@noble/hashes/eskdf.d.ts.map +1 -0
  496. package/node_modules/@noble/hashes/eskdf.js +166 -0
  497. package/node_modules/@noble/hashes/eskdf.js.map +1 -0
  498. package/node_modules/@noble/hashes/esm/_assert.d.ts +17 -0
  499. package/node_modules/@noble/hashes/esm/_assert.d.ts.map +1 -0
  500. package/node_modules/@noble/hashes/esm/_assert.js +15 -0
  501. package/node_modules/@noble/hashes/esm/_assert.js.map +1 -0
  502. package/node_modules/@noble/hashes/esm/_blake.d.ts +14 -0
  503. package/node_modules/@noble/hashes/esm/_blake.d.ts.map +1 -0
  504. package/node_modules/@noble/hashes/esm/_blake.js +45 -0
  505. package/node_modules/@noble/hashes/esm/_blake.js.map +1 -0
  506. package/node_modules/@noble/hashes/esm/_md.d.ts +51 -0
  507. package/node_modules/@noble/hashes/esm/_md.d.ts.map +1 -0
  508. package/node_modules/@noble/hashes/esm/_md.js +155 -0
  509. package/node_modules/@noble/hashes/esm/_md.js.map +1 -0
  510. package/node_modules/@noble/hashes/esm/_u64.d.ts +55 -0
  511. package/node_modules/@noble/hashes/esm/_u64.d.ts.map +1 -0
  512. package/node_modules/@noble/hashes/esm/_u64.js +67 -0
  513. package/node_modules/@noble/hashes/esm/_u64.js.map +1 -0
  514. package/node_modules/@noble/hashes/esm/argon2.d.ts +32 -0
  515. package/node_modules/@noble/hashes/esm/argon2.d.ts.map +1 -0
  516. package/node_modules/@noble/hashes/esm/argon2.js +392 -0
  517. package/node_modules/@noble/hashes/esm/argon2.js.map +1 -0
  518. package/node_modules/@noble/hashes/esm/blake1.d.ts +106 -0
  519. package/node_modules/@noble/hashes/esm/blake1.d.ts.map +1 -0
  520. package/node_modules/@noble/hashes/esm/blake1.js +452 -0
  521. package/node_modules/@noble/hashes/esm/blake1.js.map +1 -0
  522. package/node_modules/@noble/hashes/esm/blake2.d.ts +116 -0
  523. package/node_modules/@noble/hashes/esm/blake2.d.ts.map +1 -0
  524. package/node_modules/@noble/hashes/esm/blake2.js +413 -0
  525. package/node_modules/@noble/hashes/esm/blake2.js.map +1 -0
  526. package/node_modules/@noble/hashes/esm/blake2b.d.ts +11 -0
  527. package/node_modules/@noble/hashes/esm/blake2b.d.ts.map +1 -0
  528. package/node_modules/@noble/hashes/esm/blake2b.js +11 -0
  529. package/node_modules/@noble/hashes/esm/blake2b.js.map +1 -0
  530. package/node_modules/@noble/hashes/esm/blake2s.d.ts +20 -0
  531. package/node_modules/@noble/hashes/esm/blake2s.d.ts.map +1 -0
  532. package/node_modules/@noble/hashes/esm/blake2s.js +21 -0
  533. package/node_modules/@noble/hashes/esm/blake2s.js.map +1 -0
  534. package/node_modules/@noble/hashes/esm/blake3.d.ts +54 -0
  535. package/node_modules/@noble/hashes/esm/blake3.d.ts.map +1 -0
  536. package/node_modules/@noble/hashes/esm/blake3.js +251 -0
  537. package/node_modules/@noble/hashes/esm/blake3.js.map +1 -0
  538. package/node_modules/@noble/hashes/esm/crypto.d.ts +2 -0
  539. package/node_modules/@noble/hashes/esm/crypto.d.ts.map +1 -0
  540. package/node_modules/@noble/hashes/esm/crypto.js +2 -0
  541. package/node_modules/@noble/hashes/esm/crypto.js.map +1 -0
  542. package/node_modules/@noble/hashes/esm/cryptoNode.d.ts +2 -0
  543. package/node_modules/@noble/hashes/esm/cryptoNode.d.ts.map +1 -0
  544. package/node_modules/@noble/hashes/esm/cryptoNode.js +15 -0
  545. package/node_modules/@noble/hashes/esm/cryptoNode.js.map +1 -0
  546. package/node_modules/@noble/hashes/esm/eskdf.d.ts +47 -0
  547. package/node_modules/@noble/hashes/esm/eskdf.d.ts.map +1 -0
  548. package/node_modules/@noble/hashes/esm/eskdf.js +160 -0
  549. package/node_modules/@noble/hashes/esm/eskdf.js.map +1 -0
  550. package/node_modules/@noble/hashes/esm/hkdf.d.ts +36 -0
  551. package/node_modules/@noble/hashes/esm/hkdf.d.ts.map +1 -0
  552. package/node_modules/@noble/hashes/esm/hkdf.js +82 -0
  553. package/node_modules/@noble/hashes/esm/hkdf.js.map +1 -0
  554. package/node_modules/@noble/hashes/esm/hmac.d.ts +35 -0
  555. package/node_modules/@noble/hashes/esm/hmac.d.ts.map +1 -0
  556. package/node_modules/@noble/hashes/esm/hmac.js +86 -0
  557. package/node_modules/@noble/hashes/esm/hmac.js.map +1 -0
  558. package/node_modules/@noble/hashes/esm/index.d.ts +2 -0
  559. package/node_modules/@noble/hashes/esm/index.d.ts.map +1 -0
  560. package/node_modules/@noble/hashes/esm/index.js +33 -0
  561. package/node_modules/@noble/hashes/esm/index.js.map +1 -0
  562. package/node_modules/@noble/hashes/esm/legacy.d.ts +71 -0
  563. package/node_modules/@noble/hashes/esm/legacy.d.ts.map +1 -0
  564. package/node_modules/@noble/hashes/esm/legacy.js +281 -0
  565. package/node_modules/@noble/hashes/esm/legacy.js.map +1 -0
  566. package/node_modules/@noble/hashes/esm/package.json +10 -0
  567. package/node_modules/@noble/hashes/esm/pbkdf2.d.ts +23 -0
  568. package/node_modules/@noble/hashes/esm/pbkdf2.d.ts.map +1 -0
  569. package/node_modules/@noble/hashes/esm/pbkdf2.js +97 -0
  570. package/node_modules/@noble/hashes/esm/pbkdf2.js.map +1 -0
  571. package/node_modules/@noble/hashes/esm/ripemd160.d.ts +13 -0
  572. package/node_modules/@noble/hashes/esm/ripemd160.d.ts.map +1 -0
  573. package/node_modules/@noble/hashes/esm/ripemd160.js +13 -0
  574. package/node_modules/@noble/hashes/esm/ripemd160.js.map +1 -0
  575. package/node_modules/@noble/hashes/esm/scrypt.d.ts +34 -0
  576. package/node_modules/@noble/hashes/esm/scrypt.d.ts.map +1 -0
  577. package/node_modules/@noble/hashes/esm/scrypt.js +228 -0
  578. package/node_modules/@noble/hashes/esm/scrypt.js.map +1 -0
  579. package/node_modules/@noble/hashes/esm/sha1.d.ts +11 -0
  580. package/node_modules/@noble/hashes/esm/sha1.d.ts.map +1 -0
  581. package/node_modules/@noble/hashes/esm/sha1.js +11 -0
  582. package/node_modules/@noble/hashes/esm/sha1.js.map +1 -0
  583. package/node_modules/@noble/hashes/esm/sha2.d.ts +159 -0
  584. package/node_modules/@noble/hashes/esm/sha2.d.ts.map +1 -0
  585. package/node_modules/@noble/hashes/esm/sha2.js +375 -0
  586. package/node_modules/@noble/hashes/esm/sha2.js.map +1 -0
  587. package/node_modules/@noble/hashes/esm/sha256.d.ts +20 -0
  588. package/node_modules/@noble/hashes/esm/sha256.d.ts.map +1 -0
  589. package/node_modules/@noble/hashes/esm/sha256.js +20 -0
  590. package/node_modules/@noble/hashes/esm/sha256.js.map +1 -0
  591. package/node_modules/@noble/hashes/esm/sha3-addons.d.ts +142 -0
  592. package/node_modules/@noble/hashes/esm/sha3-addons.d.ts.map +1 -0
  593. package/node_modules/@noble/hashes/esm/sha3-addons.js +393 -0
  594. package/node_modules/@noble/hashes/esm/sha3-addons.js.map +1 -0
  595. package/node_modules/@noble/hashes/esm/sha3.d.ts +53 -0
  596. package/node_modules/@noble/hashes/esm/sha3.d.ts.map +1 -0
  597. package/node_modules/@noble/hashes/esm/sha3.js +234 -0
  598. package/node_modules/@noble/hashes/esm/sha3.js.map +1 -0
  599. package/node_modules/@noble/hashes/esm/sha512.d.ts +26 -0
  600. package/node_modules/@noble/hashes/esm/sha512.d.ts.map +1 -0
  601. package/node_modules/@noble/hashes/esm/sha512.js +26 -0
  602. package/node_modules/@noble/hashes/esm/sha512.js.map +1 -0
  603. package/node_modules/@noble/hashes/esm/utils.d.ts +161 -0
  604. package/node_modules/@noble/hashes/esm/utils.d.ts.map +1 -0
  605. package/node_modules/@noble/hashes/esm/utils.js +281 -0
  606. package/node_modules/@noble/hashes/esm/utils.js.map +1 -0
  607. package/node_modules/@noble/hashes/hkdf.d.ts +36 -0
  608. package/node_modules/@noble/hashes/hkdf.d.ts.map +1 -0
  609. package/node_modules/@noble/hashes/hkdf.js +88 -0
  610. package/node_modules/@noble/hashes/hkdf.js.map +1 -0
  611. package/node_modules/@noble/hashes/hmac.d.ts +35 -0
  612. package/node_modules/@noble/hashes/hmac.d.ts.map +1 -0
  613. package/node_modules/@noble/hashes/hmac.js +91 -0
  614. package/node_modules/@noble/hashes/hmac.js.map +1 -0
  615. package/node_modules/@noble/hashes/index.d.ts +1 -0
  616. package/node_modules/@noble/hashes/index.d.ts.map +1 -0
  617. package/node_modules/@noble/hashes/index.js +33 -0
  618. package/node_modules/@noble/hashes/index.js.map +1 -0
  619. package/node_modules/@noble/hashes/legacy.d.ts +71 -0
  620. package/node_modules/@noble/hashes/legacy.d.ts.map +1 -0
  621. package/node_modules/@noble/hashes/legacy.js +287 -0
  622. package/node_modules/@noble/hashes/legacy.js.map +1 -0
  623. package/node_modules/@noble/hashes/package.json +266 -0
  624. package/node_modules/@noble/hashes/pbkdf2.d.ts +23 -0
  625. package/node_modules/@noble/hashes/pbkdf2.d.ts.map +1 -0
  626. package/node_modules/@noble/hashes/pbkdf2.js +101 -0
  627. package/node_modules/@noble/hashes/pbkdf2.js.map +1 -0
  628. package/node_modules/@noble/hashes/ripemd160.d.ts +13 -0
  629. package/node_modules/@noble/hashes/ripemd160.d.ts.map +1 -0
  630. package/node_modules/@noble/hashes/ripemd160.js +16 -0
  631. package/node_modules/@noble/hashes/ripemd160.js.map +1 -0
  632. package/node_modules/@noble/hashes/scrypt.d.ts +34 -0
  633. package/node_modules/@noble/hashes/scrypt.d.ts.map +1 -0
  634. package/node_modules/@noble/hashes/scrypt.js +232 -0
  635. package/node_modules/@noble/hashes/scrypt.js.map +1 -0
  636. package/node_modules/@noble/hashes/sha1.d.ts +11 -0
  637. package/node_modules/@noble/hashes/sha1.d.ts.map +1 -0
  638. package/node_modules/@noble/hashes/sha1.js +14 -0
  639. package/node_modules/@noble/hashes/sha1.js.map +1 -0
  640. package/node_modules/@noble/hashes/sha2.d.ts +159 -0
  641. package/node_modules/@noble/hashes/sha2.d.ts.map +1 -0
  642. package/node_modules/@noble/hashes/sha2.js +384 -0
  643. package/node_modules/@noble/hashes/sha2.js.map +1 -0
  644. package/node_modules/@noble/hashes/sha256.d.ts +20 -0
  645. package/node_modules/@noble/hashes/sha256.d.ts.map +1 -0
  646. package/node_modules/@noble/hashes/sha256.js +23 -0
  647. package/node_modules/@noble/hashes/sha256.js.map +1 -0
  648. package/node_modules/@noble/hashes/sha3-addons.d.ts +142 -0
  649. package/node_modules/@noble/hashes/sha3-addons.d.ts.map +1 -0
  650. package/node_modules/@noble/hashes/sha3-addons.js +402 -0
  651. package/node_modules/@noble/hashes/sha3-addons.js.map +1 -0
  652. package/node_modules/@noble/hashes/sha3.d.ts +53 -0
  653. package/node_modules/@noble/hashes/sha3.d.ts.map +1 -0
  654. package/node_modules/@noble/hashes/sha3.js +239 -0
  655. package/node_modules/@noble/hashes/sha3.js.map +1 -0
  656. package/node_modules/@noble/hashes/sha512.d.ts +26 -0
  657. package/node_modules/@noble/hashes/sha512.d.ts.map +1 -0
  658. package/node_modules/@noble/hashes/sha512.js +29 -0
  659. package/node_modules/@noble/hashes/sha512.js.map +1 -0
  660. package/node_modules/@noble/hashes/src/_assert.ts +22 -0
  661. package/node_modules/@noble/hashes/src/_blake.ts +50 -0
  662. package/node_modules/@noble/hashes/src/_md.ts +176 -0
  663. package/node_modules/@noble/hashes/src/_u64.ts +91 -0
  664. package/node_modules/@noble/hashes/src/argon2.ts +497 -0
  665. package/node_modules/@noble/hashes/src/blake1.ts +534 -0
  666. package/node_modules/@noble/hashes/src/blake2.ts +486 -0
  667. package/node_modules/@noble/hashes/src/blake2b.ts +10 -0
  668. package/node_modules/@noble/hashes/src/blake2s.ts +20 -0
  669. package/node_modules/@noble/hashes/src/blake3.ts +272 -0
  670. package/node_modules/@noble/hashes/src/crypto.ts +9 -0
  671. package/node_modules/@noble/hashes/src/cryptoNode.ts +15 -0
  672. package/node_modules/@noble/hashes/src/eskdf.ts +187 -0
  673. package/node_modules/@noble/hashes/src/hkdf.ts +88 -0
  674. package/node_modules/@noble/hashes/src/hmac.ts +94 -0
  675. package/node_modules/@noble/hashes/src/index.ts +31 -0
  676. package/node_modules/@noble/hashes/src/legacy.ts +293 -0
  677. package/node_modules/@noble/hashes/src/pbkdf2.ts +122 -0
  678. package/node_modules/@noble/hashes/src/ripemd160.ts +12 -0
  679. package/node_modules/@noble/hashes/src/scrypt.ts +257 -0
  680. package/node_modules/@noble/hashes/src/sha1.ts +10 -0
  681. package/node_modules/@noble/hashes/src/sha2.ts +402 -0
  682. package/node_modules/@noble/hashes/src/sha256.ts +24 -0
  683. package/node_modules/@noble/hashes/src/sha3-addons.ts +499 -0
  684. package/node_modules/@noble/hashes/src/sha3.ts +258 -0
  685. package/node_modules/@noble/hashes/src/sha512.ts +34 -0
  686. package/node_modules/@noble/hashes/src/utils.ts +395 -0
  687. package/node_modules/@noble/hashes/utils.d.ts +161 -0
  688. package/node_modules/@noble/hashes/utils.d.ts.map +1 -0
  689. package/node_modules/@noble/hashes/utils.js +313 -0
  690. package/node_modules/@noble/hashes/utils.js.map +1 -0
  691. package/package.json +179 -8
  692. package/schemas/catalog.schema.json +357 -0
  693. package/src/align.ts +239 -0
  694. package/src/audio-init-parser.ts +189 -0
  695. package/src/audio-ring-buffer.ts +120 -0
  696. package/src/authored-timing.ts +63 -0
  697. package/src/block-policy.ts +903 -0
  698. package/src/carp-catalog.ts +1065 -0
  699. package/src/catalog-fec-params.ts +729 -0
  700. package/src/catalog-network-source.ts +74 -0
  701. package/src/catalog-validator.ts +34 -0
  702. package/src/cmaf-block-assembler.ts +1255 -0
  703. package/src/cmaf-group-batcher.ts +104 -0
  704. package/src/cmcd-producer.ts +329 -0
  705. package/src/codec-utils.ts +1984 -0
  706. package/src/conceal-timeline.ts +293 -0
  707. package/src/delivery-gap.ts +147 -0
  708. package/src/duplicate-ledger.ts +372 -0
  709. package/src/fec-codec.ts +61 -0
  710. package/src/fec-manager.ts +4298 -0
  711. package/src/fec-starvation-detector.ts +128 -0
  712. package/src/fec-state.ts +219 -0
  713. package/src/fec-worker-setup.ts +283 -0
  714. package/src/fmp4-completion.ts +47 -0
  715. package/src/fmp4-reader.ts +112 -0
  716. package/src/index.ts +590 -21
  717. package/src/iso-block-locator.ts +329 -0
  718. package/src/iso-bmff-timing.ts +813 -0
  719. package/src/loc.ts +128 -154
  720. package/src/manifest-auth-plan.ts +107 -0
  721. package/src/media-router.ts +841 -0
  722. package/src/mfu-reassembler.ts +3480 -155
  723. package/src/mfu-reassembly-policy.ts +134 -0
  724. package/src/mmtp-container-parser.ts +2380 -0
  725. package/src/mmtp-decode-pipeline.ts +406 -0
  726. package/src/mmtp-router.ts +4860 -0
  727. package/src/mmtp-to-cmaf.ts +977 -0
  728. package/src/multicast-auth-catalog.ts +340 -0
  729. package/src/multicast-input.ts +793 -0
  730. package/src/multicast-manager.ts +2783 -0
  731. package/src/multicast-transport.ts +1058 -0
  732. package/src/multicast-unwrap.ts +132 -0
  733. package/src/no-modules.ts +21 -0
  734. package/src/phase14-catalog-override.ts +336 -0
  735. package/src/repair-fetch.ts +772 -0
  736. package/src/repair-layers.ts +533 -0
  737. package/src/sample-combiner.ts +938 -0
  738. package/src/signaling.ts +78 -0
  739. package/src/source-auth.ts +1 -0
  740. package/src/switching-set-alignment.ts +109 -0
  741. package/src/timestamp-codec.ts +265 -0
  742. package/src/track-pipeline-setup.ts +205 -0
  743. package/src/track-pipeline.ts +332 -0
  744. package/src/track-session.ts +1010 -0
  745. package/src/transport-pool.ts +128 -0
  746. package/src/wasm-mfu-reassembler.ts +405 -0
  747. package/src/wasm-mmtp-container-parser.ts +23 -0
  748. package/test-vectors/align.json +199 -0
  749. package/test-vectors/catalog/README.md +106 -0
  750. package/test-vectors/catalog/negative/archive-origin-with-path.json +33 -0
  751. package/test-vectors/catalog/negative/archive-unknown-field.json +34 -0
  752. package/test-vectors/catalog/negative/fec-block-profile-missing.json +51 -0
  753. package/test-vectors/catalog/negative/fec-block-profile-unknown.json +52 -0
  754. package/test-vectors/catalog/negative/fec-enhancement-repair-removed.json +28 -0
  755. package/test-vectors/catalog/negative/fec-repair-layer-geometry.json +33 -0
  756. package/test-vectors/catalog/negative/fec-repair-layer-missing-symbols.json +31 -0
  757. package/test-vectors/catalog/negative/fec-repair-multiple-depends.json +32 -0
  758. package/test-vectors/catalog/negative/fec-repair-overlay-with-layer.json +34 -0
  759. package/test-vectors/catalog/negative/fec-repair-priority-out-of-band.json +24 -0
  760. package/test-vectors/catalog/negative/fec-repair-source-symbols-without-scope.json +33 -0
  761. package/test-vectors/catalog/negative/legacy-selection-params.json +15 -0
  762. package/test-vectors/catalog/negative/media-track-repair-layer.json +16 -0
  763. package/test-vectors/catalog/negative/missing-mmtp-fields.json +12 -0
  764. package/test-vectors/catalog/negative/multicast-auth-padded-key.json +37 -0
  765. package/test-vectors/catalog/negative/multicast-auth-reused-track-format.json +53 -0
  766. package/test-vectors/catalog/negative/multicast-auth-trailing-pad-bits.json +37 -0
  767. package/test-vectors/catalog/negative/multicast-endpoint-missing-protocol-and-source.json +24 -0
  768. package/test-vectors/catalog/negative/multicast-network-source-empty.json +44 -0
  769. package/test-vectors/catalog/negative/multicast-packet-id-signaling.json +26 -0
  770. package/test-vectors/catalog/negative/network-source-object.json +25 -0
  771. package/test-vectors/catalog/negative/raptorq-source-symbols-over-max.json +24 -0
  772. package/test-vectors/catalog/negative/raptorq-unaligned-symbol.json +27 -0
  773. package/test-vectors/catalog/negative/repair-track-legacy-shape.json +19 -0
  774. package/test-vectors/catalog/negative/track-role-empty.json +16 -0
  775. package/test-vectors/catalog/negative/track-role-non-string.json +16 -0
  776. package/test-vectors/catalog/positive/archive-origin.json +34 -0
  777. package/test-vectors/catalog/positive/fec-iso-ssbg1.json +52 -0
  778. package/test-vectors/catalog/positive/fec-layered-repair.json +106 -0
  779. package/test-vectors/catalog/positive/fec-multicast.json +52 -0
  780. package/test-vectors/catalog/positive/flat-av.json +30 -0
  781. package/test-vectors/catalog/positive/multicast-auth-rotation.json +55 -0
  782. package/test-vectors/catalog/positive/multicast-native-ssm-no-network-source.json +43 -0
  783. package/test-vectors/catalog/positive/ticks-and-mixed-timescales.json +39 -0
  784. package/test-vectors/catalog/positive/track-role.json +49 -0
  785. package/test-vectors/catalog/releases/moqcast-native-v16-v1/catalog.json +65 -0
  786. package/test-vectors/catalog/releases/moqcast-native-v16-v1.archive.sha256 +1 -0
  787. package/test-vectors/catalog-multicast-auth-v1.json +26 -0
  788. package/test-vectors/catalog-validation.json +732 -0
  789. package/test-vectors/catalog-validator-public-api.mjs +25 -0
  790. package/test-vectors/generate-source-auth-fixture.mjs +114 -0
  791. package/test-vectors/hevc-x265-repeat-headers-v1.json +40 -0
  792. package/test-vectors/source-auth-v1.json +61 -0
  793. package/test-vectors/source-contiguity-v1.json +131 -0
  794. package/test-vectors/subsample-placement-v1.json +230 -0
  795. package/dist/mmtp.d.ts +0 -73
  796. package/dist/mmtp.d.ts.map +0 -1
  797. package/dist/mmtp.js +0 -210
  798. package/dist/mmtp.js.map +0 -1
  799. package/src/mmtp.ts +0 -277
@@ -0,0 +1,3899 @@
1
+ // src/track-session.ts
2
+ import { fecBlockBudgetMs } from "@blockcast/mmt-fec";
3
+ function computeFecDeliveryWindowMs(config) {
4
+ const fecInterleaveMs = config.interleaveDepthMs;
5
+ const fecK = config.sourceSymbols;
6
+ if (!Number.isFinite(fecInterleaveMs) || fecInterleaveMs <= 0) {
7
+ throw new Error(
8
+ `[TrackSession] fecInterleaveMs=${fecInterleaveMs}. Derive a positive FEC interleave window from the stream catalog.`
9
+ );
10
+ }
11
+ if (!Number.isFinite(fecK) || fecK <= 0) {
12
+ throw new Error(
13
+ `[TrackSession] fecK=${fecK}. Derive a positive FEC source-symbol count from the stream catalog.`
14
+ );
15
+ }
16
+ if (config.jitterBufferMs === void 0) return Math.ceil(fecInterleaveMs);
17
+ return fecBlockBudgetMs(fecInterleaveMs, config.jitterBufferMs);
18
+ }
19
+
20
+ // src/delivery-gap.ts
21
+ var DeliveryGapEstimator = class {
22
+ /** Most recent arrival, or undefined before the first one since reset. */
23
+ #lastArrivalMs;
24
+ /** Last MMTP packet_sequence_number seen per packet_id. */
25
+ #lastSequence = /* @__PURE__ */ new Map();
26
+ /**
27
+ * Monotonic deque of (arrivalMs, gapMs): gaps strictly decrease from head to
28
+ * tail, so the head is the window maximum. Amortized O(1) per arrival.
29
+ */
30
+ #times = [];
31
+ #gaps = [];
32
+ #head = 0;
33
+ #windowMs;
34
+ /**
35
+ * @param windowMs trailing window in ms, which also caps the reported gap,
36
+ * or undefined when the catalog gives none (no gap is reported).
37
+ */
38
+ setWindowMs(windowMs) {
39
+ if (windowMs !== void 0 && !(Number.isFinite(windowMs) && windowMs > 0)) {
40
+ throw new Error(
41
+ `[DeliveryGap] window must be a finite positive ms value or undefined (got ${windowMs})`
42
+ );
43
+ }
44
+ this.#windowMs = windowMs;
45
+ }
46
+ get windowMs() {
47
+ return this.#windowMs;
48
+ }
49
+ /**
50
+ * Record one multicast datagram arrival at receiver time `nowMs`, carrying
51
+ * MMTP `packetSequenceNumber` (32-bit, per packet_id) on `packetId`.
52
+ */
53
+ noteArrival(nowMs, packetId, packetSequenceNumber) {
54
+ const last = this.#lastArrivalMs;
55
+ const previousSequence = this.#lastSequence.get(packetId);
56
+ this.#lastArrivalMs = nowMs;
57
+ this.#lastSequence.set(packetId, packetSequenceNumber);
58
+ if (last === void 0 || previousSequence === void 0) return;
59
+ if (this.#windowMs === void 0) return;
60
+ if (previousSequence + 1 >>> 0 !== packetSequenceNumber >>> 0) return;
61
+ const gap = nowMs - last;
62
+ this.#evict(nowMs);
63
+ while (this.#gaps.length > this.#head && this.#gaps[this.#gaps.length - 1] <= gap) {
64
+ this.#gaps.pop();
65
+ this.#times.pop();
66
+ }
67
+ this.#gaps.push(gap);
68
+ this.#times.push(nowMs);
69
+ this.#compact();
70
+ }
71
+ /**
72
+ * Largest gap observed within the window ending at `nowMs`, at most the
73
+ * window itself; 0 if none or when no window is set.
74
+ */
75
+ peakGapMs(nowMs) {
76
+ const windowMs = this.#windowMs;
77
+ if (windowMs === void 0) return 0;
78
+ this.#evict(nowMs);
79
+ return this.#gaps.length > this.#head ? Math.min(this.#gaps[this.#head], windowMs) : 0;
80
+ }
81
+ /** Forget everything: a new delivery path starts with no measurement. */
82
+ reset() {
83
+ this.#lastArrivalMs = void 0;
84
+ this.#lastSequence.clear();
85
+ this.#times = [];
86
+ this.#gaps = [];
87
+ this.#head = 0;
88
+ }
89
+ #evict(nowMs) {
90
+ const windowMs = this.#windowMs;
91
+ if (windowMs === void 0) return;
92
+ while (this.#times.length > this.#head && this.#times[this.#head] < nowMs - windowMs) {
93
+ this.#head++;
94
+ }
95
+ }
96
+ #compact() {
97
+ if (this.#head > 0 && this.#head * 2 >= this.#times.length) {
98
+ this.#times = this.#times.slice(this.#head);
99
+ this.#gaps = this.#gaps.slice(this.#head);
100
+ this.#head = 0;
101
+ }
102
+ }
103
+ };
104
+
105
+ // src/fec-manager.ts
106
+ import {
107
+ deriveFecInterleaveDepth as deriveFecInterleaveDepth2,
108
+ deriveFecRetentionBlocks,
109
+ tryDeriveFecRetentionBlocks
110
+ } from "@blockcast/mmt-fec";
111
+
112
+ // ../mmt-alta-parse/dist/index.js
113
+ var MPU_HEADER_SIZE = 8;
114
+ var MPU_LENGTH_FIELD_SIZE = 2;
115
+ var MPU_HEADER_REMAINDER_SIZE = MPU_HEADER_SIZE - MPU_LENGTH_FIELD_SIZE;
116
+
117
+ // ../mmt-base/dist/mmtp.js
118
+ var MPU_HEADER_SIZE2 = 8;
119
+ var MPU_LENGTH_FIELD_SIZE2 = 2;
120
+ var MPU_HEADER_REMAINDER_SIZE2 = MPU_HEADER_SIZE2 - MPU_LENGTH_FIELD_SIZE2;
121
+
122
+ // ../mmt-base/dist/ntp-short.js
123
+ var NTP_SHORT_BITS = 32;
124
+ var NTP_SHORT_MODULUS = 2 ** NTP_SHORT_BITS;
125
+ var NTP_SHORT_HALF_RANGE = 2 ** (NTP_SHORT_BITS - 1);
126
+
127
+ // ../mmt-base/dist/serial-number.js
128
+ var SERIAL32_SPAN = 4294967296;
129
+ var SERIAL32_HALF = 2147483648;
130
+ function assertUint32(value, name) {
131
+ if (!Number.isInteger(value) || value < 0 || value >= SERIAL32_SPAN) {
132
+ throw new RangeError(`${name} must be a uint32 (got ${value})`);
133
+ }
134
+ }
135
+ function serial32Add(s, n) {
136
+ assertUint32(s, "s");
137
+ if (!Number.isInteger(n) || n <= -SERIAL32_HALF || n >= SERIAL32_HALF) {
138
+ throw new RangeError(`serial addend must lie in (-2^31, 2^31) (got ${n})`);
139
+ }
140
+ return (s + n + SERIAL32_SPAN) % SERIAL32_SPAN;
141
+ }
142
+ function serial32Distance(from, to) {
143
+ return to - from >>> 0;
144
+ }
145
+ function serial32Precedes(a, b) {
146
+ const distance = serial32Distance(a, b);
147
+ return distance !== 0 && distance < SERIAL32_HALF;
148
+ }
149
+ function kStrideBlockStart(sbn, k) {
150
+ assertUint32(sbn, "sbn");
151
+ if (!Number.isInteger(k) || k < 1 || k >= SERIAL32_HALF) {
152
+ throw new RangeError(`k must be a positive integer below 2^31 (got ${k})`);
153
+ }
154
+ const high = Math.floor(sbn / 65536);
155
+ const low = sbn % 65536;
156
+ return (high * k % SERIAL32_SPAN * 65536 + low * k) % SERIAL32_SPAN;
157
+ }
158
+ function kStrideBlockPrecedes(a, b, k) {
159
+ return serial32Precedes(kStrideBlockStart(a, k), kStrideBlockStart(b, k));
160
+ }
161
+ function kStrideBlocksBehind(a, b, k) {
162
+ const from = kStrideBlockStart(a, k);
163
+ const to = kStrideBlockStart(b, k);
164
+ if (!serial32Precedes(from, to))
165
+ return 0;
166
+ return Math.ceil(serial32Distance(from, to) / k);
167
+ }
168
+ function isKStrideBlock(sbn, k) {
169
+ return Number.isInteger(sbn) && sbn >= 0 && sbn < SERIAL32_SPAN && Number.isInteger(k) && k >= 1 && k < SERIAL32_HALF;
170
+ }
171
+ function kStrideNextBlock(sbn, k) {
172
+ assertUint32(sbn, "sbn");
173
+ kStrideBlockStart(sbn, k);
174
+ return (sbn + 1) * k >= SERIAL32_SPAN ? 0 : sbn + 1;
175
+ }
176
+
177
+ // ../mmt-base/dist/fec-block-profile.js
178
+ var FEC_BLOCK_PROFILES = Object.freeze([
179
+ "k-padded-v1",
180
+ "iso-ssbg1-v1"
181
+ ]);
182
+ function isFecBlockProfile(value) {
183
+ return value === "k-padded-v1" || value === "iso-ssbg1-v1";
184
+ }
185
+ function parseFecBlockProfile(value, where) {
186
+ if (isFecBlockProfile(value))
187
+ return value;
188
+ if (value === void 0) {
189
+ throw new Error(`${where} is required: name the FEC block profile ("k-padded-v1" or "iso-ssbg1-v1")`);
190
+ }
191
+ throw new Error(`${where} must be "k-padded-v1" or "iso-ssbg1-v1" (got ${JSON.stringify(value)})`);
192
+ }
193
+ var SSBG1_LENGTH_FIELD_BYTES = 2;
194
+ function ssbg1MaxPacketLength(symbolSize) {
195
+ return symbolSize - SSBG1_LENGTH_FIELD_BYTES;
196
+ }
197
+ function ssbg1FitsSymbol(packetLength, symbolSize) {
198
+ return Number.isSafeInteger(packetLength) && packetLength >= 0 && packetLength <= ssbg1MaxPacketLength(symbolSize) && packetLength <= 65535;
199
+ }
200
+ function isoBlockEnd(ssStart, ssbLength) {
201
+ return serial32Add(ssStart, ssbLength);
202
+ }
203
+ function isoBlockContains(ssStart, ssbLength, ssId) {
204
+ return serial32Distance(ssStart, ssId) < ssbLength;
205
+ }
206
+ function isoBlocksBehindLowerBound(a, b, kMax) {
207
+ if (!Number.isSafeInteger(kMax) || kMax <= 0) {
208
+ throw new Error(`iso-ssbg1-v1 block horizon needs a positive maximum K (got ${kMax})`);
209
+ }
210
+ return serial32Precedes(a, b) ? Math.floor(serial32Distance(a, b) / kMax) : 0;
211
+ }
212
+
213
+ // src/iso-block-locator.ts
214
+ var IsoBlockLocator = class _IsoBlockLocator {
215
+ /** Known blocks and spans, in serial order of their start. */
216
+ #blocks = [];
217
+ /** Held first-arrival sources whose block no repair has stated yet. */
218
+ #held = /* @__PURE__ */ new Map();
219
+ /** End of contiguous coverage: the SS_ID the next unknown block starts at. */
220
+ #coverageEnd;
221
+ #kMax;
222
+ #retentionBlocks;
223
+ #heldEvicted = 0;
224
+ #newest;
225
+ /**
226
+ * @param kMax the catalog maximum K (`sourceSymbols`), the bound on SSB_length.
227
+ * @param retentionBlocks how many blocks behind the newest position the
228
+ * locator keeps, derived by its owner from catalog timing; required, so
229
+ * no locator exists without retention.
230
+ */
231
+ constructor(kMax, retentionBlocks) {
232
+ if (!Number.isSafeInteger(kMax) || kMax <= 0) {
233
+ throw new Error(`[IsoBlockLocator] maximum K must be a positive integer (got ${kMax})`);
234
+ }
235
+ this.#kMax = kMax;
236
+ this.#retentionBlocks = _IsoBlockLocator.#checkedRetention(retentionBlocks);
237
+ }
238
+ static #checkedRetention(retentionBlocks) {
239
+ if (!Number.isSafeInteger(retentionBlocks) || retentionBlocks <= 0) {
240
+ throw new Error(
241
+ `[IsoBlockLocator] retention must be a positive integer number of blocks (got ${retentionBlocks})`
242
+ );
243
+ }
244
+ return retentionBlocks;
245
+ }
246
+ get kMax() {
247
+ return this.#kMax;
248
+ }
249
+ /** Blocks behind the newest position the locator keeps. */
250
+ get retentionBlocks() {
251
+ return this.#retentionBlocks;
252
+ }
253
+ /** Update the retention when the catalog timing it derives from changes. */
254
+ set retentionBlocks(retentionBlocks) {
255
+ this.#retentionBlocks = _IsoBlockLocator.#checkedRetention(retentionBlocks);
256
+ }
257
+ /** The most sources held at once: the SS_IDs the retention horizon spans. */
258
+ get heldCapacity() {
259
+ return (this.#retentionBlocks + 1) * this.#kMax;
260
+ }
261
+ /**
262
+ * The newest position the stream has reached (RFC 1982 order): the newest
263
+ * SS_ID of a source held, or fed once located (`observe`), or SS_Start of a
264
+ * placed repair, so no source the stream has fed follows it. Undefined
265
+ * before the first one.
266
+ */
267
+ get newest() {
268
+ return this.#newest;
269
+ }
270
+ /** Advance `newest` to `position` if it is newer; retires nothing. */
271
+ observe(position) {
272
+ if (this.#newest === void 0 || serial32Precedes(this.#newest, position)) this.#newest = position;
273
+ }
274
+ /** Held sources evicted by `heldCapacity` (never by the horizon itself). */
275
+ get heldEvicted() {
276
+ return this.#heldEvicted;
277
+ }
278
+ /** Number of known blocks and spans. */
279
+ get blockCount() {
280
+ return this.#blocks.length;
281
+ }
282
+ /** End of contiguous coverage, or undefined before the first repair. */
283
+ get coverageEnd() {
284
+ return this.#coverageEnd;
285
+ }
286
+ /** Number of sources held for a block no repair has stated yet. */
287
+ get heldCount() {
288
+ return this.#held.size;
289
+ }
290
+ /** The known block or span containing `ssId`, with its ESI. */
291
+ locate(ssId) {
292
+ for (let i = this.#blocks.length - 1; i >= 0; i--) {
293
+ const block = this.#blocks[i];
294
+ if (isoBlockContains(block.start, block.length, ssId)) {
295
+ return { block, esi: serial32Distance(block.start, ssId) };
296
+ }
297
+ }
298
+ return null;
299
+ }
300
+ /** The known block or span starting exactly at `start`. */
301
+ blockAt(start) {
302
+ return this.#blocks.find((block) => block.start === start);
303
+ }
304
+ /** Known blocks and spans overlapping the SS_IDs strictly between `a` and `b`. */
305
+ blocksBetween(a, b) {
306
+ const first = a + 1 >>> 0;
307
+ const count = serial32Distance(a, b) - 1;
308
+ if (!serial32Precedes(a, b) || count <= 0) return [];
309
+ return this.#blocks.filter((block) => {
310
+ const toBlock = serial32Distance(first, block.start);
311
+ const fromBlock = serial32Distance(block.start, first);
312
+ return toBlock < count || fromBlock < block.length;
313
+ });
314
+ }
315
+ /**
316
+ * Whether every SS_ID strictly between `a` and `b` lies in known coverage,
317
+ * so no block that could hold them is still to be stated by a repair.
318
+ */
319
+ coversBetween(a, b) {
320
+ if (this.#coverageEnd === void 0) return false;
321
+ if (!serial32Precedes(a, b) || serial32Distance(a, b) <= 1) return true;
322
+ return serial32Precedes(b - 1 >>> 0, this.#coverageEnd);
323
+ }
324
+ /**
325
+ * Hold a first-arrival source symbol whose block is not yet known. Returns
326
+ * false for a second copy of an SS_ID already held, so the first copy wins.
327
+ * Retires behind `ssId` (see the class note).
328
+ */
329
+ hold(ssId, payload) {
330
+ if (this.#held.has(ssId)) return false;
331
+ this.#held.set(ssId, payload);
332
+ this.retireBehind(ssId);
333
+ for (const oldest of this.#held.keys()) {
334
+ if (this.#held.size <= this.heldCapacity) break;
335
+ this.#held.delete(oldest);
336
+ this.#heldEvicted++;
337
+ }
338
+ return true;
339
+ }
340
+ /** Whether `ssId` is held unplaced. */
341
+ isHeld(ssId) {
342
+ return this.#held.has(ssId);
343
+ }
344
+ /**
345
+ * Apply one repair symbol's `(SS_Start, SSB_length)`. The first repair of a
346
+ * block states its range and places every held source in it; later repair
347
+ * symbols of the same block change nothing. A range that contradicts a
348
+ * known block (a different length, or an overlap with another block or a
349
+ * span) fails closed: the repair symbol is dropped and no block changes.
350
+ * A newly stated block retires behind its SS_Start (see the class note).
351
+ */
352
+ placeRepair(start, length) {
353
+ if (!Number.isSafeInteger(length) || length <= 0 || length > this.#kMax) {
354
+ return { status: "conflict", reason: `SSB_length ${length} is outside 1..${this.#kMax}` };
355
+ }
356
+ const known = this.blockAt(start);
357
+ if (known !== void 0) {
358
+ if (known.span) {
359
+ return { status: "overlap", reason: `repair block ${start}+${length} falls inside a repair-less span` };
360
+ }
361
+ if (known.length !== length) {
362
+ return {
363
+ status: "conflict",
364
+ reason: `repair SSB_length ${length} conflicts with ${known.length} for SS_Start ${start}`
365
+ };
366
+ }
367
+ return { status: "placed", block: known, placed: [] };
368
+ }
369
+ for (const block2 of this.#blocks) {
370
+ if (isoBlockContains(block2.start, block2.length, start) || isoBlockContains(start, length, block2.start)) {
371
+ return {
372
+ status: "overlap",
373
+ reason: `repair block ${start}+${length} overlaps known block ${block2.start}+${block2.length}`
374
+ };
375
+ }
376
+ }
377
+ let span;
378
+ if (this.#coverageEnd === void 0) {
379
+ for (const ssId of this.#held.keys()) {
380
+ if (serial32Precedes(ssId, start)) this.#held.delete(ssId);
381
+ }
382
+ this.#coverageEnd = start;
383
+ } else if (serial32Precedes(start, this.#coverageEnd)) {
384
+ return { status: "stale", reason: `repair block ${start}+${length} precedes known coverage` };
385
+ } else if (start !== this.#coverageEnd) {
386
+ const spanBlock = {
387
+ start: this.#coverageEnd,
388
+ length: serial32Distance(this.#coverageEnd, start),
389
+ span: true
390
+ };
391
+ span = { block: spanBlock, placed: this.#claim(spanBlock) };
392
+ this.#blocks.push(spanBlock);
393
+ }
394
+ const block = { start, length, span: false };
395
+ const placed = this.#claim(block);
396
+ this.#blocks.push(block);
397
+ this.#coverageEnd = isoBlockEnd(start, length);
398
+ this.retireBehind(start);
399
+ return span === void 0 ? { status: "placed", block, placed } : { status: "placed", block, placed, span };
400
+ }
401
+ /**
402
+ * Retire every block and span whose last SS_ID, and every held source,
403
+ * lies more than `retentionBlocks` blocks behind `position`, with the shared
404
+ * lower-bound metric (`isoBlocksBehindLowerBound`) every ISO interleave
405
+ * horizon uses. Returns the retired block starts.
406
+ */
407
+ retireBehind(position) {
408
+ this.observe(position);
409
+ const horizon = this.#retentionBlocks;
410
+ const retired = [];
411
+ for (let i = this.#blocks.length - 1; i >= 0; i--) {
412
+ const block = this.#blocks[i];
413
+ const last = serial32Add(block.start, block.length - 1);
414
+ if (isoBlocksBehindLowerBound(last, position, this.#kMax) > horizon) {
415
+ retired.push(block.start);
416
+ this.#blocks.splice(i, 1);
417
+ }
418
+ }
419
+ for (const ssId of [...this.#held.keys()]) {
420
+ if (isoBlocksBehindLowerBound(ssId, position, this.#kMax) > horizon) this.#held.delete(ssId);
421
+ }
422
+ return retired;
423
+ }
424
+ /** Forget everything (publisher restart, track reset). */
425
+ reset() {
426
+ this.#blocks.length = 0;
427
+ this.#held.clear();
428
+ this.#coverageEnd = void 0;
429
+ this.#newest = void 0;
430
+ }
431
+ #claim(block) {
432
+ const placed = [];
433
+ for (const [ssId, payload] of this.#held) {
434
+ if (!isoBlockContains(block.start, block.length, ssId)) continue;
435
+ placed.push({ ssId, esi: serial32Distance(block.start, ssId), payload });
436
+ this.#held.delete(ssId);
437
+ }
438
+ placed.sort((x, y) => x.esi - y.esi);
439
+ return placed;
440
+ }
441
+ };
442
+
443
+ // src/fec-state.ts
444
+ var MIN_INTERLEAVE_DEPTH_MS = 33;
445
+ var FEC_INACTIVE_REASONS = Object.freeze([
446
+ "no-catalog-fec",
447
+ "reliable-moq",
448
+ "no-multicast-bridge"
449
+ ]);
450
+ var INACTIVE = Object.freeze({
451
+ "no-catalog-fec": Object.freeze({ state: "inactive", reason: "no-catalog-fec" }),
452
+ "reliable-moq": Object.freeze({ state: "inactive", reason: "reliable-moq" }),
453
+ "no-multicast-bridge": Object.freeze({ state: "inactive", reason: "no-multicast-bridge" })
454
+ });
455
+
456
+ // src/block-policy.ts
457
+ import { deriveFecInterleaveDepth } from "@blockcast/mmt-fec";
458
+ function keyframeIntervalMediaTicks(geometry, mediaTimescale) {
459
+ if (geometry.trackKind !== "video" || geometry.mode !== "classifying") return null;
460
+ if (!Number.isSafeInteger(mediaTimescale) || mediaTimescale <= 0) return null;
461
+ const { ticks, timescale } = geometry.keyframeInterval;
462
+ if (!Number.isSafeInteger(ticks) || !Number.isSafeInteger(timescale)) return null;
463
+ const scaled = BigInt(ticks) * BigInt(mediaTimescale);
464
+ const divisor = BigInt(timescale);
465
+ if (scaled % divisor !== 0n) return null;
466
+ return scaled / divisor;
467
+ }
468
+ function compareDataUnitStarts(a, b) {
469
+ return a.movieFragment - b.movieFragment || a.idx - b.idx || a.offset - b.offset;
470
+ }
471
+ var BlockClassifier = class {
472
+ #geometry;
473
+ #mode;
474
+ #reason;
475
+ /**
476
+ * False once the stream showed that an MPU boundary does not bound its
477
+ * RAPs (a RAP inside an MPU, or an MPU longer than a GOP). Recorded in
478
+ * every mode: the stall/conceal decision relies on it even when the
479
+ * catalog geometry already runs the classifier `all-protected`.
480
+ */
481
+ #mpuBoundsRaps = true;
482
+ #blocks = /* @__PURE__ */ new Map();
483
+ /**
484
+ * P1 ledger, kept for the MPUs within the classification horizon (the
485
+ * highest MPU seen and the one before it): the SBN span of every source
486
+ * packet of each MPU, and each MPU's data-unit starts in authored order.
487
+ * Valid only within one continuous SBN sequence; a source discontinuity
488
+ * clears it (`noteSourceDiscontinuity`).
489
+ */
490
+ #mpuSbnSpans = /* @__PURE__ */ new Map();
491
+ #dataUnitStarts = /* @__PURE__ */ new Map();
492
+ #highestMpu = null;
493
+ /** MPU → movie_fragment_sequence_number observed in it. */
494
+ #mpuFragments = /* @__PURE__ */ new Map();
495
+ #lastRap = null;
496
+ constructor(geometry) {
497
+ this.#geometry = geometry;
498
+ this.#mode = "classifying";
499
+ this.#reason = null;
500
+ this.reset(geometry);
501
+ }
502
+ get geometry() {
503
+ return this.#geometry;
504
+ }
505
+ get mode() {
506
+ return this.#mode;
507
+ }
508
+ get reason() {
509
+ return this.#reason;
510
+ }
511
+ /**
512
+ * Whether "same MPU on both sides of a gap" still proves the gap held no
513
+ * RAP. It stops holding for the rest of the epoch once a RAP was received
514
+ * inside an MPU or an MPU ran longer than a GOP.
515
+ */
516
+ get mpuBoundsRaps() {
517
+ return this.#mpuBoundsRaps;
518
+ }
519
+ /** Begin a fresh epoch (catalog change or track reset). */
520
+ reset(geometry = this.#geometry) {
521
+ this.#geometry = geometry;
522
+ this.#blocks.clear();
523
+ this.#clearContiguityLedger();
524
+ this.#mpuFragments.clear();
525
+ this.#lastRap = null;
526
+ this.#mpuBoundsRaps = true;
527
+ if (geometry.trackKind === "video" && geometry.mode === "all-protected") {
528
+ this.#mode = "all-protected";
529
+ this.#reason = geometry.reason;
530
+ } else {
531
+ this.#mode = "classifying";
532
+ this.#reason = null;
533
+ }
534
+ }
535
+ /**
536
+ * Retire this classifier (its stream moves to a new catalog epoch). Every
537
+ * block it had classified `delta` loses the evidence behind that class and
538
+ * reads `unknown` from now on; the returned changes say so.
539
+ */
540
+ retire() {
541
+ const changes = [];
542
+ for (const [sbn, block] of this.#blocks) {
543
+ if (block.cls !== "delta") continue;
544
+ block.cls = "unknown";
545
+ changes.push({
546
+ kind: "reclassified",
547
+ sbn,
548
+ from: "delta",
549
+ to: "unknown",
550
+ evidence: "classifier-disabled"
551
+ });
552
+ }
553
+ return changes;
554
+ }
555
+ /**
556
+ * The stream's SBN sequence restarted (FECManager source discontinuity:
557
+ * a publisher restart, or the SS_ID space wrapped). Authored order and SBN
558
+ * order are comparable only within one continuous sequence, so the P1
559
+ * ledger starts again.
560
+ */
561
+ noteSourceDiscontinuity() {
562
+ this.#clearContiguityLedger();
563
+ }
564
+ /** Register a block that exists without evidence (a vanished block). */
565
+ touch(sbn) {
566
+ this.#blockFor(sbn);
567
+ }
568
+ /** Forget a resolved block. */
569
+ forget(sbn) {
570
+ this.#blocks.delete(sbn);
571
+ }
572
+ classOf(sbn) {
573
+ if (this.#geometry.trackKind === "audio") return "audio";
574
+ return this.#blocks.get(sbn)?.cls ?? "unknown";
575
+ }
576
+ /** Record one first-arrival source symbol's evidence for block `sbn`. */
577
+ observe(sbn, evidence) {
578
+ if (this.#geometry.trackKind === "audio") return [];
579
+ const changes = [];
580
+ const block = this.#blockFor(sbn);
581
+ if (evidence.rap) block.rapSeen = true;
582
+ if (evidence.mpu !== void 0) block.mpus.add(evidence.mpu);
583
+ if (evidence.idx !== void 0) {
584
+ block.idxMin = block.idxMin === void 0 ? evidence.idx : Math.min(block.idxMin, evidence.idx);
585
+ block.idxMax = block.idxMax === void 0 ? evidence.idx : Math.max(block.idxMax, evidence.idx);
586
+ }
587
+ if (evidence.rap && evidence.idx !== void 0 && evidence.idx !== 0) {
588
+ this.#mpuBoundsRaps = false;
589
+ }
590
+ const violation = this.#checkInvariants(sbn, block, evidence);
591
+ if (violation !== null) changes.push(...this.#disable(violation));
592
+ const next = this.#classify(block);
593
+ if (next !== block.cls && this.#allowed(block.cls, next)) {
594
+ changes.push({
595
+ kind: "reclassified",
596
+ sbn,
597
+ from: block.cls,
598
+ to: next,
599
+ evidence: this.#evidenceFor(block, next)
600
+ });
601
+ block.cls = next;
602
+ }
603
+ return changes;
604
+ }
605
+ /**
606
+ * Record a RAP frame released in order. Released MPU-start RAPs of MPUs m
607
+ * and m+n must be exactly n keyframe intervals apart in the media
608
+ * timescale, compared as integers.
609
+ *
610
+ * Comparing only consecutive MPUs would miss exactly the case the window
611
+ * rule cannot survive: a short MPU whose successor's RAP was lost (that
612
+ * RAP-bearing block is the one a wrong G calls delta), because the next
613
+ * released RAP then belongs to m+2. Every MPU is one interval long, so the
614
+ * span to any later released RAP proves or refutes all MPUs in between.
615
+ */
616
+ noteReleasedRap(mpu, presentationTime, mediaTimescale) {
617
+ if (this.#geometry.trackKind === "audio" || this.#mode === "all-protected") {
618
+ return [];
619
+ }
620
+ const interval = keyframeIntervalMediaTicks(this.#geometry, mediaTimescale);
621
+ if (interval === null) return this.#disable("keyframe-interval-not-media-integral");
622
+ const previous = this.#lastRap;
623
+ if (previous !== null && previous.mpu === mpu && previous.mediaTimescale === mediaTimescale) {
624
+ return [];
625
+ }
626
+ this.#lastRap = { mpu, presentationTime, mediaTimescale };
627
+ if (previous === null || mpu < previous.mpu || previous.mediaTimescale !== mediaTimescale) {
628
+ return [];
629
+ }
630
+ if (presentationTime - previous.presentationTime === interval * BigInt(mpu - previous.mpu)) {
631
+ return [];
632
+ }
633
+ return this.#disable("rap-spacing-mismatch");
634
+ }
635
+ #blockFor(sbn) {
636
+ let block = this.#blocks.get(sbn);
637
+ if (!block) {
638
+ block = { rapSeen: false, mpus: /* @__PURE__ */ new Set(), cls: "unknown" };
639
+ this.#blocks.set(sbn, block);
640
+ }
641
+ return block;
642
+ }
643
+ #checkInvariants(sbn, block, evidence) {
644
+ if (this.#mode === "all-protected" || this.#geometry.trackKind !== "video") return null;
645
+ const geometry = this.#geometry;
646
+ if (geometry.mode !== "classifying") return null;
647
+ if (evidence.rap && evidence.idx !== void 0 && evidence.idx !== 0) {
648
+ return "rap-not-mpu-start";
649
+ }
650
+ if (evidence.idx !== void 0 && evidence.idx >= geometry.gopFrames) {
651
+ return "mpu-longer-than-gop";
652
+ }
653
+ if (block.mpus.size === 1 && block.idxMin !== void 0 && block.idxMax !== void 0 && block.idxMax - block.idxMin >= geometry.interleaveUnits) {
654
+ return "block-wider-than-interleave";
655
+ }
656
+ if (evidence.mpu !== void 0 && evidence.movieFragment !== void 0) {
657
+ const fragment = this.#mpuFragments.get(evidence.mpu);
658
+ if (fragment === void 0) {
659
+ this.#mpuFragments.set(evidence.mpu, evidence.movieFragment);
660
+ for (const mpu of this.#mpuFragments.keys()) {
661
+ if (mpu + 1 < evidence.mpu) this.#mpuFragments.delete(mpu);
662
+ }
663
+ } else if (fragment !== evidence.movieFragment) {
664
+ return "mpu-multiple-fragments";
665
+ }
666
+ }
667
+ if (evidence.mpu !== void 0 && this.#contradictsAuthoredOrder(sbn, evidence.mpu, evidence)) {
668
+ return "block-not-contiguous";
669
+ }
670
+ return null;
671
+ }
672
+ #clearContiguityLedger() {
673
+ this.#mpuSbnSpans.clear();
674
+ this.#dataUnitStarts.clear();
675
+ this.#highestMpu = null;
676
+ }
677
+ /**
678
+ * P1 check for one source packet of MPU `mpu` in block `sbn`, then record
679
+ * it. True when some recorded packet's authored order and SBN order
680
+ * disagree with this one's (see the class comment). Only positions the
681
+ * wire states are compared: MPU order for every packet, and data-unit
682
+ * order for packets that carry a DU header. A packet of one data unit
683
+ * that continues into the next block carries no DU header and so only
684
+ * takes part through its MPU, which is exactly what makes an adjacent
685
+ * straddle consistent.
686
+ */
687
+ #contradictsAuthoredOrder(sbn, mpu, evidence) {
688
+ for (const [other, span2] of this.#mpuSbnSpans) {
689
+ if (other < mpu && span2.max > sbn) return true;
690
+ if (other > mpu && span2.min < sbn) return true;
691
+ }
692
+ const inHorizon = this.#highestMpu === null || mpu + 1 >= this.#highestMpu;
693
+ let insertAt = -1;
694
+ let starts;
695
+ let start;
696
+ if (evidence.idx !== void 0) {
697
+ start = {
698
+ movieFragment: evidence.movieFragment ?? 0,
699
+ idx: evidence.idx,
700
+ offset: evidence.offset ?? 0,
701
+ sbn
702
+ };
703
+ starts = this.#dataUnitStarts.get(mpu);
704
+ if (starts !== void 0) {
705
+ let lo = 0;
706
+ let hi = starts.length;
707
+ while (lo < hi) {
708
+ const mid = lo + hi >>> 1;
709
+ if (compareDataUnitStarts(starts[mid], start) < 0) lo = mid + 1;
710
+ else hi = mid;
711
+ }
712
+ const at = starts[lo];
713
+ if (at !== void 0 && compareDataUnitStarts(at, start) === 0) {
714
+ if (at.sbn !== sbn) return true;
715
+ } else {
716
+ const before = starts[lo - 1];
717
+ if (before !== void 0 && before.sbn > sbn) return true;
718
+ if (at !== void 0 && at.sbn < sbn) return true;
719
+ insertAt = lo;
720
+ }
721
+ }
722
+ }
723
+ if (!inHorizon) return false;
724
+ const span = this.#mpuSbnSpans.get(mpu);
725
+ if (span === void 0) this.#mpuSbnSpans.set(mpu, { min: sbn, max: sbn });
726
+ else {
727
+ if (sbn < span.min) span.min = sbn;
728
+ if (sbn > span.max) span.max = sbn;
729
+ }
730
+ if (start !== void 0) {
731
+ if (starts === void 0) this.#dataUnitStarts.set(mpu, [start]);
732
+ else if (insertAt >= 0) starts.splice(insertAt, 0, start);
733
+ }
734
+ if (this.#highestMpu === null || mpu > this.#highestMpu) {
735
+ this.#highestMpu = mpu;
736
+ for (const other of [...this.#mpuSbnSpans.keys()]) {
737
+ if (other + 1 < mpu) {
738
+ this.#mpuSbnSpans.delete(other);
739
+ this.#dataUnitStarts.delete(other);
740
+ }
741
+ }
742
+ }
743
+ return false;
744
+ }
745
+ #disable(reason) {
746
+ if (reason === "rap-not-mpu-start" || reason === "mpu-longer-than-gop") {
747
+ this.#mpuBoundsRaps = false;
748
+ }
749
+ if (this.#mode === "all-protected") return [];
750
+ this.#mode = "all-protected";
751
+ this.#reason = reason;
752
+ const changes = [{ kind: "mode", mode: "all-protected", reason }];
753
+ for (const [sbn, block] of this.#blocks) {
754
+ if (block.cls !== "delta") continue;
755
+ block.cls = "unknown";
756
+ changes.push({
757
+ kind: "reclassified",
758
+ sbn,
759
+ from: "delta",
760
+ to: "unknown",
761
+ evidence: "classifier-disabled"
762
+ });
763
+ }
764
+ return changes;
765
+ }
766
+ #classify(block) {
767
+ if (block.rapSeen || block.mpus.size >= 2) return "keyframe";
768
+ if (this.#mode === "all-protected") return "unknown";
769
+ const geometry = this.#geometry;
770
+ if (geometry.trackKind !== "video" || geometry.mode !== "classifying") return "unknown";
771
+ if (block.mpus.size === 1 && block.idxMin !== void 0 && block.idxMax !== void 0) {
772
+ const span = geometry.interleaveUnits - 1;
773
+ if (block.idxMax - span >= 1 && block.idxMin + span <= geometry.gopFrames - 1) {
774
+ return "delta";
775
+ }
776
+ }
777
+ return "unknown";
778
+ }
779
+ #allowed(from, to) {
780
+ if (from === "keyframe") return false;
781
+ if (from === "unknown") return to === "delta" || to === "keyframe";
782
+ if (from === "delta") return to === "keyframe";
783
+ return false;
784
+ }
785
+ #evidenceFor(block, to) {
786
+ if (to === "delta") return "window";
787
+ return block.rapSeen ? "rap" : "mpu-span";
788
+ }
789
+ };
790
+
791
+ // src/fec-manager.ts
792
+ var WORKER_ERROR_LOG_CAP = 5;
793
+ var WORKER_ERROR_KIND_CAP = 32;
794
+ var WORKER_ERROR_OVERFLOW_KIND = "other";
795
+ var STARTUP_REPAIR_WAIT_SOURCE_BOUNDARIES = 2;
796
+ function isMulticastSymbolDelivery(value) {
797
+ return value === "datagram" || value === "ordered-relay";
798
+ }
799
+ function newFecCounters() {
800
+ return {
801
+ sourcePackets: 0,
802
+ repairPackets: 0,
803
+ duplicateSymbols: 0,
804
+ blocksClean: 0,
805
+ blocksRecovered: 0,
806
+ blocksFailed: 0
807
+ };
808
+ }
809
+ function dropOldest(collection, count) {
810
+ const dropped = [];
811
+ for (const key of collection.keys()) {
812
+ if (dropped.length >= count) break;
813
+ dropped.push(key);
814
+ }
815
+ for (const key of dropped) collection.delete(key);
816
+ return dropped;
817
+ }
818
+ var TerminalQueue = class {
819
+ #records = [];
820
+ #head = 0;
821
+ push(record) {
822
+ this.#records.push(record);
823
+ }
824
+ peek() {
825
+ return this.#records[this.#head];
826
+ }
827
+ shift() {
828
+ this.#head++;
829
+ if (this.#head * 2 >= this.#records.length) {
830
+ this.#records = this.#records.slice(this.#head);
831
+ this.#head = 0;
832
+ }
833
+ }
834
+ };
835
+ var FECManager = class _FECManager {
836
+ constructor() {
837
+ // Per-track WASM RaptorQ decoders, keyed by physical stream identity.
838
+ this.#fecDecoders = /* @__PURE__ */ new Map();
839
+ /** @deprecated Use setFecDecoder(packetId, ...). Backwards-compat shim. */
840
+ // Per-track Worker-backed FEC decoders, keyed by physical stream identity.
841
+ // Worker-backed tracks use async decode — recovered frames arrive via onFrame callback,
842
+ // not synchronous return from feedSourceSymbol/feedRepairSymbol.
843
+ this.#workerClients = /* @__PURE__ */ new Map();
844
+ // Worker clients whose streams releasePacket ended. A Worker posts
845
+ // asynchronously, so a message it sent before its reset can still arrive;
846
+ // the stream it describes is gone, and letting it in would rebuild state
847
+ // (block ledgers, statistics, lifecycle events) for a retired packet ID.
848
+ this.#releasedWorkerClients = /* @__PURE__ */ new WeakSet();
849
+ // Worker setup can finish while an interleaved block is already collecting
850
+ // on the sync decoder. Keep that block on its original decoder so later
851
+ // source/repair symbols do not split across two RaptorQ states.
852
+ this.#syncDecoderBlockOwners = /* @__PURE__ */ new Map();
853
+ this.#decoderBlockOwners = /* @__PURE__ */ new WeakMap();
854
+ this.#fecK = 4;
855
+ this.#fecSymbolSize = 0;
856
+ this.#fecTimeoutMs = 500;
857
+ // default 500ms; refined by setFramerate() when fps known
858
+ this.#trackConfigs = /* @__PURE__ */ new Map();
859
+ this.#fecStats = newFecCounters();
860
+ this.#fecTrackStats = /* @__PURE__ */ new Map();
861
+ this.#fecWorkerStats = /* @__PURE__ */ new Map();
862
+ this.#fecStatsObservers = /* @__PURE__ */ new Set();
863
+ // Block inactivity tracking: (packetId, SBN) -> the one armed timer. The
864
+ // deadline is derived from catalog geometry and refreshed on progress; only
865
+ // an idle block fails. #activeBlockDeadlinesMs is the authoritative
866
+ // deadline; the timer re-checks it when it fires (see DeadlineTimer).
867
+ this.#activeBlocks = /* @__PURE__ */ new Map();
868
+ this.#activeBlockDeadlinesMs = /* @__PURE__ */ new Map();
869
+ // The two terms #activeBlockDeadlinesMs is the later of (BLO-35597):
870
+ // - the multicast late-symbol wait, refreshed by every accepted symbol;
871
+ // - a D-28 repair hold, set by the repair scheduler only while a repair
872
+ // FETCH it admitted can still land before the block's presentation
873
+ // deadline. The multicast span elapsing never ends a fetch the scheduler
874
+ // judged able to arrive in time.
875
+ this.#blockWaitDeadlinesMs = /* @__PURE__ */ new Map();
876
+ this.#repairHoldUntilMs = /* @__PURE__ */ new Map();
877
+ // Receiver-measured multicast delivery gap: the late-symbol wait's jitter
878
+ // term (see DeliveryGapEstimator for the statistic and why).
879
+ this.#deliveryGaps = new DeliveryGapEstimator();
880
+ this.#lifecycleTrace = [];
881
+ // Worker-backed timeouts enqueue cleanup behind all prior symbol messages.
882
+ // Failure remains pending until cleanupComplete acknowledges that queue drain.
883
+ this.#pendingWorkerCleanups = /* @__PURE__ */ new Set();
884
+ this.#pendingWorkerCleanupClients = /* @__PURE__ */ new Map();
885
+ // One bound per owed cleanup: the acknowledgement is awaited for at most one
886
+ // catalog-derived block lifetime (fail bound plus repair grace).
887
+ // A Worker that hangs or drops one message must not leave the block
888
+ // 'pending' forever, which would park every later frame of its stream in
889
+ // the in-order release gate. Each entry owns its handle; the callback
890
+ // checks it still owns the key, so a settled or reset key is never touched.
891
+ this.#pendingWorkerCleanupBounds = /* @__PURE__ */ new Map();
892
+ // Source symbol count per track-scoped block — marks complete when K symbols arrive.
893
+ this.#blockSourceCounts = /* @__PURE__ */ new Map();
894
+ this.#blockSourceEsis = /* @__PURE__ */ new Map();
895
+ // Authored SSB_length per block (ISO/IEC 23008-1 §C.5.3), learned from the
896
+ // first repair symbol. The FFmpeg moq_mmt publisher closes a block every D
897
+ // media units, usually before it holds K symbols, and fec-raptorq
898
+ // force_complete() encodes it as a K-symbol RaptorQ block whose ESIs
899
+ // SSB_length..K-1 are zero padding that is never sent. Such a block is clean
900
+ // at SSB_length source arrivals and recoverable from SSB_length received
901
+ // symbols; waiting for K fails every one of them.
902
+ this.#blockAuthoredLengths = /* @__PURE__ */ new Map();
903
+ this.#ssbLengthConflictCount = 0;
904
+ this.#blockRepairRsIds = /* @__PURE__ */ new Map();
905
+ // Blocks already recovered — prevents late source symbols from restarting timeouts.
906
+ // WASM removes blocks after recovery; late symbols create empty blocks returning null,
907
+ // which would falsely start new timeouts without this guard.
908
+ //
909
+ // Every insertion goes through #markTerminal, which retires the key once its
910
+ // catalog-derived retention window (terminalRetentionMsForPacket) passes. The
911
+ // previous bound (trim to 500 at 1000) lived in cancelBlockTimeout alone, so
912
+ // a stream whose blocks only failed grew this set without limit (BLO-36599).
913
+ this.#completedBlocks = /* @__PURE__ */ new Set();
914
+ this.#terminalExpiryMs = /* @__PURE__ */ new Map();
915
+ this.#terminalQueues = /* @__PURE__ */ new Map();
916
+ // sbn -> (block key -> packetId) for every key held by #activeBlocks,
917
+ // #blockSourceCounts, #blockCatalogTrackNames or #completedBlocks. A lookup
918
+ // without a catalog name used to string-parse every key of all of those on
919
+ // each source packet, which is O(retained blocks) per packet (BLO-36599).
920
+ this.#blockKeysBySbn = /* @__PURE__ */ new Map();
921
+ // Stable per-name encodings for stream and block keys, bounded by the
922
+ // catalog's track names; #blockKey is built several times per packet.
923
+ this.#encodedTrackNames = /* @__PURE__ */ new Map();
924
+ // The last block and stream key built. One packet asks for the same block's
925
+ // key from every FECManager entry point it passes through (~6 per source
926
+ // symbol), so each key is built once per packet rather than once per call
927
+ // (BLO-36599). Both keys are pure functions of their arguments.
928
+ this.#lastBlockKeySbn = -1;
929
+ this.#lastBlockKeyPacketId = -1;
930
+ this.#lastBlockKeyTrackName = void 0;
931
+ this.#lastBlockKey = "";
932
+ this.#lastStreamKeyPacketId = -1;
933
+ this.#lastStreamKeyTrackName = void 0;
934
+ this.#lastStreamKey = "";
935
+ this.#seenSymbols = /* @__PURE__ */ new Set();
936
+ this.#blockPathCounts = /* @__PURE__ */ new Map();
937
+ // Receiver-local first-accepted-symbol time per block, bounded like
938
+ // #blockPathCounts (see blockAnchorAtMs).
939
+ this.#blockAnchorsMs = /* @__PURE__ */ new Map();
940
+ // Exact catalog identity observed on accepted symbols for each FEC block.
941
+ // Conflicting identities collapse to null so recovery fails closed.
942
+ this.#blockCatalogTrackNames = /* @__PURE__ */ new Map();
943
+ // A receiver joins an already-running live stream in the middle of D
944
+ // interleaved source blocks. This applies to both multicast and reliable MoQ:
945
+ // a MoQ group boundary is not an FEC ESI=0 boundary. Those blocks are not
946
+ // packet loss because their earlier ESIs predate the subscription. Ignore
947
+ // each such block until ESI=0 establishes a complete boundary for the track.
948
+ // After that boundary, a first observed ESI>0 is real delivery loss and must
949
+ // continue through normal FEC recovery/failure accounting.
950
+ this.#startupAlignedPackets = /* @__PURE__ */ new Set();
951
+ this.#startupPartialBlocks = /* @__PURE__ */ new Set();
952
+ // A live multicast join can observe an ESI=0 source boundary before the
953
+ // repair lane catches up. Returning false here also drops that media packet
954
+ // at MmtpRouter, so the readiness probe is multicast-only and bounded: an
955
+ // actual multicast repair unlocks the next boundary, while a second source
956
+ // boundary fails open. Reliable MoQ media continues through normal startup
957
+ // alignment and retained MoQ repair cannot prove multicast readiness.
958
+ this.#startupRequireRepairReady = false;
959
+ this.#startupRepairReadyPackets = /* @__PURE__ */ new Set();
960
+ this.#startupRepairBoundaryCounts = /* @__PURE__ */ new Map();
961
+ this.#startupRepairFailOpenPackets = /* @__PURE__ */ new Set();
962
+ // SBNs that reached K source-symbol arrivals via trackBlock. Used by the
963
+ // worker recovery suppression check (see onFrame handler below) so we only
964
+ // suppress `recovered=true` emits for blocks that ALSO completed cleanly
965
+ // via source symbols. Separating this from #completedBlocks (which is also
966
+ // populated by every worker frame emit, by failed timeouts, and by external
967
+ // callers) was the BLO-3323 L15 fix: the prior predicate caused blocksRecovered
968
+ // to stay at 0 because the SBN was always in #completedBlocks by the time the
969
+ // worker fired recovered=true.
970
+ this.#cleanlyCompletedBlocks = /* @__PURE__ */ new Set();
971
+ // D-28: blocks whose repair deadline already fired onRepairNeeded. At most
972
+ // one repair request per block; entries are removed when the block
973
+ // completes, recovers, or fails.
974
+ this.#repairRequestedBlocks = /* @__PURE__ */ new Set();
975
+ // D-28: repair deadlines refreshed by accepted source progress until the
976
+ // first repair request. A `null` value marks a deadline that already fired
977
+ // and requested repair (never re-armed). At most K accepted source symbols
978
+ // can refresh the horizon, so browser scheduling delay cannot create an
979
+ // early loss signal and repair traffic cannot keep a block alive
980
+ // indefinitely. #repairDeadlineAtMs is the authoritative deadline; the one
981
+ // armed timer re-checks it when it fires (see DeadlineTimer).
982
+ this.#repairDeadlines = /* @__PURE__ */ new Map();
983
+ this.#repairDeadlineAtMs = /* @__PURE__ */ new Map();
984
+ // Worker recovery emits all K source symbols. Track recovery once per block
985
+ // while routing only ESI slots that were absent from the direct source path.
986
+ this.#workerRecoveredBlocks = /* @__PURE__ */ new Set();
987
+ // How the multicast leg delivers symbols (see MulticastSymbolDelivery). The
988
+ // provider declares it per transport join; absence keeps datagram semantics.
989
+ this.#multicastSymbolDelivery = "datagram";
990
+ // ordered-relay: per stream, the highest SBN any multicast symbol carried.
991
+ // The relay preserves the receiving socket's order, so every block more than
992
+ // the decoder's interleave horizon behind this frontier has received every
993
+ // symbol it will ever receive over multicast.
994
+ this.#orderedFrontiers = /* @__PURE__ */ new Map();
995
+ // ordered-relay: blocks with multicast symbols that are neither terminal nor
996
+ // handed to a decoder cleanup, per stream, keyed by SBN. They carry no
997
+ // timer; the frontier settles them. At most one interleave horizon of blocks
998
+ // per stream is ever here.
999
+ this.#orderedPendingBlocks = /* @__PURE__ */ new Map();
1000
+ // ── Keyframe-protected repair policy (block-policy.ts) ──
1001
+ // One classifier per physical stream, built from that stream's geometry.
1002
+ this.#classifiers = /* @__PURE__ */ new Map();
1003
+ // Geometry a stream-scoped configuration gave one stream (streamKey). ABR
1004
+ // rungs can share a packetId with a different framerate or GOP, so the
1005
+ // packet-level geometry is only the geometry of a packet whose streams
1006
+ // were never configured apart (see #geometryFor).
1007
+ this.#streamGeometries = /* @__PURE__ */ new Map();
1008
+ // Terminal outcomes retained for in-order frame release: a failed block is
1009
+ // resolved-with-loss, a completed one resolved-clean.
1010
+ this.#failedBlocks = /* @__PURE__ */ new Set();
1011
+ // A block's class at resolution, so events emitted after the classifier
1012
+ // forgot the block (the router counts a recovery after cancelling the
1013
+ // block) still carry it.
1014
+ this.#resolvedBlockClasses = /* @__PURE__ */ new Map();
1015
+ // Blocks already reported through onBlockRepaired (once per block).
1016
+ this.#repairedBlocks = /* @__PURE__ */ new Set();
1017
+ // Blocks created without any symbol because every symbol vanished.
1018
+ this.#materializedBlocks = /* @__PURE__ */ new Set();
1019
+ // packetIds whose ordered-relay horizon became underivable and fell back to
1020
+ // the datagram deadline. Bounds the warn in #derivableOrderedHorizon to once
1021
+ // per stream. Not only throws: the ordinary out-of-range case is detected by
1022
+ // arithmetic and never throws at all.
1023
+ this.#orderedHorizonWarnedFor = /* @__PURE__ */ new Set();
1024
+ // Highest SBN seen per stream and when, for vanished-block detection.
1025
+ this.#highestSbn = /* @__PURE__ */ new Map();
1026
+ // iso-ssbg1-v1 block location per stream (streamKey). A source belongs to a
1027
+ // block only once a repair symbol stated the block's (SS_Start, SSB_length).
1028
+ this.#isoLocators = /* @__PURE__ */ new Map();
1029
+ this.#isoRepairRefusedCount = 0;
1030
+ this.#isoSourceOversizeCount = 0;
1031
+ // Held sources a locator's capacity cap evicted (IsoBlockLocator.heldCapacity):
1032
+ // only a stream whose SS_IDs are not consecutive reaches it. Cumulative,
1033
+ // unlike the locators, which a reset discards.
1034
+ this.#isoHeldEvictedCount = 0;
1035
+ // Sources a synchronous decoder held unplaced and retired at its horizon
1036
+ // (WasmFecDecoder.retire_unplaced): placed sources whose block's repair
1037
+ // never reached that decoder. Cumulative.
1038
+ this.#isoDecoderUnplacedRetired = 0;
1039
+ // Failed retire_unplaced calls. The sweep runs on every feed, so its warn is
1040
+ // rate-limited like the other per-symbol warns here. Cumulative.
1041
+ this.#isoDecoderUnplacedRetireFailed = 0;
1042
+ // The latest source symbol per stream that carried its MMTP
1043
+ // packet_sequence_number: the anchor that bounds how many blocks the
1044
+ // producer can have started before a later source symbol.
1045
+ this.#sourceAnchors = /* @__PURE__ */ new Map();
1046
+ // Catalog/reset epoch per packetId. Gap decisions carry it for fencing.
1047
+ this.#packetEpochs = /* @__PURE__ */ new Map();
1048
+ // Framerate detection state
1049
+ this.#fpsDetected = false;
1050
+ this.#fecInterleaveDepthMs = 0;
1051
+ // Worker error log rate-limiter — bounds console.warn output and exported
1052
+ // error cardinality when the FEC Worker emits the same failure repeatedly
1053
+ // with volatile packet details (e.g. ssStart) in the message text.
1054
+ // Stable Worker codes aggregate those messages; uncoded/legacy messages are
1055
+ // capped into an `other` bucket. This also covers wasm-bindgen
1056
+ // "recursive use of an object" bubbled from add_source/add_repair during
1057
+ // overlapping message processing). Without this, Point 5 of the 7-point
1058
+ // validation harness — which caps headless warnings at 500 — fails even
1059
+ // though the errors are duplicates of a single underlying event.
1060
+ this.#workerErrorCounts = /* @__PURE__ */ new Map();
1061
+ this.#decoderUnavailableEvents = 0;
1062
+ this.#repairSymbolSizeMismatchCount = 0;
1063
+ // Event callbacks — block lifecycle notifications for MmtpRouter wiring.
1064
+ // NOTE: For sync WASM path, recovery routing is handled by MmtpRouter from
1065
+ // feedSourceSymbol/feedRepairSymbol return values. For Worker path, recovery
1066
+ // arrives via onBlockRecovered callback.
1067
+ this.onBlockStarted = null;
1068
+ this.onBlockComplete = null;
1069
+ /** A block reached its fail bound. `blockClass` is its class at failure. */
1070
+ this.onBlockFailed = null;
1071
+ /**
1072
+ * Fired once per stream when its Worker decoder stops being routable.
1073
+ * `reason` is 'cleared' when clearFecWorker() removed it and 'disposed' when
1074
+ * a symbol or reset found the registered client already disposed and
1075
+ * evicted it. `fallbackDecoder` is 'sync' when a synchronous WASM decoder
1076
+ * registered for the same stream keeps decoding its symbols, and 'none'
1077
+ * when nothing does. Only 'none' leaves the stream unprotected: the host
1078
+ * must then register a decoder again or leave the stream, because counting
1079
+ * its symbols as handled would hide the loss.
1080
+ */
1081
+ this.onDecoderUnavailable = null;
1082
+ // D-28 on-demand repair callbacks. When onRepairNeeded is registered, each
1083
+ // block arms a one-shot repair deadline after the derived K-symbol source
1084
+ // horizon; a block still short of K when it fires requests a per-SBN MoQ
1085
+ // repair fetch upstream. The inactivity fail bound is unchanged either way —
1086
+ // repair and failure are orthogonal timers, so wiring D-28 never delays
1087
+ // failure accounting. onRepairResolved fires when a repair-requested block
1088
+ // leaves the active set, with the outcome ("completed" after clean completion
1089
+ // or recovery, "failed" at the fail bound) so the upstream in-flight slot
1090
+ // (MulticastManager.clearRepairForBlock) can be released and logged.
1091
+ this.onRepairNeeded = null;
1092
+ this.onRepairResolved = null;
1093
+ /**
1094
+ * A block that could not complete from its first-arrival source symbols
1095
+ * alone was completed anyway: by FEC decode (in-band or QUIC repair
1096
+ * symbols) or, after a D-28 request, by late symbols. Fired once per block.
1097
+ */
1098
+ this.onBlockRepaired = null;
1099
+ /** A block's class changed on new evidence or when classification was disabled. */
1100
+ this.onBlockReclassified = null;
1101
+ /** A video stream's classifier mode was established or changed. */
1102
+ this.onClassifierMode = null;
1103
+ /** A block every symbol of which vanished was created by SBN evidence. */
1104
+ this.onBlockVanished = null;
1105
+ /** A block reached a terminal state (completed, recovered, or failed). */
1106
+ this.onBlockResolved = null;
1107
+ /** A stream's SBN went backwards (or jumped implausibly): the source restarted. */
1108
+ this.onSourceDiscontinuity = null;
1109
+ // Worker-path callbacks — async recovery and stats from FecWorkerClient.
1110
+ this.onBlockRecovered = null;
1111
+ this.onFecStatsUpdate = null;
1112
+ }
1113
+ #fecDecoders;
1114
+ #workerClients;
1115
+ #releasedWorkerClients;
1116
+ #syncDecoderBlockOwners;
1117
+ #decoderBlockOwners;
1118
+ #fecK;
1119
+ #fecSymbolSize;
1120
+ #fecTimeoutMs;
1121
+ #trackConfigs;
1122
+ #fecStats;
1123
+ #fecTrackStats;
1124
+ #fecWorkerStats;
1125
+ #fecStatsObservers;
1126
+ #activeBlocks;
1127
+ #activeBlockDeadlinesMs;
1128
+ #blockWaitDeadlinesMs;
1129
+ #repairHoldUntilMs;
1130
+ #deliveryGaps;
1131
+ #lifecycleTrace;
1132
+ #pendingWorkerCleanups;
1133
+ #pendingWorkerCleanupClients;
1134
+ #pendingWorkerCleanupBounds;
1135
+ #blockSourceCounts;
1136
+ #blockSourceEsis;
1137
+ #blockAuthoredLengths;
1138
+ #ssbLengthConflictCount;
1139
+ #blockRepairRsIds;
1140
+ #completedBlocks;
1141
+ #terminalExpiryMs;
1142
+ #terminalQueues;
1143
+ #blockKeysBySbn;
1144
+ #encodedTrackNames;
1145
+ #lastBlockKeySbn;
1146
+ #lastBlockKeyPacketId;
1147
+ #lastBlockKeyTrackName;
1148
+ #lastBlockKey;
1149
+ #lastStreamKeyPacketId;
1150
+ #lastStreamKeyTrackName;
1151
+ #lastStreamKey;
1152
+ #seenSymbols;
1153
+ #blockPathCounts;
1154
+ #blockAnchorsMs;
1155
+ #blockCatalogTrackNames;
1156
+ #startupAlignedPackets;
1157
+ #startupPartialBlocks;
1158
+ #startupRequireRepairReady;
1159
+ #startupRepairReadyPackets;
1160
+ #startupRepairBoundaryCounts;
1161
+ #startupRepairFailOpenPackets;
1162
+ #cleanlyCompletedBlocks;
1163
+ #repairRequestedBlocks;
1164
+ #repairDeadlines;
1165
+ #repairDeadlineAtMs;
1166
+ #workerRecoveredBlocks;
1167
+ #multicastSymbolDelivery;
1168
+ #orderedFrontiers;
1169
+ #orderedPendingBlocks;
1170
+ #classifiers;
1171
+ #streamGeometries;
1172
+ #failedBlocks;
1173
+ #resolvedBlockClasses;
1174
+ #repairedBlocks;
1175
+ #materializedBlocks;
1176
+ #orderedHorizonWarnedFor;
1177
+ #highestSbn;
1178
+ #isoLocators;
1179
+ #isoRepairRefusedCount;
1180
+ #isoSourceOversizeCount;
1181
+ #isoHeldEvictedCount;
1182
+ #isoDecoderUnplacedRetired;
1183
+ #isoDecoderUnplacedRetireFailed;
1184
+ #sourceAnchors;
1185
+ #packetEpochs;
1186
+ #fpsDetected;
1187
+ #fecFps;
1188
+ #fecInterleaveDepthMs;
1189
+ #workerErrorCounts;
1190
+ #decoderUnavailableEvents;
1191
+ #repairSymbolSizeMismatchCount;
1192
+ #encodeTrackName(catalogTrackName) {
1193
+ let encoded = this.#encodedTrackNames.get(catalogTrackName);
1194
+ if (encoded === void 0) {
1195
+ encoded = encodeURIComponent(catalogTrackName);
1196
+ this.#encodedTrackNames.set(catalogTrackName, encoded);
1197
+ }
1198
+ return encoded;
1199
+ }
1200
+ #streamKey(packetId, catalogTrackName) {
1201
+ if (packetId === this.#lastStreamKeyPacketId && catalogTrackName === this.#lastStreamKeyTrackName) {
1202
+ return this.#lastStreamKey;
1203
+ }
1204
+ const key = catalogTrackName === void 0 ? `${packetId}` : `${packetId}:${this.#encodeTrackName(catalogTrackName)}`;
1205
+ this.#lastStreamKeyPacketId = packetId;
1206
+ this.#lastStreamKeyTrackName = catalogTrackName;
1207
+ this.#lastStreamKey = key;
1208
+ return key;
1209
+ }
1210
+ #belongsToPacket(key, packetId) {
1211
+ return Number(key.split(":", 1)[0]) === packetId;
1212
+ }
1213
+ #belongsToStream(key, packetId, catalogTrackName) {
1214
+ if (!this.#belongsToPacket(key, packetId)) return false;
1215
+ const parts = key.split(":");
1216
+ const encodedTrackName = this.#encodeTrackName(catalogTrackName);
1217
+ if (parts.length === 2) return parts[1] === encodedTrackName;
1218
+ if (parts.length >= 3) return parts[2] === encodedTrackName;
1219
+ return false;
1220
+ }
1221
+ #packetIdsForStreamKeys(keys) {
1222
+ return [...new Set([...keys].map((key) => Number(key.split(":", 1)[0])))];
1223
+ }
1224
+ #parseStreamKey(key) {
1225
+ const separator = key.indexOf(":");
1226
+ if (separator < 0) return { packetId: Number(key) || 0 };
1227
+ return {
1228
+ packetId: Number(key.slice(0, separator)) || 0,
1229
+ catalogTrackName: decodeURIComponent(key.slice(separator + 1))
1230
+ };
1231
+ }
1232
+ #blockKey(sbn, packetId, catalogTrackName) {
1233
+ if (sbn === this.#lastBlockKeySbn && packetId === this.#lastBlockKeyPacketId && catalogTrackName === this.#lastBlockKeyTrackName) {
1234
+ return this.#lastBlockKey;
1235
+ }
1236
+ const base = `${packetId}:${sbn}`;
1237
+ const key = catalogTrackName === void 0 ? base : `${base}:${this.#encodeTrackName(catalogTrackName)}`;
1238
+ this.#lastBlockKeySbn = sbn;
1239
+ this.#lastBlockKeyPacketId = packetId;
1240
+ this.#lastBlockKeyTrackName = catalogTrackName;
1241
+ this.#lastBlockKey = key;
1242
+ return key;
1243
+ }
1244
+ /** Record that `key` (block `sbn` on `packetId`) is held by a lookup set. */
1245
+ #indexBlockKey(key, sbn, packetId) {
1246
+ let keys = this.#blockKeysBySbn.get(sbn);
1247
+ if (!keys) {
1248
+ keys = /* @__PURE__ */ new Map();
1249
+ this.#blockKeysBySbn.set(sbn, keys);
1250
+ }
1251
+ keys.set(key, packetId);
1252
+ }
1253
+ /** True while any set a catalog-name-less lookup consults still holds `key`. */
1254
+ #isBlockKeyHeld(key) {
1255
+ return this.#activeBlocks.has(key) || this.#blockSourceCounts.has(key) || this.#blockCatalogTrackNames.has(key) || this.#completedBlocks.has(key);
1256
+ }
1257
+ /** Drop `key` from the SBN index once no lookup set holds it any more. */
1258
+ #unindexBlockKey(key) {
1259
+ if (this.#isBlockKeyHeld(key)) return;
1260
+ const sbn = this.#parseBlockKey(key).sbn;
1261
+ const keys = this.#blockKeysBySbn.get(sbn);
1262
+ if (!keys) return;
1263
+ keys.delete(key);
1264
+ if (keys.size === 0) this.#blockKeysBySbn.delete(sbn);
1265
+ }
1266
+ /**
1267
+ * Make `key` terminal: late symbols for it are duplicates from now on. The
1268
+ * key is retired terminalRetentionMsForPacket later, when no copy of any of
1269
+ * its symbols can still be in flight on either transport and no frame can
1270
+ * still be held behind it. Retirement is swept lazily here, on the stream's
1271
+ * own terminal events, so it needs no timer.
1272
+ */
1273
+ #markTerminal(key, sbn, packetId, clean) {
1274
+ const nowMs = performance.now();
1275
+ this.#retireTerminalBlocks(packetId, nowMs);
1276
+ this.#forgetOrderedPending(key);
1277
+ this.#completedBlocks.add(key);
1278
+ if (clean) this.#cleanlyCompletedBlocks.add(key);
1279
+ this.#indexBlockKey(key, sbn, packetId);
1280
+ const expiresAtMs = nowMs + this.terminalRetentionMsForPacket(packetId);
1281
+ this.#terminalExpiryMs.set(key, expiresAtMs);
1282
+ let queue = this.#terminalQueues.get(packetId);
1283
+ if (!queue) {
1284
+ queue = new TerminalQueue();
1285
+ this.#terminalQueues.set(packetId, queue);
1286
+ }
1287
+ queue.push({ key, expiresAtMs });
1288
+ }
1289
+ #retireTerminalBlocks(packetId, nowMs) {
1290
+ const queue = this.#terminalQueues.get(packetId);
1291
+ if (!queue) return;
1292
+ for (let record = queue.peek(); record !== void 0; record = queue.peek()) {
1293
+ if (record.expiresAtMs > nowMs) return;
1294
+ const current = this.#terminalExpiryMs.get(record.key) === record.expiresAtMs;
1295
+ if (current && this.#withinOrderedHorizon(record.key)) return;
1296
+ queue.shift();
1297
+ if (!current) continue;
1298
+ this.#terminalExpiryMs.delete(record.key);
1299
+ this.#completedBlocks.delete(record.key);
1300
+ this.#cleanlyCompletedBlocks.delete(record.key);
1301
+ this.#workerRecoveredBlocks.delete(record.key);
1302
+ this.#failedBlocks.delete(record.key);
1303
+ this.#resolvedBlockClasses.delete(record.key);
1304
+ this.#repairedBlocks.delete(record.key);
1305
+ this.#unindexBlockKey(record.key);
1306
+ }
1307
+ }
1308
+ // -----------------------------------------------------------------------
1309
+ // Ordered-relay settlement (MulticastSymbolDelivery 'ordered-relay')
1310
+ // -----------------------------------------------------------------------
1311
+ /**
1312
+ * The decoder's interleave horizon for one stream, in blocks: the same
1313
+ * retention the Worker and the WASM decoder are configured with, because it
1314
+ * comes from the same function they do (`deriveFecRetentionBlocks`, over the
1315
+ * block's delivery window and its span). The decoder retires every block
1316
+ * more than this many SBNs behind the newest one it saw (`cleanup_by_sbn`),
1317
+ * so settling earlier than this would decide a block the decoder would still
1318
+ * have recovered.
1319
+ *
1320
+ * This used to call `deriveFecInterleaveDepth`, which returns D -- MEDIA
1321
+ * UNITS per block, not a block count -- and a comment here asserted the two
1322
+ * matched. They did, only while the decoder's retention was also (wrongly)
1323
+ * sized in D. Once BLO-35597 sized retention as a real block count this
1324
+ * settled 2-5 blocks early on the live ISS catalog at reorderToleranceMs=100
1325
+ * (720p 1 vs 6, audio 2 vs 5), making the manager the binding constraint and
1326
+ * the decoder fix inert on ordered relays. Both sides now consume one
1327
+ * derivation; do not re-derive a horizon at either call site.
1328
+ */
1329
+ #orderedHorizonBlocksFor(packetId, config) {
1330
+ if (!_FECManager.#hasOrderedGeometry(config)) {
1331
+ throw new Error(
1332
+ `[FECManager] packetId=${packetId}: ordered-relay settlement derives the decoder's interleave horizon from the catalog interleaveDepthMs (>= ${MIN_INTERLEAVE_DEPTH_MS}ms), the media-unit rate and the block delivery window; got interleaveDepthMs=${config.interleaveDepthMs} fps=${String(config.fps)} timeoutMs=${config.timeoutMs}.`
1333
+ );
1334
+ }
1335
+ return deriveFecRetentionBlocks(config.timeoutMs, config.interleaveDepthMs);
1336
+ }
1337
+ /**
1338
+ * Whether a stream's geometry can define an ordered-relay horizon at all.
1339
+ * One definition, shared by the config-time and packet-feed callers, so the
1340
+ * two can never disagree about which streams settle by stream order.
1341
+ */
1342
+ static #hasOrderedGeometry(config) {
1343
+ return config.interleaveDepthMs >= MIN_INTERLEAVE_DEPTH_MS && // `fps` is no longer an input to the horizon, but it still gates WHICH
1344
+ // streams settle by stream order at all: a track with no usable
1345
+ // media-unit rate fell back to datagram semantics before this and must
1346
+ // keep doing so. Removing this widens ordered-relay settlement to streams
1347
+ // that never had it. Load-bearing — do not drop it as unused.
1348
+ config.fps !== void 0 && Number.isFinite(config.fps) && config.fps > 0 && Number.isFinite(config.timeoutMs) && config.timeoutMs > 0;
1349
+ }
1350
+ /**
1351
+ * The horizon for one stream, or undefined when its geometry cannot define
1352
+ * one.
1353
+ *
1354
+ * `config.timeoutMs` is the block's delivery window. On the live path
1355
+ * multicast-manager computes it with `jitterBufferMs: fec.reorderToleranceMs`
1356
+ * and passes it to configure(), which is the same `deliveryWindowMs`
1357
+ * fec-worker-setup sizes decoder retention from, so the two horizons are
1358
+ * equal there.
1359
+ *
1360
+ * They are NOT equal on the fallback path, and the skew runs in the
1361
+ * direction this file's invariant calls unsafe. `jitterBufferMs` reaches
1362
+ * fec-manager only inside an explicit configure({timeoutMs}); all four of
1363
+ * its own computeFecDeliveryWindowMs calls (one in #computeTimeout, one in
1364
+ * configure(), two in setFramerate) omit it, so a manager-derived
1365
+ * timeoutMs is tolerance-free and yields a
1366
+ * horizon NARROWER than the decoder's retention — the direction that settles
1367
+ * a block the decoder would still have recovered. At 720p/33ms the decoder
1368
+ * retains ceil(133/33)+1 = 6 while a tolerance-free 33ms window gives
1369
+ * ceil(33/33)+1 = 2. Two writes can also raise a validated timeoutMs
1370
+ * afterwards without re-validating (configure()'s widening loop and
1371
+ * setFramerate).
1372
+ *
1373
+ * Not fixed here: the real fix is to thread the tolerance into those four
1374
+ * calls so both sides derive from the same quantity, which is a change to
1375
+ * inputs fec-manager does not currently receive. Tracked in BLO-38250. The
1376
+ * live ISS path does not hit it — multicast-manager always supplies both
1377
+ * `timeoutMs` and `fps`, and setFramerate skips tracks that already carry
1378
+ * `fps` — so this is a latent skew on the fallback, not a live defect. Do
1379
+ * not read the old "can only skew narrower, never wider" note as a safety
1380
+ * argument: narrower IS the failing direction.
1381
+ */
1382
+ #derivableOrderedHorizon(config, packetId) {
1383
+ if (!_FECManager.#hasOrderedGeometry(config)) return void 0;
1384
+ try {
1385
+ const blocks = tryDeriveFecRetentionBlocks(config.timeoutMs, config.interleaveDepthMs);
1386
+ if (blocks === void 0) {
1387
+ this.#noteLeftOrderedSettlement(
1388
+ packetId,
1389
+ `geometry cannot define a retention horizon within the 255-block ceiling (timeoutMs=${config.timeoutMs}ms over interleaveDepthMs=${config.interleaveDepthMs}ms)`
1390
+ );
1391
+ }
1392
+ return blocks;
1393
+ } catch (error) {
1394
+ this.#noteLeftOrderedSettlement(packetId, String(error));
1395
+ return void 0;
1396
+ }
1397
+ }
1398
+ /**
1399
+ * Warn once per stream that it dropped from ordered-relay settlement to the
1400
+ * datagram deadline. Swallowing is right for the packet-feed contract, but a
1401
+ * configured stream changing settlement mid-flight is a behavioural change,
1402
+ * and it is otherwise indistinguishable from a stream that never had
1403
+ * geometry.
1404
+ */
1405
+ #noteLeftOrderedSettlement(packetId, reason) {
1406
+ if (this.#orderedHorizonWarnedFor.has(packetId)) return;
1407
+ this.#orderedHorizonWarnedFor.add(packetId);
1408
+ console.warn(
1409
+ `[FECManager] packetId=${packetId} left ordered-relay settlement for the datagram deadline: ${reason}`
1410
+ );
1411
+ }
1412
+ /**
1413
+ * The stream's interleave horizon, or undefined when its geometry cannot
1414
+ * define one. Two different streams reach that case, with different causes:
1415
+ * a packetId that was never configured, and so resolves to the manager
1416
+ * defaults (explicitly configured tracks are refused an underivable geometry
1417
+ * at config time — #upsertTrackConfig, setMulticastSymbolDelivery); or a
1418
+ * configured track whose timeoutMs was later widened past the 255-block
1419
+ * ceiling without re-validating, which #derivableOrderedHorizon catches
1420
+ * rather than throwing from the packet-feed path. Either way the stream
1421
+ * keeps datagram semantics instead of throwing. The manager defaults can
1422
+ * also change under a stream that already holds blocks, so a caller cannot
1423
+ * assume a horizon that was defined when a block was held is still defined
1424
+ * (see #advanceOrderedFrontier).
1425
+ */
1426
+ #orderedHorizonBlocks(packetId) {
1427
+ return this.#derivableOrderedHorizon(this.#resolveTrackConfig(packetId), packetId);
1428
+ }
1429
+ /** True while a block of an ordered-relay stream can still receive symbols. */
1430
+ #withinOrderedHorizon(key) {
1431
+ if (this.#multicastSymbolDelivery !== "ordered-relay") return false;
1432
+ const block = this.#parseBlockKey(key);
1433
+ const frontier = this.#orderedFrontiers.get(
1434
+ this.#streamKey(block.packetId, block.catalogTrackName)
1435
+ );
1436
+ if (frontier === void 0) return false;
1437
+ const horizon = this.#orderedHorizonBlocks(block.packetId);
1438
+ if (horizon === void 0) return false;
1439
+ return this.#blocksBehind(block.sbn, frontier, block.packetId) <= horizon;
1440
+ }
1441
+ /** Whether a block's fate is decided by stream order rather than a timer. */
1442
+ #settlesByOrder(key) {
1443
+ return this.#multicastSymbolDelivery === "ordered-relay" && (this.#blockPathCounts.get(key)?.multicastSymbols ?? 0) > 0 && this.#orderedHorizonBlocks(this.#parseBlockKey(key).packetId) !== void 0;
1444
+ }
1445
+ #forgetOrderedPending(key) {
1446
+ if (this.#orderedPendingBlocks.size === 0) return;
1447
+ const block = this.#parseBlockKey(key);
1448
+ const streamKey = this.#streamKey(block.packetId, block.catalogTrackName);
1449
+ const pending = this.#orderedPendingBlocks.get(streamKey);
1450
+ if (pending?.get(block.sbn) !== key) return;
1451
+ pending.delete(block.sbn);
1452
+ if (pending.size === 0) this.#orderedPendingBlocks.delete(streamKey);
1453
+ }
1454
+ /**
1455
+ * Record the stream position of a multicast symbol and settle every pending
1456
+ * block it proves final: a block more than the interleave horizon behind the
1457
+ * frontier has already been handed every symbol the relay will deliver.
1458
+ */
1459
+ #advanceOrderedFrontier(streamKey, sbn, packetId, catalogTrackName) {
1460
+ const previous = this.#orderedFrontiers.get(streamKey);
1461
+ const frontier = previous === void 0 || this.#blockPrecedes(previous, sbn, packetId) ? sbn : previous;
1462
+ if (frontier !== previous) this.#orderedFrontiers.set(streamKey, frontier);
1463
+ const pending = this.#orderedPendingBlocks.get(streamKey);
1464
+ if (!pending) return;
1465
+ const blocks = this.#orderedHorizonBlocks(packetId);
1466
+ if (blocks === void 0) {
1467
+ this.#orderedPendingBlocks.delete(streamKey);
1468
+ for (const pendingSbn of pending.keys()) {
1469
+ this.noteBlockActivity(pendingSbn, packetId, catalogTrackName);
1470
+ }
1471
+ return;
1472
+ }
1473
+ for (const [pendingSbn, key] of pending) {
1474
+ if (this.#blocksBehind(pendingSbn, frontier, packetId) <= blocks) continue;
1475
+ pending.delete(pendingSbn);
1476
+ this.#settleOrderedBlock(key, pendingSbn, packetId, catalogTrackName);
1477
+ }
1478
+ if (pending.size === 0 && this.#orderedPendingBlocks.get(streamKey) === pending) {
1479
+ this.#orderedPendingBlocks.delete(streamKey);
1480
+ }
1481
+ }
1482
+ /**
1483
+ * Decide a block the ordered relay has finished delivering. Its accounting
1484
+ * is final: SSB_length (from any repair symbol) against the source and
1485
+ * repair symbols it holds says whether the decoder can still recover it.
1486
+ * A block short of that asks for repair when D-28 is wired, under the same
1487
+ * post-request fetch budget the datagram path grants; otherwise, and for a
1488
+ * block the decoder holds enough symbols for, the decoder finalizes it
1489
+ * exactly as a datagram deadline would (a Worker acknowledges the cleanup
1490
+ * only after every earlier symbol, so an in-flight recovery still wins).
1491
+ */
1492
+ #settleOrderedBlock(key, sbn, packetId, catalogTrackName) {
1493
+ if (this.#completedBlocks.has(key) || this.#pendingWorkerCleanups.has(key) || this.#repairRequestedBlocks.has(key)) return;
1494
+ const authoredLength = this.#blockAuthoredLengths.get(key);
1495
+ const received = (this.#blockSourceCounts.get(key) ?? 0) + (this.#blockRepairRsIds.get(key)?.size ?? 0);
1496
+ if (this.onRepairNeeded && (authoredLength === void 0 || received < authoredLength)) {
1497
+ this.#requestRepair(sbn, packetId, catalogTrackName, true);
1498
+ return;
1499
+ }
1500
+ this.#traceLifecycle("horizon-settled", sbn, packetId, catalogTrackName);
1501
+ this.#finalizeExpiredBlock(sbn, packetId, catalogTrackName);
1502
+ }
1503
+ /** Register a block whose multicast symbols arrive over an ordered relay. */
1504
+ #holdForOrderedSettlement(key, sbn, packetId, catalogTrackName) {
1505
+ this.#clearRepairDeadline(key);
1506
+ if (this.#activeBlocks.has(key)) {
1507
+ this.#clearBlockTimer(key);
1508
+ this.#unindexBlockKey(key);
1509
+ }
1510
+ const streamKey = this.#streamKey(packetId, catalogTrackName);
1511
+ let pending = this.#orderedPendingBlocks.get(streamKey);
1512
+ if (!pending) {
1513
+ pending = /* @__PURE__ */ new Map();
1514
+ this.#orderedPendingBlocks.set(streamKey, pending);
1515
+ }
1516
+ pending.set(sbn, key);
1517
+ }
1518
+ /** Cancel and forget a block's inactivity timer and deadline. */
1519
+ #clearBlockTimer(key) {
1520
+ const timer = this.#activeBlocks.get(key);
1521
+ if (timer?.handle != null) clearTimeout(timer.handle);
1522
+ this.#activeBlocks.delete(key);
1523
+ this.#activeBlockDeadlinesMs.delete(key);
1524
+ this.#blockWaitDeadlinesMs.delete(key);
1525
+ this.#repairHoldUntilMs.delete(key);
1526
+ }
1527
+ #parseBlockKey(key) {
1528
+ const [packetIdText, sbnText, ...catalogParts] = key.split(":");
1529
+ const catalogTrackName = catalogParts.length > 0 ? decodeURIComponent(catalogParts.join(":")) : void 0;
1530
+ return {
1531
+ packetId: Number(packetIdText) || 0,
1532
+ sbn: Number(sbnText) || 0,
1533
+ ...catalogTrackName === void 0 ? {} : { catalogTrackName }
1534
+ };
1535
+ }
1536
+ #decoderForPacket(packetId, catalogTrackName) {
1537
+ return this.#fecDecoders.get(this.#streamKey(packetId, catalogTrackName)) ?? this.#fecDecoders.get(this.#streamKey(packetId));
1538
+ }
1539
+ #workerForPacket(packetId, catalogTrackName) {
1540
+ return this.#workerClients.get(this.#streamKey(packetId, catalogTrackName)) ?? this.#workerClients.get(this.#streamKey(packetId));
1541
+ }
1542
+ /**
1543
+ * The stream's Worker client for routing, or undefined after evicting a
1544
+ * registered client that was already disposed. A disposed client throws on
1545
+ * every command, so it leaves routing here before a symbol reaches it and
1546
+ * the stream reports its decoder unavailable instead.
1547
+ */
1548
+ #routableWorkerForPacket(packetId, catalogTrackName) {
1549
+ const client = this.#workerForPacket(packetId, catalogTrackName);
1550
+ if (client === void 0 || client.disposed !== true) return client;
1551
+ this.#evictDisposedWorker(client);
1552
+ return void 0;
1553
+ }
1554
+ #claimDecoderBlock(client, sbn, catalogTrackName) {
1555
+ let blocks = this.#decoderBlockOwners.get(client);
1556
+ if (!blocks) {
1557
+ blocks = /* @__PURE__ */ new Map();
1558
+ this.#decoderBlockOwners.set(client, blocks);
1559
+ }
1560
+ const owner = blocks.get(sbn);
1561
+ if (!owner) {
1562
+ blocks.set(sbn, { catalogTrackName, conflicted: false });
1563
+ if (blocks.size > 512) {
1564
+ const stale = [...blocks.keys()].slice(0, 256);
1565
+ for (const key of stale) blocks.delete(key);
1566
+ }
1567
+ return true;
1568
+ }
1569
+ if (owner.conflicted) return false;
1570
+ if (owner.catalogTrackName === catalogTrackName) return true;
1571
+ owner.conflicted = true;
1572
+ return false;
1573
+ }
1574
+ #decoderBlockCatalogTrackName(client, sbn) {
1575
+ const owner = this.#decoderBlockOwners.get(client)?.get(sbn);
1576
+ return owner?.conflicted ? void 0 : owner?.catalogTrackName;
1577
+ }
1578
+ #resetDecoderBlockOwners(client, catalogTrackName) {
1579
+ const blocks = this.#decoderBlockOwners.get(client);
1580
+ if (!blocks) return;
1581
+ for (const [sbn, owner] of blocks) {
1582
+ if (owner.catalogTrackName === catalogTrackName || owner.conflicted) {
1583
+ blocks.delete(sbn);
1584
+ }
1585
+ }
1586
+ if (blocks.size === 0) this.#decoderBlockOwners.delete(client);
1587
+ }
1588
+ #discardFecDecoderClient(client) {
1589
+ for (const [key, entry] of this.#fecDecoders) {
1590
+ if (entry.client === client) this.#fecDecoders.delete(key);
1591
+ }
1592
+ this.#decoderBlockOwners.delete(client);
1593
+ }
1594
+ #assertExclusiveDecoderClient(streamKey, client, registrations, kind) {
1595
+ for (const [registeredKey, registeredClient] of registrations) {
1596
+ if (registeredClient !== client || registeredKey === streamKey) continue;
1597
+ throw new Error(
1598
+ `[FECManager] ${kind} client is already registered for stream ${registeredKey}; create one client per physical stream`
1599
+ );
1600
+ }
1601
+ }
1602
+ #statsForPacket(packetId) {
1603
+ let stats = this.#fecTrackStats.get(packetId);
1604
+ if (!stats) {
1605
+ stats = newFecCounters();
1606
+ this.#fecTrackStats.set(packetId, stats);
1607
+ }
1608
+ return stats;
1609
+ }
1610
+ #markPathBlockComplete(sbn, packetId, catalogTrackName) {
1611
+ const block = this.#blockPathCounts.get(this.#blockKey(sbn, packetId, catalogTrackName));
1612
+ if (block) block.complete = true;
1613
+ }
1614
+ /** Cancel and forget a block's D-28 repair-deadline timer. */
1615
+ #clearRepairDeadline(key) {
1616
+ const timer = this.#repairDeadlines.get(key);
1617
+ if (timer?.handle != null) clearTimeout(timer.handle);
1618
+ this.#repairDeadlines.delete(key);
1619
+ this.#repairDeadlineAtMs.delete(key);
1620
+ }
1621
+ /**
1622
+ * Move a block's D-28 repair deadline to `delayMs` from now. The timer
1623
+ * already armed for an earlier moment fires first and re-arms for the
1624
+ * remainder, so repeated source progress costs no timer operation.
1625
+ */
1626
+ #armRepairDeadline(key, sbn, packetId, catalogTrackName, delayMs) {
1627
+ const deadlineMs = performance.now() + delayMs;
1628
+ this.#repairDeadlineAtMs.set(key, deadlineMs);
1629
+ const armed = this.#repairDeadlines.get(key);
1630
+ if (armed?.handle != null) {
1631
+ if (armed.fireAtMs <= deadlineMs) return;
1632
+ clearTimeout(armed.handle);
1633
+ }
1634
+ const timer = { handle: null, fireAtMs: deadlineMs };
1635
+ const onFire = () => {
1636
+ if (this.#repairDeadlines.get(key) !== timer) return;
1637
+ const currentDeadlineMs = this.#repairDeadlineAtMs.get(key);
1638
+ const nowMs = performance.now();
1639
+ if (currentDeadlineMs !== void 0 && currentDeadlineMs > nowMs) {
1640
+ timer.fireAtMs = currentDeadlineMs;
1641
+ timer.handle = setTimeout(onFire, Math.ceil(currentDeadlineMs - nowMs));
1642
+ return;
1643
+ }
1644
+ timer.handle = null;
1645
+ this.#requestRepair(sbn, packetId, catalogTrackName);
1646
+ };
1647
+ timer.handle = setTimeout(onFire, delayMs);
1648
+ this.#repairDeadlines.set(key, timer);
1649
+ }
1650
+ #requestRepair(sbn, packetId, catalogTrackName, settledByOrder = false) {
1651
+ const key = this.#blockKey(sbn, packetId, catalogTrackName);
1652
+ if (this.#completedBlocks.has(key) || !this.onRepairNeeded || this.#repairRequestedBlocks.has(key)) return;
1653
+ const deadline = this.#repairDeadlines.get(key);
1654
+ if (deadline?.handle != null) clearTimeout(deadline.handle);
1655
+ this.#repairDeadlines.set(key, null);
1656
+ this.#repairDeadlineAtMs.delete(key);
1657
+ this.#repairRequestedBlocks.add(key);
1658
+ if (settledByOrder) this.#armBlockTimeout(sbn, packetId, catalogTrackName, 0);
1659
+ this.#traceLifecycle("repair-requested", sbn, packetId, catalogTrackName);
1660
+ this.onRepairNeeded(
1661
+ sbn,
1662
+ packetId,
1663
+ catalogTrackName,
1664
+ this.blockClassFor(sbn, packetId, catalogTrackName)
1665
+ );
1666
+ }
1667
+ // ── Keyframe-protected repair policy ──
1668
+ /**
1669
+ * The classifier geometry of one stream. A stream-scoped configuration
1670
+ * wins. Once any stream of a packetId was configured with its own geometry,
1671
+ * a sibling stream without one has none that provably belongs to it: the
1672
+ * packet-level geometry may be another rung's (a different framerate or
1673
+ * GOP), so it runs all-protected (`stream-geometry-absent`).
1674
+ */
1675
+ #geometryFor(packetId, catalogTrackName) {
1676
+ const own = this.#streamGeometries.get(this.#streamKey(packetId, catalogTrackName));
1677
+ if (own) return own;
1678
+ const config = this.#trackConfigs.get(packetId);
1679
+ if (config?.trackKind === "audio") return { trackKind: "audio" };
1680
+ for (const key of this.#streamGeometries.keys()) {
1681
+ if (this.#belongsToPacket(key, packetId)) {
1682
+ return { trackKind: "video", mode: "all-protected", reason: "stream-geometry-absent" };
1683
+ }
1684
+ }
1685
+ if (config?.geometry) return config.geometry;
1686
+ return { trackKind: "video", mode: "all-protected", reason: "keyframe-cadence-absent" };
1687
+ }
1688
+ #classifierFor(packetId, catalogTrackName) {
1689
+ const streamKey = this.#streamKey(packetId, catalogTrackName);
1690
+ let classifier = this.#classifiers.get(streamKey);
1691
+ if (!classifier) {
1692
+ classifier = new BlockClassifier(this.#geometryFor(packetId, catalogTrackName));
1693
+ this.#classifiers.set(streamKey, classifier);
1694
+ if (classifier.geometry.trackKind === "video") {
1695
+ this.onClassifierMode?.(packetId, catalogTrackName, classifier.mode, classifier.reason);
1696
+ }
1697
+ }
1698
+ return classifier;
1699
+ }
1700
+ #applyClassifierChanges(packetId, catalogTrackName, changes) {
1701
+ for (const change of changes) {
1702
+ if (change.kind === "mode") {
1703
+ this.onClassifierMode?.(packetId, catalogTrackName, change.mode, change.reason);
1704
+ } else {
1705
+ this.onBlockReclassified?.(
1706
+ change.sbn,
1707
+ packetId,
1708
+ catalogTrackName,
1709
+ change.from,
1710
+ change.to,
1711
+ change.evidence
1712
+ );
1713
+ }
1714
+ }
1715
+ }
1716
+ /**
1717
+ * Freeze a block's class at resolution and drop its classifier state. The
1718
+ * frozen class answers every later query (a recovery counted after the
1719
+ * block was cancelled, a failure event) exactly.
1720
+ */
1721
+ #retireBlockClass(key, sbn, packetId, catalogTrackName) {
1722
+ const frozen = this.#resolvedBlockClasses.get(key);
1723
+ if (frozen !== void 0) return frozen;
1724
+ const blockClass = this.#peekClass(sbn, packetId, catalogTrackName);
1725
+ this.#classifiers.get(this.#streamKey(packetId, catalogTrackName))?.forget(sbn);
1726
+ this.#resolvedBlockClasses.set(key, blockClass);
1727
+ this.#materializedBlocks.delete(key);
1728
+ return blockClass;
1729
+ }
1730
+ /** A block's live class without creating classifier state for the stream. */
1731
+ #peekClass(sbn, packetId, catalogTrackName) {
1732
+ const classifier = this.#classifiers.get(this.#streamKey(packetId, catalogTrackName));
1733
+ if (classifier) return classifier.classOf(sbn);
1734
+ return this.#geometryFor(packetId, catalogTrackName).trackKind === "audio" ? "audio" : "unknown";
1735
+ }
1736
+ #viaFor(key) {
1737
+ return (this.#blockPathCounts.get(key)?.moqSymbols ?? 0) > 0 ? "quic" : "inband";
1738
+ }
1739
+ #noteBlockRepaired(key, sbn, packetId, catalogTrackName) {
1740
+ if (this.#repairedBlocks.has(key)) return;
1741
+ this.#repairedBlocks.add(key);
1742
+ this.onBlockRepaired?.(
1743
+ sbn,
1744
+ packetId,
1745
+ catalogTrackName,
1746
+ this.blockClassFor(sbn, packetId, catalogTrackName),
1747
+ this.#viaFor(key)
1748
+ );
1749
+ }
1750
+ // Deliberately one block tighter than deriveFecRetentionBlocks over the same
1751
+ // inputs (that function's `+ 1`). Different question: the retention horizon
1752
+ // asks how long the decoder still ACCEPTS symbols for a block, this asks how
1753
+ // far back a block that vanished entirely may still be repairable (see
1754
+ // #noteSbnSeen).
1755
+ // The extra block of retention is slack for a block still being fed; a block
1756
+ // nothing has been heard from does not get it. Not the units defect — but it
1757
+ // is the next place the two can drift, so change both or neither.
1758
+ #liveWindowBlocks(config) {
1759
+ if (config.interleaveDepthMs < MIN_INTERLEAVE_DEPTH_MS || config.timeoutMs <= 0) return null;
1760
+ return Math.ceil(config.timeoutMs / config.interleaveDepthMs);
1761
+ }
1762
+ /**
1763
+ * Record that a symbol for block `sbn` arrived and materialise any blocks
1764
+ * that vanished entirely between the stream's highest SBN and this one.
1765
+ *
1766
+ * The newest ceil(timeoutMs / interleaveDepthMs) vanished blocks may still
1767
+ * be repairable, so they get the normal repair and fail deadlines, armed
1768
+ * from now. Older ones are already past their fail bound and fail now, as
1769
+ * class `unknown`. A SOURCE symbol whose SBN goes back further than one
1770
+ * live window, or jumps further than the stream could have produced since
1771
+ * its last symbol, is a source discontinuity, not loss. A repair symbol can
1772
+ * only advance the frontier: a D-28 fetch legitimately answers for an old
1773
+ * block, so it never signals a discontinuity.
1774
+ *
1775
+ * The producer closes a block at its Dth media-unit boundary OR when K
1776
+ * source symbols filled it, so one interleave span can start several
1777
+ * blocks (every large keyframe does). What bounds a forward jump is
1778
+ * therefore packet evidence, not a block rate: with the MMTP
1779
+ * packet_sequence_number (contiguous per packet_id, ISO/IEC 23008-1) of
1780
+ * both source symbols, every block strictly between them holds at least one
1781
+ * of the packets sent strictly between them, and K-closes can add at most
1782
+ * one block per K of those packets on top of the D-closes elapsed time
1783
+ * allows. Without it, only the D-close rate is known.
1784
+ */
1785
+ #noteSbnSeen(sbn, packetId, catalogTrackName, symbol, packetSequenceNumber) {
1786
+ const streamKey = this.#streamKey(packetId, catalogTrackName);
1787
+ const now = performance.now();
1788
+ const previous = this.#highestSbn.get(streamKey);
1789
+ const anchor = this.#sourceAnchors.get(streamKey);
1790
+ const psn = symbol === "source" && packetSequenceNumber !== void 0 && Number.isSafeInteger(packetSequenceNumber) && packetSequenceNumber >= 0 && packetSequenceNumber <= 4294967295 ? packetSequenceNumber : void 0;
1791
+ const anchorHere = () => {
1792
+ if (psn !== void 0 && (anchor === void 0 || !this.#blockPrecedes(sbn, anchor.sbn, packetId))) {
1793
+ this.#sourceAnchors.set(streamKey, { sbn, psn, atMs: now });
1794
+ }
1795
+ };
1796
+ const discontinuity = () => {
1797
+ this.#classifiers.get(streamKey)?.noteSourceDiscontinuity();
1798
+ this.#highestSbn.set(streamKey, { sbn, atMs: now });
1799
+ if (psn === void 0) this.#sourceAnchors.delete(streamKey);
1800
+ else this.#sourceAnchors.set(streamKey, { sbn, psn, atMs: now });
1801
+ this.onSourceDiscontinuity?.(packetId, catalogTrackName);
1802
+ };
1803
+ if (previous === void 0) {
1804
+ this.#highestSbn.set(streamKey, { sbn, atMs: now });
1805
+ anchorHere();
1806
+ return;
1807
+ }
1808
+ const config = this.#resolveTrackConfig(packetId);
1809
+ const window = this.#liveWindowBlocks(config);
1810
+ if (!this.#blockPrecedes(previous.sbn, sbn, packetId)) {
1811
+ if (symbol === "source" && window !== null && this.#blocksBehind(sbn, previous.sbn, packetId) > window) {
1812
+ discontinuity();
1813
+ return;
1814
+ }
1815
+ anchorHere();
1816
+ return;
1817
+ }
1818
+ const missing = this.#blocksBehind(previous.sbn, sbn, packetId) - 1;
1819
+ let plausible = true;
1820
+ if (window !== null) {
1821
+ if (this.#isIso(packetId) && psn !== void 0 && anchor !== void 0 && this.#blockPrecedes(anchor.sbn, sbn, packetId) && psn !== anchor.psn) {
1822
+ const sent = psn - anchor.psn >>> 0;
1823
+ plausible = sent <= 2147483647 && serial32Distance(anchor.sbn, sbn) <= sent + config.k;
1824
+ } else if (psn !== void 0 && anchor !== void 0 && this.#blockPrecedes(anchor.sbn, sbn, packetId) && psn !== anchor.psn) {
1825
+ const sent = psn - anchor.psn >>> 0;
1826
+ const between = this.#blocksBehind(anchor.sbn, sbn, packetId) - 1;
1827
+ const dCloses = window + Math.ceil((now - anchor.atMs) / config.interleaveDepthMs);
1828
+ const kCloses = config.k > 0 ? Math.ceil(sent / config.k) : sent;
1829
+ plausible = sent <= 2147483647 && between <= sent - 1 && between <= dCloses + kCloses;
1830
+ } else {
1831
+ plausible = missing <= window + Math.ceil((now - previous.atMs) / config.interleaveDepthMs);
1832
+ }
1833
+ }
1834
+ if (!plausible) {
1835
+ if (symbol === "repair") return;
1836
+ discontinuity();
1837
+ return;
1838
+ }
1839
+ this.#highestSbn.set(streamKey, { sbn, atMs: now });
1840
+ anchorHere();
1841
+ if (this.#isIso(packetId)) return;
1842
+ if (missing <= 0 || window === null) return;
1843
+ let vanished = previous.sbn;
1844
+ for (let i = 0; i < missing; i++) {
1845
+ vanished = this.#nextBlock(vanished, packetId);
1846
+ this.#materializeBlock(
1847
+ vanished,
1848
+ packetId,
1849
+ catalogTrackName,
1850
+ this.#blocksBehind(vanished, sbn, packetId) <= window
1851
+ );
1852
+ }
1853
+ }
1854
+ /*
1855
+ * Block order for one stream (BLO-35597 iso:5). SS_ID is a 32-bit serial
1856
+ * number (ISO/IEC 23008-1 C.5.2), so blocks are ordered by RFC 1982
1857
+ * comparison of their first SS_ID (sbn * K mod 2^32). That is identical to
1858
+ * comparing SBNs with < for every stream that has not wrapped SS_ID; only
1859
+ * the order across the wrap changes. A block outside K-stride geometry has
1860
+ * no SS_ID position, so it keeps the SBN order it always had.
1861
+ */
1862
+ #blockPrecedes(a, b, packetId) {
1863
+ if (this.#isIso(packetId)) return serial32Precedes(a, b);
1864
+ const k = this.kForPacket(packetId);
1865
+ return isKStrideBlock(a, k) && isKStrideBlock(b, k) ? kStrideBlockPrecedes(a, b, k) : a < b;
1866
+ }
1867
+ /** Blocks `a` lies behind `b` (0 when it does not precede it). */
1868
+ #blocksBehind(a, b, packetId) {
1869
+ if (this.#isIso(packetId)) return isoBlocksBehindLowerBound(a, b, this.kForPacket(packetId));
1870
+ const k = this.kForPacket(packetId);
1871
+ return isKStrideBlock(a, k) && isKStrideBlock(b, k) ? kStrideBlocksBehind(a, b, k) : Math.max(0, b - a);
1872
+ }
1873
+ /** The block after `sbn`, stepping to block 0 where SS_ID wraps. */
1874
+ #nextBlock(sbn, packetId) {
1875
+ const k = this.kForPacket(packetId);
1876
+ return isKStrideBlock(sbn, k) ? kStrideNextBlock(sbn, k) : sbn + 1;
1877
+ }
1878
+ #materializeBlock(sbn, packetId, catalogTrackName, repairable) {
1879
+ const key = this.#blockKey(sbn, packetId, catalogTrackName);
1880
+ if (this.#blockResolutionFor(key) !== "untracked") return;
1881
+ this.#materializedBlocks.add(key);
1882
+ this.#classifierFor(packetId, catalogTrackName).touch(sbn);
1883
+ this.onBlockVanished?.(sbn, packetId, catalogTrackName);
1884
+ if (repairable) {
1885
+ this.noteBlockActivity(sbn, packetId, catalogTrackName, true);
1886
+ } else {
1887
+ this.#failBlock(sbn, packetId, catalogTrackName);
1888
+ }
1889
+ }
1890
+ #blockResolutionFor(key) {
1891
+ if (this.#failedBlocks.has(key)) return "failed";
1892
+ if (this.#completedBlocks.has(key)) return "complete";
1893
+ if (this.#activeBlocks.has(key) || this.#pendingWorkerCleanups.has(key) || this.#repairDeadlines.has(key) || this.#blockSourceCounts.has(key) || this.#blockRepairRsIds.has(key) || this.#materializedBlocks.has(key)) {
1894
+ return "pending";
1895
+ }
1896
+ return "untracked";
1897
+ }
1898
+ #bumpEpoch(packetId) {
1899
+ this.#packetEpochs.set(packetId, (this.#packetEpochs.get(packetId) ?? 0) + 1);
1900
+ }
1901
+ #traceLifecycle(event, sbn, packetId, catalogTrackName, rsId) {
1902
+ const key = this.#blockKey(sbn, packetId, catalogTrackName);
1903
+ const deadline = this.#activeBlockDeadlinesMs.get(key);
1904
+ const decoderPath = this.#syncDecoderBlockOwners.has(key) ? "sync" : this.#workerForPacket(packetId, catalogTrackName) ? "worker" : this.#decoderForPacket(packetId, catalogTrackName) ? "sync" : "none";
1905
+ this.#lifecycleTrace.push({
1906
+ atMs: Date.now(),
1907
+ event,
1908
+ packetId,
1909
+ sbn,
1910
+ ...catalogTrackName === void 0 ? {} : { catalogTrackName },
1911
+ sourceCount: this.#blockSourceCounts.get(key) ?? 0,
1912
+ sourceEsis: [...this.#blockSourceEsis.get(key) ?? []].sort((a, b) => a - b),
1913
+ repairRsIds: [...this.#blockRepairRsIds.get(key) ?? []].sort((a, b) => a - b),
1914
+ decoderPath,
1915
+ ...deadline === void 0 ? {} : { deadlineRemainingMs: Math.max(0, deadline - performance.now()) },
1916
+ pendingCleanup: this.#pendingWorkerCleanups.has(key),
1917
+ ...rsId === void 0 ? {} : { rsId }
1918
+ });
1919
+ if (this.#lifecycleTrace.length > 64) this.#lifecycleTrace.splice(0, 32);
1920
+ }
1921
+ #defaultTrackConfig() {
1922
+ return {
1923
+ k: this.#fecK,
1924
+ symbolSize: this.#fecSymbolSize,
1925
+ interleaveDepthMs: this.#fecInterleaveDepthMs,
1926
+ fps: this.#fecFps,
1927
+ timeoutMs: this.#fecTimeoutMs
1928
+ };
1929
+ }
1930
+ #resolveTrackConfig(packetId) {
1931
+ return this.#trackConfigs.get(packetId) ?? this.#defaultTrackConfig();
1932
+ }
1933
+ #computeTimeout(config) {
1934
+ if (config.timeoutMs && config.timeoutMs > 0) return config.timeoutMs;
1935
+ if (config.interleaveDepthMs >= MIN_INTERLEAVE_DEPTH_MS) {
1936
+ return Math.max(
1937
+ this.#fecTimeoutMs,
1938
+ computeFecDeliveryWindowMs({
1939
+ interleaveDepthMs: config.interleaveDepthMs,
1940
+ sourceSymbols: config.k,
1941
+ mediaUnitsPerSecond: config.fps
1942
+ })
1943
+ );
1944
+ }
1945
+ return this.#fecTimeoutMs;
1946
+ }
1947
+ /**
1948
+ * The delivery-gap window is the longest catalog keyframe interval among
1949
+ * the configured video streams. A failed video block costs every frame up
1950
+ * to the next keyframe, so a receiver must remember a stall for at least
1951
+ * that long: forgetting it sooner would let the next recurrence fail a
1952
+ * block whose damage outlives the memory of the stall that caused it. It
1953
+ * comes from the catalog's `keyframeIntervalTicks`/`keyframeIntervalMs`
1954
+ * (via the classifier geometry), never from a constant. It also caps the
1955
+ * gap the wait may absorb; with no catalog cadence no gap is reported and
1956
+ * the catalog tolerance alone applies (see DeliveryGapEstimator).
1957
+ */
1958
+ #refreshDeliveryGapWindow() {
1959
+ let windowMs;
1960
+ const consider = (geometry) => {
1961
+ if (geometry?.trackKind !== "video" || geometry.mode !== "classifying") return;
1962
+ const { ticks, timescale } = geometry.keyframeInterval;
1963
+ const ms = ticks / timescale * 1e3;
1964
+ if (Number.isFinite(ms) && ms > 0 && (windowMs === void 0 || ms > windowMs)) windowMs = ms;
1965
+ };
1966
+ for (const geometry of this.#streamGeometries.values()) consider(geometry);
1967
+ for (const config of this.#trackConfigs.values()) consider(config.geometry);
1968
+ this.#deliveryGaps.setWindowMs(windowMs);
1969
+ }
1970
+ /**
1971
+ * The multicast late-symbol wait: the block span plus the larger of the
1972
+ * catalog's signaled tolerance and the receiver-measured delivery gap.
1973
+ *
1974
+ * `config.timeoutMs` already is span + `fec.reorderToleranceMs` (or the
1975
+ * wider envelope configure() applies), so it stays the floor; the measured
1976
+ * gap widens it only when the path stalls for longer than the catalog
1977
+ * allowed for. Datagram delivery only: an ordered relay settles blocks by
1978
+ * stream order and never feeds the estimator.
1979
+ */
1980
+ #multicastWaitMs(config) {
1981
+ return this.#waitForGapMs(config, this.#deliveryGaps.peakGapMs(performance.now()));
1982
+ }
1983
+ /**
1984
+ * The widest #multicastWaitMs can become: the estimator reports at most its
1985
+ * catalog window, so this is a catalog quantity that no measurement moves.
1986
+ */
1987
+ #maxMulticastWaitMs(config) {
1988
+ return this.#waitForGapMs(config, this.#deliveryGaps.windowMs ?? 0);
1989
+ }
1990
+ #waitForGapMs(config, gapMs) {
1991
+ if (!(gapMs > 0) || !(config.interleaveDepthMs > 0)) return config.timeoutMs;
1992
+ return Math.max(config.timeoutMs, Math.ceil(config.interleaveDepthMs + gapMs));
1993
+ }
1994
+ #repairDeadlineMs(config, waitMs = this.#multicastWaitMs(config)) {
1995
+ const halfFailBoundMs = Math.ceil(waitMs / 2);
1996
+ if (config.k > 0 && config.interleaveDepthMs >= MIN_INTERLEAVE_DEPTH_MS && config.fps !== void 0 && Number.isFinite(config.fps) && config.fps > 0) {
1997
+ const mediaUnitMs = 1e3 / config.fps;
1998
+ const interleaveUnits = deriveFecInterleaveDepth2(
1999
+ config.interleaveDepthMs,
2000
+ mediaUnitMs
2001
+ );
2002
+ const sourceHorizonMs = Math.ceil(config.k * interleaveUnits * mediaUnitMs);
2003
+ if (sourceHorizonMs < waitMs) return sourceHorizonMs;
2004
+ }
2005
+ return halfFailBoundMs;
2006
+ }
2007
+ /**
2008
+ * One block lifetime: the inactivity fail bound plus the D-28 repair grace,
2009
+ * at the widest wait the catalog allows (#maxMulticastWaitMs). Everything
2010
+ * derived from it (terminal retention, the release gate's hold ceiling, the
2011
+ * duplicate ledgers, the owed-cleanup bound) is compared across time, so it
2012
+ * must not follow the live measurement.
2013
+ */
2014
+ #blockLifetimeMs(config) {
2015
+ const waitMs = this.#maxMulticastWaitMs(config);
2016
+ return waitMs + this.#repairGraceMs(config, waitMs);
2017
+ }
2018
+ /**
2019
+ * The lifetime at the live wait, capped at #blockLifetimeMs: lifetime is not
2020
+ * monotone in the wait (the repair deadline switches from half the wait to
2021
+ * the source horizon), so the cap is what keeps it inside the retention.
2022
+ */
2023
+ #liveBlockLifetimeMs(config) {
2024
+ const waitMs = this.#multicastWaitMs(config);
2025
+ return Math.min(waitMs + this.#repairGraceMs(config, waitMs), this.#blockLifetimeMs(config));
2026
+ }
2027
+ #repairGraceMs(config, waitMs) {
2028
+ return Math.max(1, waitMs - this.#repairDeadlineMs(config, waitMs));
2029
+ }
2030
+ #armBlockTimeout(sbn, packetId, catalogTrackName, delayMs) {
2031
+ const key = this.#blockKey(sbn, packetId, catalogTrackName);
2032
+ const waitAtMs = performance.now() + Math.max(1, Math.ceil(delayMs));
2033
+ const holdAtMs = this.#repairHoldUntilMs.get(key);
2034
+ this.#blockWaitDeadlinesMs.set(key, holdAtMs === void 0 ? waitAtMs : Math.min(waitAtMs, holdAtMs));
2035
+ this.#applyBlockDeadline(key, sbn, packetId, catalogTrackName);
2036
+ }
2037
+ /**
2038
+ * Arm (or move) a block's one fail timer at the later of its multicast
2039
+ * wait and its repair hold. Moving it later costs no timer operation: the
2040
+ * armed timer wakes no later than the new deadline and re-arms for the
2041
+ * remainder.
2042
+ */
2043
+ #applyBlockDeadline(key, sbn, packetId, catalogTrackName) {
2044
+ const nowMs = performance.now();
2045
+ const deadlineMs = Math.max(
2046
+ this.#blockWaitDeadlinesMs.get(key) ?? nowMs + 1,
2047
+ this.#repairHoldUntilMs.get(key) ?? -Infinity
2048
+ );
2049
+ this.#activeBlockDeadlinesMs.set(key, deadlineMs);
2050
+ const armed = this.#activeBlocks.get(key);
2051
+ if (armed?.handle != null) {
2052
+ if (armed.fireAtMs <= deadlineMs) return;
2053
+ clearTimeout(armed.handle);
2054
+ }
2055
+ const timer = { handle: null, fireAtMs: deadlineMs };
2056
+ const onFire = () => {
2057
+ if (this.#activeBlocks.get(key) !== timer) return;
2058
+ const currentDeadlineMs = this.#activeBlockDeadlinesMs.get(key);
2059
+ const firedAtMs = performance.now();
2060
+ if (currentDeadlineMs !== void 0 && currentDeadlineMs > firedAtMs) {
2061
+ timer.fireAtMs = currentDeadlineMs;
2062
+ timer.handle = setTimeout(onFire, Math.ceil(currentDeadlineMs - firedAtMs));
2063
+ return;
2064
+ }
2065
+ this.#activeBlocks.delete(key);
2066
+ this.#activeBlockDeadlinesMs.delete(key);
2067
+ this.#blockWaitDeadlinesMs.delete(key);
2068
+ this.#repairHoldUntilMs.delete(key);
2069
+ this.#traceLifecycle("timeout-queued", sbn, packetId, catalogTrackName);
2070
+ this.#finalizeExpiredBlock(sbn, packetId, catalogTrackName);
2071
+ };
2072
+ timer.handle = setTimeout(onFire, Math.max(1, Math.ceil(deadlineMs - nowMs)));
2073
+ this.#activeBlocks.set(key, timer);
2074
+ this.#indexBlockKey(key, sbn, packetId);
2075
+ }
2076
+ /**
2077
+ * Hand a block whose delivery is over to its decoder for finalization: the
2078
+ * datagram deadline fired, or the ordered relay moved past it.
2079
+ */
2080
+ #finalizeExpiredBlock(sbn, packetId, catalogTrackName) {
2081
+ const key = this.#blockKey(sbn, packetId, catalogTrackName);
2082
+ if (this.#materializedBlocks.has(key) && !this.#blockPathCounts.has(key)) {
2083
+ this.#failBlock(sbn, packetId, catalogTrackName);
2084
+ return;
2085
+ }
2086
+ const syncOwner = this.#syncDecoderBlockOwners.get(key);
2087
+ if (syncOwner) {
2088
+ this.#routeSyncCleanup(syncOwner, sbn, packetId, catalogTrackName);
2089
+ this.#failBlock(sbn, packetId, catalogTrackName);
2090
+ return;
2091
+ }
2092
+ const workerClient = this.#routableWorkerForPacket(packetId, catalogTrackName);
2093
+ if (workerClient) {
2094
+ this.#pendingWorkerCleanups.add(key);
2095
+ this.#pendingWorkerCleanupClients.set(key, workerClient);
2096
+ this.#boundOwedCleanup(key, packetId, workerClient);
2097
+ workerClient.cleanup(sbn);
2098
+ } else {
2099
+ const decoder = this.#decoderForPacket(packetId, catalogTrackName)?.client;
2100
+ if (decoder) {
2101
+ this.#routeSyncCleanup(decoder, sbn, packetId, catalogTrackName);
2102
+ this.#failBlock(sbn, packetId, catalogTrackName);
2103
+ } else {
2104
+ this.#failBlock(sbn, packetId, catalogTrackName);
2105
+ }
2106
+ }
2107
+ }
2108
+ /**
2109
+ * Upsert one packetId's track configuration. With `catalogTrackName`, the
2110
+ * classifier geometry in `config` is that stream's own and is kept per
2111
+ * stream; every other field stays per packetId.
2112
+ */
2113
+ #upsertTrackConfig(packetId, config, catalogTrackName) {
2114
+ const previous = this.#trackConfigs.get(packetId) ?? this.#defaultTrackConfig();
2115
+ const streamScopedGeometry = catalogTrackName !== void 0 && config.geometry !== void 0 ? config.geometry : void 0;
2116
+ if (streamScopedGeometry !== void 0) {
2117
+ const { geometry: _streamGeometry, ...packetLevel } = config;
2118
+ config = packetLevel;
2119
+ }
2120
+ const timingChanged = config.k !== void 0 || config.interleaveDepthMs !== void 0 || config.fps !== void 0;
2121
+ if (config.blockProfile !== void 0) {
2122
+ parseFecBlockProfile(config.blockProfile, `packetId=${packetId} fec.blockProfile`);
2123
+ }
2124
+ const blockProfile = config.blockProfile ?? previous.blockProfile;
2125
+ if (blockProfile === void 0) {
2126
+ throw new Error(
2127
+ `[FECManager] packetId=${packetId}: fec.blockProfile is required ("k-padded-v1" or "iso-ssbg1-v1"); configure the track from its catalog`
2128
+ );
2129
+ }
2130
+ if (blockProfile === "iso-ssbg1-v1") {
2131
+ const ownInterleaveDepthMs = config.interleaveDepthMs ?? this.#trackConfigs.get(packetId)?.interleaveDepthMs;
2132
+ if (ownInterleaveDepthMs === void 0 || !Number.isFinite(ownInterleaveDepthMs) || ownInterleaveDepthMs < MIN_INTERLEAVE_DEPTH_MS) {
2133
+ throw new Error(
2134
+ `[FECManager] packetId=${packetId}: iso-ssbg1-v1 requires the catalog fec.interleaveDepthMs (>= ${MIN_INTERLEAVE_DEPTH_MS} ms; got ${String(ownInterleaveDepthMs)}): it bounds how long block location keeps stated blocks and held sources`
2135
+ );
2136
+ }
2137
+ }
2138
+ const next = {
2139
+ blockProfile,
2140
+ k: config.k ?? previous.k,
2141
+ symbolSize: config.symbolSize != null && config.symbolSize > 0 ? config.symbolSize : previous.symbolSize,
2142
+ interleaveDepthMs: config.interleaveDepthMs ?? previous.interleaveDepthMs,
2143
+ fps: config.fps ?? previous.fps,
2144
+ timeoutMs: config.timeoutMs ?? (timingChanged ? 0 : previous.timeoutMs),
2145
+ ...(config.trackKind ?? previous.trackKind) === void 0 ? {} : { trackKind: config.trackKind ?? previous.trackKind },
2146
+ ...(config.codec ?? previous.codec) === void 0 ? {} : { codec: config.codec ?? previous.codec },
2147
+ ...(config.geometry ?? previous.geometry) === void 0 ? {} : { geometry: config.geometry ?? previous.geometry }
2148
+ };
2149
+ next.timeoutMs = this.#computeTimeout(next);
2150
+ if (this.#multicastSymbolDelivery === "ordered-relay") {
2151
+ this.#orderedHorizonBlocksFor(packetId, next);
2152
+ }
2153
+ if (previous.blockProfile !== void 0 && (previous.blockProfile !== blockProfile || blockProfile === "iso-ssbg1-v1" && previous.k !== next.k)) {
2154
+ this.#dropIsoLocators(packetId);
2155
+ }
2156
+ this.#trackConfigs.set(packetId, next);
2157
+ this.#refreshDeliveryGapWindow();
2158
+ let policyChanged = config.geometry !== void 0 && JSON.stringify(config.geometry) !== JSON.stringify(previous.geometry) || config.trackKind !== void 0 && config.trackKind !== previous.trackKind;
2159
+ if (streamScopedGeometry !== void 0) {
2160
+ const streamKey = this.#streamKey(packetId, catalogTrackName);
2161
+ if (JSON.stringify(streamScopedGeometry) !== JSON.stringify(this.#streamGeometries.get(streamKey))) {
2162
+ this.#streamGeometries.set(streamKey, streamScopedGeometry);
2163
+ this.#refreshDeliveryGapWindow();
2164
+ policyChanged = true;
2165
+ }
2166
+ }
2167
+ if (policyChanged) {
2168
+ this.#bumpEpoch(packetId);
2169
+ for (const [key, classifier] of [...this.#classifiers]) {
2170
+ if (!this.#belongsToPacket(key, packetId)) continue;
2171
+ const stream = this.#parseStreamKey(key);
2172
+ const geometry = this.#geometryFor(stream.packetId, stream.catalogTrackName);
2173
+ if (JSON.stringify(geometry) === JSON.stringify(classifier.geometry)) continue;
2174
+ this.#classifiers.delete(key);
2175
+ this.#applyClassifierChanges(stream.packetId, stream.catalogTrackName, classifier.retire());
2176
+ }
2177
+ }
2178
+ return next;
2179
+ }
2180
+ #normalizeSymbolData(packetId, data) {
2181
+ const symbolSize = this.symbolSizeForPacket(packetId);
2182
+ if (symbolSize <= 0 || data.byteLength === symbolSize) return data;
2183
+ const normalized = new Uint8Array(symbolSize);
2184
+ normalized.set(data.subarray(0, symbolSize));
2185
+ return normalized;
2186
+ }
2187
+ #blockKeysFor(sbn, packetId, catalogTrackName) {
2188
+ if (packetId != null && catalogTrackName !== void 0) {
2189
+ return [this.#blockKey(sbn, packetId, catalogTrackName)];
2190
+ }
2191
+ const keys = [];
2192
+ const candidates = this.#blockKeysBySbn.get(sbn);
2193
+ if (candidates) {
2194
+ for (const [key, keyPacketId] of candidates) {
2195
+ if (packetId !== void 0 && keyPacketId !== packetId) continue;
2196
+ if (!this.#isBlockKeyHeld(key)) {
2197
+ candidates.delete(key);
2198
+ continue;
2199
+ }
2200
+ keys.push(key);
2201
+ }
2202
+ if (candidates.size === 0) this.#blockKeysBySbn.delete(sbn);
2203
+ }
2204
+ if (packetId !== void 0) {
2205
+ if (keys.length === 0) return [this.#blockKey(sbn, packetId)];
2206
+ return keys.length === 1 ? keys : [];
2207
+ }
2208
+ return keys.length > 0 ? keys : [this.#blockKey(sbn, 0, catalogTrackName)];
2209
+ }
2210
+ // -----------------------------------------------------------------------
2211
+ // Accessors
2212
+ // -----------------------------------------------------------------------
2213
+ /** Internal state for diagnostics — not part of the public stats API. */
2214
+ get diagnostics() {
2215
+ return {
2216
+ timeoutMs: this.#fecTimeoutMs,
2217
+ activeBlocks: this.#activeBlocks.size,
2218
+ completedBlocks: this.#completedBlocks.size,
2219
+ cleanlyCompletedBlocks: this.#cleanlyCompletedBlocks.size,
2220
+ pendingCounts: Object.fromEntries(this.#blockSourceCounts),
2221
+ fpsDetected: this.#fpsDetected,
2222
+ interleaveDepthMs: this.#fecInterleaveDepthMs,
2223
+ startupAlignedPackets: this.#packetIdsForStreamKeys(this.#startupAlignedPackets),
2224
+ startupPartialBlocks: this.#startupPartialBlocks.size,
2225
+ startupRepairReadyPackets: this.#packetIdsForStreamKeys(this.#startupRepairReadyPackets),
2226
+ startupRepairFailOpenPackets: this.#packetIdsForStreamKeys(this.#startupRepairFailOpenPackets),
2227
+ pendingWorkerCleanups: this.#pendingWorkerCleanups.size,
2228
+ decoderUnavailableEvents: this.#decoderUnavailableEvents,
2229
+ repairTrackedBlocks: this.#blockRepairRsIds.size,
2230
+ multicastSymbolDelivery: this.#multicastSymbolDelivery,
2231
+ orderedFrontiers: Object.fromEntries(this.#orderedFrontiers),
2232
+ orderedPendingBlocks: [...this.#orderedPendingBlocks.values()].reduce((total, pending) => total + pending.size, 0),
2233
+ lifecycleTrace: this.#lifecycleTrace.map((entry) => ({ ...entry }))
2234
+ };
2235
+ }
2236
+ get fecStats() {
2237
+ const s = this.#fecStats;
2238
+ const affected = s.blocksRecovered + s.blocksFailed;
2239
+ const total = s.blocksClean + affected;
2240
+ const tracks = [...this.#fecTrackStats.entries()].sort(([left], [right]) => left - right).map(([packetId, stats]) => {
2241
+ const trackAffected = stats.blocksRecovered + stats.blocksFailed;
2242
+ const trackTotal = stats.blocksClean + trackAffected;
2243
+ return {
2244
+ ...stats,
2245
+ packetId,
2246
+ blocksComplete: trackTotal,
2247
+ k: this.#resolveTrackConfig(packetId).k,
2248
+ recoveryRate: trackAffected > 0 ? stats.blocksRecovered / trackAffected * 100 : 100,
2249
+ packetLossRate: trackTotal > 0 ? trackAffected / trackTotal * 100 : 0
2250
+ };
2251
+ });
2252
+ return {
2253
+ ...s,
2254
+ blocksComplete: total,
2255
+ mixedPathBlocks: Array.from(this.#blockPathCounts.values()).filter(
2256
+ (block) => block.multicastSymbols > 0 && block.moqSymbols > 0
2257
+ ).length,
2258
+ recentBlockPathCounts: Array.from(this.#blockPathCounts.values()).slice(-24),
2259
+ k: this.#fecK,
2260
+ // True recovery rate: of blocks that HAD loss, how many were recovered?
2261
+ recoveryRate: affected > 0 ? s.blocksRecovered / affected * 100 : 100,
2262
+ packetLossRate: total > 0 ? affected / total * 100 : 0,
2263
+ tracks,
2264
+ workers: [...this.#fecWorkerStats.entries()].sort(([left], [right]) => left - right).map(([packetId, stats]) => ({
2265
+ ...stats,
2266
+ config: {
2267
+ ...stats.config,
2268
+ resolution: stats.config.resolution ? { ...stats.config.resolution } : void 0
2269
+ },
2270
+ blocks: stats.blocks.map((block) => ({ ...block })),
2271
+ decoder: stats.decoder ? { ...stats.decoder } : void 0,
2272
+ altaFailedDetails: stats.altaFailedDetails?.map((detail) => ({ ...detail })),
2273
+ packetId
2274
+ })),
2275
+ repairSymbolSizeMismatches: this.#repairSymbolSizeMismatchCount,
2276
+ workerErrors: Object.fromEntries(this.#workerErrorCounts)
2277
+ };
2278
+ }
2279
+ /**
2280
+ * Observe Worker-backed per-track snapshots without taking ownership of the
2281
+ * legacy single-callback slot. Infrastructure diagnostics and a player may
2282
+ * therefore consume the same source event without wrapping or replacing one
2283
+ * another's callback.
2284
+ */
2285
+ addFecStatsObserver(observer) {
2286
+ this.#fecStatsObservers.add(observer);
2287
+ return () => this.#fecStatsObservers.delete(observer);
2288
+ }
2289
+ /** Start a new multicast transport join without discarding track/decoder state. */
2290
+ beginMulticastJoin() {
2291
+ this.#orderedFrontiers.clear();
2292
+ this.#startupAlignedPackets.clear();
2293
+ this.#startupPartialBlocks.clear();
2294
+ this.#startupRequireRepairReady = true;
2295
+ this.#startupRepairReadyPackets.clear();
2296
+ this.#startupRepairBoundaryCounts.clear();
2297
+ this.#startupRepairFailOpenPackets.clear();
2298
+ }
2299
+ /** How the multicast leg currently delivers symbols. */
2300
+ get multicastSymbolDelivery() {
2301
+ return this.#multicastSymbolDelivery;
2302
+ }
2303
+ /**
2304
+ * Declare how the multicast leg delivers symbols (see
2305
+ * MulticastSymbolDelivery). The multicast provider owns this fact:
2306
+ * MulticastManager applies what the window.multicast subscription declares
2307
+ * on every join, and a consumer feeding packets itself declares its own leg.
2308
+ *
2309
+ * Switching to 'ordered-relay' requires every configured track to define
2310
+ * its interleave horizon and throws otherwise. Switching back to 'datagram'
2311
+ * gives every block awaiting ordered settlement the datagram inactivity
2312
+ * deadline from now.
2313
+ */
2314
+ setMulticastSymbolDelivery(delivery) {
2315
+ if (!isMulticastSymbolDelivery(delivery)) {
2316
+ throw new Error(
2317
+ `[FECManager] multicast symbol delivery must be 'datagram' or 'ordered-relay', got ${String(delivery)}`
2318
+ );
2319
+ }
2320
+ if (delivery === this.#multicastSymbolDelivery) return;
2321
+ this.#deliveryGaps.reset();
2322
+ if (delivery === "ordered-relay") {
2323
+ for (const [packetId, config] of this.#trackConfigs) {
2324
+ this.#orderedHorizonBlocksFor(packetId, config);
2325
+ }
2326
+ this.#multicastSymbolDelivery = delivery;
2327
+ return;
2328
+ }
2329
+ this.#multicastSymbolDelivery = delivery;
2330
+ const pending = [...this.#orderedPendingBlocks.values()].flatMap((blocks) => [...blocks.values()]);
2331
+ this.#orderedPendingBlocks.clear();
2332
+ this.#orderedFrontiers.clear();
2333
+ for (const key of pending) {
2334
+ const block = this.#parseBlockKey(key);
2335
+ this.noteBlockActivity(block.sbn, block.packetId, block.catalogTrackName);
2336
+ }
2337
+ }
2338
+ /** Begin a fresh decode epoch for one physical stream. */
2339
+ resetTrack(packetId, catalogTrackName) {
2340
+ if (!Number.isInteger(packetId) || packetId < 0) {
2341
+ throw new Error(`[FECManager] invalid packetId for reset: ${packetId}`);
2342
+ }
2343
+ if (!catalogTrackName) {
2344
+ throw new Error("[FECManager] catalogTrackName is required for track reset");
2345
+ }
2346
+ const resetMap = (map) => {
2347
+ for (const key of map.keys()) {
2348
+ if (!this.#belongsToStream(key, packetId, catalogTrackName)) continue;
2349
+ map.delete(key);
2350
+ }
2351
+ };
2352
+ const resetTimerMap = (map) => {
2353
+ for (const [key, timer] of map) {
2354
+ if (!this.#belongsToStream(key, packetId, catalogTrackName)) continue;
2355
+ if (timer?.handle != null) clearTimeout(timer.handle);
2356
+ map.delete(key);
2357
+ }
2358
+ };
2359
+ const resetSet = (set) => {
2360
+ for (const key of set) {
2361
+ if (this.#belongsToStream(key, packetId, catalogTrackName)) set.delete(key);
2362
+ }
2363
+ };
2364
+ resetTimerMap(this.#activeBlocks);
2365
+ resetTimerMap(this.#repairDeadlines);
2366
+ resetMap(this.#activeBlockDeadlinesMs);
2367
+ resetMap(this.#blockWaitDeadlinesMs);
2368
+ resetMap(this.#repairHoldUntilMs);
2369
+ resetMap(this.#repairDeadlineAtMs);
2370
+ resetMap(this.#terminalExpiryMs);
2371
+ resetMap(this.#pendingWorkerCleanupClients);
2372
+ for (const key of [...this.#pendingWorkerCleanupBounds.keys()]) {
2373
+ if (this.#belongsToStream(key, packetId, catalogTrackName)) this.#clearOwedCleanupBound(key);
2374
+ }
2375
+ resetMap(this.#blockSourceCounts);
2376
+ resetMap(this.#blockSourceEsis);
2377
+ resetMap(this.#blockAuthoredLengths);
2378
+ resetMap(this.#blockRepairRsIds);
2379
+ resetMap(this.#blockPathCounts);
2380
+ resetMap(this.#blockAnchorsMs);
2381
+ resetMap(this.#blockCatalogTrackNames);
2382
+ resetMap(this.#syncDecoderBlockOwners);
2383
+ resetMap(this.#startupRepairBoundaryCounts);
2384
+ resetMap(this.#orderedFrontiers);
2385
+ resetMap(this.#orderedPendingBlocks);
2386
+ resetSet(this.#pendingWorkerCleanups);
2387
+ resetSet(this.#completedBlocks);
2388
+ resetSet(this.#cleanlyCompletedBlocks);
2389
+ resetSet(this.#repairRequestedBlocks);
2390
+ resetSet(this.#workerRecoveredBlocks);
2391
+ resetSet(this.#startupAlignedPackets);
2392
+ resetSet(this.#startupPartialBlocks);
2393
+ resetSet(this.#startupRepairReadyPackets);
2394
+ resetSet(this.#startupRepairFailOpenPackets);
2395
+ resetSet(this.#seenSymbols);
2396
+ resetSet(this.#failedBlocks);
2397
+ resetSet(this.#repairedBlocks);
2398
+ resetSet(this.#materializedBlocks);
2399
+ resetMap(this.#resolvedBlockClasses);
2400
+ resetMap(this.#highestSbn);
2401
+ resetMap(this.#isoLocators);
2402
+ resetMap(this.#sourceAnchors);
2403
+ this.#classifiers.delete(this.#streamKey(packetId, catalogTrackName));
2404
+ this.#bumpEpoch(packetId);
2405
+ for (const [sbn, keys] of this.#blockKeysBySbn) {
2406
+ for (const key of keys.keys()) {
2407
+ if (this.#belongsToStream(key, packetId, catalogTrackName)) keys.delete(key);
2408
+ }
2409
+ if (keys.size === 0) this.#blockKeysBySbn.delete(sbn);
2410
+ }
2411
+ const decoderClients = /* @__PURE__ */ new Set();
2412
+ for (const [key, entry] of this.#fecDecoders) {
2413
+ if (this.#belongsToStream(key, packetId, catalogTrackName)) {
2414
+ decoderClients.add(entry.client);
2415
+ }
2416
+ }
2417
+ const clients = /* @__PURE__ */ new Set();
2418
+ for (const client of decoderClients) {
2419
+ if (typeof client.flush === "function" && typeof client.reset_stats === "function") {
2420
+ try {
2421
+ client.flush();
2422
+ client.reset_stats();
2423
+ clients.add(client);
2424
+ } catch {
2425
+ this.#discardFecDecoderClient(client);
2426
+ }
2427
+ } else {
2428
+ this.#discardFecDecoderClient(client);
2429
+ }
2430
+ }
2431
+ for (const [key, client] of [...this.#workerClients]) {
2432
+ if (!this.#belongsToStream(key, packetId, catalogTrackName)) continue;
2433
+ if (client.disposed === true) {
2434
+ this.#evictDisposedWorker(client);
2435
+ continue;
2436
+ }
2437
+ client.reset();
2438
+ clients.add(client);
2439
+ }
2440
+ for (const client of clients) this.#resetDecoderBlockOwners(client, catalogTrackName);
2441
+ }
2442
+ /**
2443
+ * Retire one packet ID, drop-only: forget every stream on it (block
2444
+ * ledgers, deadlines and their timers, repair holds, terminal records,
2445
+ * classifiers, stream geometry), its per-packet configuration and
2446
+ * statistics, and take its decoders out of routing.
2447
+ *
2448
+ * This is the end of the packet ID's streams, not a new epoch of them (that
2449
+ * is resetTrack): afterwards isTrackConfigured() is false and nothing about
2450
+ * it remains until it is configured again. Nothing is reported, because
2451
+ * nothing happened to the media: no block fails, recovers or resolves, no
2452
+ * repair resolves, and no decoder becomes "unavailable" (a retired stream
2453
+ * has no symbols left to decode, so a decoder-unavailable event would
2454
+ * describe lost protection that no stream suffers). The epoch still
2455
+ * advances, so a gap decision made before the retirement never matches a
2456
+ * stream later configured on the same packet ID.
2457
+ *
2458
+ * Decoder and Worker clients stay caller-owned: each is flushed or reset so
2459
+ * it holds no block of the retired streams, then released from routing;
2460
+ * dispose it after this returns. Cumulative totals in fecStats keep what
2461
+ * the retired streams contributed.
2462
+ */
2463
+ releasePacket(packetId) {
2464
+ if (!Number.isInteger(packetId) || packetId < 0) {
2465
+ throw new Error(`[FECManager] invalid packetId for release: ${packetId}`);
2466
+ }
2467
+ const onPacket = (key) => this.#belongsToPacket(key, packetId);
2468
+ const dropMap = (map) => {
2469
+ for (const key of [...map.keys()]) if (onPacket(key)) map.delete(key);
2470
+ };
2471
+ const dropTimerMap = (map) => {
2472
+ for (const [key, timer] of [...map]) {
2473
+ if (!onPacket(key)) continue;
2474
+ if (timer?.handle != null) clearTimeout(timer.handle);
2475
+ map.delete(key);
2476
+ }
2477
+ };
2478
+ const dropSet = (set) => {
2479
+ for (const key of [...set]) if (onPacket(key)) set.delete(key);
2480
+ };
2481
+ dropTimerMap(this.#activeBlocks);
2482
+ dropTimerMap(this.#repairDeadlines);
2483
+ for (const key of [...this.#pendingWorkerCleanupBounds.keys()]) {
2484
+ if (onPacket(key)) this.#clearOwedCleanupBound(key);
2485
+ }
2486
+ dropMap(this.#activeBlockDeadlinesMs);
2487
+ dropMap(this.#blockWaitDeadlinesMs);
2488
+ dropMap(this.#repairHoldUntilMs);
2489
+ dropMap(this.#repairDeadlineAtMs);
2490
+ dropMap(this.#terminalExpiryMs);
2491
+ dropMap(this.#pendingWorkerCleanupClients);
2492
+ dropMap(this.#blockSourceCounts);
2493
+ dropMap(this.#blockSourceEsis);
2494
+ dropMap(this.#blockAuthoredLengths);
2495
+ dropMap(this.#blockRepairRsIds);
2496
+ dropMap(this.#blockPathCounts);
2497
+ dropMap(this.#blockAnchorsMs);
2498
+ dropMap(this.#blockCatalogTrackNames);
2499
+ dropMap(this.#syncDecoderBlockOwners);
2500
+ dropMap(this.#startupRepairBoundaryCounts);
2501
+ dropMap(this.#orderedFrontiers);
2502
+ dropMap(this.#orderedPendingBlocks);
2503
+ dropMap(this.#resolvedBlockClasses);
2504
+ dropMap(this.#highestSbn);
2505
+ dropMap(this.#isoLocators);
2506
+ dropMap(this.#sourceAnchors);
2507
+ dropMap(this.#classifiers);
2508
+ dropMap(this.#streamGeometries);
2509
+ dropSet(this.#pendingWorkerCleanups);
2510
+ dropSet(this.#completedBlocks);
2511
+ dropSet(this.#cleanlyCompletedBlocks);
2512
+ dropSet(this.#repairRequestedBlocks);
2513
+ dropSet(this.#workerRecoveredBlocks);
2514
+ dropSet(this.#startupAlignedPackets);
2515
+ dropSet(this.#startupPartialBlocks);
2516
+ dropSet(this.#startupRepairReadyPackets);
2517
+ dropSet(this.#startupRepairFailOpenPackets);
2518
+ dropSet(this.#seenSymbols);
2519
+ dropSet(this.#failedBlocks);
2520
+ dropSet(this.#repairedBlocks);
2521
+ dropSet(this.#materializedBlocks);
2522
+ for (const [sbn, keys] of [...this.#blockKeysBySbn]) {
2523
+ for (const [key, keyPacketId] of [...keys]) {
2524
+ if (keyPacketId === packetId) keys.delete(key);
2525
+ }
2526
+ if (keys.size === 0) this.#blockKeysBySbn.delete(sbn);
2527
+ }
2528
+ this.#terminalQueues.delete(packetId);
2529
+ this.#trackConfigs.delete(packetId);
2530
+ this.#fecTrackStats.delete(packetId);
2531
+ this.#fecWorkerStats.delete(packetId);
2532
+ this.#bumpEpoch(packetId);
2533
+ this.#refreshDeliveryGapWindow();
2534
+ for (const [key, entry] of [...this.#fecDecoders]) {
2535
+ if (!onPacket(key)) continue;
2536
+ this.#fecDecoders.delete(key);
2537
+ this.#decoderBlockOwners.delete(entry.client);
2538
+ try {
2539
+ entry.client.flush?.();
2540
+ } catch {
2541
+ }
2542
+ }
2543
+ for (const [key, client] of [...this.#workerClients]) {
2544
+ if (!onPacket(key)) continue;
2545
+ this.#workerClients.delete(key);
2546
+ this.#decoderBlockOwners.delete(client);
2547
+ this.#releasedWorkerClients.add(client);
2548
+ try {
2549
+ if (client.disposed !== true) client.reset();
2550
+ } catch {
2551
+ }
2552
+ }
2553
+ }
2554
+ /** Record a transport symbol and reject later duplicates of the same packetId/SBN/ESI. */
2555
+ feedSymbolFromSource(packetId, sbn, esi, _data, source, catalogTrackName) {
2556
+ const blockKey = this.#blockKey(sbn, packetId, catalogTrackName);
2557
+ const streamKey = this.#streamKey(packetId, catalogTrackName);
2558
+ if (source === "multicast" && this.#multicastSymbolDelivery === "ordered-relay") {
2559
+ this.#advanceOrderedFrontier(streamKey, sbn, packetId, catalogTrackName);
2560
+ }
2561
+ const k = this.kForPacket(packetId);
2562
+ const sourceSymbols = this.#isIso(packetId) ? this.#isoLocatorFor(packetId, catalogTrackName).blockAt(sbn)?.length ?? k : k;
2563
+ const isSourceSymbol = esi >= 0 && esi < sourceSymbols;
2564
+ const isStartupPartial = this.#startupPartialBlocks.has(blockKey);
2565
+ const isReadyMulticastRepair = this.#startupRequireRepairReady && source === "multicast" && !isSourceSymbol && this.#startupRepairReadyPackets.has(streamKey);
2566
+ const awaitingMulticastRepair = this.#startupRequireRepairReady && source === "multicast" && !this.#startupRepairReadyPackets.has(streamKey) && !this.#startupRepairFailOpenPackets.has(streamKey);
2567
+ if (awaitingMulticastRepair) {
2568
+ if (!isSourceSymbol) {
2569
+ this.#startupRepairReadyPackets.add(streamKey);
2570
+ this.#startupRepairBoundaryCounts.delete(streamKey);
2571
+ this.#markStartupPartial(blockKey);
2572
+ return false;
2573
+ }
2574
+ if (esi === 0) {
2575
+ const boundaries = (this.#startupRepairBoundaryCounts.get(streamKey) ?? 0) + 1;
2576
+ this.#startupRepairBoundaryCounts.set(streamKey, boundaries);
2577
+ if (boundaries >= STARTUP_REPAIR_WAIT_SOURCE_BOUNDARIES) {
2578
+ this.#startupRepairBoundaryCounts.delete(streamKey);
2579
+ this.#startupRepairFailOpenPackets.add(streamKey);
2580
+ } else {
2581
+ this.#markStartupPartial(blockKey);
2582
+ return false;
2583
+ }
2584
+ } else {
2585
+ this.#markStartupPartial(blockKey);
2586
+ return false;
2587
+ }
2588
+ }
2589
+ if (isSourceSymbol && esi === 0) {
2590
+ this.#startupPartialBlocks.delete(blockKey);
2591
+ this.#startupAlignedPackets.add(streamKey);
2592
+ } else if (isStartupPartial && !isReadyMulticastRepair || !this.#startupAlignedPackets.has(streamKey) && !isReadyMulticastRepair) {
2593
+ this.#markStartupPartial(blockKey);
2594
+ return false;
2595
+ }
2596
+ if (this.#completedBlocks.has(blockKey)) {
2597
+ this.#fecStats.duplicateSymbols++;
2598
+ this.#statsForPacket(packetId).duplicateSymbols++;
2599
+ return false;
2600
+ }
2601
+ const symbolKey = `${blockKey}:${esi}`;
2602
+ if (this.#seenSymbols.has(symbolKey)) {
2603
+ this.#fecStats.duplicateSymbols++;
2604
+ this.#statsForPacket(packetId).duplicateSymbols++;
2605
+ return false;
2606
+ }
2607
+ this.#seenSymbols.add(symbolKey);
2608
+ const counts = this.#blockPathCounts.get(blockKey) ?? {
2609
+ packetId,
2610
+ sbn,
2611
+ ...catalogTrackName === void 0 ? {} : { catalogTrackName },
2612
+ multicastSymbols: 0,
2613
+ moqSymbols: 0,
2614
+ complete: false
2615
+ };
2616
+ if (!this.#blockAnchorsMs.has(blockKey)) this.#blockAnchorsMs.set(blockKey, performance.now());
2617
+ if (source === "multicast") counts.multicastSymbols++;
2618
+ else counts.moqSymbols++;
2619
+ this.#blockPathCounts.set(blockKey, counts);
2620
+ this.#observeBlockCatalogTrackName(blockKey, sbn, packetId, catalogTrackName);
2621
+ if (this.#seenSymbols.size > 4096) {
2622
+ dropOldest(this.#seenSymbols, this.#seenSymbols.size - 2048);
2623
+ }
2624
+ if (this.#blockPathCounts.size > 512) {
2625
+ dropOldest(this.#blockPathCounts, this.#blockPathCounts.size - 256);
2626
+ }
2627
+ if (this.#blockAnchorsMs.size > 512) {
2628
+ dropOldest(this.#blockAnchorsMs, this.#blockAnchorsMs.size - 256);
2629
+ }
2630
+ if (this.#blockCatalogTrackNames.size > 512) {
2631
+ for (const key of dropOldest(
2632
+ this.#blockCatalogTrackNames,
2633
+ this.#blockCatalogTrackNames.size - 256
2634
+ )) {
2635
+ this.#unindexBlockKey(key);
2636
+ }
2637
+ }
2638
+ return true;
2639
+ }
2640
+ #observeBlockCatalogTrackName(blockKey, sbn, packetId, catalogTrackName) {
2641
+ if (catalogTrackName === void 0) return;
2642
+ if (!this.#blockCatalogTrackNames.has(blockKey)) {
2643
+ this.#blockCatalogTrackNames.set(blockKey, catalogTrackName);
2644
+ this.#indexBlockKey(blockKey, sbn, packetId);
2645
+ return;
2646
+ }
2647
+ if (this.#blockCatalogTrackNames.get(blockKey) !== catalogTrackName) {
2648
+ this.#blockCatalogTrackNames.set(blockKey, null);
2649
+ }
2650
+ }
2651
+ /** Exact catalog identity retained for async recovery, if unambiguous. */
2652
+ catalogTrackNameForBlock(sbn, packetId, catalogTrackName) {
2653
+ if (catalogTrackName !== void 0) {
2654
+ return this.#blockCatalogTrackNames.get(
2655
+ this.#blockKey(sbn, packetId, catalogTrackName)
2656
+ ) ?? void 0;
2657
+ }
2658
+ const names = /* @__PURE__ */ new Set();
2659
+ for (const [key, name] of this.#blockCatalogTrackNames) {
2660
+ const block = this.#parseBlockKey(key);
2661
+ if (block.packetId === packetId && block.sbn === sbn && name !== null) names.add(name);
2662
+ }
2663
+ return names.size === 1 ? [...names][0] : void 0;
2664
+ }
2665
+ #markStartupPartial(blockKey) {
2666
+ this.#startupPartialBlocks.add(blockKey);
2667
+ if (this.#startupPartialBlocks.size > 128) {
2668
+ this.#startupPartialBlocks = new Set(
2669
+ Array.from(this.#startupPartialBlocks).slice(-64)
2670
+ );
2671
+ }
2672
+ }
2673
+ /** Per-block state snapshot for FecBlockGrid visualization.
2674
+ * Returns the most recent blocks (active + recently completed). */
2675
+ getBlockStates() {
2676
+ const blocks = [];
2677
+ for (const [key] of this.#activeBlocks) {
2678
+ const { packetId, sbn } = this.#parseBlockKey(key);
2679
+ const count = this.#blockSourceCounts.get(key) ?? 0;
2680
+ const k = this.#blockAuthoredLengths.get(key) ?? this.kForPacket(packetId);
2681
+ blocks.push({ sbn, packetId, state: "collecting", sourceCount: count, k, progress: k > 0 ? Math.min(count / k, 1) : 0 });
2682
+ }
2683
+ const completed = [...this.#completedBlocks].slice(-16);
2684
+ for (const key of completed) {
2685
+ const { packetId, sbn } = this.#parseBlockKey(key);
2686
+ if (!this.#activeBlocks.has(key)) {
2687
+ const k = this.kForPacket(packetId);
2688
+ blocks.push({ sbn, packetId, state: "complete", sourceCount: k, k, progress: 1 });
2689
+ }
2690
+ }
2691
+ blocks.sort((a, b) => a.sbn - b.sbn || (a.packetId ?? 0) - (b.packetId ?? 0));
2692
+ return blocks.slice(-24);
2693
+ }
2694
+ /** The raw WASM decoder for stats access (first registered decoder) */
2695
+ get wasmDecoder() {
2696
+ return this.#fecDecoders.values().next().value?.client ?? null;
2697
+ }
2698
+ get k() {
2699
+ return this.#fecK;
2700
+ }
2701
+ kForPacket(packetId) {
2702
+ return this.#resolveTrackConfig(packetId).k;
2703
+ }
2704
+ /** FEC symbol size in bytes. */
2705
+ get symbolSize() {
2706
+ return this.#fecSymbolSize;
2707
+ }
2708
+ symbolSizeForPacket(packetId) {
2709
+ return this.#resolveTrackConfig(packetId).symbolSize;
2710
+ }
2711
+ /** FEC block span in milliseconds (0 = no interleaving). */
2712
+ get interleaveDepthMs() {
2713
+ return this.#fecInterleaveDepthMs;
2714
+ }
2715
+ interleaveDepthMsForPacket(packetId) {
2716
+ return this.#resolveTrackConfig(packetId).interleaveDepthMs;
2717
+ }
2718
+ /**
2719
+ * Record one multicast MMTP packet arrival (every packet of the leg, FEC
2720
+ * symbol or not) for the delivery-gap measurement. `packetSequenceNumber`
2721
+ * is the MMTP packet_sequence_number, which increments by one per packet of
2722
+ * a packet_id: it is what separates a stall from a loss (see
2723
+ * DeliveryGapEstimator). Datagram delivery only.
2724
+ */
2725
+ noteMulticastArrival(packetId, packetSequenceNumber) {
2726
+ if (this.#multicastSymbolDelivery !== "datagram") return;
2727
+ this.#deliveryGaps.noteArrival(performance.now(), packetId, packetSequenceNumber);
2728
+ }
2729
+ /**
2730
+ * Resolved per-track multicast late-symbol wait in milliseconds: the span
2731
+ * plus the larger of the catalog tolerance and the measured delivery gap.
2732
+ */
2733
+ timeoutMsForPacket(packetId) {
2734
+ return this.#multicastWaitMs(this.#resolveTrackConfig(packetId));
2735
+ }
2736
+ /**
2737
+ * Largest multicast delivery gap measured over the current window (ms), and
2738
+ * that window (undefined: held since the last reset). Diagnostics.
2739
+ */
2740
+ multicastDeliveryGap() {
2741
+ return {
2742
+ peakGapMs: this.#deliveryGaps.peakGapMs(performance.now()),
2743
+ windowMs: this.#deliveryGaps.windowMs
2744
+ };
2745
+ }
2746
+ /**
2747
+ * Receiver-local time of a block's first accepted symbol, or undefined for
2748
+ * a block no symbol reached (one materialized because a later SBN arrived).
2749
+ */
2750
+ blockAnchorAtMs(sbn, packetId, catalogTrackName) {
2751
+ return this.#blockAnchorsMs.get(this.#blockKey(sbn, packetId, catalogTrackName));
2752
+ }
2753
+ /** Absolute fail deadline of a still-open block, or undefined. */
2754
+ blockDeadlineAtMs(sbn, packetId, catalogTrackName) {
2755
+ return this.#activeBlockDeadlinesMs.get(this.#blockKey(sbn, packetId, catalogTrackName));
2756
+ }
2757
+ /**
2758
+ * Keep a block open until `untilAtMs` while a repair FETCH the scheduler
2759
+ * admitted is in flight. Never shortens the multicast wait already armed;
2760
+ * while set, the hold is the block's whole deadline, so no later symbol
2761
+ * extends it. Returns false when the block is no longer open (resolved, or
2762
+ * not yet tracked).
2763
+ */
2764
+ holdBlockForRepair(sbn, packetId, catalogTrackName, untilAtMs) {
2765
+ if (!Number.isFinite(untilAtMs)) {
2766
+ throw new Error(`[FECManager] repair hold must be a finite time (got ${untilAtMs})`);
2767
+ }
2768
+ const key = this.#blockKey(sbn, packetId, catalogTrackName);
2769
+ if (!this.#activeBlocks.has(key)) return false;
2770
+ const previous = this.#repairHoldUntilMs.get(key);
2771
+ if (previous !== void 0 && previous >= untilAtMs) return true;
2772
+ this.#repairHoldUntilMs.set(
2773
+ key,
2774
+ Math.max(untilAtMs, this.#blockWaitDeadlinesMs.get(key) ?? -Infinity)
2775
+ );
2776
+ this.#applyBlockDeadline(key, sbn, packetId, catalogTrackName);
2777
+ return true;
2778
+ }
2779
+ /**
2780
+ * Drop a block's repair hold: its FETCH ended without delivering. The block
2781
+ * falls back to its multicast wait, which may already have passed.
2782
+ */
2783
+ releaseRepairHold(sbn, packetId, catalogTrackName) {
2784
+ const key = this.#blockKey(sbn, packetId, catalogTrackName);
2785
+ if (!this.#repairHoldUntilMs.delete(key) || !this.#activeBlocks.has(key)) return;
2786
+ this.#applyBlockDeadline(key, sbn, packetId, catalogTrackName);
2787
+ }
2788
+ /** Resolved per-track D-28 repair request deadline in milliseconds. */
2789
+ repairDeadlineMsForPacket(packetId) {
2790
+ return this.#repairDeadlineMs(this.#resolveTrackConfig(packetId));
2791
+ }
2792
+ /**
2793
+ * One block lifetime: the longest a block stays open after its last symbol
2794
+ * (inactivity fail bound plus D-28 repair grace).
2795
+ */
2796
+ blockLifetimeMsForPacket(packetId) {
2797
+ return this.#blockLifetimeMs(this.#resolveTrackConfig(packetId));
2798
+ }
2799
+ /**
2800
+ * How long a terminal (clean, recovered or failed) block stays authoritative,
2801
+ * for late symbols and for the in-order release gate. It is also the gate's
2802
+ * hold ceiling (MmtpRouter's holdLimitMs): a retired block reads back as
2803
+ * untracked, which the gate counts as lost, so no frame may still be waiting
2804
+ * on a block that can retire. The window is one interleave depth for a gap's
2805
+ * last block to finish arriving, one block lifetime to fail it, and one more
2806
+ * for an owed Worker cleanup that can keep a failing block pending. Every
2807
+ * term comes from the track's catalog geometry; there is no separate cap.
2808
+ */
2809
+ terminalRetentionMsForPacket(packetId) {
2810
+ const config = this.#resolveTrackConfig(packetId);
2811
+ return config.interleaveDepthMs + 2 * this.#blockLifetimeMs(config);
2812
+ }
2813
+ /** Remaining fetch budget for a specific repair-requested block. */
2814
+ repairFetchTimeoutMsForBlock(sbn, packetId, catalogTrackName) {
2815
+ const deadlineMs = this.#activeBlockDeadlinesMs.get(
2816
+ this.#blockKey(sbn, packetId, catalogTrackName)
2817
+ );
2818
+ if (deadlineMs !== void 0) {
2819
+ return Math.max(1, Math.ceil(deadlineMs - performance.now()));
2820
+ }
2821
+ const trackConfig = this.#resolveTrackConfig(packetId);
2822
+ return Math.max(1, trackConfig.timeoutMs - this.#repairDeadlineMs(trackConfig));
2823
+ }
2824
+ /** True once configureTrack/setFecDecoder/setFecWorker configured this packetId. */
2825
+ // -----------------------------------------------------------------------
2826
+ // iso-ssbg1-v1 block location (BLO-35597 iso:2/iso:3)
2827
+ // -----------------------------------------------------------------------
2828
+ /** The configured `fec.blockProfile` of a packetId (undefined until configured). */
2829
+ blockProfileForPacket(packetId) {
2830
+ return this.#trackConfigs.get(packetId)?.blockProfile;
2831
+ }
2832
+ #isIso(packetId) {
2833
+ return this.#trackConfigs.get(packetId)?.blockProfile === "iso-ssbg1-v1";
2834
+ }
2835
+ #isoLocatorFor(packetId, catalogTrackName) {
2836
+ const streamKey = this.#streamKey(packetId, catalogTrackName);
2837
+ let locator = this.#isoLocators.get(streamKey);
2838
+ if (!locator) {
2839
+ locator = new IsoBlockLocator(this.kForPacket(packetId), this.#isoRetentionBlocks(packetId));
2840
+ this.#isoLocators.set(streamKey, locator);
2841
+ }
2842
+ return locator;
2843
+ }
2844
+ /**
2845
+ * The locator a call is about to grow, with its retention brought up to
2846
+ * date: the horizon follows catalog timing, which a later configuration
2847
+ * (this track's or another video track's keyframe cadence) can change.
2848
+ */
2849
+ #growingIsoLocatorFor(packetId, catalogTrackName) {
2850
+ const locator = this.#isoLocatorFor(packetId, catalogTrackName);
2851
+ locator.retentionBlocks = this.#isoRetentionBlocks(packetId);
2852
+ return locator;
2853
+ }
2854
+ #dropIsoLocators(packetId) {
2855
+ for (const key of [...this.#isoLocators.keys()]) {
2856
+ if (this.#belongsToPacket(key, packetId)) this.#isoLocators.delete(key);
2857
+ }
2858
+ }
2859
+ /**
2860
+ * The iso-ssbg1-v1 locator horizon in blocks: every block the manager can
2861
+ * still hold terminal state for (`terminalRetentionMsForPacket`) over the
2862
+ * block span, plus the block being filled, measured with the shared
2863
+ * lower-bound metric so a retained block is never forgotten early. Every
2864
+ * term is the packet's catalog geometry, which configuration validated
2865
+ * (#upsertTrackConfig), so this never falls back: an underivable horizon
2866
+ * throws rather than leave the locator without retention.
2867
+ */
2868
+ #isoRetentionBlocks(packetId) {
2869
+ const config = this.#trackConfigs.get(packetId);
2870
+ if (config?.blockProfile !== "iso-ssbg1-v1" || !(config.interleaveDepthMs >= MIN_INTERLEAVE_DEPTH_MS)) {
2871
+ throw new Error(
2872
+ `[FECManager] packetId=${packetId} has no iso-ssbg1-v1 block span to derive block-location retention from`
2873
+ );
2874
+ }
2875
+ const blocks = Math.ceil(this.terminalRetentionMsForPacket(packetId) / config.interleaveDepthMs) + 1;
2876
+ if (!Number.isSafeInteger(blocks) || blocks <= 0) {
2877
+ throw new Error(`[FECManager] packetId=${packetId} iso-ssbg1-v1 retention of ${blocks} blocks is not derivable`);
2878
+ }
2879
+ return blocks;
2880
+ }
2881
+ /** Whether an iso-ssbg1-v1 protected packet fits one ssbg_mode1 symbol (len + 2 <= T). */
2882
+ isoSourceFits(packetId, packetLength) {
2883
+ return ssbg1FitsSymbol(packetLength, this.symbolSizeForPacket(packetId));
2884
+ }
2885
+ /**
2886
+ * Locate an iso-ssbg1-v1 source by range membership in a block a repair
2887
+ * already stated. Null while its block is unknown: hold it (holdIsoSource).
2888
+ */
2889
+ locateIsoSource(packetId, catalogTrackName, ssId) {
2890
+ const located = this.#isoLocatorFor(packetId, catalogTrackName).locate(ssId);
2891
+ return located === null ? null : { sbn: located.block.start, esi: located.esi };
2892
+ }
2893
+ /**
2894
+ * Hold a first-arrival iso-ssbg1-v1 source whose block no repair has stated
2895
+ * yet. Returns false for a later copy of an SS_ID already held.
2896
+ */
2897
+ holdIsoSource(packetId, catalogTrackName, ssId, source) {
2898
+ const locator = this.#growingIsoLocatorFor(packetId, catalogTrackName);
2899
+ const evictedBefore = locator.heldEvicted;
2900
+ const held = locator.hold(ssId, source);
2901
+ const evicted = locator.heldEvicted - evictedBefore;
2902
+ if (evicted > 0) {
2903
+ const before = this.#isoHeldEvictedCount;
2904
+ this.#isoHeldEvictedCount += evicted;
2905
+ if (before < 5 || Math.floor(before / 100) !== Math.floor(this.#isoHeldEvictedCount / 100)) {
2906
+ console.warn(
2907
+ `[FECManager] packetId=${packetId} iso-ssbg1-v1 held-source cap (${locator.heldCapacity}) evicted ${evicted} unplaced source(s); SS_IDs are not consecutive (C.5.2)`
2908
+ );
2909
+ }
2910
+ }
2911
+ return held;
2912
+ }
2913
+ /**
2914
+ * Apply an iso-ssbg1-v1 repair symbol's `(SS_Start, SSB_length)`: states the
2915
+ * block, and hands back the held sources it (and any repair-less span in
2916
+ * front of it) now places. The caller feeds them as first-arrival sources,
2917
+ * then the repair symbol, the order they had on the wire. A repair that
2918
+ * contradicts a known block is refused (counted) and must be dropped.
2919
+ */
2920
+ placeIsoRepair(packetId, catalogTrackName, ssStart, ssbLength) {
2921
+ const placement = this.#growingIsoLocatorFor(packetId, catalogTrackName).placeRepair(ssStart, ssbLength);
2922
+ if (placement.status !== "placed") {
2923
+ this.#isoRepairRefusedCount++;
2924
+ if (this.#isoRepairRefusedCount <= 5 || this.#isoRepairRefusedCount % 100 === 0) {
2925
+ console.warn(`[FECManager] packetId=${packetId} iso-ssbg1-v1 repair refused (${placement.status}): ${placement.reason}`);
2926
+ }
2927
+ return placement;
2928
+ }
2929
+ if (placement.span !== void 0) {
2930
+ const key = this.#blockKey(placement.span.block.start, packetId, catalogTrackName);
2931
+ if (!this.#blockAuthoredLengths.has(key)) this.#blockAuthoredLengths.set(key, placement.span.block.length);
2932
+ }
2933
+ if (placement.span !== void 0 && placement.span.placed.length === 0) {
2934
+ this.#materializeBlock(placement.span.block.start, packetId, catalogTrackName, true);
2935
+ }
2936
+ return placement;
2937
+ }
2938
+ /**
2939
+ * Resolution of the SS_IDs strictly between two released frames' SS_IDs on
2940
+ * an iso-ssbg1-v1 stream, for in-order frame release: null while a block
2941
+ * that could hold them is pending or not yet stated by a repair, else the
2942
+ * starts of the blocks that failed or vanished.
2943
+ *
2944
+ * `untracked` reads exactly as in the k-padded-v1 release gate
2945
+ * (MmtpRouter #createFecReleaseGate): a block holding either released
2946
+ * frame's SS_ID (`fromSsId` or `toSsId`) delivered that frame, so untracked
2947
+ * there means it resolved and retired; a block starting after the stream's frontier (`highestSbn`,
2948
+ * an SS_Start here) may still be heard from; a block at or behind the
2949
+ * frontier that no symbol of this manager ever reached vanished, and its
2950
+ * media is lost.
2951
+ */
2952
+ isoSpanResolution(packetId, catalogTrackName, fromSsId, toSsId) {
2953
+ if (!serial32Precedes(fromSsId, toSsId) || serial32Distance(fromSsId, toSsId) <= 1) return [];
2954
+ const locator = this.#isoLocators.get(this.#streamKey(packetId, catalogTrackName));
2955
+ if (!locator || !locator.coversBetween(fromSsId, toSsId)) return null;
2956
+ const highest = this.highestSbn(packetId, catalogTrackName);
2957
+ const lost = [];
2958
+ for (const block of locator.blocksBetween(fromSsId, toSsId)) {
2959
+ const resolution = this.blockResolution(block.start, packetId, catalogTrackName);
2960
+ if (resolution === "pending") return null;
2961
+ if (resolution === "failed") {
2962
+ lost.push(block.start);
2963
+ } else if (resolution === "untracked") {
2964
+ if (isoBlockContains(block.start, block.length, fromSsId) || isoBlockContains(block.start, block.length, toSsId)) {
2965
+ continue;
2966
+ }
2967
+ if (highest === void 0 || serial32Precedes(highest, block.start)) return null;
2968
+ lost.push(block.start);
2969
+ }
2970
+ }
2971
+ return lost;
2972
+ }
2973
+ /**
2974
+ * iso-ssbg1-v1 block location: repair symbols it refused, oversize sources,
2975
+ * the sources held now for a block no repair has stated yet, and the held
2976
+ * sources the capacity cap evicted (cumulative), and the sources a
2977
+ * synchronous decoder held unplaced and retired at its horizon (cumulative).
2978
+ */
2979
+ get isoDiagnostics() {
2980
+ let heldSources = 0;
2981
+ for (const locator of this.#isoLocators.values()) heldSources += locator.heldCount;
2982
+ return {
2983
+ repairRefused: this.#isoRepairRefusedCount,
2984
+ sourceOversize: this.#isoSourceOversizeCount,
2985
+ heldSources,
2986
+ heldEvicted: this.#isoHeldEvictedCount,
2987
+ decoderUnplacedRetired: this.#isoDecoderUnplacedRetired,
2988
+ decoderUnplacedRetireFailed: this.#isoDecoderUnplacedRetireFailed
2989
+ };
2990
+ }
2991
+ isTrackConfigured(packetId) {
2992
+ return this.#trackConfigs.has(packetId);
2993
+ }
2994
+ /** Catalog/reset epoch of a packet's streams; gap decisions carry it. */
2995
+ epochFor(packetId) {
2996
+ return this.#packetEpochs.get(packetId) ?? 0;
2997
+ }
2998
+ /** Configured media kind of a packetId, when known. */
2999
+ trackKindFor(packetId) {
3000
+ return this.#trackConfigs.get(packetId)?.trackKind;
3001
+ }
3002
+ /** Configured codec of a packetId, when known. */
3003
+ codecFor(packetId) {
3004
+ return this.#trackConfigs.get(packetId)?.codec;
3005
+ }
3006
+ /**
3007
+ * Block classifier geometry of one stream: its own stream-scoped geometry,
3008
+ * else the packet's (fail-closed when absent, or when a sibling stream of
3009
+ * the packet has its own geometry and this one has none).
3010
+ */
3011
+ geometryFor(packetId, catalogTrackName) {
3012
+ return this.#geometryFor(packetId, catalogTrackName);
3013
+ }
3014
+ /** Current classifier mode of a stream. */
3015
+ classifierModeFor(packetId, catalogTrackName) {
3016
+ const classifier = this.#classifierFor(packetId, catalogTrackName);
3017
+ return { mode: classifier.mode, reason: classifier.reason };
3018
+ }
3019
+ /**
3020
+ * Whether an MPU boundary still bounds this stream's RAPs (see
3021
+ * `BlockClassifier.mpuBoundsRaps`). Gap decisions drop MPU evidence once
3022
+ * it does not. A stream with no classifier yet has shown nothing against it.
3023
+ */
3024
+ mpuBoundsRapsFor(packetId, catalogTrackName) {
3025
+ return this.#classifiers.get(this.#streamKey(packetId, catalogTrackName))?.mpuBoundsRaps ?? true;
3026
+ }
3027
+ /** A block's class now, or frozen at its resolution. */
3028
+ blockClassFor(sbn, packetId, catalogTrackName) {
3029
+ const frozen = this.#resolvedBlockClasses.get(this.#blockKey(sbn, packetId, catalogTrackName));
3030
+ if (frozen !== void 0) return frozen;
3031
+ return this.#peekClass(sbn, packetId, catalogTrackName);
3032
+ }
3033
+ /**
3034
+ * Lifecycle resolution of one block for in-order frame release: `pending`
3035
+ * while it can still complete, `complete`/`failed` once terminal, and
3036
+ * `untracked` when no symbol of it was ever accepted.
3037
+ */
3038
+ blockResolution(sbn, packetId, catalogTrackName) {
3039
+ return this.#blockResolutionFor(this.#blockKey(sbn, packetId, catalogTrackName));
3040
+ }
3041
+ /** Highest SBN any symbol of this stream carried in the current epoch. */
3042
+ highestSbn(packetId, catalogTrackName) {
3043
+ return this.#highestSbn.get(this.#streamKey(packetId, catalogTrackName))?.sbn;
3044
+ }
3045
+ /**
3046
+ * Record a RAP frame released in order, for the classifier's RAP-spacing
3047
+ * invariant (consecutive MPUs exactly one keyframe interval apart).
3048
+ */
3049
+ noteReleasedRap(packetId, catalogTrackName, mpu, presentationTime, mediaTimescale) {
3050
+ if (!this.#trackConfigs.has(packetId)) return;
3051
+ this.#applyClassifierChanges(
3052
+ packetId,
3053
+ catalogTrackName,
3054
+ this.#classifierFor(packetId, catalogTrackName).noteReleasedRap(
3055
+ mpu,
3056
+ presentationTime,
3057
+ mediaTimescale
3058
+ )
3059
+ );
3060
+ }
3061
+ // -----------------------------------------------------------------------
3062
+ // Configuration
3063
+ // -----------------------------------------------------------------------
3064
+ /** Configure FEC parameters. */
3065
+ configure(config) {
3066
+ if (config.k != null) this.#fecK = config.k;
3067
+ if (config.symbolSize != null && config.symbolSize > 0) this.#fecSymbolSize = config.symbolSize;
3068
+ if (config.interleaveDepthMs != null) this.#fecInterleaveDepthMs = config.interleaveDepthMs;
3069
+ if (config.fps != null && Number.isFinite(config.fps) && config.fps > 0) {
3070
+ this.#fecFps = config.fps;
3071
+ }
3072
+ if (config.interleaveDepthMs != null && config.interleaveDepthMs > 0 && config.interleaveDepthMs < MIN_INTERLEAVE_DEPTH_MS) {
3073
+ console.warn(`[FECManager] interleaveDepthMs=${config.interleaveDepthMs} looks like frame count, not milliseconds (\xA75.1 requires ms). Expected range: ${MIN_INTERLEAVE_DEPTH_MS}-10000ms.`);
3074
+ }
3075
+ if (config.timeoutMs != null && config.timeoutMs > 0) {
3076
+ this.#fecTimeoutMs = config.timeoutMs;
3077
+ } else if (config.interleaveDepthMs != null && config.interleaveDepthMs >= MIN_INTERLEAVE_DEPTH_MS) {
3078
+ this.#fecTimeoutMs = computeFecDeliveryWindowMs({
3079
+ interleaveDepthMs: config.interleaveDepthMs,
3080
+ sourceSymbols: this.#fecK,
3081
+ symbolSize: this.#fecSymbolSize,
3082
+ mediaUnitsPerSecond: config.fps
3083
+ });
3084
+ }
3085
+ for (const [packetId, track] of this.#trackConfigs) {
3086
+ if (track.timeoutMs < this.#fecTimeoutMs) {
3087
+ this.#trackConfigs.set(packetId, { ...track, timeoutMs: this.#fecTimeoutMs });
3088
+ }
3089
+ }
3090
+ }
3091
+ /**
3092
+ * Configure FEC parameters for one packetId without changing legacy
3093
+ * defaults. With `catalogTrackName`, `config.geometry` is that stream's
3094
+ * own classifier geometry (ABR rungs that share a packetId each get
3095
+ * theirs); K, T, timing, kind and codec stay per packetId.
3096
+ */
3097
+ configureTrack(packetId, config, catalogTrackName) {
3098
+ this.#upsertTrackConfig(packetId, config, catalogTrackName);
3099
+ }
3100
+ /** Register a FEC decoder for a specific packetId (track). */
3101
+ setFecDecoder(packetId, client, k, config, catalogTrackName) {
3102
+ const streamKey = this.#streamKey(packetId, catalogTrackName);
3103
+ this.#assertExclusiveDecoderClient(
3104
+ streamKey,
3105
+ client,
3106
+ [...this.#fecDecoders].map(([key, entry]) => [key, entry.client]),
3107
+ "decoder"
3108
+ );
3109
+ const trackConfig = this.#upsertTrackConfig(packetId, { ...config, k }, catalogTrackName);
3110
+ this.#fecDecoders.set(streamKey, { client, k: trackConfig.k });
3111
+ }
3112
+ /**
3113
+ * Register a Worker-backed FEC decoder for a specific packetId (track).
3114
+ *
3115
+ * Worker-backed tracks decode asynchronously — feedSourceSymbol/feedRepairSymbol
3116
+ * return null immediately, and recovered frames arrive via the onBlockRecovered callback.
3117
+ *
3118
+ * Takes precedence over sync WASM decoders for the same packetId.
3119
+ */
3120
+ setFecWorker(packetId, client, config, catalogTrackName) {
3121
+ const k = config?.k;
3122
+ if (k === void 0 || !Number.isFinite(k) || k <= 0) {
3123
+ throw new Error(`[FECManager] packetId=${packetId} Worker requires an explicit positive k`);
3124
+ }
3125
+ const symbolSize = config.symbolSize;
3126
+ if (symbolSize === void 0 || !Number.isFinite(symbolSize) || symbolSize <= 0) {
3127
+ throw new Error(
3128
+ `[FECManager] packetId=${packetId} Worker requires an explicit positive symbolSize`
3129
+ );
3130
+ }
3131
+ const streamKey = this.#streamKey(packetId, catalogTrackName);
3132
+ this.#assertExclusiveDecoderClient(
3133
+ streamKey,
3134
+ client,
3135
+ this.#workerClients,
3136
+ "worker"
3137
+ );
3138
+ const incumbent = this.#workerClients.get(streamKey);
3139
+ if (incumbent !== void 0 && incumbent !== client) {
3140
+ throw new Error(
3141
+ `[FECManager] stream ${streamKey} already routes to a Worker; call clearFecWorker before registering another`
3142
+ );
3143
+ }
3144
+ if (config) this.#upsertTrackConfig(packetId, config, catalogTrackName);
3145
+ const workerK = this.#resolveTrackConfig(packetId).k;
3146
+ this.#workerClients.set(streamKey, client);
3147
+ this.#releasedWorkerClients.delete(client);
3148
+ client.onFrame((data, meta) => {
3149
+ if (this.#releasedWorkerClients.has(client)) return;
3150
+ if (meta.recovered) {
3151
+ if (!Number.isInteger(meta.ssbLength) || meta.ssbLength <= 0 || meta.ssbLength > workerK) {
3152
+ const kind = "recovered-symbol-ssb-length-invalid";
3153
+ const seen = this.#workerErrorCounts.get(kind) ?? 0;
3154
+ this.#workerErrorCounts.set(kind, seen + 1);
3155
+ if (seen < WORKER_ERROR_LOG_CAP) {
3156
+ console.warn(
3157
+ `[FECManager] Dropping recovered symbol with invalid SSB_length: pid=${packetId} sbn=${meta.sbn} ssbLength=${String(meta.ssbLength)}`
3158
+ );
3159
+ }
3160
+ return;
3161
+ }
3162
+ if (!Number.isInteger(meta.esi) || meta.esi < 0 || meta.esi >= meta.ssbLength) {
3163
+ const kind = "recovered-symbol-missing-esi";
3164
+ const seen = this.#workerErrorCounts.get(kind) ?? 0;
3165
+ this.#workerErrorCounts.set(kind, seen + 1);
3166
+ if (seen < WORKER_ERROR_LOG_CAP) {
3167
+ console.warn(
3168
+ `[FECManager] Dropping recovered symbol without valid ESI: pid=${packetId} sbn=${meta.sbn} esi=${String(meta.esi)} ssbLength=${String(meta.ssbLength)}`
3169
+ );
3170
+ }
3171
+ return;
3172
+ }
3173
+ const esi = meta.esi;
3174
+ const recoveredCatalogTrackName = catalogTrackName ?? this.#decoderBlockCatalogTrackName(client, meta.sbn);
3175
+ if (!this.#claimDecoderBlock(client, meta.sbn, recoveredCatalogTrackName)) return;
3176
+ const blockKey = this.#blockKey(meta.sbn, packetId, recoveredCatalogTrackName);
3177
+ const cleanlyCompleted = this.#cleanlyCompletedBlocks.has(blockKey);
3178
+ if (!cleanlyCompleted) {
3179
+ if (!this.#workerRecoveredBlocks.has(blockKey)) {
3180
+ this.#traceLifecycle(
3181
+ "recovered",
3182
+ meta.sbn,
3183
+ packetId,
3184
+ recoveredCatalogTrackName
3185
+ );
3186
+ this.#workerRecoveredBlocks.add(blockKey);
3187
+ this.#fecStats.blocksRecovered++;
3188
+ this.#statsForPacket(packetId).blocksRecovered++;
3189
+ if (this.#workerRecoveredBlocks.size > 1e3) {
3190
+ dropOldest(this.#workerRecoveredBlocks, 500);
3191
+ }
3192
+ this.#noteBlockRepaired(blockKey, meta.sbn, packetId, recoveredCatalogTrackName);
3193
+ }
3194
+ const symbolKey = `${blockKey}:${esi}`;
3195
+ if (!this.#seenSymbols.has(symbolKey)) {
3196
+ this.#seenSymbols.add(symbolKey);
3197
+ this.onBlockRecovered?.(
3198
+ meta.sbn,
3199
+ packetId,
3200
+ data,
3201
+ meta,
3202
+ recoveredCatalogTrackName
3203
+ );
3204
+ }
3205
+ }
3206
+ if (esi >= meta.ssbLength - 1 && !this.#completedBlocks.has(blockKey)) {
3207
+ this.cancelBlockTimeout(meta.sbn, packetId, recoveredCatalogTrackName);
3208
+ if (recoveredCatalogTrackName === void 0) {
3209
+ this.onBlockComplete?.(meta.sbn, packetId);
3210
+ } else {
3211
+ this.onBlockComplete?.(
3212
+ meta.sbn,
3213
+ packetId,
3214
+ recoveredCatalogTrackName
3215
+ );
3216
+ }
3217
+ }
3218
+ } else {
3219
+ const completedCatalogTrackName = catalogTrackName ?? this.#decoderBlockCatalogTrackName(client, meta.sbn);
3220
+ this.markBlockCompleted(meta.sbn, packetId, completedCatalogTrackName);
3221
+ }
3222
+ });
3223
+ client.onSnapshot((stats) => {
3224
+ if (this.#releasedWorkerClients.has(client)) return;
3225
+ this.#fecWorkerStats.set(packetId, stats);
3226
+ try {
3227
+ this.onFecStatsUpdate?.(packetId, stats);
3228
+ } catch (error) {
3229
+ console.warn("[FECManager] FEC stats callback failed:", error);
3230
+ }
3231
+ for (const observer of this.#fecStatsObservers) {
3232
+ try {
3233
+ observer(packetId, stats);
3234
+ } catch (error) {
3235
+ console.warn("[FECManager] FEC stats observer failed:", error);
3236
+ }
3237
+ }
3238
+ });
3239
+ client.onCleanupComplete((sbn) => {
3240
+ if (this.#releasedWorkerClients.has(client)) return;
3241
+ const cleanupKeys = [...this.#pendingWorkerCleanups].filter((key) => {
3242
+ const block = this.#parseBlockKey(key);
3243
+ return block.packetId === packetId && block.sbn === sbn && this.#pendingWorkerCleanupClients.get(key) === client;
3244
+ });
3245
+ for (const key of cleanupKeys) this.#settleOwedCleanup(key, "cleanup-ack");
3246
+ });
3247
+ client.onError((msg, code) => {
3248
+ const requestedKind = code ?? msg;
3249
+ const kind = this.#workerErrorCounts.has(requestedKind) || this.#workerErrorCounts.size < WORKER_ERROR_KIND_CAP - 1 ? requestedKind : WORKER_ERROR_OVERFLOW_KIND;
3250
+ const seen = this.#workerErrorCounts.get(kind) ?? 0;
3251
+ this.#workerErrorCounts.set(kind, seen + 1);
3252
+ if (seen < WORKER_ERROR_LOG_CAP) {
3253
+ console.warn("[FECManager] Worker error:", msg);
3254
+ } else if (seen === WORKER_ERROR_LOG_CAP) {
3255
+ console.warn(
3256
+ `[FECManager] Worker error (suppressing further copies of ${kind}): ${msg}`
3257
+ );
3258
+ }
3259
+ });
3260
+ }
3261
+ /**
3262
+ * Remove a stream's Worker decoder from routing.
3263
+ *
3264
+ * Returns false when no Worker is registered under that exact stream key.
3265
+ * On success every cleanup acknowledgement still owed by the client is
3266
+ * resolved by failing its block now (a Worker that is going away cannot
3267
+ * post it), the client's block ownership is released, and
3268
+ * onDecoderUnavailable fires once with reason 'cleared' and the decoder
3269
+ * that remains for the stream ('sync' or 'none'). Call this before
3270
+ * disposing the client; a client disposed while still registered is
3271
+ * evicted on its next symbol or reset with reason 'disposed'.
3272
+ */
3273
+ clearFecWorker(packetId, catalogTrackName) {
3274
+ const streamKey = this.#streamKey(packetId, catalogTrackName);
3275
+ const client = this.#workerClients.get(streamKey);
3276
+ if (!client) return false;
3277
+ this.#retireWorker(streamKey, client, "cleared");
3278
+ return true;
3279
+ }
3280
+ /**
3281
+ * Take a Worker client out of routing under one stream key, fail the blocks
3282
+ * whose completion it still owed, and report the stream's decoder
3283
+ * unavailable together with whatever still decodes the stream.
3284
+ */
3285
+ #retireWorker(streamKey, client, reason) {
3286
+ this.#workerClients.delete(streamKey);
3287
+ this.#releaseWorkerClient(client);
3288
+ this.#decoderUnavailableEvents++;
3289
+ const stream = this.#parseStreamKey(streamKey);
3290
+ const fallbackDecoder = this.#decoderForPacket(stream.packetId, stream.catalogTrackName) ? "sync" : "none";
3291
+ this.onDecoderUnavailable?.({
3292
+ packetId: stream.packetId,
3293
+ ...stream.catalogTrackName === void 0 ? {} : { catalogTrackName: stream.catalogTrackName },
3294
+ reason,
3295
+ fallbackDecoder
3296
+ });
3297
+ }
3298
+ /**
3299
+ * Fail every block whose Worker cleanup acknowledgement is owed by `client`
3300
+ * and drop its block ownership, so no block waits on a message the Worker
3301
+ * will never post.
3302
+ */
3303
+ #releaseWorkerClient(client) {
3304
+ const owed = [...this.#pendingWorkerCleanupClients].filter(([, owner]) => owner === client).map(([key]) => key);
3305
+ for (const key of owed) this.#settleOwedCleanup(key, "decoder-unavailable");
3306
+ this.#decoderBlockOwners.delete(client);
3307
+ }
3308
+ /**
3309
+ * Bound the wait for one Worker cleanup acknowledgement by the block's
3310
+ * catalog-derived terminal lifetime. When it elapses with the cleanup still
3311
+ * owed to the same client, the block fails as if the ack had arrived: its
3312
+ * fail deadline already passed, so failing it is the decision that deadline
3313
+ * made. A late ack then finds nothing owed and is a no-op.
3314
+ */
3315
+ #boundOwedCleanup(key, packetId, client) {
3316
+ this.#clearOwedCleanupBound(key);
3317
+ const bound = {
3318
+ handle: setTimeout(() => {
3319
+ if (this.#pendingWorkerCleanupBounds.get(key) !== bound) return;
3320
+ this.#pendingWorkerCleanupBounds.delete(key);
3321
+ if (!this.#pendingWorkerCleanups.has(key) || this.#pendingWorkerCleanupClients.get(key) !== client) {
3322
+ return;
3323
+ }
3324
+ this.#settleOwedCleanup(key, "cleanup-ack-timeout");
3325
+ }, Math.ceil(this.#liveBlockLifetimeMs(this.#resolveTrackConfig(packetId))))
3326
+ };
3327
+ this.#pendingWorkerCleanupBounds.set(key, bound);
3328
+ }
3329
+ #clearOwedCleanupBound(key) {
3330
+ const bound = this.#pendingWorkerCleanupBounds.get(key);
3331
+ if (!bound) return;
3332
+ clearTimeout(bound.handle);
3333
+ this.#pendingWorkerCleanupBounds.delete(key);
3334
+ }
3335
+ /**
3336
+ * Resolve one owed Worker cleanup: drop the pending entry and fail the block
3337
+ * unless it already completed. Shared by the cleanup acknowledgement and by
3338
+ * Worker retirement, so the two paths cannot drift in their bookkeeping.
3339
+ */
3340
+ #settleOwedCleanup(key, event) {
3341
+ this.#clearOwedCleanupBound(key);
3342
+ this.#pendingWorkerCleanups.delete(key);
3343
+ this.#pendingWorkerCleanupClients.delete(key);
3344
+ if (this.#completedBlocks.has(key)) return;
3345
+ const block = this.#parseBlockKey(key);
3346
+ this.#traceLifecycle(event, block.sbn, block.packetId, block.catalogTrackName);
3347
+ this.#failBlock(block.sbn, block.packetId, block.catalogTrackName);
3348
+ }
3349
+ /** Evict a disposed client under every stream key that still maps to it. */
3350
+ #evictDisposedWorker(client) {
3351
+ for (const [key, registered] of [...this.#workerClients]) {
3352
+ if (registered !== client) continue;
3353
+ const stream = this.#parseStreamKey(key);
3354
+ const track = stream.catalogTrackName === void 0 ? "" : ` track=${stream.catalogTrackName}`;
3355
+ console.warn(
3356
+ `[FECManager] packetId=${stream.packetId}${track} Worker decoder was disposed while still registered; evicting it`
3357
+ );
3358
+ this.#retireWorker(key, client, "disposed");
3359
+ }
3360
+ }
3361
+ /** Called by player when framerate is known (from decoder output). Refines FEC block timeout. */
3362
+ setFramerate(fps) {
3363
+ if (this.#fpsDetected || fps <= 0) return;
3364
+ this.#fpsDetected = true;
3365
+ this.#fecFps = fps;
3366
+ if (this.#fecInterleaveDepthMs >= MIN_INTERLEAVE_DEPTH_MS) {
3367
+ this.#fecTimeoutMs = computeFecDeliveryWindowMs({
3368
+ interleaveDepthMs: this.#fecInterleaveDepthMs,
3369
+ sourceSymbols: this.#fecK,
3370
+ symbolSize: this.#fecSymbolSize,
3371
+ mediaUnitsPerSecond: fps
3372
+ });
3373
+ }
3374
+ for (const [packetId, config] of this.#trackConfigs) {
3375
+ if (config.fps !== void 0 || config.interleaveDepthMs < MIN_INTERLEAVE_DEPTH_MS) continue;
3376
+ this.#trackConfigs.set(packetId, {
3377
+ ...config,
3378
+ fps,
3379
+ timeoutMs: Math.max(
3380
+ this.#fecTimeoutMs,
3381
+ computeFecDeliveryWindowMs({
3382
+ interleaveDepthMs: config.interleaveDepthMs,
3383
+ sourceSymbols: config.k,
3384
+ symbolSize: config.symbolSize,
3385
+ mediaUnitsPerSecond: fps
3386
+ })
3387
+ )
3388
+ });
3389
+ }
3390
+ }
3391
+ // -----------------------------------------------------------------------
3392
+ // Symbol feeding (codec-agnostic)
3393
+ // -----------------------------------------------------------------------
3394
+ /**
3395
+ * Feed a source symbol to the WASM FEC decoder.
3396
+ *
3397
+ * Does NOT track block source counts or fire lifecycle events — call
3398
+ * `trackBlock()` separately BEFORE this method (MmtpRouter does this).
3399
+ *
3400
+ * @returns Recovered block data if WASM completed the block, else null.
3401
+ * Caller is responsible for routing recovered data and updating
3402
+ * block completion state via `markBlockCompleted()`.
3403
+ */
3404
+ feedSourceSymbol(packetId, ssId, symbolData, alta, catalogTrackName) {
3405
+ this.#fecStats.sourcePackets++;
3406
+ this.#statsForPacket(packetId).sourcePackets++;
3407
+ let sbn;
3408
+ let fecData;
3409
+ if (this.#isIso(packetId)) {
3410
+ const locator = this.#isoLocatorFor(packetId, catalogTrackName);
3411
+ const located = locator.locate(ssId);
3412
+ if (located === null) {
3413
+ throw new Error(
3414
+ `[FECManager] iso-ssbg1-v1 source SS_ID=${ssId} (packetId=${packetId}) has no stated block; hold it until a repair places it (placeIsoRepair)`
3415
+ );
3416
+ }
3417
+ locator.observe(ssId);
3418
+ if (!ssbg1FitsSymbol(symbolData.byteLength, this.symbolSizeForPacket(packetId))) {
3419
+ this.#isoSourceOversizeCount++;
3420
+ return null;
3421
+ }
3422
+ sbn = located.block.start;
3423
+ fecData = symbolData;
3424
+ } else {
3425
+ fecData = this.#normalizeSymbolData(packetId, symbolData);
3426
+ sbn = Math.floor(ssId / this.kForPacket(packetId));
3427
+ }
3428
+ const blockKey = this.#blockKey(sbn, packetId, catalogTrackName);
3429
+ const syncBlockOwner = this.#syncDecoderBlockOwners.get(blockKey);
3430
+ const workerClient = this.#routableWorkerForPacket(packetId, catalogTrackName);
3431
+ if (workerClient && !syncBlockOwner) {
3432
+ if (!this.#claimDecoderBlock(workerClient, sbn, catalogTrackName)) return null;
3433
+ const buf = fecData !== symbolData ? fecData.buffer : fecData.buffer.slice(
3434
+ fecData.byteOffset,
3435
+ fecData.byteOffset + fecData.byteLength
3436
+ );
3437
+ if (alta) {
3438
+ const authBuf = alta.auth.buffer.slice(
3439
+ alta.auth.byteOffset,
3440
+ alta.auth.byteOffset + alta.auth.byteLength
3441
+ );
3442
+ workerClient.feedSource(ssId, buf, performance.now(), {
3443
+ auth: authBuf,
3444
+ payloadEnd: alta.payloadEnd
3445
+ });
3446
+ } else {
3447
+ workerClient.feedSource(ssId, buf, performance.now());
3448
+ }
3449
+ return null;
3450
+ }
3451
+ const entry = this.#decoderForPacket(packetId, catalogTrackName);
3452
+ const client = syncBlockOwner ?? entry?.client;
3453
+ if (!client) return null;
3454
+ if (!this.#claimDecoderBlock(client, sbn, catalogTrackName)) return null;
3455
+ this.#syncDecoderBlockOwners.set(blockKey, client);
3456
+ let recovered = null;
3457
+ try {
3458
+ recovered = client.add_source(ssId, fecData, performance.now());
3459
+ } catch {
3460
+ }
3461
+ this.#retireIsoUnplaced(client, packetId, catalogTrackName);
3462
+ if (recovered || this.#completedBlocks.has(blockKey)) {
3463
+ this.#syncDecoderBlockOwners.delete(blockKey);
3464
+ }
3465
+ return recovered;
3466
+ }
3467
+ /**
3468
+ * Feed a repair symbol to the WASM FEC decoder.
3469
+ *
3470
+ * @returns Recovered block data if repair completed the block, else null.
3471
+ * Caller is responsible for routing recovered data and updating
3472
+ * block completion state via `markBlockCompleted()`.
3473
+ */
3474
+ feedRepairSymbol(packetId, ssStart, ssbLength, rsId, repairData, catalogTrackName) {
3475
+ this.#fecStats.repairPackets++;
3476
+ this.#statsForPacket(packetId).repairPackets++;
3477
+ const symbolSize = this.symbolSizeForPacket(packetId);
3478
+ if (symbolSize > 0 && repairData.byteLength !== symbolSize) {
3479
+ this.#repairSymbolSizeMismatchCount++;
3480
+ if (this.#repairSymbolSizeMismatchCount <= 5 || this.#repairSymbolSizeMismatchCount % 100 === 0) {
3481
+ console.warn(
3482
+ `[FECManager] Dropping repair symbol with invalid size: got ${repairData.byteLength}B, expected ${symbolSize}B (packetId=${packetId}, ssStart=${ssStart}, ssbLength=${ssbLength}, rsId=${rsId})`
3483
+ );
3484
+ }
3485
+ return null;
3486
+ }
3487
+ const fecData = repairData;
3488
+ const k = this.kForPacket(packetId);
3489
+ let sbn;
3490
+ if (this.#isIso(packetId)) {
3491
+ const block = this.#isoLocatorFor(packetId, catalogTrackName).blockAt(ssStart);
3492
+ if (ssbLength <= 0 || ssbLength > k || block === void 0 || block.span || block.length !== ssbLength) {
3493
+ return null;
3494
+ }
3495
+ sbn = ssStart;
3496
+ } else {
3497
+ if (ssbLength <= 0 || ssbLength > k || ssStart % k !== 0) return null;
3498
+ sbn = Math.floor(ssStart / k);
3499
+ }
3500
+ this.#noteSbnSeen(sbn, packetId, catalogTrackName, "repair");
3501
+ this.noteBlockActivity(sbn, packetId, catalogTrackName);
3502
+ const blockKey = this.#blockKey(sbn, packetId, catalogTrackName);
3503
+ if (this.#completedBlocks.has(blockKey)) return null;
3504
+ if (!this.#noteAuthoredLength(blockKey, sbn, packetId, ssbLength, catalogTrackName)) {
3505
+ return null;
3506
+ }
3507
+ let repairIds = this.#blockRepairRsIds.get(blockKey);
3508
+ if (!repairIds) {
3509
+ repairIds = /* @__PURE__ */ new Set();
3510
+ this.#blockRepairRsIds.set(blockKey, repairIds);
3511
+ }
3512
+ repairIds.add(rsId);
3513
+ if (this.#repairRequestedBlocks.has(blockKey)) {
3514
+ this.#traceLifecycle("repair-arrived", sbn, packetId, catalogTrackName, rsId);
3515
+ }
3516
+ const cleanOnArrival = (this.#blockSourceCounts.get(blockKey) ?? 0) >= ssbLength;
3517
+ const recovered = this.#dispatchRepairSymbol(
3518
+ blockKey,
3519
+ sbn,
3520
+ packetId,
3521
+ ssStart,
3522
+ ssbLength,
3523
+ rsId,
3524
+ fecData,
3525
+ catalogTrackName
3526
+ );
3527
+ if (cleanOnArrival) {
3528
+ this.#completeCleanBlock(blockKey, sbn, packetId, catalogTrackName);
3529
+ return null;
3530
+ }
3531
+ return recovered;
3532
+ }
3533
+ /**
3534
+ * Record a block's authored SSB_length from a repair symbol. Returns false,
3535
+ * leaving the block unchanged, when the symbol contradicts the block: a
3536
+ * different SSB_length than an earlier repair symbol declared is dropped,
3537
+ * and an SSB_length that places an already-received source ESI inside the
3538
+ * zero padding fails the block, since its geometry can no longer be trusted.
3539
+ */
3540
+ #noteAuthoredLength(blockKey, sbn, packetId, ssbLength, catalogTrackName) {
3541
+ const known = this.#blockAuthoredLengths.get(blockKey);
3542
+ if (known === ssbLength) return true;
3543
+ if (known !== void 0) {
3544
+ this.#ssbLengthConflictCount++;
3545
+ if (this.#ssbLengthConflictCount <= 5 || this.#ssbLengthConflictCount % 100 === 0) {
3546
+ console.warn(
3547
+ `[FECManager] Dropping repair symbol whose SSB_length ${ssbLength} conflicts with ${known} for packetId=${packetId} sbn=${sbn}`
3548
+ );
3549
+ }
3550
+ return false;
3551
+ }
3552
+ const sourceEsis = this.#blockSourceEsis.get(blockKey);
3553
+ if (sourceEsis && [...sourceEsis].some((esi) => esi >= ssbLength)) {
3554
+ this.#failBlock(sbn, packetId, catalogTrackName);
3555
+ return false;
3556
+ }
3557
+ this.#blockAuthoredLengths.set(blockKey, ssbLength);
3558
+ return true;
3559
+ }
3560
+ #dispatchRepairSymbol(blockKey, sbn, packetId, ssStart, ssbLength, rsId, fecData, catalogTrackName) {
3561
+ const syncBlockOwner = this.#syncDecoderBlockOwners.get(blockKey);
3562
+ const workerClient = this.#routableWorkerForPacket(packetId, catalogTrackName);
3563
+ if (workerClient && !syncBlockOwner) {
3564
+ if (!this.#claimDecoderBlock(workerClient, sbn, catalogTrackName)) return null;
3565
+ const buf = fecData.buffer.slice(
3566
+ fecData.byteOffset,
3567
+ fecData.byteOffset + fecData.byteLength
3568
+ );
3569
+ workerClient.feedRepair(ssStart, ssbLength, rsId, buf, performance.now());
3570
+ return null;
3571
+ }
3572
+ const entry = this.#decoderForPacket(packetId, catalogTrackName);
3573
+ const client = syncBlockOwner ?? entry?.client;
3574
+ if (!client) return null;
3575
+ if (!this.#claimDecoderBlock(client, sbn, catalogTrackName)) return null;
3576
+ this.#syncDecoderBlockOwners.set(blockKey, client);
3577
+ let recovered = null;
3578
+ try {
3579
+ recovered = client.add_repair(ssStart, ssbLength, rsId, fecData, performance.now());
3580
+ } catch {
3581
+ }
3582
+ this.#retireIsoUnplaced(client, packetId, catalogTrackName);
3583
+ if (recovered) this.#syncDecoderBlockOwners.delete(blockKey);
3584
+ return recovered;
3585
+ }
3586
+ #routeSyncCleanup(decoder, sbn, packetId, catalogTrackName) {
3587
+ try {
3588
+ decoder.cleanup_blocks_detailed(Uint32Array.of(sbn));
3589
+ } catch (error) {
3590
+ console.warn(`[FECManager] Scoped cleanup failed for SBN ${sbn}:`, error);
3591
+ }
3592
+ this.#retireIsoUnplaced(decoder, packetId, catalogTrackName);
3593
+ }
3594
+ /**
3595
+ * iso-ssbg1-v1: a placed source the decoder took before its block's repair
3596
+ * (fed in wire order, sources first) waits unplaced in the decoder; one
3597
+ * whose repair never reaches it, such as a source of a repair-less span,
3598
+ * would stay there. Retire those by the decoder's horizon behind the newest
3599
+ * SS_ID the stream's locator has seen. Run after every feed that grows the
3600
+ * decoder and when a block is retired, as the fec-worker sweeps its own
3601
+ * pool: a stream that loses only repair symbols never expires a block, so
3602
+ * a sweep tied to expiry alone would never run there.
3603
+ */
3604
+ #retireIsoUnplaced(decoder, packetId, catalogTrackName) {
3605
+ if (!this.#isIso(packetId)) return;
3606
+ const newest = this.#isoLocators.get(this.#streamKey(packetId, catalogTrackName))?.newest;
3607
+ if (newest === void 0) return;
3608
+ try {
3609
+ this.#isoDecoderUnplacedRetired += decoder.retire_unplaced(newest);
3610
+ } catch (error) {
3611
+ this.#isoDecoderUnplacedRetireFailed++;
3612
+ const failed = this.#isoDecoderUnplacedRetireFailed;
3613
+ if (failed <= 5 || failed % 100 === 0) {
3614
+ console.warn(
3615
+ `[FECManager] iso-ssbg1-v1 unplaced retirement failed for packetId=${packetId} (${failed} total):`,
3616
+ error
3617
+ );
3618
+ }
3619
+ }
3620
+ }
3621
+ // -----------------------------------------------------------------------
3622
+ // Block lifecycle
3623
+ // -----------------------------------------------------------------------
3624
+ /**
3625
+ * Track a first-arrival network source symbol for an FEC block. Refreshes
3626
+ * the inactivity deadline on each symbol, records the symbol's in-band
3627
+ * classification evidence, and marks the block clean when all of its
3628
+ * authored source symbols arrived: the SSB_length a repair symbol declared,
3629
+ * otherwise K.
3630
+ *
3631
+ * Throws for a packetId that was never configured, so a classified block
3632
+ * never reads the manager-global K or timeout.
3633
+ */
3634
+ trackBlock(sbn, packetId, catalogTrackName, esi, evidence) {
3635
+ const trackConfig = this.#trackConfigs.get(packetId);
3636
+ if (trackConfig === void 0) {
3637
+ throw new Error(
3638
+ `[FECManager] trackBlock for packetId=${packetId}, which was never configured; configure the track from its catalog (configureTrack/setFecWorker/setFecDecoder) first`
3639
+ );
3640
+ }
3641
+ const key = this.#blockKey(sbn, packetId, catalogTrackName);
3642
+ const k = trackConfig.k;
3643
+ if (this.#completedBlocks.has(key)) return;
3644
+ this.#noteSbnSeen(sbn, packetId, catalogTrackName, "source", evidence.packetSequenceNumber);
3645
+ const authoredLength = this.#blockAuthoredLengths.get(key) ?? k;
3646
+ if (Number.isInteger(esi) && esi >= authoredLength) {
3647
+ this.#failBlock(sbn, packetId, catalogTrackName);
3648
+ return;
3649
+ }
3650
+ this.#applyClassifierChanges(
3651
+ packetId,
3652
+ catalogTrackName,
3653
+ this.#classifierFor(packetId, catalogTrackName).observe(sbn, evidence)
3654
+ );
3655
+ const count = (this.#blockSourceCounts.get(key) ?? 0) + 1;
3656
+ this.#blockSourceCounts.set(key, count);
3657
+ if (count === 1) this.#indexBlockKey(key, sbn, packetId);
3658
+ if (Number.isInteger(esi) && esi >= 0 && esi < k) {
3659
+ let sourceEsis = this.#blockSourceEsis.get(key);
3660
+ if (!sourceEsis) {
3661
+ sourceEsis = /* @__PURE__ */ new Set();
3662
+ this.#blockSourceEsis.set(key, sourceEsis);
3663
+ }
3664
+ sourceEsis.add(esi);
3665
+ }
3666
+ if (count === 1) {
3667
+ if (catalogTrackName === void 0) {
3668
+ this.onBlockStarted?.(sbn, packetId);
3669
+ } else {
3670
+ this.onBlockStarted?.(sbn, packetId, catalogTrackName);
3671
+ }
3672
+ }
3673
+ if (k > 0 && count >= authoredLength) {
3674
+ this.#completeCleanBlock(key, sbn, packetId, catalogTrackName);
3675
+ return;
3676
+ }
3677
+ this.noteBlockActivity(sbn, packetId, catalogTrackName, true);
3678
+ }
3679
+ #completeCleanBlock(key, sbn, packetId, catalogTrackName) {
3680
+ this.#fecStats.blocksClean++;
3681
+ this.#statsForPacket(packetId).blocksClean++;
3682
+ this.cancelBlockTimeout(sbn, packetId, catalogTrackName);
3683
+ this.#blockSourceCounts.delete(key);
3684
+ this.#blockSourceEsis.delete(key);
3685
+ this.#blockAuthoredLengths.delete(key);
3686
+ this.#blockRepairRsIds.delete(key);
3687
+ this.#markTerminal(key, sbn, packetId, true);
3688
+ this.#markPathBlockComplete(sbn, packetId, catalogTrackName);
3689
+ if (catalogTrackName === void 0) {
3690
+ this.onBlockComplete?.(sbn, packetId);
3691
+ } else {
3692
+ this.onBlockComplete?.(sbn, packetId, catalogTrackName);
3693
+ }
3694
+ queueMicrotask(() => this.#syncDecoderBlockOwners.delete(key));
3695
+ }
3696
+ /** Refresh deadlines after accepted block activity. */
3697
+ noteBlockActivity(sbn, packetId, catalogTrackName, sourceProgress = false) {
3698
+ const key = this.#blockKey(sbn, packetId, catalogTrackName);
3699
+ if (this.#completedBlocks.has(key)) return;
3700
+ if (this.#pendingWorkerCleanups.has(key)) return;
3701
+ const repairRequested = this.#repairRequestedBlocks.has(key);
3702
+ if (repairRequested && this.#settlesByOrder(key)) return;
3703
+ if (this.#settlesByOrder(key)) {
3704
+ this.#holdForOrderedSettlement(key, sbn, packetId, catalogTrackName);
3705
+ return;
3706
+ }
3707
+ const trackConfig = this.#resolveTrackConfig(packetId);
3708
+ const timeoutMs = this.#multicastWaitMs(trackConfig);
3709
+ if (!(timeoutMs > 0)) {
3710
+ throw new Error(`[FECManager] packetId=${packetId} has no positive fail bound (timeoutMs=${timeoutMs})`);
3711
+ }
3712
+ const repairDeadline = this.#repairDeadlines.get(key);
3713
+ if (this.onRepairNeeded && !repairRequested && (repairDeadline === void 0 || sourceProgress)) {
3714
+ this.#armRepairDeadline(
3715
+ key,
3716
+ sbn,
3717
+ packetId,
3718
+ catalogTrackName,
3719
+ this.#repairDeadlineMs(trackConfig)
3720
+ );
3721
+ }
3722
+ this.#armBlockTimeout(sbn, packetId, catalogTrackName, timeoutMs);
3723
+ }
3724
+ #failBlock(sbn, packetId, catalogTrackName) {
3725
+ const key = this.#blockKey(sbn, packetId, catalogTrackName);
3726
+ this.#traceLifecycle("failed", sbn, packetId, catalogTrackName);
3727
+ this.#clearRepairDeadline(key);
3728
+ this.#clearBlockTimer(key);
3729
+ const blockClass = this.#retireBlockClass(key, sbn, packetId, catalogTrackName);
3730
+ if (this.#repairRequestedBlocks.delete(key)) {
3731
+ this.onRepairResolved?.(sbn, packetId, "failed", catalogTrackName, null);
3732
+ }
3733
+ this.#blockSourceCounts.delete(key);
3734
+ this.#blockSourceEsis.delete(key);
3735
+ this.#blockAuthoredLengths.delete(key);
3736
+ this.#blockRepairRsIds.delete(key);
3737
+ this.#syncDecoderBlockOwners.delete(key);
3738
+ this.#markTerminal(key, sbn, packetId, false);
3739
+ this.#failedBlocks.add(key);
3740
+ this.#markPathBlockComplete(sbn, packetId, catalogTrackName);
3741
+ this.#fecStats.blocksFailed++;
3742
+ this.#statsForPacket(packetId).blocksFailed++;
3743
+ this.onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass);
3744
+ this.onBlockResolved?.(sbn, packetId, catalogTrackName);
3745
+ }
3746
+ /**
3747
+ * Resolve a block as completed (clean, recovered, or externally marked).
3748
+ * A block that had a D-28 repair outstanding counts as repaired: it was
3749
+ * short of its source at the repair deadline and completed anyway.
3750
+ */
3751
+ #resolveCompleted(key) {
3752
+ const block = this.#parseBlockKey(key);
3753
+ this.#markTerminal(key, block.sbn, block.packetId, false);
3754
+ this.#clearOwedCleanupBound(key);
3755
+ this.#pendingWorkerCleanups.delete(key);
3756
+ this.#pendingWorkerCleanupClients.delete(key);
3757
+ this.#markPathBlockComplete(block.sbn, block.packetId, block.catalogTrackName);
3758
+ this.#clearRepairDeadline(key);
3759
+ if (this.#repairRequestedBlocks.delete(key)) {
3760
+ this.#noteBlockRepaired(key, block.sbn, block.packetId, block.catalogTrackName);
3761
+ this.onRepairResolved?.(
3762
+ block.sbn,
3763
+ block.packetId,
3764
+ "completed",
3765
+ block.catalogTrackName,
3766
+ this.#viaFor(key)
3767
+ );
3768
+ }
3769
+ this.#retireBlockClass(key, block.sbn, block.packetId, block.catalogTrackName);
3770
+ this.#blockSourceCounts.delete(key);
3771
+ this.#blockSourceEsis.delete(key);
3772
+ this.#blockAuthoredLengths.delete(key);
3773
+ this.#blockRepairRsIds.delete(key);
3774
+ this.#clearBlockTimer(key);
3775
+ this.onBlockResolved?.(block.sbn, block.packetId, block.catalogTrackName);
3776
+ }
3777
+ /** Check whether a block has already been completed (clean or recovered). */
3778
+ isBlockCompleted(sbn, packetId, catalogTrackName) {
3779
+ return this.#blockKeysFor(sbn, packetId, catalogTrackName).some((key) => this.#completedBlocks.has(key));
3780
+ }
3781
+ /** Mark a block as completed externally (e.g., after routing recovered data). */
3782
+ markBlockCompleted(sbn, packetId, catalogTrackName) {
3783
+ for (const key of this.#blockKeysFor(sbn, packetId, catalogTrackName)) {
3784
+ this.#resolveCompleted(key);
3785
+ }
3786
+ }
3787
+ /**
3788
+ * Increment blocksRecovered (called by MmtpRouter after routing a
3789
+ * synchronous recovery). With the block's coordinates the recovery is also
3790
+ * reported through onBlockRepaired, once per block.
3791
+ */
3792
+ countRecovery(packetId, sbn, catalogTrackName) {
3793
+ this.#fecStats.blocksRecovered++;
3794
+ if (packetId != null) this.#statsForPacket(packetId).blocksRecovered++;
3795
+ if (packetId != null && sbn != null) {
3796
+ this.#noteBlockRepaired(
3797
+ this.#blockKey(sbn, packetId, catalogTrackName),
3798
+ sbn,
3799
+ packetId,
3800
+ catalogTrackName
3801
+ );
3802
+ }
3803
+ }
3804
+ /**
3805
+ * Strict delivery provenance for one FEC block.
3806
+ *
3807
+ * A recovered symbol is a function of every symbol that entered its block,
3808
+ * so it can only be attributed to multicast when NO symbol in that block
3809
+ * arrived over MoQ. Repair symbols routinely arrive over reliable QUIC
3810
+ * (`MmtpRouter.feedRepairPacket`), so a block that recovered with MoQ help
3811
+ * produced bytes that never crossed a multicast socket.
3812
+ *
3813
+ * An untracked block returns 'unknown' rather than a guess: absence of
3814
+ * evidence is not evidence of multicast.
3815
+ */
3816
+ blockDeliveryProvenance(sbn, packetId, catalogTrackName) {
3817
+ const counts = this.blockPathCount(sbn, packetId, catalogTrackName);
3818
+ if (!counts) return "unknown";
3819
+ if (counts.moqSymbols > 0) return "mixed";
3820
+ if (counts.multicastSymbols > 0) return "multicast";
3821
+ return "unknown";
3822
+ }
3823
+ /** Per-path symbol counts for one block (D-28 gate logging), if tracked. */
3824
+ blockPathCount(sbn, packetId, catalogTrackName) {
3825
+ return this.#blockPathCounts.get(
3826
+ this.#blockKey(sbn, packetId, catalogTrackName)
3827
+ ) ?? null;
3828
+ }
3829
+ /** Cancel block timeout (block completed via clean reception or FEC recovery). */
3830
+ cancelBlockTimeout(sbn, packetId, catalogTrackName) {
3831
+ for (const key of this.#blockKeysFor(sbn, packetId, catalogTrackName)) {
3832
+ this.#resolveCompleted(key);
3833
+ }
3834
+ }
3835
+ // -----------------------------------------------------------------------
3836
+ // Cleanup
3837
+ // -----------------------------------------------------------------------
3838
+ dispose() {
3839
+ for (const timer of this.#activeBlocks.values()) {
3840
+ if (timer.handle != null) clearTimeout(timer.handle);
3841
+ }
3842
+ this.#activeBlocks.clear();
3843
+ this.#activeBlockDeadlinesMs.clear();
3844
+ this.#blockWaitDeadlinesMs.clear();
3845
+ this.#repairHoldUntilMs.clear();
3846
+ this.#deliveryGaps.reset();
3847
+ this.#pendingWorkerCleanups.clear();
3848
+ this.#pendingWorkerCleanupClients.clear();
3849
+ for (const bound of this.#pendingWorkerCleanupBounds.values()) clearTimeout(bound.handle);
3850
+ this.#pendingWorkerCleanupBounds.clear();
3851
+ for (const timer of this.#repairDeadlines.values()) {
3852
+ if (timer?.handle != null) clearTimeout(timer.handle);
3853
+ }
3854
+ this.#repairDeadlines.clear();
3855
+ this.#repairDeadlineAtMs.clear();
3856
+ this.#terminalExpiryMs.clear();
3857
+ this.#terminalQueues.clear();
3858
+ this.#blockKeysBySbn.clear();
3859
+ this.#blockSourceCounts.clear();
3860
+ this.#blockSourceEsis.clear();
3861
+ this.#blockAuthoredLengths.clear();
3862
+ this.#blockRepairRsIds.clear();
3863
+ this.#completedBlocks.clear();
3864
+ this.#cleanlyCompletedBlocks.clear();
3865
+ this.#repairRequestedBlocks.clear();
3866
+ this.#workerRecoveredBlocks.clear();
3867
+ this.#syncDecoderBlockOwners.clear();
3868
+ this.#startupAlignedPackets.clear();
3869
+ this.#startupPartialBlocks.clear();
3870
+ this.#startupRequireRepairReady = false;
3871
+ this.#startupRepairReadyPackets.clear();
3872
+ this.#startupRepairBoundaryCounts.clear();
3873
+ this.#startupRepairFailOpenPackets.clear();
3874
+ this.#orderedFrontiers.clear();
3875
+ this.#orderedPendingBlocks.clear();
3876
+ this.#seenSymbols.clear();
3877
+ this.#blockPathCounts.clear();
3878
+ this.#blockAnchorsMs.clear();
3879
+ this.#blockCatalogTrackNames.clear();
3880
+ this.#classifiers.clear();
3881
+ this.#streamGeometries.clear();
3882
+ this.#failedBlocks.clear();
3883
+ this.#resolvedBlockClasses.clear();
3884
+ this.#repairedBlocks.clear();
3885
+ this.#materializedBlocks.clear();
3886
+ this.#highestSbn.clear();
3887
+ this.#isoLocators.clear();
3888
+ this.#sourceAnchors.clear();
3889
+ this.#trackConfigs.clear();
3890
+ this.#fecDecoders.clear();
3891
+ for (const client of new Set(this.#workerClients.values())) client.dispose();
3892
+ this.#workerClients.clear();
3893
+ }
3894
+ };
3895
+ export {
3896
+ FECManager,
3897
+ MIN_INTERLEAVE_DEPTH_MS
3898
+ };
3899
+ //# sourceMappingURL=fec-manager.js.map