@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,2081 @@
1
+ /**
2
+ * FEC Manager — codec-agnostic FEC decode service.
3
+ *
4
+ * Receives (packetId, sbn, esi, symbolData) tuples from any transport
5
+ * (MMTP, LOC, etc.) and manages FEC block lifecycle: source counting,
6
+ * WASM RaptorQ decode, block timeout, and recovery/failure notification.
7
+ *
8
+ * Extracted from MulticastManager to decouple FEC block logic from
9
+ * MMTP packet parsing and frame routing.
10
+ */
11
+
12
+ import { computeFecDeliveryWindowMs } from './track-session.js'
13
+ import { deriveFecInterleaveDepth } from '@blockcast/mmt-fec'
14
+
15
+ import type { MmtpDeliveryProvenance } from './mfu-reassembler.js'
16
+ import type {
17
+ FecWorkerClient,
18
+ FecTrackStats,
19
+ FecBlockSnapshot,
20
+ FrameMeta,
21
+ FecWorkerErrorCode,
22
+ } from '@blockcast/fec-worker'
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // Constants
26
+ // ---------------------------------------------------------------------------
27
+
28
+ /**
29
+ * Minimum valid FEC interleave depth in milliseconds, per spec §5.1
30
+ * (the interactive/gaming floor; broadcast/ATSC 3.0 ranges up to 10000ms).
31
+ *
32
+ * A protocol constant, NOT a tuning knob. Values below this floor are almost
33
+ * certainly a raw frame count (e.g. `2`) that a caller passed when no framerate
34
+ * was available to convert to ms (common on audio tracks). Feeding such a value
35
+ * into computeFecTimeout treats it as `2ms`, yielding a near-zero FEC block
36
+ * timeout that abandons startup FEC blocks before their repair symbols arrive
37
+ * (measured: moqtail startupFailed=4, shaka dropped=5.8% on startup). Timing
38
+ * derivations MUST reject sub-floor values and fall back to the established
39
+ * timeout instead of collapsing.
40
+ */
41
+ export const MIN_INTERLEAVE_DEPTH_MS = 33
42
+ const WORKER_ERROR_LOG_CAP = 5
43
+ const WORKER_ERROR_KIND_CAP = 32
44
+ const WORKER_ERROR_OVERFLOW_KIND = 'other'
45
+ const STARTUP_REPAIR_WAIT_SOURCE_BOUNDARIES = 2
46
+
47
+ // ---------------------------------------------------------------------------
48
+ // Types
49
+ // ---------------------------------------------------------------------------
50
+
51
+ /**
52
+ * Algorithm-agnostic FEC decoder backend interface.
53
+ *
54
+ * Implementations:
55
+ * - RaptorQ WASM (mmt_wasm.js) — RFC 6330
56
+ * - Reed-Solomon WASM — future
57
+ * - XOR — future
58
+ *
59
+ * Source/repair symbol IDs follow ISO 23008-1 §C.5.2/C.5.3:
60
+ * source: SS_ID (flat 32-bit symbol identifier)
61
+ * repair: SS_Start + SSB_length + RS_ID (block-scoped repair coords)
62
+ */
63
+ export type WasmFecDecoder = {
64
+ add_source(ss_id: number, data: Uint8Array, ts: number): Uint8Array | null
65
+ add_repair(ss_start: number, ssb_length: number, rs_id: number, data: Uint8Array, ts: number): Uint8Array | null
66
+ configure(oti: Uint8Array): void
67
+ cleanup(current_time: number): Uint8Array
68
+ cleanup_blocks_detailed(sbns: Uint32Array): Uint8Array
69
+ flush?(): void
70
+ reset_stats?(): void
71
+ }
72
+
73
+ /** Algorithm-agnostic FEC stats snapshot. */
74
+ export interface FecStats {
75
+ sourcePackets: number
76
+ repairPackets: number
77
+ duplicateSymbols: number
78
+ blocksClean: number
79
+ blocksRecovered: number
80
+ blocksFailed: number
81
+ greenFillFrames: number
82
+ blocksComplete: number
83
+ mixedPathBlocks: number
84
+ recentBlockPathCounts: FecBlockPathCount[]
85
+ k: number
86
+ /** Of blocks with loss, what percentage were recovered (0-100). */
87
+ recoveryRate: number
88
+ /** What percentage of blocks had any loss (0-100). */
89
+ packetLossRate: number
90
+ /** Per-MMTP-packet-ID counters for the synchronous WASM decoder path. */
91
+ tracks: FecPacketStats[]
92
+ /** Latest Worker/WASM snapshot for each packet ID. */
93
+ workers: Array<FecTrackStats & { packetId: number }>
94
+ /** Main-thread repair symbols rejected before dispatch because their size did not match T. */
95
+ repairSymbolSizeMismatches: number
96
+ /** Bounded Worker error counts keyed by stable code, or message text for legacy errors. */
97
+ workerErrors: Record<string, number>
98
+ }
99
+
100
+ export interface FecPacketStats {
101
+ packetId: number
102
+ sourcePackets: number
103
+ repairPackets: number
104
+ duplicateSymbols: number
105
+ blocksClean: number
106
+ blocksRecovered: number
107
+ blocksFailed: number
108
+ greenFillFrames: number
109
+ blocksComplete: number
110
+ k: number
111
+ recoveryRate: number
112
+ packetLossRate: number
113
+ }
114
+
115
+ export type FecSymbolSource = 'multicast' | 'moq'
116
+
117
+ export interface FecBlockPathCount {
118
+ packetId: number
119
+ sbn: number
120
+ catalogTrackName?: string
121
+ multicastSymbols: number
122
+ moqSymbols: number
123
+ complete: boolean
124
+ }
125
+
126
+ type FecLifecycleTrace = {
127
+ atMs: number
128
+ event:
129
+ | 'repair-requested'
130
+ | 'repair-arrived'
131
+ | 'timeout-queued'
132
+ | 'cleanup-ack'
133
+ | 'recovered'
134
+ | 'failed'
135
+ | 'decoder-unavailable'
136
+ packetId: number
137
+ sbn: number
138
+ catalogTrackName?: string
139
+ sourceCount: number
140
+ sourceEsis: number[]
141
+ repairRsIds: number[]
142
+ decoderPath: 'worker' | 'sync' | 'none'
143
+ deadlineRemainingMs?: number
144
+ pendingCleanup: boolean
145
+ rsId?: number
146
+ }
147
+
148
+ export interface FECManagerConfig {
149
+ k?: number
150
+ /** Symbol size in bytes (T). NOT repair count. */
151
+ symbolSize?: number
152
+ interleaveDepthMs?: number
153
+ fps?: number
154
+ /** Guarded block inactivity deadline derived from the full delivery geometry. */
155
+ timeoutMs?: number
156
+ }
157
+
158
+ interface ResolvedFECTrackConfig {
159
+ k: number
160
+ symbolSize: number
161
+ interleaveDepthMs: number
162
+ timeoutMs: number
163
+ fps?: number
164
+ }
165
+
166
+ interface MutableFecCounters {
167
+ sourcePackets: number
168
+ repairPackets: number
169
+ duplicateSymbols: number
170
+ blocksClean: number
171
+ blocksRecovered: number
172
+ blocksFailed: number
173
+ greenFillFrames: number
174
+ }
175
+
176
+ function newFecCounters(): MutableFecCounters {
177
+ return {
178
+ sourcePackets: 0,
179
+ repairPackets: 0,
180
+ duplicateSymbols: 0,
181
+ blocksClean: 0,
182
+ blocksRecovered: 0,
183
+ blocksFailed: 0,
184
+ greenFillFrames: 0,
185
+ }
186
+ }
187
+
188
+ // ---------------------------------------------------------------------------
189
+ // FECManager
190
+ // ---------------------------------------------------------------------------
191
+
192
+ /** Why a stream's Worker decoder stopped being routable. */
193
+ export type FecDecoderUnavailableReason = 'cleared' | 'disposed'
194
+
195
+ /**
196
+ * What still decodes a stream after its Worker left routing: the synchronous
197
+ * WASM decoder registered for the same stream (the Worker path took precedence
198
+ * over it while both were live), or nothing.
199
+ */
200
+ export type FecFallbackDecoder = 'sync' | 'none'
201
+
202
+ export interface FecDecoderUnavailableEvent {
203
+ readonly packetId: number
204
+ readonly catalogTrackName?: string
205
+ readonly reason: FecDecoderUnavailableReason
206
+ /**
207
+ * What decodes the stream at the moment the Worker left. A synchronous
208
+ * decoder discarded later (a resetTrack flush failure drops it without an
209
+ * event) is not reported here; a consumer that keeps a stream alive on
210
+ * 'sync' owns that decoder's lifecycle itself.
211
+ */
212
+ readonly fallbackDecoder: FecFallbackDecoder
213
+ }
214
+
215
+ export class FECManager {
216
+ // Per-track WASM RaptorQ decoders, keyed by physical stream identity.
217
+ #fecDecoders = new Map<string, { client: WasmFecDecoder; k: number }>()
218
+ /** @deprecated Use setFecDecoder(packetId, ...). Backwards-compat shim. */
219
+
220
+ // Per-track Worker-backed FEC decoders, keyed by physical stream identity.
221
+ // Worker-backed tracks use async decode — recovered frames arrive via onFrame callback,
222
+ // not synchronous return from feedSourceSymbol/feedRepairSymbol.
223
+ #workerClients = new Map<string, FecWorkerClient>()
224
+ // Worker setup can finish while an interleaved block is already collecting
225
+ // on the sync decoder. Keep that block on its original decoder so later
226
+ // source/repair symbols do not split across two RaptorQ states.
227
+ #syncDecoderBlockOwners = new Map<string, WasmFecDecoder>()
228
+ #decoderBlockOwners = new WeakMap<object, Map<number, {
229
+ catalogTrackName?: string
230
+ conflicted: boolean
231
+ }>>()
232
+
233
+ #fecK = 4
234
+ #fecSymbolSize = 0
235
+ #fecTimeoutMs = 500 // default 500ms; refined by setFramerate() when fps known
236
+ #trackConfigs = new Map<number, ResolvedFECTrackConfig>()
237
+ #fecStats = newFecCounters()
238
+ #fecTrackStats = new Map<number, MutableFecCounters>()
239
+ #fecWorkerStats = new Map<number, FecTrackStats>()
240
+ #fecStatsObservers = new Set<(packetId: number, stats: FecTrackStats) => void>()
241
+
242
+ // Block inactivity tracking: (packetId, SBN) -> timer. The deadline is
243
+ // derived from catalog geometry and refreshed on progress; only an idle
244
+ // block fails.
245
+ #activeBlocks = new Map<string, ReturnType<typeof setTimeout>>()
246
+ #activeBlockDeadlinesMs = new Map<string, number>()
247
+ #activeBlockTimeoutOwners = new Map<string, object>()
248
+ #lifecycleTrace: FecLifecycleTrace[] = []
249
+ // Worker-backed timeouts enqueue cleanup behind all prior symbol messages.
250
+ // Failure remains pending until cleanupComplete acknowledges that queue drain.
251
+ #pendingWorkerCleanups = new Set<string>()
252
+ #pendingWorkerCleanupClients = new Map<string, FecWorkerClient>()
253
+ // Source symbol count per track-scoped block — marks complete when K symbols arrive.
254
+ #blockSourceCounts = new Map<string, number>()
255
+ #blockSourceEsis = new Map<string, Set<number>>()
256
+ #blockRepairRsIds = new Map<string, Set<number>>()
257
+ // Blocks already recovered — prevents late source symbols from restarting timeouts.
258
+ // WASM removes blocks after recovery; late symbols create empty blocks returning null,
259
+ // which would falsely start new timeouts without this guard.
260
+ #completedBlocks = new Set<string>()
261
+ #seenSymbols = new Set<string>()
262
+ #blockPathCounts = new Map<string, FecBlockPathCount>()
263
+ // Exact catalog identity observed on accepted symbols for each FEC block.
264
+ // Conflicting identities collapse to null so recovery fails closed.
265
+ #blockCatalogTrackNames = new Map<string, string | null>()
266
+ // A receiver joins an already-running live stream in the middle of D
267
+ // interleaved source blocks. This applies to both multicast and reliable MoQ:
268
+ // a MoQ group boundary is not an FEC ESI=0 boundary. Those blocks are not
269
+ // packet loss because their earlier ESIs predate the subscription. Ignore
270
+ // each such block until ESI=0 establishes a complete boundary for the track.
271
+ // After that boundary, a first observed ESI>0 is real delivery loss and must
272
+ // continue through normal FEC recovery/failure accounting.
273
+ #startupAlignedPackets = new Set<string>()
274
+ #startupPartialBlocks = new Set<string>()
275
+ // A live multicast join can observe an ESI=0 source boundary before the
276
+ // repair lane catches up. Returning false here also drops that media packet
277
+ // at MmtpRouter, so the readiness probe is multicast-only and bounded: an
278
+ // actual multicast repair unlocks the next boundary, while a second source
279
+ // boundary fails open. Reliable MoQ media continues through normal startup
280
+ // alignment and retained MoQ repair cannot prove multicast readiness.
281
+ #startupRequireRepairReady = false
282
+ #startupRepairReadyPackets = new Set<string>()
283
+ #startupRepairBoundaryCounts = new Map<string, number>()
284
+ #startupRepairFailOpenPackets = new Set<string>()
285
+ // SBNs that reached K source-symbol arrivals via trackBlock. Used by the
286
+ // worker recovery suppression check (see onFrame handler below) so we only
287
+ // suppress `recovered=true` emits for blocks that ALSO completed cleanly
288
+ // via source symbols. Separating this from #completedBlocks (which is also
289
+ // populated by every worker frame emit, by failed timeouts, and by external
290
+ // callers) was the BLO-3323 L15 fix: the prior predicate caused blocksRecovered
291
+ // to stay at 0 because the SBN was always in #completedBlocks by the time the
292
+ // worker fired recovered=true.
293
+ #cleanlyCompletedBlocks = new Set<string>()
294
+ // D-28: blocks whose repair deadline already fired onRepairNeeded. At most
295
+ // one repair request per block; entries are removed when the block
296
+ // completes, recovers, or fails.
297
+ #repairRequestedBlocks = new Set<string>()
298
+ // D-28: repair-deadline timers refreshed by accepted source progress until
299
+ // the first repair request. A `null` value marks a deadline that already
300
+ // fired (never re-armed). At most K accepted source symbols can refresh the
301
+ // horizon, so browser scheduling delay cannot create an early loss signal
302
+ // and repair traffic cannot keep a block alive indefinitely.
303
+ #repairDeadlines = new Map<string, ReturnType<typeof setTimeout> | null>()
304
+ // Worker recovery emits all K source symbols. Track recovery once per block
305
+ // while routing only ESI slots that were absent from the direct source path.
306
+ #workerRecoveredBlocks = new Set<string>()
307
+
308
+ // Framerate detection state
309
+ #fpsDetected = false
310
+ #fecFps?: number
311
+ #fecInterleaveDepthMs = 0
312
+
313
+ // Worker error log rate-limiter — bounds console.warn output and exported
314
+ // error cardinality when the FEC Worker emits the same failure repeatedly
315
+ // with volatile packet details (e.g. ssStart) in the message text.
316
+ // Stable Worker codes aggregate those messages; uncoded/legacy messages are
317
+ // capped into an `other` bucket. This also covers wasm-bindgen
318
+ // "recursive use of an object" bubbled from add_source/add_repair during
319
+ // overlapping message processing). Without this, Point 5 of the 7-point
320
+ // validation harness — which caps headless warnings at 500 — fails even
321
+ // though the errors are duplicates of a single underlying event.
322
+ #workerErrorCounts = new Map<string, number>()
323
+ #decoderUnavailableEvents = 0
324
+ #repairSymbolSizeMismatchCount = 0
325
+
326
+ // Event callbacks — block lifecycle notifications for MmtpRouter wiring.
327
+ // NOTE: For sync WASM path, recovery routing is handled by MmtpRouter from
328
+ // feedSourceSymbol/feedRepairSymbol return values. For Worker path, recovery
329
+ // arrives via onBlockRecovered callback.
330
+ onBlockStarted:
331
+ | ((sbn: number, packetId: number, catalogTrackName?: string) => void)
332
+ | null = null
333
+ onBlockComplete:
334
+ | ((sbn: number, packetId: number, catalogTrackName?: string) => void)
335
+ | null = null
336
+ onBlockFailed:
337
+ | ((sbn: number, packetId?: number, catalogTrackName?: string) => void)
338
+ | null = null
339
+ /**
340
+ * Fired once per stream when its Worker decoder stops being routable.
341
+ * `reason` is 'cleared' when clearFecWorker() removed it and 'disposed' when
342
+ * a symbol or reset found the registered client already disposed and
343
+ * evicted it. `fallbackDecoder` is 'sync' when a synchronous WASM decoder
344
+ * registered for the same stream keeps decoding its symbols, and 'none'
345
+ * when nothing does. Only 'none' leaves the stream unprotected: the host
346
+ * must then register a decoder again or leave the stream, because counting
347
+ * its symbols as handled would hide the loss.
348
+ */
349
+ onDecoderUnavailable: ((event: FecDecoderUnavailableEvent) => void) | null = null
350
+
351
+ // D-28 on-demand repair callbacks. When onRepairNeeded is registered, each
352
+ // block arms a one-shot repair deadline after the derived K-symbol source
353
+ // horizon; a block still short of K when it fires requests a per-SBN MoQ
354
+ // repair fetch upstream. The inactivity fail bound is unchanged either way —
355
+ // repair and failure are orthogonal timers, so wiring D-28 never delays
356
+ // failure accounting. onRepairResolved fires when a repair-requested block
357
+ // leaves the active set, with the outcome ("completed" after clean completion
358
+ // or recovery, "failed" at the fail bound) so the upstream in-flight slot
359
+ // (MulticastManager.clearRepairForBlock) can be released and logged.
360
+ onRepairNeeded:
361
+ | ((sbn: number, packetId: number, catalogTrackName?: string) => void)
362
+ | null = null
363
+ onRepairResolved:
364
+ | ((
365
+ sbn: number,
366
+ packetId: number,
367
+ outcome: 'completed' | 'failed',
368
+ catalogTrackName?: string,
369
+ ) => void)
370
+ | null = null
371
+
372
+ // Worker-path callbacks — async recovery and stats from FecWorkerClient.
373
+ onBlockRecovered: ((sbn: number, packetId: number, data: ArrayBuffer, meta: FrameMeta, catalogTrackName?: string) => void) | null = null
374
+ onFecStatsUpdate: ((packetId: number, stats: FecTrackStats) => void) | null = null
375
+ onGreenFill: ((sbn: number, packetId: number, data: ArrayBuffer) => void) | null = null
376
+
377
+ #streamKey(packetId: number, catalogTrackName?: string): string {
378
+ return catalogTrackName === undefined
379
+ ? `${packetId}`
380
+ : `${packetId}:${encodeURIComponent(catalogTrackName)}`
381
+ }
382
+
383
+ #belongsToPacket(key: string, packetId: number): boolean {
384
+ return Number(key.split(':', 1)[0]) === packetId
385
+ }
386
+
387
+ #belongsToStream(key: string, packetId: number, catalogTrackName: string): boolean {
388
+ if (!this.#belongsToPacket(key, packetId)) return false
389
+ const parts = key.split(':')
390
+ const encodedTrackName = encodeURIComponent(catalogTrackName)
391
+ if (parts.length === 2) return parts[1] === encodedTrackName
392
+ if (parts.length >= 3) return parts[2] === encodedTrackName
393
+ return false
394
+ }
395
+
396
+ #packetIdsForStreamKeys(keys: Set<string>): number[] {
397
+ return [...new Set([...keys].map((key) => Number(key.split(':', 1)[0])))]
398
+ }
399
+
400
+ #parseStreamKey(key: string): { packetId: number; catalogTrackName?: string } {
401
+ const separator = key.indexOf(':')
402
+ if (separator < 0) return { packetId: Number(key) || 0 }
403
+ return {
404
+ packetId: Number(key.slice(0, separator)) || 0,
405
+ catalogTrackName: decodeURIComponent(key.slice(separator + 1)),
406
+ }
407
+ }
408
+
409
+ #blockKey(sbn: number, packetId: number, catalogTrackName?: string): string {
410
+ const base = `${packetId}:${sbn}`
411
+ return catalogTrackName === undefined
412
+ ? base
413
+ : `${base}:${encodeURIComponent(catalogTrackName)}`
414
+ }
415
+
416
+ #parseBlockKey(key: string): { packetId: number; sbn: number; catalogTrackName?: string } {
417
+ const [packetIdText, sbnText, ...catalogParts] = key.split(':')
418
+ const catalogTrackName = catalogParts.length > 0
419
+ ? decodeURIComponent(catalogParts.join(':'))
420
+ : undefined
421
+ return {
422
+ packetId: Number(packetIdText) || 0,
423
+ sbn: Number(sbnText) || 0,
424
+ ...(catalogTrackName === undefined ? {} : { catalogTrackName }),
425
+ }
426
+ }
427
+
428
+ #decoderForPacket(packetId: number, catalogTrackName?: string) {
429
+ return this.#fecDecoders.get(this.#streamKey(packetId, catalogTrackName))
430
+ ?? this.#fecDecoders.get(this.#streamKey(packetId))
431
+ }
432
+
433
+ #workerForPacket(packetId: number, catalogTrackName?: string) {
434
+ return this.#workerClients.get(this.#streamKey(packetId, catalogTrackName))
435
+ ?? this.#workerClients.get(this.#streamKey(packetId))
436
+ }
437
+
438
+ /**
439
+ * The stream's Worker client for routing, or undefined after evicting a
440
+ * registered client that was already disposed. A disposed client throws on
441
+ * every command, so it leaves routing here before a symbol reaches it and
442
+ * the stream reports its decoder unavailable instead.
443
+ */
444
+ #routableWorkerForPacket(packetId: number, catalogTrackName?: string): FecWorkerClient | undefined {
445
+ const client = this.#workerForPacket(packetId, catalogTrackName)
446
+ if (client === undefined || client.disposed !== true) return client
447
+ this.#evictDisposedWorker(client)
448
+ return undefined
449
+ }
450
+
451
+ #claimDecoderBlock(
452
+ client: object,
453
+ sbn: number,
454
+ catalogTrackName?: string,
455
+ ): boolean {
456
+ let blocks = this.#decoderBlockOwners.get(client)
457
+ if (!blocks) {
458
+ blocks = new Map()
459
+ this.#decoderBlockOwners.set(client, blocks)
460
+ }
461
+ const owner = blocks.get(sbn)
462
+ if (!owner) {
463
+ blocks.set(sbn, { catalogTrackName, conflicted: false })
464
+ if (blocks.size > 512) {
465
+ const stale = [...blocks.keys()].slice(0, 256)
466
+ for (const key of stale) blocks.delete(key)
467
+ }
468
+ return true
469
+ }
470
+ if (owner.conflicted) return false
471
+ if (owner.catalogTrackName === catalogTrackName) return true
472
+ owner.conflicted = true
473
+ return false
474
+ }
475
+
476
+ #decoderBlockCatalogTrackName(client: object, sbn: number): string | undefined {
477
+ const owner = this.#decoderBlockOwners.get(client)?.get(sbn)
478
+ return owner?.conflicted ? undefined : owner?.catalogTrackName
479
+ }
480
+
481
+ #resetDecoderBlockOwners(client: object, catalogTrackName: string): void {
482
+ const blocks = this.#decoderBlockOwners.get(client)
483
+ if (!blocks) return
484
+ for (const [sbn, owner] of blocks) {
485
+ if (owner.catalogTrackName === catalogTrackName || owner.conflicted) {
486
+ blocks.delete(sbn)
487
+ }
488
+ }
489
+ if (blocks.size === 0) this.#decoderBlockOwners.delete(client)
490
+ }
491
+
492
+ #discardFecDecoderClient(client: WasmFecDecoder): void {
493
+ for (const [key, entry] of this.#fecDecoders) {
494
+ if (entry.client === client) this.#fecDecoders.delete(key)
495
+ }
496
+ this.#decoderBlockOwners.delete(client)
497
+ }
498
+
499
+ #assertExclusiveDecoderClient(
500
+ streamKey: string,
501
+ client: object,
502
+ registrations: Iterable<readonly [string, object]>,
503
+ kind: 'decoder' | 'worker',
504
+ ): void {
505
+ for (const [registeredKey, registeredClient] of registrations) {
506
+ if (registeredClient !== client || registeredKey === streamKey) continue
507
+ throw new Error(
508
+ `[FECManager] ${kind} client is already registered for stream ${registeredKey}; ` +
509
+ `create one client per physical stream`,
510
+ )
511
+ }
512
+ }
513
+
514
+ #statsForPacket(packetId: number): MutableFecCounters {
515
+ let stats = this.#fecTrackStats.get(packetId)
516
+ if (!stats) {
517
+ stats = newFecCounters()
518
+ this.#fecTrackStats.set(packetId, stats)
519
+ }
520
+ return stats
521
+ }
522
+
523
+ #markPathBlockComplete(sbn: number, packetId: number, catalogTrackName?: string): void {
524
+ const block = this.#blockPathCounts.get(this.#blockKey(sbn, packetId, catalogTrackName))
525
+ if (block) block.complete = true
526
+ }
527
+
528
+ /** Cancel and forget a block's D-28 repair-deadline timer. */
529
+ #clearRepairDeadline(key: string): void {
530
+ const timer = this.#repairDeadlines.get(key)
531
+ if (timer != null) clearTimeout(timer)
532
+ this.#repairDeadlines.delete(key)
533
+ }
534
+
535
+ #requestRepair(
536
+ sbn: number,
537
+ packetId: number,
538
+ catalogTrackName?: string,
539
+ ): void {
540
+ const key = this.#blockKey(sbn, packetId, catalogTrackName)
541
+ if (
542
+ this.#completedBlocks.has(key) ||
543
+ !this.onRepairNeeded ||
544
+ this.#repairRequestedBlocks.has(key)
545
+ ) return
546
+
547
+ const deadline = this.#repairDeadlines.get(key)
548
+ if (deadline) clearTimeout(deadline)
549
+ this.#repairDeadlines.set(key, null)
550
+ this.#repairRequestedBlocks.add(key)
551
+
552
+ // Preserve the source-progress inactivity deadline and append exactly one
553
+ // bounded repair-fetch grace. Once requested, noteBlockActivity() no
554
+ // longer moves this terminal cleanup boundary.
555
+ const config = this.#resolveTrackConfig(packetId)
556
+ const graceMs = this.#repairGraceMs(config)
557
+ const now = performance.now()
558
+ const currentDeadline = this.#activeBlockDeadlinesMs.get(key) ?? now
559
+ this.#armBlockTimeout(
560
+ sbn,
561
+ packetId,
562
+ catalogTrackName,
563
+ Math.max(1, Math.ceil(Math.max(now, currentDeadline) + graceMs - now)),
564
+ )
565
+ this.#traceLifecycle('repair-requested', sbn, packetId, catalogTrackName)
566
+ if (catalogTrackName === undefined) {
567
+ this.onRepairNeeded(sbn, packetId)
568
+ } else {
569
+ this.onRepairNeeded(sbn, packetId, catalogTrackName)
570
+ }
571
+ }
572
+
573
+ #traceLifecycle(
574
+ event: FecLifecycleTrace['event'],
575
+ sbn: number,
576
+ packetId: number,
577
+ catalogTrackName?: string,
578
+ rsId?: number,
579
+ ): void {
580
+ const key = this.#blockKey(sbn, packetId, catalogTrackName)
581
+ const deadline = this.#activeBlockDeadlinesMs.get(key)
582
+ const decoderPath = this.#syncDecoderBlockOwners.has(key)
583
+ ? 'sync'
584
+ : this.#workerForPacket(packetId, catalogTrackName)
585
+ ? 'worker'
586
+ : this.#decoderForPacket(packetId, catalogTrackName)
587
+ ? 'sync'
588
+ : 'none'
589
+ this.#lifecycleTrace.push({
590
+ atMs: Date.now(),
591
+ event,
592
+ packetId,
593
+ sbn,
594
+ ...(catalogTrackName === undefined ? {} : { catalogTrackName }),
595
+ sourceCount: this.#blockSourceCounts.get(key) ?? 0,
596
+ sourceEsis: [...(this.#blockSourceEsis.get(key) ?? [])].sort((a, b) => a - b),
597
+ repairRsIds: [...(this.#blockRepairRsIds.get(key) ?? [])].sort((a, b) => a - b),
598
+ decoderPath,
599
+ ...(deadline === undefined
600
+ ? {}
601
+ : { deadlineRemainingMs: Math.max(0, deadline - performance.now()) }),
602
+ pendingCleanup: this.#pendingWorkerCleanups.has(key),
603
+ ...(rsId === undefined ? {} : { rsId }),
604
+ })
605
+ if (this.#lifecycleTrace.length > 64) this.#lifecycleTrace.splice(0, 32)
606
+ }
607
+
608
+ #defaultTrackConfig(): ResolvedFECTrackConfig {
609
+ return {
610
+ k: this.#fecK,
611
+ symbolSize: this.#fecSymbolSize,
612
+ interleaveDepthMs: this.#fecInterleaveDepthMs,
613
+ fps: this.#fecFps,
614
+ timeoutMs: this.#fecTimeoutMs,
615
+ }
616
+ }
617
+
618
+ #resolveTrackConfig(packetId: number): ResolvedFECTrackConfig {
619
+ return this.#trackConfigs.get(packetId) ?? this.#defaultTrackConfig()
620
+ }
621
+
622
+ #computeTimeout(config: {
623
+ k: number
624
+ interleaveDepthMs: number
625
+ fps?: number
626
+ timeoutMs?: number
627
+ }): number {
628
+ if (config.timeoutMs && config.timeoutMs > 0) return config.timeoutMs
629
+ // Only a valid §5.1 ms interleave (>= MIN_INTERLEAVE_DEPTH_MS) drives the
630
+ // timeout. A sub-floor value is a raw frame count (no fps to convert with);
631
+ // fall back to the established #fecTimeoutMs rather than collapse to ~0.
632
+ if (config.interleaveDepthMs >= MIN_INTERLEAVE_DEPTH_MS) {
633
+ return Math.max(
634
+ this.#fecTimeoutMs,
635
+ computeFecDeliveryWindowMs({
636
+ interleaveDepthMs: config.interleaveDepthMs,
637
+ sourceSymbols: config.k,
638
+ mediaUnitsPerSecond: config.fps,
639
+ }),
640
+ )
641
+ }
642
+ return this.#fecTimeoutMs
643
+ }
644
+
645
+ #repairDeadlineMs(config: ResolvedFECTrackConfig): number {
646
+ const halfFailBoundMs = Math.ceil(config.timeoutMs / 2)
647
+ if (
648
+ config.k > 0 &&
649
+ config.interleaveDepthMs >= MIN_INTERLEAVE_DEPTH_MS &&
650
+ config.fps !== undefined &&
651
+ Number.isFinite(config.fps) &&
652
+ config.fps > 0
653
+ ) {
654
+ const mediaUnitMs = 1000 / config.fps
655
+ const interleaveUnits = deriveFecInterleaveDepth(
656
+ config.interleaveDepthMs,
657
+ mediaUnitMs,
658
+ )
659
+ const sourceHorizonMs = Math.ceil(config.k * interleaveUnits * mediaUnitMs)
660
+ if (sourceHorizonMs < config.timeoutMs) return sourceHorizonMs
661
+ }
662
+ return halfFailBoundMs
663
+ }
664
+
665
+ #repairGraceMs(config: ResolvedFECTrackConfig): number {
666
+ return Math.max(1, config.timeoutMs - this.#repairDeadlineMs(config))
667
+ }
668
+
669
+ #armBlockTimeout(
670
+ sbn: number,
671
+ packetId: number,
672
+ catalogTrackName: string | undefined,
673
+ delayMs: number,
674
+ ): void {
675
+ const key = this.#blockKey(sbn, packetId, catalogTrackName)
676
+ const current = this.#activeBlocks.get(key)
677
+ if (current) clearTimeout(current)
678
+ const boundedDelayMs = Math.max(1, Math.ceil(delayMs))
679
+ this.#activeBlockDeadlinesMs.set(key, performance.now() + boundedDelayMs)
680
+ const owner = {}
681
+ this.#activeBlockTimeoutOwners.set(key, owner)
682
+ const timer = setTimeout(() => {
683
+ // clearTimeout() cannot retract a callback that the browser already
684
+ // moved onto its task queue. Only the timer that still owns this
685
+ // block's deadline may fail it or enqueue Worker cleanup.
686
+ if (this.#activeBlockTimeoutOwners.get(key) !== owner) return
687
+ this.#activeBlocks.delete(key)
688
+ this.#activeBlockDeadlinesMs.delete(key)
689
+ this.#activeBlockTimeoutOwners.delete(key)
690
+ this.#traceLifecycle('timeout-queued', sbn, packetId, catalogTrackName)
691
+
692
+ // A block that began on the synchronous decoder remains owned by that
693
+ // decoder even if Worker setup completed later. Cleanup must follow the
694
+ // same ownership rule as source and repair symbol dispatch.
695
+ const syncOwner = this.#syncDecoderBlockOwners.get(key)
696
+ if (syncOwner) {
697
+ this.#routeSyncCleanup(syncOwner, sbn, packetId)
698
+ this.#failBlock(sbn, packetId, false, catalogTrackName)
699
+ return
700
+ }
701
+
702
+ const workerClient = this.#routableWorkerForPacket(packetId, catalogTrackName)
703
+ if (workerClient) {
704
+ this.#pendingWorkerCleanups.add(key)
705
+ this.#pendingWorkerCleanupClients.set(key, workerClient)
706
+ workerClient.cleanup(sbn)
707
+ } else {
708
+ const decoder = this.#decoderForPacket(packetId, catalogTrackName)?.client
709
+ if (decoder) {
710
+ this.#routeSyncCleanup(decoder, sbn, packetId)
711
+ this.#failBlock(sbn, packetId, false, catalogTrackName)
712
+ } else {
713
+ this.#failBlock(sbn, packetId, false, catalogTrackName)
714
+ }
715
+ }
716
+ }, boundedDelayMs)
717
+ this.#activeBlocks.set(key, timer)
718
+ }
719
+
720
+ #upsertTrackConfig(packetId: number, config: FECManagerConfig): ResolvedFECTrackConfig {
721
+ const previous = this.#trackConfigs.get(packetId) ?? this.#defaultTrackConfig()
722
+ const timingChanged =
723
+ config.k !== undefined ||
724
+ config.interleaveDepthMs !== undefined ||
725
+ config.fps !== undefined
726
+ const next: ResolvedFECTrackConfig = {
727
+ k: config.k ?? previous.k,
728
+ symbolSize:
729
+ config.symbolSize != null && config.symbolSize > 0
730
+ ? config.symbolSize
731
+ : previous.symbolSize,
732
+ interleaveDepthMs: config.interleaveDepthMs ?? previous.interleaveDepthMs,
733
+ fps: config.fps ?? previous.fps,
734
+ timeoutMs: config.timeoutMs ?? (timingChanged ? 0 : previous.timeoutMs),
735
+ }
736
+ next.timeoutMs = this.#computeTimeout(next)
737
+ this.#trackConfigs.set(packetId, next)
738
+ return next
739
+ }
740
+
741
+ #normalizeSymbolData(packetId: number, data: Uint8Array): Uint8Array {
742
+ const symbolSize = this.symbolSizeForPacket(packetId)
743
+ if (symbolSize <= 0 || data.byteLength === symbolSize) return data
744
+ const normalized = new Uint8Array(symbolSize)
745
+ normalized.set(data.subarray(0, symbolSize))
746
+ return normalized
747
+ }
748
+
749
+ #blockKeysFor(sbn: number, packetId?: number, catalogTrackName?: string): string[] {
750
+ if (packetId != null && catalogTrackName !== undefined) {
751
+ return [this.#blockKey(sbn, packetId, catalogTrackName)]
752
+ }
753
+
754
+ const keys = new Set<string>()
755
+ for (const source of [
756
+ this.#activeBlocks,
757
+ this.#blockSourceCounts,
758
+ this.#blockCatalogTrackNames,
759
+ ]) {
760
+ for (const key of source.keys()) {
761
+ const block = this.#parseBlockKey(key)
762
+ if (
763
+ block.sbn === sbn &&
764
+ (packetId === undefined || block.packetId === packetId)
765
+ ) keys.add(key)
766
+ }
767
+ }
768
+ for (const source of [this.#completedBlocks, this.#cleanlyCompletedBlocks]) {
769
+ for (const key of source) {
770
+ const block = this.#parseBlockKey(key)
771
+ if (
772
+ block.sbn === sbn &&
773
+ (packetId === undefined || block.packetId === packetId)
774
+ ) keys.add(key)
775
+ }
776
+ }
777
+
778
+ if (packetId !== undefined) {
779
+ if (keys.size === 0) return [this.#blockKey(sbn, packetId)]
780
+ return keys.size === 1 ? [...keys] : []
781
+ }
782
+ return keys.size > 0 ? [...keys] : [this.#blockKey(sbn, 0, catalogTrackName)]
783
+ }
784
+
785
+ // -----------------------------------------------------------------------
786
+ // Accessors
787
+ // -----------------------------------------------------------------------
788
+
789
+ /** Internal state for diagnostics — not part of the public stats API. */
790
+ get diagnostics() {
791
+ return {
792
+ timeoutMs: this.#fecTimeoutMs,
793
+ activeBlocks: this.#activeBlocks.size,
794
+ completedBlocks: this.#completedBlocks.size,
795
+ pendingCounts: Object.fromEntries(this.#blockSourceCounts),
796
+ fpsDetected: this.#fpsDetected,
797
+ interleaveDepthMs: this.#fecInterleaveDepthMs,
798
+ startupAlignedPackets: this.#packetIdsForStreamKeys(this.#startupAlignedPackets),
799
+ startupPartialBlocks: this.#startupPartialBlocks.size,
800
+ startupRepairReadyPackets: this.#packetIdsForStreamKeys(this.#startupRepairReadyPackets),
801
+ startupRepairFailOpenPackets: this.#packetIdsForStreamKeys(this.#startupRepairFailOpenPackets),
802
+ pendingWorkerCleanups: this.#pendingWorkerCleanups.size,
803
+ decoderUnavailableEvents: this.#decoderUnavailableEvents,
804
+ repairTrackedBlocks: this.#blockRepairRsIds.size,
805
+ lifecycleTrace: this.#lifecycleTrace.map((entry) => ({ ...entry })),
806
+ }
807
+ }
808
+
809
+ get fecStats(): FecStats {
810
+ const s = this.#fecStats
811
+ const affected = s.blocksRecovered + s.blocksFailed
812
+ const total = s.blocksClean + affected
813
+ const tracks = [...this.#fecTrackStats.entries()]
814
+ .sort(([left], [right]) => left - right)
815
+ .map(([packetId, stats]) => {
816
+ const trackAffected = stats.blocksRecovered + stats.blocksFailed
817
+ const trackTotal = stats.blocksClean + trackAffected
818
+ return {
819
+ ...stats,
820
+ packetId,
821
+ blocksComplete: trackTotal,
822
+ k: this.#resolveTrackConfig(packetId).k,
823
+ recoveryRate: trackAffected > 0
824
+ ? (stats.blocksRecovered / trackAffected) * 100
825
+ : 100,
826
+ packetLossRate: trackTotal > 0 ? (trackAffected / trackTotal) * 100 : 0,
827
+ }
828
+ })
829
+ return {
830
+ ...s,
831
+ blocksComplete: total,
832
+ mixedPathBlocks: Array.from(this.#blockPathCounts.values()).filter(
833
+ (block) => block.multicastSymbols > 0 && block.moqSymbols > 0,
834
+ ).length,
835
+ recentBlockPathCounts: Array.from(this.#blockPathCounts.values()).slice(-24),
836
+ k: this.#fecK,
837
+ // True recovery rate: of blocks that HAD loss, how many were recovered?
838
+ recoveryRate: affected > 0 ? (s.blocksRecovered / affected) * 100 : 100,
839
+ packetLossRate: total > 0 ? (affected / total) * 100 : 0,
840
+ tracks,
841
+ workers: [...this.#fecWorkerStats.entries()]
842
+ .sort(([left], [right]) => left - right)
843
+ .map(([packetId, stats]) => ({
844
+ ...stats,
845
+ config: {
846
+ ...stats.config,
847
+ resolution: stats.config.resolution ? { ...stats.config.resolution } : undefined,
848
+ },
849
+ blocks: stats.blocks.map((block) => ({ ...block })),
850
+ decoder: stats.decoder ? { ...stats.decoder } : undefined,
851
+ altaFailedDetails: stats.altaFailedDetails?.map((detail) => ({ ...detail })),
852
+ packetId,
853
+ })),
854
+ repairSymbolSizeMismatches: this.#repairSymbolSizeMismatchCount,
855
+ workerErrors: Object.fromEntries(this.#workerErrorCounts),
856
+ }
857
+ }
858
+
859
+ /**
860
+ * Observe Worker-backed per-track snapshots without taking ownership of the
861
+ * legacy single-callback slot. Infrastructure diagnostics and a player may
862
+ * therefore consume the same source event without wrapping or replacing one
863
+ * another's callback.
864
+ */
865
+ addFecStatsObserver(
866
+ observer: (packetId: number, stats: FecTrackStats) => void,
867
+ ): () => void {
868
+ this.#fecStatsObservers.add(observer)
869
+ return () => this.#fecStatsObservers.delete(observer)
870
+ }
871
+
872
+ /** Start a new multicast transport join without discarding track/decoder state. */
873
+ beginMulticastJoin(): void {
874
+ this.#startupAlignedPackets.clear()
875
+ this.#startupPartialBlocks.clear()
876
+ this.#startupRequireRepairReady = true
877
+ this.#startupRepairReadyPackets.clear()
878
+ this.#startupRepairBoundaryCounts.clear()
879
+ this.#startupRepairFailOpenPackets.clear()
880
+ }
881
+
882
+ /** Begin a fresh decode epoch for one physical stream. */
883
+ resetTrack(packetId: number, catalogTrackName: string): void {
884
+ if (!Number.isInteger(packetId) || packetId < 0) {
885
+ throw new Error(`[FECManager] invalid packetId for reset: ${packetId}`)
886
+ }
887
+ if (!catalogTrackName) {
888
+ throw new Error('[FECManager] catalogTrackName is required for track reset')
889
+ }
890
+ const resetMap = <T>(map: Map<string, T>): void => {
891
+ for (const key of map.keys()) {
892
+ if (!this.#belongsToStream(key, packetId, catalogTrackName)) continue
893
+ map.delete(key)
894
+ }
895
+ }
896
+ const resetTimerMap = (
897
+ map: Map<string, ReturnType<typeof setTimeout> | null>,
898
+ ): void => {
899
+ for (const [key, timer] of map) {
900
+ if (!this.#belongsToStream(key, packetId, catalogTrackName)) continue
901
+ if (timer != null) clearTimeout(timer)
902
+ map.delete(key)
903
+ }
904
+ }
905
+ const resetSet = (set: Set<string>): void => {
906
+ for (const key of set) {
907
+ if (this.#belongsToStream(key, packetId, catalogTrackName)) set.delete(key)
908
+ }
909
+ }
910
+
911
+ resetTimerMap(this.#activeBlocks)
912
+ resetTimerMap(this.#repairDeadlines)
913
+ resetMap(this.#activeBlockDeadlinesMs)
914
+ resetMap(this.#activeBlockTimeoutOwners)
915
+ resetMap(this.#pendingWorkerCleanupClients)
916
+ resetMap(this.#blockSourceCounts)
917
+ resetMap(this.#blockSourceEsis)
918
+ resetMap(this.#blockRepairRsIds)
919
+ resetMap(this.#blockPathCounts)
920
+ resetMap(this.#blockCatalogTrackNames)
921
+ resetMap(this.#syncDecoderBlockOwners)
922
+ resetMap(this.#startupRepairBoundaryCounts)
923
+ resetSet(this.#pendingWorkerCleanups)
924
+ resetSet(this.#completedBlocks)
925
+ resetSet(this.#cleanlyCompletedBlocks)
926
+ resetSet(this.#repairRequestedBlocks)
927
+ resetSet(this.#workerRecoveredBlocks)
928
+ resetSet(this.#startupAlignedPackets)
929
+ resetSet(this.#startupPartialBlocks)
930
+ resetSet(this.#startupRepairReadyPackets)
931
+ resetSet(this.#startupRepairFailOpenPackets)
932
+ resetSet(this.#seenSymbols)
933
+
934
+ const decoderClients = new Set<WasmFecDecoder>()
935
+ for (const [key, entry] of this.#fecDecoders) {
936
+ if (this.#belongsToStream(key, packetId, catalogTrackName)) {
937
+ decoderClients.add(entry.client)
938
+ }
939
+ }
940
+ const clients = new Set<object>()
941
+ for (const client of decoderClients) {
942
+ if (
943
+ typeof client.flush === 'function' &&
944
+ typeof client.reset_stats === 'function'
945
+ ) {
946
+ try {
947
+ client.flush()
948
+ client.reset_stats()
949
+ clients.add(client)
950
+ } catch {
951
+ this.#discardFecDecoderClient(client)
952
+ }
953
+ } else {
954
+ this.#discardFecDecoderClient(client)
955
+ }
956
+ }
957
+ for (const [key, client] of [...this.#workerClients]) {
958
+ if (!this.#belongsToStream(key, packetId, catalogTrackName)) continue
959
+ if (client.disposed === true) {
960
+ // reset() throws on a disposed client; evict it (reporting the
961
+ // decoder unavailable) instead of resetting a Worker that is gone.
962
+ this.#evictDisposedWorker(client)
963
+ continue
964
+ }
965
+ client.reset()
966
+ clients.add(client)
967
+ }
968
+ for (const client of clients) this.#resetDecoderBlockOwners(client, catalogTrackName)
969
+ }
970
+
971
+ /** Record a transport symbol and reject later duplicates of the same packetId/SBN/ESI. */
972
+ feedSymbolFromSource(
973
+ packetId: number,
974
+ sbn: number,
975
+ esi: number,
976
+ _data: Uint8Array,
977
+ source: FecSymbolSource,
978
+ catalogTrackName?: string,
979
+ ): boolean {
980
+ const blockKey = this.#blockKey(sbn, packetId, catalogTrackName)
981
+ const streamKey = this.#streamKey(packetId, catalogTrackName)
982
+ const k = this.kForPacket(packetId)
983
+ const isSourceSymbol = esi >= 0 && esi < k
984
+ const isStartupPartial = this.#startupPartialBlocks.has(blockKey)
985
+ const isReadyMulticastRepair =
986
+ this.#startupRequireRepairReady &&
987
+ source === 'multicast' &&
988
+ !isSourceSymbol &&
989
+ this.#startupRepairReadyPackets.has(streamKey)
990
+ const awaitingMulticastRepair =
991
+ this.#startupRequireRepairReady &&
992
+ source === 'multicast' &&
993
+ !this.#startupRepairReadyPackets.has(streamKey) &&
994
+ !this.#startupRepairFailOpenPackets.has(streamKey)
995
+ if (awaitingMulticastRepair) {
996
+ if (!isSourceSymbol) {
997
+ this.#startupRepairReadyPackets.add(streamKey)
998
+ this.#startupRepairBoundaryCounts.delete(streamKey)
999
+ this.#markStartupPartial(blockKey)
1000
+ return false
1001
+ }
1002
+
1003
+ if (esi === 0) {
1004
+ const boundaries = (this.#startupRepairBoundaryCounts.get(streamKey) ?? 0) + 1
1005
+ this.#startupRepairBoundaryCounts.set(streamKey, boundaries)
1006
+ if (boundaries >= STARTUP_REPAIR_WAIT_SOURCE_BOUNDARIES) {
1007
+ this.#startupRepairBoundaryCounts.delete(streamKey)
1008
+ this.#startupRepairFailOpenPackets.add(streamKey)
1009
+ } else {
1010
+ this.#markStartupPartial(blockKey)
1011
+ return false
1012
+ }
1013
+ } else {
1014
+ this.#markStartupPartial(blockKey)
1015
+ return false
1016
+ }
1017
+ }
1018
+ if (isSourceSymbol && esi === 0) {
1019
+ // ESI=0 establishes a complete source boundary for this SBN. Remove
1020
+ // only this block: other interleaved SBNs may still be join partials.
1021
+ this.#startupPartialBlocks.delete(blockKey)
1022
+ this.#startupAlignedPackets.add(streamKey)
1023
+ } else if (
1024
+ (isStartupPartial && !isReadyMulticastRepair) ||
1025
+ (!this.#startupAlignedPackets.has(streamKey) && !isReadyMulticastRepair)
1026
+ ) {
1027
+ this.#markStartupPartial(blockKey)
1028
+ return false
1029
+ }
1030
+
1031
+ // A completed block remains authoritative after its individual symbol
1032
+ // identities age out of the bounded #seenSymbols cache. Without this
1033
+ // guard, a delayed copy from the other transport can recreate the block
1034
+ // inside a Worker decoder; that orphan never receives the full source and
1035
+ // repair set, so cleanup later reports a false failure/green fill.
1036
+ if (this.#completedBlocks.has(blockKey)) {
1037
+ this.#fecStats.duplicateSymbols++
1038
+ this.#statsForPacket(packetId).duplicateSymbols++
1039
+ return false
1040
+ }
1041
+
1042
+ const symbolKey = `${blockKey}:${esi}`
1043
+ if (this.#seenSymbols.has(symbolKey)) {
1044
+ this.#fecStats.duplicateSymbols++
1045
+ this.#statsForPacket(packetId).duplicateSymbols++
1046
+ return false
1047
+ }
1048
+ this.#seenSymbols.add(symbolKey)
1049
+
1050
+ const counts = this.#blockPathCounts.get(blockKey) ?? {
1051
+ packetId,
1052
+ sbn,
1053
+ ...(catalogTrackName === undefined ? {} : { catalogTrackName }),
1054
+ multicastSymbols: 0,
1055
+ moqSymbols: 0,
1056
+ complete: false,
1057
+ }
1058
+ if (source === 'multicast') counts.multicastSymbols++
1059
+ else counts.moqSymbols++
1060
+ this.#blockPathCounts.set(blockKey, counts)
1061
+ this.#observeBlockCatalogTrackName(blockKey, catalogTrackName)
1062
+
1063
+ if (this.#seenSymbols.size > 4096) {
1064
+ this.#seenSymbols = new Set(Array.from(this.#seenSymbols).slice(-2048))
1065
+ }
1066
+ if (this.#blockPathCounts.size > 512) {
1067
+ this.#blockPathCounts = new Map(Array.from(this.#blockPathCounts).slice(-256))
1068
+ }
1069
+ if (this.#blockCatalogTrackNames.size > 512) {
1070
+ this.#blockCatalogTrackNames = new Map(
1071
+ Array.from(this.#blockCatalogTrackNames).slice(-256),
1072
+ )
1073
+ }
1074
+
1075
+ return true
1076
+ }
1077
+
1078
+ #observeBlockCatalogTrackName(blockKey: string, catalogTrackName?: string): void {
1079
+ if (catalogTrackName === undefined) return
1080
+ if (!this.#blockCatalogTrackNames.has(blockKey)) {
1081
+ this.#blockCatalogTrackNames.set(blockKey, catalogTrackName)
1082
+ return
1083
+ }
1084
+ if (this.#blockCatalogTrackNames.get(blockKey) !== catalogTrackName) {
1085
+ this.#blockCatalogTrackNames.set(blockKey, null)
1086
+ }
1087
+ }
1088
+
1089
+ /** Exact catalog identity retained for async recovery, if unambiguous. */
1090
+ catalogTrackNameForBlock(
1091
+ sbn: number,
1092
+ packetId: number,
1093
+ catalogTrackName?: string,
1094
+ ): string | undefined {
1095
+ if (catalogTrackName !== undefined) {
1096
+ return this.#blockCatalogTrackNames.get(
1097
+ this.#blockKey(sbn, packetId, catalogTrackName),
1098
+ ) ?? undefined
1099
+ }
1100
+ const names = new Set<string>()
1101
+ for (const [key, name] of this.#blockCatalogTrackNames) {
1102
+ const block = this.#parseBlockKey(key)
1103
+ if (block.packetId === packetId && block.sbn === sbn && name !== null) names.add(name)
1104
+ }
1105
+ return names.size === 1 ? [...names][0] : undefined
1106
+ }
1107
+
1108
+ #markStartupPartial(blockKey: string): void {
1109
+ this.#startupPartialBlocks.add(blockKey)
1110
+ if (this.#startupPartialBlocks.size > 128) {
1111
+ this.#startupPartialBlocks = new Set(
1112
+ Array.from(this.#startupPartialBlocks).slice(-64),
1113
+ )
1114
+ }
1115
+ }
1116
+
1117
+ /** Per-block state snapshot for FecBlockGrid visualization.
1118
+ * Returns the most recent blocks (active + recently completed). */
1119
+ getBlockStates(): Array<{
1120
+ sbn: number;
1121
+ packetId?: number;
1122
+ state: 'collecting' | 'complete' | 'failed';
1123
+ sourceCount: number;
1124
+ k: number;
1125
+ progress: number;
1126
+ }> {
1127
+ const blocks: Array<{ sbn: number; packetId?: number; state: 'collecting' | 'complete' | 'failed'; sourceCount: number; k: number; progress: number }> = [];
1128
+ // Active blocks (still collecting symbols)
1129
+ for (const [key] of this.#activeBlocks) {
1130
+ const { packetId, sbn } = this.#parseBlockKey(key);
1131
+ const count = this.#blockSourceCounts.get(key) ?? 0;
1132
+ const k = this.kForPacket(packetId);
1133
+ blocks.push({ sbn, packetId, state: 'collecting', sourceCount: count, k, progress: k > 0 ? Math.min(count / k, 1) : 0 });
1134
+ }
1135
+ // Recently completed blocks (last 16)
1136
+ const completed = [...this.#completedBlocks].slice(-16);
1137
+ for (const key of completed) {
1138
+ const { packetId, sbn } = this.#parseBlockKey(key);
1139
+ if (!this.#activeBlocks.has(key)) {
1140
+ const k = this.kForPacket(packetId);
1141
+ blocks.push({ sbn, packetId, state: 'complete', sourceCount: k, k, progress: 1 });
1142
+ }
1143
+ }
1144
+ blocks.sort((a, b) => a.sbn - b.sbn || (a.packetId ?? 0) - (b.packetId ?? 0));
1145
+ return blocks.slice(-24); // cap at 24 most recent
1146
+ }
1147
+
1148
+ /** The raw WASM decoder for stats access (first registered decoder) */
1149
+ get wasmDecoder(): WasmFecDecoder | null {
1150
+ return this.#fecDecoders.values().next().value?.client ?? null
1151
+ }
1152
+
1153
+ get k(): number { return this.#fecK }
1154
+ kForPacket(packetId: number): number { return this.#resolveTrackConfig(packetId).k }
1155
+ /** FEC symbol size in bytes. */
1156
+ get symbolSize(): number { return this.#fecSymbolSize }
1157
+ symbolSizeForPacket(packetId: number): number { return this.#resolveTrackConfig(packetId).symbolSize }
1158
+ /** FEC block span in milliseconds (0 = no interleaving). */
1159
+ get interleaveDepthMs(): number { return this.#fecInterleaveDepthMs }
1160
+ interleaveDepthMsForPacket(packetId: number): number { return this.#resolveTrackConfig(packetId).interleaveDepthMs }
1161
+ /** Resolved per-track block fail bound in milliseconds. */
1162
+ timeoutMsForPacket(packetId: number): number { return this.#resolveTrackConfig(packetId).timeoutMs }
1163
+ /** Resolved per-track D-28 repair request deadline in milliseconds. */
1164
+ repairDeadlineMsForPacket(packetId: number): number {
1165
+ return this.#repairDeadlineMs(this.#resolveTrackConfig(packetId))
1166
+ }
1167
+ /** Remaining fetch budget for a specific repair-requested block. */
1168
+ repairFetchTimeoutMsForBlock(
1169
+ sbn: number,
1170
+ packetId: number,
1171
+ catalogTrackName?: string,
1172
+ ): number {
1173
+ const deadlineMs = this.#activeBlockDeadlinesMs.get(
1174
+ this.#blockKey(sbn, packetId, catalogTrackName),
1175
+ )
1176
+ if (deadlineMs !== undefined) {
1177
+ return Math.max(1, Math.ceil(deadlineMs - performance.now()))
1178
+ }
1179
+ const trackConfig = this.#resolveTrackConfig(packetId)
1180
+ return Math.max(1, trackConfig.timeoutMs - this.#repairDeadlineMs(trackConfig))
1181
+ }
1182
+
1183
+ // -----------------------------------------------------------------------
1184
+ // Configuration
1185
+ // -----------------------------------------------------------------------
1186
+
1187
+ /** Configure FEC parameters. */
1188
+ configure(config: FECManagerConfig): void {
1189
+ if (config.k != null) this.#fecK = config.k
1190
+ if (config.symbolSize != null && config.symbolSize > 0) this.#fecSymbolSize = config.symbolSize
1191
+ if (config.interleaveDepthMs != null) this.#fecInterleaveDepthMs = config.interleaveDepthMs
1192
+ if (config.fps != null && Number.isFinite(config.fps) && config.fps > 0) {
1193
+ this.#fecFps = config.fps
1194
+ }
1195
+ // interleaveDepthMs is in milliseconds per spec §5.1.
1196
+ // Valid range: 33ms (interactive/gaming) to 10000ms (broadcast ATSC 3.0).
1197
+ // Warn if value looks like frame count instead of ms (common misconfiguration).
1198
+ if (config.interleaveDepthMs != null && config.interleaveDepthMs > 0 && config.interleaveDepthMs < MIN_INTERLEAVE_DEPTH_MS) {
1199
+ console.warn(`[FECManager] interleaveDepthMs=${config.interleaveDepthMs} looks like frame count, not milliseconds (§5.1 requires ms). Expected range: ${MIN_INTERLEAVE_DEPTH_MS}-10000ms.`)
1200
+ }
1201
+ if (config.timeoutMs != null && config.timeoutMs > 0) {
1202
+ this.#fecTimeoutMs = config.timeoutMs
1203
+ } else if (
1204
+ config.interleaveDepthMs != null &&
1205
+ config.interleaveDepthMs >= MIN_INTERLEAVE_DEPTH_MS
1206
+ ) {
1207
+ this.#fecTimeoutMs = computeFecDeliveryWindowMs({
1208
+ interleaveDepthMs: config.interleaveDepthMs,
1209
+ sourceSymbols: this.#fecK,
1210
+ symbolSize: this.#fecSymbolSize,
1211
+ mediaUnitsPerSecond: config.fps,
1212
+ })
1213
+ }
1214
+ // A shared multicast parser receives every packetId. Keep existing track
1215
+ // deadlines at least as wide as the selected video delivery envelope so a
1216
+ // short audio interleave cannot expire during browser/AMT scheduling gaps.
1217
+ for (const [packetId, track] of this.#trackConfigs) {
1218
+ if (track.timeoutMs < this.#fecTimeoutMs) {
1219
+ this.#trackConfigs.set(packetId, { ...track, timeoutMs: this.#fecTimeoutMs })
1220
+ }
1221
+ }
1222
+ }
1223
+
1224
+ /** Configure FEC parameters for one packetId without changing legacy defaults. */
1225
+ configureTrack(packetId: number, config: FECManagerConfig): void {
1226
+ this.#upsertTrackConfig(packetId, config)
1227
+ }
1228
+
1229
+ /** Register a FEC decoder for a specific packetId (track). */
1230
+ setFecDecoder(
1231
+ packetId: number,
1232
+ client: WasmFecDecoder,
1233
+ k: number,
1234
+ config?: FECManagerConfig,
1235
+ catalogTrackName?: string,
1236
+ ): void {
1237
+ const streamKey = this.#streamKey(packetId, catalogTrackName)
1238
+ this.#assertExclusiveDecoderClient(
1239
+ streamKey,
1240
+ client,
1241
+ [...this.#fecDecoders].map(([key, entry]) => [key, entry.client] as const),
1242
+ 'decoder',
1243
+ )
1244
+ const trackConfig = this.#upsertTrackConfig(packetId, { ...config, k })
1245
+ this.#fecDecoders.set(streamKey, { client, k: trackConfig.k })
1246
+ }
1247
+
1248
+ /**
1249
+ * Register a Worker-backed FEC decoder for a specific packetId (track).
1250
+ *
1251
+ * Worker-backed tracks decode asynchronously — feedSourceSymbol/feedRepairSymbol
1252
+ * return null immediately, and recovered frames arrive via the onBlockRecovered callback.
1253
+ *
1254
+ * Takes precedence over sync WASM decoders for the same packetId.
1255
+ */
1256
+ setFecWorker(
1257
+ packetId: number,
1258
+ client: FecWorkerClient,
1259
+ config: FECManagerConfig,
1260
+ catalogTrackName?: string,
1261
+ ): void {
1262
+ const k = config?.k
1263
+ if (k === undefined || !Number.isFinite(k) || k <= 0) {
1264
+ throw new Error(`[FECManager] packetId=${packetId} Worker requires an explicit positive k`)
1265
+ }
1266
+ const symbolSize = config.symbolSize
1267
+ if (symbolSize === undefined || !Number.isFinite(symbolSize) || symbolSize <= 0) {
1268
+ throw new Error(
1269
+ `[FECManager] packetId=${packetId} Worker requires an explicit positive symbolSize`,
1270
+ )
1271
+ }
1272
+ const streamKey = this.#streamKey(packetId, catalogTrackName)
1273
+ this.#assertExclusiveDecoderClient(
1274
+ streamKey,
1275
+ client,
1276
+ this.#workerClients,
1277
+ 'worker',
1278
+ )
1279
+ // Every Worker leaves routing through #retireWorker, which fails the
1280
+ // blocks it still owed and reports the stream's decoder unavailable.
1281
+ // Overwriting a registration would skip that path and strand those
1282
+ // blocks, so a second Worker under a routed stream is refused.
1283
+ const incumbent = this.#workerClients.get(streamKey)
1284
+ if (incumbent !== undefined && incumbent !== client) {
1285
+ throw new Error(
1286
+ `[FECManager] stream ${streamKey} already routes to a Worker; ` +
1287
+ `call clearFecWorker before registering another`,
1288
+ )
1289
+ }
1290
+ if (config) this.#upsertTrackConfig(packetId, config)
1291
+ const workerK = this.#resolveTrackConfig(packetId).k
1292
+ this.#workerClients.set(streamKey, client)
1293
+
1294
+ client.onFrame((data: ArrayBuffer, meta: FrameMeta) => {
1295
+ if (meta.recovered) {
1296
+ // Recovered Worker events represent one exact source-symbol position.
1297
+ // Block-completion accounting needs that exact ESI, and counting an
1298
+ // ESI-less event as recovered would report healthy FEC while dropping
1299
+ // the media. Enforce the Worker contract before any recovery metric.
1300
+ if (!Number.isInteger(meta.esi) || meta.esi! < 0 || meta.esi! >= workerK) {
1301
+ const kind = 'recovered-symbol-missing-esi'
1302
+ const seen = this.#workerErrorCounts.get(kind) ?? 0
1303
+ this.#workerErrorCounts.set(kind, seen + 1)
1304
+ if (seen < WORKER_ERROR_LOG_CAP) {
1305
+ console.warn(
1306
+ `[FECManager] Dropping recovered symbol without valid ESI: pid=${packetId} sbn=${meta.sbn} esi=${String(meta.esi)}`,
1307
+ )
1308
+ }
1309
+ return
1310
+ }
1311
+ const esi = meta.esi!
1312
+
1313
+ // Suppress duplicate routing when the worker fires recovered=true
1314
+ // for a block ALREADY CLEANLY COMPLETED by K source symbols.
1315
+ //
1316
+ // BLO-3323 L15: predicate was previously `#completedBlocks.has(sbn)`,
1317
+ // but `#completedBlocks` is populated by 5 paths (clean K-arrival,
1318
+ // failed timeout, every worker frame emit via markBlockCompleted below,
1319
+ // external markBlockCompleted, and cancelBlockTimeout). The first
1320
+ // worker frame for a block adds the SBN, so subsequent recovered=true
1321
+ // emits for the same block were always suppressed → blocksRecovered
1322
+ // stayed at 0 even when recovery actually fired. Switching to the
1323
+ // narrower #cleanlyCompletedBlocks set (populated only by trackBlock
1324
+ // when count >= K) preserves the original dedup intent (BLO-3035
1325
+ // layer 4 — don't double-route a block already received via source)
1326
+ // while letting genuinely-recovered blocks bump the counter.
1327
+ const recoveredCatalogTrackName = catalogTrackName
1328
+ ?? this.#decoderBlockCatalogTrackName(client, meta.sbn)
1329
+ if (!this.#claimDecoderBlock(client, meta.sbn, recoveredCatalogTrackName)) return
1330
+ const blockKey = this.#blockKey(meta.sbn, packetId, recoveredCatalogTrackName)
1331
+ const cleanlyCompleted = this.#cleanlyCompletedBlocks.has(blockKey)
1332
+ if (!cleanlyCompleted) {
1333
+ if (!this.#workerRecoveredBlocks.has(blockKey)) {
1334
+ this.#traceLifecycle(
1335
+ 'recovered',
1336
+ meta.sbn,
1337
+ packetId,
1338
+ recoveredCatalogTrackName,
1339
+ )
1340
+ this.#workerRecoveredBlocks.add(blockKey)
1341
+ this.#fecStats.blocksRecovered++
1342
+ this.#statsForPacket(packetId).blocksRecovered++
1343
+ if (this.#workerRecoveredBlocks.size > 1000) {
1344
+ const stale = Array.from(this.#workerRecoveredBlocks).slice(0, 500)
1345
+ for (const key of stale) this.#workerRecoveredBlocks.delete(key)
1346
+ }
1347
+ }
1348
+
1349
+ // RaptorQ returns the full K-symbol block, not only erased symbols.
1350
+ // The direct path already parsed every source ESI in #seenSymbols;
1351
+ // routing those symbols again injects duplicate MFU fragments and can
1352
+ // leave a real FI=3 tail followed by duplicate FI=2 fragments.
1353
+ const symbolKey = `${blockKey}:${esi}`
1354
+ if (!this.#seenSymbols.has(symbolKey)) {
1355
+ this.#seenSymbols.add(symbolKey)
1356
+ this.onBlockRecovered?.(
1357
+ meta.sbn,
1358
+ packetId,
1359
+ data,
1360
+ meta,
1361
+ recoveredCatalogTrackName,
1362
+ )
1363
+ }
1364
+ }
1365
+
1366
+ // The worker emits recovered source symbols in ESI order. Complete the
1367
+ // block only after the final slot so assemblers receive every missing
1368
+ // symbol before their completion callback flushes buffered media.
1369
+ if (
1370
+ esi >= workerK - 1 &&
1371
+ !this.#completedBlocks.has(blockKey)
1372
+ ) {
1373
+ this.cancelBlockTimeout(meta.sbn, packetId, recoveredCatalogTrackName)
1374
+ if (recoveredCatalogTrackName === undefined) {
1375
+ this.onBlockComplete?.(meta.sbn, packetId)
1376
+ } else {
1377
+ this.onBlockComplete?.(
1378
+ meta.sbn,
1379
+ packetId,
1380
+ recoveredCatalogTrackName,
1381
+ )
1382
+ }
1383
+ }
1384
+ } else {
1385
+ const completedCatalogTrackName = catalogTrackName
1386
+ ?? this.#decoderBlockCatalogTrackName(client, meta.sbn)
1387
+ this.markBlockCompleted(meta.sbn, packetId, completedCatalogTrackName)
1388
+ }
1389
+ })
1390
+
1391
+ client.onSnapshot((stats: FecTrackStats) => {
1392
+ this.#fecWorkerStats.set(packetId, stats)
1393
+ try {
1394
+ this.onFecStatsUpdate?.(packetId, stats)
1395
+ } catch (error) {
1396
+ console.warn('[FECManager] FEC stats callback failed:', error)
1397
+ }
1398
+ for (const observer of this.#fecStatsObservers) {
1399
+ try {
1400
+ observer(packetId, stats)
1401
+ } catch (error) {
1402
+ console.warn('[FECManager] FEC stats observer failed:', error)
1403
+ }
1404
+ }
1405
+ })
1406
+
1407
+ client.onGreenFill((sbn: number, data: ArrayBuffer) => {
1408
+ this.#fecStats.greenFillFrames++
1409
+ this.#statsForPacket(packetId).greenFillFrames++
1410
+ this.onGreenFill?.(sbn, packetId, data)
1411
+ })
1412
+
1413
+ client.onCleanupComplete((sbn: number) => {
1414
+ const cleanupKeys = [...this.#pendingWorkerCleanups].filter((key) => {
1415
+ const block = this.#parseBlockKey(key)
1416
+ return block.packetId === packetId &&
1417
+ block.sbn === sbn &&
1418
+ this.#pendingWorkerCleanupClients.get(key) === client
1419
+ })
1420
+ for (const key of cleanupKeys) this.#settleOwedCleanup(key, 'cleanup-ack')
1421
+ })
1422
+
1423
+ client.onError((msg: string, code?: FecWorkerErrorCode) => {
1424
+ const requestedKind = code ?? msg
1425
+ const kind =
1426
+ this.#workerErrorCounts.has(requestedKind) ||
1427
+ this.#workerErrorCounts.size < WORKER_ERROR_KIND_CAP - 1
1428
+ ? requestedKind
1429
+ : WORKER_ERROR_OVERFLOW_KIND
1430
+ const seen = this.#workerErrorCounts.get(kind) ?? 0
1431
+ this.#workerErrorCounts.set(kind, seen + 1)
1432
+ if (seen < WORKER_ERROR_LOG_CAP) {
1433
+ console.warn('[FECManager] Worker error:', msg)
1434
+ } else if (seen === WORKER_ERROR_LOG_CAP) {
1435
+ console.warn(
1436
+ `[FECManager] Worker error (suppressing further copies of ${kind}): ${msg}`,
1437
+ )
1438
+ }
1439
+ })
1440
+ }
1441
+
1442
+ /**
1443
+ * Remove a stream's Worker decoder from routing.
1444
+ *
1445
+ * Returns false when no Worker is registered under that exact stream key.
1446
+ * On success every cleanup acknowledgement still owed by the client is
1447
+ * resolved by failing its block now (a Worker that is going away cannot
1448
+ * post it), the client's block ownership is released, and
1449
+ * onDecoderUnavailable fires once with reason 'cleared' and the decoder
1450
+ * that remains for the stream ('sync' or 'none'). Call this before
1451
+ * disposing the client; a client disposed while still registered is
1452
+ * evicted on its next symbol or reset with reason 'disposed'.
1453
+ */
1454
+ clearFecWorker(packetId: number, catalogTrackName?: string): boolean {
1455
+ const streamKey = this.#streamKey(packetId, catalogTrackName)
1456
+ const client = this.#workerClients.get(streamKey)
1457
+ if (!client) return false
1458
+ this.#retireWorker(streamKey, client, 'cleared')
1459
+ return true
1460
+ }
1461
+
1462
+ /**
1463
+ * Take a Worker client out of routing under one stream key, fail the blocks
1464
+ * whose completion it still owed, and report the stream's decoder
1465
+ * unavailable together with whatever still decodes the stream.
1466
+ */
1467
+ #retireWorker(streamKey: string, client: FecWorkerClient, reason: FecDecoderUnavailableReason): void {
1468
+ this.#workerClients.delete(streamKey)
1469
+ this.#releaseWorkerClient(client)
1470
+ this.#decoderUnavailableEvents++
1471
+ const stream = this.#parseStreamKey(streamKey)
1472
+ const fallbackDecoder: FecFallbackDecoder =
1473
+ this.#decoderForPacket(stream.packetId, stream.catalogTrackName) ? 'sync' : 'none'
1474
+ this.onDecoderUnavailable?.({
1475
+ packetId: stream.packetId,
1476
+ ...(stream.catalogTrackName === undefined ? {} : { catalogTrackName: stream.catalogTrackName }),
1477
+ reason,
1478
+ fallbackDecoder,
1479
+ })
1480
+ }
1481
+
1482
+ /**
1483
+ * Fail every block whose Worker cleanup acknowledgement is owed by `client`
1484
+ * and drop its block ownership, so no block waits on a message the Worker
1485
+ * will never post.
1486
+ */
1487
+ #releaseWorkerClient(client: FecWorkerClient): void {
1488
+ const owed = [...this.#pendingWorkerCleanupClients]
1489
+ .filter(([, owner]) => owner === client)
1490
+ .map(([key]) => key)
1491
+ for (const key of owed) this.#settleOwedCleanup(key, 'decoder-unavailable')
1492
+ this.#decoderBlockOwners.delete(client)
1493
+ }
1494
+
1495
+ /**
1496
+ * Resolve one owed Worker cleanup: drop the pending entry and fail the block
1497
+ * unless it already completed. Shared by the cleanup acknowledgement and by
1498
+ * Worker retirement, so the two paths cannot drift in their bookkeeping.
1499
+ */
1500
+ #settleOwedCleanup(key: string, event: 'cleanup-ack' | 'decoder-unavailable'): void {
1501
+ this.#pendingWorkerCleanups.delete(key)
1502
+ this.#pendingWorkerCleanupClients.delete(key)
1503
+ if (this.#completedBlocks.has(key)) return
1504
+ const block = this.#parseBlockKey(key)
1505
+ this.#traceLifecycle(event, block.sbn, block.packetId, block.catalogTrackName)
1506
+ this.#failBlock(block.sbn, block.packetId, false, block.catalogTrackName)
1507
+ }
1508
+
1509
+ /** Evict a disposed client under every stream key that still maps to it. */
1510
+ #evictDisposedWorker(client: FecWorkerClient): void {
1511
+ for (const [key, registered] of [...this.#workerClients]) {
1512
+ if (registered !== client) continue
1513
+ const stream = this.#parseStreamKey(key)
1514
+ const track = stream.catalogTrackName === undefined ? '' : ` track=${stream.catalogTrackName}`
1515
+ console.warn(
1516
+ `[FECManager] packetId=${stream.packetId}${track} Worker decoder was disposed while still registered; evicting it`,
1517
+ )
1518
+ this.#retireWorker(key, client, 'disposed')
1519
+ }
1520
+ }
1521
+
1522
+ /** Called by player when framerate is known (from decoder output). Refines FEC block timeout. */
1523
+ setFramerate(fps: number): void {
1524
+ if (this.#fpsDetected || fps <= 0) return
1525
+ this.#fpsDetected = true
1526
+ this.#fecFps = fps
1527
+ // Only a valid §5.1 ms interleave refines the timeout; a sub-floor value is
1528
+ // a raw frame count and would collapse it (see MIN_INTERLEAVE_DEPTH_MS).
1529
+ if (this.#fecInterleaveDepthMs >= MIN_INTERLEAVE_DEPTH_MS) {
1530
+ this.#fecTimeoutMs = computeFecDeliveryWindowMs({
1531
+ interleaveDepthMs: this.#fecInterleaveDepthMs,
1532
+ sourceSymbols: this.#fecK,
1533
+ symbolSize: this.#fecSymbolSize,
1534
+ mediaUnitsPerSecond: fps,
1535
+ })
1536
+ }
1537
+ for (const [packetId, config] of this.#trackConfigs) {
1538
+ if (config.fps !== undefined || config.interleaveDepthMs < MIN_INTERLEAVE_DEPTH_MS) continue
1539
+ this.#trackConfigs.set(packetId, {
1540
+ ...config,
1541
+ fps,
1542
+ timeoutMs: Math.max(
1543
+ this.#fecTimeoutMs,
1544
+ computeFecDeliveryWindowMs({
1545
+ interleaveDepthMs: config.interleaveDepthMs,
1546
+ sourceSymbols: config.k,
1547
+ symbolSize: config.symbolSize,
1548
+ mediaUnitsPerSecond: fps,
1549
+ }),
1550
+ ),
1551
+ })
1552
+ }
1553
+ }
1554
+
1555
+ // -----------------------------------------------------------------------
1556
+ // Symbol feeding (codec-agnostic)
1557
+ // -----------------------------------------------------------------------
1558
+
1559
+ /**
1560
+ * Feed a source symbol to the WASM FEC decoder.
1561
+ *
1562
+ * Does NOT track block source counts or fire lifecycle events — call
1563
+ * `trackBlock()` separately BEFORE this method (MmtpRouter does this).
1564
+ *
1565
+ * @returns Recovered block data if WASM completed the block, else null.
1566
+ * Caller is responsible for routing recovered data and updating
1567
+ * block completion state via `markBlockCompleted()`.
1568
+ */
1569
+ feedSourceSymbol(
1570
+ packetId: number,
1571
+ ssId: number,
1572
+ symbolData: Uint8Array,
1573
+ alta?: { auth: Uint8Array; payloadEnd: number },
1574
+ catalogTrackName?: string,
1575
+ ): Uint8Array | null {
1576
+ this.#fecStats.sourcePackets++
1577
+ this.#statsForPacket(packetId).sourcePackets++
1578
+ const fecData = this.#normalizeSymbolData(packetId, symbolData)
1579
+ const sbn = Math.floor(ssId / this.kForPacket(packetId))
1580
+ const blockKey = this.#blockKey(sbn, packetId, catalogTrackName)
1581
+ const syncBlockOwner = this.#syncDecoderBlockOwners.get(blockKey)
1582
+
1583
+ // Worker-backed track: dispatch async, return null (recovery via onFrame callback)
1584
+ const workerClient = this.#routableWorkerForPacket(packetId, catalogTrackName)
1585
+ if (workerClient && !syncBlockOwner) {
1586
+ if (!this.#claimDecoderBlock(workerClient, sbn, catalogTrackName)) return null
1587
+ // Create clean ArrayBuffer copy — prevents transferring a shared underlying buffer
1588
+ // when symbolData is a view into a larger packet (T-03-06)
1589
+ const buf = fecData.buffer.slice(
1590
+ fecData.byteOffset,
1591
+ fecData.byteOffset + fecData.byteLength,
1592
+ ) as ArrayBuffer
1593
+ // Pass the flat SS_ID through to the worker. The WASM decoder derives
1594
+ // SBN/ESI internally per ISO 23008-1 §C.5.2; the WASM add_source
1595
+ // signature is (ss_id, data, timestamp) — pre-computing sbn/esi on this
1596
+ // side produces wrong WASM argument alignment and silently breaks
1597
+ // block assembly.
1598
+ if (alta) {
1599
+ // Copy auth into a fresh ArrayBuffer to transfer zero-copy. The
1600
+ // underlying buffer of the parsed trailer view belongs to the wire
1601
+ // packet, which may be transferred separately or reused.
1602
+ const authBuf = alta.auth.buffer.slice(
1603
+ alta.auth.byteOffset,
1604
+ alta.auth.byteOffset + alta.auth.byteLength,
1605
+ ) as ArrayBuffer
1606
+ workerClient.feedSource(ssId, buf, performance.now(), {
1607
+ auth: authBuf,
1608
+ payloadEnd: alta.payloadEnd,
1609
+ })
1610
+ } else {
1611
+ workerClient.feedSource(ssId, buf, performance.now())
1612
+ }
1613
+ return null
1614
+ }
1615
+
1616
+ // Synchronous WASM path.
1617
+ const entry = this.#decoderForPacket(packetId, catalogTrackName)
1618
+ const client = syncBlockOwner ?? entry?.client
1619
+ if (!client) return null
1620
+ if (!this.#claimDecoderBlock(client, sbn, catalogTrackName)) return null
1621
+ this.#syncDecoderBlockOwners.set(blockKey, client)
1622
+
1623
+ let recovered: Uint8Array | null = null
1624
+ try {
1625
+ recovered = client.add_source(ssId, fecData, performance.now())
1626
+ } catch {
1627
+ // WASM RefCell panic — decoder instance shared with multicast repair path
1628
+ }
1629
+ if (recovered || this.#completedBlocks.has(blockKey)) {
1630
+ this.#syncDecoderBlockOwners.delete(blockKey)
1631
+ }
1632
+
1633
+ return recovered
1634
+ }
1635
+
1636
+ /**
1637
+ * Feed a repair symbol to the WASM FEC decoder.
1638
+ *
1639
+ * @returns Recovered block data if repair completed the block, else null.
1640
+ * Caller is responsible for routing recovered data and updating
1641
+ * block completion state via `markBlockCompleted()`.
1642
+ */
1643
+ feedRepairSymbol(
1644
+ packetId: number,
1645
+ ssStart: number,
1646
+ ssbLength: number,
1647
+ rsId: number,
1648
+ repairData: Uint8Array,
1649
+ catalogTrackName?: string,
1650
+ ): Uint8Array | null {
1651
+ this.#fecStats.repairPackets++
1652
+ this.#statsForPacket(packetId).repairPackets++
1653
+ const symbolSize = this.symbolSizeForPacket(packetId)
1654
+ if (symbolSize > 0 && repairData.byteLength !== symbolSize) {
1655
+ this.#repairSymbolSizeMismatchCount++
1656
+ if (this.#repairSymbolSizeMismatchCount <= 5 || this.#repairSymbolSizeMismatchCount % 100 === 0) {
1657
+ console.warn(
1658
+ `[FECManager] Dropping repair symbol with invalid size: got ${repairData.byteLength}B, expected ${symbolSize}B (packetId=${packetId}, ssStart=${ssStart}, ssbLength=${ssbLength}, rsId=${rsId})`,
1659
+ )
1660
+ }
1661
+ return null
1662
+ }
1663
+ const fecData = repairData
1664
+
1665
+ // SSB_length is the authored source-symbol count in this block, not the
1666
+ // decoder's configured block width. Short authored blocks are padded to
1667
+ // the configured K, so all internal keys and lifecycle coordinates use K.
1668
+ // Keep the wire tuple unchanged when calling the decoder below.
1669
+ const k = this.kForPacket(packetId)
1670
+ if (ssbLength <= 0 || ssbLength > k || ssStart % k !== 0) return null
1671
+ const sbn = Math.floor(ssStart / k)
1672
+
1673
+ // Repair symbols are block activity: refresh the inactivity fail bound so
1674
+ // a block being actively repaired over MoQ is not failed mid-recovery.
1675
+ // The multicast in-band path already does this at the router; this covers
1676
+ // the MoQ backfill path, which bypasses the router's packet accounting.
1677
+ this.noteBlockActivity(sbn, packetId, catalogTrackName)
1678
+ const blockKey = this.#blockKey(sbn, packetId, catalogTrackName)
1679
+ if (this.#completedBlocks.has(blockKey)) return null
1680
+ const syncBlockOwner = this.#syncDecoderBlockOwners.get(blockKey)
1681
+ let repairIds = this.#blockRepairRsIds.get(blockKey)
1682
+ if (!repairIds) {
1683
+ repairIds = new Set()
1684
+ this.#blockRepairRsIds.set(blockKey, repairIds)
1685
+ }
1686
+ repairIds.add(rsId)
1687
+ if (this.#repairRequestedBlocks.has(blockKey)) {
1688
+ this.#traceLifecycle('repair-arrived', sbn, packetId, catalogTrackName, rsId)
1689
+ }
1690
+
1691
+ // Worker-backed track: dispatch async, return null (recovery via onFrame callback)
1692
+ const workerClient = this.#routableWorkerForPacket(packetId, catalogTrackName)
1693
+ if (workerClient && !syncBlockOwner) {
1694
+ if (!this.#claimDecoderBlock(workerClient, sbn, catalogTrackName)) return null
1695
+ // Create clean ArrayBuffer copy (T-03-06)
1696
+ const buf = fecData.buffer.slice(
1697
+ fecData.byteOffset,
1698
+ fecData.byteOffset + fecData.byteLength,
1699
+ ) as ArrayBuffer
1700
+ // Forward the wire-level Repair FEC Payload ID (ISO 23008-1 §C.5.3)
1701
+ // directly. WASM add_repair signature is
1702
+ // (ss_start, ssb_length, rs_id, data, timestamp) — pre-computing
1703
+ // (sbn, esi) on this side misaligns WASM args and produces 0% recovery.
1704
+ workerClient.feedRepair(ssStart, ssbLength, rsId, buf, performance.now())
1705
+ return null
1706
+ }
1707
+
1708
+ // Synchronous WASM path.
1709
+ const entry = this.#decoderForPacket(packetId, catalogTrackName)
1710
+ const client = syncBlockOwner ?? entry?.client
1711
+ if (!client) return null
1712
+ if (!this.#claimDecoderBlock(client, sbn, catalogTrackName)) return null
1713
+ this.#syncDecoderBlockOwners.set(blockKey, client)
1714
+
1715
+ let recovered: Uint8Array | null = null
1716
+ try {
1717
+ recovered = client.add_repair(ssStart, ssbLength, rsId, fecData, performance.now())
1718
+ } catch {
1719
+ // WASM add_repair failure — block will timeout and be marked failed
1720
+ }
1721
+ if (recovered) this.#syncDecoderBlockOwners.delete(blockKey)
1722
+
1723
+ return recovered
1724
+ }
1725
+
1726
+ #routeSyncCleanup(
1727
+ decoder: WasmFecDecoder,
1728
+ sbn: number,
1729
+ packetId: number,
1730
+ ): void {
1731
+ let framed: Uint8Array
1732
+ try {
1733
+ framed = decoder.cleanup_blocks_detailed(Uint32Array.of(sbn))
1734
+ } catch (error) {
1735
+ console.warn(`[FECManager] Scoped cleanup failed for SBN ${sbn}:`, error)
1736
+ return
1737
+ }
1738
+
1739
+ const view = new DataView(framed.buffer, framed.byteOffset, framed.byteLength)
1740
+ let offset = 0
1741
+ while (offset < framed.byteLength) {
1742
+ if (framed.byteLength - offset < 8) {
1743
+ console.warn(`[FECManager] Scoped cleanup returned a truncated record for SBN ${sbn}`)
1744
+ return
1745
+ }
1746
+ const fillSbn = view.getUint32(offset, false)
1747
+ const length = view.getUint32(offset + 4, false)
1748
+ offset += 8
1749
+ if (length > framed.byteLength - offset) {
1750
+ console.warn(`[FECManager] Scoped cleanup returned a truncated payload for SBN ${sbn}`)
1751
+ return
1752
+ }
1753
+ const data = framed.slice(offset, offset + length).buffer as ArrayBuffer
1754
+ offset += length
1755
+ this.#fecStats.greenFillFrames++
1756
+ this.#statsForPacket(packetId).greenFillFrames++
1757
+ this.onGreenFill?.(fillSbn, packetId, data)
1758
+ }
1759
+ }
1760
+
1761
+ // -----------------------------------------------------------------------
1762
+ // Block lifecycle
1763
+ // -----------------------------------------------------------------------
1764
+
1765
+ /**
1766
+ * Track a source symbol for an FEC block. Refreshes the inactivity deadline
1767
+ * on each symbol and marks the block clean when K source symbols arrive.
1768
+ */
1769
+ trackBlock(
1770
+ sbn: number,
1771
+ packetId: number,
1772
+ catalogTrackName?: string,
1773
+ esi?: number,
1774
+ ): void {
1775
+ const key = this.#blockKey(sbn, packetId, catalogTrackName)
1776
+ const trackConfig = this.#resolveTrackConfig(packetId)
1777
+ const k = trackConfig.k
1778
+
1779
+ // Skip blocks already recovered — late source symbols must not restart timeouts
1780
+ if (this.#completedBlocks.has(key)) return
1781
+
1782
+ // Count source symbols per block
1783
+ const count = (this.#blockSourceCounts.get(key) ?? 0) + 1
1784
+ this.#blockSourceCounts.set(key, count)
1785
+ if (Number.isInteger(esi) && esi! >= 0 && esi! < k) {
1786
+ let sourceEsis = this.#blockSourceEsis.get(key)
1787
+ if (!sourceEsis) {
1788
+ sourceEsis = new Set()
1789
+ this.#blockSourceEsis.set(key, sourceEsis)
1790
+ }
1791
+ sourceEsis.add(esi!)
1792
+ }
1793
+ if (count === 1) {
1794
+ if (catalogTrackName === undefined) {
1795
+ this.onBlockStarted?.(sbn, packetId)
1796
+ } else {
1797
+ this.onBlockStarted?.(sbn, packetId, catalogTrackName)
1798
+ }
1799
+ }
1800
+
1801
+ // When K source symbols received, block is clean (no loss, no FEC needed).
1802
+ if (k > 0 && count >= k) {
1803
+ this.#fecStats.blocksClean++
1804
+ this.#statsForPacket(packetId).blocksClean++
1805
+ this.cancelBlockTimeout(sbn, packetId, catalogTrackName)
1806
+ this.#blockSourceCounts.delete(key)
1807
+ this.#blockSourceEsis.delete(key)
1808
+ this.#blockRepairRsIds.delete(key)
1809
+ this.#completedBlocks.add(key)
1810
+ // BLO-3323 L15: track which SBNs reached K source-symbol arrivals so the
1811
+ // worker recovery suppression can distinguish "block completed by source"
1812
+ // (legitimately suppress duplicate recovery) from "block touched by any
1813
+ // prior emit / timeout / external mark" (do NOT suppress recovery).
1814
+ this.#cleanlyCompletedBlocks.add(key)
1815
+ this.#markPathBlockComplete(sbn, packetId, catalogTrackName)
1816
+ if (catalogTrackName === undefined) {
1817
+ this.onBlockComplete?.(sbn, packetId)
1818
+ } else {
1819
+ this.onBlockComplete?.(sbn, packetId, catalogTrackName)
1820
+ }
1821
+ // MmtpRouter feeds the K-th source immediately after this callback.
1822
+ // Defer cleanup so that synchronous feed selects the same decoder; this
1823
+ // also clears blocks whose K-th packet was too large for decoder input.
1824
+ queueMicrotask(() => this.#syncDecoderBlockOwners.delete(key))
1825
+ return
1826
+ }
1827
+
1828
+ this.noteBlockActivity(sbn, packetId, catalogTrackName, true)
1829
+
1830
+ // Source ESI is intentionally not a repair trigger. Multicast delivery is
1831
+ // unordered, so K-1 may precede an interior symbol in a clean block. The
1832
+ // progress-refreshed source horizon above is the earliest loss signal.
1833
+ }
1834
+
1835
+ /** Refresh deadlines after accepted block activity. */
1836
+ noteBlockActivity(
1837
+ sbn: number,
1838
+ packetId: number,
1839
+ catalogTrackName?: string,
1840
+ sourceProgress = false,
1841
+ ): void {
1842
+ const key = this.#blockKey(sbn, packetId, catalogTrackName)
1843
+
1844
+ // Late symbols must not resurrect a block that already completed or failed.
1845
+ if (this.#completedBlocks.has(key)) return
1846
+ // The worker cleanup command is already queued after all symbols that made
1847
+ // the inactivity deadline. Symbols arriving after that boundary are late
1848
+ // and must not arm a second timer before the acknowledgement resolves it.
1849
+ if (this.#pendingWorkerCleanups.has(key)) return
1850
+
1851
+ // #requestRepair appends one bounded grace to the active deadline. Source
1852
+ // or repair symbols may still complete the block, but cannot extend it.
1853
+ if (this.#repairRequestedBlocks.has(key)) return
1854
+
1855
+ const trackConfig = this.#resolveTrackConfig(packetId)
1856
+ const timeoutMs = trackConfig.timeoutMs
1857
+ if (timeoutMs <= 0) return
1858
+
1859
+ // D-28: accepted source progress refreshes the pre-request horizon. There
1860
+ // are at most K such arrivals; repair traffic never refreshes this timer.
1861
+ const repairDeadline = this.#repairDeadlines.get(key)
1862
+ if (
1863
+ this.onRepairNeeded &&
1864
+ (repairDeadline === undefined || sourceProgress)
1865
+ ) {
1866
+ if (repairDeadline != null) clearTimeout(repairDeadline)
1867
+ this.#repairDeadlines.set(
1868
+ key,
1869
+ setTimeout(() => {
1870
+ this.#requestRepair(sbn, packetId, catalogTrackName)
1871
+ }, this.#repairDeadlineMs(trackConfig)),
1872
+ )
1873
+ }
1874
+
1875
+ this.#armBlockTimeout(sbn, packetId, catalogTrackName, timeoutMs)
1876
+ }
1877
+
1878
+ #failBlock(
1879
+ sbn: number,
1880
+ packetId: number,
1881
+ countGreenFill: boolean,
1882
+ catalogTrackName?: string,
1883
+ ): void {
1884
+ const key = this.#blockKey(sbn, packetId, catalogTrackName)
1885
+ this.#traceLifecycle('failed', sbn, packetId, catalogTrackName)
1886
+ this.#clearRepairDeadline(key)
1887
+ const activeTimer = this.#activeBlocks.get(key)
1888
+ if (activeTimer) {
1889
+ clearTimeout(activeTimer)
1890
+ this.#activeBlocks.delete(key)
1891
+ }
1892
+ this.#activeBlockDeadlinesMs.delete(key)
1893
+ if (this.#repairRequestedBlocks.delete(key)) {
1894
+ if (catalogTrackName === undefined) {
1895
+ this.onRepairResolved?.(sbn, packetId, 'failed')
1896
+ } else {
1897
+ this.onRepairResolved?.(sbn, packetId, 'failed', catalogTrackName)
1898
+ }
1899
+ }
1900
+ this.#blockSourceCounts.delete(key)
1901
+ this.#blockSourceEsis.delete(key)
1902
+ this.#blockRepairRsIds.delete(key)
1903
+ this.#activeBlockTimeoutOwners.delete(key)
1904
+ this.#syncDecoderBlockOwners.delete(key)
1905
+ this.#completedBlocks.add(key)
1906
+ this.#markPathBlockComplete(sbn, packetId, catalogTrackName)
1907
+ this.#fecStats.blocksFailed++
1908
+ this.#statsForPacket(packetId).blocksFailed++
1909
+ if (countGreenFill) {
1910
+ this.#fecStats.greenFillFrames++
1911
+ this.#statsForPacket(packetId).greenFillFrames++
1912
+ }
1913
+ if (catalogTrackName === undefined) {
1914
+ this.onBlockFailed?.(sbn, packetId)
1915
+ } else {
1916
+ this.onBlockFailed?.(sbn, packetId, catalogTrackName)
1917
+ }
1918
+ }
1919
+
1920
+ /** Check whether a block has already been completed (clean or recovered). */
1921
+ isBlockCompleted(sbn: number, packetId?: number, catalogTrackName?: string): boolean {
1922
+ return this.#blockKeysFor(sbn, packetId, catalogTrackName).some((key) => this.#completedBlocks.has(key))
1923
+ }
1924
+
1925
+ /** Mark a block as completed externally (e.g., after routing recovered data). */
1926
+ markBlockCompleted(sbn: number, packetId?: number, catalogTrackName?: string): void {
1927
+ for (const key of this.#blockKeysFor(sbn, packetId, catalogTrackName)) {
1928
+ this.#completedBlocks.add(key)
1929
+ this.#pendingWorkerCleanups.delete(key)
1930
+ this.#pendingWorkerCleanupClients.delete(key)
1931
+ const block = this.#parseBlockKey(key)
1932
+ this.#markPathBlockComplete(block.sbn, block.packetId, block.catalogTrackName)
1933
+ this.#clearRepairDeadline(key)
1934
+ if (this.#repairRequestedBlocks.delete(key)) {
1935
+ if (block.catalogTrackName === undefined) {
1936
+ this.onRepairResolved?.(block.sbn, block.packetId, 'completed')
1937
+ } else {
1938
+ this.onRepairResolved?.(
1939
+ block.sbn,
1940
+ block.packetId,
1941
+ 'completed',
1942
+ block.catalogTrackName,
1943
+ )
1944
+ }
1945
+ }
1946
+ this.#blockSourceCounts.delete(key)
1947
+ this.#blockSourceEsis.delete(key)
1948
+ this.#blockRepairRsIds.delete(key)
1949
+ const timer = this.#activeBlocks.get(key)
1950
+ if (timer) {
1951
+ clearTimeout(timer)
1952
+ this.#activeBlocks.delete(key)
1953
+ }
1954
+ this.#activeBlockDeadlinesMs.delete(key)
1955
+ this.#activeBlockTimeoutOwners.delete(key)
1956
+ }
1957
+ }
1958
+
1959
+ /** Increment blocksRecovered stat (called by MmtpRouter after routing recovery). */
1960
+ countRecovery(packetId?: number): void {
1961
+ this.#fecStats.blocksRecovered++
1962
+ if (packetId != null) this.#statsForPacket(packetId).blocksRecovered++
1963
+ }
1964
+
1965
+ /**
1966
+ * Strict delivery provenance for one FEC block.
1967
+ *
1968
+ * A recovered symbol is a function of every symbol that entered its block,
1969
+ * so it can only be attributed to multicast when NO symbol in that block
1970
+ * arrived over MoQ. Repair symbols routinely arrive over reliable QUIC
1971
+ * (`MmtpRouter.feedRepairPacket`), so a block that recovered with MoQ help
1972
+ * produced bytes that never crossed a multicast socket.
1973
+ *
1974
+ * An untracked block returns 'unknown' rather than a guess: absence of
1975
+ * evidence is not evidence of multicast.
1976
+ */
1977
+ blockDeliveryProvenance(
1978
+ sbn: number,
1979
+ packetId: number,
1980
+ catalogTrackName?: string,
1981
+ ): MmtpDeliveryProvenance {
1982
+ const counts = this.blockPathCount(sbn, packetId, catalogTrackName)
1983
+ if (!counts) return 'unknown'
1984
+ if (counts.moqSymbols > 0) return 'mixed'
1985
+ if (counts.multicastSymbols > 0) return 'multicast'
1986
+ return 'unknown'
1987
+ }
1988
+
1989
+ /** Per-path symbol counts for one block (D-28 gate logging), if tracked. */
1990
+ blockPathCount(
1991
+ sbn: number,
1992
+ packetId: number,
1993
+ catalogTrackName?: string,
1994
+ ): FecBlockPathCount | null {
1995
+ return this.#blockPathCounts.get(
1996
+ this.#blockKey(sbn, packetId, catalogTrackName),
1997
+ ) ?? null
1998
+ }
1999
+
2000
+ /** Cancel block timeout (block completed via clean reception or FEC recovery). */
2001
+ cancelBlockTimeout(sbn: number, packetId?: number, catalogTrackName?: string): void {
2002
+ for (const key of this.#blockKeysFor(sbn, packetId, catalogTrackName)) {
2003
+ this.#completedBlocks.add(key)
2004
+ this.#pendingWorkerCleanups.delete(key)
2005
+ this.#pendingWorkerCleanupClients.delete(key)
2006
+ const block = this.#parseBlockKey(key)
2007
+ this.#markPathBlockComplete(block.sbn, block.packetId, block.catalogTrackName)
2008
+ this.#clearRepairDeadline(key)
2009
+ if (this.#repairRequestedBlocks.delete(key)) {
2010
+ if (block.catalogTrackName === undefined) {
2011
+ this.onRepairResolved?.(block.sbn, block.packetId, 'completed')
2012
+ } else {
2013
+ this.onRepairResolved?.(
2014
+ block.sbn,
2015
+ block.packetId,
2016
+ 'completed',
2017
+ block.catalogTrackName,
2018
+ )
2019
+ }
2020
+ }
2021
+ this.#blockSourceCounts.delete(key)
2022
+ this.#blockSourceEsis.delete(key)
2023
+ this.#blockRepairRsIds.delete(key)
2024
+ const timer = this.#activeBlocks.get(key)
2025
+ if (timer) {
2026
+ clearTimeout(timer)
2027
+ this.#activeBlocks.delete(key)
2028
+ }
2029
+ this.#activeBlockDeadlinesMs.delete(key)
2030
+ this.#activeBlockTimeoutOwners.delete(key)
2031
+ }
2032
+ // Trim old entries to prevent unbounded growth (keep last ~500 blocks)
2033
+ if (this.#completedBlocks.size > 1000) {
2034
+ const iter = this.#completedBlocks.values()
2035
+ for (let i = 0; i < 500; i++) iter.next()
2036
+ // Sets iterate in insertion order — first 500 are oldest
2037
+ const keep = new Set<string>()
2038
+ for (const v of iter) keep.add(v)
2039
+ this.#completedBlocks = keep
2040
+ }
2041
+ }
2042
+
2043
+ // -----------------------------------------------------------------------
2044
+ // Cleanup
2045
+ // -----------------------------------------------------------------------
2046
+
2047
+ dispose(): void {
2048
+ for (const timer of this.#activeBlocks.values()) clearTimeout(timer)
2049
+ this.#activeBlocks.clear()
2050
+ this.#activeBlockDeadlinesMs.clear()
2051
+ this.#activeBlockTimeoutOwners.clear()
2052
+ this.#pendingWorkerCleanups.clear()
2053
+ this.#pendingWorkerCleanupClients.clear()
2054
+ for (const timer of this.#repairDeadlines.values()) {
2055
+ if (timer != null) clearTimeout(timer)
2056
+ }
2057
+ this.#repairDeadlines.clear()
2058
+ this.#blockSourceCounts.clear()
2059
+ this.#blockSourceEsis.clear()
2060
+ this.#blockRepairRsIds.clear()
2061
+ this.#completedBlocks.clear()
2062
+ this.#cleanlyCompletedBlocks.clear()
2063
+ this.#repairRequestedBlocks.clear()
2064
+ this.#workerRecoveredBlocks.clear()
2065
+ this.#syncDecoderBlockOwners.clear()
2066
+ this.#startupAlignedPackets.clear()
2067
+ this.#startupPartialBlocks.clear()
2068
+ this.#startupRequireRepairReady = false
2069
+ this.#startupRepairReadyPackets.clear()
2070
+ this.#startupRepairBoundaryCounts.clear()
2071
+ this.#startupRepairFailOpenPackets.clear()
2072
+ this.#seenSymbols.clear()
2073
+ this.#blockPathCounts.clear()
2074
+ this.#blockCatalogTrackNames.clear()
2075
+ this.#trackConfigs.clear()
2076
+ this.#fecDecoders.clear()
2077
+ // Dispose Worker clients (T-03-07: prevent Worker leak)
2078
+ for (const client of new Set(this.#workerClients.values())) client.dispose()
2079
+ this.#workerClients.clear()
2080
+ }
2081
+ }