@blockcast/mmt-container 0.1.0 → 0.3.0-main.0bde85f7082f

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (799) hide show
  1. package/README.md +524 -0
  2. package/dist/.build-stamp +0 -0
  3. package/dist/__tests__/test-utils.d.ts +71 -0
  4. package/dist/__tests__/test-utils.d.ts.map +1 -0
  5. package/dist/align.d.ts +152 -0
  6. package/dist/align.d.ts.map +1 -0
  7. package/dist/align.js +88 -0
  8. package/dist/align.js.map +7 -0
  9. package/dist/audio-init-parser.d.ts +66 -0
  10. package/dist/audio-init-parser.d.ts.map +1 -0
  11. package/dist/audio-init-parser.js +118 -0
  12. package/dist/audio-init-parser.js.map +7 -0
  13. package/dist/audio-ring-buffer.d.ts +50 -0
  14. package/dist/audio-ring-buffer.d.ts.map +1 -0
  15. package/dist/audio-ring-buffer.js +80 -0
  16. package/dist/audio-ring-buffer.js.map +7 -0
  17. package/dist/authored-timing.d.ts +32 -0
  18. package/dist/authored-timing.d.ts.map +1 -0
  19. package/dist/authored-timing.js +32 -0
  20. package/dist/authored-timing.js.map +7 -0
  21. package/dist/block-policy.d.ts +314 -0
  22. package/dist/block-policy.d.ts.map +1 -0
  23. package/dist/block-policy.js +466 -0
  24. package/dist/block-policy.js.map +7 -0
  25. package/dist/browser-runtime.js +26549 -0
  26. package/dist/browser-runtime.js.map +7 -0
  27. package/dist/carp-catalog.d.ts +429 -0
  28. package/dist/carp-catalog.d.ts.map +1 -0
  29. package/dist/carp-catalog.js +985 -0
  30. package/dist/carp-catalog.js.map +7 -0
  31. package/dist/catalog-fec-params.d.ts +275 -0
  32. package/dist/catalog-fec-params.d.ts.map +1 -0
  33. package/dist/catalog-fec-params.js +702 -0
  34. package/dist/catalog-fec-params.js.map +7 -0
  35. package/dist/catalog-network-source.d.ts +42 -0
  36. package/dist/catalog-network-source.d.ts.map +1 -0
  37. package/dist/catalog-network-source.js +47 -0
  38. package/dist/catalog-network-source.js.map +7 -0
  39. package/dist/catalog-validator.d.ts +18 -0
  40. package/dist/catalog-validator.d.ts.map +1 -0
  41. package/dist/catalog-validator.js +7747 -0
  42. package/dist/catalog-validator.js.map +7 -0
  43. package/dist/cmaf-block-assembler.d.ts +285 -0
  44. package/dist/cmaf-block-assembler.d.ts.map +1 -0
  45. package/dist/cmaf-block-assembler.js +1966 -0
  46. package/dist/cmaf-block-assembler.js.map +7 -0
  47. package/dist/cmaf-group-batcher.d.ts +38 -0
  48. package/dist/cmaf-group-batcher.d.ts.map +1 -0
  49. package/dist/cmaf-group-batcher.js +176 -0
  50. package/dist/cmaf-group-batcher.js.map +7 -0
  51. package/dist/cmcd-producer.d.ts +104 -0
  52. package/dist/cmcd-producer.d.ts.map +1 -0
  53. package/dist/cmcd-producer.js +183 -0
  54. package/dist/cmcd-producer.js.map +7 -0
  55. package/dist/codec-utils.d.ts +384 -0
  56. package/dist/codec-utils.d.ts.map +1 -0
  57. package/dist/codec-utils.js +1546 -0
  58. package/dist/codec-utils.js.map +7 -0
  59. package/dist/conceal-timeline.d.ts +146 -0
  60. package/dist/conceal-timeline.d.ts.map +1 -0
  61. package/dist/conceal-timeline.js +137 -0
  62. package/dist/conceal-timeline.js.map +7 -0
  63. package/dist/delivery-gap.d.ts +75 -0
  64. package/dist/delivery-gap.d.ts.map +1 -0
  65. package/dist/delivery-gap.js +88 -0
  66. package/dist/delivery-gap.js.map +7 -0
  67. package/dist/duplicate-ledger.d.ts +197 -0
  68. package/dist/duplicate-ledger.d.ts.map +1 -0
  69. package/dist/duplicate-ledger.js +226 -0
  70. package/dist/duplicate-ledger.js.map +7 -0
  71. package/dist/fec-codec.d.ts +32 -0
  72. package/dist/fec-codec.d.ts.map +1 -0
  73. package/dist/fec-codec.js +47 -0
  74. package/dist/fec-codec.js.map +7 -0
  75. package/dist/fec-manager.d.ts +583 -0
  76. package/dist/fec-manager.d.ts.map +1 -0
  77. package/dist/fec-manager.js +3899 -0
  78. package/dist/fec-manager.js.map +7 -0
  79. package/dist/fec-starvation-detector.d.ts +59 -0
  80. package/dist/fec-starvation-detector.d.ts.map +1 -0
  81. package/dist/fec-starvation-detector.js +60 -0
  82. package/dist/fec-starvation-detector.js.map +7 -0
  83. package/dist/fec-state.d.ts +131 -0
  84. package/dist/fec-state.d.ts.map +1 -0
  85. package/dist/fec-state.js +87 -0
  86. package/dist/fec-state.js.map +7 -0
  87. package/dist/fec-worker-setup.d.ts +94 -0
  88. package/dist/fec-worker-setup.d.ts.map +1 -0
  89. package/dist/fec-worker-setup.js +673 -0
  90. package/dist/fec-worker-setup.js.map +7 -0
  91. package/dist/fmp4-completion.d.ts +30 -0
  92. package/dist/fmp4-completion.d.ts.map +1 -0
  93. package/dist/fmp4-completion.js +21 -0
  94. package/dist/fmp4-completion.js.map +7 -0
  95. package/dist/fmp4-reader.d.ts +37 -0
  96. package/dist/fmp4-reader.d.ts.map +1 -0
  97. package/dist/fmp4-reader.js +100 -0
  98. package/dist/fmp4-reader.js.map +7 -0
  99. package/dist/index.d.ts +44 -3
  100. package/dist/index.d.ts.map +1 -1
  101. package/dist/index.js +460 -14
  102. package/dist/index.js.map +7 -1
  103. package/dist/iso-block-locator.d.ts +118 -0
  104. package/dist/iso-block-locator.d.ts.map +1 -0
  105. package/dist/iso-block-locator.js +289 -0
  106. package/dist/iso-block-locator.js.map +7 -0
  107. package/dist/iso-bmff-timing.d.ts +78 -0
  108. package/dist/iso-bmff-timing.d.ts.map +1 -0
  109. package/dist/iso-bmff-timing.js +593 -0
  110. package/dist/iso-bmff-timing.js.map +7 -0
  111. package/dist/loc.d.ts +70 -75
  112. package/dist/loc.d.ts.map +1 -1
  113. package/dist/loc.js +106 -192
  114. package/dist/loc.js.map +7 -1
  115. package/dist/manifest-auth-plan.d.ts +30 -0
  116. package/dist/manifest-auth-plan.d.ts.map +1 -0
  117. package/dist/manifest-auth-plan.js +1266 -0
  118. package/dist/manifest-auth-plan.js.map +7 -0
  119. package/dist/media-router.d.ts +262 -0
  120. package/dist/media-router.d.ts.map +1 -0
  121. package/dist/media-router.js +584 -0
  122. package/dist/media-router.js.map +7 -0
  123. package/dist/mfu-reassembler.d.ts +313 -9
  124. package/dist/mfu-reassembler.d.ts.map +1 -1
  125. package/dist/mfu-reassembler.js +3051 -285
  126. package/dist/mfu-reassembler.js.map +7 -1
  127. package/dist/mfu-reassembly-policy.d.ts +24 -0
  128. package/dist/mfu-reassembly-policy.d.ts.map +1 -0
  129. package/dist/mfu-reassembly-policy.js +99 -0
  130. package/dist/mfu-reassembly-policy.js.map +7 -0
  131. package/dist/mmtp-container-parser.d.ts +568 -0
  132. package/dist/mmtp-container-parser.d.ts.map +1 -0
  133. package/dist/mmtp-container-parser.js +6580 -0
  134. package/dist/mmtp-container-parser.js.map +7 -0
  135. package/dist/mmtp-container-parser.no-modules.js +9616 -0
  136. package/dist/mmtp-container-parser.no-modules.js.map +7 -0
  137. package/dist/mmtp-decode-pipeline.d.ts +138 -0
  138. package/dist/mmtp-decode-pipeline.d.ts.map +1 -0
  139. package/dist/mmtp-decode-pipeline.js +6814 -0
  140. package/dist/mmtp-decode-pipeline.js.map +7 -0
  141. package/dist/mmtp-router.d.ts +679 -0
  142. package/dist/mmtp-router.d.ts.map +1 -0
  143. package/dist/mmtp-router.js +11278 -0
  144. package/dist/mmtp-router.js.map +7 -0
  145. package/dist/mmtp-to-cmaf.d.ts +187 -0
  146. package/dist/mmtp-to-cmaf.d.ts.map +1 -0
  147. package/dist/mmtp-to-cmaf.js +716 -0
  148. package/dist/mmtp-to-cmaf.js.map +7 -0
  149. package/dist/multicast-auth-catalog.d.ts +115 -0
  150. package/dist/multicast-auth-catalog.d.ts.map +1 -0
  151. package/dist/multicast-auth-catalog.js +202 -0
  152. package/dist/multicast-auth-catalog.js.map +7 -0
  153. package/dist/multicast-input.d.ts +141 -0
  154. package/dist/multicast-input.d.ts.map +1 -0
  155. package/dist/multicast-input.js +390 -0
  156. package/dist/multicast-input.js.map +7 -0
  157. package/dist/multicast-manager.d.ts +669 -0
  158. package/dist/multicast-manager.d.ts.map +1 -0
  159. package/dist/multicast-manager.js +19101 -0
  160. package/dist/multicast-manager.js.map +7 -0
  161. package/dist/multicast-transport.d.ts +271 -0
  162. package/dist/multicast-transport.d.ts.map +1 -0
  163. package/dist/multicast-transport.js +2264 -0
  164. package/dist/multicast-transport.js.map +7 -0
  165. package/dist/multicast-unwrap.d.ts +57 -0
  166. package/dist/multicast-unwrap.d.ts.map +1 -0
  167. package/dist/multicast-unwrap.js +418 -0
  168. package/dist/multicast-unwrap.js.map +7 -0
  169. package/dist/no-modules.d.ts +10 -0
  170. package/dist/no-modules.d.ts.map +1 -0
  171. package/dist/no-modules.js +9491 -0
  172. package/dist/no-modules.js.map +7 -0
  173. package/dist/phase14-catalog-override.d.ts +126 -0
  174. package/dist/phase14-catalog-override.d.ts.map +1 -0
  175. package/dist/phase14-catalog-override.js +155 -0
  176. package/dist/phase14-catalog-override.js.map +7 -0
  177. package/dist/repair-fetch.d.ts +270 -0
  178. package/dist/repair-fetch.d.ts.map +1 -0
  179. package/dist/repair-fetch.js +381 -0
  180. package/dist/repair-fetch.js.map +7 -0
  181. package/dist/repair-layers.d.ts +158 -0
  182. package/dist/repair-layers.d.ts.map +1 -0
  183. package/dist/repair-layers.js +344 -0
  184. package/dist/repair-layers.js.map +7 -0
  185. package/dist/sample-combiner.d.ts +212 -0
  186. package/dist/sample-combiner.d.ts.map +1 -0
  187. package/dist/sample-combiner.js +595 -0
  188. package/dist/sample-combiner.js.map +7 -0
  189. package/dist/signaling.d.ts +29 -0
  190. package/dist/signaling.d.ts.map +1 -0
  191. package/dist/signaling.js +572 -0
  192. package/dist/signaling.js.map +7 -0
  193. package/dist/source-auth.d.ts +2 -0
  194. package/dist/source-auth.d.ts.map +1 -0
  195. package/dist/source-auth.js +1209 -0
  196. package/dist/source-auth.js.map +7 -0
  197. package/dist/switching-set-alignment.d.ts +61 -0
  198. package/dist/switching-set-alignment.d.ts.map +1 -0
  199. package/dist/switching-set-alignment.js +70 -0
  200. package/dist/switching-set-alignment.js.map +7 -0
  201. package/dist/timestamp-codec.d.ts +34 -0
  202. package/dist/timestamp-codec.d.ts.map +1 -0
  203. package/dist/timestamp-codec.js +473 -0
  204. package/dist/timestamp-codec.js.map +7 -0
  205. package/dist/track-pipeline-setup.d.ts +83 -0
  206. package/dist/track-pipeline-setup.d.ts.map +1 -0
  207. package/dist/track-pipeline-setup.js +291 -0
  208. package/dist/track-pipeline-setup.js.map +7 -0
  209. package/dist/track-pipeline.d.ts +121 -0
  210. package/dist/track-pipeline.d.ts.map +1 -0
  211. package/dist/track-pipeline.js +209 -0
  212. package/dist/track-pipeline.js.map +7 -0
  213. package/dist/track-session.d.ts +414 -0
  214. package/dist/track-session.d.ts.map +1 -0
  215. package/dist/track-session.js +754 -0
  216. package/dist/track-session.js.map +7 -0
  217. package/dist/transport-pool.d.ts +69 -0
  218. package/dist/transport-pool.d.ts.map +1 -0
  219. package/dist/transport-pool.js +74 -0
  220. package/dist/transport-pool.js.map +7 -0
  221. package/dist/wasm-mfu-reassembler.d.ts +97 -0
  222. package/dist/wasm-mfu-reassembler.d.ts.map +1 -0
  223. package/dist/wasm-mfu-reassembler.js +2133 -0
  224. package/dist/wasm-mfu-reassembler.js.map +7 -0
  225. package/dist/wasm-mmtp-container-parser.d.ts +10 -0
  226. package/dist/wasm-mmtp-container-parser.d.ts.map +1 -0
  227. package/dist/wasm-mmtp-container-parser.js +6834 -0
  228. package/dist/wasm-mmtp-container-parser.js.map +7 -0
  229. package/node_modules/@blockcast/fec-worker/README.md +223 -0
  230. package/node_modules/@blockcast/fec-worker/dist/.build-stamp +0 -0
  231. package/node_modules/@blockcast/fec-worker/dist/block-order.d.ts +38 -0
  232. package/node_modules/@blockcast/fec-worker/dist/block-order.d.ts.map +1 -0
  233. package/node_modules/@blockcast/fec-worker/dist/block-order.js +78 -0
  234. package/node_modules/@blockcast/fec-worker/dist/block-order.js.map +1 -0
  235. package/node_modules/@blockcast/fec-worker/dist/fec-worker-client.d.ts +93 -0
  236. package/node_modules/@blockcast/fec-worker/dist/fec-worker-client.d.ts.map +1 -0
  237. package/node_modules/@blockcast/fec-worker/dist/fec-worker-client.js +346 -0
  238. package/node_modules/@blockcast/fec-worker/dist/fec-worker-client.js.map +1 -0
  239. package/node_modules/@blockcast/fec-worker/dist/fec-worker-types.d.ts +380 -0
  240. package/node_modules/@blockcast/fec-worker/dist/fec-worker-types.d.ts.map +1 -0
  241. package/node_modules/@blockcast/fec-worker/dist/fec-worker-types.js +11 -0
  242. package/node_modules/@blockcast/fec-worker/dist/fec-worker-types.js.map +1 -0
  243. package/node_modules/@blockcast/fec-worker/dist/fec-worker.d.ts +14 -0
  244. package/node_modules/@blockcast/fec-worker/dist/fec-worker.d.ts.map +1 -0
  245. package/node_modules/@blockcast/fec-worker/dist/fec-worker.js +859 -0
  246. package/node_modules/@blockcast/fec-worker/dist/fec-worker.js.map +7 -0
  247. package/node_modules/@blockcast/fec-worker/dist/index.d.ts +10 -0
  248. package/node_modules/@blockcast/fec-worker/dist/index.d.ts.map +1 -0
  249. package/node_modules/@blockcast/fec-worker/dist/index.js +8 -0
  250. package/node_modules/@blockcast/fec-worker/dist/index.js.map +1 -0
  251. package/node_modules/@blockcast/fec-worker/dist/shred-fec-worker.d.ts +20 -0
  252. package/node_modules/@blockcast/fec-worker/dist/shred-fec-worker.d.ts.map +1 -0
  253. package/node_modules/@blockcast/fec-worker/dist/shred-fec-worker.js +370 -0
  254. package/node_modules/@blockcast/fec-worker/dist/shred-fec-worker.js.map +7 -0
  255. package/node_modules/@blockcast/fec-worker/dist/shred-worker-client.d.ts +104 -0
  256. package/node_modules/@blockcast/fec-worker/dist/shred-worker-client.d.ts.map +1 -0
  257. package/node_modules/@blockcast/fec-worker/dist/shred-worker-client.js +181 -0
  258. package/node_modules/@blockcast/fec-worker/dist/shred-worker-client.js.map +1 -0
  259. package/node_modules/@blockcast/fec-worker/dist/shred-worker-types.d.ts +194 -0
  260. package/node_modules/@blockcast/fec-worker/dist/shred-worker-types.d.ts.map +1 -0
  261. package/node_modules/@blockcast/fec-worker/dist/shred-worker-types.js +62 -0
  262. package/node_modules/@blockcast/fec-worker/dist/shred-worker-types.js.map +1 -0
  263. package/node_modules/@blockcast/fec-worker/dist/transfer.d.ts +9 -0
  264. package/node_modules/@blockcast/fec-worker/dist/transfer.d.ts.map +1 -0
  265. package/node_modules/@blockcast/fec-worker/dist/transfer.js +13 -0
  266. package/node_modules/@blockcast/fec-worker/dist/transfer.js.map +1 -0
  267. package/node_modules/@blockcast/fec-worker/package.json +59 -0
  268. package/node_modules/@blockcast/fec-worker/src/block-order.ts +93 -0
  269. package/node_modules/@blockcast/fec-worker/src/fec-worker-client.ts +392 -0
  270. package/node_modules/@blockcast/fec-worker/src/fec-worker-types.ts +399 -0
  271. package/node_modules/@blockcast/fec-worker/src/fec-worker.ts +1502 -0
  272. package/node_modules/@blockcast/fec-worker/src/index.ts +35 -0
  273. package/node_modules/@blockcast/fec-worker/src/shred-fec-worker.ts +589 -0
  274. package/node_modules/@blockcast/fec-worker/src/shred-worker-client.ts +222 -0
  275. package/node_modules/@blockcast/fec-worker/src/shred-worker-types.ts +209 -0
  276. package/node_modules/@blockcast/fec-worker/src/transfer.ts +12 -0
  277. package/node_modules/@blockcast/mmt-alta-parse/README.md +181 -0
  278. package/node_modules/@blockcast/mmt-alta-parse/dist/.build-stamp +0 -0
  279. package/node_modules/@blockcast/mmt-alta-parse/dist/index.d.ts +57 -0
  280. package/node_modules/@blockcast/mmt-alta-parse/dist/index.d.ts.map +1 -0
  281. package/node_modules/@blockcast/mmt-alta-parse/dist/index.js +115 -0
  282. package/node_modules/@blockcast/mmt-alta-parse/dist/index.js.map +1 -0
  283. package/node_modules/@blockcast/mmt-alta-parse/package.json +50 -0
  284. package/node_modules/@blockcast/mmt-alta-parse/src/index.ts +136 -0
  285. package/node_modules/@blockcast/mmt-base/README.md +134 -0
  286. package/node_modules/@blockcast/mmt-base/dist/.build-stamp +0 -0
  287. package/node_modules/@blockcast/mmt-base/dist/fec-block-profile.d.ts +55 -0
  288. package/node_modules/@blockcast/mmt-base/dist/fec-block-profile.d.ts.map +1 -0
  289. package/node_modules/@blockcast/mmt-base/dist/fec-block-profile.js +128 -0
  290. package/node_modules/@blockcast/mmt-base/dist/fec-block-profile.js.map +1 -0
  291. package/node_modules/@blockcast/mmt-base/dist/index.d.ts +7 -0
  292. package/node_modules/@blockcast/mmt-base/dist/index.d.ts.map +1 -0
  293. package/node_modules/@blockcast/mmt-base/dist/index.js +7 -0
  294. package/node_modules/@blockcast/mmt-base/dist/index.js.map +1 -0
  295. package/node_modules/@blockcast/mmt-base/dist/mmtp.d.ts +292 -0
  296. package/node_modules/@blockcast/mmt-base/dist/mmtp.d.ts.map +1 -0
  297. package/node_modules/@blockcast/mmt-base/dist/mmtp.js +612 -0
  298. package/node_modules/@blockcast/mmt-base/dist/mmtp.js.map +1 -0
  299. package/node_modules/@blockcast/mmt-base/dist/net-constants.d.ts +18 -0
  300. package/node_modules/@blockcast/mmt-base/dist/net-constants.d.ts.map +1 -0
  301. package/node_modules/@blockcast/mmt-base/dist/net-constants.js +18 -0
  302. package/node_modules/@blockcast/mmt-base/dist/net-constants.js.map +1 -0
  303. package/node_modules/@blockcast/mmt-base/dist/ntp-short.d.ts +47 -0
  304. package/node_modules/@blockcast/mmt-base/dist/ntp-short.d.ts.map +1 -0
  305. package/node_modules/@blockcast/mmt-base/dist/ntp-short.js +54 -0
  306. package/node_modules/@blockcast/mmt-base/dist/ntp-short.js.map +1 -0
  307. package/node_modules/@blockcast/mmt-base/dist/serial-number.d.ts +57 -0
  308. package/node_modules/@blockcast/mmt-base/dist/serial-number.d.ts.map +1 -0
  309. package/node_modules/@blockcast/mmt-base/dist/serial-number.js +98 -0
  310. package/node_modules/@blockcast/mmt-base/dist/serial-number.js.map +1 -0
  311. package/node_modules/@blockcast/mmt-base/dist/signalling.d.ts +312 -0
  312. package/node_modules/@blockcast/mmt-base/dist/signalling.d.ts.map +1 -0
  313. package/node_modules/@blockcast/mmt-base/dist/signalling.js +637 -0
  314. package/node_modules/@blockcast/mmt-base/dist/signalling.js.map +1 -0
  315. package/node_modules/@blockcast/mmt-base/package.json +49 -0
  316. package/node_modules/@blockcast/mmt-base/src/fec-block-profile.ts +145 -0
  317. package/node_modules/@blockcast/mmt-base/src/index.ts +6 -0
  318. package/node_modules/@blockcast/mmt-base/src/mmtp.ts +892 -0
  319. package/node_modules/@blockcast/mmt-base/src/net-constants.ts +17 -0
  320. package/node_modules/@blockcast/mmt-base/src/ntp-short.ts +67 -0
  321. package/node_modules/@blockcast/mmt-base/src/serial-number.ts +107 -0
  322. package/node_modules/@blockcast/mmt-base/src/signalling.ts +862 -0
  323. package/node_modules/@blockcast/mmt-fec/README.md +271 -0
  324. package/node_modules/@blockcast/mmt-fec/dist/.build-stamp +0 -0
  325. package/node_modules/@blockcast/mmt-fec/dist/block-manager.d.ts +82 -0
  326. package/node_modules/@blockcast/mmt-fec/dist/block-manager.d.ts.map +1 -0
  327. package/node_modules/@blockcast/mmt-fec/dist/block-manager.js +296 -0
  328. package/node_modules/@blockcast/mmt-fec/dist/block-manager.js.map +1 -0
  329. package/node_modules/@blockcast/mmt-fec/dist/block-processor.d.ts +106 -0
  330. package/node_modules/@blockcast/mmt-fec/dist/block-processor.d.ts.map +1 -0
  331. package/node_modules/@blockcast/mmt-fec/dist/block-processor.js +288 -0
  332. package/node_modules/@blockcast/mmt-fec/dist/block-processor.js.map +1 -0
  333. package/node_modules/@blockcast/mmt-fec/dist/cmaf-recovery.d.ts +46 -0
  334. package/node_modules/@blockcast/mmt-fec/dist/cmaf-recovery.d.ts.map +1 -0
  335. package/node_modules/@blockcast/mmt-fec/dist/cmaf-recovery.js +96 -0
  336. package/node_modules/@blockcast/mmt-fec/dist/cmaf-recovery.js.map +1 -0
  337. package/node_modules/@blockcast/mmt-fec/dist/cmaf-symbols.d.ts +33 -0
  338. package/node_modules/@blockcast/mmt-fec/dist/cmaf-symbols.d.ts.map +1 -0
  339. package/node_modules/@blockcast/mmt-fec/dist/cmaf-symbols.js +57 -0
  340. package/node_modules/@blockcast/mmt-fec/dist/cmaf-symbols.js.map +1 -0
  341. package/node_modules/@blockcast/mmt-fec/dist/fec-block-mapper.d.ts +205 -0
  342. package/node_modules/@blockcast/mmt-fec/dist/fec-block-mapper.d.ts.map +1 -0
  343. package/node_modules/@blockcast/mmt-fec/dist/fec-block-mapper.js +240 -0
  344. package/node_modules/@blockcast/mmt-fec/dist/fec-block-mapper.js.map +1 -0
  345. package/node_modules/@blockcast/mmt-fec/dist/index.d.ts +28 -0
  346. package/node_modules/@blockcast/mmt-fec/dist/index.d.ts.map +1 -0
  347. package/node_modules/@blockcast/mmt-fec/dist/index.js +28 -0
  348. package/node_modules/@blockcast/mmt-fec/dist/index.js.map +1 -0
  349. package/node_modules/@blockcast/mmt-fec/dist/loc-symbols.d.ts +20 -0
  350. package/node_modules/@blockcast/mmt-fec/dist/loc-symbols.d.ts.map +1 -0
  351. package/node_modules/@blockcast/mmt-fec/dist/loc-symbols.js +28 -0
  352. package/node_modules/@blockcast/mmt-fec/dist/loc-symbols.js.map +1 -0
  353. package/node_modules/@blockcast/mmt-fec/dist/oti.d.ts +258 -0
  354. package/node_modules/@blockcast/mmt-fec/dist/oti.d.ts.map +1 -0
  355. package/node_modules/@blockcast/mmt-fec/dist/oti.js +382 -0
  356. package/node_modules/@blockcast/mmt-fec/dist/oti.js.map +1 -0
  357. package/node_modules/@blockcast/mmt-fec/dist/reorder-buffer.d.ts +41 -0
  358. package/node_modules/@blockcast/mmt-fec/dist/reorder-buffer.d.ts.map +1 -0
  359. package/node_modules/@blockcast/mmt-fec/dist/reorder-buffer.js +81 -0
  360. package/node_modules/@blockcast/mmt-fec/dist/reorder-buffer.js.map +1 -0
  361. package/node_modules/@blockcast/mmt-fec/dist/repair-group.d.ts +82 -0
  362. package/node_modules/@blockcast/mmt-fec/dist/repair-group.d.ts.map +1 -0
  363. package/node_modules/@blockcast/mmt-fec/dist/repair-group.js +170 -0
  364. package/node_modules/@blockcast/mmt-fec/dist/repair-group.js.map +1 -0
  365. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-alta-parse/README.md +181 -0
  366. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-alta-parse/dist/.build-stamp +0 -0
  367. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-alta-parse/dist/index.d.ts +57 -0
  368. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-alta-parse/dist/index.d.ts.map +1 -0
  369. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-alta-parse/dist/index.js +115 -0
  370. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-alta-parse/dist/index.js.map +1 -0
  371. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-alta-parse/package.json +50 -0
  372. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-alta-parse/src/index.ts +136 -0
  373. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/README.md +134 -0
  374. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/.build-stamp +0 -0
  375. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/fec-block-profile.d.ts +55 -0
  376. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/fec-block-profile.d.ts.map +1 -0
  377. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/fec-block-profile.js +128 -0
  378. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/fec-block-profile.js.map +1 -0
  379. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/index.d.ts +7 -0
  380. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/index.d.ts.map +1 -0
  381. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/index.js +7 -0
  382. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/index.js.map +1 -0
  383. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/mmtp.d.ts +292 -0
  384. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/mmtp.d.ts.map +1 -0
  385. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/mmtp.js +612 -0
  386. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/mmtp.js.map +1 -0
  387. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/net-constants.d.ts +18 -0
  388. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/net-constants.d.ts.map +1 -0
  389. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/net-constants.js +18 -0
  390. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/net-constants.js.map +1 -0
  391. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/ntp-short.d.ts +47 -0
  392. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/ntp-short.d.ts.map +1 -0
  393. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/ntp-short.js +54 -0
  394. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/ntp-short.js.map +1 -0
  395. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/serial-number.d.ts +57 -0
  396. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/serial-number.d.ts.map +1 -0
  397. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/serial-number.js +98 -0
  398. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/serial-number.js.map +1 -0
  399. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/signalling.d.ts +312 -0
  400. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/signalling.d.ts.map +1 -0
  401. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/signalling.js +637 -0
  402. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/signalling.js.map +1 -0
  403. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/package.json +49 -0
  404. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/src/fec-block-profile.ts +145 -0
  405. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/src/index.ts +6 -0
  406. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/src/mmtp.ts +892 -0
  407. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/src/net-constants.ts +17 -0
  408. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/src/ntp-short.ts +67 -0
  409. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/src/serial-number.ts +107 -0
  410. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/src/signalling.ts +862 -0
  411. package/node_modules/@blockcast/mmt-fec/package.json +79 -0
  412. package/node_modules/@blockcast/mmt-fec/src/block-manager.ts +371 -0
  413. package/node_modules/@blockcast/mmt-fec/src/block-processor.ts +439 -0
  414. package/node_modules/@blockcast/mmt-fec/src/cmaf-recovery.ts +127 -0
  415. package/node_modules/@blockcast/mmt-fec/src/cmaf-symbols.ts +68 -0
  416. package/node_modules/@blockcast/mmt-fec/src/fec-block-mapper.ts +310 -0
  417. package/node_modules/@blockcast/mmt-fec/src/index.ts +94 -0
  418. package/node_modules/@blockcast/mmt-fec/src/loc-symbols.ts +34 -0
  419. package/node_modules/@blockcast/mmt-fec/src/oti.ts +463 -0
  420. package/node_modules/@blockcast/mmt-fec/src/reorder-buffer.ts +101 -0
  421. package/node_modules/@blockcast/mmt-fec/src/repair-group.ts +195 -0
  422. package/node_modules/@blockcast/mmt-manifest-verify/dist/.build-stamp +0 -0
  423. package/node_modules/@blockcast/mmt-manifest-verify/dist/admission.d.ts +28 -0
  424. package/node_modules/@blockcast/mmt-manifest-verify/dist/admission.d.ts.map +1 -0
  425. package/node_modules/@blockcast/mmt-manifest-verify/dist/admission.js +128 -0
  426. package/node_modules/@blockcast/mmt-manifest-verify/dist/admission.js.map +1 -0
  427. package/node_modules/@blockcast/mmt-manifest-verify/dist/index.d.ts +4 -0
  428. package/node_modules/@blockcast/mmt-manifest-verify/dist/index.d.ts.map +1 -0
  429. package/node_modules/@blockcast/mmt-manifest-verify/dist/index.js +4 -0
  430. package/node_modules/@blockcast/mmt-manifest-verify/dist/index.js.map +1 -0
  431. package/node_modules/@blockcast/mmt-manifest-verify/dist/source-auth.d.ts +37 -0
  432. package/node_modules/@blockcast/mmt-manifest-verify/dist/source-auth.d.ts.map +1 -0
  433. package/node_modules/@blockcast/mmt-manifest-verify/dist/source-auth.js +179 -0
  434. package/node_modules/@blockcast/mmt-manifest-verify/dist/source-auth.js.map +1 -0
  435. package/node_modules/@blockcast/mmt-manifest-verify/dist/verifier.d.ts +99 -0
  436. package/node_modules/@blockcast/mmt-manifest-verify/dist/verifier.d.ts.map +1 -0
  437. package/node_modules/@blockcast/mmt-manifest-verify/dist/verifier.js +492 -0
  438. package/node_modules/@blockcast/mmt-manifest-verify/dist/verifier.js.map +1 -0
  439. package/node_modules/@blockcast/mmt-manifest-verify/package.json +45 -0
  440. package/node_modules/@blockcast/mmt-manifest-verify/src/admission.ts +171 -0
  441. package/node_modules/@blockcast/mmt-manifest-verify/src/index.ts +3 -0
  442. package/node_modules/@blockcast/mmt-manifest-verify/src/source-auth.ts +230 -0
  443. package/node_modules/@blockcast/mmt-manifest-verify/src/verifier.ts +669 -0
  444. package/node_modules/@noble/hashes/LICENSE +21 -0
  445. package/node_modules/@noble/hashes/README.md +521 -0
  446. package/node_modules/@noble/hashes/_assert.d.ts +17 -0
  447. package/node_modules/@noble/hashes/_assert.d.ts.map +1 -0
  448. package/node_modules/@noble/hashes/_assert.js +18 -0
  449. package/node_modules/@noble/hashes/_assert.js.map +1 -0
  450. package/node_modules/@noble/hashes/_blake.d.ts +14 -0
  451. package/node_modules/@noble/hashes/_blake.d.ts.map +1 -0
  452. package/node_modules/@noble/hashes/_blake.js +50 -0
  453. package/node_modules/@noble/hashes/_blake.js.map +1 -0
  454. package/node_modules/@noble/hashes/_md.d.ts +51 -0
  455. package/node_modules/@noble/hashes/_md.d.ts.map +1 -0
  456. package/node_modules/@noble/hashes/_md.js +162 -0
  457. package/node_modules/@noble/hashes/_md.js.map +1 -0
  458. package/node_modules/@noble/hashes/_u64.d.ts +55 -0
  459. package/node_modules/@noble/hashes/_u64.d.ts.map +1 -0
  460. package/node_modules/@noble/hashes/_u64.js +90 -0
  461. package/node_modules/@noble/hashes/_u64.js.map +1 -0
  462. package/node_modules/@noble/hashes/argon2.d.ts +32 -0
  463. package/node_modules/@noble/hashes/argon2.d.ts.map +1 -0
  464. package/node_modules/@noble/hashes/argon2.js +401 -0
  465. package/node_modules/@noble/hashes/argon2.js.map +1 -0
  466. package/node_modules/@noble/hashes/blake1.d.ts +106 -0
  467. package/node_modules/@noble/hashes/blake1.d.ts.map +1 -0
  468. package/node_modules/@noble/hashes/blake1.js +459 -0
  469. package/node_modules/@noble/hashes/blake1.js.map +1 -0
  470. package/node_modules/@noble/hashes/blake2.d.ts +116 -0
  471. package/node_modules/@noble/hashes/blake2.d.ts.map +1 -0
  472. package/node_modules/@noble/hashes/blake2.js +420 -0
  473. package/node_modules/@noble/hashes/blake2.js.map +1 -0
  474. package/node_modules/@noble/hashes/blake2b.d.ts +11 -0
  475. package/node_modules/@noble/hashes/blake2b.d.ts.map +1 -0
  476. package/node_modules/@noble/hashes/blake2b.js +14 -0
  477. package/node_modules/@noble/hashes/blake2b.js.map +1 -0
  478. package/node_modules/@noble/hashes/blake2s.d.ts +20 -0
  479. package/node_modules/@noble/hashes/blake2s.d.ts.map +1 -0
  480. package/node_modules/@noble/hashes/blake2s.js +24 -0
  481. package/node_modules/@noble/hashes/blake2s.js.map +1 -0
  482. package/node_modules/@noble/hashes/blake3.d.ts +54 -0
  483. package/node_modules/@noble/hashes/blake3.d.ts.map +1 -0
  484. package/node_modules/@noble/hashes/blake3.js +255 -0
  485. package/node_modules/@noble/hashes/blake3.js.map +1 -0
  486. package/node_modules/@noble/hashes/crypto.d.ts +2 -0
  487. package/node_modules/@noble/hashes/crypto.d.ts.map +1 -0
  488. package/node_modules/@noble/hashes/crypto.js +5 -0
  489. package/node_modules/@noble/hashes/crypto.js.map +1 -0
  490. package/node_modules/@noble/hashes/cryptoNode.d.ts +2 -0
  491. package/node_modules/@noble/hashes/cryptoNode.d.ts.map +1 -0
  492. package/node_modules/@noble/hashes/cryptoNode.js +18 -0
  493. package/node_modules/@noble/hashes/cryptoNode.js.map +1 -0
  494. package/node_modules/@noble/hashes/eskdf.d.ts +47 -0
  495. package/node_modules/@noble/hashes/eskdf.d.ts.map +1 -0
  496. package/node_modules/@noble/hashes/eskdf.js +166 -0
  497. package/node_modules/@noble/hashes/eskdf.js.map +1 -0
  498. package/node_modules/@noble/hashes/esm/_assert.d.ts +17 -0
  499. package/node_modules/@noble/hashes/esm/_assert.d.ts.map +1 -0
  500. package/node_modules/@noble/hashes/esm/_assert.js +15 -0
  501. package/node_modules/@noble/hashes/esm/_assert.js.map +1 -0
  502. package/node_modules/@noble/hashes/esm/_blake.d.ts +14 -0
  503. package/node_modules/@noble/hashes/esm/_blake.d.ts.map +1 -0
  504. package/node_modules/@noble/hashes/esm/_blake.js +45 -0
  505. package/node_modules/@noble/hashes/esm/_blake.js.map +1 -0
  506. package/node_modules/@noble/hashes/esm/_md.d.ts +51 -0
  507. package/node_modules/@noble/hashes/esm/_md.d.ts.map +1 -0
  508. package/node_modules/@noble/hashes/esm/_md.js +155 -0
  509. package/node_modules/@noble/hashes/esm/_md.js.map +1 -0
  510. package/node_modules/@noble/hashes/esm/_u64.d.ts +55 -0
  511. package/node_modules/@noble/hashes/esm/_u64.d.ts.map +1 -0
  512. package/node_modules/@noble/hashes/esm/_u64.js +67 -0
  513. package/node_modules/@noble/hashes/esm/_u64.js.map +1 -0
  514. package/node_modules/@noble/hashes/esm/argon2.d.ts +32 -0
  515. package/node_modules/@noble/hashes/esm/argon2.d.ts.map +1 -0
  516. package/node_modules/@noble/hashes/esm/argon2.js +392 -0
  517. package/node_modules/@noble/hashes/esm/argon2.js.map +1 -0
  518. package/node_modules/@noble/hashes/esm/blake1.d.ts +106 -0
  519. package/node_modules/@noble/hashes/esm/blake1.d.ts.map +1 -0
  520. package/node_modules/@noble/hashes/esm/blake1.js +452 -0
  521. package/node_modules/@noble/hashes/esm/blake1.js.map +1 -0
  522. package/node_modules/@noble/hashes/esm/blake2.d.ts +116 -0
  523. package/node_modules/@noble/hashes/esm/blake2.d.ts.map +1 -0
  524. package/node_modules/@noble/hashes/esm/blake2.js +413 -0
  525. package/node_modules/@noble/hashes/esm/blake2.js.map +1 -0
  526. package/node_modules/@noble/hashes/esm/blake2b.d.ts +11 -0
  527. package/node_modules/@noble/hashes/esm/blake2b.d.ts.map +1 -0
  528. package/node_modules/@noble/hashes/esm/blake2b.js +11 -0
  529. package/node_modules/@noble/hashes/esm/blake2b.js.map +1 -0
  530. package/node_modules/@noble/hashes/esm/blake2s.d.ts +20 -0
  531. package/node_modules/@noble/hashes/esm/blake2s.d.ts.map +1 -0
  532. package/node_modules/@noble/hashes/esm/blake2s.js +21 -0
  533. package/node_modules/@noble/hashes/esm/blake2s.js.map +1 -0
  534. package/node_modules/@noble/hashes/esm/blake3.d.ts +54 -0
  535. package/node_modules/@noble/hashes/esm/blake3.d.ts.map +1 -0
  536. package/node_modules/@noble/hashes/esm/blake3.js +251 -0
  537. package/node_modules/@noble/hashes/esm/blake3.js.map +1 -0
  538. package/node_modules/@noble/hashes/esm/crypto.d.ts +2 -0
  539. package/node_modules/@noble/hashes/esm/crypto.d.ts.map +1 -0
  540. package/node_modules/@noble/hashes/esm/crypto.js +2 -0
  541. package/node_modules/@noble/hashes/esm/crypto.js.map +1 -0
  542. package/node_modules/@noble/hashes/esm/cryptoNode.d.ts +2 -0
  543. package/node_modules/@noble/hashes/esm/cryptoNode.d.ts.map +1 -0
  544. package/node_modules/@noble/hashes/esm/cryptoNode.js +15 -0
  545. package/node_modules/@noble/hashes/esm/cryptoNode.js.map +1 -0
  546. package/node_modules/@noble/hashes/esm/eskdf.d.ts +47 -0
  547. package/node_modules/@noble/hashes/esm/eskdf.d.ts.map +1 -0
  548. package/node_modules/@noble/hashes/esm/eskdf.js +160 -0
  549. package/node_modules/@noble/hashes/esm/eskdf.js.map +1 -0
  550. package/node_modules/@noble/hashes/esm/hkdf.d.ts +36 -0
  551. package/node_modules/@noble/hashes/esm/hkdf.d.ts.map +1 -0
  552. package/node_modules/@noble/hashes/esm/hkdf.js +82 -0
  553. package/node_modules/@noble/hashes/esm/hkdf.js.map +1 -0
  554. package/node_modules/@noble/hashes/esm/hmac.d.ts +35 -0
  555. package/node_modules/@noble/hashes/esm/hmac.d.ts.map +1 -0
  556. package/node_modules/@noble/hashes/esm/hmac.js +86 -0
  557. package/node_modules/@noble/hashes/esm/hmac.js.map +1 -0
  558. package/node_modules/@noble/hashes/esm/index.d.ts +2 -0
  559. package/node_modules/@noble/hashes/esm/index.d.ts.map +1 -0
  560. package/node_modules/@noble/hashes/esm/index.js +33 -0
  561. package/node_modules/@noble/hashes/esm/index.js.map +1 -0
  562. package/node_modules/@noble/hashes/esm/legacy.d.ts +71 -0
  563. package/node_modules/@noble/hashes/esm/legacy.d.ts.map +1 -0
  564. package/node_modules/@noble/hashes/esm/legacy.js +281 -0
  565. package/node_modules/@noble/hashes/esm/legacy.js.map +1 -0
  566. package/node_modules/@noble/hashes/esm/package.json +10 -0
  567. package/node_modules/@noble/hashes/esm/pbkdf2.d.ts +23 -0
  568. package/node_modules/@noble/hashes/esm/pbkdf2.d.ts.map +1 -0
  569. package/node_modules/@noble/hashes/esm/pbkdf2.js +97 -0
  570. package/node_modules/@noble/hashes/esm/pbkdf2.js.map +1 -0
  571. package/node_modules/@noble/hashes/esm/ripemd160.d.ts +13 -0
  572. package/node_modules/@noble/hashes/esm/ripemd160.d.ts.map +1 -0
  573. package/node_modules/@noble/hashes/esm/ripemd160.js +13 -0
  574. package/node_modules/@noble/hashes/esm/ripemd160.js.map +1 -0
  575. package/node_modules/@noble/hashes/esm/scrypt.d.ts +34 -0
  576. package/node_modules/@noble/hashes/esm/scrypt.d.ts.map +1 -0
  577. package/node_modules/@noble/hashes/esm/scrypt.js +228 -0
  578. package/node_modules/@noble/hashes/esm/scrypt.js.map +1 -0
  579. package/node_modules/@noble/hashes/esm/sha1.d.ts +11 -0
  580. package/node_modules/@noble/hashes/esm/sha1.d.ts.map +1 -0
  581. package/node_modules/@noble/hashes/esm/sha1.js +11 -0
  582. package/node_modules/@noble/hashes/esm/sha1.js.map +1 -0
  583. package/node_modules/@noble/hashes/esm/sha2.d.ts +159 -0
  584. package/node_modules/@noble/hashes/esm/sha2.d.ts.map +1 -0
  585. package/node_modules/@noble/hashes/esm/sha2.js +375 -0
  586. package/node_modules/@noble/hashes/esm/sha2.js.map +1 -0
  587. package/node_modules/@noble/hashes/esm/sha256.d.ts +20 -0
  588. package/node_modules/@noble/hashes/esm/sha256.d.ts.map +1 -0
  589. package/node_modules/@noble/hashes/esm/sha256.js +20 -0
  590. package/node_modules/@noble/hashes/esm/sha256.js.map +1 -0
  591. package/node_modules/@noble/hashes/esm/sha3-addons.d.ts +142 -0
  592. package/node_modules/@noble/hashes/esm/sha3-addons.d.ts.map +1 -0
  593. package/node_modules/@noble/hashes/esm/sha3-addons.js +393 -0
  594. package/node_modules/@noble/hashes/esm/sha3-addons.js.map +1 -0
  595. package/node_modules/@noble/hashes/esm/sha3.d.ts +53 -0
  596. package/node_modules/@noble/hashes/esm/sha3.d.ts.map +1 -0
  597. package/node_modules/@noble/hashes/esm/sha3.js +234 -0
  598. package/node_modules/@noble/hashes/esm/sha3.js.map +1 -0
  599. package/node_modules/@noble/hashes/esm/sha512.d.ts +26 -0
  600. package/node_modules/@noble/hashes/esm/sha512.d.ts.map +1 -0
  601. package/node_modules/@noble/hashes/esm/sha512.js +26 -0
  602. package/node_modules/@noble/hashes/esm/sha512.js.map +1 -0
  603. package/node_modules/@noble/hashes/esm/utils.d.ts +161 -0
  604. package/node_modules/@noble/hashes/esm/utils.d.ts.map +1 -0
  605. package/node_modules/@noble/hashes/esm/utils.js +281 -0
  606. package/node_modules/@noble/hashes/esm/utils.js.map +1 -0
  607. package/node_modules/@noble/hashes/hkdf.d.ts +36 -0
  608. package/node_modules/@noble/hashes/hkdf.d.ts.map +1 -0
  609. package/node_modules/@noble/hashes/hkdf.js +88 -0
  610. package/node_modules/@noble/hashes/hkdf.js.map +1 -0
  611. package/node_modules/@noble/hashes/hmac.d.ts +35 -0
  612. package/node_modules/@noble/hashes/hmac.d.ts.map +1 -0
  613. package/node_modules/@noble/hashes/hmac.js +91 -0
  614. package/node_modules/@noble/hashes/hmac.js.map +1 -0
  615. package/node_modules/@noble/hashes/index.d.ts +1 -0
  616. package/node_modules/@noble/hashes/index.d.ts.map +1 -0
  617. package/node_modules/@noble/hashes/index.js +33 -0
  618. package/node_modules/@noble/hashes/index.js.map +1 -0
  619. package/node_modules/@noble/hashes/legacy.d.ts +71 -0
  620. package/node_modules/@noble/hashes/legacy.d.ts.map +1 -0
  621. package/node_modules/@noble/hashes/legacy.js +287 -0
  622. package/node_modules/@noble/hashes/legacy.js.map +1 -0
  623. package/node_modules/@noble/hashes/package.json +266 -0
  624. package/node_modules/@noble/hashes/pbkdf2.d.ts +23 -0
  625. package/node_modules/@noble/hashes/pbkdf2.d.ts.map +1 -0
  626. package/node_modules/@noble/hashes/pbkdf2.js +101 -0
  627. package/node_modules/@noble/hashes/pbkdf2.js.map +1 -0
  628. package/node_modules/@noble/hashes/ripemd160.d.ts +13 -0
  629. package/node_modules/@noble/hashes/ripemd160.d.ts.map +1 -0
  630. package/node_modules/@noble/hashes/ripemd160.js +16 -0
  631. package/node_modules/@noble/hashes/ripemd160.js.map +1 -0
  632. package/node_modules/@noble/hashes/scrypt.d.ts +34 -0
  633. package/node_modules/@noble/hashes/scrypt.d.ts.map +1 -0
  634. package/node_modules/@noble/hashes/scrypt.js +232 -0
  635. package/node_modules/@noble/hashes/scrypt.js.map +1 -0
  636. package/node_modules/@noble/hashes/sha1.d.ts +11 -0
  637. package/node_modules/@noble/hashes/sha1.d.ts.map +1 -0
  638. package/node_modules/@noble/hashes/sha1.js +14 -0
  639. package/node_modules/@noble/hashes/sha1.js.map +1 -0
  640. package/node_modules/@noble/hashes/sha2.d.ts +159 -0
  641. package/node_modules/@noble/hashes/sha2.d.ts.map +1 -0
  642. package/node_modules/@noble/hashes/sha2.js +384 -0
  643. package/node_modules/@noble/hashes/sha2.js.map +1 -0
  644. package/node_modules/@noble/hashes/sha256.d.ts +20 -0
  645. package/node_modules/@noble/hashes/sha256.d.ts.map +1 -0
  646. package/node_modules/@noble/hashes/sha256.js +23 -0
  647. package/node_modules/@noble/hashes/sha256.js.map +1 -0
  648. package/node_modules/@noble/hashes/sha3-addons.d.ts +142 -0
  649. package/node_modules/@noble/hashes/sha3-addons.d.ts.map +1 -0
  650. package/node_modules/@noble/hashes/sha3-addons.js +402 -0
  651. package/node_modules/@noble/hashes/sha3-addons.js.map +1 -0
  652. package/node_modules/@noble/hashes/sha3.d.ts +53 -0
  653. package/node_modules/@noble/hashes/sha3.d.ts.map +1 -0
  654. package/node_modules/@noble/hashes/sha3.js +239 -0
  655. package/node_modules/@noble/hashes/sha3.js.map +1 -0
  656. package/node_modules/@noble/hashes/sha512.d.ts +26 -0
  657. package/node_modules/@noble/hashes/sha512.d.ts.map +1 -0
  658. package/node_modules/@noble/hashes/sha512.js +29 -0
  659. package/node_modules/@noble/hashes/sha512.js.map +1 -0
  660. package/node_modules/@noble/hashes/src/_assert.ts +22 -0
  661. package/node_modules/@noble/hashes/src/_blake.ts +50 -0
  662. package/node_modules/@noble/hashes/src/_md.ts +176 -0
  663. package/node_modules/@noble/hashes/src/_u64.ts +91 -0
  664. package/node_modules/@noble/hashes/src/argon2.ts +497 -0
  665. package/node_modules/@noble/hashes/src/blake1.ts +534 -0
  666. package/node_modules/@noble/hashes/src/blake2.ts +486 -0
  667. package/node_modules/@noble/hashes/src/blake2b.ts +10 -0
  668. package/node_modules/@noble/hashes/src/blake2s.ts +20 -0
  669. package/node_modules/@noble/hashes/src/blake3.ts +272 -0
  670. package/node_modules/@noble/hashes/src/crypto.ts +9 -0
  671. package/node_modules/@noble/hashes/src/cryptoNode.ts +15 -0
  672. package/node_modules/@noble/hashes/src/eskdf.ts +187 -0
  673. package/node_modules/@noble/hashes/src/hkdf.ts +88 -0
  674. package/node_modules/@noble/hashes/src/hmac.ts +94 -0
  675. package/node_modules/@noble/hashes/src/index.ts +31 -0
  676. package/node_modules/@noble/hashes/src/legacy.ts +293 -0
  677. package/node_modules/@noble/hashes/src/pbkdf2.ts +122 -0
  678. package/node_modules/@noble/hashes/src/ripemd160.ts +12 -0
  679. package/node_modules/@noble/hashes/src/scrypt.ts +257 -0
  680. package/node_modules/@noble/hashes/src/sha1.ts +10 -0
  681. package/node_modules/@noble/hashes/src/sha2.ts +402 -0
  682. package/node_modules/@noble/hashes/src/sha256.ts +24 -0
  683. package/node_modules/@noble/hashes/src/sha3-addons.ts +499 -0
  684. package/node_modules/@noble/hashes/src/sha3.ts +258 -0
  685. package/node_modules/@noble/hashes/src/sha512.ts +34 -0
  686. package/node_modules/@noble/hashes/src/utils.ts +395 -0
  687. package/node_modules/@noble/hashes/utils.d.ts +161 -0
  688. package/node_modules/@noble/hashes/utils.d.ts.map +1 -0
  689. package/node_modules/@noble/hashes/utils.js +313 -0
  690. package/node_modules/@noble/hashes/utils.js.map +1 -0
  691. package/package.json +179 -8
  692. package/schemas/catalog.schema.json +357 -0
  693. package/src/align.ts +239 -0
  694. package/src/audio-init-parser.ts +189 -0
  695. package/src/audio-ring-buffer.ts +120 -0
  696. package/src/authored-timing.ts +63 -0
  697. package/src/block-policy.ts +903 -0
  698. package/src/carp-catalog.ts +1065 -0
  699. package/src/catalog-fec-params.ts +729 -0
  700. package/src/catalog-network-source.ts +74 -0
  701. package/src/catalog-validator.ts +34 -0
  702. package/src/cmaf-block-assembler.ts +1255 -0
  703. package/src/cmaf-group-batcher.ts +104 -0
  704. package/src/cmcd-producer.ts +329 -0
  705. package/src/codec-utils.ts +1984 -0
  706. package/src/conceal-timeline.ts +293 -0
  707. package/src/delivery-gap.ts +147 -0
  708. package/src/duplicate-ledger.ts +372 -0
  709. package/src/fec-codec.ts +61 -0
  710. package/src/fec-manager.ts +4298 -0
  711. package/src/fec-starvation-detector.ts +128 -0
  712. package/src/fec-state.ts +219 -0
  713. package/src/fec-worker-setup.ts +283 -0
  714. package/src/fmp4-completion.ts +47 -0
  715. package/src/fmp4-reader.ts +112 -0
  716. package/src/index.ts +590 -21
  717. package/src/iso-block-locator.ts +329 -0
  718. package/src/iso-bmff-timing.ts +813 -0
  719. package/src/loc.ts +128 -154
  720. package/src/manifest-auth-plan.ts +107 -0
  721. package/src/media-router.ts +841 -0
  722. package/src/mfu-reassembler.ts +3480 -155
  723. package/src/mfu-reassembly-policy.ts +134 -0
  724. package/src/mmtp-container-parser.ts +2380 -0
  725. package/src/mmtp-decode-pipeline.ts +406 -0
  726. package/src/mmtp-router.ts +4860 -0
  727. package/src/mmtp-to-cmaf.ts +977 -0
  728. package/src/multicast-auth-catalog.ts +340 -0
  729. package/src/multicast-input.ts +793 -0
  730. package/src/multicast-manager.ts +2783 -0
  731. package/src/multicast-transport.ts +1058 -0
  732. package/src/multicast-unwrap.ts +132 -0
  733. package/src/no-modules.ts +21 -0
  734. package/src/phase14-catalog-override.ts +336 -0
  735. package/src/repair-fetch.ts +772 -0
  736. package/src/repair-layers.ts +533 -0
  737. package/src/sample-combiner.ts +938 -0
  738. package/src/signaling.ts +78 -0
  739. package/src/source-auth.ts +1 -0
  740. package/src/switching-set-alignment.ts +109 -0
  741. package/src/timestamp-codec.ts +265 -0
  742. package/src/track-pipeline-setup.ts +205 -0
  743. package/src/track-pipeline.ts +332 -0
  744. package/src/track-session.ts +1010 -0
  745. package/src/transport-pool.ts +128 -0
  746. package/src/wasm-mfu-reassembler.ts +405 -0
  747. package/src/wasm-mmtp-container-parser.ts +23 -0
  748. package/test-vectors/align.json +199 -0
  749. package/test-vectors/catalog/README.md +106 -0
  750. package/test-vectors/catalog/negative/archive-origin-with-path.json +33 -0
  751. package/test-vectors/catalog/negative/archive-unknown-field.json +34 -0
  752. package/test-vectors/catalog/negative/fec-block-profile-missing.json +51 -0
  753. package/test-vectors/catalog/negative/fec-block-profile-unknown.json +52 -0
  754. package/test-vectors/catalog/negative/fec-enhancement-repair-removed.json +28 -0
  755. package/test-vectors/catalog/negative/fec-repair-layer-geometry.json +33 -0
  756. package/test-vectors/catalog/negative/fec-repair-layer-missing-symbols.json +31 -0
  757. package/test-vectors/catalog/negative/fec-repair-multiple-depends.json +32 -0
  758. package/test-vectors/catalog/negative/fec-repair-overlay-with-layer.json +34 -0
  759. package/test-vectors/catalog/negative/fec-repair-priority-out-of-band.json +24 -0
  760. package/test-vectors/catalog/negative/fec-repair-source-symbols-without-scope.json +33 -0
  761. package/test-vectors/catalog/negative/legacy-selection-params.json +15 -0
  762. package/test-vectors/catalog/negative/media-track-repair-layer.json +16 -0
  763. package/test-vectors/catalog/negative/missing-mmtp-fields.json +12 -0
  764. package/test-vectors/catalog/negative/multicast-auth-padded-key.json +37 -0
  765. package/test-vectors/catalog/negative/multicast-auth-reused-track-format.json +53 -0
  766. package/test-vectors/catalog/negative/multicast-auth-trailing-pad-bits.json +37 -0
  767. package/test-vectors/catalog/negative/multicast-endpoint-missing-protocol-and-source.json +24 -0
  768. package/test-vectors/catalog/negative/multicast-network-source-empty.json +44 -0
  769. package/test-vectors/catalog/negative/multicast-packet-id-signaling.json +26 -0
  770. package/test-vectors/catalog/negative/network-source-object.json +25 -0
  771. package/test-vectors/catalog/negative/raptorq-source-symbols-over-max.json +24 -0
  772. package/test-vectors/catalog/negative/raptorq-unaligned-symbol.json +27 -0
  773. package/test-vectors/catalog/negative/repair-track-legacy-shape.json +19 -0
  774. package/test-vectors/catalog/negative/track-role-empty.json +16 -0
  775. package/test-vectors/catalog/negative/track-role-non-string.json +16 -0
  776. package/test-vectors/catalog/positive/archive-origin.json +34 -0
  777. package/test-vectors/catalog/positive/fec-iso-ssbg1.json +52 -0
  778. package/test-vectors/catalog/positive/fec-layered-repair.json +106 -0
  779. package/test-vectors/catalog/positive/fec-multicast.json +52 -0
  780. package/test-vectors/catalog/positive/flat-av.json +30 -0
  781. package/test-vectors/catalog/positive/multicast-auth-rotation.json +55 -0
  782. package/test-vectors/catalog/positive/multicast-native-ssm-no-network-source.json +43 -0
  783. package/test-vectors/catalog/positive/ticks-and-mixed-timescales.json +39 -0
  784. package/test-vectors/catalog/positive/track-role.json +49 -0
  785. package/test-vectors/catalog/releases/moqcast-native-v16-v1/catalog.json +65 -0
  786. package/test-vectors/catalog/releases/moqcast-native-v16-v1.archive.sha256 +1 -0
  787. package/test-vectors/catalog-multicast-auth-v1.json +26 -0
  788. package/test-vectors/catalog-validation.json +732 -0
  789. package/test-vectors/catalog-validator-public-api.mjs +25 -0
  790. package/test-vectors/generate-source-auth-fixture.mjs +114 -0
  791. package/test-vectors/hevc-x265-repeat-headers-v1.json +40 -0
  792. package/test-vectors/source-auth-v1.json +61 -0
  793. package/test-vectors/source-contiguity-v1.json +131 -0
  794. package/test-vectors/subsample-placement-v1.json +230 -0
  795. package/dist/mmtp.d.ts +0 -73
  796. package/dist/mmtp.d.ts.map +0 -1
  797. package/dist/mmtp.js +0 -210
  798. package/dist/mmtp.js.map +0 -1
  799. package/src/mmtp.ts +0 -277
