@blockcast/mmt-container 0.1.0-main.0399efef1f07

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 (694) hide show
  1. package/README.md +432 -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 +93 -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/browser-runtime.js +20087 -0
  22. package/dist/browser-runtime.js.map +7 -0
  23. package/dist/carp-catalog.d.ts +318 -0
  24. package/dist/carp-catalog.d.ts.map +1 -0
  25. package/dist/carp-catalog.js +873 -0
  26. package/dist/carp-catalog.js.map +7 -0
  27. package/dist/catalog-fec-params.d.ts +202 -0
  28. package/dist/catalog-fec-params.d.ts.map +1 -0
  29. package/dist/catalog-fec-params.js +568 -0
  30. package/dist/catalog-fec-params.js.map +7 -0
  31. package/dist/catalog-network-source.d.ts +19 -0
  32. package/dist/catalog-network-source.d.ts.map +1 -0
  33. package/dist/catalog-network-source.js +22 -0
  34. package/dist/catalog-network-source.js.map +7 -0
  35. package/dist/catalog-validator.d.ts +18 -0
  36. package/dist/catalog-validator.d.ts.map +1 -0
  37. package/dist/catalog-validator.js +7747 -0
  38. package/dist/catalog-validator.js.map +7 -0
  39. package/dist/cmaf-block-assembler.d.ts +217 -0
  40. package/dist/cmaf-block-assembler.d.ts.map +1 -0
  41. package/dist/cmaf-block-assembler.js +1769 -0
  42. package/dist/cmaf-block-assembler.js.map +7 -0
  43. package/dist/cmaf-group-batcher.d.ts +38 -0
  44. package/dist/cmaf-group-batcher.d.ts.map +1 -0
  45. package/dist/cmaf-group-batcher.js +195 -0
  46. package/dist/cmaf-group-batcher.js.map +7 -0
  47. package/dist/cmcd-producer.d.ts +104 -0
  48. package/dist/cmcd-producer.d.ts.map +1 -0
  49. package/dist/cmcd-producer.js +207 -0
  50. package/dist/cmcd-producer.js.map +7 -0
  51. package/dist/codec-utils.d.ts +248 -0
  52. package/dist/codec-utils.d.ts.map +1 -0
  53. package/dist/codec-utils.js +1268 -0
  54. package/dist/codec-utils.js.map +7 -0
  55. package/dist/fec-codec.d.ts +32 -0
  56. package/dist/fec-codec.d.ts.map +1 -0
  57. package/dist/fec-codec.js +47 -0
  58. package/dist/fec-codec.js.map +7 -0
  59. package/dist/fec-manager.d.ts +307 -0
  60. package/dist/fec-manager.d.ts.map +1 -0
  61. package/dist/fec-manager.js +1530 -0
  62. package/dist/fec-manager.js.map +7 -0
  63. package/dist/fec-starvation-detector.d.ts +59 -0
  64. package/dist/fec-starvation-detector.d.ts.map +1 -0
  65. package/dist/fec-starvation-detector.js +75 -0
  66. package/dist/fec-starvation-detector.js.map +7 -0
  67. package/dist/fec-worker-setup.d.ts +88 -0
  68. package/dist/fec-worker-setup.d.ts.map +1 -0
  69. package/dist/fec-worker-setup.js +614 -0
  70. package/dist/fec-worker-setup.js.map +7 -0
  71. package/dist/fmp4-completion.d.ts +30 -0
  72. package/dist/fmp4-completion.d.ts.map +1 -0
  73. package/dist/fmp4-completion.js +21 -0
  74. package/dist/fmp4-completion.js.map +7 -0
  75. package/dist/fmp4-reader.d.ts +37 -0
  76. package/dist/fmp4-reader.d.ts.map +1 -0
  77. package/dist/fmp4-reader.js +100 -0
  78. package/dist/fmp4-reader.js.map +7 -0
  79. package/dist/index.d.ts +47 -0
  80. package/dist/index.d.ts.map +1 -0
  81. package/dist/index.js +380 -0
  82. package/dist/index.js.map +7 -0
  83. package/dist/iso-bmff-timing.d.ts +78 -0
  84. package/dist/iso-bmff-timing.d.ts.map +1 -0
  85. package/dist/iso-bmff-timing.js +618 -0
  86. package/dist/iso-bmff-timing.js.map +7 -0
  87. package/dist/loc.d.ts +68 -0
  88. package/dist/loc.d.ts.map +1 -0
  89. package/dist/loc.js +143 -0
  90. package/dist/loc.js.map +7 -0
  91. package/dist/manifest-auth-plan.d.ts +30 -0
  92. package/dist/manifest-auth-plan.d.ts.map +1 -0
  93. package/dist/manifest-auth-plan.js +1285 -0
  94. package/dist/manifest-auth-plan.js.map +7 -0
  95. package/dist/media-router.d.ts +251 -0
  96. package/dist/media-router.d.ts.map +1 -0
  97. package/dist/media-router.js +624 -0
  98. package/dist/media-router.js.map +7 -0
  99. package/dist/mfu-reassembler.d.ts +396 -0
  100. package/dist/mfu-reassembler.d.ts.map +1 -0
  101. package/dist/mfu-reassembler.js +3085 -0
  102. package/dist/mfu-reassembler.js.map +7 -0
  103. package/dist/mfu-reassembly-policy.d.ts +24 -0
  104. package/dist/mfu-reassembly-policy.d.ts.map +1 -0
  105. package/dist/mfu-reassembly-policy.js +99 -0
  106. package/dist/mfu-reassembly-policy.js.map +7 -0
  107. package/dist/mmtp-container-parser.d.ts +434 -0
  108. package/dist/mmtp-container-parser.d.ts.map +1 -0
  109. package/dist/mmtp-container-parser.js +6164 -0
  110. package/dist/mmtp-container-parser.js.map +7 -0
  111. package/dist/mmtp-container-parser.no-modules.js +8999 -0
  112. package/dist/mmtp-container-parser.no-modules.js.map +7 -0
  113. package/dist/mmtp-decode-pipeline.d.ts +132 -0
  114. package/dist/mmtp-decode-pipeline.d.ts.map +1 -0
  115. package/dist/mmtp-decode-pipeline.js +6413 -0
  116. package/dist/mmtp-decode-pipeline.js.map +7 -0
  117. package/dist/mmtp-router.d.ts +390 -0
  118. package/dist/mmtp-router.d.ts.map +1 -0
  119. package/dist/mmtp-router.js +9242 -0
  120. package/dist/mmtp-router.js.map +7 -0
  121. package/dist/mmtp-to-cmaf.d.ts +181 -0
  122. package/dist/mmtp-to-cmaf.d.ts.map +1 -0
  123. package/dist/mmtp-to-cmaf.js +802 -0
  124. package/dist/mmtp-to-cmaf.js.map +7 -0
  125. package/dist/multicast-auth-catalog.d.ts +115 -0
  126. package/dist/multicast-auth-catalog.d.ts.map +1 -0
  127. package/dist/multicast-auth-catalog.js +202 -0
  128. package/dist/multicast-auth-catalog.js.map +7 -0
  129. package/dist/multicast-input.d.ts +107 -0
  130. package/dist/multicast-input.d.ts.map +1 -0
  131. package/dist/multicast-input.js +256 -0
  132. package/dist/multicast-input.js.map +7 -0
  133. package/dist/multicast-manager.d.ts +369 -0
  134. package/dist/multicast-manager.d.ts.map +1 -0
  135. package/dist/multicast-manager.js +13400 -0
  136. package/dist/multicast-manager.js.map +7 -0
  137. package/dist/multicast-transport.d.ts +61 -0
  138. package/dist/multicast-transport.d.ts.map +1 -0
  139. package/dist/multicast-transport.js +1665 -0
  140. package/dist/multicast-transport.js.map +7 -0
  141. package/dist/multicast-unwrap.d.ts +57 -0
  142. package/dist/multicast-unwrap.d.ts.map +1 -0
  143. package/dist/multicast-unwrap.js +384 -0
  144. package/dist/multicast-unwrap.js.map +7 -0
  145. package/dist/no-modules.d.ts +9 -0
  146. package/dist/no-modules.d.ts.map +1 -0
  147. package/dist/no-modules.js +8965 -0
  148. package/dist/no-modules.js.map +7 -0
  149. package/dist/phase14-catalog-override.d.ts +126 -0
  150. package/dist/phase14-catalog-override.d.ts.map +1 -0
  151. package/dist/phase14-catalog-override.js +155 -0
  152. package/dist/phase14-catalog-override.js.map +7 -0
  153. package/dist/repair-fetch.d.ts +58 -0
  154. package/dist/repair-fetch.d.ts.map +1 -0
  155. package/dist/repair-fetch.js +127 -0
  156. package/dist/repair-fetch.js.map +7 -0
  157. package/dist/repair-layers.d.ts +141 -0
  158. package/dist/repair-layers.d.ts.map +1 -0
  159. package/dist/repair-layers.js +303 -0
  160. package/dist/repair-layers.js.map +7 -0
  161. package/dist/sample-combiner.d.ts +212 -0
  162. package/dist/sample-combiner.d.ts.map +1 -0
  163. package/dist/sample-combiner.js +616 -0
  164. package/dist/sample-combiner.js.map +7 -0
  165. package/dist/signaling.d.ts +29 -0
  166. package/dist/signaling.d.ts.map +1 -0
  167. package/dist/signaling.js +232 -0
  168. package/dist/signaling.js.map +7 -0
  169. package/dist/source-auth.d.ts +2 -0
  170. package/dist/source-auth.d.ts.map +1 -0
  171. package/dist/source-auth.js +1238 -0
  172. package/dist/source-auth.js.map +7 -0
  173. package/dist/switching-set-alignment.d.ts +61 -0
  174. package/dist/switching-set-alignment.d.ts.map +1 -0
  175. package/dist/switching-set-alignment.js +85 -0
  176. package/dist/switching-set-alignment.js.map +7 -0
  177. package/dist/timestamp-codec.d.ts +34 -0
  178. package/dist/timestamp-codec.d.ts.map +1 -0
  179. package/dist/timestamp-codec.js +456 -0
  180. package/dist/timestamp-codec.js.map +7 -0
  181. package/dist/track-pipeline-setup.d.ts +72 -0
  182. package/dist/track-pipeline-setup.d.ts.map +1 -0
  183. package/dist/track-pipeline-setup.js +306 -0
  184. package/dist/track-pipeline-setup.js.map +7 -0
  185. package/dist/track-pipeline.d.ts +121 -0
  186. package/dist/track-pipeline.d.ts.map +1 -0
  187. package/dist/track-pipeline.js +229 -0
  188. package/dist/track-pipeline.js.map +7 -0
  189. package/dist/track-session.d.ts +398 -0
  190. package/dist/track-session.d.ts.map +1 -0
  191. package/dist/track-session.js +767 -0
  192. package/dist/track-session.js.map +7 -0
  193. package/dist/transport-pool.d.ts +69 -0
  194. package/dist/transport-pool.d.ts.map +1 -0
  195. package/dist/transport-pool.js +88 -0
  196. package/dist/transport-pool.js.map +7 -0
  197. package/dist/wasm-mfu-reassembler.d.ts +92 -0
  198. package/dist/wasm-mfu-reassembler.d.ts.map +1 -0
  199. package/dist/wasm-mfu-reassembler.js +2139 -0
  200. package/dist/wasm-mfu-reassembler.js.map +7 -0
  201. package/dist/wasm-mmtp-container-parser.d.ts +10 -0
  202. package/dist/wasm-mmtp-container-parser.d.ts.map +1 -0
  203. package/dist/wasm-mmtp-container-parser.js +6418 -0
  204. package/dist/wasm-mmtp-container-parser.js.map +7 -0
  205. package/node_modules/@blockcast/fec-worker/README.md +213 -0
  206. package/node_modules/@blockcast/fec-worker/dist/.build-stamp +0 -0
  207. package/node_modules/@blockcast/fec-worker/dist/fec-worker-client.d.ts +87 -0
  208. package/node_modules/@blockcast/fec-worker/dist/fec-worker-client.d.ts.map +1 -0
  209. package/node_modules/@blockcast/fec-worker/dist/fec-worker-client.js +279 -0
  210. package/node_modules/@blockcast/fec-worker/dist/fec-worker-client.js.map +1 -0
  211. package/node_modules/@blockcast/fec-worker/dist/fec-worker-types.d.ts +252 -0
  212. package/node_modules/@blockcast/fec-worker/dist/fec-worker-types.d.ts.map +1 -0
  213. package/node_modules/@blockcast/fec-worker/dist/fec-worker-types.js +11 -0
  214. package/node_modules/@blockcast/fec-worker/dist/fec-worker-types.js.map +1 -0
  215. package/node_modules/@blockcast/fec-worker/dist/fec-worker.d.ts +14 -0
  216. package/node_modules/@blockcast/fec-worker/dist/fec-worker.d.ts.map +1 -0
  217. package/node_modules/@blockcast/fec-worker/dist/fec-worker.js +565 -0
  218. package/node_modules/@blockcast/fec-worker/dist/fec-worker.js.map +7 -0
  219. package/node_modules/@blockcast/fec-worker/dist/index.d.ts +10 -0
  220. package/node_modules/@blockcast/fec-worker/dist/index.d.ts.map +1 -0
  221. package/node_modules/@blockcast/fec-worker/dist/index.js +8 -0
  222. package/node_modules/@blockcast/fec-worker/dist/index.js.map +1 -0
  223. package/node_modules/@blockcast/fec-worker/dist/shred-fec-worker.d.ts +20 -0
  224. package/node_modules/@blockcast/fec-worker/dist/shred-fec-worker.d.ts.map +1 -0
  225. package/node_modules/@blockcast/fec-worker/dist/shred-fec-worker.js +349 -0
  226. package/node_modules/@blockcast/fec-worker/dist/shred-fec-worker.js.map +7 -0
  227. package/node_modules/@blockcast/fec-worker/dist/shred-worker-client.d.ts +104 -0
  228. package/node_modules/@blockcast/fec-worker/dist/shred-worker-client.d.ts.map +1 -0
  229. package/node_modules/@blockcast/fec-worker/dist/shred-worker-client.js +181 -0
  230. package/node_modules/@blockcast/fec-worker/dist/shred-worker-client.js.map +1 -0
  231. package/node_modules/@blockcast/fec-worker/dist/shred-worker-types.d.ts +179 -0
  232. package/node_modules/@blockcast/fec-worker/dist/shred-worker-types.d.ts.map +1 -0
  233. package/node_modules/@blockcast/fec-worker/dist/shred-worker-types.js +47 -0
  234. package/node_modules/@blockcast/fec-worker/dist/shred-worker-types.js.map +1 -0
  235. package/node_modules/@blockcast/fec-worker/dist/transfer.d.ts +9 -0
  236. package/node_modules/@blockcast/fec-worker/dist/transfer.d.ts.map +1 -0
  237. package/node_modules/@blockcast/fec-worker/dist/transfer.js +13 -0
  238. package/node_modules/@blockcast/fec-worker/dist/transfer.js.map +1 -0
  239. package/node_modules/@blockcast/fec-worker/package.json +59 -0
  240. package/node_modules/@blockcast/fec-worker/src/fec-worker-client.ts +319 -0
  241. package/node_modules/@blockcast/fec-worker/src/fec-worker-types.ts +268 -0
  242. package/node_modules/@blockcast/fec-worker/src/fec-worker.ts +1048 -0
  243. package/node_modules/@blockcast/fec-worker/src/index.ts +32 -0
  244. package/node_modules/@blockcast/fec-worker/src/shred-fec-worker.ts +558 -0
  245. package/node_modules/@blockcast/fec-worker/src/shred-worker-client.ts +222 -0
  246. package/node_modules/@blockcast/fec-worker/src/shred-worker-types.ts +194 -0
  247. package/node_modules/@blockcast/fec-worker/src/transfer.ts +12 -0
  248. package/node_modules/@blockcast/mmt-alta-parse/README.md +181 -0
  249. package/node_modules/@blockcast/mmt-alta-parse/dist/.build-stamp +0 -0
  250. package/node_modules/@blockcast/mmt-alta-parse/dist/index.d.ts +57 -0
  251. package/node_modules/@blockcast/mmt-alta-parse/dist/index.d.ts.map +1 -0
  252. package/node_modules/@blockcast/mmt-alta-parse/dist/index.js +115 -0
  253. package/node_modules/@blockcast/mmt-alta-parse/dist/index.js.map +1 -0
  254. package/node_modules/@blockcast/mmt-alta-parse/package.json +50 -0
  255. package/node_modules/@blockcast/mmt-alta-parse/src/index.ts +136 -0
  256. package/node_modules/@blockcast/mmt-base/README.md +120 -0
  257. package/node_modules/@blockcast/mmt-base/dist/.build-stamp +0 -0
  258. package/node_modules/@blockcast/mmt-base/dist/index.d.ts +2 -0
  259. package/node_modules/@blockcast/mmt-base/dist/index.d.ts.map +1 -0
  260. package/node_modules/@blockcast/mmt-base/dist/index.js +2 -0
  261. package/node_modules/@blockcast/mmt-base/dist/index.js.map +1 -0
  262. package/node_modules/@blockcast/mmt-base/dist/mmtp.d.ts +276 -0
  263. package/node_modules/@blockcast/mmt-base/dist/mmtp.d.ts.map +1 -0
  264. package/node_modules/@blockcast/mmt-base/dist/mmtp.js +645 -0
  265. package/node_modules/@blockcast/mmt-base/dist/mmtp.js.map +1 -0
  266. package/node_modules/@blockcast/mmt-base/package.json +49 -0
  267. package/node_modules/@blockcast/mmt-base/src/index.ts +1 -0
  268. package/node_modules/@blockcast/mmt-base/src/mmtp.ts +899 -0
  269. package/node_modules/@blockcast/mmt-fec/README.md +270 -0
  270. package/node_modules/@blockcast/mmt-fec/dist/.build-stamp +0 -0
  271. package/node_modules/@blockcast/mmt-fec/dist/block-manager.d.ts +82 -0
  272. package/node_modules/@blockcast/mmt-fec/dist/block-manager.d.ts.map +1 -0
  273. package/node_modules/@blockcast/mmt-fec/dist/block-manager.js +296 -0
  274. package/node_modules/@blockcast/mmt-fec/dist/block-manager.js.map +1 -0
  275. package/node_modules/@blockcast/mmt-fec/dist/block-processor.d.ts +72 -0
  276. package/node_modules/@blockcast/mmt-fec/dist/block-processor.d.ts.map +1 -0
  277. package/node_modules/@blockcast/mmt-fec/dist/block-processor.js +192 -0
  278. package/node_modules/@blockcast/mmt-fec/dist/block-processor.js.map +1 -0
  279. package/node_modules/@blockcast/mmt-fec/dist/cmaf-recovery.d.ts +46 -0
  280. package/node_modules/@blockcast/mmt-fec/dist/cmaf-recovery.d.ts.map +1 -0
  281. package/node_modules/@blockcast/mmt-fec/dist/cmaf-recovery.js +96 -0
  282. package/node_modules/@blockcast/mmt-fec/dist/cmaf-recovery.js.map +1 -0
  283. package/node_modules/@blockcast/mmt-fec/dist/cmaf-symbols.d.ts +33 -0
  284. package/node_modules/@blockcast/mmt-fec/dist/cmaf-symbols.d.ts.map +1 -0
  285. package/node_modules/@blockcast/mmt-fec/dist/cmaf-symbols.js +57 -0
  286. package/node_modules/@blockcast/mmt-fec/dist/cmaf-symbols.js.map +1 -0
  287. package/node_modules/@blockcast/mmt-fec/dist/fec-block-mapper.d.ts +185 -0
  288. package/node_modules/@blockcast/mmt-fec/dist/fec-block-mapper.d.ts.map +1 -0
  289. package/node_modules/@blockcast/mmt-fec/dist/fec-block-mapper.js +273 -0
  290. package/node_modules/@blockcast/mmt-fec/dist/fec-block-mapper.js.map +1 -0
  291. package/node_modules/@blockcast/mmt-fec/dist/index.d.ts +22 -0
  292. package/node_modules/@blockcast/mmt-fec/dist/index.d.ts.map +1 -0
  293. package/node_modules/@blockcast/mmt-fec/dist/index.js +22 -0
  294. package/node_modules/@blockcast/mmt-fec/dist/index.js.map +1 -0
  295. package/node_modules/@blockcast/mmt-fec/dist/loc-symbols.d.ts +20 -0
  296. package/node_modules/@blockcast/mmt-fec/dist/loc-symbols.d.ts.map +1 -0
  297. package/node_modules/@blockcast/mmt-fec/dist/loc-symbols.js +28 -0
  298. package/node_modules/@blockcast/mmt-fec/dist/loc-symbols.js.map +1 -0
  299. package/node_modules/@blockcast/mmt-fec/dist/oti.d.ts +172 -0
  300. package/node_modules/@blockcast/mmt-fec/dist/oti.d.ts.map +1 -0
  301. package/node_modules/@blockcast/mmt-fec/dist/oti.js +254 -0
  302. package/node_modules/@blockcast/mmt-fec/dist/oti.js.map +1 -0
  303. package/node_modules/@blockcast/mmt-fec/dist/reorder-buffer.d.ts +41 -0
  304. package/node_modules/@blockcast/mmt-fec/dist/reorder-buffer.d.ts.map +1 -0
  305. package/node_modules/@blockcast/mmt-fec/dist/reorder-buffer.js +81 -0
  306. package/node_modules/@blockcast/mmt-fec/dist/reorder-buffer.js.map +1 -0
  307. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-alta-parse/README.md +181 -0
  308. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-alta-parse/dist/.build-stamp +0 -0
  309. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-alta-parse/dist/index.d.ts +57 -0
  310. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-alta-parse/dist/index.d.ts.map +1 -0
  311. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-alta-parse/dist/index.js +115 -0
  312. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-alta-parse/dist/index.js.map +1 -0
  313. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-alta-parse/package.json +50 -0
  314. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-alta-parse/src/index.ts +136 -0
  315. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/README.md +120 -0
  316. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/.build-stamp +0 -0
  317. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/index.d.ts +2 -0
  318. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/index.d.ts.map +1 -0
  319. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/index.js +2 -0
  320. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/index.js.map +1 -0
  321. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/mmtp.d.ts +276 -0
  322. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/mmtp.d.ts.map +1 -0
  323. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/mmtp.js +645 -0
  324. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/mmtp.js.map +1 -0
  325. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/package.json +49 -0
  326. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/src/index.ts +1 -0
  327. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/src/mmtp.ts +899 -0
  328. package/node_modules/@blockcast/mmt-fec/package.json +79 -0
  329. package/node_modules/@blockcast/mmt-fec/src/block-manager.ts +371 -0
  330. package/node_modules/@blockcast/mmt-fec/src/block-processor.ts +277 -0
  331. package/node_modules/@blockcast/mmt-fec/src/cmaf-recovery.ts +127 -0
  332. package/node_modules/@blockcast/mmt-fec/src/cmaf-symbols.ts +68 -0
  333. package/node_modules/@blockcast/mmt-fec/src/fec-block-mapper.ts +332 -0
  334. package/node_modules/@blockcast/mmt-fec/src/index.ts +77 -0
  335. package/node_modules/@blockcast/mmt-fec/src/loc-symbols.ts +34 -0
  336. package/node_modules/@blockcast/mmt-fec/src/oti.ts +317 -0
  337. package/node_modules/@blockcast/mmt-fec/src/reorder-buffer.ts +101 -0
  338. package/node_modules/@blockcast/mmt-manifest-verify/dist/.build-stamp +0 -0
  339. package/node_modules/@blockcast/mmt-manifest-verify/dist/admission.d.ts +28 -0
  340. package/node_modules/@blockcast/mmt-manifest-verify/dist/admission.d.ts.map +1 -0
  341. package/node_modules/@blockcast/mmt-manifest-verify/dist/admission.js +128 -0
  342. package/node_modules/@blockcast/mmt-manifest-verify/dist/admission.js.map +1 -0
  343. package/node_modules/@blockcast/mmt-manifest-verify/dist/index.d.ts +4 -0
  344. package/node_modules/@blockcast/mmt-manifest-verify/dist/index.d.ts.map +1 -0
  345. package/node_modules/@blockcast/mmt-manifest-verify/dist/index.js +4 -0
  346. package/node_modules/@blockcast/mmt-manifest-verify/dist/index.js.map +1 -0
  347. package/node_modules/@blockcast/mmt-manifest-verify/dist/source-auth.d.ts +37 -0
  348. package/node_modules/@blockcast/mmt-manifest-verify/dist/source-auth.d.ts.map +1 -0
  349. package/node_modules/@blockcast/mmt-manifest-verify/dist/source-auth.js +179 -0
  350. package/node_modules/@blockcast/mmt-manifest-verify/dist/source-auth.js.map +1 -0
  351. package/node_modules/@blockcast/mmt-manifest-verify/dist/verifier.d.ts +99 -0
  352. package/node_modules/@blockcast/mmt-manifest-verify/dist/verifier.d.ts.map +1 -0
  353. package/node_modules/@blockcast/mmt-manifest-verify/dist/verifier.js +492 -0
  354. package/node_modules/@blockcast/mmt-manifest-verify/dist/verifier.js.map +1 -0
  355. package/node_modules/@blockcast/mmt-manifest-verify/package.json +45 -0
  356. package/node_modules/@blockcast/mmt-manifest-verify/src/admission.ts +171 -0
  357. package/node_modules/@blockcast/mmt-manifest-verify/src/index.ts +3 -0
  358. package/node_modules/@blockcast/mmt-manifest-verify/src/source-auth.ts +230 -0
  359. package/node_modules/@blockcast/mmt-manifest-verify/src/verifier.ts +669 -0
  360. package/node_modules/@noble/hashes/LICENSE +21 -0
  361. package/node_modules/@noble/hashes/README.md +521 -0
  362. package/node_modules/@noble/hashes/_assert.d.ts +17 -0
  363. package/node_modules/@noble/hashes/_assert.d.ts.map +1 -0
  364. package/node_modules/@noble/hashes/_assert.js +18 -0
  365. package/node_modules/@noble/hashes/_assert.js.map +1 -0
  366. package/node_modules/@noble/hashes/_blake.d.ts +14 -0
  367. package/node_modules/@noble/hashes/_blake.d.ts.map +1 -0
  368. package/node_modules/@noble/hashes/_blake.js +50 -0
  369. package/node_modules/@noble/hashes/_blake.js.map +1 -0
  370. package/node_modules/@noble/hashes/_md.d.ts +51 -0
  371. package/node_modules/@noble/hashes/_md.d.ts.map +1 -0
  372. package/node_modules/@noble/hashes/_md.js +162 -0
  373. package/node_modules/@noble/hashes/_md.js.map +1 -0
  374. package/node_modules/@noble/hashes/_u64.d.ts +55 -0
  375. package/node_modules/@noble/hashes/_u64.d.ts.map +1 -0
  376. package/node_modules/@noble/hashes/_u64.js +90 -0
  377. package/node_modules/@noble/hashes/_u64.js.map +1 -0
  378. package/node_modules/@noble/hashes/argon2.d.ts +32 -0
  379. package/node_modules/@noble/hashes/argon2.d.ts.map +1 -0
  380. package/node_modules/@noble/hashes/argon2.js +401 -0
  381. package/node_modules/@noble/hashes/argon2.js.map +1 -0
  382. package/node_modules/@noble/hashes/blake1.d.ts +106 -0
  383. package/node_modules/@noble/hashes/blake1.d.ts.map +1 -0
  384. package/node_modules/@noble/hashes/blake1.js +459 -0
  385. package/node_modules/@noble/hashes/blake1.js.map +1 -0
  386. package/node_modules/@noble/hashes/blake2.d.ts +116 -0
  387. package/node_modules/@noble/hashes/blake2.d.ts.map +1 -0
  388. package/node_modules/@noble/hashes/blake2.js +420 -0
  389. package/node_modules/@noble/hashes/blake2.js.map +1 -0
  390. package/node_modules/@noble/hashes/blake2b.d.ts +11 -0
  391. package/node_modules/@noble/hashes/blake2b.d.ts.map +1 -0
  392. package/node_modules/@noble/hashes/blake2b.js +14 -0
  393. package/node_modules/@noble/hashes/blake2b.js.map +1 -0
  394. package/node_modules/@noble/hashes/blake2s.d.ts +20 -0
  395. package/node_modules/@noble/hashes/blake2s.d.ts.map +1 -0
  396. package/node_modules/@noble/hashes/blake2s.js +24 -0
  397. package/node_modules/@noble/hashes/blake2s.js.map +1 -0
  398. package/node_modules/@noble/hashes/blake3.d.ts +54 -0
  399. package/node_modules/@noble/hashes/blake3.d.ts.map +1 -0
  400. package/node_modules/@noble/hashes/blake3.js +255 -0
  401. package/node_modules/@noble/hashes/blake3.js.map +1 -0
  402. package/node_modules/@noble/hashes/crypto.d.ts +2 -0
  403. package/node_modules/@noble/hashes/crypto.d.ts.map +1 -0
  404. package/node_modules/@noble/hashes/crypto.js +5 -0
  405. package/node_modules/@noble/hashes/crypto.js.map +1 -0
  406. package/node_modules/@noble/hashes/cryptoNode.d.ts +2 -0
  407. package/node_modules/@noble/hashes/cryptoNode.d.ts.map +1 -0
  408. package/node_modules/@noble/hashes/cryptoNode.js +18 -0
  409. package/node_modules/@noble/hashes/cryptoNode.js.map +1 -0
  410. package/node_modules/@noble/hashes/eskdf.d.ts +47 -0
  411. package/node_modules/@noble/hashes/eskdf.d.ts.map +1 -0
  412. package/node_modules/@noble/hashes/eskdf.js +166 -0
  413. package/node_modules/@noble/hashes/eskdf.js.map +1 -0
  414. package/node_modules/@noble/hashes/esm/_assert.d.ts +17 -0
  415. package/node_modules/@noble/hashes/esm/_assert.d.ts.map +1 -0
  416. package/node_modules/@noble/hashes/esm/_assert.js +15 -0
  417. package/node_modules/@noble/hashes/esm/_assert.js.map +1 -0
  418. package/node_modules/@noble/hashes/esm/_blake.d.ts +14 -0
  419. package/node_modules/@noble/hashes/esm/_blake.d.ts.map +1 -0
  420. package/node_modules/@noble/hashes/esm/_blake.js +45 -0
  421. package/node_modules/@noble/hashes/esm/_blake.js.map +1 -0
  422. package/node_modules/@noble/hashes/esm/_md.d.ts +51 -0
  423. package/node_modules/@noble/hashes/esm/_md.d.ts.map +1 -0
  424. package/node_modules/@noble/hashes/esm/_md.js +155 -0
  425. package/node_modules/@noble/hashes/esm/_md.js.map +1 -0
  426. package/node_modules/@noble/hashes/esm/_u64.d.ts +55 -0
  427. package/node_modules/@noble/hashes/esm/_u64.d.ts.map +1 -0
  428. package/node_modules/@noble/hashes/esm/_u64.js +67 -0
  429. package/node_modules/@noble/hashes/esm/_u64.js.map +1 -0
  430. package/node_modules/@noble/hashes/esm/argon2.d.ts +32 -0
  431. package/node_modules/@noble/hashes/esm/argon2.d.ts.map +1 -0
  432. package/node_modules/@noble/hashes/esm/argon2.js +392 -0
  433. package/node_modules/@noble/hashes/esm/argon2.js.map +1 -0
  434. package/node_modules/@noble/hashes/esm/blake1.d.ts +106 -0
  435. package/node_modules/@noble/hashes/esm/blake1.d.ts.map +1 -0
  436. package/node_modules/@noble/hashes/esm/blake1.js +452 -0
  437. package/node_modules/@noble/hashes/esm/blake1.js.map +1 -0
  438. package/node_modules/@noble/hashes/esm/blake2.d.ts +116 -0
  439. package/node_modules/@noble/hashes/esm/blake2.d.ts.map +1 -0
  440. package/node_modules/@noble/hashes/esm/blake2.js +413 -0
  441. package/node_modules/@noble/hashes/esm/blake2.js.map +1 -0
  442. package/node_modules/@noble/hashes/esm/blake2b.d.ts +11 -0
  443. package/node_modules/@noble/hashes/esm/blake2b.d.ts.map +1 -0
  444. package/node_modules/@noble/hashes/esm/blake2b.js +11 -0
  445. package/node_modules/@noble/hashes/esm/blake2b.js.map +1 -0
  446. package/node_modules/@noble/hashes/esm/blake2s.d.ts +20 -0
  447. package/node_modules/@noble/hashes/esm/blake2s.d.ts.map +1 -0
  448. package/node_modules/@noble/hashes/esm/blake2s.js +21 -0
  449. package/node_modules/@noble/hashes/esm/blake2s.js.map +1 -0
  450. package/node_modules/@noble/hashes/esm/blake3.d.ts +54 -0
  451. package/node_modules/@noble/hashes/esm/blake3.d.ts.map +1 -0
  452. package/node_modules/@noble/hashes/esm/blake3.js +251 -0
  453. package/node_modules/@noble/hashes/esm/blake3.js.map +1 -0
  454. package/node_modules/@noble/hashes/esm/crypto.d.ts +2 -0
  455. package/node_modules/@noble/hashes/esm/crypto.d.ts.map +1 -0
  456. package/node_modules/@noble/hashes/esm/crypto.js +2 -0
  457. package/node_modules/@noble/hashes/esm/crypto.js.map +1 -0
  458. package/node_modules/@noble/hashes/esm/cryptoNode.d.ts +2 -0
  459. package/node_modules/@noble/hashes/esm/cryptoNode.d.ts.map +1 -0
  460. package/node_modules/@noble/hashes/esm/cryptoNode.js +15 -0
  461. package/node_modules/@noble/hashes/esm/cryptoNode.js.map +1 -0
  462. package/node_modules/@noble/hashes/esm/eskdf.d.ts +47 -0
  463. package/node_modules/@noble/hashes/esm/eskdf.d.ts.map +1 -0
  464. package/node_modules/@noble/hashes/esm/eskdf.js +160 -0
  465. package/node_modules/@noble/hashes/esm/eskdf.js.map +1 -0
  466. package/node_modules/@noble/hashes/esm/hkdf.d.ts +36 -0
  467. package/node_modules/@noble/hashes/esm/hkdf.d.ts.map +1 -0
  468. package/node_modules/@noble/hashes/esm/hkdf.js +82 -0
  469. package/node_modules/@noble/hashes/esm/hkdf.js.map +1 -0
  470. package/node_modules/@noble/hashes/esm/hmac.d.ts +35 -0
  471. package/node_modules/@noble/hashes/esm/hmac.d.ts.map +1 -0
  472. package/node_modules/@noble/hashes/esm/hmac.js +86 -0
  473. package/node_modules/@noble/hashes/esm/hmac.js.map +1 -0
  474. package/node_modules/@noble/hashes/esm/index.d.ts +2 -0
  475. package/node_modules/@noble/hashes/esm/index.d.ts.map +1 -0
  476. package/node_modules/@noble/hashes/esm/index.js +33 -0
  477. package/node_modules/@noble/hashes/esm/index.js.map +1 -0
  478. package/node_modules/@noble/hashes/esm/legacy.d.ts +71 -0
  479. package/node_modules/@noble/hashes/esm/legacy.d.ts.map +1 -0
  480. package/node_modules/@noble/hashes/esm/legacy.js +281 -0
  481. package/node_modules/@noble/hashes/esm/legacy.js.map +1 -0
  482. package/node_modules/@noble/hashes/esm/package.json +10 -0
  483. package/node_modules/@noble/hashes/esm/pbkdf2.d.ts +23 -0
  484. package/node_modules/@noble/hashes/esm/pbkdf2.d.ts.map +1 -0
  485. package/node_modules/@noble/hashes/esm/pbkdf2.js +97 -0
  486. package/node_modules/@noble/hashes/esm/pbkdf2.js.map +1 -0
  487. package/node_modules/@noble/hashes/esm/ripemd160.d.ts +13 -0
  488. package/node_modules/@noble/hashes/esm/ripemd160.d.ts.map +1 -0
  489. package/node_modules/@noble/hashes/esm/ripemd160.js +13 -0
  490. package/node_modules/@noble/hashes/esm/ripemd160.js.map +1 -0
  491. package/node_modules/@noble/hashes/esm/scrypt.d.ts +34 -0
  492. package/node_modules/@noble/hashes/esm/scrypt.d.ts.map +1 -0
  493. package/node_modules/@noble/hashes/esm/scrypt.js +228 -0
  494. package/node_modules/@noble/hashes/esm/scrypt.js.map +1 -0
  495. package/node_modules/@noble/hashes/esm/sha1.d.ts +11 -0
  496. package/node_modules/@noble/hashes/esm/sha1.d.ts.map +1 -0
  497. package/node_modules/@noble/hashes/esm/sha1.js +11 -0
  498. package/node_modules/@noble/hashes/esm/sha1.js.map +1 -0
  499. package/node_modules/@noble/hashes/esm/sha2.d.ts +159 -0
  500. package/node_modules/@noble/hashes/esm/sha2.d.ts.map +1 -0
  501. package/node_modules/@noble/hashes/esm/sha2.js +375 -0
  502. package/node_modules/@noble/hashes/esm/sha2.js.map +1 -0
  503. package/node_modules/@noble/hashes/esm/sha256.d.ts +20 -0
  504. package/node_modules/@noble/hashes/esm/sha256.d.ts.map +1 -0
  505. package/node_modules/@noble/hashes/esm/sha256.js +20 -0
  506. package/node_modules/@noble/hashes/esm/sha256.js.map +1 -0
  507. package/node_modules/@noble/hashes/esm/sha3-addons.d.ts +142 -0
  508. package/node_modules/@noble/hashes/esm/sha3-addons.d.ts.map +1 -0
  509. package/node_modules/@noble/hashes/esm/sha3-addons.js +393 -0
  510. package/node_modules/@noble/hashes/esm/sha3-addons.js.map +1 -0
  511. package/node_modules/@noble/hashes/esm/sha3.d.ts +53 -0
  512. package/node_modules/@noble/hashes/esm/sha3.d.ts.map +1 -0
  513. package/node_modules/@noble/hashes/esm/sha3.js +234 -0
  514. package/node_modules/@noble/hashes/esm/sha3.js.map +1 -0
  515. package/node_modules/@noble/hashes/esm/sha512.d.ts +26 -0
  516. package/node_modules/@noble/hashes/esm/sha512.d.ts.map +1 -0
  517. package/node_modules/@noble/hashes/esm/sha512.js +26 -0
  518. package/node_modules/@noble/hashes/esm/sha512.js.map +1 -0
  519. package/node_modules/@noble/hashes/esm/utils.d.ts +161 -0
  520. package/node_modules/@noble/hashes/esm/utils.d.ts.map +1 -0
  521. package/node_modules/@noble/hashes/esm/utils.js +281 -0
  522. package/node_modules/@noble/hashes/esm/utils.js.map +1 -0
  523. package/node_modules/@noble/hashes/hkdf.d.ts +36 -0
  524. package/node_modules/@noble/hashes/hkdf.d.ts.map +1 -0
  525. package/node_modules/@noble/hashes/hkdf.js +88 -0
  526. package/node_modules/@noble/hashes/hkdf.js.map +1 -0
  527. package/node_modules/@noble/hashes/hmac.d.ts +35 -0
  528. package/node_modules/@noble/hashes/hmac.d.ts.map +1 -0
  529. package/node_modules/@noble/hashes/hmac.js +91 -0
  530. package/node_modules/@noble/hashes/hmac.js.map +1 -0
  531. package/node_modules/@noble/hashes/index.d.ts +1 -0
  532. package/node_modules/@noble/hashes/index.d.ts.map +1 -0
  533. package/node_modules/@noble/hashes/index.js +33 -0
  534. package/node_modules/@noble/hashes/index.js.map +1 -0
  535. package/node_modules/@noble/hashes/legacy.d.ts +71 -0
  536. package/node_modules/@noble/hashes/legacy.d.ts.map +1 -0
  537. package/node_modules/@noble/hashes/legacy.js +287 -0
  538. package/node_modules/@noble/hashes/legacy.js.map +1 -0
  539. package/node_modules/@noble/hashes/package.json +266 -0
  540. package/node_modules/@noble/hashes/pbkdf2.d.ts +23 -0
  541. package/node_modules/@noble/hashes/pbkdf2.d.ts.map +1 -0
  542. package/node_modules/@noble/hashes/pbkdf2.js +101 -0
  543. package/node_modules/@noble/hashes/pbkdf2.js.map +1 -0
  544. package/node_modules/@noble/hashes/ripemd160.d.ts +13 -0
  545. package/node_modules/@noble/hashes/ripemd160.d.ts.map +1 -0
  546. package/node_modules/@noble/hashes/ripemd160.js +16 -0
  547. package/node_modules/@noble/hashes/ripemd160.js.map +1 -0
  548. package/node_modules/@noble/hashes/scrypt.d.ts +34 -0
  549. package/node_modules/@noble/hashes/scrypt.d.ts.map +1 -0
  550. package/node_modules/@noble/hashes/scrypt.js +232 -0
  551. package/node_modules/@noble/hashes/scrypt.js.map +1 -0
  552. package/node_modules/@noble/hashes/sha1.d.ts +11 -0
  553. package/node_modules/@noble/hashes/sha1.d.ts.map +1 -0
  554. package/node_modules/@noble/hashes/sha1.js +14 -0
  555. package/node_modules/@noble/hashes/sha1.js.map +1 -0
  556. package/node_modules/@noble/hashes/sha2.d.ts +159 -0
  557. package/node_modules/@noble/hashes/sha2.d.ts.map +1 -0
  558. package/node_modules/@noble/hashes/sha2.js +384 -0
  559. package/node_modules/@noble/hashes/sha2.js.map +1 -0
  560. package/node_modules/@noble/hashes/sha256.d.ts +20 -0
  561. package/node_modules/@noble/hashes/sha256.d.ts.map +1 -0
  562. package/node_modules/@noble/hashes/sha256.js +23 -0
  563. package/node_modules/@noble/hashes/sha256.js.map +1 -0
  564. package/node_modules/@noble/hashes/sha3-addons.d.ts +142 -0
  565. package/node_modules/@noble/hashes/sha3-addons.d.ts.map +1 -0
  566. package/node_modules/@noble/hashes/sha3-addons.js +402 -0
  567. package/node_modules/@noble/hashes/sha3-addons.js.map +1 -0
  568. package/node_modules/@noble/hashes/sha3.d.ts +53 -0
  569. package/node_modules/@noble/hashes/sha3.d.ts.map +1 -0
  570. package/node_modules/@noble/hashes/sha3.js +239 -0
  571. package/node_modules/@noble/hashes/sha3.js.map +1 -0
  572. package/node_modules/@noble/hashes/sha512.d.ts +26 -0
  573. package/node_modules/@noble/hashes/sha512.d.ts.map +1 -0
  574. package/node_modules/@noble/hashes/sha512.js +29 -0
  575. package/node_modules/@noble/hashes/sha512.js.map +1 -0
  576. package/node_modules/@noble/hashes/src/_assert.ts +22 -0
  577. package/node_modules/@noble/hashes/src/_blake.ts +50 -0
  578. package/node_modules/@noble/hashes/src/_md.ts +176 -0
  579. package/node_modules/@noble/hashes/src/_u64.ts +91 -0
  580. package/node_modules/@noble/hashes/src/argon2.ts +497 -0
  581. package/node_modules/@noble/hashes/src/blake1.ts +534 -0
  582. package/node_modules/@noble/hashes/src/blake2.ts +486 -0
  583. package/node_modules/@noble/hashes/src/blake2b.ts +10 -0
  584. package/node_modules/@noble/hashes/src/blake2s.ts +20 -0
  585. package/node_modules/@noble/hashes/src/blake3.ts +272 -0
  586. package/node_modules/@noble/hashes/src/crypto.ts +9 -0
  587. package/node_modules/@noble/hashes/src/cryptoNode.ts +15 -0
  588. package/node_modules/@noble/hashes/src/eskdf.ts +187 -0
  589. package/node_modules/@noble/hashes/src/hkdf.ts +88 -0
  590. package/node_modules/@noble/hashes/src/hmac.ts +94 -0
  591. package/node_modules/@noble/hashes/src/index.ts +31 -0
  592. package/node_modules/@noble/hashes/src/legacy.ts +293 -0
  593. package/node_modules/@noble/hashes/src/pbkdf2.ts +122 -0
  594. package/node_modules/@noble/hashes/src/ripemd160.ts +12 -0
  595. package/node_modules/@noble/hashes/src/scrypt.ts +257 -0
  596. package/node_modules/@noble/hashes/src/sha1.ts +10 -0
  597. package/node_modules/@noble/hashes/src/sha2.ts +402 -0
  598. package/node_modules/@noble/hashes/src/sha256.ts +24 -0
  599. package/node_modules/@noble/hashes/src/sha3-addons.ts +499 -0
  600. package/node_modules/@noble/hashes/src/sha3.ts +258 -0
  601. package/node_modules/@noble/hashes/src/sha512.ts +34 -0
  602. package/node_modules/@noble/hashes/src/utils.ts +395 -0
  603. package/node_modules/@noble/hashes/utils.d.ts +161 -0
  604. package/node_modules/@noble/hashes/utils.d.ts.map +1 -0
  605. package/node_modules/@noble/hashes/utils.js +313 -0
  606. package/node_modules/@noble/hashes/utils.js.map +1 -0
  607. package/package.json +203 -0
  608. package/schemas/catalog.schema.json +360 -0
  609. package/src/align.ts +239 -0
  610. package/src/audio-init-parser.ts +189 -0
  611. package/src/audio-ring-buffer.ts +120 -0
  612. package/src/authored-timing.ts +63 -0
  613. package/src/carp-catalog.ts +845 -0
  614. package/src/catalog-fec-params.ts +535 -0
  615. package/src/catalog-network-source.ts +49 -0
  616. package/src/catalog-validator.ts +34 -0
  617. package/src/cmaf-block-assembler.ts +1167 -0
  618. package/src/cmaf-group-batcher.ts +104 -0
  619. package/src/cmcd-producer.ts +334 -0
  620. package/src/codec-utils.ts +1611 -0
  621. package/src/fec-codec.ts +61 -0
  622. package/src/fec-manager.ts +2081 -0
  623. package/src/fec-starvation-detector.ts +128 -0
  624. package/src/fec-worker-setup.ts +213 -0
  625. package/src/fmp4-completion.ts +47 -0
  626. package/src/fmp4-reader.ts +112 -0
  627. package/src/index.ts +486 -0
  628. package/src/iso-bmff-timing.ts +813 -0
  629. package/src/loc.ts +181 -0
  630. package/src/manifest-auth-plan.ts +107 -0
  631. package/src/media-router.ts +795 -0
  632. package/src/mfu-reassembler.ts +3741 -0
  633. package/src/mfu-reassembly-policy.ts +134 -0
  634. package/src/mmtp-container-parser.ts +1836 -0
  635. package/src/mmtp-decode-pipeline.ts +389 -0
  636. package/src/mmtp-router.ts +3136 -0
  637. package/src/mmtp-to-cmaf.ts +1024 -0
  638. package/src/multicast-auth-catalog.ts +340 -0
  639. package/src/multicast-input.ts +439 -0
  640. package/src/multicast-manager.ts +1720 -0
  641. package/src/multicast-transport.ts +321 -0
  642. package/src/multicast-unwrap.ts +132 -0
  643. package/src/no-modules.ts +20 -0
  644. package/src/phase14-catalog-override.ts +336 -0
  645. package/src/repair-fetch.ts +238 -0
  646. package/src/repair-layers.ts +490 -0
  647. package/src/sample-combiner.ts +938 -0
  648. package/src/signaling.ts +78 -0
  649. package/src/source-auth.ts +1 -0
  650. package/src/switching-set-alignment.ts +109 -0
  651. package/src/timestamp-codec.ts +265 -0
  652. package/src/track-pipeline-setup.ts +189 -0
  653. package/src/track-pipeline.ts +332 -0
  654. package/src/track-session.ts +994 -0
  655. package/src/transport-pool.ts +128 -0
  656. package/src/wasm-mfu-reassembler.ts +389 -0
  657. package/src/wasm-mmtp-container-parser.ts +23 -0
  658. package/test-vectors/align.json +199 -0
  659. package/test-vectors/catalog/README.md +40 -0
  660. package/test-vectors/catalog/negative/fec-enhancement-repair-removed.json +27 -0
  661. package/test-vectors/catalog/negative/fec-repair-layer-geometry.json +32 -0
  662. package/test-vectors/catalog/negative/fec-repair-layer-missing-symbols.json +30 -0
  663. package/test-vectors/catalog/negative/fec-repair-multiple-depends.json +31 -0
  664. package/test-vectors/catalog/negative/fec-repair-overlay-with-layer.json +33 -0
  665. package/test-vectors/catalog/negative/fec-repair-priority-out-of-band.json +23 -0
  666. package/test-vectors/catalog/negative/fec-repair-source-symbols-without-scope.json +32 -0
  667. package/test-vectors/catalog/negative/legacy-selection-params.json +15 -0
  668. package/test-vectors/catalog/negative/media-track-repair-layer.json +16 -0
  669. package/test-vectors/catalog/negative/missing-mmtp-fields.json +12 -0
  670. package/test-vectors/catalog/negative/multicast-auth-padded-key.json +37 -0
  671. package/test-vectors/catalog/negative/multicast-auth-reused-track-format.json +53 -0
  672. package/test-vectors/catalog/negative/multicast-auth-trailing-pad-bits.json +37 -0
  673. package/test-vectors/catalog/negative/multicast-endpoint-missing-protocol-and-source.json +24 -0
  674. package/test-vectors/catalog/negative/network-source-object.json +25 -0
  675. package/test-vectors/catalog/negative/raptorq-source-symbols-over-max.json +23 -0
  676. package/test-vectors/catalog/negative/raptorq-unaligned-symbol.json +26 -0
  677. package/test-vectors/catalog/negative/repair-track-legacy-shape.json +19 -0
  678. package/test-vectors/catalog/negative/track-role-empty.json +16 -0
  679. package/test-vectors/catalog/negative/track-role-non-string.json +16 -0
  680. package/test-vectors/catalog/positive/fec-layered-repair.json +104 -0
  681. package/test-vectors/catalog/positive/fec-multicast.json +51 -0
  682. package/test-vectors/catalog/positive/flat-av.json +30 -0
  683. package/test-vectors/catalog/positive/multicast-auth-rotation.json +55 -0
  684. package/test-vectors/catalog/positive/ticks-and-mixed-timescales.json +39 -0
  685. package/test-vectors/catalog/positive/track-role.json +49 -0
  686. package/test-vectors/catalog/releases/moqcast-native-v16-v1/catalog.json +65 -0
  687. package/test-vectors/catalog/releases/moqcast-native-v16-v1.archive.sha256 +1 -0
  688. package/test-vectors/catalog-multicast-auth-v1.json +26 -0
  689. package/test-vectors/catalog-validation.json +148 -0
  690. package/test-vectors/catalog-validator-public-api.mjs +25 -0
  691. package/test-vectors/generate-source-auth-fixture.mjs +114 -0
  692. package/test-vectors/source-auth-v1.json +61 -0
  693. package/test-vectors/source-contiguity-v1.json +131 -0
  694. package/test-vectors/subsample-placement-v1.json +230 -0
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/track-session.ts", "../src/fec-manager.ts"],
4
+ "sourcesContent": ["/**\n * TrackSession \u2014 per-track transport state machine.\n * SessionManager \u2014 orchestrates all sessions + TransportPool.\n *\n * Implements Patent Claim 9 (draft-ramadan-moq-fec APPLICATION-1):\n * (a) subscribe unicast\n * (b) simultaneously join multicast\n * (c) render from unicast during join\n * (d) activate multicast on RAP, suppress unicast\n * (e) cross-transport FEC repair during transition\n * (f) hysteresis before switching away from multicast\n * (g) re-subscribe unicast on sustained failure\n *\n * PIM: manages transport state without inspecting packet contents.\n */\n\nimport {\n TransportPool,\n type Subscription,\n type SubscriptionMode,\n type AcquireOptions,\n type DirectSocketHealth,\n type TransportPoolOptions,\n} from './transport-pool.js'\nimport { fecBlockBudgetMs } from '@blockcast/mmt-fec'\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport type TrackState =\n | 'idle'\n | 'unicast'\n | 'unicast-fallback'\n | 'joining'\n | 'multicast'\n | 'amt'\n | 'failing'\n\nexport interface TrackSessionConfig {\n trackName: string\n trackType: 'video' | 'audio'\n packetId: number\n /** FEC interleave depth from catalog (ms). Used for hysteresis timer. */\n fecInterleaveMs: number\n /** Explicit catalog jitter/reorder allowance in ms. */\n fecReorderToleranceMs?: number\n /** FEC K from catalog. Used for audio worklet latency calculation. */\n fecK: number\n /** FEC repair symbols per block (P). NOT symbol size. */\n fecRepairCount: number\n /**\n * Repair symbols per block this receiver actually joined: the sum of\n * `repairSymbols` over the joined BASE layers, not a count of layers\n * (draft-ramadan-moq-fec 6.3). A keyframe overlay is a separate FEC instance\n * and is not included.\n *\n * This is runtime join state, not catalog geometry: a receiver on a clean\n * path joins layer 0 only while a lossy one adds layers, so the catalog's\n * total P is not what this receiver is receiving. Diagnostics that report P\n * (the worker snapshot, CMCD `x-bc-fec-p`) must read this.\n *\n * Absent when the caller joins no layers explicitly, in which case P_0 is\n * what was joined. That is an identity on a single-layer track, not a\n * catalog default: with one layer, P_0 is the total.\n */\n joinedRepairCount?: number\n /** FEC symbol size in bytes (T). Required for WASM decoder OTI. */\n fecSymbolSize: number\n /** MoQ repair track name from catalog FEC config. */\n repairTrackName?: string\n /** Frame samples from initData parsing (audio-init-parser.ts). */\n frameSamples?: number\n /** Audio sample rate from catalog. */\n sampleRate?: number\n /** Ordered transport preference: best first. */\n preferredTransports: SubscriptionMode[]\n /** Subscription key for multicast/AMT: \"(S,G,port)\". */\n multicastKey?: string\n /** MoQ track name for unicast subscription. */\n moqTrackName?: string\n /** AMT relay address (if AMT is in preference chain). */\n amtRelay?: string\n /** MoQ relay URL (for WebTransport subscription). */\n moqRelayUrl?: string\n}\n\n// ---------------------------------------------------------------------------\n// PIM callbacks (protocol-independent \u2014 transport state only)\n// ---------------------------------------------------------------------------\n\nexport interface SessionManagerCallbacks {\n /** Fired on every transport state transition. */\n onStateChange?(trackName: string, from: TrackState, to: TrackState): void\n /** Fired to suppress/unsuppress unicast for this track (transport arbitration). */\n onSuppress?(trackName: string, suppressed: boolean): void\n /**\n * Fired when a track sat in `joining` for SessionManager.takeoverDeadlineMs\n * without a RAP (BLO-9413/D9). The session has already returned to `unicast`\n * (nothing was suppressed, so nothing to restore) and it keeps its FEC\n * decoder armed, because the multicast socket is still joined and the router\n * still admits the stream's symbols. The host should abandon the router-side\n * attempt (e.g. MediaRouter.failbackToMoq()) so the router stops waiting for\n * a keyframe that is never coming; it must not dispose the decoder here. The\n * decoder is released on the next transport edge (unsubscribe, failbackAll,\n * or transportFailed falling back to unicast) through fec-teardown.\n */\n onTakeoverTimeout?(trackName: string): void\n}\n\n// ---------------------------------------------------------------------------\n// MMTP application-layer extension (FEC lifecycle, media resets)\n// ---------------------------------------------------------------------------\n\n/**\n * MMTP track events (discriminated union).\n * Each protocol layer defines its own event union \u2014 MMTP events cover\n * media resets, latency, and FEC lifecycle.\n */\nexport type MmtpTrackEvent =\n | { type: 'reset' }\n | { type: 'latency'; latencyMs: number }\n | { type: 'fec-setup' }\n | { type: 'fec-teardown' }\n\n/**\n * MMTP application-layer extension of SessionManagerCallbacks.\n * Single onTrackEvent dispatches all MMTP-specific events.\n * PIM consumers use the base SessionManagerCallbacks (no onTrackEvent).\n *\n * IWA can define its own subinterface with its own TrackEvent union\n * (e.g., IwaTrackEvent for socket health, AMT state) using the same pattern.\n */\nexport interface MmtpSessionCallbacks extends SessionManagerCallbacks {\n onTrackEvent?(trackName: string, event: MmtpTrackEvent, config: TrackSessionConfig): void\n}\n\n/**\n * Loss-hysteresis window covering the signaled FEC block span plus jitter.\n *\n * `fecK` no longer feeds the math; it is still validated as a catalog\n * well-formedness guard and kept in the signature for positional API compat\n * (removal is a follow-up with the next cross-repo pin bump).\n */\nexport function computeFecLossWindowMs(fecInterleaveMs: number, fecK: number, jitterBufferMs?: number): number {\n return computeFecDeliveryWindowMs({\n interleaveDepthMs: fecInterleaveMs,\n sourceSymbols: fecK,\n jitterBufferMs,\n })\n}\n\nexport interface FecDeliveryWindowConfig {\n interleaveDepthMs: number\n sourceSymbols: number\n repairSymbols?: number\n symbolSize?: number\n mediaUnitsPerSecond?: number\n bandwidthBps?: number\n /**\n * Explicit jitter/reorder allowance in ms, added once to the complete span.\n * Callers pass their signaled catalog allowance, never a local default.\n */\n jitterBufferMs?: number\n}\n\n/**\n * The catalog interleaveDepthMs is the complete FEC block span, including the\n * repair tail. K, P, cadence, and bandwidth must not expand it again. Geometry\n * fields remain accepted for existing callers, but do not feed deadline math.\n */\nexport function computeFecDeliveryWindowMs(\n config: FecDeliveryWindowConfig,\n): number {\n const fecInterleaveMs = config.interleaveDepthMs\n const fecK = config.sourceSymbols\n if (!Number.isFinite(fecInterleaveMs) || fecInterleaveMs <= 0) {\n throw new Error(\n `[TrackSession] fecInterleaveMs=${fecInterleaveMs}. ` +\n `Derive a positive FEC interleave window from the stream catalog.`\n )\n }\n if (!Number.isFinite(fecK) || fecK <= 0) {\n throw new Error(\n `[TrackSession] fecK=${fecK}. ` +\n `Derive a positive FEC source-symbol count from the stream catalog.`\n )\n }\n\n // Absence stays absent; it does not authorize a synthesized reorder budget.\n if (config.jitterBufferMs === undefined) return Math.ceil(fecInterleaveMs)\n return fecBlockBudgetMs(fecInterleaveMs, config.jitterBufferMs)\n}\n\n/** Convert the shared FEC delivery deadline into an integer media timescale. */\nexport function computeFecDeliveryWindowMediaUnits(\n config: FecDeliveryWindowConfig,\n timestampTimescale: number,\n): number {\n if (!Number.isFinite(timestampTimescale) || timestampTimescale <= 0) {\n throw new Error(\n `[TrackSession] timestampTimescale=${timestampTimescale}. ` +\n 'Derive a positive media timescale from the container.',\n )\n }\n const deliveryWindow = Math.ceil(\n computeFecDeliveryWindowMs(config) * timestampTimescale / 1000,\n )\n if (!Number.isSafeInteger(deliveryWindow)) {\n throw new Error('[TrackSession] FEC delivery window exceeds the media timestamp range.')\n }\n return deliveryWindow\n}\n\n// ---------------------------------------------------------------------------\n// TrackSession\n// ---------------------------------------------------------------------------\n\nexport class TrackSession {\n readonly config: TrackSessionConfig\n #state: TrackState = 'idle'\n #subscription: Subscription | null = null\n #hysteresisTimer: ReturnType<typeof setTimeout> | null = null\n #joinDeadlineTimer: ReturnType<typeof setTimeout> | null = null\n #previousPrimary: SubscriptionMode | null = null\n #manager: SessionManager\n // Single suppression authority flag (BLO-9413 failback coherence): every\n // suppress this session fires is recorded here, and every path that leaves\n // a suppressed state unwinds through #unsuppress(). Before this flag,\n // unsubscribe() only un-suppressed from multicast/amt \u2014 a session torn down\n // from `failing` (exactly where a starving track sits) was re-subscribed to\n // unicast with the host's media still muted: MoQ playing into a dead sink.\n #suppressFired = false\n // Single FEC authority flag: set by the fec-setup edges (joinStarted,\n // enterUnicastFallback), cleared by the fec-teardown edge in #transition.\n // Setup and teardown are paired by construction, so a takeover timeout that\n // parks the session in `unicast` keeps the decoder the still-joined\n // transport is feeding, and the later unsubscribe still tears it down.\n #fecArmed = false\n\n constructor(config: TrackSessionConfig, manager: SessionManager) {\n this.config = config\n this.#manager = manager\n }\n\n // -----------------------------------------------------------------------\n // State\n // -----------------------------------------------------------------------\n\n get state(): TrackState { return this.#state }\n get subscription(): Subscription | null { return this.#subscription }\n\n /** True while this session's onSuppress(true) is outstanding (no matching\n * onSuppress(false) yet). Test/assertion surface for failback coherence. */\n get isSuppressed(): boolean { return this.#suppressFired }\n\n /** Current active transport mode, or null if idle. */\n get transport(): SubscriptionMode | null {\n switch (this.#state) {\n case 'multicast': return 'multicast'\n case 'amt': return 'amt'\n case 'unicast': return 'moq'\n case 'unicast-fallback': return 'moq' // SSM bridge absent \u2014 MoQ unicast is the only source\n case 'joining': return 'moq' // MoQ still primary during join\n case 'failing': return this.#previousPrimary\n default: return null\n }\n }\n\n // -----------------------------------------------------------------------\n // Computed (no magic numbers)\n // -----------------------------------------------------------------------\n\n /** Audio worklet latency: max(fecInterleaveMs, frameDurationMs * fecK).\n * Returns undefined if frame duration unknown (no initData). */\n get audioLatencyMs(): number | undefined {\n const fd = this.frameDurationMs\n if (fd === undefined) {\n return this.config.fecInterleaveMs > 0 ? this.config.fecInterleaveMs : undefined\n }\n return Math.max(this.config.fecInterleaveMs, fd * this.config.fecK)\n }\n\n /** Frame duration in ms, derived from frameSamples + sampleRate. */\n get frameDurationMs(): number | undefined {\n const { frameSamples, sampleRate } = this.config\n if (!frameSamples || !sampleRate || sampleRate <= 0) return undefined\n return (frameSamples / sampleRate) * 1000\n }\n\n // -----------------------------------------------------------------------\n // Transitions (Patent Claim 9)\n // -----------------------------------------------------------------------\n\n /** idle \u2192 unicast: player subscribes to this track. */\n subscribe(): void {\n if (this.#state !== 'idle') return\n this.#transition('unicast')\n\n // Emit audio latency on first subscribe\n if (this.config.trackType === 'audio') {\n const latency = this.audioLatencyMs\n if (latency !== undefined) {\n this.#manager.mmtp?.onTrackEvent?.(this.config.trackName, { type: 'latency', latencyMs: latency }, this.config)\n }\n }\n }\n\n /** unicast \u2192 joining: multicast/AMT capability detected, join in progress. */\n joinStarted(): void {\n if (this.#state !== 'unicast') return\n this.#transition('joining')\n // FEC decoders must be ready before multicast data arrives (Claim 9e:\n // cross-transport repair). A session re-joining after a takeover timeout\n // still owns its decoder, so fec-setup fires once per FEC epoch.\n this.#armFec()\n }\n\n /**\n * any non-fallback state \u2192 unicast-fallback. Used when the SSM bridge is\n * absent (vanilla Chromium / no extension) and the host falls back to a\n * MoQ unicast subscription per track. Terminal-ish: activateMulticast,\n * failbackAll, and transportFailed are no-ops while in this state.\n *\n * Fires fec-setup so the per-track repair worker spins up \u2014 repair packets\n * still flow over the MoQ repair-track subscription, so FEC source+repair\n * can converge as in the multicast path.\n *\n * Audio latency is NOT re-emitted here: subscribe() already fires latency\n * once on the idle \u2192 unicast edge, and enterUnicastFallback is only ever\n * reached from a non-idle state (SessionManager skips idle sessions).\n */\n enterUnicastFallback(): void {\n if (this.#state === 'unicast-fallback') return\n this.#clearHysteresis()\n this.#transition('unicast-fallback')\n // Suppress the redundant unicast media decoder. On the no-bridge path the\n // host feeds MoQ-unicast source packets into MulticastManager (the render\n // consumer); a second base-media subscribe (the @moq/watch decoder) would\n // split the single connection's QUIC flow-control budget and stall delivery\n // (BLO-8969). Fire suppress here, on the fallback edge, so the decoder is\n // gated off before it subscribes rather than after the relay forward task\n // has already wedged.\n this.#suppress()\n this.#armFec()\n }\n\n /**\n * unicast-fallback \u2192 unicast. Called when the SSM bridge appears mid-session\n * (extension installed after fallback engaged). Host first aborts the fallback\n * drainers, then transitions sessions back to unicast so the regular\n * multicast/AMT preference chain can take over via joinStarted().\n */\n exitUnicastFallback(): void {\n if (this.#state !== 'unicast-fallback') return\n this.#transition('unicast')\n // Un-suppress the unicast decoder: the bridge appeared mid-session, so the\n // host tears down the fallback drainers and the regular preference chain\n // (joinStarted -> joining -> rapReceived) will re-suppress on multicast RAP.\n this.#unsuppress()\n }\n\n /**\n * joining \u2192 multicast or amt: RAP received on the joined transport (Claim 9d).\n * Resets audio worklet, then suppresses MoQ.\n */\n rapReceived(mode: 'multicast' | 'amt'): void {\n if (this.#state !== 'joining') return\n this.#previousPrimary = mode\n this.#transition(mode)\n if (this.config.trackType === 'audio') {\n this.#manager.mmtp?.onTrackEvent?.(this.config.trackName, { type: 'reset' }, this.config)\n }\n this.#suppress()\n }\n\n /** multicast/amt \u2192 failing: loss detected, start hysteresis timer (Claim 9f). */\n lossDetected(): void {\n if (this.#state !== 'multicast' && this.#state !== 'amt') return\n this.#previousPrimary = this.#state as SubscriptionMode\n this.#transition('failing')\n\n // Hysteresis timer: sustained loss before failback (Claim 9f). Use the\n // same complete span plus explicit allowance as FECManager and workers.\n const hysteresisMs = computeFecLossWindowMs(\n this.config.fecInterleaveMs,\n this.config.fecK,\n this.config.fecReorderToleranceMs,\n )\n this.#hysteresisTimer = setTimeout(() => {\n this.hysteresisExpired()\n }, hysteresisMs)\n }\n\n /** failing \u2192 previous primary: loss recovered within hysteresis window. */\n lossRecovered(): void {\n if (this.#state !== 'failing') return\n this.#clearHysteresis()\n if (this.#previousPrimary) {\n this.#transition(this.#previousPrimary as TrackState)\n }\n }\n\n /**\n * Transport failed (AMT relay down, multicast interface gone).\n * Try next transport in preference chain.\n */\n transportFailed(): void {\n this.#clearHysteresis()\n // Unicast-fallback is terminal: the host owns the subscription and\n // SessionManager transport machinery does not arbitrate it.\n if (this.#state === 'unicast-fallback') return\n const current = this.#previousPrimary ?? this.transport\n const prefs = this.config.preferredTransports\n const currentIdx = current ? prefs.indexOf(current) : -1\n const nextMode = prefs[currentIdx + 1]\n\n if (nextMode !== undefined && nextMode !== 'moq') {\n // Next is multicast or AMT: start joining. `joining` means MoQ is\n // primary again (see transport getter), so if the failed transport had\n // suppressed unicast, restore it for the join window or the viewer\n // watches black video until the next RAP (which may never come).\n // The chain position moves with the attempt: a join that times out\n // parks the session in `unicast` with this transport recorded, so the\n // next failure continues past it rather than re-trying the transport\n // that already failed and re-firing onTakeoverTimeout on every lap.\n this.#previousPrimary = nextMode\n this.#transition('joining')\n this.#unsuppress()\n return\n }\n // MoQ is next, or the chain is exhausted: unicast either way (Claim 9g).\n // For a session parked in `unicast` with its decoder retained after a\n // takeover timeout, this transition is a self-edge and fires no teardown,\n // so the release is explicit. It is the \"transportFailed falling back to\n // unicast\" release path the onTakeoverTimeout doc names, and #disarmFec is\n // idempotent, so it is a no-op when the transition already fired the\n // teardown. The audio reset is not flag-guarded, so it is tied to the edge\n // itself: a session already resting in unicast changed no transport and\n // owes the host no reset, however many pool subscriptions fail for it.\n const from = this.#state\n this.#previousPrimary = null\n this.#transition('unicast')\n this.#disarmFec()\n this.#unsuppress()\n if (from !== 'unicast' && this.config.trackType === 'audio') {\n this.#manager.mmtp?.onTrackEvent?.(this.config.trackName, { type: 'reset' }, this.config)\n }\n }\n\n /** Hysteresis expired \u2192 failback to next transport (Claim 9g). */\n hysteresisExpired(): void {\n if (this.#state !== 'failing') return\n this.#clearHysteresis()\n this.transportFailed()\n }\n\n /** \u2192 idle: player unsubscribes / tab close / cleanup. */\n unsubscribe(): void {\n this.#clearHysteresis()\n if (this.#state !== 'idle') {\n this.#transition('idle')\n // Unwind any outstanding suppression regardless of which state we left.\n // The old `wasActive` check (multicast/amt only) leaked suppression when\n // tearing down from `failing` or `unicast-fallback` \u2014 failbackAll()'s\n // unsubscribe\u2192subscribe cycle then re-entered unicast with the host's\n // media still muted (BLO-9413 freeze: \"failing back\" logged, video dead).\n this.#unsuppress()\n }\n }\n\n /** True when SessionManager.enterUnicastFallback put this track into the\n * host-driven unicast subscription mode. Host can iterate sessions to\n * decide whether to keep its fallback drainer alive across MediaRouter\n * arbitration events. */\n get isUnicastFallback(): boolean {\n return this.#state === 'unicast-fallback'\n }\n\n /**\n * True while this track owns a FEC decoder (fec-setup fired, fec-teardown\n * not yet). Stays true across a takeover timeout that parked the session in\n * `unicast`; SessionManager.failbackAll uses it to release such sessions.\n */\n get isFecArmed(): boolean {\n return this.#fecArmed\n }\n\n /**\n * Start the deadline a `joining` entry deferred while the join was inactive\n * (SessionManager.joinActive). The track gets the FULL deadline from here,\n * because that is when media can first arrive. No-op outside `joining` and\n * when a deadline is already running, so it never shortens or restarts one.\n *\n * The `joinActive` guard is what makes this safe to call from anywhere: a\n * track parked in `joining` with a deferred (null) timer satisfies the other\n * two, so without it a host calling this while the join is still inactive\n * would arm the exact deadline the gate exists to defer -- silently\n * reinstating BLO-34289. It is a no-op for the internal caller, which writes\n * `#joinActive = true` before iterating the sessions.\n */\n armDeferredJoinDeadline(): void {\n if (this.#state !== 'joining' || this.#joinDeadlineTimer !== null) return\n if (!this.#manager.joinActive) return\n this.#armJoinDeadline(this.#manager.takeoverDeadlineMs)\n }\n\n dispose(): void {\n this.#clearHysteresis()\n this.#clearJoinDeadline()\n this.#state = 'idle'\n this.#subscription = null\n // Release through the teardown edge, not a bare assignment: the\n // retained-FEC park makes `unicast` + isFecArmed a resting state, and\n // SessionManager.dispose() calls this directly (removeTrack() reaches it\n // via unsubscribe(), which has already disarmed). A bare assignment there\n // drops the host's decoder without a fec-teardown -- a leaked Worker.\n this.#disarmFec()\n }\n\n // -----------------------------------------------------------------------\n // Internal\n // -----------------------------------------------------------------------\n\n #transition(to: TrackState, opts?: { retainFec?: boolean }): void {\n const from = this.#state\n if (from === to) return\n // Takeover deadline (BLO-9413/D9): every entry into `joining` arms it\n // (joinStarted AND transportFailed-failover both route through here);\n // every exit cancels it. Without this, a transport that accepts the\n // subscribe but never delivers a RAP parks the session in `joining`\n // forever (observed live in the Phase-0 headless repro). The value is\n // resolved before any state write, so the one residual failure -- a\n // manager whose deadline was never set at all, which addTrack() refuses --\n // leaves the session in its previous state rather than half-transitioned.\n // Invalid values never reach here: SessionManager's setter refuses them.\n // The read is unconditional even when `joinActive` defers the arm, so a\n // manager without a deadline still fails closed on every joining edge.\n const joinDeadlineMs = to === 'joining' ? this.#manager.takeoverDeadlineMs : undefined\n this.#state = to\n if (from === 'joining') this.#clearJoinDeadline()\n if (joinDeadlineMs !== undefined && this.#manager.joinActive) {\n this.#armJoinDeadline(joinDeadlineMs)\n }\n // FEC teardown: entering a state without a FEC decoder releases the one\n // this track armed, unless the caller keeps it (takeover timeout: the\n // transport is still joined and still feeding the decoder).\n const fecActive = (s: TrackState) =>\n s === 'joining' ||\n s === 'multicast' ||\n s === 'amt' ||\n s === 'failing' ||\n s === 'unicast-fallback'\n if (!fecActive(to) && !opts?.retainFec) this.#disarmFec()\n this.#manager.callbacks?.onStateChange?.(this.config.trackName, from, to)\n }\n\n #clearHysteresis(): void {\n if (this.#hysteresisTimer !== null) {\n clearTimeout(this.#hysteresisTimer)\n this.#hysteresisTimer = null\n }\n }\n\n #armJoinDeadline(deadlineMs: number): void {\n this.#clearJoinDeadline()\n if (deadlineMs === 0) return // a declared 0 disables the deadline\n this.#joinDeadlineTimer = setTimeout(() => {\n this.#joinDeadlineTimer = null\n if (this.#state !== 'joining') return\n // No RAP within the deadline: return to unicast for arbitration only.\n // MoQ was never suppressed in `joining`, so there is nothing to restore\n // and this edge stays silent on onSuppress (asserted in tests). The\n // multicast socket is still joined and the router still admits this\n // stream's symbols, so the FEC decoder stays armed; the next transport\n // edge releases it.\n this.#transition('unicast', { retainFec: true })\n this.#manager.callbacks?.onTakeoverTimeout?.(this.config.trackName)\n }, deadlineMs)\n }\n\n #clearJoinDeadline(): void {\n if (this.#joinDeadlineTimer !== null) {\n clearTimeout(this.#joinDeadlineTimer)\n this.#joinDeadlineTimer = null\n }\n }\n\n /** Fire fec-setup exactly once per FEC epoch. */\n #armFec(): void {\n if (this.#fecArmed) return\n this.#fecArmed = true\n this.#manager.mmtp?.onTrackEvent?.(this.config.trackName, { type: 'fec-setup' }, this.config)\n }\n\n /** Fire fec-teardown for an armed decoder; no-op when none is armed. */\n #disarmFec(): void {\n if (!this.#fecArmed) return\n this.#fecArmed = false\n this.#manager.mmtp?.onTrackEvent?.(this.config.trackName, { type: 'fec-teardown' }, this.config)\n }\n\n /** Fire onSuppress(true) exactly once per suppression epoch. */\n #suppress(): void {\n if (this.#suppressFired) return\n this.#suppressFired = true\n this.#manager.callbacks?.onSuppress?.(this.config.trackName, true)\n }\n\n /** Unwind an outstanding suppression; no-op when none is outstanding. */\n #unsuppress(): void {\n if (!this.#suppressFired) return\n this.#suppressFired = false\n this.#manager.callbacks?.onSuppress?.(this.config.trackName, false)\n }\n}\n\n// ---------------------------------------------------------------------------\n// SessionManager\n// ---------------------------------------------------------------------------\n\n/**\n * Largest takeover deadline setTimeout represents faithfully. setTimeout\n * coerces its delay to a signed 32-bit int, so a larger value fires on the\n * next tick (Node also warns TimeoutOverflowWarning) instead of after the\n * window the host asked for -- silently inverting the feature. Rejecting it\n * keeps the classic units bug (seconds-as-ms, a wrong timescale) loud.\n */\nexport const MAX_TAKEOVER_DEADLINE_MS = 2_147_483_647\n\n/**\n * How long `joinActive` may stay false before the library says so. The deferral\n * is unbounded by construction -- that is the feature -- so a host that never\n * flips it back parks every track in `joining` with no timer at all: exactly the\n * forever-join the takeover deadline exists to prevent, only silent. MoQ stays\n * unsuppressed so the viewer sees video, but onTakeoverTimeout never fires, the\n * FEC decoder stays armed, and the preference chain never advances past the\n * transport that failed. It looks like a slow join. A host is one un-awaited\n * rejection between `false` and `true` away from it, so the library complains\n * rather than trusting every future host to get all of its exit paths right.\n *\n * Generous against a genuinely slow subscribe -- a bridge reconnect or AMT relay\n * handshake is seconds -- so anything still unsettled here is broken, not slow.\n */\nexport const JOIN_DEFER_WARN_MS = 30_000\n\nexport class SessionManager {\n readonly pool: TransportPool\n readonly sessions = new Map<string, TrackSession>()\n callbacks: SessionManagerCallbacks | null = null\n #takeoverDeadlineMs: number | undefined = undefined\n #joinActive = true\n #joinDeferWarnTimer: ReturnType<typeof setTimeout> | null = null\n\n /**\n * Takeover deadline (BLO-9413/D9): the time a session may sit in `joining`\n * without a RAP before returning to `unicast` and firing onTakeoverTimeout.\n * MoQ is never suppressed during `joining`, so expiry abandons the takeover\n * without a playback glitch. The host derives it from the catalog (RAP\n * cadence from groupDurationTicks/timescale plus the signaled FEC delivery\n * window or reorder tolerance) and sets it before addTrack(). There is no\n * default: an unset value fails addTrack() closed, and reading the getter\n * before assignment throws the same way, so no host ever holds a\n * `number | undefined` it could coalesce back into a default.\n * `hasTakeoverDeadline` answers whether one was admitted. A declared 0\n * disables the deadline. The value is an integer number of milliseconds:\n * setTimeout truncates fractions, so a host that divides ticks by a\n * timescale must round before assigning.\n *\n * Assignment validates, so the diagnostic lands at the host's own call site.\n * That is what bounds the blast radius: `joining` is entered from\n * joinStarted(), from the transportFailed() failover branch -- itself\n * reachable from a setTimeout via hysteresisExpired(), where no caller could\n * catch a throw -- and from the activateMulticast()/activateAmt()/pool-failure\n * loops, where a throw on one session would abandon its siblings mid-loop\n * (the split-brain those loops exist to prevent). Refusing a bad value at\n * assignment means no host mutation can make an already-admitted session\n * throw out of a transport edge.\n */\n get takeoverDeadlineMs(): number {\n return this.#requireTakeoverDeadlineMs()\n }\n\n set takeoverDeadlineMs(value: number) {\n if (!Number.isInteger(value) || value < 0 || value > MAX_TAKEOVER_DEADLINE_MS) {\n throw new Error(\n `[SessionManager] takeoverDeadlineMs must be an integer from 0 to ${MAX_TAKEOVER_DEADLINE_MS} (0 disables it, larger values overflow setTimeout); got ${String(value)}`,\n )\n }\n this.#takeoverDeadlineMs = value\n }\n\n /** True once the host has admitted a deadline; a declared 0 counts. */\n get hasTakeoverDeadline(): boolean {\n return this.#takeoverDeadlineMs !== undefined\n }\n\n /**\n * Whether the transport the joining tracks are waiting on is actually\n * subscribed. The takeover deadline is a media-arrival budget and media\n * cannot arrive before the join exists, so while this is false, entering\n * `joining` arms no deadline; flipping it true starts the FULL deadline for\n * every track already parked there.\n *\n * Default true, which is the arm-on-`joining` behaviour every host had\n * before: a host that only calls joinStarted() after its own join resolves\n * (moqtail-private's player, post-BLO-33339) is already at the right edge and\n * needs no gate. A host that arms joinStarted() up front \u2014 to have FEC\n * decoders ready before the first datagram \u2014 sets this false before addTrack()\n * and true when its subscribe settles. Charging a slow subscribe (bridge\n * reconnect, AMT relay handshake) against the media budget expired the\n * deadline before the subscription existed and abandoned the join on the\n * happy path. BLO-34289.\n *\n * The flag tracks the host's primary join, not per-track transport state. A\n * track that fails over to the next transport in its own chain while this is\n * false (transportFailed's failover branch also enters `joining`) has that\n * deadline gated on the primary join activating, not on its own transport.\n * Under the documented usage -- false once at setup, true once the subscribe\n * settles -- the two coincide.\n *\n * The gate is on the ENTRY into `joining`, and false cannot take back a\n * deadline already running: the `true -> false` edge arms the warn below and\n * clears nothing. So set it false BEFORE the `joining` entry it is meant to\n * gate. A host that instead flipped it false after a `transportFailed()`\n * failover had already entered `joining` would leave the full deadline\n * counting against a subscribe that had not begun -- BLO-34289 again, on the\n * failover edge. Clearing running deadlines on that edge is deliberately NOT\n * done: this flag is manager-wide, so one transport's brief false would\n * cancel an unrelated track's live deadline and then re-arm it at FULL length\n * on the next activation (armDeferredJoinDeadline sees `joining` + a null\n * timer and cannot tell a cleared deadline from a deferred one). Every\n * unrelated transport blip would restart the whole budget, so under repeated\n * failover a track that should have timed out is extended indefinitely and\n * never advances. Only a false that never returns true loses the timer\n * outright, and JOIN_DEFER_WARN_MS already covers that.\n *\n * Leaving this false is not free: see JOIN_DEFER_WARN_MS.\n */\n get joinActive(): boolean {\n return this.#joinActive\n }\n\n set joinActive(active: boolean) {\n const wasActive = this.#joinActive\n this.#joinActive = active\n if (!active) {\n // Edge-triggered: a host re-declaring false has not restarted its\n // subscribe, and total deferral is what the warning is about.\n if (wasActive) this.#armJoinDeferWarn()\n return\n }\n this.#clearJoinDeferWarn()\n if (wasActive) return\n for (const [, session] of this.sessions) session.armDeferredJoinDeadline()\n }\n\n #armJoinDeferWarn(): void {\n this.#joinDeferWarnTimer = setTimeout(() => {\n this.#joinDeferWarnTimer = null\n const deferred = [...this.sessions.values()].filter(s => s.state === 'joining').length\n console.warn(\n `[SessionManager] joinActive has been false for ${JOIN_DEFER_WARN_MS} ms and ${deferred} track(s) sit in 'joining' with no takeover deadline. Set joinActive = true when the subscribe settles, on every exit path including failures, or these tracks never time out, never release their FEC decoders, and never advance to the next transport.`,\n )\n }, JOIN_DEFER_WARN_MS)\n }\n\n #clearJoinDeferWarn(): void {\n if (this.#joinDeferWarnTimer === null) return\n clearTimeout(this.#joinDeferWarnTimer)\n this.#joinDeferWarnTimer = null\n }\n\n /** Narrow to MMTP callbacks (onTrackEvent). Safe \u2014 optional chaining handles PIM-only consumers. */\n get mmtp(): MmtpSessionCallbacks | null {\n return this.callbacks as MmtpSessionCallbacks | null\n }\n\n constructor(poolOptions?: TransportPoolOptions) {\n this.pool = new TransportPool(poolOptions)\n\n // Wire pool failure \u2192 per-track failover\n this.pool.onSubscriptionFailed(sub => {\n for (const trackName of sub.consumers) {\n const session = this.sessions.get(trackName)\n if (session) session.transportFailed()\n }\n })\n }\n\n // -----------------------------------------------------------------------\n // Track management\n // -----------------------------------------------------------------------\n\n /**\n * The admitted takeover deadline. Throws only when the host never set one,\n * every other invalid value being refused by the setter, and returns a\n * declared 0 as 0. The getter and addTrack() both route through it, so a\n * session cannot exist without an admitted deadline, which is what makes\n * the #transition() read unable to throw on an already-admitted session.\n */\n #requireTakeoverDeadlineMs(): number {\n const value = this.#takeoverDeadlineMs\n if (value === undefined) {\n throw new Error(\n '[SessionManager] takeoverDeadlineMs is unset; set a catalog-derived takeover deadline (0 disables it) before adding tracks',\n )\n }\n return value\n }\n\n /** Add a track session from catalog config. */\n addTrack(config: TrackSessionConfig): TrackSession {\n if (this.sessions.has(config.trackName)) {\n throw new Error(`[SessionManager] Track \"${config.trackName}\" already exists`)\n }\n this.#requireTakeoverDeadlineMs()\n const session = new TrackSession(config, this)\n this.sessions.set(config.trackName, session)\n return session\n }\n\n /** Remove a track session. Releases subscription with grace period. */\n removeTrack(trackName: string): void {\n const session = this.sessions.get(trackName)\n if (!session) return\n session.unsubscribe()\n\n // Release pool subscription\n const subKey = session.config.multicastKey ?? session.config.moqTrackName\n if (subKey) this.pool.releaseSubscription(subKey, trackName)\n\n session.dispose()\n this.sessions.delete(trackName)\n }\n\n // -----------------------------------------------------------------------\n // Atomic multi-track operations\n // -----------------------------------------------------------------------\n\n /**\n * All tracks with multicast RAP \u2192 multicast state (atomic).\n * Called when MediaRouter fires onSourceSwitch('multicast').\n * Prevents split-brain between video and audio.\n *\n * A takeover deadline can return a track from `joining` to `unicast` while\n * the multicast socket keeps delivering packets and the track keeps its FEC\n * decoder armed. If a valid RAP arrives later, that late-RAP edge suppresses\n * unicast and promotes multicast without a second fec-setup.\n */\n activateMulticast(): void {\n for (const [, session] of this.sessions) {\n if (session.state === 'unicast') {\n session.joinStarted()\n }\n if (session.state === 'joining') {\n session.rapReceived('multicast')\n }\n }\n }\n\n /**\n * All tracks with AMT RAP \u2192 amt state (atomic).\n */\n activateAmt(): void {\n for (const [, session] of this.sessions) {\n if (session.state === 'unicast') {\n session.joinStarted()\n }\n if (session.state === 'joining') {\n session.rapReceived('amt')\n }\n }\n }\n\n /**\n * All tracks \u2192 unicast (failback, atomic).\n * Called when MulticastManager fires onFailback().\n * unicast-fallback sessions are skipped \u2014 the host owns those subscriptions\n * and they must drain uninterrupted across transport-state events.\n *\n * Coherence invariant (BLO-9413): after this returns, every non-fallback\n * session is `unicast` (or `idle`), unsuppressed, and without a FEC decoder.\n * unsubscribe() unwinds any outstanding suppression regardless of the state\n * it tears down from (`failing` included; see TrackSession.#suppressFired),\n * and a session that a takeover timeout parked in `unicast` with its decoder\n * still armed is cycled too, because this transport edge is what releases\n * that decoder (unsubscribe fires fec-teardown).\n */\n failbackAll(): void {\n for (const [, session] of this.sessions) {\n const parkedWithFec = session.state === 'unicast' && session.isFecArmed\n if (\n parkedWithFec ||\n (session.state !== 'idle' &&\n session.state !== 'unicast' &&\n session.state !== 'unicast-fallback')\n ) {\n session.unsubscribe()\n session.subscribe() // re-enter unicast\n }\n }\n }\n\n /**\n * All tracks \u2192 unicast-fallback (atomic).\n * Called by the host when the multicast gateway is absent and\n * the player will subscribe to source tracks over MoQ unicast and feed\n * packets via MulticastManager.feedUnicastSource() (or manager.router\n * directly). Sessions in this state stay terminal until the host calls\n * unsubscribe() / disposes the SessionManager.\n */\n enterUnicastFallback(): void {\n for (const [, session] of this.sessions) {\n if (session.state === 'idle') continue\n session.enterUnicastFallback()\n }\n }\n\n /**\n * True if any session is currently in unicast-fallback. Hosts use this to\n * gate `multicastTrack.close()` on failback events \u2014 closing the shared\n * broadcast would kill the host's fallback drainers.\n */\n hasActiveFallback(): boolean {\n for (const [, session] of this.sessions) {\n if (session.isUnicastFallback) return true\n }\n return false\n }\n\n /**\n * All fallback tracks \u2192 unicast (atomic).\n * Called when the SSM bridge appears mid-session and the host has aborted\n * the fallback drainers. The session re-enters the regular preference chain\n * via joinStarted() (issued by the host immediately after).\n */\n exitUnicastFallback(): void {\n for (const [, session] of this.sessions) {\n if (session.isUnicastFallback) session.exitUnicastFallback()\n }\n }\n\n // -----------------------------------------------------------------------\n // Cleanup lifecycle\n // -----------------------------------------------------------------------\n\n /** Tab closed \u2014 release all subscriptions for this tab (grace period). */\n tabClosed(tabId: string): void {\n // All tracks owned by this tab \u2192 idle\n for (const [trackName, session] of this.sessions) {\n session.unsubscribe()\n this.pool.releaseAllForConsumer(trackName)\n }\n }\n\n /** Bridge WebTransport connection dropped \u2014 same as tab close. */\n bridgeDisconnected(): void {\n this.tabClosed('bridge')\n }\n\n /**\n * Track selection changed (user or ABR).\n * Old track released (grace period), new track subscribed.\n */\n trackSelectionChanged(oldTrackName: string, newConfig: TrackSessionConfig): void {\n this.removeTrack(oldTrackName)\n const session = this.addTrack(newConfig)\n session.subscribe()\n }\n\n // -----------------------------------------------------------------------\n // Query\n // -----------------------------------------------------------------------\n\n /** Get a track session by name. */\n getTrack(trackName: string): TrackSession | undefined {\n return this.sessions.get(trackName)\n }\n\n // -----------------------------------------------------------------------\n // Cleanup\n // -----------------------------------------------------------------------\n\n dispose(): void {\n this.#clearJoinDeferWarn()\n for (const [, session] of this.sessions) {\n session.dispose()\n }\n this.sessions.clear()\n this.pool.dispose()\n this.callbacks = null\n }\n}\n", "/**\n * FEC Manager \u2014 codec-agnostic FEC decode service.\n *\n * Receives (packetId, sbn, esi, symbolData) tuples from any transport\n * (MMTP, LOC, etc.) and manages FEC block lifecycle: source counting,\n * WASM RaptorQ decode, block timeout, and recovery/failure notification.\n *\n * Extracted from MulticastManager to decouple FEC block logic from\n * MMTP packet parsing and frame routing.\n */\n\nimport { computeFecDeliveryWindowMs } from './track-session.js'\nimport { deriveFecInterleaveDepth } from '@blockcast/mmt-fec'\n\nimport type { MmtpDeliveryProvenance } from './mfu-reassembler.js'\nimport type {\n FecWorkerClient,\n FecTrackStats,\n FecBlockSnapshot,\n FrameMeta,\n FecWorkerErrorCode,\n} from '@blockcast/fec-worker'\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\n/**\n * Minimum valid FEC interleave depth in milliseconds, per spec \u00A75.1\n * (the interactive/gaming floor; broadcast/ATSC 3.0 ranges up to 10000ms).\n *\n * A protocol constant, NOT a tuning knob. Values below this floor are almost\n * certainly a raw frame count (e.g. `2`) that a caller passed when no framerate\n * was available to convert to ms (common on audio tracks). Feeding such a value\n * into computeFecTimeout treats it as `2ms`, yielding a near-zero FEC block\n * timeout that abandons startup FEC blocks before their repair symbols arrive\n * (measured: moqtail startupFailed=4, shaka dropped=5.8% on startup). Timing\n * derivations MUST reject sub-floor values and fall back to the established\n * timeout instead of collapsing.\n */\nexport const MIN_INTERLEAVE_DEPTH_MS = 33\nconst WORKER_ERROR_LOG_CAP = 5\nconst WORKER_ERROR_KIND_CAP = 32\nconst WORKER_ERROR_OVERFLOW_KIND = 'other'\nconst STARTUP_REPAIR_WAIT_SOURCE_BOUNDARIES = 2\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/**\n * Algorithm-agnostic FEC decoder backend interface.\n *\n * Implementations:\n * - RaptorQ WASM (mmt_wasm.js) \u2014 RFC 6330\n * - Reed-Solomon WASM \u2014 future\n * - XOR \u2014 future\n *\n * Source/repair symbol IDs follow ISO 23008-1 \u00A7C.5.2/C.5.3:\n * source: SS_ID (flat 32-bit symbol identifier)\n * repair: SS_Start + SSB_length + RS_ID (block-scoped repair coords)\n */\nexport type WasmFecDecoder = {\n add_source(ss_id: number, data: Uint8Array, ts: number): Uint8Array | null\n add_repair(ss_start: number, ssb_length: number, rs_id: number, data: Uint8Array, ts: number): Uint8Array | null\n configure(oti: Uint8Array): void\n cleanup(current_time: number): Uint8Array\n cleanup_blocks_detailed(sbns: Uint32Array): Uint8Array\n flush?(): void\n reset_stats?(): void\n}\n\n/** Algorithm-agnostic FEC stats snapshot. */\nexport interface FecStats {\n sourcePackets: number\n repairPackets: number\n duplicateSymbols: number\n blocksClean: number\n blocksRecovered: number\n blocksFailed: number\n greenFillFrames: number\n blocksComplete: number\n mixedPathBlocks: number\n recentBlockPathCounts: FecBlockPathCount[]\n k: number\n /** Of blocks with loss, what percentage were recovered (0-100). */\n recoveryRate: number\n /** What percentage of blocks had any loss (0-100). */\n packetLossRate: number\n /** Per-MMTP-packet-ID counters for the synchronous WASM decoder path. */\n tracks: FecPacketStats[]\n /** Latest Worker/WASM snapshot for each packet ID. */\n workers: Array<FecTrackStats & { packetId: number }>\n /** Main-thread repair symbols rejected before dispatch because their size did not match T. */\n repairSymbolSizeMismatches: number\n /** Bounded Worker error counts keyed by stable code, or message text for legacy errors. */\n workerErrors: Record<string, number>\n}\n\nexport interface FecPacketStats {\n packetId: number\n sourcePackets: number\n repairPackets: number\n duplicateSymbols: number\n blocksClean: number\n blocksRecovered: number\n blocksFailed: number\n greenFillFrames: number\n blocksComplete: number\n k: number\n recoveryRate: number\n packetLossRate: number\n}\n\nexport type FecSymbolSource = 'multicast' | 'moq'\n\nexport interface FecBlockPathCount {\n packetId: number\n sbn: number\n catalogTrackName?: string\n multicastSymbols: number\n moqSymbols: number\n complete: boolean\n}\n\ntype FecLifecycleTrace = {\n atMs: number\n event:\n | 'repair-requested'\n | 'repair-arrived'\n | 'timeout-queued'\n | 'cleanup-ack'\n | 'recovered'\n | 'failed'\n | 'decoder-unavailable'\n packetId: number\n sbn: number\n catalogTrackName?: string\n sourceCount: number\n sourceEsis: number[]\n repairRsIds: number[]\n decoderPath: 'worker' | 'sync' | 'none'\n deadlineRemainingMs?: number\n pendingCleanup: boolean\n rsId?: number\n}\n\nexport interface FECManagerConfig {\n k?: number\n /** Symbol size in bytes (T). NOT repair count. */\n symbolSize?: number\n interleaveDepthMs?: number\n fps?: number\n /** Guarded block inactivity deadline derived from the full delivery geometry. */\n timeoutMs?: number\n}\n\ninterface ResolvedFECTrackConfig {\n k: number\n symbolSize: number\n interleaveDepthMs: number\n timeoutMs: number\n fps?: number\n}\n\ninterface MutableFecCounters {\n sourcePackets: number\n repairPackets: number\n duplicateSymbols: number\n blocksClean: number\n blocksRecovered: number\n blocksFailed: number\n greenFillFrames: number\n}\n\nfunction newFecCounters(): MutableFecCounters {\n return {\n sourcePackets: 0,\n repairPackets: 0,\n duplicateSymbols: 0,\n blocksClean: 0,\n blocksRecovered: 0,\n blocksFailed: 0,\n greenFillFrames: 0,\n }\n}\n\n// ---------------------------------------------------------------------------\n// FECManager\n// ---------------------------------------------------------------------------\n\n/** Why a stream's Worker decoder stopped being routable. */\nexport type FecDecoderUnavailableReason = 'cleared' | 'disposed'\n\n/**\n * What still decodes a stream after its Worker left routing: the synchronous\n * WASM decoder registered for the same stream (the Worker path took precedence\n * over it while both were live), or nothing.\n */\nexport type FecFallbackDecoder = 'sync' | 'none'\n\nexport interface FecDecoderUnavailableEvent {\n readonly packetId: number\n readonly catalogTrackName?: string\n readonly reason: FecDecoderUnavailableReason\n /**\n * What decodes the stream at the moment the Worker left. A synchronous\n * decoder discarded later (a resetTrack flush failure drops it without an\n * event) is not reported here; a consumer that keeps a stream alive on\n * 'sync' owns that decoder's lifecycle itself.\n */\n readonly fallbackDecoder: FecFallbackDecoder\n}\n\nexport class FECManager {\n // Per-track WASM RaptorQ decoders, keyed by physical stream identity.\n #fecDecoders = new Map<string, { client: WasmFecDecoder; k: number }>()\n /** @deprecated Use setFecDecoder(packetId, ...). Backwards-compat shim. */\n\n // Per-track Worker-backed FEC decoders, keyed by physical stream identity.\n // Worker-backed tracks use async decode \u2014 recovered frames arrive via onFrame callback,\n // not synchronous return from feedSourceSymbol/feedRepairSymbol.\n #workerClients = new Map<string, FecWorkerClient>()\n // Worker setup can finish while an interleaved block is already collecting\n // on the sync decoder. Keep that block on its original decoder so later\n // source/repair symbols do not split across two RaptorQ states.\n #syncDecoderBlockOwners = new Map<string, WasmFecDecoder>()\n #decoderBlockOwners = new WeakMap<object, Map<number, {\n catalogTrackName?: string\n conflicted: boolean\n }>>()\n\n #fecK = 4\n #fecSymbolSize = 0\n #fecTimeoutMs = 500 // default 500ms; refined by setFramerate() when fps known\n #trackConfigs = new Map<number, ResolvedFECTrackConfig>()\n #fecStats = newFecCounters()\n #fecTrackStats = new Map<number, MutableFecCounters>()\n #fecWorkerStats = new Map<number, FecTrackStats>()\n #fecStatsObservers = new Set<(packetId: number, stats: FecTrackStats) => void>()\n\n // Block inactivity tracking: (packetId, SBN) -> timer. The deadline is\n // derived from catalog geometry and refreshed on progress; only an idle\n // block fails.\n #activeBlocks = new Map<string, ReturnType<typeof setTimeout>>()\n #activeBlockDeadlinesMs = new Map<string, number>()\n #activeBlockTimeoutOwners = new Map<string, object>()\n #lifecycleTrace: FecLifecycleTrace[] = []\n // Worker-backed timeouts enqueue cleanup behind all prior symbol messages.\n // Failure remains pending until cleanupComplete acknowledges that queue drain.\n #pendingWorkerCleanups = new Set<string>()\n #pendingWorkerCleanupClients = new Map<string, FecWorkerClient>()\n // Source symbol count per track-scoped block \u2014 marks complete when K symbols arrive.\n #blockSourceCounts = new Map<string, number>()\n #blockSourceEsis = new Map<string, Set<number>>()\n #blockRepairRsIds = new Map<string, Set<number>>()\n // Blocks already recovered \u2014 prevents late source symbols from restarting timeouts.\n // WASM removes blocks after recovery; late symbols create empty blocks returning null,\n // which would falsely start new timeouts without this guard.\n #completedBlocks = new Set<string>()\n #seenSymbols = new Set<string>()\n #blockPathCounts = new Map<string, FecBlockPathCount>()\n // Exact catalog identity observed on accepted symbols for each FEC block.\n // Conflicting identities collapse to null so recovery fails closed.\n #blockCatalogTrackNames = new Map<string, string | null>()\n // A receiver joins an already-running live stream in the middle of D\n // interleaved source blocks. This applies to both multicast and reliable MoQ:\n // a MoQ group boundary is not an FEC ESI=0 boundary. Those blocks are not\n // packet loss because their earlier ESIs predate the subscription. Ignore\n // each such block until ESI=0 establishes a complete boundary for the track.\n // After that boundary, a first observed ESI>0 is real delivery loss and must\n // continue through normal FEC recovery/failure accounting.\n #startupAlignedPackets = new Set<string>()\n #startupPartialBlocks = new Set<string>()\n // A live multicast join can observe an ESI=0 source boundary before the\n // repair lane catches up. Returning false here also drops that media packet\n // at MmtpRouter, so the readiness probe is multicast-only and bounded: an\n // actual multicast repair unlocks the next boundary, while a second source\n // boundary fails open. Reliable MoQ media continues through normal startup\n // alignment and retained MoQ repair cannot prove multicast readiness.\n #startupRequireRepairReady = false\n #startupRepairReadyPackets = new Set<string>()\n #startupRepairBoundaryCounts = new Map<string, number>()\n #startupRepairFailOpenPackets = new Set<string>()\n // SBNs that reached K source-symbol arrivals via trackBlock. Used by the\n // worker recovery suppression check (see onFrame handler below) so we only\n // suppress `recovered=true` emits for blocks that ALSO completed cleanly\n // via source symbols. Separating this from #completedBlocks (which is also\n // populated by every worker frame emit, by failed timeouts, and by external\n // callers) was the BLO-3323 L15 fix: the prior predicate caused blocksRecovered\n // to stay at 0 because the SBN was always in #completedBlocks by the time the\n // worker fired recovered=true.\n #cleanlyCompletedBlocks = new Set<string>()\n // D-28: blocks whose repair deadline already fired onRepairNeeded. At most\n // one repair request per block; entries are removed when the block\n // completes, recovers, or fails.\n #repairRequestedBlocks = new Set<string>()\n // D-28: repair-deadline timers refreshed by accepted source progress until\n // the first repair request. A `null` value marks a deadline that already\n // fired (never re-armed). At most K accepted source symbols can refresh the\n // horizon, so browser scheduling delay cannot create an early loss signal\n // and repair traffic cannot keep a block alive indefinitely.\n #repairDeadlines = new Map<string, ReturnType<typeof setTimeout> | null>()\n // Worker recovery emits all K source symbols. Track recovery once per block\n // while routing only ESI slots that were absent from the direct source path.\n #workerRecoveredBlocks = new Set<string>()\n\n // Framerate detection state\n #fpsDetected = false\n #fecFps?: number\n #fecInterleaveDepthMs = 0\n\n // Worker error log rate-limiter \u2014 bounds console.warn output and exported\n // error cardinality when the FEC Worker emits the same failure repeatedly\n // with volatile packet details (e.g. ssStart) in the message text.\n // Stable Worker codes aggregate those messages; uncoded/legacy messages are\n // capped into an `other` bucket. This also covers wasm-bindgen\n // \"recursive use of an object\" bubbled from add_source/add_repair during\n // overlapping message processing). Without this, Point 5 of the 7-point\n // validation harness \u2014 which caps headless warnings at 500 \u2014 fails even\n // though the errors are duplicates of a single underlying event.\n #workerErrorCounts = new Map<string, number>()\n #decoderUnavailableEvents = 0\n #repairSymbolSizeMismatchCount = 0\n\n // Event callbacks \u2014 block lifecycle notifications for MmtpRouter wiring.\n // NOTE: For sync WASM path, recovery routing is handled by MmtpRouter from\n // feedSourceSymbol/feedRepairSymbol return values. For Worker path, recovery\n // arrives via onBlockRecovered callback.\n onBlockStarted:\n | ((sbn: number, packetId: number, catalogTrackName?: string) => void)\n | null = null\n onBlockComplete:\n | ((sbn: number, packetId: number, catalogTrackName?: string) => void)\n | null = null\n onBlockFailed:\n | ((sbn: number, packetId?: number, catalogTrackName?: string) => void)\n | null = null\n /**\n * Fired once per stream when its Worker decoder stops being routable.\n * `reason` is 'cleared' when clearFecWorker() removed it and 'disposed' when\n * a symbol or reset found the registered client already disposed and\n * evicted it. `fallbackDecoder` is 'sync' when a synchronous WASM decoder\n * registered for the same stream keeps decoding its symbols, and 'none'\n * when nothing does. Only 'none' leaves the stream unprotected: the host\n * must then register a decoder again or leave the stream, because counting\n * its symbols as handled would hide the loss.\n */\n onDecoderUnavailable: ((event: FecDecoderUnavailableEvent) => void) | null = null\n\n // D-28 on-demand repair callbacks. When onRepairNeeded is registered, each\n // block arms a one-shot repair deadline after the derived K-symbol source\n // horizon; a block still short of K when it fires requests a per-SBN MoQ\n // repair fetch upstream. The inactivity fail bound is unchanged either way \u2014\n // repair and failure are orthogonal timers, so wiring D-28 never delays\n // failure accounting. onRepairResolved fires when a repair-requested block\n // leaves the active set, with the outcome (\"completed\" after clean completion\n // or recovery, \"failed\" at the fail bound) so the upstream in-flight slot\n // (MulticastManager.clearRepairForBlock) can be released and logged.\n onRepairNeeded:\n | ((sbn: number, packetId: number, catalogTrackName?: string) => void)\n | null = null\n onRepairResolved:\n | ((\n sbn: number,\n packetId: number,\n outcome: 'completed' | 'failed',\n catalogTrackName?: string,\n ) => void)\n | null = null\n\n // Worker-path callbacks \u2014 async recovery and stats from FecWorkerClient.\n onBlockRecovered: ((sbn: number, packetId: number, data: ArrayBuffer, meta: FrameMeta, catalogTrackName?: string) => void) | null = null\n onFecStatsUpdate: ((packetId: number, stats: FecTrackStats) => void) | null = null\n onGreenFill: ((sbn: number, packetId: number, data: ArrayBuffer) => void) | null = null\n\n #streamKey(packetId: number, catalogTrackName?: string): string {\n return catalogTrackName === undefined\n ? `${packetId}`\n : `${packetId}:${encodeURIComponent(catalogTrackName)}`\n }\n\n #belongsToPacket(key: string, packetId: number): boolean {\n return Number(key.split(':', 1)[0]) === packetId\n }\n\n #belongsToStream(key: string, packetId: number, catalogTrackName: string): boolean {\n if (!this.#belongsToPacket(key, packetId)) return false\n const parts = key.split(':')\n const encodedTrackName = encodeURIComponent(catalogTrackName)\n if (parts.length === 2) return parts[1] === encodedTrackName\n if (parts.length >= 3) return parts[2] === encodedTrackName\n return false\n }\n\n #packetIdsForStreamKeys(keys: Set<string>): number[] {\n return [...new Set([...keys].map((key) => Number(key.split(':', 1)[0])))]\n }\n\n #parseStreamKey(key: string): { packetId: number; catalogTrackName?: string } {\n const separator = key.indexOf(':')\n if (separator < 0) return { packetId: Number(key) || 0 }\n return {\n packetId: Number(key.slice(0, separator)) || 0,\n catalogTrackName: decodeURIComponent(key.slice(separator + 1)),\n }\n }\n\n #blockKey(sbn: number, packetId: number, catalogTrackName?: string): string {\n const base = `${packetId}:${sbn}`\n return catalogTrackName === undefined\n ? base\n : `${base}:${encodeURIComponent(catalogTrackName)}`\n }\n\n #parseBlockKey(key: string): { packetId: number; sbn: number; catalogTrackName?: string } {\n const [packetIdText, sbnText, ...catalogParts] = key.split(':')\n const catalogTrackName = catalogParts.length > 0\n ? decodeURIComponent(catalogParts.join(':'))\n : undefined\n return {\n packetId: Number(packetIdText) || 0,\n sbn: Number(sbnText) || 0,\n ...(catalogTrackName === undefined ? {} : { catalogTrackName }),\n }\n }\n\n #decoderForPacket(packetId: number, catalogTrackName?: string) {\n return this.#fecDecoders.get(this.#streamKey(packetId, catalogTrackName))\n ?? this.#fecDecoders.get(this.#streamKey(packetId))\n }\n\n #workerForPacket(packetId: number, catalogTrackName?: string) {\n return this.#workerClients.get(this.#streamKey(packetId, catalogTrackName))\n ?? this.#workerClients.get(this.#streamKey(packetId))\n }\n\n /**\n * The stream's Worker client for routing, or undefined after evicting a\n * registered client that was already disposed. A disposed client throws on\n * every command, so it leaves routing here before a symbol reaches it and\n * the stream reports its decoder unavailable instead.\n */\n #routableWorkerForPacket(packetId: number, catalogTrackName?: string): FecWorkerClient | undefined {\n const client = this.#workerForPacket(packetId, catalogTrackName)\n if (client === undefined || client.disposed !== true) return client\n this.#evictDisposedWorker(client)\n return undefined\n }\n\n #claimDecoderBlock(\n client: object,\n sbn: number,\n catalogTrackName?: string,\n ): boolean {\n let blocks = this.#decoderBlockOwners.get(client)\n if (!blocks) {\n blocks = new Map()\n this.#decoderBlockOwners.set(client, blocks)\n }\n const owner = blocks.get(sbn)\n if (!owner) {\n blocks.set(sbn, { catalogTrackName, conflicted: false })\n if (blocks.size > 512) {\n const stale = [...blocks.keys()].slice(0, 256)\n for (const key of stale) blocks.delete(key)\n }\n return true\n }\n if (owner.conflicted) return false\n if (owner.catalogTrackName === catalogTrackName) return true\n owner.conflicted = true\n return false\n }\n\n #decoderBlockCatalogTrackName(client: object, sbn: number): string | undefined {\n const owner = this.#decoderBlockOwners.get(client)?.get(sbn)\n return owner?.conflicted ? undefined : owner?.catalogTrackName\n }\n\n #resetDecoderBlockOwners(client: object, catalogTrackName: string): void {\n const blocks = this.#decoderBlockOwners.get(client)\n if (!blocks) return\n for (const [sbn, owner] of blocks) {\n if (owner.catalogTrackName === catalogTrackName || owner.conflicted) {\n blocks.delete(sbn)\n }\n }\n if (blocks.size === 0) this.#decoderBlockOwners.delete(client)\n }\n\n #discardFecDecoderClient(client: WasmFecDecoder): void {\n for (const [key, entry] of this.#fecDecoders) {\n if (entry.client === client) this.#fecDecoders.delete(key)\n }\n this.#decoderBlockOwners.delete(client)\n }\n\n #assertExclusiveDecoderClient(\n streamKey: string,\n client: object,\n registrations: Iterable<readonly [string, object]>,\n kind: 'decoder' | 'worker',\n ): void {\n for (const [registeredKey, registeredClient] of registrations) {\n if (registeredClient !== client || registeredKey === streamKey) continue\n throw new Error(\n `[FECManager] ${kind} client is already registered for stream ${registeredKey}; ` +\n `create one client per physical stream`,\n )\n }\n }\n\n #statsForPacket(packetId: number): MutableFecCounters {\n let stats = this.#fecTrackStats.get(packetId)\n if (!stats) {\n stats = newFecCounters()\n this.#fecTrackStats.set(packetId, stats)\n }\n return stats\n }\n\n #markPathBlockComplete(sbn: number, packetId: number, catalogTrackName?: string): void {\n const block = this.#blockPathCounts.get(this.#blockKey(sbn, packetId, catalogTrackName))\n if (block) block.complete = true\n }\n\n /** Cancel and forget a block's D-28 repair-deadline timer. */\n #clearRepairDeadline(key: string): void {\n const timer = this.#repairDeadlines.get(key)\n if (timer != null) clearTimeout(timer)\n this.#repairDeadlines.delete(key)\n }\n\n #requestRepair(\n sbn: number,\n packetId: number,\n catalogTrackName?: string,\n ): void {\n const key = this.#blockKey(sbn, packetId, catalogTrackName)\n if (\n this.#completedBlocks.has(key) ||\n !this.onRepairNeeded ||\n this.#repairRequestedBlocks.has(key)\n ) return\n\n const deadline = this.#repairDeadlines.get(key)\n if (deadline) clearTimeout(deadline)\n this.#repairDeadlines.set(key, null)\n this.#repairRequestedBlocks.add(key)\n\n // Preserve the source-progress inactivity deadline and append exactly one\n // bounded repair-fetch grace. Once requested, noteBlockActivity() no\n // longer moves this terminal cleanup boundary.\n const config = this.#resolveTrackConfig(packetId)\n const graceMs = this.#repairGraceMs(config)\n const now = performance.now()\n const currentDeadline = this.#activeBlockDeadlinesMs.get(key) ?? now\n this.#armBlockTimeout(\n sbn,\n packetId,\n catalogTrackName,\n Math.max(1, Math.ceil(Math.max(now, currentDeadline) + graceMs - now)),\n )\n this.#traceLifecycle('repair-requested', sbn, packetId, catalogTrackName)\n if (catalogTrackName === undefined) {\n this.onRepairNeeded(sbn, packetId)\n } else {\n this.onRepairNeeded(sbn, packetId, catalogTrackName)\n }\n }\n\n #traceLifecycle(\n event: FecLifecycleTrace['event'],\n sbn: number,\n packetId: number,\n catalogTrackName?: string,\n rsId?: number,\n ): void {\n const key = this.#blockKey(sbn, packetId, catalogTrackName)\n const deadline = this.#activeBlockDeadlinesMs.get(key)\n const decoderPath = this.#syncDecoderBlockOwners.has(key)\n ? 'sync'\n : this.#workerForPacket(packetId, catalogTrackName)\n ? 'worker'\n : this.#decoderForPacket(packetId, catalogTrackName)\n ? 'sync'\n : 'none'\n this.#lifecycleTrace.push({\n atMs: Date.now(),\n event,\n packetId,\n sbn,\n ...(catalogTrackName === undefined ? {} : { catalogTrackName }),\n sourceCount: this.#blockSourceCounts.get(key) ?? 0,\n sourceEsis: [...(this.#blockSourceEsis.get(key) ?? [])].sort((a, b) => a - b),\n repairRsIds: [...(this.#blockRepairRsIds.get(key) ?? [])].sort((a, b) => a - b),\n decoderPath,\n ...(deadline === undefined\n ? {}\n : { deadlineRemainingMs: Math.max(0, deadline - performance.now()) }),\n pendingCleanup: this.#pendingWorkerCleanups.has(key),\n ...(rsId === undefined ? {} : { rsId }),\n })\n if (this.#lifecycleTrace.length > 64) this.#lifecycleTrace.splice(0, 32)\n }\n\n #defaultTrackConfig(): ResolvedFECTrackConfig {\n return {\n k: this.#fecK,\n symbolSize: this.#fecSymbolSize,\n interleaveDepthMs: this.#fecInterleaveDepthMs,\n fps: this.#fecFps,\n timeoutMs: this.#fecTimeoutMs,\n }\n }\n\n #resolveTrackConfig(packetId: number): ResolvedFECTrackConfig {\n return this.#trackConfigs.get(packetId) ?? this.#defaultTrackConfig()\n }\n\n #computeTimeout(config: {\n k: number\n interleaveDepthMs: number\n fps?: number\n timeoutMs?: number\n }): number {\n if (config.timeoutMs && config.timeoutMs > 0) return config.timeoutMs\n // Only a valid \u00A75.1 ms interleave (>= MIN_INTERLEAVE_DEPTH_MS) drives the\n // timeout. A sub-floor value is a raw frame count (no fps to convert with);\n // fall back to the established #fecTimeoutMs rather than collapse to ~0.\n if (config.interleaveDepthMs >= MIN_INTERLEAVE_DEPTH_MS) {\n return Math.max(\n this.#fecTimeoutMs,\n computeFecDeliveryWindowMs({\n interleaveDepthMs: config.interleaveDepthMs,\n sourceSymbols: config.k,\n mediaUnitsPerSecond: config.fps,\n }),\n )\n }\n return this.#fecTimeoutMs\n }\n\n #repairDeadlineMs(config: ResolvedFECTrackConfig): number {\n const halfFailBoundMs = Math.ceil(config.timeoutMs / 2)\n if (\n config.k > 0 &&\n config.interleaveDepthMs >= MIN_INTERLEAVE_DEPTH_MS &&\n config.fps !== undefined &&\n Number.isFinite(config.fps) &&\n config.fps > 0\n ) {\n const mediaUnitMs = 1000 / config.fps\n const interleaveUnits = deriveFecInterleaveDepth(\n config.interleaveDepthMs,\n mediaUnitMs,\n )\n const sourceHorizonMs = Math.ceil(config.k * interleaveUnits * mediaUnitMs)\n if (sourceHorizonMs < config.timeoutMs) return sourceHorizonMs\n }\n return halfFailBoundMs\n }\n\n #repairGraceMs(config: ResolvedFECTrackConfig): number {\n return Math.max(1, config.timeoutMs - this.#repairDeadlineMs(config))\n }\n\n #armBlockTimeout(\n sbn: number,\n packetId: number,\n catalogTrackName: string | undefined,\n delayMs: number,\n ): void {\n const key = this.#blockKey(sbn, packetId, catalogTrackName)\n const current = this.#activeBlocks.get(key)\n if (current) clearTimeout(current)\n const boundedDelayMs = Math.max(1, Math.ceil(delayMs))\n this.#activeBlockDeadlinesMs.set(key, performance.now() + boundedDelayMs)\n const owner = {}\n this.#activeBlockTimeoutOwners.set(key, owner)\n const timer = setTimeout(() => {\n // clearTimeout() cannot retract a callback that the browser already\n // moved onto its task queue. Only the timer that still owns this\n // block's deadline may fail it or enqueue Worker cleanup.\n if (this.#activeBlockTimeoutOwners.get(key) !== owner) return\n this.#activeBlocks.delete(key)\n this.#activeBlockDeadlinesMs.delete(key)\n this.#activeBlockTimeoutOwners.delete(key)\n this.#traceLifecycle('timeout-queued', sbn, packetId, catalogTrackName)\n\n // A block that began on the synchronous decoder remains owned by that\n // decoder even if Worker setup completed later. Cleanup must follow the\n // same ownership rule as source and repair symbol dispatch.\n const syncOwner = this.#syncDecoderBlockOwners.get(key)\n if (syncOwner) {\n this.#routeSyncCleanup(syncOwner, sbn, packetId)\n this.#failBlock(sbn, packetId, false, catalogTrackName)\n return\n }\n\n const workerClient = this.#routableWorkerForPacket(packetId, catalogTrackName)\n if (workerClient) {\n this.#pendingWorkerCleanups.add(key)\n this.#pendingWorkerCleanupClients.set(key, workerClient)\n workerClient.cleanup(sbn)\n } else {\n const decoder = this.#decoderForPacket(packetId, catalogTrackName)?.client\n if (decoder) {\n this.#routeSyncCleanup(decoder, sbn, packetId)\n this.#failBlock(sbn, packetId, false, catalogTrackName)\n } else {\n this.#failBlock(sbn, packetId, false, catalogTrackName)\n }\n }\n }, boundedDelayMs)\n this.#activeBlocks.set(key, timer)\n }\n\n #upsertTrackConfig(packetId: number, config: FECManagerConfig): ResolvedFECTrackConfig {\n const previous = this.#trackConfigs.get(packetId) ?? this.#defaultTrackConfig()\n const timingChanged =\n config.k !== undefined ||\n config.interleaveDepthMs !== undefined ||\n config.fps !== undefined\n const next: ResolvedFECTrackConfig = {\n k: config.k ?? previous.k,\n symbolSize:\n config.symbolSize != null && config.symbolSize > 0\n ? config.symbolSize\n : previous.symbolSize,\n interleaveDepthMs: config.interleaveDepthMs ?? previous.interleaveDepthMs,\n fps: config.fps ?? previous.fps,\n timeoutMs: config.timeoutMs ?? (timingChanged ? 0 : previous.timeoutMs),\n }\n next.timeoutMs = this.#computeTimeout(next)\n this.#trackConfigs.set(packetId, next)\n return next\n }\n\n #normalizeSymbolData(packetId: number, data: Uint8Array): Uint8Array {\n const symbolSize = this.symbolSizeForPacket(packetId)\n if (symbolSize <= 0 || data.byteLength === symbolSize) return data\n const normalized = new Uint8Array(symbolSize)\n normalized.set(data.subarray(0, symbolSize))\n return normalized\n }\n\n #blockKeysFor(sbn: number, packetId?: number, catalogTrackName?: string): string[] {\n if (packetId != null && catalogTrackName !== undefined) {\n return [this.#blockKey(sbn, packetId, catalogTrackName)]\n }\n\n const keys = new Set<string>()\n for (const source of [\n this.#activeBlocks,\n this.#blockSourceCounts,\n this.#blockCatalogTrackNames,\n ]) {\n for (const key of source.keys()) {\n const block = this.#parseBlockKey(key)\n if (\n block.sbn === sbn &&\n (packetId === undefined || block.packetId === packetId)\n ) keys.add(key)\n }\n }\n for (const source of [this.#completedBlocks, this.#cleanlyCompletedBlocks]) {\n for (const key of source) {\n const block = this.#parseBlockKey(key)\n if (\n block.sbn === sbn &&\n (packetId === undefined || block.packetId === packetId)\n ) keys.add(key)\n }\n }\n\n if (packetId !== undefined) {\n if (keys.size === 0) return [this.#blockKey(sbn, packetId)]\n return keys.size === 1 ? [...keys] : []\n }\n return keys.size > 0 ? [...keys] : [this.#blockKey(sbn, 0, catalogTrackName)]\n }\n\n // -----------------------------------------------------------------------\n // Accessors\n // -----------------------------------------------------------------------\n\n /** Internal state for diagnostics \u2014 not part of the public stats API. */\n get diagnostics() {\n return {\n timeoutMs: this.#fecTimeoutMs,\n activeBlocks: this.#activeBlocks.size,\n completedBlocks: this.#completedBlocks.size,\n pendingCounts: Object.fromEntries(this.#blockSourceCounts),\n fpsDetected: this.#fpsDetected,\n interleaveDepthMs: this.#fecInterleaveDepthMs,\n startupAlignedPackets: this.#packetIdsForStreamKeys(this.#startupAlignedPackets),\n startupPartialBlocks: this.#startupPartialBlocks.size,\n startupRepairReadyPackets: this.#packetIdsForStreamKeys(this.#startupRepairReadyPackets),\n startupRepairFailOpenPackets: this.#packetIdsForStreamKeys(this.#startupRepairFailOpenPackets),\n pendingWorkerCleanups: this.#pendingWorkerCleanups.size,\n decoderUnavailableEvents: this.#decoderUnavailableEvents,\n repairTrackedBlocks: this.#blockRepairRsIds.size,\n lifecycleTrace: this.#lifecycleTrace.map((entry) => ({ ...entry })),\n }\n }\n\n get fecStats(): FecStats {\n const s = this.#fecStats\n const affected = s.blocksRecovered + s.blocksFailed\n const total = s.blocksClean + affected\n const tracks = [...this.#fecTrackStats.entries()]\n .sort(([left], [right]) => left - right)\n .map(([packetId, stats]) => {\n const trackAffected = stats.blocksRecovered + stats.blocksFailed\n const trackTotal = stats.blocksClean + trackAffected\n return {\n ...stats,\n packetId,\n blocksComplete: trackTotal,\n k: this.#resolveTrackConfig(packetId).k,\n recoveryRate: trackAffected > 0\n ? (stats.blocksRecovered / trackAffected) * 100\n : 100,\n packetLossRate: trackTotal > 0 ? (trackAffected / trackTotal) * 100 : 0,\n }\n })\n return {\n ...s,\n blocksComplete: total,\n mixedPathBlocks: Array.from(this.#blockPathCounts.values()).filter(\n (block) => block.multicastSymbols > 0 && block.moqSymbols > 0,\n ).length,\n recentBlockPathCounts: Array.from(this.#blockPathCounts.values()).slice(-24),\n k: this.#fecK,\n // True recovery rate: of blocks that HAD loss, how many were recovered?\n recoveryRate: affected > 0 ? (s.blocksRecovered / affected) * 100 : 100,\n packetLossRate: total > 0 ? (affected / total) * 100 : 0,\n tracks,\n workers: [...this.#fecWorkerStats.entries()]\n .sort(([left], [right]) => left - right)\n .map(([packetId, stats]) => ({\n ...stats,\n config: {\n ...stats.config,\n resolution: stats.config.resolution ? { ...stats.config.resolution } : undefined,\n },\n blocks: stats.blocks.map((block) => ({ ...block })),\n decoder: stats.decoder ? { ...stats.decoder } : undefined,\n altaFailedDetails: stats.altaFailedDetails?.map((detail) => ({ ...detail })),\n packetId,\n })),\n repairSymbolSizeMismatches: this.#repairSymbolSizeMismatchCount,\n workerErrors: Object.fromEntries(this.#workerErrorCounts),\n }\n }\n\n /**\n * Observe Worker-backed per-track snapshots without taking ownership of the\n * legacy single-callback slot. Infrastructure diagnostics and a player may\n * therefore consume the same source event without wrapping or replacing one\n * another's callback.\n */\n addFecStatsObserver(\n observer: (packetId: number, stats: FecTrackStats) => void,\n ): () => void {\n this.#fecStatsObservers.add(observer)\n return () => this.#fecStatsObservers.delete(observer)\n }\n\n /** Start a new multicast transport join without discarding track/decoder state. */\n beginMulticastJoin(): void {\n this.#startupAlignedPackets.clear()\n this.#startupPartialBlocks.clear()\n this.#startupRequireRepairReady = true\n this.#startupRepairReadyPackets.clear()\n this.#startupRepairBoundaryCounts.clear()\n this.#startupRepairFailOpenPackets.clear()\n }\n\n /** Begin a fresh decode epoch for one physical stream. */\n resetTrack(packetId: number, catalogTrackName: string): void {\n if (!Number.isInteger(packetId) || packetId < 0) {\n throw new Error(`[FECManager] invalid packetId for reset: ${packetId}`)\n }\n if (!catalogTrackName) {\n throw new Error('[FECManager] catalogTrackName is required for track reset')\n }\n const resetMap = <T>(map: Map<string, T>): void => {\n for (const key of map.keys()) {\n if (!this.#belongsToStream(key, packetId, catalogTrackName)) continue\n map.delete(key)\n }\n }\n const resetTimerMap = (\n map: Map<string, ReturnType<typeof setTimeout> | null>,\n ): void => {\n for (const [key, timer] of map) {\n if (!this.#belongsToStream(key, packetId, catalogTrackName)) continue\n if (timer != null) clearTimeout(timer)\n map.delete(key)\n }\n }\n const resetSet = (set: Set<string>): void => {\n for (const key of set) {\n if (this.#belongsToStream(key, packetId, catalogTrackName)) set.delete(key)\n }\n }\n\n resetTimerMap(this.#activeBlocks)\n resetTimerMap(this.#repairDeadlines)\n resetMap(this.#activeBlockDeadlinesMs)\n resetMap(this.#activeBlockTimeoutOwners)\n resetMap(this.#pendingWorkerCleanupClients)\n resetMap(this.#blockSourceCounts)\n resetMap(this.#blockSourceEsis)\n resetMap(this.#blockRepairRsIds)\n resetMap(this.#blockPathCounts)\n resetMap(this.#blockCatalogTrackNames)\n resetMap(this.#syncDecoderBlockOwners)\n resetMap(this.#startupRepairBoundaryCounts)\n resetSet(this.#pendingWorkerCleanups)\n resetSet(this.#completedBlocks)\n resetSet(this.#cleanlyCompletedBlocks)\n resetSet(this.#repairRequestedBlocks)\n resetSet(this.#workerRecoveredBlocks)\n resetSet(this.#startupAlignedPackets)\n resetSet(this.#startupPartialBlocks)\n resetSet(this.#startupRepairReadyPackets)\n resetSet(this.#startupRepairFailOpenPackets)\n resetSet(this.#seenSymbols)\n\n const decoderClients = new Set<WasmFecDecoder>()\n for (const [key, entry] of this.#fecDecoders) {\n if (this.#belongsToStream(key, packetId, catalogTrackName)) {\n decoderClients.add(entry.client)\n }\n }\n const clients = new Set<object>()\n for (const client of decoderClients) {\n if (\n typeof client.flush === 'function' &&\n typeof client.reset_stats === 'function'\n ) {\n try {\n client.flush()\n client.reset_stats()\n clients.add(client)\n } catch {\n this.#discardFecDecoderClient(client)\n }\n } else {\n this.#discardFecDecoderClient(client)\n }\n }\n for (const [key, client] of [...this.#workerClients]) {\n if (!this.#belongsToStream(key, packetId, catalogTrackName)) continue\n if (client.disposed === true) {\n // reset() throws on a disposed client; evict it (reporting the\n // decoder unavailable) instead of resetting a Worker that is gone.\n this.#evictDisposedWorker(client)\n continue\n }\n client.reset()\n clients.add(client)\n }\n for (const client of clients) this.#resetDecoderBlockOwners(client, catalogTrackName)\n }\n\n /** Record a transport symbol and reject later duplicates of the same packetId/SBN/ESI. */\n feedSymbolFromSource(\n packetId: number,\n sbn: number,\n esi: number,\n _data: Uint8Array,\n source: FecSymbolSource,\n catalogTrackName?: string,\n ): boolean {\n const blockKey = this.#blockKey(sbn, packetId, catalogTrackName)\n const streamKey = this.#streamKey(packetId, catalogTrackName)\n const k = this.kForPacket(packetId)\n const isSourceSymbol = esi >= 0 && esi < k\n const isStartupPartial = this.#startupPartialBlocks.has(blockKey)\n const isReadyMulticastRepair =\n this.#startupRequireRepairReady &&\n source === 'multicast' &&\n !isSourceSymbol &&\n this.#startupRepairReadyPackets.has(streamKey)\n const awaitingMulticastRepair =\n this.#startupRequireRepairReady &&\n source === 'multicast' &&\n !this.#startupRepairReadyPackets.has(streamKey) &&\n !this.#startupRepairFailOpenPackets.has(streamKey)\n if (awaitingMulticastRepair) {\n if (!isSourceSymbol) {\n this.#startupRepairReadyPackets.add(streamKey)\n this.#startupRepairBoundaryCounts.delete(streamKey)\n this.#markStartupPartial(blockKey)\n return false\n }\n\n if (esi === 0) {\n const boundaries = (this.#startupRepairBoundaryCounts.get(streamKey) ?? 0) + 1\n this.#startupRepairBoundaryCounts.set(streamKey, boundaries)\n if (boundaries >= STARTUP_REPAIR_WAIT_SOURCE_BOUNDARIES) {\n this.#startupRepairBoundaryCounts.delete(streamKey)\n this.#startupRepairFailOpenPackets.add(streamKey)\n } else {\n this.#markStartupPartial(blockKey)\n return false\n }\n } else {\n this.#markStartupPartial(blockKey)\n return false\n }\n }\n if (isSourceSymbol && esi === 0) {\n // ESI=0 establishes a complete source boundary for this SBN. Remove\n // only this block: other interleaved SBNs may still be join partials.\n this.#startupPartialBlocks.delete(blockKey)\n this.#startupAlignedPackets.add(streamKey)\n } else if (\n (isStartupPartial && !isReadyMulticastRepair) ||\n (!this.#startupAlignedPackets.has(streamKey) && !isReadyMulticastRepair)\n ) {\n this.#markStartupPartial(blockKey)\n return false\n }\n\n // A completed block remains authoritative after its individual symbol\n // identities age out of the bounded #seenSymbols cache. Without this\n // guard, a delayed copy from the other transport can recreate the block\n // inside a Worker decoder; that orphan never receives the full source and\n // repair set, so cleanup later reports a false failure/green fill.\n if (this.#completedBlocks.has(blockKey)) {\n this.#fecStats.duplicateSymbols++\n this.#statsForPacket(packetId).duplicateSymbols++\n return false\n }\n\n const symbolKey = `${blockKey}:${esi}`\n if (this.#seenSymbols.has(symbolKey)) {\n this.#fecStats.duplicateSymbols++\n this.#statsForPacket(packetId).duplicateSymbols++\n return false\n }\n this.#seenSymbols.add(symbolKey)\n\n const counts = this.#blockPathCounts.get(blockKey) ?? {\n packetId,\n sbn,\n ...(catalogTrackName === undefined ? {} : { catalogTrackName }),\n multicastSymbols: 0,\n moqSymbols: 0,\n complete: false,\n }\n if (source === 'multicast') counts.multicastSymbols++\n else counts.moqSymbols++\n this.#blockPathCounts.set(blockKey, counts)\n this.#observeBlockCatalogTrackName(blockKey, catalogTrackName)\n\n if (this.#seenSymbols.size > 4096) {\n this.#seenSymbols = new Set(Array.from(this.#seenSymbols).slice(-2048))\n }\n if (this.#blockPathCounts.size > 512) {\n this.#blockPathCounts = new Map(Array.from(this.#blockPathCounts).slice(-256))\n }\n if (this.#blockCatalogTrackNames.size > 512) {\n this.#blockCatalogTrackNames = new Map(\n Array.from(this.#blockCatalogTrackNames).slice(-256),\n )\n }\n\n return true\n }\n\n #observeBlockCatalogTrackName(blockKey: string, catalogTrackName?: string): void {\n if (catalogTrackName === undefined) return\n if (!this.#blockCatalogTrackNames.has(blockKey)) {\n this.#blockCatalogTrackNames.set(blockKey, catalogTrackName)\n return\n }\n if (this.#blockCatalogTrackNames.get(blockKey) !== catalogTrackName) {\n this.#blockCatalogTrackNames.set(blockKey, null)\n }\n }\n\n /** Exact catalog identity retained for async recovery, if unambiguous. */\n catalogTrackNameForBlock(\n sbn: number,\n packetId: number,\n catalogTrackName?: string,\n ): string | undefined {\n if (catalogTrackName !== undefined) {\n return this.#blockCatalogTrackNames.get(\n this.#blockKey(sbn, packetId, catalogTrackName),\n ) ?? undefined\n }\n const names = new Set<string>()\n for (const [key, name] of this.#blockCatalogTrackNames) {\n const block = this.#parseBlockKey(key)\n if (block.packetId === packetId && block.sbn === sbn && name !== null) names.add(name)\n }\n return names.size === 1 ? [...names][0] : undefined\n }\n\n #markStartupPartial(blockKey: string): void {\n this.#startupPartialBlocks.add(blockKey)\n if (this.#startupPartialBlocks.size > 128) {\n this.#startupPartialBlocks = new Set(\n Array.from(this.#startupPartialBlocks).slice(-64),\n )\n }\n }\n\n /** Per-block state snapshot for FecBlockGrid visualization.\n * Returns the most recent blocks (active + recently completed). */\n getBlockStates(): Array<{\n sbn: number;\n packetId?: number;\n state: 'collecting' | 'complete' | 'failed';\n sourceCount: number;\n k: number;\n progress: number;\n }> {\n const blocks: Array<{ sbn: number; packetId?: number; state: 'collecting' | 'complete' | 'failed'; sourceCount: number; k: number; progress: number }> = [];\n // Active blocks (still collecting symbols)\n for (const [key] of this.#activeBlocks) {\n const { packetId, sbn } = this.#parseBlockKey(key);\n const count = this.#blockSourceCounts.get(key) ?? 0;\n const k = this.kForPacket(packetId);\n blocks.push({ sbn, packetId, state: 'collecting', sourceCount: count, k, progress: k > 0 ? Math.min(count / k, 1) : 0 });\n }\n // Recently completed blocks (last 16)\n const completed = [...this.#completedBlocks].slice(-16);\n for (const key of completed) {\n const { packetId, sbn } = this.#parseBlockKey(key);\n if (!this.#activeBlocks.has(key)) {\n const k = this.kForPacket(packetId);\n blocks.push({ sbn, packetId, state: 'complete', sourceCount: k, k, progress: 1 });\n }\n }\n blocks.sort((a, b) => a.sbn - b.sbn || (a.packetId ?? 0) - (b.packetId ?? 0));\n return blocks.slice(-24); // cap at 24 most recent\n }\n\n /** The raw WASM decoder for stats access (first registered decoder) */\n get wasmDecoder(): WasmFecDecoder | null {\n return this.#fecDecoders.values().next().value?.client ?? null\n }\n\n get k(): number { return this.#fecK }\n kForPacket(packetId: number): number { return this.#resolveTrackConfig(packetId).k }\n /** FEC symbol size in bytes. */\n get symbolSize(): number { return this.#fecSymbolSize }\n symbolSizeForPacket(packetId: number): number { return this.#resolveTrackConfig(packetId).symbolSize }\n /** FEC block span in milliseconds (0 = no interleaving). */\n get interleaveDepthMs(): number { return this.#fecInterleaveDepthMs }\n interleaveDepthMsForPacket(packetId: number): number { return this.#resolveTrackConfig(packetId).interleaveDepthMs }\n /** Resolved per-track block fail bound in milliseconds. */\n timeoutMsForPacket(packetId: number): number { return this.#resolveTrackConfig(packetId).timeoutMs }\n /** Resolved per-track D-28 repair request deadline in milliseconds. */\n repairDeadlineMsForPacket(packetId: number): number {\n return this.#repairDeadlineMs(this.#resolveTrackConfig(packetId))\n }\n /** Remaining fetch budget for a specific repair-requested block. */\n repairFetchTimeoutMsForBlock(\n sbn: number,\n packetId: number,\n catalogTrackName?: string,\n ): number {\n const deadlineMs = this.#activeBlockDeadlinesMs.get(\n this.#blockKey(sbn, packetId, catalogTrackName),\n )\n if (deadlineMs !== undefined) {\n return Math.max(1, Math.ceil(deadlineMs - performance.now()))\n }\n const trackConfig = this.#resolveTrackConfig(packetId)\n return Math.max(1, trackConfig.timeoutMs - this.#repairDeadlineMs(trackConfig))\n }\n\n // -----------------------------------------------------------------------\n // Configuration\n // -----------------------------------------------------------------------\n\n /** Configure FEC parameters. */\n configure(config: FECManagerConfig): void {\n if (config.k != null) this.#fecK = config.k\n if (config.symbolSize != null && config.symbolSize > 0) this.#fecSymbolSize = config.symbolSize\n if (config.interleaveDepthMs != null) this.#fecInterleaveDepthMs = config.interleaveDepthMs\n if (config.fps != null && Number.isFinite(config.fps) && config.fps > 0) {\n this.#fecFps = config.fps\n }\n // interleaveDepthMs is in milliseconds per spec \u00A75.1.\n // Valid range: 33ms (interactive/gaming) to 10000ms (broadcast ATSC 3.0).\n // Warn if value looks like frame count instead of ms (common misconfiguration).\n if (config.interleaveDepthMs != null && config.interleaveDepthMs > 0 && config.interleaveDepthMs < MIN_INTERLEAVE_DEPTH_MS) {\n console.warn(`[FECManager] interleaveDepthMs=${config.interleaveDepthMs} looks like frame count, not milliseconds (\u00A75.1 requires ms). Expected range: ${MIN_INTERLEAVE_DEPTH_MS}-10000ms.`)\n }\n if (config.timeoutMs != null && config.timeoutMs > 0) {\n this.#fecTimeoutMs = config.timeoutMs\n } else if (\n config.interleaveDepthMs != null &&\n config.interleaveDepthMs >= MIN_INTERLEAVE_DEPTH_MS\n ) {\n this.#fecTimeoutMs = computeFecDeliveryWindowMs({\n interleaveDepthMs: config.interleaveDepthMs,\n sourceSymbols: this.#fecK,\n symbolSize: this.#fecSymbolSize,\n mediaUnitsPerSecond: config.fps,\n })\n }\n // A shared multicast parser receives every packetId. Keep existing track\n // deadlines at least as wide as the selected video delivery envelope so a\n // short audio interleave cannot expire during browser/AMT scheduling gaps.\n for (const [packetId, track] of this.#trackConfigs) {\n if (track.timeoutMs < this.#fecTimeoutMs) {\n this.#trackConfigs.set(packetId, { ...track, timeoutMs: this.#fecTimeoutMs })\n }\n }\n }\n\n /** Configure FEC parameters for one packetId without changing legacy defaults. */\n configureTrack(packetId: number, config: FECManagerConfig): void {\n this.#upsertTrackConfig(packetId, config)\n }\n\n /** Register a FEC decoder for a specific packetId (track). */\n setFecDecoder(\n packetId: number,\n client: WasmFecDecoder,\n k: number,\n config?: FECManagerConfig,\n catalogTrackName?: string,\n ): void {\n const streamKey = this.#streamKey(packetId, catalogTrackName)\n this.#assertExclusiveDecoderClient(\n streamKey,\n client,\n [...this.#fecDecoders].map(([key, entry]) => [key, entry.client] as const),\n 'decoder',\n )\n const trackConfig = this.#upsertTrackConfig(packetId, { ...config, k })\n this.#fecDecoders.set(streamKey, { client, k: trackConfig.k })\n }\n\n /**\n * Register a Worker-backed FEC decoder for a specific packetId (track).\n *\n * Worker-backed tracks decode asynchronously \u2014 feedSourceSymbol/feedRepairSymbol\n * return null immediately, and recovered frames arrive via the onBlockRecovered callback.\n *\n * Takes precedence over sync WASM decoders for the same packetId.\n */\n setFecWorker(\n packetId: number,\n client: FecWorkerClient,\n config: FECManagerConfig,\n catalogTrackName?: string,\n ): void {\n const k = config?.k\n if (k === undefined || !Number.isFinite(k) || k <= 0) {\n throw new Error(`[FECManager] packetId=${packetId} Worker requires an explicit positive k`)\n }\n const symbolSize = config.symbolSize\n if (symbolSize === undefined || !Number.isFinite(symbolSize) || symbolSize <= 0) {\n throw new Error(\n `[FECManager] packetId=${packetId} Worker requires an explicit positive symbolSize`,\n )\n }\n const streamKey = this.#streamKey(packetId, catalogTrackName)\n this.#assertExclusiveDecoderClient(\n streamKey,\n client,\n this.#workerClients,\n 'worker',\n )\n // Every Worker leaves routing through #retireWorker, which fails the\n // blocks it still owed and reports the stream's decoder unavailable.\n // Overwriting a registration would skip that path and strand those\n // blocks, so a second Worker under a routed stream is refused.\n const incumbent = this.#workerClients.get(streamKey)\n if (incumbent !== undefined && incumbent !== client) {\n throw new Error(\n `[FECManager] stream ${streamKey} already routes to a Worker; ` +\n `call clearFecWorker before registering another`,\n )\n }\n if (config) this.#upsertTrackConfig(packetId, config)\n const workerK = this.#resolveTrackConfig(packetId).k\n this.#workerClients.set(streamKey, client)\n\n client.onFrame((data: ArrayBuffer, meta: FrameMeta) => {\n if (meta.recovered) {\n // Recovered Worker events represent one exact source-symbol position.\n // Block-completion accounting needs that exact ESI, and counting an\n // ESI-less event as recovered would report healthy FEC while dropping\n // the media. Enforce the Worker contract before any recovery metric.\n if (!Number.isInteger(meta.esi) || meta.esi! < 0 || meta.esi! >= workerK) {\n const kind = 'recovered-symbol-missing-esi'\n const seen = this.#workerErrorCounts.get(kind) ?? 0\n this.#workerErrorCounts.set(kind, seen + 1)\n if (seen < WORKER_ERROR_LOG_CAP) {\n console.warn(\n `[FECManager] Dropping recovered symbol without valid ESI: pid=${packetId} sbn=${meta.sbn} esi=${String(meta.esi)}`,\n )\n }\n return\n }\n const esi = meta.esi!\n\n // Suppress duplicate routing when the worker fires recovered=true\n // for a block ALREADY CLEANLY COMPLETED by K source symbols.\n //\n // BLO-3323 L15: predicate was previously `#completedBlocks.has(sbn)`,\n // but `#completedBlocks` is populated by 5 paths (clean K-arrival,\n // failed timeout, every worker frame emit via markBlockCompleted below,\n // external markBlockCompleted, and cancelBlockTimeout). The first\n // worker frame for a block adds the SBN, so subsequent recovered=true\n // emits for the same block were always suppressed \u2192 blocksRecovered\n // stayed at 0 even when recovery actually fired. Switching to the\n // narrower #cleanlyCompletedBlocks set (populated only by trackBlock\n // when count >= K) preserves the original dedup intent (BLO-3035\n // layer 4 \u2014 don't double-route a block already received via source)\n // while letting genuinely-recovered blocks bump the counter.\n const recoveredCatalogTrackName = catalogTrackName\n ?? this.#decoderBlockCatalogTrackName(client, meta.sbn)\n if (!this.#claimDecoderBlock(client, meta.sbn, recoveredCatalogTrackName)) return\n const blockKey = this.#blockKey(meta.sbn, packetId, recoveredCatalogTrackName)\n const cleanlyCompleted = this.#cleanlyCompletedBlocks.has(blockKey)\n if (!cleanlyCompleted) {\n if (!this.#workerRecoveredBlocks.has(blockKey)) {\n this.#traceLifecycle(\n 'recovered',\n meta.sbn,\n packetId,\n recoveredCatalogTrackName,\n )\n this.#workerRecoveredBlocks.add(blockKey)\n this.#fecStats.blocksRecovered++\n this.#statsForPacket(packetId).blocksRecovered++\n if (this.#workerRecoveredBlocks.size > 1000) {\n const stale = Array.from(this.#workerRecoveredBlocks).slice(0, 500)\n for (const key of stale) this.#workerRecoveredBlocks.delete(key)\n }\n }\n\n // RaptorQ returns the full K-symbol block, not only erased symbols.\n // The direct path already parsed every source ESI in #seenSymbols;\n // routing those symbols again injects duplicate MFU fragments and can\n // leave a real FI=3 tail followed by duplicate FI=2 fragments.\n const symbolKey = `${blockKey}:${esi}`\n if (!this.#seenSymbols.has(symbolKey)) {\n this.#seenSymbols.add(symbolKey)\n this.onBlockRecovered?.(\n meta.sbn,\n packetId,\n data,\n meta,\n recoveredCatalogTrackName,\n )\n }\n }\n\n // The worker emits recovered source symbols in ESI order. Complete the\n // block only after the final slot so assemblers receive every missing\n // symbol before their completion callback flushes buffered media.\n if (\n esi >= workerK - 1 &&\n !this.#completedBlocks.has(blockKey)\n ) {\n this.cancelBlockTimeout(meta.sbn, packetId, recoveredCatalogTrackName)\n if (recoveredCatalogTrackName === undefined) {\n this.onBlockComplete?.(meta.sbn, packetId)\n } else {\n this.onBlockComplete?.(\n meta.sbn,\n packetId,\n recoveredCatalogTrackName,\n )\n }\n }\n } else {\n const completedCatalogTrackName = catalogTrackName\n ?? this.#decoderBlockCatalogTrackName(client, meta.sbn)\n this.markBlockCompleted(meta.sbn, packetId, completedCatalogTrackName)\n }\n })\n\n client.onSnapshot((stats: FecTrackStats) => {\n this.#fecWorkerStats.set(packetId, stats)\n try {\n this.onFecStatsUpdate?.(packetId, stats)\n } catch (error) {\n console.warn('[FECManager] FEC stats callback failed:', error)\n }\n for (const observer of this.#fecStatsObservers) {\n try {\n observer(packetId, stats)\n } catch (error) {\n console.warn('[FECManager] FEC stats observer failed:', error)\n }\n }\n })\n\n client.onGreenFill((sbn: number, data: ArrayBuffer) => {\n this.#fecStats.greenFillFrames++\n this.#statsForPacket(packetId).greenFillFrames++\n this.onGreenFill?.(sbn, packetId, data)\n })\n\n client.onCleanupComplete((sbn: number) => {\n const cleanupKeys = [...this.#pendingWorkerCleanups].filter((key) => {\n const block = this.#parseBlockKey(key)\n return block.packetId === packetId &&\n block.sbn === sbn &&\n this.#pendingWorkerCleanupClients.get(key) === client\n })\n for (const key of cleanupKeys) this.#settleOwedCleanup(key, 'cleanup-ack')\n })\n\n client.onError((msg: string, code?: FecWorkerErrorCode) => {\n const requestedKind = code ?? msg\n const kind =\n this.#workerErrorCounts.has(requestedKind) ||\n this.#workerErrorCounts.size < WORKER_ERROR_KIND_CAP - 1\n ? requestedKind\n : WORKER_ERROR_OVERFLOW_KIND\n const seen = this.#workerErrorCounts.get(kind) ?? 0\n this.#workerErrorCounts.set(kind, seen + 1)\n if (seen < WORKER_ERROR_LOG_CAP) {\n console.warn('[FECManager] Worker error:', msg)\n } else if (seen === WORKER_ERROR_LOG_CAP) {\n console.warn(\n `[FECManager] Worker error (suppressing further copies of ${kind}): ${msg}`,\n )\n }\n })\n }\n\n /**\n * Remove a stream's Worker decoder from routing.\n *\n * Returns false when no Worker is registered under that exact stream key.\n * On success every cleanup acknowledgement still owed by the client is\n * resolved by failing its block now (a Worker that is going away cannot\n * post it), the client's block ownership is released, and\n * onDecoderUnavailable fires once with reason 'cleared' and the decoder\n * that remains for the stream ('sync' or 'none'). Call this before\n * disposing the client; a client disposed while still registered is\n * evicted on its next symbol or reset with reason 'disposed'.\n */\n clearFecWorker(packetId: number, catalogTrackName?: string): boolean {\n const streamKey = this.#streamKey(packetId, catalogTrackName)\n const client = this.#workerClients.get(streamKey)\n if (!client) return false\n this.#retireWorker(streamKey, client, 'cleared')\n return true\n }\n\n /**\n * Take a Worker client out of routing under one stream key, fail the blocks\n * whose completion it still owed, and report the stream's decoder\n * unavailable together with whatever still decodes the stream.\n */\n #retireWorker(streamKey: string, client: FecWorkerClient, reason: FecDecoderUnavailableReason): void {\n this.#workerClients.delete(streamKey)\n this.#releaseWorkerClient(client)\n this.#decoderUnavailableEvents++\n const stream = this.#parseStreamKey(streamKey)\n const fallbackDecoder: FecFallbackDecoder =\n this.#decoderForPacket(stream.packetId, stream.catalogTrackName) ? 'sync' : 'none'\n this.onDecoderUnavailable?.({\n packetId: stream.packetId,\n ...(stream.catalogTrackName === undefined ? {} : { catalogTrackName: stream.catalogTrackName }),\n reason,\n fallbackDecoder,\n })\n }\n\n /**\n * Fail every block whose Worker cleanup acknowledgement is owed by `client`\n * and drop its block ownership, so no block waits on a message the Worker\n * will never post.\n */\n #releaseWorkerClient(client: FecWorkerClient): void {\n const owed = [...this.#pendingWorkerCleanupClients]\n .filter(([, owner]) => owner === client)\n .map(([key]) => key)\n for (const key of owed) this.#settleOwedCleanup(key, 'decoder-unavailable')\n this.#decoderBlockOwners.delete(client)\n }\n\n /**\n * Resolve one owed Worker cleanup: drop the pending entry and fail the block\n * unless it already completed. Shared by the cleanup acknowledgement and by\n * Worker retirement, so the two paths cannot drift in their bookkeeping.\n */\n #settleOwedCleanup(key: string, event: 'cleanup-ack' | 'decoder-unavailable'): void {\n this.#pendingWorkerCleanups.delete(key)\n this.#pendingWorkerCleanupClients.delete(key)\n if (this.#completedBlocks.has(key)) return\n const block = this.#parseBlockKey(key)\n this.#traceLifecycle(event, block.sbn, block.packetId, block.catalogTrackName)\n this.#failBlock(block.sbn, block.packetId, false, block.catalogTrackName)\n }\n\n /** Evict a disposed client under every stream key that still maps to it. */\n #evictDisposedWorker(client: FecWorkerClient): void {\n for (const [key, registered] of [...this.#workerClients]) {\n if (registered !== client) continue\n const stream = this.#parseStreamKey(key)\n const track = stream.catalogTrackName === undefined ? '' : ` track=${stream.catalogTrackName}`\n console.warn(\n `[FECManager] packetId=${stream.packetId}${track} Worker decoder was disposed while still registered; evicting it`,\n )\n this.#retireWorker(key, client, 'disposed')\n }\n }\n\n /** Called by player when framerate is known (from decoder output). Refines FEC block timeout. */\n setFramerate(fps: number): void {\n if (this.#fpsDetected || fps <= 0) return\n this.#fpsDetected = true\n this.#fecFps = fps\n // Only a valid \u00A75.1 ms interleave refines the timeout; a sub-floor value is\n // a raw frame count and would collapse it (see MIN_INTERLEAVE_DEPTH_MS).\n if (this.#fecInterleaveDepthMs >= MIN_INTERLEAVE_DEPTH_MS) {\n this.#fecTimeoutMs = computeFecDeliveryWindowMs({\n interleaveDepthMs: this.#fecInterleaveDepthMs,\n sourceSymbols: this.#fecK,\n symbolSize: this.#fecSymbolSize,\n mediaUnitsPerSecond: fps,\n })\n }\n for (const [packetId, config] of this.#trackConfigs) {\n if (config.fps !== undefined || config.interleaveDepthMs < MIN_INTERLEAVE_DEPTH_MS) continue\n this.#trackConfigs.set(packetId, {\n ...config,\n fps,\n timeoutMs: Math.max(\n this.#fecTimeoutMs,\n computeFecDeliveryWindowMs({\n interleaveDepthMs: config.interleaveDepthMs,\n sourceSymbols: config.k,\n symbolSize: config.symbolSize,\n mediaUnitsPerSecond: fps,\n }),\n ),\n })\n }\n }\n\n // -----------------------------------------------------------------------\n // Symbol feeding (codec-agnostic)\n // -----------------------------------------------------------------------\n\n /**\n * Feed a source symbol to the WASM FEC decoder.\n *\n * Does NOT track block source counts or fire lifecycle events \u2014 call\n * `trackBlock()` separately BEFORE this method (MmtpRouter does this).\n *\n * @returns Recovered block data if WASM completed the block, else null.\n * Caller is responsible for routing recovered data and updating\n * block completion state via `markBlockCompleted()`.\n */\n feedSourceSymbol(\n packetId: number,\n ssId: number,\n symbolData: Uint8Array,\n alta?: { auth: Uint8Array; payloadEnd: number },\n catalogTrackName?: string,\n ): Uint8Array | null {\n this.#fecStats.sourcePackets++\n this.#statsForPacket(packetId).sourcePackets++\n const fecData = this.#normalizeSymbolData(packetId, symbolData)\n const sbn = Math.floor(ssId / this.kForPacket(packetId))\n const blockKey = this.#blockKey(sbn, packetId, catalogTrackName)\n const syncBlockOwner = this.#syncDecoderBlockOwners.get(blockKey)\n\n // Worker-backed track: dispatch async, return null (recovery via onFrame callback)\n const workerClient = this.#routableWorkerForPacket(packetId, catalogTrackName)\n if (workerClient && !syncBlockOwner) {\n if (!this.#claimDecoderBlock(workerClient, sbn, catalogTrackName)) return null\n // Create clean ArrayBuffer copy \u2014 prevents transferring a shared underlying buffer\n // when symbolData is a view into a larger packet (T-03-06)\n const buf = fecData.buffer.slice(\n fecData.byteOffset,\n fecData.byteOffset + fecData.byteLength,\n ) as ArrayBuffer\n // Pass the flat SS_ID through to the worker. The WASM decoder derives\n // SBN/ESI internally per ISO 23008-1 \u00A7C.5.2; the WASM add_source\n // signature is (ss_id, data, timestamp) \u2014 pre-computing sbn/esi on this\n // side produces wrong WASM argument alignment and silently breaks\n // block assembly.\n if (alta) {\n // Copy auth into a fresh ArrayBuffer to transfer zero-copy. The\n // underlying buffer of the parsed trailer view belongs to the wire\n // packet, which may be transferred separately or reused.\n const authBuf = alta.auth.buffer.slice(\n alta.auth.byteOffset,\n alta.auth.byteOffset + alta.auth.byteLength,\n ) as ArrayBuffer\n workerClient.feedSource(ssId, buf, performance.now(), {\n auth: authBuf,\n payloadEnd: alta.payloadEnd,\n })\n } else {\n workerClient.feedSource(ssId, buf, performance.now())\n }\n return null\n }\n\n // Synchronous WASM path.\n const entry = this.#decoderForPacket(packetId, catalogTrackName)\n const client = syncBlockOwner ?? entry?.client\n if (!client) return null\n if (!this.#claimDecoderBlock(client, sbn, catalogTrackName)) return null\n this.#syncDecoderBlockOwners.set(blockKey, client)\n\n let recovered: Uint8Array | null = null\n try {\n recovered = client.add_source(ssId, fecData, performance.now())\n } catch {\n // WASM RefCell panic \u2014 decoder instance shared with multicast repair path\n }\n if (recovered || this.#completedBlocks.has(blockKey)) {\n this.#syncDecoderBlockOwners.delete(blockKey)\n }\n\n return recovered\n }\n\n /**\n * Feed a repair symbol to the WASM FEC decoder.\n *\n * @returns Recovered block data if repair completed the block, else null.\n * Caller is responsible for routing recovered data and updating\n * block completion state via `markBlockCompleted()`.\n */\n feedRepairSymbol(\n packetId: number,\n ssStart: number,\n ssbLength: number,\n rsId: number,\n repairData: Uint8Array,\n catalogTrackName?: string,\n ): Uint8Array | null {\n this.#fecStats.repairPackets++\n this.#statsForPacket(packetId).repairPackets++\n const symbolSize = this.symbolSizeForPacket(packetId)\n if (symbolSize > 0 && repairData.byteLength !== symbolSize) {\n this.#repairSymbolSizeMismatchCount++\n if (this.#repairSymbolSizeMismatchCount <= 5 || this.#repairSymbolSizeMismatchCount % 100 === 0) {\n console.warn(\n `[FECManager] Dropping repair symbol with invalid size: got ${repairData.byteLength}B, expected ${symbolSize}B (packetId=${packetId}, ssStart=${ssStart}, ssbLength=${ssbLength}, rsId=${rsId})`,\n )\n }\n return null\n }\n const fecData = repairData\n\n // SSB_length is the authored source-symbol count in this block, not the\n // decoder's configured block width. Short authored blocks are padded to\n // the configured K, so all internal keys and lifecycle coordinates use K.\n // Keep the wire tuple unchanged when calling the decoder below.\n const k = this.kForPacket(packetId)\n if (ssbLength <= 0 || ssbLength > k || ssStart % k !== 0) return null\n const sbn = Math.floor(ssStart / k)\n\n // Repair symbols are block activity: refresh the inactivity fail bound so\n // a block being actively repaired over MoQ is not failed mid-recovery.\n // The multicast in-band path already does this at the router; this covers\n // the MoQ backfill path, which bypasses the router's packet accounting.\n this.noteBlockActivity(sbn, packetId, catalogTrackName)\n const blockKey = this.#blockKey(sbn, packetId, catalogTrackName)\n if (this.#completedBlocks.has(blockKey)) return null\n const syncBlockOwner = this.#syncDecoderBlockOwners.get(blockKey)\n let repairIds = this.#blockRepairRsIds.get(blockKey)\n if (!repairIds) {\n repairIds = new Set()\n this.#blockRepairRsIds.set(blockKey, repairIds)\n }\n repairIds.add(rsId)\n if (this.#repairRequestedBlocks.has(blockKey)) {\n this.#traceLifecycle('repair-arrived', sbn, packetId, catalogTrackName, rsId)\n }\n\n // Worker-backed track: dispatch async, return null (recovery via onFrame callback)\n const workerClient = this.#routableWorkerForPacket(packetId, catalogTrackName)\n if (workerClient && !syncBlockOwner) {\n if (!this.#claimDecoderBlock(workerClient, sbn, catalogTrackName)) return null\n // Create clean ArrayBuffer copy (T-03-06)\n const buf = fecData.buffer.slice(\n fecData.byteOffset,\n fecData.byteOffset + fecData.byteLength,\n ) as ArrayBuffer\n // Forward the wire-level Repair FEC Payload ID (ISO 23008-1 \u00A7C.5.3)\n // directly. WASM add_repair signature is\n // (ss_start, ssb_length, rs_id, data, timestamp) \u2014 pre-computing\n // (sbn, esi) on this side misaligns WASM args and produces 0% recovery.\n workerClient.feedRepair(ssStart, ssbLength, rsId, buf, performance.now())\n return null\n }\n\n // Synchronous WASM path.\n const entry = this.#decoderForPacket(packetId, catalogTrackName)\n const client = syncBlockOwner ?? entry?.client\n if (!client) return null\n if (!this.#claimDecoderBlock(client, sbn, catalogTrackName)) return null\n this.#syncDecoderBlockOwners.set(blockKey, client)\n\n let recovered: Uint8Array | null = null\n try {\n recovered = client.add_repair(ssStart, ssbLength, rsId, fecData, performance.now())\n } catch {\n // WASM add_repair failure \u2014 block will timeout and be marked failed\n }\n if (recovered) this.#syncDecoderBlockOwners.delete(blockKey)\n\n return recovered\n }\n\n #routeSyncCleanup(\n decoder: WasmFecDecoder,\n sbn: number,\n packetId: number,\n ): void {\n let framed: Uint8Array\n try {\n framed = decoder.cleanup_blocks_detailed(Uint32Array.of(sbn))\n } catch (error) {\n console.warn(`[FECManager] Scoped cleanup failed for SBN ${sbn}:`, error)\n return\n }\n\n const view = new DataView(framed.buffer, framed.byteOffset, framed.byteLength)\n let offset = 0\n while (offset < framed.byteLength) {\n if (framed.byteLength - offset < 8) {\n console.warn(`[FECManager] Scoped cleanup returned a truncated record for SBN ${sbn}`)\n return\n }\n const fillSbn = view.getUint32(offset, false)\n const length = view.getUint32(offset + 4, false)\n offset += 8\n if (length > framed.byteLength - offset) {\n console.warn(`[FECManager] Scoped cleanup returned a truncated payload for SBN ${sbn}`)\n return\n }\n const data = framed.slice(offset, offset + length).buffer as ArrayBuffer\n offset += length\n this.#fecStats.greenFillFrames++\n this.#statsForPacket(packetId).greenFillFrames++\n this.onGreenFill?.(fillSbn, packetId, data)\n }\n }\n\n // -----------------------------------------------------------------------\n // Block lifecycle\n // -----------------------------------------------------------------------\n\n /**\n * Track a source symbol for an FEC block. Refreshes the inactivity deadline\n * on each symbol and marks the block clean when K source symbols arrive.\n */\n trackBlock(\n sbn: number,\n packetId: number,\n catalogTrackName?: string,\n esi?: number,\n ): void {\n const key = this.#blockKey(sbn, packetId, catalogTrackName)\n const trackConfig = this.#resolveTrackConfig(packetId)\n const k = trackConfig.k\n\n // Skip blocks already recovered \u2014 late source symbols must not restart timeouts\n if (this.#completedBlocks.has(key)) return\n\n // Count source symbols per block\n const count = (this.#blockSourceCounts.get(key) ?? 0) + 1\n this.#blockSourceCounts.set(key, count)\n if (Number.isInteger(esi) && esi! >= 0 && esi! < k) {\n let sourceEsis = this.#blockSourceEsis.get(key)\n if (!sourceEsis) {\n sourceEsis = new Set()\n this.#blockSourceEsis.set(key, sourceEsis)\n }\n sourceEsis.add(esi!)\n }\n if (count === 1) {\n if (catalogTrackName === undefined) {\n this.onBlockStarted?.(sbn, packetId)\n } else {\n this.onBlockStarted?.(sbn, packetId, catalogTrackName)\n }\n }\n\n // When K source symbols received, block is clean (no loss, no FEC needed).\n if (k > 0 && count >= k) {\n this.#fecStats.blocksClean++\n this.#statsForPacket(packetId).blocksClean++\n this.cancelBlockTimeout(sbn, packetId, catalogTrackName)\n this.#blockSourceCounts.delete(key)\n this.#blockSourceEsis.delete(key)\n this.#blockRepairRsIds.delete(key)\n this.#completedBlocks.add(key)\n // BLO-3323 L15: track which SBNs reached K source-symbol arrivals so the\n // worker recovery suppression can distinguish \"block completed by source\"\n // (legitimately suppress duplicate recovery) from \"block touched by any\n // prior emit / timeout / external mark\" (do NOT suppress recovery).\n this.#cleanlyCompletedBlocks.add(key)\n this.#markPathBlockComplete(sbn, packetId, catalogTrackName)\n if (catalogTrackName === undefined) {\n this.onBlockComplete?.(sbn, packetId)\n } else {\n this.onBlockComplete?.(sbn, packetId, catalogTrackName)\n }\n // MmtpRouter feeds the K-th source immediately after this callback.\n // Defer cleanup so that synchronous feed selects the same decoder; this\n // also clears blocks whose K-th packet was too large for decoder input.\n queueMicrotask(() => this.#syncDecoderBlockOwners.delete(key))\n return\n }\n\n this.noteBlockActivity(sbn, packetId, catalogTrackName, true)\n\n // Source ESI is intentionally not a repair trigger. Multicast delivery is\n // unordered, so K-1 may precede an interior symbol in a clean block. The\n // progress-refreshed source horizon above is the earliest loss signal.\n }\n\n /** Refresh deadlines after accepted block activity. */\n noteBlockActivity(\n sbn: number,\n packetId: number,\n catalogTrackName?: string,\n sourceProgress = false,\n ): void {\n const key = this.#blockKey(sbn, packetId, catalogTrackName)\n\n // Late symbols must not resurrect a block that already completed or failed.\n if (this.#completedBlocks.has(key)) return\n // The worker cleanup command is already queued after all symbols that made\n // the inactivity deadline. Symbols arriving after that boundary are late\n // and must not arm a second timer before the acknowledgement resolves it.\n if (this.#pendingWorkerCleanups.has(key)) return\n\n // #requestRepair appends one bounded grace to the active deadline. Source\n // or repair symbols may still complete the block, but cannot extend it.\n if (this.#repairRequestedBlocks.has(key)) return\n\n const trackConfig = this.#resolveTrackConfig(packetId)\n const timeoutMs = trackConfig.timeoutMs\n if (timeoutMs <= 0) return\n\n // D-28: accepted source progress refreshes the pre-request horizon. There\n // are at most K such arrivals; repair traffic never refreshes this timer.\n const repairDeadline = this.#repairDeadlines.get(key)\n if (\n this.onRepairNeeded &&\n (repairDeadline === undefined || sourceProgress)\n ) {\n if (repairDeadline != null) clearTimeout(repairDeadline)\n this.#repairDeadlines.set(\n key,\n setTimeout(() => {\n this.#requestRepair(sbn, packetId, catalogTrackName)\n }, this.#repairDeadlineMs(trackConfig)),\n )\n }\n\n this.#armBlockTimeout(sbn, packetId, catalogTrackName, timeoutMs)\n }\n\n #failBlock(\n sbn: number,\n packetId: number,\n countGreenFill: boolean,\n catalogTrackName?: string,\n ): void {\n const key = this.#blockKey(sbn, packetId, catalogTrackName)\n this.#traceLifecycle('failed', sbn, packetId, catalogTrackName)\n this.#clearRepairDeadline(key)\n const activeTimer = this.#activeBlocks.get(key)\n if (activeTimer) {\n clearTimeout(activeTimer)\n this.#activeBlocks.delete(key)\n }\n this.#activeBlockDeadlinesMs.delete(key)\n if (this.#repairRequestedBlocks.delete(key)) {\n if (catalogTrackName === undefined) {\n this.onRepairResolved?.(sbn, packetId, 'failed')\n } else {\n this.onRepairResolved?.(sbn, packetId, 'failed', catalogTrackName)\n }\n }\n this.#blockSourceCounts.delete(key)\n this.#blockSourceEsis.delete(key)\n this.#blockRepairRsIds.delete(key)\n this.#activeBlockTimeoutOwners.delete(key)\n this.#syncDecoderBlockOwners.delete(key)\n this.#completedBlocks.add(key)\n this.#markPathBlockComplete(sbn, packetId, catalogTrackName)\n this.#fecStats.blocksFailed++\n this.#statsForPacket(packetId).blocksFailed++\n if (countGreenFill) {\n this.#fecStats.greenFillFrames++\n this.#statsForPacket(packetId).greenFillFrames++\n }\n if (catalogTrackName === undefined) {\n this.onBlockFailed?.(sbn, packetId)\n } else {\n this.onBlockFailed?.(sbn, packetId, catalogTrackName)\n }\n }\n\n /** Check whether a block has already been completed (clean or recovered). */\n isBlockCompleted(sbn: number, packetId?: number, catalogTrackName?: string): boolean {\n return this.#blockKeysFor(sbn, packetId, catalogTrackName).some((key) => this.#completedBlocks.has(key))\n }\n\n /** Mark a block as completed externally (e.g., after routing recovered data). */\n markBlockCompleted(sbn: number, packetId?: number, catalogTrackName?: string): void {\n for (const key of this.#blockKeysFor(sbn, packetId, catalogTrackName)) {\n this.#completedBlocks.add(key)\n this.#pendingWorkerCleanups.delete(key)\n this.#pendingWorkerCleanupClients.delete(key)\n const block = this.#parseBlockKey(key)\n this.#markPathBlockComplete(block.sbn, block.packetId, block.catalogTrackName)\n this.#clearRepairDeadline(key)\n if (this.#repairRequestedBlocks.delete(key)) {\n if (block.catalogTrackName === undefined) {\n this.onRepairResolved?.(block.sbn, block.packetId, 'completed')\n } else {\n this.onRepairResolved?.(\n block.sbn,\n block.packetId,\n 'completed',\n block.catalogTrackName,\n )\n }\n }\n this.#blockSourceCounts.delete(key)\n this.#blockSourceEsis.delete(key)\n this.#blockRepairRsIds.delete(key)\n const timer = this.#activeBlocks.get(key)\n if (timer) {\n clearTimeout(timer)\n this.#activeBlocks.delete(key)\n }\n this.#activeBlockDeadlinesMs.delete(key)\n this.#activeBlockTimeoutOwners.delete(key)\n }\n }\n\n /** Increment blocksRecovered stat (called by MmtpRouter after routing recovery). */\n countRecovery(packetId?: number): void {\n this.#fecStats.blocksRecovered++\n if (packetId != null) this.#statsForPacket(packetId).blocksRecovered++\n }\n\n /**\n * Strict delivery provenance for one FEC block.\n *\n * A recovered symbol is a function of every symbol that entered its block,\n * so it can only be attributed to multicast when NO symbol in that block\n * arrived over MoQ. Repair symbols routinely arrive over reliable QUIC\n * (`MmtpRouter.feedRepairPacket`), so a block that recovered with MoQ help\n * produced bytes that never crossed a multicast socket.\n *\n * An untracked block returns 'unknown' rather than a guess: absence of\n * evidence is not evidence of multicast.\n */\n blockDeliveryProvenance(\n sbn: number,\n packetId: number,\n catalogTrackName?: string,\n ): MmtpDeliveryProvenance {\n const counts = this.blockPathCount(sbn, packetId, catalogTrackName)\n if (!counts) return 'unknown'\n if (counts.moqSymbols > 0) return 'mixed'\n if (counts.multicastSymbols > 0) return 'multicast'\n return 'unknown'\n }\n\n /** Per-path symbol counts for one block (D-28 gate logging), if tracked. */\n blockPathCount(\n sbn: number,\n packetId: number,\n catalogTrackName?: string,\n ): FecBlockPathCount | null {\n return this.#blockPathCounts.get(\n this.#blockKey(sbn, packetId, catalogTrackName),\n ) ?? null\n }\n\n /** Cancel block timeout (block completed via clean reception or FEC recovery). */\n cancelBlockTimeout(sbn: number, packetId?: number, catalogTrackName?: string): void {\n for (const key of this.#blockKeysFor(sbn, packetId, catalogTrackName)) {\n this.#completedBlocks.add(key)\n this.#pendingWorkerCleanups.delete(key)\n this.#pendingWorkerCleanupClients.delete(key)\n const block = this.#parseBlockKey(key)\n this.#markPathBlockComplete(block.sbn, block.packetId, block.catalogTrackName)\n this.#clearRepairDeadline(key)\n if (this.#repairRequestedBlocks.delete(key)) {\n if (block.catalogTrackName === undefined) {\n this.onRepairResolved?.(block.sbn, block.packetId, 'completed')\n } else {\n this.onRepairResolved?.(\n block.sbn,\n block.packetId,\n 'completed',\n block.catalogTrackName,\n )\n }\n }\n this.#blockSourceCounts.delete(key)\n this.#blockSourceEsis.delete(key)\n this.#blockRepairRsIds.delete(key)\n const timer = this.#activeBlocks.get(key)\n if (timer) {\n clearTimeout(timer)\n this.#activeBlocks.delete(key)\n }\n this.#activeBlockDeadlinesMs.delete(key)\n this.#activeBlockTimeoutOwners.delete(key)\n }\n // Trim old entries to prevent unbounded growth (keep last ~500 blocks)\n if (this.#completedBlocks.size > 1000) {\n const iter = this.#completedBlocks.values()\n for (let i = 0; i < 500; i++) iter.next()\n // Sets iterate in insertion order \u2014 first 500 are oldest\n const keep = new Set<string>()\n for (const v of iter) keep.add(v)\n this.#completedBlocks = keep\n }\n }\n\n // -----------------------------------------------------------------------\n // Cleanup\n // -----------------------------------------------------------------------\n\n dispose(): void {\n for (const timer of this.#activeBlocks.values()) clearTimeout(timer)\n this.#activeBlocks.clear()\n this.#activeBlockDeadlinesMs.clear()\n this.#activeBlockTimeoutOwners.clear()\n this.#pendingWorkerCleanups.clear()\n this.#pendingWorkerCleanupClients.clear()\n for (const timer of this.#repairDeadlines.values()) {\n if (timer != null) clearTimeout(timer)\n }\n this.#repairDeadlines.clear()\n this.#blockSourceCounts.clear()\n this.#blockSourceEsis.clear()\n this.#blockRepairRsIds.clear()\n this.#completedBlocks.clear()\n this.#cleanlyCompletedBlocks.clear()\n this.#repairRequestedBlocks.clear()\n this.#workerRecoveredBlocks.clear()\n this.#syncDecoderBlockOwners.clear()\n this.#startupAlignedPackets.clear()\n this.#startupPartialBlocks.clear()\n this.#startupRequireRepairReady = false\n this.#startupRepairReadyPackets.clear()\n this.#startupRepairBoundaryCounts.clear()\n this.#startupRepairFailOpenPackets.clear()\n this.#seenSymbols.clear()\n this.#blockPathCounts.clear()\n this.#blockCatalogTrackNames.clear()\n this.#trackConfigs.clear()\n this.#fecDecoders.clear()\n // Dispose Worker clients (T-03-07: prevent Worker leak)\n for (const client of new Set(this.#workerClients.values())) client.dispose()\n this.#workerClients.clear()\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * FecStarvationDetector — post-cutover source-starvation detection (BLO-9413/D4).
3
+ *
4
+ * The packet-presence watchdog (MulticastTransport, 5s) only catches TOTAL
5
+ * silence: any packet — including FEC repair — resets it. The observed
6
+ * BLO-9413 failure delivered repair symbols continuously while ZERO source
7
+ * symbols arrived (`sourceSymbols:0, repairSymbols:551, blocksComplete:0`):
8
+ * the watchdog stayed fed forever while nothing reconstructable reached the
9
+ * decoder. This detector closes that hole by watching delivery progress and
10
+ * driving the designed-but-previously-uncalled TrackSession loss arcs:
11
+ *
12
+ * starvation sustained ≥ windowMs → onStarvation() → session.lossDetected()
13
+ * block progress resumes → onRecovery() → session.lossRecovered()
14
+ *
15
+ * TrackSession's own FEC block-span hysteresis then decides whether a recovery
16
+ * cancels the failback — this detector deliberately does NOT add a second
17
+ * hysteresis layer.
18
+ *
19
+ * Push-fed: call update() with each FecTrackStats-shaped snapshot (e.g. from
20
+ * MulticastManager.setOnFecStatsUpdate or a host sampler). Hosts that can also
21
+ * observe decoded/appended media should include a monotonic mediaFrames count:
22
+ * media progress is delivery progress even if sparse FEC block counters do not
23
+ * advance in the same sample window. Evaluation happens only on update — a
24
+ * fully dead transport stops producing updates and stays the transport
25
+ * watchdog's responsibility (total-silence backstop).
26
+ */
27
+ /** Monotonic counters sampled from the FEC layer (FecTrackStats subset). */
28
+ export interface FecStarvationSnapshot {
29
+ sourceSymbols: number;
30
+ repairSymbols: number;
31
+ blocksComplete: number;
32
+ blocksRecovered: number;
33
+ /** Optional host-observed decoded/appended media frames for this packetId. */
34
+ mediaFrames?: number;
35
+ }
36
+ export interface FecStarvationDetectorConfig {
37
+ /**
38
+ * How long starvation must persist before onStarvation fires. Derive from
39
+ * the stream's FEC block-span config — no magic defaults, matching the
40
+ * TrackSession hysteresis convention.
41
+ */
42
+ windowMs: number;
43
+ /** Starvation sustained ≥ windowMs. Fires once per starvation episode. */
44
+ onStarvation(): void;
45
+ /** Healthy delivery resumed after onStarvation fired. Fires once per episode. */
46
+ onRecovery(): void;
47
+ /** Injectable clock for tests. Defaults to Date.now. */
48
+ now?: () => number;
49
+ }
50
+ export declare class FecStarvationDetector {
51
+ #private;
52
+ constructor(config: FecStarvationDetectorConfig);
53
+ /** True while a starvation episode is active (onStarvation fired, no recovery yet). */
54
+ get isStarved(): boolean;
55
+ update(snapshot: FecStarvationSnapshot): void;
56
+ /** Reset episode state (e.g. on transport switch / re-join). Keeps no baseline. */
57
+ reset(): void;
58
+ }
59
+ //# sourceMappingURL=fec-starvation-detector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fec-starvation-detector.d.ts","sourceRoot":"","sources":["../src/fec-starvation-detector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,4EAA4E;AAC5E,MAAM,WAAW,qBAAqB;IACpC,aAAa,EAAE,MAAM,CAAA;IACrB,aAAa,EAAE,MAAM,CAAA;IACrB,cAAc,EAAE,MAAM,CAAA;IACtB,eAAe,EAAE,MAAM,CAAA;IACvB,8EAA8E;IAC9E,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,MAAM,WAAW,2BAA2B;IAC1C;;;;OAIG;IACH,QAAQ,EAAE,MAAM,CAAA;IAChB,0EAA0E;IAC1E,YAAY,IAAI,IAAI,CAAA;IACpB,iFAAiF;IACjF,UAAU,IAAI,IAAI,CAAA;IAClB,wDAAwD;IACxD,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CACnB;AAED,qBAAa,qBAAqB;;gBAQpB,MAAM,EAAE,2BAA2B;IAY/C,uFAAuF;IACvF,IAAI,SAAS,IAAI,OAAO,CAAyB;IAEjD,MAAM,CAAC,QAAQ,EAAE,qBAAqB,GAAG,IAAI;IA8C7C,mFAAmF;IACnF,KAAK,IAAI,IAAI;CAKd"}
@@ -0,0 +1,75 @@
1
+ var __typeError = (msg) => {
2
+ throw TypeError(msg);
3
+ };
4
+ var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
5
+ var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
6
+ var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
7
+ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
8
+
9
+ // src/fec-starvation-detector.ts
10
+ var _config, _now, _last, _lastSampleAt, _starvedSince, _latched;
11
+ var FecStarvationDetector = class {
12
+ constructor(config) {
13
+ __privateAdd(this, _config);
14
+ __privateAdd(this, _now);
15
+ __privateAdd(this, _last, null);
16
+ __privateAdd(this, _lastSampleAt, 0);
17
+ __privateAdd(this, _starvedSince, null);
18
+ __privateAdd(this, _latched, false);
19
+ if (config.windowMs <= 0) {
20
+ throw new Error(
21
+ `[FecStarvationDetector] windowMs=${config.windowMs}. Derive a positive window from the stream's FEC block-span config \u2014 a non-positive window would fire on the first repair packet of a healthy stream.`
22
+ );
23
+ }
24
+ __privateSet(this, _config, config);
25
+ __privateSet(this, _now, config.now ?? Date.now);
26
+ }
27
+ /** True while a starvation episode is active (onStarvation fired, no recovery yet). */
28
+ get isStarved() {
29
+ return __privateGet(this, _latched);
30
+ }
31
+ update(snapshot) {
32
+ const last = __privateGet(this, _last);
33
+ const prevSampleAt = __privateGet(this, _lastSampleAt);
34
+ const now = __privateGet(this, _now).call(this);
35
+ __privateSet(this, _last, { ...snapshot });
36
+ __privateSet(this, _lastSampleAt, now);
37
+ if (last === null) return;
38
+ const srcDelta = snapshot.sourceSymbols - last.sourceSymbols;
39
+ const repDelta = snapshot.repairSymbols - last.repairSymbols;
40
+ const doneDelta = snapshot.blocksComplete - last.blocksComplete + (snapshot.blocksRecovered - last.blocksRecovered);
41
+ const mediaDelta = (snapshot.mediaFrames ?? 0) - (last.mediaFrames ?? 0);
42
+ const packetsFlowing = srcDelta > 0 || repDelta > 0;
43
+ if (!packetsFlowing && mediaDelta <= 0) return;
44
+ const starving = doneDelta === 0 && mediaDelta <= 0;
45
+ if (starving) {
46
+ if (__privateGet(this, _starvedSince) === null) __privateSet(this, _starvedSince, prevSampleAt);
47
+ if (!__privateGet(this, _latched) && now - __privateGet(this, _starvedSince) >= __privateGet(this, _config).windowMs) {
48
+ __privateSet(this, _latched, true);
49
+ __privateGet(this, _config).onStarvation();
50
+ }
51
+ return;
52
+ }
53
+ __privateSet(this, _starvedSince, null);
54
+ if (__privateGet(this, _latched)) {
55
+ __privateSet(this, _latched, false);
56
+ __privateGet(this, _config).onRecovery();
57
+ }
58
+ }
59
+ /** Reset episode state (e.g. on transport switch / re-join). Keeps no baseline. */
60
+ reset() {
61
+ __privateSet(this, _last, null);
62
+ __privateSet(this, _starvedSince, null);
63
+ __privateSet(this, _latched, false);
64
+ }
65
+ };
66
+ _config = new WeakMap();
67
+ _now = new WeakMap();
68
+ _last = new WeakMap();
69
+ _lastSampleAt = new WeakMap();
70
+ _starvedSince = new WeakMap();
71
+ _latched = new WeakMap();
72
+ export {
73
+ FecStarvationDetector
74
+ };
75
+ //# sourceMappingURL=fec-starvation-detector.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/fec-starvation-detector.ts"],
4
+ "sourcesContent": ["/**\n * FecStarvationDetector \u2014 post-cutover source-starvation detection (BLO-9413/D4).\n *\n * The packet-presence watchdog (MulticastTransport, 5s) only catches TOTAL\n * silence: any packet \u2014 including FEC repair \u2014 resets it. The observed\n * BLO-9413 failure delivered repair symbols continuously while ZERO source\n * symbols arrived (`sourceSymbols:0, repairSymbols:551, blocksComplete:0`):\n * the watchdog stayed fed forever while nothing reconstructable reached the\n * decoder. This detector closes that hole by watching delivery progress and\n * driving the designed-but-previously-uncalled TrackSession loss arcs:\n *\n * starvation sustained \u2265 windowMs \u2192 onStarvation() \u2192 session.lossDetected()\n * block progress resumes \u2192 onRecovery() \u2192 session.lossRecovered()\n *\n * TrackSession's own FEC block-span hysteresis then decides whether a recovery\n * cancels the failback \u2014 this detector deliberately does NOT add a second\n * hysteresis layer.\n *\n * Push-fed: call update() with each FecTrackStats-shaped snapshot (e.g. from\n * MulticastManager.setOnFecStatsUpdate or a host sampler). Hosts that can also\n * observe decoded/appended media should include a monotonic mediaFrames count:\n * media progress is delivery progress even if sparse FEC block counters do not\n * advance in the same sample window. Evaluation happens only on update \u2014 a\n * fully dead transport stops producing updates and stays the transport\n * watchdog's responsibility (total-silence backstop).\n */\n\n/** Monotonic counters sampled from the FEC layer (FecTrackStats subset). */\nexport interface FecStarvationSnapshot {\n sourceSymbols: number\n repairSymbols: number\n blocksComplete: number\n blocksRecovered: number\n /** Optional host-observed decoded/appended media frames for this packetId. */\n mediaFrames?: number\n}\n\nexport interface FecStarvationDetectorConfig {\n /**\n * How long starvation must persist before onStarvation fires. Derive from\n * the stream's FEC block-span config \u2014 no magic defaults, matching the\n * TrackSession hysteresis convention.\n */\n windowMs: number\n /** Starvation sustained \u2265 windowMs. Fires once per starvation episode. */\n onStarvation(): void\n /** Healthy delivery resumed after onStarvation fired. Fires once per episode. */\n onRecovery(): void\n /** Injectable clock for tests. Defaults to Date.now. */\n now?: () => number\n}\n\nexport class FecStarvationDetector {\n #config: FecStarvationDetectorConfig\n #now: () => number\n #last: FecStarvationSnapshot | null = null\n #lastSampleAt = 0\n #starvedSince: number | null = null\n #latched = false\n\n constructor(config: FecStarvationDetectorConfig) {\n if (config.windowMs <= 0) {\n throw new Error(\n `[FecStarvationDetector] windowMs=${config.windowMs}. Derive a positive window ` +\n `from the stream's FEC block-span config \u2014 a non-positive ` +\n `window would fire on the first repair packet of a healthy stream.`\n )\n }\n this.#config = config\n this.#now = config.now ?? Date.now\n }\n\n /** True while a starvation episode is active (onStarvation fired, no recovery yet). */\n get isStarved(): boolean { return this.#latched }\n\n update(snapshot: FecStarvationSnapshot): void {\n const last = this.#last\n const prevSampleAt = this.#lastSampleAt\n const now = this.#now()\n this.#last = { ...snapshot }\n this.#lastSampleAt = now\n if (last === null) return // first sample = baseline only\n\n const srcDelta = snapshot.sourceSymbols - last.sourceSymbols\n const repDelta = snapshot.repairSymbols - last.repairSymbols\n const doneDelta =\n (snapshot.blocksComplete - last.blocksComplete) +\n (snapshot.blocksRecovered - last.blocksRecovered)\n const mediaDelta = (snapshot.mediaFrames ?? 0) - (last.mediaFrames ?? 0)\n\n const packetsFlowing = srcDelta > 0 || repDelta > 0\n if (!packetsFlowing && mediaDelta <= 0) return // total silence \u2014 transport watchdog's domain\n\n // Starving: traffic arrives but nothing reconstructable comes out. Source\n // and repair symbols arrive at symbol cadence while complete/recovered\n // blocks arrive at block cadence; requiring source and block deltas in the\n // same snapshot false-fails healthy live streams. A repaired block is still\n // decodable media, and host-observed frame progress is even stronger\n // evidence, so either resets the starvation clock.\n const starving = doneDelta === 0 && mediaDelta <= 0\n\n if (starving) {\n // The episode began when delivery was last known healthy \u2014 the previous\n // sample's timestamp, not this sample's arrival (the deltas in THIS\n // sample describe the interval since then).\n if (this.#starvedSince === null) this.#starvedSince = prevSampleAt\n if (!this.#latched && now - this.#starvedSince >= this.#config.windowMs) {\n this.#latched = true\n this.#config.onStarvation()\n }\n return\n }\n\n // Healthy sample: completed or recovered blocks are reaching the decoder.\n this.#starvedSince = null\n if (this.#latched) {\n this.#latched = false\n this.#config.onRecovery()\n }\n }\n\n /** Reset episode state (e.g. on transport switch / re-join). Keeps no baseline. */\n reset(): void {\n this.#last = null\n this.#starvedSince = null\n this.#latched = false\n }\n}\n"],
5
+ "mappings": ";;;;;;;;;AAAA;AAoDO,IAAM,wBAAN,MAA4B;AAAA,EAQjC,YAAY,QAAqC;AAPjD;AACA;AACA,8BAAsC;AACtC,sCAAgB;AAChB,sCAA+B;AAC/B,iCAAW;AAGT,QAAI,OAAO,YAAY,GAAG;AACxB,YAAM,IAAI;AAAA,QACR,oCAAoC,OAAO,QAAQ;AAAA,MAGrD;AAAA,IACF;AACA,uBAAK,SAAU;AACf,uBAAK,MAAO,OAAO,OAAO,KAAK;AAAA,EACjC;AAAA;AAAA,EAGA,IAAI,YAAqB;AAAE,WAAO,mBAAK;AAAA,EAAS;AAAA,EAEhD,OAAO,UAAuC;AAC5C,UAAM,OAAO,mBAAK;AAClB,UAAM,eAAe,mBAAK;AAC1B,UAAM,MAAM,mBAAK,MAAL;AACZ,uBAAK,OAAQ,EAAE,GAAG,SAAS;AAC3B,uBAAK,eAAgB;AACrB,QAAI,SAAS,KAAM;AAEnB,UAAM,WAAW,SAAS,gBAAgB,KAAK;AAC/C,UAAM,WAAW,SAAS,gBAAgB,KAAK;AAC/C,UAAM,YACH,SAAS,iBAAiB,KAAK,kBAC/B,SAAS,kBAAkB,KAAK;AACnC,UAAM,cAAc,SAAS,eAAe,MAAM,KAAK,eAAe;AAEtE,UAAM,iBAAiB,WAAW,KAAK,WAAW;AAClD,QAAI,CAAC,kBAAkB,cAAc,EAAG;AAQxC,UAAM,WAAW,cAAc,KAAK,cAAc;AAElD,QAAI,UAAU;AAIZ,UAAI,mBAAK,mBAAkB,KAAM,oBAAK,eAAgB;AACtD,UAAI,CAAC,mBAAK,aAAY,MAAM,mBAAK,kBAAiB,mBAAK,SAAQ,UAAU;AACvE,2BAAK,UAAW;AAChB,2BAAK,SAAQ,aAAa;AAAA,MAC5B;AACA;AAAA,IACF;AAGA,uBAAK,eAAgB;AACrB,QAAI,mBAAK,WAAU;AACjB,yBAAK,UAAW;AAChB,yBAAK,SAAQ,WAAW;AAAA,IAC1B;AAAA,EACF;AAAA;AAAA,EAGA,QAAc;AACZ,uBAAK,OAAQ;AACb,uBAAK,eAAgB;AACrB,uBAAK,UAAW;AAAA,EAClB;AACF;AA1EE;AACA;AACA;AACA;AACA;AACA;",
6
+ "names": []
7
+ }