@blockcast/mmt-container 0.1.0-main.149f78a4adae

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