@@ -0,0 +1,2783 @@
1
+ /**
2
+ * Facade composing FECManager + MmtpRouter + MulticastTransport.
3
+ *
4
+ * Public methods delegate to the decomposed components. Applications that
5
+ * need individual layers can use them directly:
6
+ * - FECManager — codec-agnostic FEC decode
7
+ * - MmtpRouter — MMTP packet parsing + frame routing
8
+ * - MulticastTransport — SSM join/socket/watchdog
9
+ */
10
+
11
+ import { parseFecBlockProfile, type FecBlockProfile } from "@blockcast/mmt-base";
12
+ import {
13
+ FECManager,
14
+ MIN_INTERLEAVE_DEPTH_MS,
15
+ type FECManagerConfig,
16
+ type WasmFecDecoder,
17
+ type FecDecoderUnavailableReason,
18
+ type FecFallbackDecoder,
19
+ type MulticastSymbolDelivery,
20
+ type RepairVia,
21
+ } from "./fec-manager.js";
22
+ import {
23
+ deriveBlockClassifierGeometry,
24
+ repairPriorityForClass,
25
+ type BlockClass,
26
+ type ClassifierMode,
27
+ type ClassifierReason,
28
+ type FrameGapDecision,
29
+ type ReclassificationEvidence,
30
+ type RepairPriority,
31
+ type TrackKind,
32
+ } from "./block-policy.js";
33
+ import type {
34
+ RepairFetchHost,
35
+ RepairPriorityReason,
36
+ RepairSkipReason,
37
+ RepairSubscribeCallback,
38
+ } from "./repair-fetch.js";
39
+ import {
40
+ MmtpRouter,
41
+ type FrameCallback,
42
+ type ManifestIdentityResolver,
43
+ type MmtpIngressContext,
44
+ type MmtpStreamIdentity,
45
+ type StagedStreamSwitch,
46
+ type StreamSwitchCommit,
47
+ type TrackPacketCallback,
48
+ } from "./mmtp-router.js";
49
+ import { isMulticastTrackPacketId } from "@blockcast/mmt-transport";
50
+ import {
51
+ MulticastTransport,
52
+ type MulticastFailbackReason,
53
+ type MulticastJoinOutcome,
54
+ type MulticastJoinStats,
55
+ } from "./multicast-transport.js";
56
+ import { computeFecDeliveryWindowMs } from "./track-session.js";
57
+ import { DEFAULT_AMT_RELAY_PORT } from "./catalog-network-source.js";
58
+ import { deriveFecInterleaveDepth, FecBlockMapper } from "@blockcast/mmt-fec";
59
+ import type {
60
+ FecBlockSnapshot,
61
+ FecTrackConfig,
62
+ FecTrackStats,
63
+ FecWorkerClient,
64
+ } from "@blockcast/fec-worker";
65
+ import type { MfuReassemblerBackend } from "./mfu-reassembler.js";
66
+ import type { MfuReassemblyPolicy } from "./mfu-reassembly-policy.js";
67
+ import type {
68
+ ManifestObjectIdentity,
69
+ ManifestVerifier,
70
+ } from "@blockcast/mmt-manifest-verify";
71
+ import type {
72
+ BlockAssemblerConfig,
73
+ CmafBlockSample,
74
+ } from "./cmaf-block-assembler.js";
75
+ import {
76
+ hasSwitchingSetRole,
77
+ parseTrackRole,
78
+ validateSwitchingSet,
79
+ isFecCatalogConfig,
80
+ type FecCatalogConfig,
81
+ type TrackRole,
82
+ } from "./carp-catalog.js";
83
+
84
+ export interface MulticastEndpointConfig {
85
+ sourceAddress: string;
86
+ groupAddress: string;
87
+ port: number;
88
+ /** Explicit gateway transport policy. Omitted means gateway-selected auto mode. */
89
+ mode?: 'auto' | 'native' | 'tunnel';
90
+ /** AMT relay hostname/IP from catalog endpoint. */
91
+ relayAddress?: string;
92
+ /** AMT relay port from catalog endpoint. */
93
+ relayPort?: number;
94
+ /** AMT relay hostname/IP from player/UI overrides. Wins over `relayAddress`. */
95
+ amtRelay?: string;
96
+ /** AMT relay port from player/UI overrides. Wins over `relayPort`. */
97
+ amtRelayPort?: number;
98
+ /** MMTP packet IDs to forward before data enters the page transport queue. */
99
+ packetIds?: number[];
100
+ /** Exact catalog tracks carried by this physical multicast endpoint. */
101
+ tracks?: MmtpIngressContext["tracks"];
102
+ /**
103
+ * The catalog `fec.blockProfile` (BLO-35597): `k-padded-v1` or
104
+ * `iso-ssbg1-v1`. Required for per-track FEC configuration; never defaulted.
105
+ */
106
+ fecBlockProfile?: FecBlockProfile;
107
+ fecK?: number;
108
+ /** Number of repair symbols per FEC block (P). Used to size recovery reorder latency. */
109
+ fecRepairSymbols?: number;
110
+ /** Symbol size in bytes (T). NOT repair count. */
111
+ fecSymbolSize?: number;
112
+ /** Catalog fec.reorderToleranceMs — signaled jitter/reorder allowance, ADDED
113
+ * to the derived delivery window. Never a multiplier on the span, so one
114
+ * network yields the same slack on a wide video span and a narrow audio one.
115
+ *
116
+ * Absence widens the window by zero rather than throwing, unlike the datagram
117
+ * MFU deadline in hang-mmt-fec. The distinction is which way absence fails:
118
+ * there the tolerance IS the deadline's slack term and guessing one invents
119
+ * a reorder budget, so it must fail loud; here the window is already fully
120
+ * derived from other catalog values and the tolerance only widens it, so
121
+ * adding nothing is the identity and errs toward earlier failback. Do not
122
+ * "harmonize" this into a throw, and do not turn the zero into a default. */
123
+ fecReorderToleranceMs?: number;
124
+ fecInterleaveDepthMs?: number;
125
+ /** Aggregate endpoint bandwidth in bits per second from the multicast catalog. */
126
+ bandwidth?: number;
127
+ fps?: number;
128
+ /** Track media kind (repair class `audio` vs video block classification). */
129
+ trackKind?: TrackKind;
130
+ /** Catalog codec string (video conceal mode, codec-aware RAP checks). */
131
+ codec?: string;
132
+ /** Catalog track timescale for `keyframeIntervalTicks`. */
133
+ timescale?: number;
134
+ /** Raw catalog `keyframeIntervalTicks` (never a resolved cadence). */
135
+ keyframeIntervalTicks?: number;
136
+ /** Raw catalog `keyframeIntervalMs` (never a resolved cadence). */
137
+ keyframeIntervalMs?: number;
138
+ /** Catalog-derived lower bound for packet-silence failback (for example, keyframe repair cadence). */
139
+ minimumFailbackWindowMs?: number;
140
+ }
141
+ export interface MulticastManagerOptions {
142
+ /**
143
+ * MFU reassembly timeout (ms). Threaded to the inner MmtpRouter (and its
144
+ * MmtpContainerParser). Caller derives from catalog `fec.interleaveDepthMs`,
145
+ * which is already in milliseconds — `moqenc_mmt.c` converts
146
+ * `depth_ms = fec_depth * (1000/fps)` before populating the catalog.
147
+ * Defaults to 1000ms (with parser warning) when omitted.
148
+ */
149
+ mfuTimeoutMs?: number;
150
+ reassemblyPolicy?: MfuReassemblyPolicy;
151
+ reassembler?: MfuReassemblerBackend;
152
+ maxBufferedMfus?: number;
153
+ manifestVerifier?: ManifestVerifier;
154
+ manifestVerifierOwnership?: "caller" | "router";
155
+ resolveManifestIdentity?: (
156
+ packetId: number,
157
+ sbn: number,
158
+ esi: number,
159
+ catalogTrackName?: string,
160
+ ) => ManifestObjectIdentity | null;
161
+ resolveManifestIdentityForRoute?: ManifestIdentityResolver;
162
+ /**
163
+ * Also write every repair lifecycle event to the console as one
164
+ * `REPAIR_LIFECYCLE_LOG_PREFIX` JSON line. Off unless asked for: the lines
165
+ * are serialized on the page thread for every failed block and repair step
166
+ * (and again for an attached inspector), and addRepairLifecycleObserver is
167
+ * the lifecycle API. Enable only for interactive debugging (BLO-36599).
168
+ */
169
+ logRepairLifecycle?: boolean;
170
+ }
171
+
172
+ /**
173
+ * An in-place video rendition switch on the live join: MulticastManager
174
+ * .switchRendition. Every rendition of a ladder can share one SSM group and
175
+ * differ only by MMTP packet ID, so the switch remaps packet IDs at a RAP
176
+ * instead of leaving and re-joining the group.
177
+ */
178
+ export interface RenditionSwitchRequest {
179
+ /** The rendition playing now: an ingress track of the live join. */
180
+ readonly from: MmtpStreamIdentity;
181
+ /**
182
+ * The rendition replacing it, already registered as video with everything
183
+ * it plays through (setPacketIdType, setTrackCodec, configureTrackFec, its
184
+ * FEC decoder, frame and init callbacks, MSE assembler, repair owner).
185
+ */
186
+ readonly to: MmtpStreamIdentity;
187
+ /**
188
+ * The live join's complete ingress once the switch commits: every track
189
+ * that stays admitted (audio, repair legs) plus `to` and its repair legs.
190
+ * It must carry `to`, nothing on `from`'s packet ID (the commit retires
191
+ * it), and no live packet ID under another name.
192
+ */
193
+ readonly tracks: MmtpIngressContext["tracks"];
194
+ /**
195
+ * The allow-list once the switch commits, when it changes: an allow-list
196
+ * scoped to the active rendition. Omit it for a rendition-stable allow-list
197
+ * that already admits `to`; the live allow-list is then kept unchanged.
198
+ */
199
+ readonly packetIds?: readonly number[];
200
+ /**
201
+ * Called synchronously at the commit, inside the frame path, after `from`
202
+ * is retired and the admission narrowed, and before `to`'s newest init and
203
+ * its RAP are delivered: reconfigure the decoder here. Throwing fails the
204
+ * switch (`commit-failed`): `from` cannot be restored, so `to` is retired
205
+ * as well and none of it is delivered.
206
+ */
207
+ readonly onCommit: (commit: StreamSwitchCommit) => void;
208
+ /** Called once when the switch ends without a completed commit. */
209
+ readonly onCancel: (cancel: RenditionSwitchCancel) => void;
210
+ }
211
+
212
+ /**
213
+ * Why a staged rendition switch ended without a completed commit.
214
+ *
215
+ * - `cancelled`: RenditionSwitch.cancel().
216
+ * - `superseded`: join() or dispose() replaced the admission it was staged on.
217
+ * - `provider-refused`: the subscription refused to widen its packet filter to
218
+ * `to`, so `to` can never arrive.
219
+ * - `commit-failed`: `onCommit` threw. `from` was already retired, so `to` is
220
+ * retired as well and the subscription now delivers neither rendition;
221
+ * recover with join().
222
+ */
223
+ export type RenditionSwitchCancel =
224
+ | { readonly reason: "cancelled" }
225
+ | { readonly reason: "superseded" }
226
+ | { readonly reason: "provider-refused"; readonly error: unknown }
227
+ | { readonly reason: "commit-failed"; readonly error: unknown };
228
+
229
+ export type RenditionSwitchState = "staged" | "committed" | "cancelled" | "failed";
230
+
231
+ /** A rendition switch MulticastManager.switchRendition staged. */
232
+ export interface RenditionSwitch {
233
+ readonly state: RenditionSwitchState;
234
+ /**
235
+ * Abandon the switch before it commits: the pre-switch admission is
236
+ * restored and `to` is retired (its router registrations and FEC state
237
+ * released). Returns false once the switch committed or ended.
238
+ */
239
+ cancel(): boolean;
240
+ }
241
+
242
+ interface ActiveRenditionSwitch {
243
+ readonly from: MmtpStreamIdentity;
244
+ readonly to: MmtpStreamIdentity;
245
+ readonly onCommit: (commit: StreamSwitchCommit) => void;
246
+ readonly onCancel: (cancel: RenditionSwitchCancel) => void;
247
+ /** The admission the switch was staged on, restored by cancel(). */
248
+ readonly liveIngress: MmtpIngressContext;
249
+ readonly livePacketIds: ReadonlySet<number> | null;
250
+ /** True when staging widened the allow-list (so cancel narrows it back). */
251
+ readonly widened: boolean;
252
+ /** The admission once committed. */
253
+ readonly committedIngress: MmtpIngressContext;
254
+ readonly committedPacketIds: readonly number[] | undefined;
255
+ router: StagedStreamSwitch | null;
256
+ state: RenditionSwitchState;
257
+ }
258
+
259
+ function freezeIngress(tracks: Iterable<Readonly<{ name: string; packetId: number }>>): MmtpIngressContext {
260
+ return Object.freeze({
261
+ tracks: Object.freeze(
262
+ [...tracks].map(({ name, packetId }) => Object.freeze({ name, packetId })),
263
+ ),
264
+ });
265
+ }
266
+
267
+ function sameIdSet(a: ReadonlySet<number>, b: ReadonlySet<number>): boolean {
268
+ if (a.size !== b.size) return false;
269
+ for (const id of a) if (!b.has(id)) return false;
270
+ return true;
271
+ }
272
+
273
+ export interface RepairStreamIdentity {
274
+ readonly packetId: number;
275
+ /** Non-empty catalog identity, or null when the producer has no track name. */
276
+ readonly catalogTrackName: string | null;
277
+ }
278
+
279
+ export interface RepairBlockIdentity extends RepairStreamIdentity {
280
+ readonly sbn: number;
281
+ }
282
+
283
+ interface RepairSymbolCounts {
284
+ readonly multicast: number;
285
+ readonly moq: number;
286
+ }
287
+
288
+ /**
289
+ * Typed repair lifecycle, version 2 (keyframe-protected FEC policy). One
290
+ * clean cut from version 1: there is no v1 emit and no alias.
291
+ *
292
+ * - `requested`: a block short of its source at the repair deadline; its
293
+ * class sets the QUIC repair priority (P0 keyframe/unknown, P1 audio,
294
+ * P2 delta).
295
+ * - `reclassified`: new evidence (or a disabled classifier) changed a block's
296
+ * class.
297
+ * - `recovered` / `failed`: every block that could not complete from its
298
+ * first-arrival source symbols resolves with exactly one of these.
299
+ * - `repair-preempted` / `repair-deferred`: the scheduler aborted a delta
300
+ * repair for keyframe work, or never ran it before its block resolved.
301
+ * - `repair-priority`: the catalog could not supply the SUBSCRIBER_PRIORITY a
302
+ * repair FETCH must carry (draft-ramadan-moq-fec §11.4.5), so the FETCH was
303
+ * sent without the parameter (`omitted`, keyframe/unknown blocks) or not
304
+ * sent at all (`declined`, delta/audio blocks).
305
+ * - `classifier`: a video stream's classifier mode and why.
306
+ * - `gap`: the stall/conceal decision attached to the first frame released
307
+ * after a lossy gap — the very object carried as `gapBefore`.
308
+ * - `decoder-unavailable`: a stream's Worker decoder left routing.
309
+ */
310
+ export type RepairLifecycleEvent =
311
+ | {
312
+ readonly version: 2;
313
+ readonly type: "requested";
314
+ readonly block: RepairBlockIdentity;
315
+ readonly blockClass: BlockClass;
316
+ readonly priority: RepairPriority;
317
+ }
318
+ | {
319
+ readonly version: 2;
320
+ readonly type: "reclassified";
321
+ readonly block: RepairBlockIdentity;
322
+ readonly from: BlockClass;
323
+ readonly to: BlockClass;
324
+ readonly evidence: ReclassificationEvidence;
325
+ }
326
+ | {
327
+ readonly version: 2;
328
+ readonly type: "recovered";
329
+ readonly block: RepairBlockIdentity;
330
+ readonly blockClass: BlockClass;
331
+ readonly via: RepairVia;
332
+ readonly symbols: RepairSymbolCounts;
333
+ }
334
+ | {
335
+ readonly version: 2;
336
+ readonly type: "failed";
337
+ readonly block: RepairBlockIdentity;
338
+ readonly blockClass: BlockClass;
339
+ readonly symbols: RepairSymbolCounts;
340
+ }
341
+ | {
342
+ readonly version: 2;
343
+ readonly type: "repair-preempted" | "repair-deferred";
344
+ readonly block: RepairBlockIdentity;
345
+ }
346
+ | {
347
+ readonly version: 2;
348
+ /**
349
+ * The scheduler sent no FETCH for a requested block (BLO-35597): the
350
+ * measured reliable-path RTT could not come back before the block's
351
+ * presentation deadline, the path offered no RTT, or the player gave
352
+ * no presentation delay. `rttMs` is null when unmeasured; `budgetMs` is
353
+ * the time that was left to the deadline.
354
+ */
355
+ readonly type: "repair-skipped";
356
+ readonly block: RepairBlockIdentity;
357
+ readonly reason: RepairSkipReason;
358
+ readonly rttMs: number | null;
359
+ readonly budgetMs: number;
360
+ }
361
+ | {
362
+ readonly version: 2;
363
+ readonly type: "repair-priority";
364
+ readonly block: RepairBlockIdentity;
365
+ readonly blockClass: BlockClass;
366
+ readonly action: "omitted" | "declined";
367
+ readonly reason: RepairPriorityReason;
368
+ }
369
+ | {
370
+ readonly version: 2;
371
+ readonly type: "classifier";
372
+ readonly stream: RepairStreamIdentity;
373
+ readonly mode: ClassifierMode;
374
+ readonly reason: ClassifierReason | null;
375
+ }
376
+ | FrameGapDecision
377
+ | {
378
+ readonly version: 2;
379
+ /**
380
+ * The stream's Worker decoder left routing (FECManager.clearFecWorker,
381
+ * or a disposed client evicted on its next symbol). `fallbackDecoder`
382
+ * says what still decodes the stream: 'sync' when a synchronous WASM
383
+ * decoder registered for the same stream keeps serving it, 'none' when
384
+ * nothing does. Only 'none' means the stream's symbols reach no decoder
385
+ * until a host registers one again; consumers fail closed on that
386
+ * instead of reading the following silence as a clean stream.
387
+ */
388
+ readonly type: "decoder-unavailable";
389
+ readonly stream: RepairStreamIdentity;
390
+ readonly reason: FecDecoderUnavailableReason;
391
+ readonly fallbackDecoder: FecFallbackDecoder;
392
+ };
393
+
394
+ /**
395
+ * Keyframe-protected repair policy counters for one catalog track. Reset
396
+ * whenever the track's epoch changes (catalog geometry change or reset).
397
+ *
398
+ * "Protected" means the block was short of its source at the repair deadline
399
+ * with class `keyframe` or `unknown`. The `keyframe*` recovered/failed
400
+ * counters cover every P0 block (class `keyframe` or `unknown`).
401
+ */
402
+ export interface BlockPolicyStats {
403
+ readonly packetId: number;
404
+ readonly catalogTrackName: string | null;
405
+ readonly epoch: number;
406
+ classifierMode: ClassifierMode | null;
407
+ classifierReason: ClassifierReason | null;
408
+ keyframeBlocksProtected: number;
409
+ unknownBlocksProtected: number;
410
+ keyframeBlocksRecoveredInband: number;
411
+ keyframeBlocksRecoveredQuic: number;
412
+ keyframeBlocksFailed: number;
413
+ deltaBlocksShort: number;
414
+ deltaBlocksRecoveredInband: number;
415
+ deltaBlocksRecoveredQuic: number;
416
+ deltaRepairsPreempted: number;
417
+ deltaRepairsDeferred: number;
418
+ /** Repair requests sent no FETCH (see the `repair-skipped` event). */
419
+ repairsSkipped: number;
420
+ deltaBlocksFailed: number;
421
+ audioBlocksRecoveredInband: number;
422
+ audioBlocksRecoveredQuic: number;
423
+ audioBlocksFailed: number;
424
+ stalls: number;
425
+ concealedBlocks: number;
426
+ concealedGaps: number;
427
+ concealedFrames: number;
428
+ concealedAudioMs: number;
429
+ vanishedBlocks: number;
430
+ }
431
+
432
+ function newBlockPolicyStats(
433
+ packetId: number,
434
+ catalogTrackName: string | null,
435
+ epoch: number,
436
+ ): BlockPolicyStats {
437
+ return {
438
+ packetId,
439
+ catalogTrackName,
440
+ epoch,
441
+ classifierMode: null,
442
+ classifierReason: null,
443
+ keyframeBlocksProtected: 0,
444
+ unknownBlocksProtected: 0,
445
+ keyframeBlocksRecoveredInband: 0,
446
+ keyframeBlocksRecoveredQuic: 0,
447
+ keyframeBlocksFailed: 0,
448
+ deltaBlocksShort: 0,
449
+ deltaBlocksRecoveredInband: 0,
450
+ deltaBlocksRecoveredQuic: 0,
451
+ deltaRepairsPreempted: 0,
452
+ deltaRepairsDeferred: 0,
453
+ repairsSkipped: 0,
454
+ deltaBlocksFailed: 0,
455
+ audioBlocksRecoveredInband: 0,
456
+ audioBlocksRecoveredQuic: 0,
457
+ audioBlocksFailed: 0,
458
+ stalls: 0,
459
+ concealedBlocks: 0,
460
+ concealedGaps: 0,
461
+ concealedFrames: 0,
462
+ concealedAudioMs: 0,
463
+ vanishedBlocks: 0,
464
+ };
465
+ }
466
+
467
+ export type RepairLifecycleObserver = (event: RepairLifecycleEvent) => void;
468
+
469
+ export interface RepairLifecycleSource {
470
+ addRepairLifecycleObserver(observer: RepairLifecycleObserver): () => void;
471
+ }
472
+
473
+ /** Minimal event shape accepted by application-owned event targets. */
474
+ export interface RepairLifecycleApplicationEvent {
475
+ readonly type: string;
476
+ readonly detail: RepairLifecycleEvent;
477
+ }
478
+
479
+ /** DOM EventTarget event shape, including the Node 18 Event fallback. */
480
+ export interface RepairLifecycleDomEvent extends Event {
481
+ readonly detail: RepairLifecycleEvent;
482
+ }
483
+
484
+ /** Structural target contract shared by DOM and Closure-style event targets. */
485
+ export interface RepairLifecycleEventTarget<
486
+ TEvent extends RepairLifecycleApplicationEvent,
487
+ > {
488
+ dispatchEvent(event: TEvent): unknown;
489
+ }
490
+
491
+ export interface RepairLifecycleEventOptions<
492
+ TEvent extends RepairLifecycleApplicationEvent = RepairLifecycleDomEvent,
493
+ > {
494
+ bubbles?: boolean;
495
+ composed?: boolean;
496
+ createEvent?: RepairLifecycleEventFactory<TEvent>;
497
+ }
498
+
499
+ export type RepairLifecycleEventFactory<
500
+ TEvent extends RepairLifecycleApplicationEvent = RepairLifecycleDomEvent,
501
+ > = (
502
+ type: typeof REPAIR_LIFECYCLE_EVENT_TYPE,
503
+ detail: RepairLifecycleEvent,
504
+ options: Readonly<{ bubbles: boolean; composed: boolean }>,
505
+ ) => TEvent;
506
+
507
+ /** Public EventTarget event carrying an immutable RepairLifecycleEvent detail. */
508
+ export const REPAIR_LIFECYCLE_EVENT_TYPE = "repairlifecycle";
509
+
510
+ /** Diagnostic JSON-lines prefix. Never consume logs as a lifecycle API. */
511
+ export const REPAIR_LIFECYCLE_LOG_PREFIX =
512
+ "[MulticastManager] repair-lifecycle ";
513
+
514
+ function createDefaultRepairLifecycleEvent(
515
+ detail: RepairLifecycleEvent,
516
+ options: Readonly<{ bubbles: boolean; composed: boolean }>,
517
+ ): RepairLifecycleDomEvent {
518
+ if (typeof globalThis.CustomEvent === "function") {
519
+ return new CustomEvent<RepairLifecycleEvent>(REPAIR_LIFECYCLE_EVENT_TYPE, {
520
+ detail,
521
+ ...options,
522
+ });
523
+ }
524
+
525
+ const event = new Event(REPAIR_LIFECYCLE_EVENT_TYPE, options);
526
+ Object.defineProperty(event, "detail", {
527
+ value: detail,
528
+ enumerable: true,
529
+ });
530
+ return event as RepairLifecycleDomEvent;
531
+ }
532
+
533
+ function isRecord(value: unknown): value is Record<string, unknown> {
534
+ return value !== null && typeof value === "object" && !Array.isArray(value);
535
+ }
536
+
537
+ function isRepairStreamIdentity(value: unknown): value is RepairStreamIdentity {
538
+ if (!isRecord(value)) return false;
539
+ const packetId = value["packetId"];
540
+ const catalogTrackName = value["catalogTrackName"];
541
+ return (
542
+ Number.isSafeInteger(packetId) &&
543
+ (packetId as number) >= 0 &&
544
+ (catalogTrackName === null ||
545
+ (typeof catalogTrackName === "string" && catalogTrackName.length > 0))
546
+ );
547
+ }
548
+
549
+ function isRepairBlockIdentity(value: unknown): value is RepairBlockIdentity {
550
+ if (!isRecord(value)) return false;
551
+ const sbn = value["sbn"];
552
+ return (
553
+ isRepairStreamIdentity(value) &&
554
+ Number.isSafeInteger(sbn) &&
555
+ (sbn as number) >= 0
556
+ );
557
+ }
558
+
559
+ function isDecoderUnavailableReason(
560
+ value: unknown,
561
+ ): value is FecDecoderUnavailableReason {
562
+ return value === "cleared" || value === "disposed";
563
+ }
564
+
565
+ function isFecFallbackDecoder(value: unknown): value is FecFallbackDecoder {
566
+ return value === "sync" || value === "none";
567
+ }
568
+
569
+ const BLOCK_CLASSES: ReadonlySet<unknown> = new Set(["keyframe", "delta", "unknown", "audio"]);
570
+ const RECLASSIFICATION_EVIDENCE: ReadonlySet<unknown> = new Set([
571
+ "rap",
572
+ "mpu-span",
573
+ "window",
574
+ "classifier-disabled",
575
+ ]);
576
+ const CLASSIFIER_MODES: ReadonlySet<unknown> = new Set(["classifying", "all-protected"]);
577
+ const CLASSIFIER_REASONS: ReadonlySet<unknown> = new Set([
578
+ "framerate-absent",
579
+ "keyframe-cadence-absent",
580
+ "keyframe-interval-not-integral",
581
+ "block-spans-gop",
582
+ "keyframe-interval-not-media-integral",
583
+ "rap-not-mpu-start",
584
+ "stream-geometry-absent",
585
+ "mpu-longer-than-gop",
586
+ "block-not-contiguous",
587
+ "rap-spacing-mismatch",
588
+ "mpu-multiple-fragments",
589
+ "block-wider-than-interleave",
590
+ ]);
591
+ const REPAIR_PRIORITY_REASONS: ReadonlySet<unknown> = new Set([
592
+ "source-priority-absent",
593
+ "source-priority-invalid",
594
+ "repair-priority-absent",
595
+ "repair-priority-invalid",
596
+ ]);
597
+ const GAP_REASONS: Readonly<Record<string, "stall" | "conceal">> = {
598
+ "rap-lost": "stall",
599
+ "rap-unknown": "stall",
600
+ discontinuity: "stall",
601
+ "delta-lost": "conceal",
602
+ "rap-next": "conceal",
603
+ "audio-gap": "conceal",
604
+ "audio-discontinuity": "conceal",
605
+ };
606
+
607
+ function hasExactKeys(value: Record<string, unknown>, keys: readonly string[]): boolean {
608
+ const present = Object.keys(value);
609
+ return present.length === keys.length && keys.every((key) => key in value);
610
+ }
611
+
612
+ function isNonNegativeSafeInteger(value: unknown): boolean {
613
+ return Number.isSafeInteger(value) && (value as number) >= 0;
614
+ }
615
+
616
+ function isSymbolCounts(value: unknown): boolean {
617
+ return (
618
+ isRecord(value) &&
619
+ hasExactKeys(value, ["multicast", "moq"]) &&
620
+ isNonNegativeSafeInteger(value["multicast"]) &&
621
+ isNonNegativeSafeInteger(value["moq"])
622
+ );
623
+ }
624
+
625
+ function isNullableNonNegative(value: unknown): boolean {
626
+ return value === null || (typeof value === "number" && Number.isFinite(value) && value >= 0);
627
+ }
628
+
629
+ /** Runtime guard for observer, DOM-event, and cross-realm consumers. */
630
+ export function isRepairLifecycleEvent(
631
+ value: unknown,
632
+ ): value is RepairLifecycleEvent {
633
+ if (!isRecord(value) || value["version"] !== 2) return false;
634
+ const block = value["block"];
635
+ switch (value["type"]) {
636
+ case "requested": {
637
+ const blockClass = value["blockClass"];
638
+ return (
639
+ hasExactKeys(value, ["version", "type", "block", "blockClass", "priority"]) &&
640
+ isRepairBlockIdentity(block) &&
641
+ BLOCK_CLASSES.has(blockClass) &&
642
+ value["priority"] === repairPriorityForClass(blockClass as BlockClass)
643
+ );
644
+ }
645
+ case "reclassified":
646
+ return (
647
+ hasExactKeys(value, ["version", "type", "block", "from", "to", "evidence"]) &&
648
+ isRepairBlockIdentity(block) &&
649
+ BLOCK_CLASSES.has(value["from"]) &&
650
+ BLOCK_CLASSES.has(value["to"]) &&
651
+ RECLASSIFICATION_EVIDENCE.has(value["evidence"])
652
+ );
653
+ case "recovered":
654
+ return (
655
+ hasExactKeys(value, ["version", "type", "block", "blockClass", "via", "symbols"]) &&
656
+ isRepairBlockIdentity(block) &&
657
+ BLOCK_CLASSES.has(value["blockClass"]) &&
658
+ (value["via"] === "inband" || value["via"] === "quic") &&
659
+ isSymbolCounts(value["symbols"])
660
+ );
661
+ case "failed":
662
+ return (
663
+ hasExactKeys(value, ["version", "type", "block", "blockClass", "symbols"]) &&
664
+ isRepairBlockIdentity(block) &&
665
+ BLOCK_CLASSES.has(value["blockClass"]) &&
666
+ isSymbolCounts(value["symbols"])
667
+ );
668
+ case "repair-preempted":
669
+ case "repair-deferred":
670
+ return hasExactKeys(value, ["version", "type", "block"]) && isRepairBlockIdentity(block);
671
+ case "repair-priority": {
672
+ const blockClass = value["blockClass"];
673
+ const action = value["action"];
674
+ const reason = value["reason"];
675
+ // The action follows from the class: a keyframe/unknown request is
676
+ // sent without the parameter, a delta/audio one is not sent.
677
+ const expected =
678
+ BLOCK_CLASSES.has(blockClass) &&
679
+ repairPriorityForClass(blockClass as BlockClass) === 0
680
+ ? "omitted"
681
+ : "declined";
682
+ return (
683
+ hasExactKeys(value, ["version", "type", "block", "blockClass", "action", "reason"]) &&
684
+ isRepairBlockIdentity(block) &&
685
+ BLOCK_CLASSES.has(blockClass) &&
686
+ action === expected &&
687
+ REPAIR_PRIORITY_REASONS.has(reason) &&
688
+ (action === "omitted") === (reason === "source-priority-absent" || reason === "source-priority-invalid")
689
+ );
690
+ }
691
+ case "classifier": {
692
+ const reason = value["reason"];
693
+ return (
694
+ hasExactKeys(value, ["version", "type", "stream", "mode", "reason"]) &&
695
+ isRepairStreamIdentity(value["stream"]) &&
696
+ CLASSIFIER_MODES.has(value["mode"]) &&
697
+ (reason === null || CLASSIFIER_REASONS.has(reason))
698
+ );
699
+ }
700
+ case "gap": {
701
+ const reason = value["reason"];
702
+ const blocks = value["blocks"];
703
+ return (
704
+ hasExactKeys(value, [
705
+ "version",
706
+ "type",
707
+ "stream",
708
+ "trackKind",
709
+ "action",
710
+ "reason",
711
+ "blocks",
712
+ "missingFrames",
713
+ "missingMs",
714
+ "epoch",
715
+ ]) &&
716
+ isRepairStreamIdentity(value["stream"]) &&
717
+ (value["trackKind"] === "video" || value["trackKind"] === "audio") &&
718
+ typeof reason === "string" &&
719
+ GAP_REASONS[reason] !== undefined &&
720
+ GAP_REASONS[reason] === value["action"] &&
721
+ Array.isArray(blocks) &&
722
+ blocks.every(isNonNegativeSafeInteger) &&
723
+ (value["missingFrames"] === null || isNonNegativeSafeInteger(value["missingFrames"])) &&
724
+ isNullableNonNegative(value["missingMs"]) &&
725
+ isNonNegativeSafeInteger(value["epoch"])
726
+ );
727
+ }
728
+ case "decoder-unavailable":
729
+ return (
730
+ hasExactKeys(value, ["version", "type", "stream", "reason", "fallbackDecoder"]) &&
731
+ isRepairStreamIdentity(value["stream"]) &&
732
+ isDecoderUnavailableReason(value["reason"]) &&
733
+ isFecFallbackDecoder(value["fallbackDecoder"])
734
+ );
735
+ default:
736
+ return false;
737
+ }
738
+ }
739
+
740
+ /**
741
+ * Forward typed repair lifecycle observations through an application-owned
742
+ * EventTarget. The adapter retains no history; the returned function detaches
743
+ * the observer.
744
+ */
745
+ export function forwardRepairLifecycleEvents<
746
+ TEvent extends RepairLifecycleApplicationEvent,
747
+ >(
748
+ source: RepairLifecycleSource,
749
+ target: RepairLifecycleEventTarget<TEvent>,
750
+ options: RepairLifecycleEventOptions<TEvent> & {
751
+ createEvent: RepairLifecycleEventFactory<TEvent>;
752
+ },
753
+ ): () => void;
754
+ export function forwardRepairLifecycleEvents(
755
+ source: RepairLifecycleSource,
756
+ target: EventTarget,
757
+ options?: RepairLifecycleEventOptions,
758
+ ): () => void;
759
+ export function forwardRepairLifecycleEvents(
760
+ source: RepairLifecycleSource,
761
+ target:
762
+ | EventTarget
763
+ | RepairLifecycleEventTarget<RepairLifecycleApplicationEvent>,
764
+ options: RepairLifecycleEventOptions<RepairLifecycleApplicationEvent> = {},
765
+ ): () => void {
766
+ return source.addRepairLifecycleObserver((event) => {
767
+ const eventOptions = {
768
+ bubbles: options.bubbles ?? false,
769
+ composed: options.composed ?? false,
770
+ };
771
+ const forwardedEvent = options.createEvent
772
+ ? options.createEvent(REPAIR_LIFECYCLE_EVENT_TYPE, event, eventOptions)
773
+ : createDefaultRepairLifecycleEvent(event, eventOptions);
774
+ (
775
+ target as RepairLifecycleEventTarget<RepairLifecycleApplicationEvent>
776
+ ).dispatchEvent(forwardedEvent);
777
+ });
778
+ }
779
+
780
+ function repairStreamIdentity(
781
+ packetId: number,
782
+ catalogTrackName?: string,
783
+ ): RepairStreamIdentity {
784
+ if (!Number.isSafeInteger(packetId) || packetId < 0) {
785
+ throw new TypeError("packetId must be a non-negative safe integer");
786
+ }
787
+ if (
788
+ catalogTrackName !== undefined &&
789
+ (typeof catalogTrackName !== "string" || catalogTrackName.length === 0)
790
+ ) {
791
+ throw new TypeError("catalogTrackName must be a non-empty string when provided");
792
+ }
793
+ return {
794
+ packetId,
795
+ catalogTrackName: catalogTrackName ?? null,
796
+ };
797
+ }
798
+
799
+ function repairBlockIdentity(
800
+ packetId: number,
801
+ sbn: number,
802
+ catalogTrackName?: string,
803
+ ): RepairBlockIdentity {
804
+ const stream = repairStreamIdentity(packetId, catalogTrackName);
805
+ if (!Number.isSafeInteger(sbn) || sbn < 0) {
806
+ throw new TypeError("sbn must be a non-negative safe integer");
807
+ }
808
+ return { ...stream, sbn };
809
+ }
810
+
811
+ /**
812
+ * Reject a catalog geometry value that is present but not usable. Absent stays
813
+ * absent — an omitted field is resolved from the previously configured track or
814
+ * left unset — but a supplied non-finite/non-positive value is a catalog defect
815
+ * and must fail at configuration time rather than collapse a derived deadline
816
+ * to ~0 packets later.
817
+ */
818
+ function assertPositiveGeometry(
819
+ label: string,
820
+ value: number | undefined,
821
+ ): void {
822
+ if (value === undefined) return;
823
+ if (!Number.isFinite(value) || value <= 0) {
824
+ throw new Error(
825
+ `[MulticastManager] ${label}=${value}. ` +
826
+ `Derive a positive ${label} from the stream catalog.`,
827
+ );
828
+ }
829
+ }
830
+
831
+ /** Repair-symbol count (P) may legitimately be 0; only reject malformed values. */
832
+ function assertNonNegativeGeometry(
833
+ label: string,
834
+ value: number | undefined,
835
+ ): void {
836
+ if (value === undefined) return;
837
+ if (!Number.isFinite(value) || value < 0) {
838
+ throw new Error(
839
+ `[MulticastManager] ${label}=${value}. ` +
840
+ `Derive a non-negative ${label} from the stream catalog.`,
841
+ );
842
+ }
843
+ }
844
+
845
+ /**
846
+ * One contributor to the stream-global FEC timing reduction.
847
+ *
848
+ * FEC geometry is per-track, but a MulticastManager owns exactly one transport
849
+ * and one router, so the failback watchdog and the router's MFU/reorder timing
850
+ * are stream-global. They are reduced with `max` across every contributor:
851
+ * failback must not fire, and the router must not evict, while ANY configured
852
+ * track's FEC block could still complete.
853
+ */
854
+ interface FecTimingContribution {
855
+ deliveryWindowMs?: number;
856
+ interleaveDepthMs?: number;
857
+ fps?: number;
858
+ k?: number;
859
+ repairSymbols?: number;
860
+ minimumFailbackWindowMs?: number;
861
+ }
862
+
863
+ function fecReorderWindowFrames(
864
+ fecInterleaveDepthMs?: number,
865
+ fps?: number,
866
+ _k?: number,
867
+ _repairSymbols?: number,
868
+ ): number {
869
+ if (!fecInterleaveDepthMs || fecInterleaveDepthMs <= 0 || !fps || fps <= 0) return 0;
870
+ const frameMs = 1000 / fps;
871
+ // Reorder across the FEC block span: fecInterleaveDepthMs IS the full block
872
+ // span (§5.1), so this window admits any FEC-recovered frame from the block
873
+ // without holding frames longer than one block.
874
+ return deriveFecInterleaveDepth(fecInterleaveDepthMs, frameMs);
875
+ }
876
+
877
+ /** Catalog track descriptor for configureMultiTrack(). */
878
+ export interface MultiTrackCatalogTrack {
879
+ name: string;
880
+ altGroup?: number;
881
+ framerate?: number;
882
+ fec?: unknown;
883
+ /** PacketId for testability — in production, learned at subscribe time (Plan 03). */
884
+ packetId?: number;
885
+ /** `"fec-repair"` marks a self-describing repair track (draft-ramadan-moq-fec
886
+ * §6.3). Absent on media tracks and on naming-era catalogs. */
887
+ packaging?: string;
888
+ /** The one source track this repair track carries repair symbols for. Layers
889
+ * 1..n and a keyframe overlay are reachable only through `depends`; layer 0
890
+ * is additionally named by the source's `fec.repairTrack`. */
891
+ depends?: string[];
892
+ /** Base layer index. Absent on a keyframe overlay, which is a separate FEC
893
+ * instance rather than a member of the layer chain. */
894
+ repairLayer?: number;
895
+ /** `"keyframe"` on an overlay track. */
896
+ scope?: string;
897
+ }
898
+
899
+ /** Multi-track catalog shape accepted by configureMultiTrack(). */
900
+ export interface MultiTrackCatalog {
901
+ tracks: MultiTrackCatalogTrack[];
902
+ }
903
+
904
+ /** Creates one stateful synchronous FEC decoder per physical catalog stream. */
905
+ export type WasmFecDecoderFactory = (
906
+ track: Readonly<MultiTrackCatalogTrack>,
907
+ ) => WasmFecDecoder;
908
+
909
+ const DEFAULT_ALT_GROUP = "__default__";
910
+
911
+ type FecDiagnosticsGateway = {
912
+ reportFecDiagnostics?: (snapshot: unknown) => void;
913
+ };
914
+
915
+ /** Active-block tail forwarded to the bridge; mirrors the IWA's bound. */
916
+ const FEC_DIAGNOSTIC_BLOCK_TAIL = 8;
917
+
918
+ /**
919
+ * Project a Worker block snapshot onto the bridge/IWA `subscription_diagnostics`
920
+ * contract. The contract names the per-block repair symbol count `t`; the
921
+ * Worker's `repairCount` is the same quantity (P) under its unambiguous name.
922
+ */
923
+ function projectFecDiagnosticBlock(block: FecBlockSnapshot) {
924
+ return {
925
+ sbn: block.sbn,
926
+ state: block.state,
927
+ sourceReceived: block.sourceReceived,
928
+ repairReceived: block.repairReceived,
929
+ k: block.k,
930
+ t: block.repairCount,
931
+ firstSymbolTs: block.firstSymbolTs,
932
+ lastSymbolTs: block.lastSymbolTs,
933
+ ...(block.completedTs === undefined ? {} : { completedTs: block.completedTs }),
934
+ deadlineTs: block.deadlineTs,
935
+ altaVerified: block.altaVerified,
936
+ relayBlockSigValid: block.relayBlockSigValid,
937
+ };
938
+ }
939
+
940
+ /**
941
+ * Project the Worker track config onto the bridge/IWA contract. Every key
942
+ * below is a named field of that contract (see pim-multicast-gateway
943
+ * `normalizeSubscriptionDiagnostics`); `k`/`t`/`symbolSize` are three
944
+ * independent axes there, so `repairCount` is emitted as `t` and never
945
+ * confused with the symbol size. Worker-internal fields such as
946
+ * `deliveryWindowMs` are not part of the contract and are not forwarded.
947
+ */
948
+ function projectFecDiagnosticConfig(config: FecTrackConfig) {
949
+ return {
950
+ codec: config.codec,
951
+ trackName: config.trackName,
952
+ trackType: config.trackType,
953
+ ...(config.resolution === undefined
954
+ ? {}
955
+ : { resolution: { w: config.resolution.w, h: config.resolution.h } }),
956
+ ...(config.framerate === undefined ? {} : { framerate: config.framerate }),
957
+ ...(config.sampleRate === undefined ? {} : { sampleRate: config.sampleRate }),
958
+ ...(config.algorithm === undefined ? {} : { algorithm: config.algorithm }),
959
+ k: config.k,
960
+ t: config.repairCount,
961
+ symbolSize: config.symbolSize,
962
+ interleaveDepth: config.interleaveDepth,
963
+ interleaveMs: config.interleaveMs,
964
+ fecMode: config.fecMode,
965
+ ...(config.repairTrack === undefined ? {} : { repairTrack: config.repairTrack }),
966
+ altaEnabled: config.altaEnabled,
967
+ relayBlockSigEnabled: config.relayBlockSigEnabled,
968
+ };
969
+ }
970
+
971
+ function publishFecDiagnostics(packetId: number, stats: FecTrackStats): boolean {
972
+ const gateway = (globalThis as typeof globalThis & {
973
+ multicast?: FecDiagnosticsGateway;
974
+ }).multicast;
975
+ if (typeof gateway?.reportFecDiagnostics !== "function") return false;
976
+
977
+ // Keep the control-plane payload bounded and JSON-safe. The Worker snapshot
978
+ // already bounds active blocks; the bridge needs only the current block tail
979
+ // and stable counters, not decoder internals or authentication failure text.
980
+ // The shape is the IWA contract, not the Worker's internal types: spreading
981
+ // FecTrackConfig raw once shipped `repairCount` with no `t`, and the IWA
982
+ // discarded every report ("fec_track.stats.config.t must be a finite
983
+ // non-negative number").
984
+ const snapshot = {
985
+ version: 1,
986
+ packetId,
987
+ observedAt: Date.now(),
988
+ stats: {
989
+ config: projectFecDiagnosticConfig(stats.config),
990
+ sourceSymbols: stats.sourceSymbols,
991
+ repairSymbols: stats.repairSymbols,
992
+ blocksComplete: stats.blocksComplete,
993
+ blocksRecovered: stats.blocksRecovered,
994
+ blocksFailed: stats.blocksFailed,
995
+ // Required by the IWA subscription_diagnostics contract (pim
996
+ // normalizeSubscriptionDiagnostics rejects a report without it). No
997
+ // fill frame is ever synthesised any more — concealment is the sink's
998
+ // hold-last-good-frame — so the value is 0 until that contract drops
999
+ // the field.
1000
+ greenFillFrames: 0,
1001
+ recoveryRate: stats.recoveryRate,
1002
+ liveSbn: stats.liveSbn,
1003
+ blocks: stats.blocks.slice(-FEC_DIAGNOSTIC_BLOCK_TAIL).map(projectFecDiagnosticBlock),
1004
+ interleaveWindowBlocks: stats.interleaveWindowBlocks,
1005
+ avgRecoveryMs: stats.avgRecoveryMs,
1006
+ maxRecoveryMs: stats.maxRecoveryMs,
1007
+ },
1008
+ };
1009
+
1010
+ gateway.reportFecDiagnostics(snapshot);
1011
+ return true;
1012
+ }
1013
+
1014
+ function altGroupKey(track: MultiTrackCatalogTrack): string {
1015
+ return track.altGroup === undefined ? DEFAULT_ALT_GROUP : String(track.altGroup);
1016
+ }
1017
+
1018
+ /**
1019
+ * Every repair track of each source: layer 0 named by the source's
1020
+ * `fec.repairTrack`, plus every self-describing `packaging: "fec-repair"` track
1021
+ * whose `depends` names that source (layers 1..n and a keyframe overlay), per
1022
+ * draft-ramadan-moq-fec §6.3.
1023
+ *
1024
+ * Membership comes from the explicit pointer and from `depends`, never from a
1025
+ * name suffix, so a publisher-chosen name like "parity-alpha" resolves.
1026
+ *
1027
+ * This deliberately does not call `resolveRepairLayers`: that validates the
1028
+ * full layer contract and requires `packaging: "mmtp"` on the source plus a
1029
+ * declared layer-0 repair track, neither of which a `MultiTrackCatalog`
1030
+ * carries. Running it here would throw on every naming-era catalog. Ownership
1031
+ * registration needs membership only; RS_ID offsets and layer ordering are the
1032
+ * decoder's concern, not the router's.
1033
+ */
1034
+ function repairTrackNamesBySource(
1035
+ tracks: readonly MultiTrackCatalogTrack[],
1036
+ ): Map<string, Set<string>> {
1037
+ const bySource = new Map<string, Set<string>>();
1038
+ const add = (source: string, repair: string): void => {
1039
+ if (!source || !repair) return;
1040
+ const names = bySource.get(source) ?? new Set<string>();
1041
+ names.add(repair);
1042
+ bySource.set(source, names);
1043
+ };
1044
+ for (const track of tracks) {
1045
+ if (
1046
+ track.fec !== undefined &&
1047
+ isFecCatalogConfig(track.fec) &&
1048
+ track.fec.repairTrack
1049
+ ) {
1050
+ add(track.name, track.fec.repairTrack);
1051
+ }
1052
+ if (track.packaging === "fec-repair" && Array.isArray(track.depends) && track.depends.length === 1) {
1053
+ add(track.depends[0]!, track.name);
1054
+ }
1055
+ }
1056
+ return bySource;
1057
+ }
1058
+
1059
+ export class MulticastManager {
1060
+ readonly #fec: FECManager;
1061
+ readonly #router: MmtpRouter;
1062
+ readonly #transport: MulticastTransport<MmtpIngressContext>;
1063
+ #reportedFecDiagnosticsFailure = false;
1064
+
1065
+ // ── Stream-global FEC timing reduction ──
1066
+ //
1067
+ // FEC geometry is per-track; the transport failback watchdog and the router's
1068
+ // MFU/reorder timing are stream-global (one transport, one router). Both
1069
+ // configuration entry points feed the same reduction so neither can silently
1070
+ // clip the other: configureFec() supplies the baseline, configureTrackFec()
1071
+ // supplies one entry per track, and #applyStreamGlobalFecTiming() takes the
1072
+ // max across all of them.
1073
+ #streamFecBaseline?: FecTimingContribution;
1074
+ readonly #trackFecTiming = new Map<number, FecTimingContribution>();
1075
+
1076
+ // ── Phase 14: multi-track state (D-06, D-07, D-17, D-26, D-28) ──
1077
+
1078
+ /** Maps altGroup → track names sharing that altGroup. */
1079
+ #switchingSets: Map<number, string[]> = new Map();
1080
+
1081
+ /** Repair packetIds that should not get their own FEC decoder (they ARE the repair source). */
1082
+ #repairPacketIds: Set<number> = new Set();
1083
+
1084
+ /** Tracks physical-stream FEC blocks with an outstanding repair fetch. */
1085
+ #repairInFlight: Set<string> = new Set();
1086
+
1087
+ /** Causal repair events; observers own any retention they require. */
1088
+ #repairLifecycleObservers = new Set<RepairLifecycleObserver>();
1089
+ readonly #logRepairLifecycle: boolean;
1090
+
1091
+ #trackResetObservers = new Set<
1092
+ (packetId: number, catalogTrackName: string) => void
1093
+ >();
1094
+
1095
+ /** Callback fired when a repair fetch should be issued for a block. */
1096
+ #onRepairSubscribeCb: RepairSubscribeCallback | null = null;
1097
+
1098
+ /** Keyframe-protected repair policy counters per catalog track. */
1099
+ #blockPolicyStats = new Map<string, BlockPolicyStats>();
1100
+
1101
+ /** The staged in-place rendition switch, if any. */
1102
+ #renditionSwitch: ActiveRenditionSwitch | null = null;
1103
+
1104
+ constructor(options?: MulticastManagerOptions) {
1105
+ this.#logRepairLifecycle = options?.logRepairLifecycle === true;
1106
+ this.#fec = new FECManager();
1107
+ this.#fec.addFecStatsObserver((packetId, stats) => {
1108
+ try {
1109
+ if (publishFecDiagnostics(packetId, stats)) {
1110
+ this.#reportedFecDiagnosticsFailure = false;
1111
+ }
1112
+ } catch (error) {
1113
+ if (!this.#reportedFecDiagnosticsFailure) {
1114
+ this.#reportedFecDiagnosticsFailure = true;
1115
+ console.warn('[MulticastManager] Failed to publish FEC diagnostics:', error);
1116
+ }
1117
+ }
1118
+ });
1119
+ this.#router = new MmtpRouter({
1120
+ fecManager: this.#fec,
1121
+ mfuTimeoutMs: options?.mfuTimeoutMs,
1122
+ reassemblyPolicy: options?.reassemblyPolicy,
1123
+ reassembler: options?.reassembler,
1124
+ maxBufferedMfus: options?.maxBufferedMfus,
1125
+ manifestVerifier: options?.manifestVerifier,
1126
+ manifestVerifierOwnership: options?.manifestVerifierOwnership,
1127
+ resolveManifestIdentity: options?.resolveManifestIdentity,
1128
+ resolveManifestIdentityForRoute: options?.resolveManifestIdentityForRoute,
1129
+ // The router has reset its own state for the stream; reset the rest.
1130
+ onPublisherRestart: (packetId, catalogTrackName) => {
1131
+ this.#resetStreamOutsideRouter(packetId, catalogTrackName);
1132
+ },
1133
+ onBlockFailed: (sbn, packetId, catalogTrackName, blockClass) => {
1134
+ const pathCount = this.#fec.blockPathCount(
1135
+ sbn,
1136
+ packetId,
1137
+ catalogTrackName,
1138
+ );
1139
+ const stats = this.#statsFor(packetId, catalogTrackName);
1140
+ if (blockClass === "delta") stats.deltaBlocksFailed++;
1141
+ else if (blockClass === "audio") stats.audioBlocksFailed++;
1142
+ else stats.keyframeBlocksFailed++;
1143
+ this.#emitRepairLifecycle({
1144
+ version: 2,
1145
+ type: "failed",
1146
+ block: repairBlockIdentity(packetId, sbn, catalogTrackName),
1147
+ blockClass,
1148
+ symbols: {
1149
+ multicast: pathCount?.multicastSymbols ?? 0,
1150
+ moq: pathCount?.moqSymbols ?? 0,
1151
+ },
1152
+ });
1153
+ },
1154
+ onFrameGap: (decision) => {
1155
+ const stats = this.#statsFor(
1156
+ decision.stream.packetId,
1157
+ decision.stream.catalogTrackName ?? undefined,
1158
+ );
1159
+ if (decision.action === "stall") {
1160
+ stats.stalls++;
1161
+ } else {
1162
+ stats.concealedGaps++;
1163
+ stats.concealedBlocks += decision.blocks.length;
1164
+ if (decision.trackKind === "audio") {
1165
+ stats.concealedAudioMs += decision.missingMs ?? 0;
1166
+ } else {
1167
+ stats.concealedFrames += decision.missingFrames ?? 0;
1168
+ }
1169
+ }
1170
+ this.#emitRepairLifecycle(decision);
1171
+ },
1172
+ });
1173
+ this.#transport = new MulticastTransport<MmtpIngressContext>((packet, ingress) => {
1174
+ this.#router.feedPacket(packet, "multicast", undefined, ingress);
1175
+ });
1176
+ // Each joined subscription declares how it delivers packets. An ordered
1177
+ // relay decides FEC block fate by stream order instead of the datagram
1178
+ // inactivity deadline (see MulticastSymbolDelivery in fec-manager).
1179
+ this.#transport.onDelivery((delivery) => {
1180
+ this.#fec.setMulticastSymbolDelivery(delivery);
1181
+ });
1182
+
1183
+ // Worker-path FEC recovery: route recovered source-symbol bytes through
1184
+ // MmtpRouter so MFU reassembly + dedup runs. Without this wiring the
1185
+ // FecWorkerClient's recovered frames fire FECManager.onBlockRecovered
1186
+ // into a null callback and VideoDecoder never sees a real frame.
1187
+ this.#fec.onBlockRecovered = (sbn, packetId, data, meta, recoveredTrackName) => {
1188
+ const recovered = new Uint8Array(data);
1189
+ const catalogTrackName = recoveredTrackName
1190
+ ?? this.#fec.catalogTrackNameForBlock(sbn, packetId);
1191
+ if (catalogTrackName === undefined) {
1192
+ this.#router.routeRecoveredSymbol(sbn, recovered, packetId, meta.esi);
1193
+ } else {
1194
+ this.#router.routeRecoveredSymbol(
1195
+ sbn,
1196
+ recovered,
1197
+ packetId,
1198
+ meta.esi,
1199
+ catalogTrackName,
1200
+ );
1201
+ }
1202
+ };
1203
+
1204
+ // D-28: release the in-flight repair slot when a repair-requested block
1205
+ // resolves (completes, recovers, or fails), and emit the gate log line.
1206
+ // The request-side bridge (FECManager.onRepairNeeded) is wired in
1207
+ // onRepairSubscribe() so repair deadlines only arm once a player has
1208
+ // registered a fetcher — consumers without D-28 pay no timer cost.
1209
+ this.#fec.onRepairResolved = (sbn, packetId, _outcome, catalogTrackName) => {
1210
+ this.clearRepairForBlock(sbn, packetId, catalogTrackName);
1211
+ };
1212
+ // Every block that could not complete from its first-arrival source
1213
+ // symbols and completed anyway reports `recovered` exactly once.
1214
+ this.#fec.onBlockRepaired = (sbn, packetId, catalogTrackName, blockClass, via) => {
1215
+ const pathCount = this.#fec.blockPathCount(sbn, packetId, catalogTrackName);
1216
+ const stats = this.#statsFor(packetId, catalogTrackName);
1217
+ if (blockClass === "delta") {
1218
+ if (via === "quic") stats.deltaBlocksRecoveredQuic++;
1219
+ else stats.deltaBlocksRecoveredInband++;
1220
+ } else if (blockClass === "audio") {
1221
+ if (via === "quic") stats.audioBlocksRecoveredQuic++;
1222
+ else stats.audioBlocksRecoveredInband++;
1223
+ } else if (via === "quic") {
1224
+ stats.keyframeBlocksRecoveredQuic++;
1225
+ } else {
1226
+ stats.keyframeBlocksRecoveredInband++;
1227
+ }
1228
+ this.#emitRepairLifecycle({
1229
+ version: 2,
1230
+ type: "recovered",
1231
+ block: repairBlockIdentity(packetId, sbn, catalogTrackName),
1232
+ blockClass,
1233
+ via,
1234
+ symbols: {
1235
+ multicast: pathCount?.multicastSymbols ?? 0,
1236
+ moq: pathCount?.moqSymbols ?? 0,
1237
+ },
1238
+ });
1239
+ };
1240
+ this.#fec.onBlockReclassified = (sbn, packetId, catalogTrackName, from, to, evidence) => {
1241
+ this.#emitRepairLifecycle({
1242
+ version: 2,
1243
+ type: "reclassified",
1244
+ block: repairBlockIdentity(packetId, sbn, catalogTrackName),
1245
+ from,
1246
+ to,
1247
+ evidence,
1248
+ });
1249
+ };
1250
+ this.#fec.onClassifierMode = (packetId, catalogTrackName, mode, reason) => {
1251
+ const stats = this.#statsFor(packetId, catalogTrackName);
1252
+ stats.classifierMode = mode;
1253
+ stats.classifierReason = reason;
1254
+ this.#emitRepairLifecycle({
1255
+ version: 2,
1256
+ type: "classifier",
1257
+ stream: repairStreamIdentity(packetId, catalogTrackName),
1258
+ mode,
1259
+ reason,
1260
+ });
1261
+ };
1262
+ this.#fec.onBlockVanished = (_sbn, packetId, catalogTrackName) => {
1263
+ this.#statsFor(packetId, catalogTrackName).vanishedBlocks++;
1264
+ };
1265
+ this.#fec.onDecoderUnavailable = (event) => {
1266
+ this.#emitRepairLifecycle({
1267
+ version: 2,
1268
+ type: "decoder-unavailable",
1269
+ stream: repairStreamIdentity(event.packetId, event.catalogTrackName),
1270
+ reason: event.reason,
1271
+ fallbackDecoder: event.fallbackDecoder,
1272
+ });
1273
+ };
1274
+ }
1275
+
1276
+ // ---------------------------------------------------------------------------
1277
+ // Decomposed component accessors (for new code)
1278
+ // ---------------------------------------------------------------------------
1279
+
1280
+ get fecManager(): FECManager {
1281
+ return this.#fec;
1282
+ }
1283
+ get router(): MmtpRouter {
1284
+ return this.#router;
1285
+ }
1286
+ get transport(): MulticastTransport<MmtpIngressContext> {
1287
+ return this.#transport;
1288
+ }
1289
+
1290
+ // ---------------------------------------------------------------------------
1291
+ // Stats
1292
+ // ---------------------------------------------------------------------------
1293
+
1294
+ get reassembler() {
1295
+ return this.#router.reassembler;
1296
+ }
1297
+ get diagnostics() {
1298
+ return {
1299
+ ...this.#router.getDiagnostics(),
1300
+ _fec: this.#fec.diagnostics,
1301
+ };
1302
+ }
1303
+ get stats() {
1304
+ return this.#transport.stats;
1305
+ }
1306
+
1307
+ /**
1308
+ * See MulticastTransport.lastJoinOutcome: how the newest join() to resolve
1309
+ * did, and the typed reason it resolved false. join() here adds no false
1310
+ * path of its own, so every false it returns is named there; a join() that
1311
+ * rejects (catalog FEC configuration that configureFec refuses, for one)
1312
+ * records nothing.
1313
+ */
1314
+ get lastJoinOutcome(): MulticastJoinOutcome | undefined {
1315
+ return this.#transport.lastJoinOutcome;
1316
+ }
1317
+ /** See MulticastTransport.joinStats: join() calls by how they resolved. */
1318
+ get joinStats(): MulticastJoinStats {
1319
+ return this.#transport.joinStats;
1320
+ }
1321
+
1322
+ /**
1323
+ * Accepted bytes for these MMTP packet_ids, cumulative like
1324
+ * `stats.bytesReceived`; see MulticastTransport.bytesReceivedForPacketIds.
1325
+ * Scopes an ABR throughput sample to the active tier when the join admits
1326
+ * more packet_ids than that tier's. Monotonic only while the set is fixed:
1327
+ * re-baseline when the queried set changes (e.g. on a tier switch).
1328
+ */
1329
+ bytesReceivedForPacketIds(packetIds: ReadonlySet<number>): number {
1330
+ return this.#transport.bytesReceivedForPacketIds(packetIds);
1331
+ }
1332
+
1333
+ get fecStats() {
1334
+ return this.#fec.fecStats;
1335
+ }
1336
+
1337
+ /** The raw WASM decoder for stats access */
1338
+ get wasmDecoder() {
1339
+ return this.#fec.wasmDecoder;
1340
+ }
1341
+
1342
+ // ---------------------------------------------------------------------------
1343
+ // FEC configuration (delegates to FECManager)
1344
+ // ---------------------------------------------------------------------------
1345
+
1346
+ setFecDecoder(
1347
+ packetId: number,
1348
+ client: WasmFecDecoder,
1349
+ k: number,
1350
+ trackType: "video" | "audio" = "video",
1351
+ config?: FECManagerConfig,
1352
+ catalogTrackName?: string,
1353
+ ) {
1354
+ const fecConfig = { ...config, k, trackKind: config?.trackKind ?? trackType };
1355
+ this.#fec.setFecDecoder(packetId, client, k, fecConfig, catalogTrackName);
1356
+ this.#configureTrackMapper(packetId, fecConfig);
1357
+ this.#router.setPacketIdType(packetId, trackType);
1358
+ }
1359
+
1360
+ /**
1361
+ * Register a Worker-backed FEC decoder for a specific packetId (track).
1362
+ * Worker-backed tracks decode asynchronously — recovered frames arrive via
1363
+ * FECManager.onBlockRecovered callback instead of feedSourceSymbol return value.
1364
+ */
1365
+ setFecWorker(
1366
+ packetId: number,
1367
+ client: FecWorkerClient,
1368
+ trackType: "video" | "audio",
1369
+ config: FECManagerConfig,
1370
+ catalogTrackName?: string,
1371
+ ): void {
1372
+ this.#fec.setFecWorker(
1373
+ packetId,
1374
+ client,
1375
+ { ...config, trackKind: config.trackKind ?? trackType },
1376
+ catalogTrackName,
1377
+ );
1378
+ this.#configureTrackMapper(packetId, config);
1379
+ this.#router.setPacketIdType(packetId, trackType);
1380
+ }
1381
+
1382
+ /**
1383
+ * Stop routing a stream's symbols to its Worker decoder. Returns false when
1384
+ * none is registered; on success one `decoder-unavailable` repair lifecycle
1385
+ * event with reason 'cleared' reaches every observer, carrying whether a
1386
+ * synchronous WASM decoder still serves the stream. The packet mapping and
1387
+ * track type are untouched: the track still exists, it just has no Worker
1388
+ * decoder. Call this before disposing the client.
1389
+ */
1390
+ clearFecWorker(packetId: number, catalogTrackName?: string): boolean {
1391
+ return this.#fec.clearFecWorker(packetId, catalogTrackName);
1392
+ }
1393
+
1394
+ /** Register callback for Worker-level FecTrackStats updates. */
1395
+ setOnFecStatsUpdate(
1396
+ cb: (packetId: number, stats: FecTrackStats) => void,
1397
+ ): void {
1398
+ this.#fec.onFecStatsUpdate = cb;
1399
+ }
1400
+
1401
+ setFramerate(fps: number) {
1402
+ this.#router.setFramerate(fps);
1403
+ }
1404
+
1405
+ #configureTrackMapper(packetId: number, config: FECManagerConfig): void {
1406
+ const k = config.k;
1407
+ const interleaveMs = config.interleaveDepthMs;
1408
+ const fps = config.fps;
1409
+ const blockProfile = config.blockProfile;
1410
+ if (blockProfile !== undefined && k && k > 0 && interleaveMs && interleaveMs > 0 && fps && fps > 0) {
1411
+ this.#router.setFecMapper(
1412
+ new FecBlockMapper({
1413
+ blockProfile,
1414
+ k,
1415
+ interleaveDepthMs: interleaveMs,
1416
+ gopDurationMs: 1000 / fps,
1417
+ }),
1418
+ packetId,
1419
+ );
1420
+ }
1421
+ }
1422
+
1423
+ /**
1424
+ * Reduce every registered FEC timing contributor to the stream-global
1425
+ * deadlines and apply them to the single transport and single router.
1426
+ *
1427
+ * `max`, not `min` or last-write-wins: a narrower track must never pull the
1428
+ * failback watchdog or the router's reorder window in under a wider track's
1429
+ * still-completable FEC block. Recomputed on every registration so a track
1430
+ * that arrives late widens the stream rather than being clipped by it.
1431
+ */
1432
+ #applyStreamGlobalFecTiming(): void {
1433
+ const contributions: FecTimingContribution[] = [
1434
+ ...(this.#streamFecBaseline ? [this.#streamFecBaseline] : []),
1435
+ ...this.#trackFecTiming.values(),
1436
+ ];
1437
+ if (contributions.length === 0) return;
1438
+
1439
+ let deliveryWindowMs: number | undefined;
1440
+ let minimumFailbackWindowMs: number | undefined;
1441
+ let mfuTimeoutMs: number | undefined;
1442
+ let reorderWindowFrames: number | undefined;
1443
+
1444
+ for (const c of contributions) {
1445
+ if (c.deliveryWindowMs !== undefined) {
1446
+ deliveryWindowMs = Math.max(deliveryWindowMs ?? 0, c.deliveryWindowMs);
1447
+ }
1448
+ if (c.minimumFailbackWindowMs !== undefined) {
1449
+ minimumFailbackWindowMs = Math.max(
1450
+ minimumFailbackWindowMs ?? 0,
1451
+ c.minimumFailbackWindowMs,
1452
+ );
1453
+ }
1454
+ // Only a valid §5.1 ms interleave may drive router timing. A sub-floor
1455
+ // value is a raw frame count (no fps to convert with) and would collapse
1456
+ // the MFU timeout to ~0 and the reorder window to a single frame; skip
1457
+ // the derivation rather than clamp it, leaving previously-configured
1458
+ // router timing untouched.
1459
+ const interleaveOk =
1460
+ c.interleaveDepthMs !== undefined &&
1461
+ c.interleaveDepthMs >= MIN_INTERLEAVE_DEPTH_MS;
1462
+ if (interleaveOk && c.deliveryWindowMs) {
1463
+ mfuTimeoutMs = Math.max(mfuTimeoutMs ?? 0, c.deliveryWindowMs);
1464
+ }
1465
+ if (interleaveOk && c.fps && c.fps > 0) {
1466
+ reorderWindowFrames = Math.max(
1467
+ reorderWindowFrames ?? 0,
1468
+ fecReorderWindowFrames(
1469
+ c.interleaveDepthMs,
1470
+ c.fps,
1471
+ c.k,
1472
+ c.repairSymbols,
1473
+ ),
1474
+ );
1475
+ }
1476
+ }
1477
+
1478
+ const failbackWindowMs =
1479
+ deliveryWindowMs === undefined
1480
+ ? minimumFailbackWindowMs
1481
+ : Math.max(deliveryWindowMs, minimumFailbackWindowMs ?? 0);
1482
+ if (failbackWindowMs !== undefined) {
1483
+ this.#transport.setFailbackWindowMs(failbackWindowMs);
1484
+ }
1485
+ if (mfuTimeoutMs !== undefined) {
1486
+ this.#router.setMfuTimeoutMs(mfuTimeoutMs);
1487
+ }
1488
+ if (reorderWindowFrames !== undefined) {
1489
+ this.#router.setReorderWindow(reorderWindowFrames);
1490
+ }
1491
+ }
1492
+
1493
+ /**
1494
+ * Configure one track's FEC geometry, block mapper, and block-inactivity
1495
+ * deadline from that track's own catalog values, then re-reduce the
1496
+ * stream-global deadlines across every registered track.
1497
+ *
1498
+ * This is the packet-specific replacement for the global `configureFec`
1499
+ * side effects: per-track concerns stay per-track, and the genuinely
1500
+ * stream-global ones (transport failback, router MFU timeout, router reorder
1501
+ * window) become an explicit max reduction instead of a last-caller-wins race
1502
+ * in which whichever track configured last silently set the whole stream's
1503
+ * timing.
1504
+ *
1505
+ * `catalogTrackName` scopes the block-classifier geometry (kind, fps,
1506
+ * interleave, cadence) to that one stream. Pass it whenever ABR rungs can
1507
+ * share this packetId: each rung then classifies with its own framerate
1508
+ * and GOP, and a rung on the packetId that was never configured this way
1509
+ * runs all-protected (`stream-geometry-absent`) instead of borrowing a
1510
+ * sibling's geometry. K, T and the delivery timing stay per packetId.
1511
+ */
1512
+ configureTrackFec(
1513
+ packetId: number,
1514
+ config: Pick<
1515
+ MulticastEndpointConfig,
1516
+ | "fecBlockProfile"
1517
+ | "fecK"
1518
+ | "fecRepairSymbols"
1519
+ | "fecSymbolSize"
1520
+ | "fecInterleaveDepthMs"
1521
+ | "fecReorderToleranceMs"
1522
+ | "fps"
1523
+ | "bandwidth"
1524
+ | "minimumFailbackWindowMs"
1525
+ | "trackKind"
1526
+ | "codec"
1527
+ | "timescale"
1528
+ | "keyframeIntervalTicks"
1529
+ | "keyframeIntervalMs"
1530
+ >,
1531
+ catalogTrackName?: string,
1532
+ ): void {
1533
+ if (catalogTrackName !== undefined && catalogTrackName.length === 0) {
1534
+ throw new Error(
1535
+ `[MulticastManager] configureTrackFec(${packetId}) catalogTrackName must be non-empty when given`,
1536
+ );
1537
+ }
1538
+ if (config.fecK === undefined || config.fecInterleaveDepthMs === undefined) {
1539
+ throw new Error(
1540
+ `[MulticastManager] configureTrackFec(${packetId}) requires fecK and ` +
1541
+ "fecInterleaveDepthMs. Derive the per-track FEC geometry from the " +
1542
+ "stream catalog.",
1543
+ );
1544
+ }
1545
+ const blockProfile = parseFecBlockProfile(
1546
+ config.fecBlockProfile,
1547
+ `[MulticastManager] configureTrackFec(${packetId}) fecBlockProfile`,
1548
+ );
1549
+ assertPositiveGeometry("fecK", config.fecK);
1550
+ assertPositiveGeometry("fecInterleaveDepthMs", config.fecInterleaveDepthMs);
1551
+ assertPositiveGeometry("fecSymbolSize", config.fecSymbolSize);
1552
+ assertPositiveGeometry("fps", config.fps);
1553
+ assertPositiveGeometry("bandwidth", config.bandwidth);
1554
+ assertPositiveGeometry(
1555
+ "minimumFailbackWindowMs",
1556
+ config.minimumFailbackWindowMs,
1557
+ );
1558
+ assertPositiveGeometry("timescale", config.timescale);
1559
+ assertPositiveGeometry("keyframeIntervalTicks", config.keyframeIntervalTicks);
1560
+ assertPositiveGeometry("keyframeIntervalMs", config.keyframeIntervalMs);
1561
+ assertNonNegativeGeometry("fecRepairSymbols", config.fecRepairSymbols);
1562
+ // A tolerance is the network path's allowance, added to the encoder's
1563
+ // span. These are public entry points: the catalog parser validates what it
1564
+ // parses, but a direct caller reaches this arithmetic without passing
1565
+ // through it. A negative value silently *shortens* the delivery window
1566
+ // below the signaled span, and Infinity/NaN yields a window no block can
1567
+ // ever miss -- the eviction bound stops bounding. Reuse the shared
1568
+ // non-negative guard rather than an inline check: undefined must stay
1569
+ // undefined (absent means the encoder never signaled one, and must not be
1570
+ // defaulted), while a declared 0 is a real value that has to survive.
1571
+ assertNonNegativeGeometry(
1572
+ "fecReorderToleranceMs",
1573
+ config.fecReorderToleranceMs,
1574
+ );
1575
+
1576
+ // Same derivation as the stream-global path, but from THIS track's
1577
+ // geometry. FECManager already stores a per-track timeoutMs
1578
+ // (configureTrack/timeoutMsForPacket); it just never had the repair-symbol
1579
+ // and bandwidth inputs needed to compute one.
1580
+ const deliveryWindowMs =
1581
+ config.fecInterleaveDepthMs >= MIN_INTERLEAVE_DEPTH_MS
1582
+ ? computeFecDeliveryWindowMs({
1583
+ interleaveDepthMs: config.fecInterleaveDepthMs,
1584
+ sourceSymbols: config.fecK,
1585
+ repairSymbols: config.fecRepairSymbols,
1586
+ symbolSize: config.fecSymbolSize,
1587
+ mediaUnitsPerSecond: config.fps,
1588
+ bandwidthBps: config.bandwidth,
1589
+ jitterBufferMs: config.fecReorderToleranceMs,
1590
+ })
1591
+ : undefined;
1592
+
1593
+ // The block classifier's geometry comes from the raw catalog cadence
1594
+ // fields; a track whose media kind is not known yet classifies fail-closed.
1595
+ const trackKind = config.trackKind ?? this.#router.trackKindFor(packetId);
1596
+ const geometry = trackKind === undefined
1597
+ ? undefined
1598
+ : deriveBlockClassifierGeometry({
1599
+ trackKind,
1600
+ framerate: config.fps,
1601
+ interleaveDepthMs: config.fecInterleaveDepthMs,
1602
+ timescale: config.timescale,
1603
+ keyframeIntervalTicks: config.keyframeIntervalTicks,
1604
+ keyframeIntervalMs: config.keyframeIntervalMs,
1605
+ });
1606
+ const fecConfig: FECManagerConfig = {
1607
+ blockProfile,
1608
+ k: config.fecK,
1609
+ symbolSize: config.fecSymbolSize,
1610
+ interleaveDepthMs: config.fecInterleaveDepthMs,
1611
+ fps: config.fps,
1612
+ timeoutMs: deliveryWindowMs,
1613
+ ...(trackKind === undefined ? {} : { trackKind }),
1614
+ ...(config.codec === undefined ? {} : { codec: config.codec }),
1615
+ ...(geometry === undefined ? {} : { geometry }),
1616
+ };
1617
+ this.#fec.configureTrack(packetId, fecConfig, catalogTrackName);
1618
+ this.#configureTrackMapper(packetId, fecConfig);
1619
+
1620
+ this.#trackFecTiming.set(packetId, {
1621
+ deliveryWindowMs,
1622
+ interleaveDepthMs: config.fecInterleaveDepthMs,
1623
+ fps: config.fps,
1624
+ k: config.fecK,
1625
+ repairSymbols: config.fecRepairSymbols,
1626
+ minimumFailbackWindowMs: config.minimumFailbackWindowMs,
1627
+ });
1628
+ this.#applyStreamGlobalFecTiming();
1629
+ }
1630
+
1631
+ configureFec(
1632
+ config: Pick<
1633
+ MulticastEndpointConfig,
1634
+ | "fecBlockProfile"
1635
+ | "fecK"
1636
+ | "fecRepairSymbols"
1637
+ | "fecSymbolSize"
1638
+ | "fecInterleaveDepthMs"
1639
+ | "fecReorderToleranceMs"
1640
+ | "fps"
1641
+ | "bandwidth"
1642
+ | "minimumFailbackWindowMs"
1643
+ >,
1644
+ ) {
1645
+ const minimumFailbackWindowMs = config.minimumFailbackWindowMs;
1646
+ if (
1647
+ minimumFailbackWindowMs !== undefined &&
1648
+ (!Number.isFinite(minimumFailbackWindowMs) || minimumFailbackWindowMs <= 0)
1649
+ ) {
1650
+ throw new Error(
1651
+ `[MulticastManager] minimumFailbackWindowMs=${minimumFailbackWindowMs}. ` +
1652
+ "Derive a positive failback floor from the stream catalog.",
1653
+ );
1654
+ }
1655
+ // A tolerance is the network path's allowance, added to the encoder's
1656
+ // span. These are public entry points: the catalog parser validates what it
1657
+ // parses, but a direct caller reaches this arithmetic without passing
1658
+ // through it. A negative value silently *shortens* the delivery window
1659
+ // below the signaled span, and Infinity/NaN yields a window no block can
1660
+ // ever miss -- the eviction bound stops bounding. Reuse the shared
1661
+ // non-negative guard rather than an inline check: undefined must stay
1662
+ // undefined (absent means the encoder never signaled one, and must not be
1663
+ // defaulted), while a declared 0 is a real value that has to survive.
1664
+ assertNonNegativeGeometry(
1665
+ "fecReorderToleranceMs",
1666
+ config.fecReorderToleranceMs,
1667
+ );
1668
+ const deliveryWindowMs =
1669
+ config.fecK &&
1670
+ config.fecK > 0 &&
1671
+ config.fecInterleaveDepthMs &&
1672
+ config.fecInterleaveDepthMs >= MIN_INTERLEAVE_DEPTH_MS
1673
+ ? computeFecDeliveryWindowMs({
1674
+ interleaveDepthMs: config.fecInterleaveDepthMs,
1675
+ sourceSymbols: config.fecK,
1676
+ repairSymbols: config.fecRepairSymbols,
1677
+ symbolSize: config.fecSymbolSize,
1678
+ mediaUnitsPerSecond: config.fps,
1679
+ bandwidthBps: config.bandwidth,
1680
+ jitterBufferMs: config.fecReorderToleranceMs,
1681
+ })
1682
+ : undefined;
1683
+ this.#fec.configure({
1684
+ k: config.fecK,
1685
+ symbolSize: config.fecSymbolSize,
1686
+ interleaveDepthMs: config.fecInterleaveDepthMs,
1687
+ fps: config.fps,
1688
+ timeoutMs: deliveryWindowMs,
1689
+ });
1690
+ // External-feed consumers own the multicast socket and call feedPacket()
1691
+ // directly, so they never enter join(). Configure the transport watchdog
1692
+ // here where both transport-owned and external-feed paths share the same
1693
+ // catalog-derived FEC timing.
1694
+ //
1695
+ // This call supplies the stream-global BASELINE contribution. Any track
1696
+ // registered via configureTrackFec() participates in the same max
1697
+ // reduction, so a per-track window can only widen these deadlines, never
1698
+ // narrow them below what this baseline already guarantees.
1699
+ this.#streamFecBaseline = {
1700
+ deliveryWindowMs,
1701
+ interleaveDepthMs: config.fecInterleaveDepthMs,
1702
+ fps: config.fps,
1703
+ k: config.fecK,
1704
+ repairSymbols: config.fecRepairSymbols,
1705
+ minimumFailbackWindowMs,
1706
+ };
1707
+ this.#applyStreamGlobalFecTiming();
1708
+ // Wire FecBlockMapper to MmtpRouter so SBN/ESI derivation works for FEC recovery.
1709
+ //
1710
+ // gopDurationMs MUST be the per-frame interval (1000/fps), NOT the
1711
+ // interleaveDepthMs. Passing interleaveDepthMs collapses D to 1, which
1712
+ // makes the mapper math equivalent to the no-mapper fallback
1713
+ // (floor(ssId/k), ssId%k) and breaks SBN/ESI derivation for any interleaved
1714
+ // FEC. Surfaced by BLO-3323 L14 (pim-multicast-gateway run 26373805710):
1715
+ // with K=32 + interleaveDepthMs=1000ms but gopDurationMs=1000ms, blocks
1716
+ // completed with only ~8 source + ~8 repair symbols (out of 40 expected at
1717
+ // 25-30fps), so the WASM decoder failed to recover any block at 10% loss.
1718
+ const k = config.fecK;
1719
+ const interleaveMs = config.fecInterleaveDepthMs;
1720
+ const fps = config.fps;
1721
+ // The stream-global mapper is the k-padded-v1 SS_ID locator; it exists
1722
+ // only for a stream whose catalog names that profile.
1723
+ const blockProfile = config.fecBlockProfile === undefined
1724
+ ? undefined
1725
+ : parseFecBlockProfile(config.fecBlockProfile, "[MulticastManager] configureFec fecBlockProfile");
1726
+ if (blockProfile !== undefined && k && k > 0 && interleaveMs && interleaveMs > 0 && fps && fps > 0) {
1727
+ const mapper = new FecBlockMapper({
1728
+ blockProfile,
1729
+ k,
1730
+ interleaveDepthMs: interleaveMs,
1731
+ gopDurationMs: 1000 / fps,
1732
+ });
1733
+ this.#router.setFecMapper(mapper);
1734
+ }
1735
+ }
1736
+
1737
+ // ---------------------------------------------------------------------------
1738
+ // Track configuration (delegates to MmtpRouter)
1739
+ // ---------------------------------------------------------------------------
1740
+
1741
+ // Track type discovery callback — fires when setPacketIdType is called.
1742
+ // Players wire this to MediaRouter.registerTrackTypeFromInit() for
1743
+ // init data authority (authoritative until next catalog update).
1744
+ #onTrackTypeDiscovered:
1745
+ | ((packetId: number, trackType: "video" | "audio") => void)
1746
+ | null = null;
1747
+
1748
+ /** Register callback for track type discovery (from init data / inline signaling). */
1749
+ onTrackTypeDiscovered(
1750
+ cb: (packetId: number, trackType: "video" | "audio") => void,
1751
+ ): void {
1752
+ this.#onTrackTypeDiscovered = cb;
1753
+ }
1754
+
1755
+ setPacketIdType(packetId: number, trackType: "video" | "audio") {
1756
+ this.#router.setPacketIdType(packetId, trackType);
1757
+ this.#onTrackTypeDiscovered?.(packetId, trackType);
1758
+ }
1759
+
1760
+ /**
1761
+ * Set the keyframe loss timeout on the inner MmtpRouter (Phase 13).
1762
+ * Applies only to tracks WITHOUT FEC: an FEC track never arms the timer,
1763
+ * because its vanished blocks resolve through catalog-derived block
1764
+ * deadlines and the release gate's stall decision.
1765
+ * Must be called BEFORE setPacketIdType('video'). A reader is passed
1766
+ * through; see MmtpRouter.setKeyframeLostTimeoutMs.
1767
+ */
1768
+ setKeyframeLostTimeoutMs(ms: number | (() => number)): void {
1769
+ this.#router.setKeyframeLostTimeoutMs(ms);
1770
+ }
1771
+
1772
+ setMfuTimeoutMs(ms: number): void {
1773
+ this.#router.setMfuTimeoutMs(ms);
1774
+ }
1775
+
1776
+ setTrackCodec(packetId: number, codec: string) {
1777
+ this.#router.setTrackCodec(packetId, codec);
1778
+ // Never configure an FEC track by side effect: only an already
1779
+ // configured FEC track learns its codec here.
1780
+ if (this.#fec.isTrackConfigured(packetId)) {
1781
+ this.#fec.configureTrack(packetId, { codec });
1782
+ }
1783
+ }
1784
+
1785
+ // ---------------------------------------------------------------------------
1786
+ // Callbacks (delegates to MmtpRouter)
1787
+ // ---------------------------------------------------------------------------
1788
+
1789
+ onVideoFrame(cb: FrameCallback) {
1790
+ this.#router.onVideoFrame(cb);
1791
+ }
1792
+ onAudioFrame(cb: FrameCallback) {
1793
+ this.#router.onAudioFrame(cb);
1794
+ }
1795
+ onTrackFrame(packetId: number, cb: FrameCallback) {
1796
+ this.#router.onTrackFrame(packetId, cb);
1797
+ }
1798
+ onTrackPacket(packetId: number, cb: TrackPacketCallback) {
1799
+ this.#router.onTrackPacket(packetId, cb);
1800
+ }
1801
+ onInitSegment(cb: (data: Uint8Array) => void) {
1802
+ this.#router.onInitSegment(cb);
1803
+ }
1804
+ onTrackInit(packetId: number, cb: (data: Uint8Array) => void) {
1805
+ this.#router.onTrackInit(packetId, cb);
1806
+ }
1807
+ onFecBlockStarted(cb: (sbn: number, packetId: number) => void) {
1808
+ this.#router.onFecBlockStarted(cb);
1809
+ }
1810
+ /** Unrecoverable-FEC-block signal. Players drive decoder recovery from this
1811
+ * (a starved decoder cannot resync on its own once the block is abandoned). */
1812
+ onFecBlockFailed(
1813
+ cb: (sbn: number, packetId: number, catalogTrackName?: string) => void,
1814
+ ) {
1815
+ this.#router.onFecBlockFailed(cb);
1816
+ }
1817
+
1818
+ // ---------------------------------------------------------------------------
1819
+ // MSE integration (delegates to MmtpRouter)
1820
+ // ---------------------------------------------------------------------------
1821
+
1822
+ enableMseAssembler(
1823
+ packetId: number,
1824
+ config: Omit<BlockAssemblerConfig, "k">,
1825
+ catalogTrackName?: string,
1826
+ ) {
1827
+ return this.#router.enableMseAssembler(packetId, config, catalogTrackName);
1828
+ }
1829
+
1830
+ onMseSegment(packetId: number, cb: (segment: Uint8Array) => void, catalogTrackName?: string) {
1831
+ this.#router.onMseSegment(packetId, cb, catalogTrackName);
1832
+ }
1833
+
1834
+ onMseSample(packetId: number, cb: (sample: CmafBlockSample) => void, catalogTrackName?: string) {
1835
+ this.#router.onMseSample(packetId, cb, catalogTrackName);
1836
+ }
1837
+
1838
+ onMseInitSegment(packetId: number, cb: (init: Uint8Array) => void, catalogTrackName?: string) {
1839
+ this.#router.onMseInitSegment(packetId, cb, catalogTrackName);
1840
+ }
1841
+
1842
+ emitMseInit(packetId: number, catalogTrackName?: string) {
1843
+ this.#router.emitMseInit(packetId, catalogTrackName);
1844
+ }
1845
+
1846
+ markExternalVideoSync(packetId?: number, catalogTrackName?: string) {
1847
+ this.#router.markExternalVideoSync(packetId, catalogTrackName);
1848
+ }
1849
+
1850
+ // ---------------------------------------------------------------------------
1851
+ // Transport (delegates to MulticastTransport)
1852
+ // ---------------------------------------------------------------------------
1853
+
1854
+ onTakeover(cb: () => void) {
1855
+ this.#transport.onTakeover(cb);
1856
+ }
1857
+ onFailback(cb: (reason: MulticastFailbackReason) => void) {
1858
+ this.#transport.onFailback(cb);
1859
+ }
1860
+ /** See MulticastTransport.onDataPlaneLive: the join's first accepted packet. */
1861
+ onDataPlaneLive(cb: () => void) {
1862
+ this.#transport.onDataPlaneLive(cb);
1863
+ }
1864
+ confirmVideo() {
1865
+ this.#transport.confirmVideo();
1866
+ }
1867
+ confirmAudio() {
1868
+ this.#transport.confirmAudio();
1869
+ }
1870
+
1871
+ static isAvailable(): boolean {
1872
+ return MulticastTransport.isAvailable();
1873
+ }
1874
+
1875
+ async join(config: MulticastEndpointConfig): Promise<boolean> {
1876
+ // A join replaces the admission a staged switch was built on. The new
1877
+ // join's config decides what is admitted, so nothing is restored or
1878
+ // retired here: the switch just ends.
1879
+ const staged = this.#renditionSwitch;
1880
+ if (staged !== null) this.#endRenditionSwitch(staged, { reason: "superseded" }, false);
1881
+
1882
+ // Configure every FEC/MFU deadline before subscribing so direct-feed and
1883
+ // transport-owned consumers use the same catalog-derived delivery window.
1884
+ this.configureFec(config);
1885
+
1886
+ // Every transport join starts at an arbitrary multicast symbol. Reset only
1887
+ // join-boundary classification; retain configured tracks, decoders, and
1888
+ // cumulative diagnostics across failback/rejoin cycles.
1889
+ this.#fec.beginMulticastJoin();
1890
+ const selectedPacketIds = config.packetIds?.length
1891
+ ? new Set(config.packetIds)
1892
+ : null;
1893
+ const catalogTracks = config.tracks?.filter(({ packetId }) =>
1894
+ selectedPacketIds === null || selectedPacketIds.has(packetId));
1895
+ const ingressTracks = catalogTracks
1896
+ ? catalogTracks
1897
+ : selectedPacketIds === null
1898
+ ? undefined
1899
+ : [...selectedPacketIds].map((packetId) => ({ name: "", packetId }));
1900
+ const ingress = ingressTracks === undefined
1901
+ ? undefined
1902
+ : {
1903
+ tracks: Object.freeze(
1904
+ ingressTracks.map(({ name, packetId }) =>
1905
+ Object.freeze({ name, packetId }),
1906
+ ),
1907
+ ),
1908
+ };
1909
+ // Precedence between two sources, not a tolerant alias. BOTH operands can
1910
+ // carry a catalog-declared relay, so neither one is "the" inlet:
1911
+ // `amtRelay` — the player/UI override, AND the post-migration catalog
1912
+ // path: `endpointToMulticastConfig` (packages/transport
1913
+ // multicast-catalog-policy.ts) resolves
1914
+ // `multicast.networkSource[{type:"amt"}]` into it
1915
+ // through `findAmtRelay`.
1916
+ // `relayAddress` — the pre-migration per-endpoint relay, retained so a
1917
+ // catalog published before BLO-37834 still joins.
1918
+ // The override/networkSource value wins, else the legacy endpoint value.
1919
+ // `??`, not `||`, so precedence is deliberately NOT trim-aware: a player
1920
+ // clearing its override field means "no AMT", not "fall back to the legacy
1921
+ // relay". Blank is absent for the port decision below but present here.
1922
+ // Retiring `relayAddress` is the legacy cut (BLO-38068), not this branch.
1923
+ const amtRelay = config.amtRelay ?? config.relayAddress;
1924
+ const transportConfig = {
1925
+ sourceAddress: config.sourceAddress,
1926
+ groupAddress: config.groupAddress,
1927
+ port: config.port,
1928
+ mode: config.mode,
1929
+ amtRelay,
1930
+ // A relay here is a commitment to that relay, not a hint: a relay with no
1931
+ // port does NOT fall back to DRIAD, it joins on RFC 7450's 2268.
1932
+ // `toWindowMulticastSubscribeConfig` (packages/transport
1933
+ // endpoint-config.ts) deliberately has no implicit relay port — it throws
1934
+ // and `MulticastTransport.join` resolves false — so the default is
1935
+ // resolved HERE and arrives there explicit, exactly as the two catalog
1936
+ // readers do it. This is the last of the three readers: the `relayAddress`
1937
+ // half cannot supply a port at all (the pre-migration wire shape has no
1938
+ // port field), and `resolveNetworkSource` emits the same port-less shape.
1939
+ // Keyed on the resolved relay, so a DRIAD entry — which pins no relay —
1940
+ // never has a port manufactured for it. Blank counts as no relay, the
1941
+ // same test `catalogAmtNetworkSource` applies (it trims, then skips) and
1942
+ // the same one `toWindowMulticastSubscribeConfig` re-derives downstream
1943
+ // (`input.amtRelay?.trim()`): a port paired with a relay that consumer
1944
+ // reads as absent is a shape no reader here should manufacture. Only the
1945
+ // default is gated — an explicit `amtRelayPort` is caller data and passes
1946
+ // through whatever the relay looks like, since dropping it would be the
1947
+ // worse behaviour.
1948
+ amtRelayPort:
1949
+ config.amtRelayPort ??
1950
+ config.relayPort ??
1951
+ (amtRelay?.trim() ? DEFAULT_AMT_RELAY_PORT : undefined),
1952
+ packetIds: config.packetIds,
1953
+ tracks: catalogTracks?.map(({ name, packetId }) => ({ name, packetId })),
1954
+ };
1955
+ return ingress === undefined
1956
+ ? this.#transport.join(transportConfig)
1957
+ : this.#transport.join(transportConfig, ingress);
1958
+ }
1959
+
1960
+ // ---------------------------------------------------------------------------
1961
+ // In-place rendition switch
1962
+ // ---------------------------------------------------------------------------
1963
+
1964
+ /**
1965
+ * Switch the live join from one video rendition to another in place: the
1966
+ * subscription (socket, group membership, AMT tunnel) is kept, and the
1967
+ * packet-ID remap commits atomically at a RAP of the incoming rendition.
1968
+ *
1969
+ * Staging widens the admission to carry both renditions: the ingress
1970
+ * tracks become the live ones plus `tracks`, and, when `packetIds` is
1971
+ * given, the allow-list becomes the live one plus `packetIds` (a provider
1972
+ * that cannot widen in place is refused here, before anything changes).
1973
+ * The incoming rendition's packets then feed its FEC and parser while the
1974
+ * outgoing rendition keeps playing, and MmtpRouter.stageStreamSwitch
1975
+ * withholds every incoming frame and its init until the commit.
1976
+ *
1977
+ * The commit is one synchronous step inside the frame path, at the
1978
+ * incoming RAP (see MmtpRouter.stageStreamSwitch for where exactly): the
1979
+ * outgoing packet ID is retired from the router, the admission becomes
1980
+ * exactly `tracks` (and `packetIds`), the outgoing stream's FEC and repair
1981
+ * state is released (FECManager.releasePacket; repair fetches in flight are
1982
+ * aborted through the track-reset observers), `onCommit` runs, and then the
1983
+ * incoming init and RAP are delivered. No outgoing frame is delivered after
1984
+ * it and no incoming frame before it. No timer is armed: the switch waits
1985
+ * on RAPs and on the outgoing stream's own frames.
1986
+ *
1987
+ * Exactly one of `onCommit` and `onCancel` is called per switch. One switch
1988
+ * may be staged at a time.
1989
+ */
1990
+ switchRendition(request: RenditionSwitchRequest): RenditionSwitch {
1991
+ if (this.#renditionSwitch !== null) {
1992
+ throw new Error(
1993
+ "[MulticastManager] a rendition switch is already staged; cancel it before staging another",
1994
+ );
1995
+ }
1996
+ if (typeof request?.onCommit !== "function" || typeof request.onCancel !== "function") {
1997
+ throw new Error("[MulticastManager] switchRendition requires onCommit and onCancel callbacks");
1998
+ }
1999
+ const live = this.#transport.context;
2000
+ if (!this.#transport.joined || live === undefined) {
2001
+ throw new Error(
2002
+ "[MulticastManager] switchRendition needs a live join with catalog ingress tracks: " +
2003
+ "join() with packetIds or tracks first",
2004
+ );
2005
+ }
2006
+ const { from, to } = request;
2007
+ if (!Array.isArray(request.tracks)) {
2008
+ throw new TypeError("[MulticastManager] switchRendition tracks must be an array");
2009
+ }
2010
+ for (const track of request.tracks) {
2011
+ if (
2012
+ typeof track?.name !== "string" ||
2013
+ track.name.length === 0 ||
2014
+ !isMulticastTrackPacketId(track.packetId)
2015
+ ) {
2016
+ throw new RangeError(
2017
+ "[MulticastManager] switchRendition tracks need a non-empty catalog name and a track packet ID each",
2018
+ );
2019
+ }
2020
+ }
2021
+ const holds = (
2022
+ tracks: MmtpIngressContext["tracks"],
2023
+ stream: MmtpStreamIdentity | undefined,
2024
+ ): boolean =>
2025
+ stream !== undefined &&
2026
+ tracks.some((track) => track.name === stream.catalogTrackName && track.packetId === stream.packetId);
2027
+ if (!holds(live.tracks, from)) {
2028
+ throw new Error(
2029
+ `[MulticastManager] switchRendition: from (${from?.catalogTrackName} on ${from?.packetId}) ` +
2030
+ "is not an ingress track of the live join",
2031
+ );
2032
+ }
2033
+ if (to === undefined || live.tracks.some((track) => track.packetId === to.packetId)) {
2034
+ throw new Error(
2035
+ `[MulticastManager] switchRendition: to's packet ID ${to?.packetId} is already admitted ` +
2036
+ "by the live join's ingress; an in-place switch admits a rendition that is not playing",
2037
+ );
2038
+ }
2039
+ if (!holds(request.tracks, to)) {
2040
+ throw new Error("[MulticastManager] switchRendition: tracks must carry to");
2041
+ }
2042
+ if (request.tracks.some((track) => track.packetId === from.packetId)) {
2043
+ throw new Error(
2044
+ `[MulticastManager] switchRendition: tracks must not carry from's packet ID ${from.packetId}; ` +
2045
+ "the commit retires it",
2046
+ );
2047
+ }
2048
+ // While staged, the ingress is the live tracks plus these. One packet ID
2049
+ // under two names is ambiguous to the router, which drops it, so a track
2050
+ // must keep the name the live join gave its packet ID.
2051
+ const renamed = request.tracks.find((track) =>
2052
+ live.tracks.some((liveTrack) => liveTrack.packetId === track.packetId && liveTrack.name !== track.name));
2053
+ if (renamed !== undefined) {
2054
+ throw new Error(
2055
+ `[MulticastManager] switchRendition: tracks name packet ID ${renamed.packetId} ` +
2056
+ `"${renamed.name}", but the live join admits it under another name`,
2057
+ );
2058
+ }
2059
+ const livePacketIds = this.#transport.packetIds;
2060
+ let committedPacketIds: readonly number[] | undefined;
2061
+ if (request.packetIds !== undefined) {
2062
+ if (
2063
+ !Array.isArray(request.packetIds) ||
2064
+ request.packetIds.length === 0 ||
2065
+ !request.packetIds.every(isMulticastTrackPacketId)
2066
+ ) {
2067
+ throw new RangeError(
2068
+ "[MulticastManager] switchRendition packetIds must be a non-empty list of track packet IDs",
2069
+ );
2070
+ }
2071
+ if (!request.packetIds.includes(to.packetId)) {
2072
+ throw new Error("[MulticastManager] switchRendition: packetIds must admit to");
2073
+ }
2074
+ committedPacketIds = Object.freeze([...new Set(request.packetIds)]);
2075
+ } else if (livePacketIds !== null && !livePacketIds.has(to.packetId)) {
2076
+ throw new Error(
2077
+ `[MulticastManager] switchRendition: the live allow-list does not admit to (${to.packetId}); ` +
2078
+ "pass the post-switch packetIds",
2079
+ );
2080
+ }
2081
+
2082
+ const stagedTracks = new Map<string, Readonly<{ name: string; packetId: number }>>();
2083
+ for (const track of [...live.tracks, ...request.tracks]) {
2084
+ stagedTracks.set(`${track.packetId}\0${track.name}`, track);
2085
+ }
2086
+ const stagedIngress = freezeIngress(stagedTracks.values());
2087
+ const stagedPacketIds =
2088
+ committedPacketIds === undefined || livePacketIds === null
2089
+ ? null
2090
+ : new Set([...livePacketIds, ...committedPacketIds]);
2091
+ const widened = stagedPacketIds !== null && !sameIdSet(stagedPacketIds, livePacketIds!);
2092
+
2093
+ const active: ActiveRenditionSwitch = {
2094
+ from: Object.freeze({ packetId: from.packetId, catalogTrackName: from.catalogTrackName }),
2095
+ to: Object.freeze({ packetId: to.packetId, catalogTrackName: to.catalogTrackName }),
2096
+ onCommit: request.onCommit,
2097
+ onCancel: request.onCancel,
2098
+ liveIngress: live,
2099
+ livePacketIds,
2100
+ widened,
2101
+ committedIngress: freezeIngress(request.tracks),
2102
+ committedPacketIds,
2103
+ router: null,
2104
+ state: "staged",
2105
+ };
2106
+ // Throws on an unregistered or non-video stream before any admission changes.
2107
+ active.router = this.#router.stageStreamSwitch({
2108
+ from: active.from,
2109
+ to: active.to,
2110
+ onCommit: (commit) => this.#commitRenditionSwitch(active, commit),
2111
+ });
2112
+ let widening: Promise<void> | undefined;
2113
+ try {
2114
+ if (widened) widening = this.#transport.setPacketIds([...stagedPacketIds!]);
2115
+ this.#transport.setContext(stagedIngress);
2116
+ } catch (error) {
2117
+ active.router.cancel();
2118
+ throw error;
2119
+ }
2120
+ this.#renditionSwitch = active;
2121
+ widening?.catch((error: unknown) => {
2122
+ if (this.#renditionSwitch !== active) return;
2123
+ this.#endRenditionSwitch(active, { reason: "provider-refused", error }, true);
2124
+ });
2125
+ return {
2126
+ get state() {
2127
+ return active.state;
2128
+ },
2129
+ cancel: () => {
2130
+ if (this.#renditionSwitch !== active) return false;
2131
+ this.#endRenditionSwitch(active, { reason: "cancelled" }, true);
2132
+ return true;
2133
+ },
2134
+ };
2135
+ }
2136
+
2137
+ /** The router committed the staged switch (it already retired `from`). */
2138
+ #commitRenditionSwitch(active: ActiveRenditionSwitch, commit: StreamSwitchCommit): void {
2139
+ if (this.#renditionSwitch !== active) return;
2140
+ this.#renditionSwitch = null;
2141
+ active.state = "committed";
2142
+ active.router = null;
2143
+ if (this.#transport.joined) {
2144
+ this.#transport.setContext(active.committedIngress);
2145
+ if (active.committedPacketIds !== undefined) {
2146
+ // The page-side filter narrows now; the provider may keep delivering
2147
+ // the retired IDs until it applies the narrower list, and the page
2148
+ // drops them meanwhile.
2149
+ this.#transport.setPacketIds(active.committedPacketIds).catch((error: unknown) => {
2150
+ console.warn(
2151
+ "[MulticastManager] the provider did not narrow its packet filter after a rendition switch; " +
2152
+ "the page-side filter drops the retired packet IDs",
2153
+ error,
2154
+ );
2155
+ });
2156
+ }
2157
+ }
2158
+ this.#releaseRetiredStream(commit.from);
2159
+ try {
2160
+ active.onCommit(commit);
2161
+ } catch (error) {
2162
+ // The caller could not take `to`. Report it, release what the manager
2163
+ // holds for `to`, and rethrow so the router retires `to` instead of
2164
+ // feeding it to a decoder still configured for `from`.
2165
+ active.state = "failed";
2166
+ this.#releaseRetiredStream(active.to);
2167
+ try {
2168
+ active.onCancel({ reason: "commit-failed", error });
2169
+ } catch (cancelError) {
2170
+ console.warn("[MulticastManager] rendition switch onCancel failed", cancelError);
2171
+ }
2172
+ throw error;
2173
+ }
2174
+ }
2175
+
2176
+ /**
2177
+ * End a staged switch without committing. `restore` puts the pre-switch
2178
+ * admission back and retires `to`; a join or dispose supersedes both.
2179
+ */
2180
+ #endRenditionSwitch(
2181
+ active: ActiveRenditionSwitch,
2182
+ cancel: RenditionSwitchCancel,
2183
+ restore: boolean,
2184
+ ): void {
2185
+ if (this.#renditionSwitch !== active) return;
2186
+ this.#renditionSwitch = null;
2187
+ active.state = "cancelled";
2188
+ active.router?.cancel();
2189
+ active.router = null;
2190
+ if (restore) {
2191
+ if (this.#transport.joined) {
2192
+ this.#transport.setContext(active.liveIngress);
2193
+ if (active.widened && active.livePacketIds !== null) {
2194
+ this.#transport.setPacketIds([...active.livePacketIds]).catch((error: unknown) => {
2195
+ console.warn(
2196
+ "[MulticastManager] the provider did not narrow its packet filter after a cancelled " +
2197
+ "rendition switch; the page-side filter drops the incoming packet IDs",
2198
+ error,
2199
+ );
2200
+ });
2201
+ }
2202
+ }
2203
+ this.#router.retirePacketId(active.to.packetId);
2204
+ this.#releaseRetiredStream(active.to);
2205
+ }
2206
+ try {
2207
+ active.onCancel(cancel);
2208
+ } catch (error) {
2209
+ console.warn("[MulticastManager] rendition switch onCancel failed", error);
2210
+ }
2211
+ }
2212
+
2213
+ /** Release what the manager and FEC hold for a stream whose packet ID the router retired. */
2214
+ #releaseRetiredStream(stream: MmtpStreamIdentity): void {
2215
+ // Repair transports abort I/O for a stream on a reset notice
2216
+ // (wireRepairFetch); a retired stream's repair fetches end with it.
2217
+ for (const observer of this.#trackResetObservers) {
2218
+ try {
2219
+ observer(stream.packetId, stream.catalogTrackName);
2220
+ } catch (error) {
2221
+ console.warn("[MulticastManager] track reset observer failed", error);
2222
+ }
2223
+ }
2224
+ for (const key of [...this.#repairInFlight]) {
2225
+ try {
2226
+ const [keyPacketId] = JSON.parse(key);
2227
+ if (keyPacketId === stream.packetId) this.#repairInFlight.delete(key);
2228
+ } catch {
2229
+ // Internal keys are JSON tuples; preserve unknown legacy entries.
2230
+ }
2231
+ }
2232
+ for (const [key, stats] of [...this.#blockPolicyStats]) {
2233
+ if (stats.packetId === stream.packetId) this.#blockPolicyStats.delete(key);
2234
+ }
2235
+ // The retired track stops contributing to the stream-global FEC timing;
2236
+ // the deadlines already applied stay until the next configuration
2237
+ // re-reduces them, which only ever errs toward the wider window.
2238
+ this.#trackFecTiming.delete(stream.packetId);
2239
+ this.#fec.releasePacket(stream.packetId);
2240
+ }
2241
+
2242
+ /** Feed a raw packet from an external source (AMT tunnel, ASM subscription).
2243
+ *
2244
+ * For MoQ-unicast-fallback mode (no SSM bridge), call
2245
+ * `manager.router.feedPacket(packet, "moq")` directly to bypass
2246
+ * MulticastTransport's takeover/watchdog/failback gates while preserving
2247
+ * transport attribution in FEC path counters.
2248
+ * The `router` accessor above is already public.
2249
+ */
2250
+ feedPacket(data: ArrayBuffer | Uint8Array, ingress?: MmtpIngressContext) {
2251
+ this.#transport.feedPacket(data, ingress);
2252
+ }
2253
+
2254
+ /**
2255
+ * Declare how packets given to feedPacket() reach this manager. A consumer
2256
+ * that owns the multicast socket itself declares its own leg; join()
2257
+ * applies what the window.multicast subscription declares instead.
2258
+ */
2259
+ setMulticastSymbolDelivery(delivery: MulticastSymbolDelivery): void {
2260
+ this.#fec.setMulticastSymbolDelivery(delivery);
2261
+ }
2262
+
2263
+ /** Process a repair packet from MoQ (reliable QUIC). */
2264
+ processRepairFromMoQ(
2265
+ packet: Uint8Array,
2266
+ ownerPacketId?: number,
2267
+ catalogTrackName?: string,
2268
+ ) {
2269
+ this.#router.feedRepairPacket(packet, ownerPacketId, catalogTrackName);
2270
+ }
2271
+
2272
+ /** Route a Worker-recovered FEC block through the MMTP parser + dedup pipeline. */
2273
+ routeRecoveredBlock(
2274
+ blockId: number,
2275
+ data: Uint8Array,
2276
+ packetId: number,
2277
+ catalogTrackName?: string,
2278
+ ) {
2279
+ this.#router.routeRecoveredBlock(blockId, data, packetId, catalogTrackName);
2280
+ }
2281
+
2282
+ /** Route one Worker-recovered source symbol through the MMTP parser + dedup pipeline. */
2283
+ routeRecoveredSymbol(
2284
+ blockId: number,
2285
+ data: Uint8Array,
2286
+ packetId: number,
2287
+ esi = 0,
2288
+ catalogTrackName?: string,
2289
+ ) {
2290
+ this.#router.routeRecoveredSymbol(blockId, data, packetId, esi, catalogTrackName);
2291
+ }
2292
+
2293
+ // ---------------------------------------------------------------------------
2294
+ // Phase 14: multi-track catalog ingest (D-04, D-06, D-07, D-17, D-26, D-28)
2295
+ // ---------------------------------------------------------------------------
2296
+
2297
+ /**
2298
+ * Configure the manager for a multi-track (base/delta/repair) CMAF switching set.
2299
+ *
2300
+ * D-04: Validates completeness via validateSwitchingSet — throws if incomplete.
2301
+ * D-07: Applies per-track FEC configs (base+delta get decoders; repair does not).
2302
+ * D-17: Builds altGroup → track-name index for getSwitchingSet().
2303
+ * D-26: Registers packetId → TrackRole on MmtpRouter for block-complete fanout.
2304
+ *
2305
+ * @param catalog - Multi-track catalog (tracks array with name, altGroup, fec, packetId).
2306
+ * @param wasmDecoderSource - One WASM FEC decoder for a single FEC stream, or
2307
+ * a factory returning a distinct decoder per stream.
2308
+ */
2309
+ configureMultiTrack(
2310
+ catalog: MultiTrackCatalog,
2311
+ wasmDecoderSource?: WasmFecDecoder | WasmFecDecoderFactory,
2312
+ ): void {
2313
+ // D-06: Resolve explicit fec.repairTrack pointers FIRST. This allows
2314
+ // publisher-chosen repair track names (e.g. "video/repair-alt") that don't
2315
+ // match the standard "/repair" suffix convention. The explicit pointer wins
2316
+ // over the naming convention: a track pointed to by repairTrack is always
2317
+ // 'repair'. The pointer lives on the owner, and a repair leg is a raw
2318
+ // application/raptorq track the publisher never tags with an altGroup, so
2319
+ // the leg belongs to its owner's group regardless of what the leg itself
2320
+ // declares.
2321
+ //
2322
+ // Layers 1..n and a keyframe overlay are named only by their own `depends`
2323
+ // (draft-ramadan-moq-fec §6.3), so they are resolved here too. A layer that
2324
+ // did not reach this map would fall through to parseTrackRole and be
2325
+ // mis-roled or rejected on its name, and would land in its own altGroup
2326
+ // rather than its owner's.
2327
+ const repairNamesBySource = repairTrackNamesBySource(catalog.tracks);
2328
+ const repairOwnerGroup = new Map<string, string>();
2329
+ const repairOwnerAltGroup = new Map<string, number | undefined>();
2330
+ for (const track of catalog.tracks) {
2331
+ for (const repairName of repairNamesBySource.get(track.name) ?? []) {
2332
+ repairOwnerGroup.set(repairName, altGroupKey(track));
2333
+ repairOwnerAltGroup.set(repairName, track.altGroup);
2334
+ }
2335
+ }
2336
+ const groupKeyFor = (track: MultiTrackCatalogTrack): string =>
2337
+ repairOwnerGroup.get(track.name) ?? altGroupKey(track);
2338
+
2339
+ const trackNamesByGroup = new Map<string, string[]>();
2340
+ for (const track of catalog.tracks) {
2341
+ const groupKey = groupKeyFor(track);
2342
+ const groupNames = trackNamesByGroup.get(groupKey) ?? [];
2343
+ groupNames.push(track.name);
2344
+ trackNamesByGroup.set(groupKey, groupNames);
2345
+ }
2346
+
2347
+ const hasSwitchingSetByGroup = new Map<string, boolean>();
2348
+ for (const [groupKey, trackNames] of trackNamesByGroup) {
2349
+ hasSwitchingSetByGroup.set(groupKey, hasSwitchingSetRole(trackNames));
2350
+ }
2351
+
2352
+ // Resolve role for each track — explicit pointer wins over naming convention.
2353
+ // Normalize non-standard repair names to 'repair' before calling validateSwitchingSet,
2354
+ // which relies on parseTrackRole and would throw on custom repair-track names.
2355
+ const resolvedRoles: TrackRole[] = catalog.tracks.map((track) => {
2356
+ if (repairOwnerGroup.has(track.name)) return "repair";
2357
+ const hasSwitchingSet = hasSwitchingSetByGroup.get(altGroupKey(track)) ?? false;
2358
+ if (!hasSwitchingSet) return "single";
2359
+ return parseTrackRole(track.name);
2360
+ });
2361
+
2362
+ // D-04: Validate completeness per altGroup using resolved roles.
2363
+ // We replicate validateSwitchingSet logic here to avoid re-calling parseTrackRole
2364
+ // (which would throw on explicit-pointer repair track names like "video/repair-alt").
2365
+ const resolvedRolesByGroup = new Map<string, TrackRole[]>();
2366
+ for (let i = 0; i < catalog.tracks.length; i++) {
2367
+ const groupKey = groupKeyFor(catalog.tracks[i]!);
2368
+ const roles = resolvedRolesByGroup.get(groupKey) ?? [];
2369
+ roles.push(resolvedRoles[i]!);
2370
+ resolvedRolesByGroup.set(groupKey, roles);
2371
+ }
2372
+ for (const [groupKey, roles] of resolvedRolesByGroup) {
2373
+ if (!(hasSwitchingSetByGroup.get(groupKey) ?? false)) continue;
2374
+
2375
+ const presentRoles = new Set(roles.filter((r) => r !== "single"));
2376
+ const required: Array<"base" | "delta" | "repair"> = [
2377
+ "base",
2378
+ "delta",
2379
+ "repair",
2380
+ ];
2381
+ const missing = required.filter((r) => !presentRoles.has(r));
2382
+ if (missing.length > 0) {
2383
+ throw new Error(
2384
+ `[MulticastManager] incomplete switching set: missing ${missing.join(", ")}`,
2385
+ );
2386
+ }
2387
+ }
2388
+
2389
+ const fecTrackIndexes: number[] = [];
2390
+ for (let i = 0; i < catalog.tracks.length; i++) {
2391
+ const track = catalog.tracks[i]!;
2392
+ if (resolvedRoles[i] === "repair" || track.fec === undefined) continue;
2393
+ if (!isFecCatalogConfig(track.fec)) {
2394
+ throw new Error(
2395
+ `[MulticastManager] invalid fec config for track '${track.name}'`,
2396
+ );
2397
+ }
2398
+ if (track.packetId !== undefined) fecTrackIndexes.push(i);
2399
+ }
2400
+
2401
+ // Stateful decoders cannot span physical streams: resetting one stream
2402
+ // would otherwise clear or disable every peer sharing that instance.
2403
+ const fecDecoders = new Map<number, WasmFecDecoder>();
2404
+ if (wasmDecoderSource !== undefined && fecTrackIndexes.length > 0) {
2405
+ if (typeof wasmDecoderSource !== "function") {
2406
+ if (fecTrackIndexes.length > 1) {
2407
+ throw new Error(
2408
+ "[MulticastManager] multiple FEC streams require a decoder factory",
2409
+ );
2410
+ }
2411
+ fecDecoders.set(fecTrackIndexes[0]!, wasmDecoderSource);
2412
+ } else {
2413
+ const clients = new Set<WasmFecDecoder>();
2414
+ for (const index of fecTrackIndexes) {
2415
+ const client = wasmDecoderSource(catalog.tracks[index]!);
2416
+ if (clients.has(client)) {
2417
+ throw new Error(
2418
+ "[MulticastManager] decoder factory must return one client per physical stream",
2419
+ );
2420
+ }
2421
+ clients.add(client);
2422
+ fecDecoders.set(index, client);
2423
+ }
2424
+ }
2425
+ }
2426
+
2427
+ // Reset switching-set index and repair packetId set.
2428
+ this.#switchingSets.clear();
2429
+ this.#repairPacketIds.clear();
2430
+ this.#router.clearRepairPacketIdOwners();
2431
+ const catalogTracksByName = new Map(
2432
+ catalog.tracks.map((track) => [track.name, track] as const),
2433
+ );
2434
+
2435
+ for (let i = 0; i < catalog.tracks.length; i++) {
2436
+ const track = catalog.tracks[i]!;
2437
+ const role = resolvedRoles[i]!;
2438
+
2439
+ // D-17: Build altGroup → track-name index. A repair leg belongs to the
2440
+ // switching set of the owner that points at it.
2441
+ const altGroup = repairOwnerAltGroup.has(track.name)
2442
+ ? repairOwnerAltGroup.get(track.name)
2443
+ : track.altGroup;
2444
+ if (altGroup !== undefined) {
2445
+ const existing = this.#switchingSets.get(altGroup) ?? [];
2446
+ existing.push(track.name);
2447
+ this.#switchingSets.set(altGroup, existing);
2448
+ }
2449
+
2450
+ // D-26: Register packetId → role on MmtpRouter (if packetId provided at ingest time).
2451
+ if (track.packetId !== undefined) {
2452
+ this.#router.setPacketIdRole(track.packetId, role);
2453
+ }
2454
+
2455
+ if (role === "repair") {
2456
+ // D-06: Repair track is the repair source — no FEC decoder registered on it.
2457
+ if (track.packetId !== undefined) {
2458
+ this.#repairPacketIds.add(track.packetId);
2459
+ }
2460
+ continue;
2461
+ }
2462
+
2463
+ // D-07: Apply per-track FEC config for non-repair tracks.
2464
+ if (track.fec !== undefined) {
2465
+ const fecCfg = track.fec as FecCatalogConfig;
2466
+ // Every repair layer and the keyframe overlay route to this source's
2467
+ // decoder, not only layer 0. A layer whose packetId had no registered
2468
+ // owner would have its repair symbols dropped as an unknown owner, and
2469
+ // two sources whose RS_ID ranges overlap would be indistinguishable.
2470
+ if (track.packetId !== undefined) {
2471
+ for (const repairName of repairNamesBySource.get(track.name) ?? []) {
2472
+ const repairTrack = catalogTracksByName.get(repairName);
2473
+ if (repairTrack?.packetId !== undefined) {
2474
+ this.#router.setRepairPacketIdOwner(repairTrack.packetId, track.packetId);
2475
+ }
2476
+ }
2477
+ }
2478
+ const wasmDecoder = fecDecoders.get(i);
2479
+ if (track.packetId !== undefined && wasmDecoder !== undefined) {
2480
+ // trackType: base/delta/single video tracks are 'video'; audio would be 'audio'.
2481
+ // At this layer we treat all content tracks as 'video' — audio tracks typically
2482
+ // have no FEC in Phase 14 (D-26 covers video base+delta only). Callers that need
2483
+ // audio FEC can call setFecDecoder() directly after configureMultiTrack().
2484
+ const blockProfile = parseFecBlockProfile(
2485
+ (fecCfg as { blockProfile?: unknown }).blockProfile,
2486
+ `tracks[${JSON.stringify(track.name)}].fec.blockProfile`,
2487
+ );
2488
+ this.#fec.setFecDecoder(
2489
+ track.packetId,
2490
+ wasmDecoder,
2491
+ fecCfg.sourceSymbols,
2492
+ {
2493
+ blockProfile,
2494
+ k: fecCfg.sourceSymbols,
2495
+ symbolSize: fecCfg.symbolSize,
2496
+ interleaveDepthMs: fecCfg.interleaveDepthMs,
2497
+ fps: track.framerate,
2498
+ trackKind: "video",
2499
+ },
2500
+ track.name,
2501
+ );
2502
+ this.#configureTrackMapper(track.packetId, {
2503
+ blockProfile,
2504
+ k: fecCfg.sourceSymbols,
2505
+ symbolSize: fecCfg.symbolSize,
2506
+ interleaveDepthMs: fecCfg.interleaveDepthMs,
2507
+ fps: track.framerate,
2508
+ });
2509
+ this.#router.setPacketIdType(track.packetId, "video");
2510
+ }
2511
+ }
2512
+ }
2513
+ }
2514
+
2515
+ /**
2516
+ * Return the track names belonging to the given altGroup switching set.
2517
+ * D-17: altGroup groups base+delta+repair into one switching set.
2518
+ *
2519
+ * @param altGroup - The altGroup number to look up.
2520
+ * @returns Array of track names in that switching set (empty if none).
2521
+ */
2522
+ getSwitchingSet(altGroup: number): string[] {
2523
+ return this.#switchingSets.get(altGroup) ?? [];
2524
+ }
2525
+
2526
+ /**
2527
+ * Register callback fired when a repair fetch should be issued for an FEC block.
2528
+ *
2529
+ * D-28: At most one outstanding repair fetch per (packetId, sbn) block.
2530
+ * clearRepairForBlock fires automatically when the block resolves (via
2531
+ * FECManager.onRepairResolved); call it manually only on fetch failure
2532
+ * paths the FEC layer cannot observe (fetch error, range not retained,
2533
+ * transport timeout) so a stuck fetch cannot wedge the block's slot.
2534
+ *
2535
+ * Registering also arms the FEC repair deadline: each block gets a one-shot
2536
+ * timer for the derived K-symbol source-production horizon; a block still
2537
+ * short of K when it fires requests repair here. Failure accounting timing
2538
+ * is unchanged.
2539
+ */
2540
+ onRepairSubscribe(cb: RepairSubscribeCallback): void {
2541
+ this.#onRepairSubscribeCb = cb;
2542
+ this.#fec.onRepairNeeded = (sbn, packetId, catalogTrackName, blockClass) => {
2543
+ this.requestRepairForBlock(sbn, packetId, catalogTrackName, blockClass);
2544
+ };
2545
+ }
2546
+
2547
+ /**
2548
+ * Record a repair request whose SUBSCRIBER_PRIORITY the catalog could not
2549
+ * supply (draft-ramadan-moq-fec §11.4.5): sent without the parameter, or
2550
+ * not sent. Emits one `repair-priority` lifecycle event.
2551
+ */
2552
+ noteRepairPriority(
2553
+ action: "omitted" | "declined",
2554
+ reason: RepairPriorityReason,
2555
+ blockClass: BlockClass,
2556
+ sbn: number,
2557
+ packetId: number,
2558
+ catalogTrackName?: string,
2559
+ ): void {
2560
+ this.#emitRepairLifecycle({
2561
+ version: 2,
2562
+ type: "repair-priority",
2563
+ block: repairBlockIdentity(packetId, sbn, catalogTrackName),
2564
+ blockClass,
2565
+ action,
2566
+ reason,
2567
+ });
2568
+ }
2569
+
2570
+ /** Record a repair request the scheduler sent no FETCH for. */
2571
+ noteRepairSkipped(
2572
+ reason: RepairSkipReason,
2573
+ measurement: { readonly rttMs: number | null; readonly budgetMs: number },
2574
+ sbn: number,
2575
+ packetId: number,
2576
+ catalogTrackName?: string,
2577
+ ): void {
2578
+ this.#statsFor(packetId, catalogTrackName).repairsSkipped++;
2579
+ this.#emitRepairLifecycle({
2580
+ version: 2,
2581
+ type: "repair-skipped",
2582
+ block: repairBlockIdentity(packetId, sbn, catalogTrackName),
2583
+ reason,
2584
+ rttMs: measurement.rttMs,
2585
+ budgetMs: measurement.budgetMs,
2586
+ });
2587
+ }
2588
+
2589
+ /**
2590
+ * Record a repair-scheduler decision about a delta block: preempted for
2591
+ * keyframe/audio work, or never dispatched before its block resolved.
2592
+ */
2593
+ noteRepairScheduling(
2594
+ type: "repair-preempted" | "repair-deferred",
2595
+ sbn: number,
2596
+ packetId: number,
2597
+ catalogTrackName?: string,
2598
+ ): void {
2599
+ const stats = this.#statsFor(packetId, catalogTrackName);
2600
+ if (type === "repair-preempted") stats.deltaRepairsPreempted++;
2601
+ else stats.deltaRepairsDeferred++;
2602
+ this.#emitRepairLifecycle({
2603
+ version: 2,
2604
+ type,
2605
+ block: repairBlockIdentity(packetId, sbn, catalogTrackName),
2606
+ });
2607
+ }
2608
+
2609
+ /** Catalog/reset epoch of a packet's streams; gap decisions carry it. */
2610
+ epochFor(packetId: number): number {
2611
+ return this.#fec.epochFor(packetId);
2612
+ }
2613
+
2614
+ /**
2615
+ * Keyframe-protected repair policy counters, keyed by catalog track name
2616
+ * (`packetId:<n>` for a stream with no catalog name). Each track's
2617
+ * counters reset with its epoch.
2618
+ */
2619
+ getBlockPolicyStats(): Record<string, BlockPolicyStats> {
2620
+ const out: Record<string, BlockPolicyStats> = {};
2621
+ for (const [key, stats] of this.#blockPolicyStats) {
2622
+ if (stats.epoch !== this.#fec.epochFor(stats.packetId)) continue;
2623
+ out[key] = { ...stats };
2624
+ }
2625
+ return out;
2626
+ }
2627
+
2628
+ #statsFor(packetId: number, catalogTrackName?: string): BlockPolicyStats {
2629
+ const key = catalogTrackName ?? `packetId:${packetId}`;
2630
+ const epoch = this.#fec.epochFor(packetId);
2631
+ let stats = this.#blockPolicyStats.get(key);
2632
+ if (!stats || stats.epoch !== epoch || stats.packetId !== packetId) {
2633
+ stats = newBlockPolicyStats(packetId, catalogTrackName ?? null, epoch);
2634
+ this.#blockPolicyStats.set(key, stats);
2635
+ }
2636
+ return stats;
2637
+ }
2638
+
2639
+ /**
2640
+ * Observe causal repair lifecycle events without retaining snapshots in the
2641
+ * transport. Returns an unsubscribe function.
2642
+ */
2643
+ addRepairLifecycleObserver(observer: RepairLifecycleObserver): () => void {
2644
+ this.#repairLifecycleObservers.add(observer);
2645
+ return () => this.#repairLifecycleObservers.delete(observer);
2646
+ }
2647
+
2648
+ /** Observe per-stream epoch resets (used by repair transports to abort I/O). */
2649
+ addTrackResetObserver(
2650
+ observer: (packetId: number, catalogTrackName: string) => void,
2651
+ ): () => void {
2652
+ this.#trackResetObservers.add(observer);
2653
+ return () => this.#trackResetObservers.delete(observer);
2654
+ }
2655
+
2656
+ /** Begin a fresh retained-replay epoch for one physical media stream. */
2657
+ resetTrack(packetId: number, catalogTrackName: string): void {
2658
+ if (!catalogTrackName) {
2659
+ throw new Error('[MulticastManager] catalogTrackName is required for track reset');
2660
+ }
2661
+ this.#resetStreamOutsideRouter(packetId, catalogTrackName);
2662
+ this.#router.resetTrack(packetId, catalogTrackName);
2663
+ }
2664
+
2665
+ /** resetTrack() for everything but the router's own state. */
2666
+ #resetStreamOutsideRouter(packetId: number, catalogTrackName: string): void {
2667
+ for (const observer of this.#trackResetObservers) {
2668
+ try {
2669
+ observer(packetId, catalogTrackName);
2670
+ } catch (error) {
2671
+ console.warn('[MulticastManager] track reset observer failed', error);
2672
+ }
2673
+ }
2674
+ for (const key of this.#repairInFlight) {
2675
+ try {
2676
+ const [keyPacketId, keyTrackName] = JSON.parse(key);
2677
+ if (keyPacketId === packetId && keyTrackName === catalogTrackName) {
2678
+ this.#repairInFlight.delete(key);
2679
+ }
2680
+ } catch {
2681
+ // Internal keys are JSON tuples; preserve unknown legacy entries.
2682
+ }
2683
+ }
2684
+ this.#fec.resetTrack(packetId, catalogTrackName);
2685
+ }
2686
+
2687
+ /**
2688
+ * Request a repair-track fetch for the given FEC block.
2689
+ *
2690
+ * D-28: duplicate requests for the same (packetId, sbn) are suppressed
2691
+ * until clearRepairForBlock is called.
2692
+ *
2693
+ * @param sbn - Source Block Number identifying the FEC block needing repair.
2694
+ * @param packetId - MMTP packet_id owning the block (video/audio tracks have
2695
+ * independent SBN spaces, so the in-flight key must be composite).
2696
+ */
2697
+ requestRepairForBlock(
2698
+ sbn: number,
2699
+ packetId: number,
2700
+ catalogTrackName: string | undefined,
2701
+ blockClass: BlockClass,
2702
+ ): void {
2703
+ // D-28: at most one outstanding fetch per FEC block (per packetId:sbn).
2704
+ // Different blocks are independent and are NOT blocked by each other.
2705
+ const block = repairBlockIdentity(packetId, sbn, catalogTrackName);
2706
+ const key = JSON.stringify([packetId, block.catalogTrackName, sbn]);
2707
+ if (this.#repairInFlight.has(key)) return;
2708
+ this.#repairInFlight.add(key);
2709
+ const stats = this.#statsFor(packetId, catalogTrackName);
2710
+ if (blockClass === "keyframe") stats.keyframeBlocksProtected++;
2711
+ else if (blockClass === "unknown") stats.unknownBlocksProtected++;
2712
+ else if (blockClass === "delta") stats.deltaBlocksShort++;
2713
+ this.#emitRepairLifecycle({
2714
+ version: 2,
2715
+ type: "requested",
2716
+ block,
2717
+ blockClass,
2718
+ priority: repairPriorityForClass(blockClass),
2719
+ });
2720
+ this.#onRepairSubscribeCb?.(sbn, packetId, catalogTrackName, blockClass);
2721
+ }
2722
+
2723
+ /**
2724
+ * Clear in-flight repair state for the given block.
2725
+ * Fired automatically when the block resolves; safe to call redundantly.
2726
+ */
2727
+ clearRepairForBlock(
2728
+ sbn: number,
2729
+ packetId: number,
2730
+ catalogTrackName?: string,
2731
+ ): void {
2732
+ this.#repairInFlight.delete(
2733
+ JSON.stringify([packetId, catalogTrackName ?? null, sbn]),
2734
+ );
2735
+ }
2736
+
2737
+ #emitRepairLifecycle(event: RepairLifecycleEvent): void {
2738
+ // A gap decision is already frozen and is the same object the frame
2739
+ // carries as gapBefore: forward it as is, never a copy.
2740
+ const immutableEvent = event.type === "gap"
2741
+ ? event
2742
+ : (Object.freeze(
2743
+ "stream" in event
2744
+ ? { ...event, stream: Object.freeze({ ...event.stream }) }
2745
+ : {
2746
+ ...event,
2747
+ block: Object.freeze({ ...event.block }),
2748
+ ...("symbols" in event
2749
+ ? { symbols: Object.freeze({ ...event.symbols }) }
2750
+ : {}),
2751
+ },
2752
+ ) as RepairLifecycleEvent);
2753
+ // Serialize before invoking observers so a re-entrant observer cannot log
2754
+ // a resolution before the request that caused it.
2755
+ if (this.#logRepairLifecycle) {
2756
+ console.info(
2757
+ `${REPAIR_LIFECYCLE_LOG_PREFIX}${JSON.stringify(immutableEvent)}`,
2758
+ );
2759
+ }
2760
+ for (const observer of this.#repairLifecycleObservers) {
2761
+ try {
2762
+ observer(immutableEvent);
2763
+ } catch (error) {
2764
+ console.warn("[MulticastManager] repair lifecycle observer failed", error);
2765
+ }
2766
+ }
2767
+ }
2768
+
2769
+ dispose() {
2770
+ const staged = this.#renditionSwitch;
2771
+ if (staged !== null) this.#endRenditionSwitch(staged, { reason: "superseded" }, false);
2772
+ this.#trackResetObservers.clear();
2773
+ this.#repairLifecycleObservers.clear();
2774
+ this.#transport.dispose();
2775
+ this.#router.dispose();
2776
+ this.#fec.dispose();
2777
+ }
2778
+ }
2779
+
2780
+ // Compile-time contract: the manager is the host the D-28 repair scheduler
2781
+ // (wireRepairFetch) drives.
2782
+ const repairFetchHost = (manager: MulticastManager): RepairFetchHost => manager;
2783
+ void repairFetchHost;