@blockcast/mmt-container 0.3.0 → 0.4.0-main.71d61b2a53f2

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 (605) hide show
  1. package/README.md +33 -16
  2. package/dist/browser-runtime.js +14743 -14412
  3. package/dist/browser-runtime.js.map +4 -4
  4. package/dist/carp-catalog.d.ts +24 -19
  5. package/dist/carp-catalog.d.ts.map +1 -1
  6. package/dist/carp-catalog.js +29 -14
  7. package/dist/carp-catalog.js.map +4 -4
  8. package/dist/catalog-fec-params.d.ts +88 -28
  9. package/dist/catalog-fec-params.d.ts.map +1 -1
  10. package/dist/catalog-fec-params.js +60 -23
  11. package/dist/catalog-fec-params.js.map +4 -4
  12. package/dist/catalog-network-source.js +1 -7
  13. package/dist/catalog-network-source.js.map +4 -4
  14. package/dist/cmaf-block-assembler.d.ts +0 -2
  15. package/dist/cmaf-block-assembler.d.ts.map +1 -1
  16. package/dist/cmaf-block-assembler.js +0 -1
  17. package/dist/cmaf-block-assembler.js.map +2 -2
  18. package/dist/cmcd-producer.d.ts.map +1 -1
  19. package/dist/cmcd-producer.js +0 -4
  20. package/dist/cmcd-producer.js.map +2 -2
  21. package/dist/delivery-catalog.d.ts +124 -0
  22. package/dist/delivery-catalog.d.ts.map +1 -0
  23. package/dist/delivery-catalog.js +1857 -0
  24. package/dist/delivery-catalog.js.map +7 -0
  25. package/dist/delta-fallback-config.d.ts +42 -0
  26. package/dist/delta-fallback-config.d.ts.map +1 -0
  27. package/dist/delta-fallback-config.js +59 -0
  28. package/dist/delta-fallback-config.js.map +7 -0
  29. package/dist/fec-manager.d.ts +36 -30
  30. package/dist/fec-manager.d.ts.map +1 -1
  31. package/dist/fec-manager.js +303 -293
  32. package/dist/fec-manager.js.map +4 -4
  33. package/dist/fec-state.d.ts +19 -31
  34. package/dist/fec-state.d.ts.map +1 -1
  35. package/dist/fec-state.js +4 -10
  36. package/dist/fec-state.js.map +4 -4
  37. package/dist/fec-worker-setup.d.ts +18 -14
  38. package/dist/fec-worker-setup.d.ts.map +1 -1
  39. package/dist/fec-worker-setup.js +81 -96
  40. package/dist/fec-worker-setup.js.map +4 -4
  41. package/dist/index.d.ts +5 -3
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +16 -0
  44. package/dist/index.js.map +2 -2
  45. package/dist/iso-block-locator.js +1 -7
  46. package/dist/iso-block-locator.js.map +4 -4
  47. package/dist/media-router.d.ts +20 -8
  48. package/dist/media-router.d.ts.map +1 -1
  49. package/dist/media-router.js +46 -21
  50. package/dist/media-router.js.map +2 -2
  51. package/dist/mfu-reassembler.js +24 -30
  52. package/dist/mfu-reassembler.js.map +4 -4
  53. package/dist/mmtp-container-parser.d.ts +6 -11
  54. package/dist/mmtp-container-parser.d.ts.map +1 -1
  55. package/dist/mmtp-container-parser.js +56 -97
  56. package/dist/mmtp-container-parser.js.map +4 -4
  57. package/dist/mmtp-container-parser.no-modules.js +78 -105
  58. package/dist/mmtp-container-parser.no-modules.js.map +4 -4
  59. package/dist/mmtp-decode-pipeline.js +56 -97
  60. package/dist/mmtp-decode-pipeline.js.map +4 -4
  61. package/dist/mmtp-router.d.ts +37 -26
  62. package/dist/mmtp-router.d.ts.map +1 -1
  63. package/dist/mmtp-router.js +162 -313
  64. package/dist/mmtp-router.js.map +4 -4
  65. package/dist/multicast-input.js +1 -7
  66. package/dist/multicast-input.js.map +4 -4
  67. package/dist/multicast-manager.d.ts +34 -6
  68. package/dist/multicast-manager.d.ts.map +1 -1
  69. package/dist/multicast-manager.js +709 -658
  70. package/dist/multicast-manager.js.map +4 -4
  71. package/dist/multicast-transport.d.ts +1 -1
  72. package/dist/multicast-transport.d.ts.map +1 -1
  73. package/dist/multicast-transport.js +44 -26
  74. package/dist/multicast-transport.js.map +4 -4
  75. package/dist/multicast-unwrap.js +29 -35
  76. package/dist/multicast-unwrap.js.map +4 -4
  77. package/dist/no-modules.js +77 -104
  78. package/dist/no-modules.js.map +4 -4
  79. package/dist/repair-fetch.d.ts +30 -19
  80. package/dist/repair-fetch.d.ts.map +1 -1
  81. package/dist/repair-fetch.js +33 -24
  82. package/dist/repair-fetch.js.map +4 -4
  83. package/dist/repair-layers.d.ts +18 -7
  84. package/dist/repair-layers.d.ts.map +1 -1
  85. package/dist/repair-layers.js +12 -10
  86. package/dist/repair-layers.js.map +4 -4
  87. package/dist/signaling.js +2 -11
  88. package/dist/signaling.js.map +4 -4
  89. package/dist/timestamp-codec.js +24 -30
  90. package/dist/timestamp-codec.js.map +4 -4
  91. package/dist/track-session.d.ts +7 -0
  92. package/dist/track-session.d.ts.map +1 -1
  93. package/dist/track-session.js +15 -8
  94. package/dist/track-session.js.map +2 -2
  95. package/dist/wasm-mfu-reassembler.js +1 -7
  96. package/dist/wasm-mfu-reassembler.js.map +4 -4
  97. package/dist/wasm-mmtp-container-parser.js +56 -97
  98. package/dist/wasm-mmtp-container-parser.js.map +4 -4
  99. package/package.json +7 -14
  100. package/schemas/catalog.schema.json +3 -3
  101. package/src/carp-catalog.ts +46 -24
  102. package/src/catalog-fec-params.ts +173 -64
  103. package/src/cmaf-block-assembler.ts +1 -4
  104. package/src/cmcd-producer.ts +0 -6
  105. package/src/delivery-catalog.ts +498 -0
  106. package/src/delta-fallback-config.ts +72 -0
  107. package/src/fec-manager.ts +414 -334
  108. package/src/fec-state.ts +20 -32
  109. package/src/fec-worker-setup.ts +109 -90
  110. package/src/index.ts +32 -0
  111. package/src/media-router.ts +59 -21
  112. package/src/mmtp-container-parser.ts +27 -18
  113. package/src/mmtp-router.ts +146 -313
  114. package/src/multicast-manager.ts +184 -83
  115. package/src/multicast-transport.ts +16 -9
  116. package/src/repair-fetch.ts +46 -27
  117. package/src/repair-layers.ts +28 -8
  118. package/src/track-session.ts +28 -9
  119. package/test-vectors/catalog/README.md +37 -17
  120. package/test-vectors/catalog/negative/fec-block-profile-retired.json +52 -0
  121. package/test-vectors/catalog/negative/fec-enhancement-repair-removed.json +1 -1
  122. package/test-vectors/catalog/negative/fec-repair-layer-geometry.json +1 -1
  123. package/test-vectors/catalog/negative/fec-repair-layer-missing-symbols.json +1 -1
  124. package/test-vectors/catalog/negative/fec-repair-multiple-depends.json +1 -1
  125. package/test-vectors/catalog/negative/fec-repair-overlay-with-layer.json +1 -1
  126. package/test-vectors/catalog/negative/fec-repair-priority-out-of-band.json +1 -1
  127. package/test-vectors/catalog/negative/fec-repair-source-symbols-without-scope.json +1 -1
  128. package/test-vectors/catalog/negative/multicast-endpoint-relay-address.json +39 -0
  129. package/test-vectors/catalog/negative/multicast-network-source-empty.json +1 -1
  130. package/test-vectors/catalog/negative/raptorq-source-symbols-over-max.json +1 -1
  131. package/test-vectors/catalog/negative/raptorq-unaligned-symbol.json +1 -1
  132. package/test-vectors/catalog/positive/fec-layered-repair.json +2 -2
  133. package/test-vectors/catalog/positive/fec-multicast.json +2 -1
  134. package/test-vectors/catalog/positive/multicast-amt-manual-relay.json +38 -0
  135. package/test-vectors/catalog/positive/multicast-native-ssm-no-network-source.json +2 -1
  136. package/test-vectors/catalog-timing.json +356 -0
  137. package/test-vectors/catalog-validation.json +40 -5
  138. package/test-vectors/delivery-catalog/README.md +11 -0
  139. package/test-vectors/delivery-catalog/negative/missing-block-profile.json +58 -0
  140. package/test-vectors/delivery-catalog/negative/packet-id-signaling.json +59 -0
  141. package/test-vectors/delivery-catalog/negative/retired-k-padded-block-profile.json +59 -0
  142. package/test-vectors/delivery-catalog/positive/dash.json +56 -0
  143. package/test-vectors/delivery-catalog/positive/mmtp-source-only.json +43 -0
  144. package/node_modules/@blockcast/fec-worker/README.md +0 -223
  145. package/node_modules/@blockcast/fec-worker/dist/.build-stamp +0 -0
  146. package/node_modules/@blockcast/fec-worker/dist/block-order.d.ts +0 -38
  147. package/node_modules/@blockcast/fec-worker/dist/block-order.d.ts.map +0 -1
  148. package/node_modules/@blockcast/fec-worker/dist/block-order.js +0 -78
  149. package/node_modules/@blockcast/fec-worker/dist/block-order.js.map +0 -1
  150. package/node_modules/@blockcast/fec-worker/dist/fec-worker-client.d.ts +0 -93
  151. package/node_modules/@blockcast/fec-worker/dist/fec-worker-client.d.ts.map +0 -1
  152. package/node_modules/@blockcast/fec-worker/dist/fec-worker-client.js +0 -346
  153. package/node_modules/@blockcast/fec-worker/dist/fec-worker-client.js.map +0 -1
  154. package/node_modules/@blockcast/fec-worker/dist/fec-worker-types.d.ts +0 -380
  155. package/node_modules/@blockcast/fec-worker/dist/fec-worker-types.d.ts.map +0 -1
  156. package/node_modules/@blockcast/fec-worker/dist/fec-worker-types.js +0 -11
  157. package/node_modules/@blockcast/fec-worker/dist/fec-worker-types.js.map +0 -1
  158. package/node_modules/@blockcast/fec-worker/dist/fec-worker.d.ts +0 -14
  159. package/node_modules/@blockcast/fec-worker/dist/fec-worker.d.ts.map +0 -1
  160. package/node_modules/@blockcast/fec-worker/dist/fec-worker.js +0 -859
  161. package/node_modules/@blockcast/fec-worker/dist/fec-worker.js.map +0 -7
  162. package/node_modules/@blockcast/fec-worker/dist/index.d.ts +0 -10
  163. package/node_modules/@blockcast/fec-worker/dist/index.d.ts.map +0 -1
  164. package/node_modules/@blockcast/fec-worker/dist/index.js +0 -8
  165. package/node_modules/@blockcast/fec-worker/dist/index.js.map +0 -1
  166. package/node_modules/@blockcast/fec-worker/dist/shred-fec-worker.d.ts +0 -20
  167. package/node_modules/@blockcast/fec-worker/dist/shred-fec-worker.d.ts.map +0 -1
  168. package/node_modules/@blockcast/fec-worker/dist/shred-fec-worker.js +0 -370
  169. package/node_modules/@blockcast/fec-worker/dist/shred-fec-worker.js.map +0 -7
  170. package/node_modules/@blockcast/fec-worker/dist/shred-worker-client.d.ts +0 -104
  171. package/node_modules/@blockcast/fec-worker/dist/shred-worker-client.d.ts.map +0 -1
  172. package/node_modules/@blockcast/fec-worker/dist/shred-worker-client.js +0 -181
  173. package/node_modules/@blockcast/fec-worker/dist/shred-worker-client.js.map +0 -1
  174. package/node_modules/@blockcast/fec-worker/dist/shred-worker-types.d.ts +0 -194
  175. package/node_modules/@blockcast/fec-worker/dist/shred-worker-types.d.ts.map +0 -1
  176. package/node_modules/@blockcast/fec-worker/dist/shred-worker-types.js +0 -62
  177. package/node_modules/@blockcast/fec-worker/dist/shred-worker-types.js.map +0 -1
  178. package/node_modules/@blockcast/fec-worker/dist/transfer.d.ts +0 -9
  179. package/node_modules/@blockcast/fec-worker/dist/transfer.d.ts.map +0 -1
  180. package/node_modules/@blockcast/fec-worker/dist/transfer.js +0 -13
  181. package/node_modules/@blockcast/fec-worker/dist/transfer.js.map +0 -1
  182. package/node_modules/@blockcast/fec-worker/package.json +0 -59
  183. package/node_modules/@blockcast/fec-worker/src/block-order.ts +0 -93
  184. package/node_modules/@blockcast/fec-worker/src/fec-worker-client.ts +0 -392
  185. package/node_modules/@blockcast/fec-worker/src/fec-worker-types.ts +0 -399
  186. package/node_modules/@blockcast/fec-worker/src/fec-worker.ts +0 -1502
  187. package/node_modules/@blockcast/fec-worker/src/index.ts +0 -35
  188. package/node_modules/@blockcast/fec-worker/src/shred-fec-worker.ts +0 -589
  189. package/node_modules/@blockcast/fec-worker/src/shred-worker-client.ts +0 -222
  190. package/node_modules/@blockcast/fec-worker/src/shred-worker-types.ts +0 -209
  191. package/node_modules/@blockcast/fec-worker/src/transfer.ts +0 -12
  192. package/node_modules/@blockcast/mmt-alta-parse/README.md +0 -181
  193. package/node_modules/@blockcast/mmt-alta-parse/dist/.build-stamp +0 -0
  194. package/node_modules/@blockcast/mmt-alta-parse/dist/index.d.ts +0 -57
  195. package/node_modules/@blockcast/mmt-alta-parse/dist/index.d.ts.map +0 -1
  196. package/node_modules/@blockcast/mmt-alta-parse/dist/index.js +0 -115
  197. package/node_modules/@blockcast/mmt-alta-parse/dist/index.js.map +0 -1
  198. package/node_modules/@blockcast/mmt-alta-parse/package.json +0 -50
  199. package/node_modules/@blockcast/mmt-alta-parse/src/index.ts +0 -136
  200. package/node_modules/@blockcast/mmt-base/README.md +0 -134
  201. package/node_modules/@blockcast/mmt-base/dist/.build-stamp +0 -0
  202. package/node_modules/@blockcast/mmt-base/dist/fec-block-profile.d.ts +0 -55
  203. package/node_modules/@blockcast/mmt-base/dist/fec-block-profile.d.ts.map +0 -1
  204. package/node_modules/@blockcast/mmt-base/dist/fec-block-profile.js +0 -128
  205. package/node_modules/@blockcast/mmt-base/dist/fec-block-profile.js.map +0 -1
  206. package/node_modules/@blockcast/mmt-base/dist/index.d.ts +0 -7
  207. package/node_modules/@blockcast/mmt-base/dist/index.d.ts.map +0 -1
  208. package/node_modules/@blockcast/mmt-base/dist/index.js +0 -7
  209. package/node_modules/@blockcast/mmt-base/dist/index.js.map +0 -1
  210. package/node_modules/@blockcast/mmt-base/dist/mmtp.d.ts +0 -292
  211. package/node_modules/@blockcast/mmt-base/dist/mmtp.d.ts.map +0 -1
  212. package/node_modules/@blockcast/mmt-base/dist/mmtp.js +0 -612
  213. package/node_modules/@blockcast/mmt-base/dist/mmtp.js.map +0 -1
  214. package/node_modules/@blockcast/mmt-base/dist/net-constants.d.ts +0 -18
  215. package/node_modules/@blockcast/mmt-base/dist/net-constants.d.ts.map +0 -1
  216. package/node_modules/@blockcast/mmt-base/dist/net-constants.js +0 -18
  217. package/node_modules/@blockcast/mmt-base/dist/net-constants.js.map +0 -1
  218. package/node_modules/@blockcast/mmt-base/dist/ntp-short.d.ts +0 -47
  219. package/node_modules/@blockcast/mmt-base/dist/ntp-short.d.ts.map +0 -1
  220. package/node_modules/@blockcast/mmt-base/dist/ntp-short.js +0 -54
  221. package/node_modules/@blockcast/mmt-base/dist/ntp-short.js.map +0 -1
  222. package/node_modules/@blockcast/mmt-base/dist/serial-number.d.ts +0 -57
  223. package/node_modules/@blockcast/mmt-base/dist/serial-number.d.ts.map +0 -1
  224. package/node_modules/@blockcast/mmt-base/dist/serial-number.js +0 -98
  225. package/node_modules/@blockcast/mmt-base/dist/serial-number.js.map +0 -1
  226. package/node_modules/@blockcast/mmt-base/dist/signalling.d.ts +0 -312
  227. package/node_modules/@blockcast/mmt-base/dist/signalling.d.ts.map +0 -1
  228. package/node_modules/@blockcast/mmt-base/dist/signalling.js +0 -637
  229. package/node_modules/@blockcast/mmt-base/dist/signalling.js.map +0 -1
  230. package/node_modules/@blockcast/mmt-base/package.json +0 -49
  231. package/node_modules/@blockcast/mmt-base/src/fec-block-profile.ts +0 -145
  232. package/node_modules/@blockcast/mmt-base/src/index.ts +0 -6
  233. package/node_modules/@blockcast/mmt-base/src/mmtp.ts +0 -892
  234. package/node_modules/@blockcast/mmt-base/src/net-constants.ts +0 -17
  235. package/node_modules/@blockcast/mmt-base/src/ntp-short.ts +0 -67
  236. package/node_modules/@blockcast/mmt-base/src/serial-number.ts +0 -107
  237. package/node_modules/@blockcast/mmt-base/src/signalling.ts +0 -862
  238. package/node_modules/@blockcast/mmt-fec/README.md +0 -271
  239. package/node_modules/@blockcast/mmt-fec/dist/.build-stamp +0 -0
  240. package/node_modules/@blockcast/mmt-fec/dist/block-manager.d.ts +0 -82
  241. package/node_modules/@blockcast/mmt-fec/dist/block-manager.d.ts.map +0 -1
  242. package/node_modules/@blockcast/mmt-fec/dist/block-manager.js +0 -296
  243. package/node_modules/@blockcast/mmt-fec/dist/block-manager.js.map +0 -1
  244. package/node_modules/@blockcast/mmt-fec/dist/block-processor.d.ts +0 -106
  245. package/node_modules/@blockcast/mmt-fec/dist/block-processor.d.ts.map +0 -1
  246. package/node_modules/@blockcast/mmt-fec/dist/block-processor.js +0 -288
  247. package/node_modules/@blockcast/mmt-fec/dist/block-processor.js.map +0 -1
  248. package/node_modules/@blockcast/mmt-fec/dist/cmaf-recovery.d.ts +0 -46
  249. package/node_modules/@blockcast/mmt-fec/dist/cmaf-recovery.d.ts.map +0 -1
  250. package/node_modules/@blockcast/mmt-fec/dist/cmaf-recovery.js +0 -96
  251. package/node_modules/@blockcast/mmt-fec/dist/cmaf-recovery.js.map +0 -1
  252. package/node_modules/@blockcast/mmt-fec/dist/cmaf-symbols.d.ts +0 -33
  253. package/node_modules/@blockcast/mmt-fec/dist/cmaf-symbols.d.ts.map +0 -1
  254. package/node_modules/@blockcast/mmt-fec/dist/cmaf-symbols.js +0 -57
  255. package/node_modules/@blockcast/mmt-fec/dist/cmaf-symbols.js.map +0 -1
  256. package/node_modules/@blockcast/mmt-fec/dist/fec-block-mapper.d.ts +0 -205
  257. package/node_modules/@blockcast/mmt-fec/dist/fec-block-mapper.d.ts.map +0 -1
  258. package/node_modules/@blockcast/mmt-fec/dist/fec-block-mapper.js +0 -240
  259. package/node_modules/@blockcast/mmt-fec/dist/fec-block-mapper.js.map +0 -1
  260. package/node_modules/@blockcast/mmt-fec/dist/index.d.ts +0 -28
  261. package/node_modules/@blockcast/mmt-fec/dist/index.d.ts.map +0 -1
  262. package/node_modules/@blockcast/mmt-fec/dist/index.js +0 -28
  263. package/node_modules/@blockcast/mmt-fec/dist/index.js.map +0 -1
  264. package/node_modules/@blockcast/mmt-fec/dist/loc-symbols.d.ts +0 -20
  265. package/node_modules/@blockcast/mmt-fec/dist/loc-symbols.d.ts.map +0 -1
  266. package/node_modules/@blockcast/mmt-fec/dist/loc-symbols.js +0 -28
  267. package/node_modules/@blockcast/mmt-fec/dist/loc-symbols.js.map +0 -1
  268. package/node_modules/@blockcast/mmt-fec/dist/oti.d.ts +0 -258
  269. package/node_modules/@blockcast/mmt-fec/dist/oti.d.ts.map +0 -1
  270. package/node_modules/@blockcast/mmt-fec/dist/oti.js +0 -382
  271. package/node_modules/@blockcast/mmt-fec/dist/oti.js.map +0 -1
  272. package/node_modules/@blockcast/mmt-fec/dist/reorder-buffer.d.ts +0 -41
  273. package/node_modules/@blockcast/mmt-fec/dist/reorder-buffer.d.ts.map +0 -1
  274. package/node_modules/@blockcast/mmt-fec/dist/reorder-buffer.js +0 -81
  275. package/node_modules/@blockcast/mmt-fec/dist/reorder-buffer.js.map +0 -1
  276. package/node_modules/@blockcast/mmt-fec/dist/repair-group.d.ts +0 -82
  277. package/node_modules/@blockcast/mmt-fec/dist/repair-group.d.ts.map +0 -1
  278. package/node_modules/@blockcast/mmt-fec/dist/repair-group.js +0 -170
  279. package/node_modules/@blockcast/mmt-fec/dist/repair-group.js.map +0 -1
  280. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-alta-parse/README.md +0 -181
  281. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-alta-parse/dist/.build-stamp +0 -0
  282. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-alta-parse/dist/index.d.ts +0 -57
  283. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-alta-parse/dist/index.d.ts.map +0 -1
  284. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-alta-parse/dist/index.js +0 -115
  285. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-alta-parse/dist/index.js.map +0 -1
  286. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-alta-parse/package.json +0 -50
  287. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-alta-parse/src/index.ts +0 -136
  288. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/README.md +0 -134
  289. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/.build-stamp +0 -0
  290. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/fec-block-profile.d.ts +0 -55
  291. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/fec-block-profile.d.ts.map +0 -1
  292. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/fec-block-profile.js +0 -128
  293. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/fec-block-profile.js.map +0 -1
  294. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/index.d.ts +0 -7
  295. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/index.d.ts.map +0 -1
  296. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/index.js +0 -7
  297. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/index.js.map +0 -1
  298. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/mmtp.d.ts +0 -292
  299. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/mmtp.d.ts.map +0 -1
  300. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/mmtp.js +0 -612
  301. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/mmtp.js.map +0 -1
  302. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/net-constants.d.ts +0 -18
  303. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/net-constants.d.ts.map +0 -1
  304. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/net-constants.js +0 -18
  305. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/net-constants.js.map +0 -1
  306. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/ntp-short.d.ts +0 -47
  307. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/ntp-short.d.ts.map +0 -1
  308. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/ntp-short.js +0 -54
  309. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/ntp-short.js.map +0 -1
  310. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/serial-number.d.ts +0 -57
  311. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/serial-number.d.ts.map +0 -1
  312. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/serial-number.js +0 -98
  313. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/serial-number.js.map +0 -1
  314. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/signalling.d.ts +0 -312
  315. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/signalling.d.ts.map +0 -1
  316. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/signalling.js +0 -637
  317. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/signalling.js.map +0 -1
  318. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/package.json +0 -49
  319. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/src/fec-block-profile.ts +0 -145
  320. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/src/index.ts +0 -6
  321. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/src/mmtp.ts +0 -892
  322. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/src/net-constants.ts +0 -17
  323. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/src/ntp-short.ts +0 -67
  324. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/src/serial-number.ts +0 -107
  325. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/src/signalling.ts +0 -862
  326. package/node_modules/@blockcast/mmt-fec/package.json +0 -79
  327. package/node_modules/@blockcast/mmt-fec/src/block-manager.ts +0 -371
  328. package/node_modules/@blockcast/mmt-fec/src/block-processor.ts +0 -439
  329. package/node_modules/@blockcast/mmt-fec/src/cmaf-recovery.ts +0 -127
  330. package/node_modules/@blockcast/mmt-fec/src/cmaf-symbols.ts +0 -68
  331. package/node_modules/@blockcast/mmt-fec/src/fec-block-mapper.ts +0 -310
  332. package/node_modules/@blockcast/mmt-fec/src/index.ts +0 -94
  333. package/node_modules/@blockcast/mmt-fec/src/loc-symbols.ts +0 -34
  334. package/node_modules/@blockcast/mmt-fec/src/oti.ts +0 -463
  335. package/node_modules/@blockcast/mmt-fec/src/reorder-buffer.ts +0 -101
  336. package/node_modules/@blockcast/mmt-fec/src/repair-group.ts +0 -195
  337. package/node_modules/@blockcast/mmt-manifest-verify/dist/.build-stamp +0 -0
  338. package/node_modules/@blockcast/mmt-manifest-verify/dist/admission.d.ts +0 -28
  339. package/node_modules/@blockcast/mmt-manifest-verify/dist/admission.d.ts.map +0 -1
  340. package/node_modules/@blockcast/mmt-manifest-verify/dist/admission.js +0 -128
  341. package/node_modules/@blockcast/mmt-manifest-verify/dist/admission.js.map +0 -1
  342. package/node_modules/@blockcast/mmt-manifest-verify/dist/index.d.ts +0 -4
  343. package/node_modules/@blockcast/mmt-manifest-verify/dist/index.d.ts.map +0 -1
  344. package/node_modules/@blockcast/mmt-manifest-verify/dist/index.js +0 -4
  345. package/node_modules/@blockcast/mmt-manifest-verify/dist/index.js.map +0 -1
  346. package/node_modules/@blockcast/mmt-manifest-verify/dist/source-auth.d.ts +0 -37
  347. package/node_modules/@blockcast/mmt-manifest-verify/dist/source-auth.d.ts.map +0 -1
  348. package/node_modules/@blockcast/mmt-manifest-verify/dist/source-auth.js +0 -179
  349. package/node_modules/@blockcast/mmt-manifest-verify/dist/source-auth.js.map +0 -1
  350. package/node_modules/@blockcast/mmt-manifest-verify/dist/verifier.d.ts +0 -99
  351. package/node_modules/@blockcast/mmt-manifest-verify/dist/verifier.d.ts.map +0 -1
  352. package/node_modules/@blockcast/mmt-manifest-verify/dist/verifier.js +0 -492
  353. package/node_modules/@blockcast/mmt-manifest-verify/dist/verifier.js.map +0 -1
  354. package/node_modules/@blockcast/mmt-manifest-verify/package.json +0 -45
  355. package/node_modules/@blockcast/mmt-manifest-verify/src/admission.ts +0 -171
  356. package/node_modules/@blockcast/mmt-manifest-verify/src/index.ts +0 -3
  357. package/node_modules/@blockcast/mmt-manifest-verify/src/source-auth.ts +0 -230
  358. package/node_modules/@blockcast/mmt-manifest-verify/src/verifier.ts +0 -669
  359. package/node_modules/@noble/hashes/LICENSE +0 -21
  360. package/node_modules/@noble/hashes/README.md +0 -521
  361. package/node_modules/@noble/hashes/_assert.d.ts +0 -17
  362. package/node_modules/@noble/hashes/_assert.d.ts.map +0 -1
  363. package/node_modules/@noble/hashes/_assert.js +0 -18
  364. package/node_modules/@noble/hashes/_assert.js.map +0 -1
  365. package/node_modules/@noble/hashes/_blake.d.ts +0 -14
  366. package/node_modules/@noble/hashes/_blake.d.ts.map +0 -1
  367. package/node_modules/@noble/hashes/_blake.js +0 -50
  368. package/node_modules/@noble/hashes/_blake.js.map +0 -1
  369. package/node_modules/@noble/hashes/_md.d.ts +0 -51
  370. package/node_modules/@noble/hashes/_md.d.ts.map +0 -1
  371. package/node_modules/@noble/hashes/_md.js +0 -162
  372. package/node_modules/@noble/hashes/_md.js.map +0 -1
  373. package/node_modules/@noble/hashes/_u64.d.ts +0 -55
  374. package/node_modules/@noble/hashes/_u64.d.ts.map +0 -1
  375. package/node_modules/@noble/hashes/_u64.js +0 -90
  376. package/node_modules/@noble/hashes/_u64.js.map +0 -1
  377. package/node_modules/@noble/hashes/argon2.d.ts +0 -32
  378. package/node_modules/@noble/hashes/argon2.d.ts.map +0 -1
  379. package/node_modules/@noble/hashes/argon2.js +0 -401
  380. package/node_modules/@noble/hashes/argon2.js.map +0 -1
  381. package/node_modules/@noble/hashes/blake1.d.ts +0 -106
  382. package/node_modules/@noble/hashes/blake1.d.ts.map +0 -1
  383. package/node_modules/@noble/hashes/blake1.js +0 -459
  384. package/node_modules/@noble/hashes/blake1.js.map +0 -1
  385. package/node_modules/@noble/hashes/blake2.d.ts +0 -116
  386. package/node_modules/@noble/hashes/blake2.d.ts.map +0 -1
  387. package/node_modules/@noble/hashes/blake2.js +0 -420
  388. package/node_modules/@noble/hashes/blake2.js.map +0 -1
  389. package/node_modules/@noble/hashes/blake2b.d.ts +0 -11
  390. package/node_modules/@noble/hashes/blake2b.d.ts.map +0 -1
  391. package/node_modules/@noble/hashes/blake2b.js +0 -14
  392. package/node_modules/@noble/hashes/blake2b.js.map +0 -1
  393. package/node_modules/@noble/hashes/blake2s.d.ts +0 -20
  394. package/node_modules/@noble/hashes/blake2s.d.ts.map +0 -1
  395. package/node_modules/@noble/hashes/blake2s.js +0 -24
  396. package/node_modules/@noble/hashes/blake2s.js.map +0 -1
  397. package/node_modules/@noble/hashes/blake3.d.ts +0 -54
  398. package/node_modules/@noble/hashes/blake3.d.ts.map +0 -1
  399. package/node_modules/@noble/hashes/blake3.js +0 -255
  400. package/node_modules/@noble/hashes/blake3.js.map +0 -1
  401. package/node_modules/@noble/hashes/crypto.d.ts +0 -2
  402. package/node_modules/@noble/hashes/crypto.d.ts.map +0 -1
  403. package/node_modules/@noble/hashes/crypto.js +0 -5
  404. package/node_modules/@noble/hashes/crypto.js.map +0 -1
  405. package/node_modules/@noble/hashes/cryptoNode.d.ts +0 -2
  406. package/node_modules/@noble/hashes/cryptoNode.d.ts.map +0 -1
  407. package/node_modules/@noble/hashes/cryptoNode.js +0 -18
  408. package/node_modules/@noble/hashes/cryptoNode.js.map +0 -1
  409. package/node_modules/@noble/hashes/eskdf.d.ts +0 -47
  410. package/node_modules/@noble/hashes/eskdf.d.ts.map +0 -1
  411. package/node_modules/@noble/hashes/eskdf.js +0 -166
  412. package/node_modules/@noble/hashes/eskdf.js.map +0 -1
  413. package/node_modules/@noble/hashes/esm/_assert.d.ts +0 -17
  414. package/node_modules/@noble/hashes/esm/_assert.d.ts.map +0 -1
  415. package/node_modules/@noble/hashes/esm/_assert.js +0 -15
  416. package/node_modules/@noble/hashes/esm/_assert.js.map +0 -1
  417. package/node_modules/@noble/hashes/esm/_blake.d.ts +0 -14
  418. package/node_modules/@noble/hashes/esm/_blake.d.ts.map +0 -1
  419. package/node_modules/@noble/hashes/esm/_blake.js +0 -45
  420. package/node_modules/@noble/hashes/esm/_blake.js.map +0 -1
  421. package/node_modules/@noble/hashes/esm/_md.d.ts +0 -51
  422. package/node_modules/@noble/hashes/esm/_md.d.ts.map +0 -1
  423. package/node_modules/@noble/hashes/esm/_md.js +0 -155
  424. package/node_modules/@noble/hashes/esm/_md.js.map +0 -1
  425. package/node_modules/@noble/hashes/esm/_u64.d.ts +0 -55
  426. package/node_modules/@noble/hashes/esm/_u64.d.ts.map +0 -1
  427. package/node_modules/@noble/hashes/esm/_u64.js +0 -67
  428. package/node_modules/@noble/hashes/esm/_u64.js.map +0 -1
  429. package/node_modules/@noble/hashes/esm/argon2.d.ts +0 -32
  430. package/node_modules/@noble/hashes/esm/argon2.d.ts.map +0 -1
  431. package/node_modules/@noble/hashes/esm/argon2.js +0 -392
  432. package/node_modules/@noble/hashes/esm/argon2.js.map +0 -1
  433. package/node_modules/@noble/hashes/esm/blake1.d.ts +0 -106
  434. package/node_modules/@noble/hashes/esm/blake1.d.ts.map +0 -1
  435. package/node_modules/@noble/hashes/esm/blake1.js +0 -452
  436. package/node_modules/@noble/hashes/esm/blake1.js.map +0 -1
  437. package/node_modules/@noble/hashes/esm/blake2.d.ts +0 -116
  438. package/node_modules/@noble/hashes/esm/blake2.d.ts.map +0 -1
  439. package/node_modules/@noble/hashes/esm/blake2.js +0 -413
  440. package/node_modules/@noble/hashes/esm/blake2.js.map +0 -1
  441. package/node_modules/@noble/hashes/esm/blake2b.d.ts +0 -11
  442. package/node_modules/@noble/hashes/esm/blake2b.d.ts.map +0 -1
  443. package/node_modules/@noble/hashes/esm/blake2b.js +0 -11
  444. package/node_modules/@noble/hashes/esm/blake2b.js.map +0 -1
  445. package/node_modules/@noble/hashes/esm/blake2s.d.ts +0 -20
  446. package/node_modules/@noble/hashes/esm/blake2s.d.ts.map +0 -1
  447. package/node_modules/@noble/hashes/esm/blake2s.js +0 -21
  448. package/node_modules/@noble/hashes/esm/blake2s.js.map +0 -1
  449. package/node_modules/@noble/hashes/esm/blake3.d.ts +0 -54
  450. package/node_modules/@noble/hashes/esm/blake3.d.ts.map +0 -1
  451. package/node_modules/@noble/hashes/esm/blake3.js +0 -251
  452. package/node_modules/@noble/hashes/esm/blake3.js.map +0 -1
  453. package/node_modules/@noble/hashes/esm/crypto.d.ts +0 -2
  454. package/node_modules/@noble/hashes/esm/crypto.d.ts.map +0 -1
  455. package/node_modules/@noble/hashes/esm/crypto.js +0 -2
  456. package/node_modules/@noble/hashes/esm/crypto.js.map +0 -1
  457. package/node_modules/@noble/hashes/esm/cryptoNode.d.ts +0 -2
  458. package/node_modules/@noble/hashes/esm/cryptoNode.d.ts.map +0 -1
  459. package/node_modules/@noble/hashes/esm/cryptoNode.js +0 -15
  460. package/node_modules/@noble/hashes/esm/cryptoNode.js.map +0 -1
  461. package/node_modules/@noble/hashes/esm/eskdf.d.ts +0 -47
  462. package/node_modules/@noble/hashes/esm/eskdf.d.ts.map +0 -1
  463. package/node_modules/@noble/hashes/esm/eskdf.js +0 -160
  464. package/node_modules/@noble/hashes/esm/eskdf.js.map +0 -1
  465. package/node_modules/@noble/hashes/esm/hkdf.d.ts +0 -36
  466. package/node_modules/@noble/hashes/esm/hkdf.d.ts.map +0 -1
  467. package/node_modules/@noble/hashes/esm/hkdf.js +0 -82
  468. package/node_modules/@noble/hashes/esm/hkdf.js.map +0 -1
  469. package/node_modules/@noble/hashes/esm/hmac.d.ts +0 -35
  470. package/node_modules/@noble/hashes/esm/hmac.d.ts.map +0 -1
  471. package/node_modules/@noble/hashes/esm/hmac.js +0 -86
  472. package/node_modules/@noble/hashes/esm/hmac.js.map +0 -1
  473. package/node_modules/@noble/hashes/esm/index.d.ts +0 -2
  474. package/node_modules/@noble/hashes/esm/index.d.ts.map +0 -1
  475. package/node_modules/@noble/hashes/esm/index.js +0 -33
  476. package/node_modules/@noble/hashes/esm/index.js.map +0 -1
  477. package/node_modules/@noble/hashes/esm/legacy.d.ts +0 -71
  478. package/node_modules/@noble/hashes/esm/legacy.d.ts.map +0 -1
  479. package/node_modules/@noble/hashes/esm/legacy.js +0 -281
  480. package/node_modules/@noble/hashes/esm/legacy.js.map +0 -1
  481. package/node_modules/@noble/hashes/esm/package.json +0 -10
  482. package/node_modules/@noble/hashes/esm/pbkdf2.d.ts +0 -23
  483. package/node_modules/@noble/hashes/esm/pbkdf2.d.ts.map +0 -1
  484. package/node_modules/@noble/hashes/esm/pbkdf2.js +0 -97
  485. package/node_modules/@noble/hashes/esm/pbkdf2.js.map +0 -1
  486. package/node_modules/@noble/hashes/esm/ripemd160.d.ts +0 -13
  487. package/node_modules/@noble/hashes/esm/ripemd160.d.ts.map +0 -1
  488. package/node_modules/@noble/hashes/esm/ripemd160.js +0 -13
  489. package/node_modules/@noble/hashes/esm/ripemd160.js.map +0 -1
  490. package/node_modules/@noble/hashes/esm/scrypt.d.ts +0 -34
  491. package/node_modules/@noble/hashes/esm/scrypt.d.ts.map +0 -1
  492. package/node_modules/@noble/hashes/esm/scrypt.js +0 -228
  493. package/node_modules/@noble/hashes/esm/scrypt.js.map +0 -1
  494. package/node_modules/@noble/hashes/esm/sha1.d.ts +0 -11
  495. package/node_modules/@noble/hashes/esm/sha1.d.ts.map +0 -1
  496. package/node_modules/@noble/hashes/esm/sha1.js +0 -11
  497. package/node_modules/@noble/hashes/esm/sha1.js.map +0 -1
  498. package/node_modules/@noble/hashes/esm/sha2.d.ts +0 -159
  499. package/node_modules/@noble/hashes/esm/sha2.d.ts.map +0 -1
  500. package/node_modules/@noble/hashes/esm/sha2.js +0 -375
  501. package/node_modules/@noble/hashes/esm/sha2.js.map +0 -1
  502. package/node_modules/@noble/hashes/esm/sha256.d.ts +0 -20
  503. package/node_modules/@noble/hashes/esm/sha256.d.ts.map +0 -1
  504. package/node_modules/@noble/hashes/esm/sha256.js +0 -20
  505. package/node_modules/@noble/hashes/esm/sha256.js.map +0 -1
  506. package/node_modules/@noble/hashes/esm/sha3-addons.d.ts +0 -142
  507. package/node_modules/@noble/hashes/esm/sha3-addons.d.ts.map +0 -1
  508. package/node_modules/@noble/hashes/esm/sha3-addons.js +0 -393
  509. package/node_modules/@noble/hashes/esm/sha3-addons.js.map +0 -1
  510. package/node_modules/@noble/hashes/esm/sha3.d.ts +0 -53
  511. package/node_modules/@noble/hashes/esm/sha3.d.ts.map +0 -1
  512. package/node_modules/@noble/hashes/esm/sha3.js +0 -234
  513. package/node_modules/@noble/hashes/esm/sha3.js.map +0 -1
  514. package/node_modules/@noble/hashes/esm/sha512.d.ts +0 -26
  515. package/node_modules/@noble/hashes/esm/sha512.d.ts.map +0 -1
  516. package/node_modules/@noble/hashes/esm/sha512.js +0 -26
  517. package/node_modules/@noble/hashes/esm/sha512.js.map +0 -1
  518. package/node_modules/@noble/hashes/esm/utils.d.ts +0 -161
  519. package/node_modules/@noble/hashes/esm/utils.d.ts.map +0 -1
  520. package/node_modules/@noble/hashes/esm/utils.js +0 -281
  521. package/node_modules/@noble/hashes/esm/utils.js.map +0 -1
  522. package/node_modules/@noble/hashes/hkdf.d.ts +0 -36
  523. package/node_modules/@noble/hashes/hkdf.d.ts.map +0 -1
  524. package/node_modules/@noble/hashes/hkdf.js +0 -88
  525. package/node_modules/@noble/hashes/hkdf.js.map +0 -1
  526. package/node_modules/@noble/hashes/hmac.d.ts +0 -35
  527. package/node_modules/@noble/hashes/hmac.d.ts.map +0 -1
  528. package/node_modules/@noble/hashes/hmac.js +0 -91
  529. package/node_modules/@noble/hashes/hmac.js.map +0 -1
  530. package/node_modules/@noble/hashes/index.d.ts +0 -1
  531. package/node_modules/@noble/hashes/index.d.ts.map +0 -1
  532. package/node_modules/@noble/hashes/index.js +0 -33
  533. package/node_modules/@noble/hashes/index.js.map +0 -1
  534. package/node_modules/@noble/hashes/legacy.d.ts +0 -71
  535. package/node_modules/@noble/hashes/legacy.d.ts.map +0 -1
  536. package/node_modules/@noble/hashes/legacy.js +0 -287
  537. package/node_modules/@noble/hashes/legacy.js.map +0 -1
  538. package/node_modules/@noble/hashes/package.json +0 -266
  539. package/node_modules/@noble/hashes/pbkdf2.d.ts +0 -23
  540. package/node_modules/@noble/hashes/pbkdf2.d.ts.map +0 -1
  541. package/node_modules/@noble/hashes/pbkdf2.js +0 -101
  542. package/node_modules/@noble/hashes/pbkdf2.js.map +0 -1
  543. package/node_modules/@noble/hashes/ripemd160.d.ts +0 -13
  544. package/node_modules/@noble/hashes/ripemd160.d.ts.map +0 -1
  545. package/node_modules/@noble/hashes/ripemd160.js +0 -16
  546. package/node_modules/@noble/hashes/ripemd160.js.map +0 -1
  547. package/node_modules/@noble/hashes/scrypt.d.ts +0 -34
  548. package/node_modules/@noble/hashes/scrypt.d.ts.map +0 -1
  549. package/node_modules/@noble/hashes/scrypt.js +0 -232
  550. package/node_modules/@noble/hashes/scrypt.js.map +0 -1
  551. package/node_modules/@noble/hashes/sha1.d.ts +0 -11
  552. package/node_modules/@noble/hashes/sha1.d.ts.map +0 -1
  553. package/node_modules/@noble/hashes/sha1.js +0 -14
  554. package/node_modules/@noble/hashes/sha1.js.map +0 -1
  555. package/node_modules/@noble/hashes/sha2.d.ts +0 -159
  556. package/node_modules/@noble/hashes/sha2.d.ts.map +0 -1
  557. package/node_modules/@noble/hashes/sha2.js +0 -384
  558. package/node_modules/@noble/hashes/sha2.js.map +0 -1
  559. package/node_modules/@noble/hashes/sha256.d.ts +0 -20
  560. package/node_modules/@noble/hashes/sha256.d.ts.map +0 -1
  561. package/node_modules/@noble/hashes/sha256.js +0 -23
  562. package/node_modules/@noble/hashes/sha256.js.map +0 -1
  563. package/node_modules/@noble/hashes/sha3-addons.d.ts +0 -142
  564. package/node_modules/@noble/hashes/sha3-addons.d.ts.map +0 -1
  565. package/node_modules/@noble/hashes/sha3-addons.js +0 -402
  566. package/node_modules/@noble/hashes/sha3-addons.js.map +0 -1
  567. package/node_modules/@noble/hashes/sha3.d.ts +0 -53
  568. package/node_modules/@noble/hashes/sha3.d.ts.map +0 -1
  569. package/node_modules/@noble/hashes/sha3.js +0 -239
  570. package/node_modules/@noble/hashes/sha3.js.map +0 -1
  571. package/node_modules/@noble/hashes/sha512.d.ts +0 -26
  572. package/node_modules/@noble/hashes/sha512.d.ts.map +0 -1
  573. package/node_modules/@noble/hashes/sha512.js +0 -29
  574. package/node_modules/@noble/hashes/sha512.js.map +0 -1
  575. package/node_modules/@noble/hashes/src/_assert.ts +0 -22
  576. package/node_modules/@noble/hashes/src/_blake.ts +0 -50
  577. package/node_modules/@noble/hashes/src/_md.ts +0 -176
  578. package/node_modules/@noble/hashes/src/_u64.ts +0 -91
  579. package/node_modules/@noble/hashes/src/argon2.ts +0 -497
  580. package/node_modules/@noble/hashes/src/blake1.ts +0 -534
  581. package/node_modules/@noble/hashes/src/blake2.ts +0 -486
  582. package/node_modules/@noble/hashes/src/blake2b.ts +0 -10
  583. package/node_modules/@noble/hashes/src/blake2s.ts +0 -20
  584. package/node_modules/@noble/hashes/src/blake3.ts +0 -272
  585. package/node_modules/@noble/hashes/src/crypto.ts +0 -9
  586. package/node_modules/@noble/hashes/src/cryptoNode.ts +0 -15
  587. package/node_modules/@noble/hashes/src/eskdf.ts +0 -187
  588. package/node_modules/@noble/hashes/src/hkdf.ts +0 -88
  589. package/node_modules/@noble/hashes/src/hmac.ts +0 -94
  590. package/node_modules/@noble/hashes/src/index.ts +0 -31
  591. package/node_modules/@noble/hashes/src/legacy.ts +0 -293
  592. package/node_modules/@noble/hashes/src/pbkdf2.ts +0 -122
  593. package/node_modules/@noble/hashes/src/ripemd160.ts +0 -12
  594. package/node_modules/@noble/hashes/src/scrypt.ts +0 -257
  595. package/node_modules/@noble/hashes/src/sha1.ts +0 -10
  596. package/node_modules/@noble/hashes/src/sha2.ts +0 -402
  597. package/node_modules/@noble/hashes/src/sha256.ts +0 -24
  598. package/node_modules/@noble/hashes/src/sha3-addons.ts +0 -499
  599. package/node_modules/@noble/hashes/src/sha3.ts +0 -258
  600. package/node_modules/@noble/hashes/src/sha512.ts +0 -34
  601. package/node_modules/@noble/hashes/src/utils.ts +0 -395
  602. package/node_modules/@noble/hashes/utils.d.ts +0 -161
  603. package/node_modules/@noble/hashes/utils.d.ts.map +0 -1
  604. package/node_modules/@noble/hashes/utils.js +0 -313
  605. package/node_modules/@noble/hashes/utils.js.map +0 -1
@@ -1,12 +1,7 @@
1
- // ../mmt-alta-parse/dist/index.js
1
+ // ../mmt-base/dist/mmtp.js
2
2
  var MPU_HEADER_SIZE = 8;
3
3
  var MPU_LENGTH_FIELD_SIZE = 2;
4
4
  var MPU_HEADER_REMAINDER_SIZE = MPU_HEADER_SIZE - MPU_LENGTH_FIELD_SIZE;
5
-
6
- // ../mmt-base/dist/mmtp.js
7
- var MPU_HEADER_SIZE2 = 8;
8
- var MPU_LENGTH_FIELD_SIZE2 = 2;
9
- var MPU_HEADER_REMAINDER_SIZE2 = MPU_HEADER_SIZE2 - MPU_LENGTH_FIELD_SIZE2;
10
5
  var MMTP_SIGNALING_PACKET_ID = 0;
11
6
  var MMTP_PACKET_ID_MAX = 65535;
12
7
 
@@ -17,7 +12,6 @@ var NTP_SHORT_HALF_RANGE = 2 ** (NTP_SHORT_BITS - 1);
17
12
 
18
13
  // ../mmt-base/dist/fec-block-profile.js
19
14
  var FEC_BLOCK_PROFILES = Object.freeze([
20
- "k-padded-v1",
21
15
  "iso-ssbg1-v1"
22
16
  ]);
23
17
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../../mmt-alta-parse/src/index.ts", "../../mmt-base/src/mmtp.ts", "../../mmt-base/src/ntp-short.ts", "../../mmt-base/src/fec-block-profile.ts", "../src/multicast-input.ts"],
4
- "sourcesContent": ["/**\n * Zero-dependency helper for extracting the detached ALTA authenticator trailer\n * from an MMTP source packet.\n *\n * Lives in its own leaf package so both `@blockcast/mmt-container` (wire-path\n * parsing) and `@blockcast/fec-worker` (FEC-recovered-source parsing) can share\n * one implementation without creating a package cycle between them.\n *\n * Spec: ISO/IEC 23008-1:2023 + draft-ramadan-moq-mmt \u00A712 (detached trailer, Path 2).\n */\n\n// --- Private MMTP/MPU/MFU constants (ISO/IEC 23008-1 \u00A79.2.2\u20139.2.3) ---\n// Duplicated here as immutable spec values so this package stays a leaf with\n// zero `@blockcast/*` dependencies. Re-deriving from `mmt-container` would\n// reintroduce the package cycle this split exists to avoid.\n\nconst MMTP_HEADER_SIZE = 12\nconst MPU_HEADER_SIZE = 8\nconst MPU_LENGTH_FIELD_SIZE = 2\nconst MPU_HEADER_REMAINDER_SIZE = MPU_HEADER_SIZE - MPU_LENGTH_FIELD_SIZE\nconst TIMED_MFU_DU_HEADER_SIZE = 14\nconst NONTIMED_MFU_DU_HEADER_SIZE = 4\nconst PACKET_TYPE_MPU = 0x00\n\n// --- Public ALTA trailer constants ---\n\n/**\n * MMTP header-extension ext_type assigned to the detached ALTA authenticator\n * trailer (Path 2 / 08-07). Carried in an `[ext_type][ext_length][auth]`\n * block appended after the MPU's declared payload bytes, before the 4-byte\n * Source FEC Payload ID. MMTP X-bit stays 0 \u2014 non-ALTA MMTP receivers treat\n * the trailer as bytes past `payload_length` and skip it (per ISO 23008-1\n * \u00A79.2.2 \"may be discarded without impacting correct processing\").\n */\nexport const MMTP_EXT_TYPE_ALTA_AUTH = 0xf0a1\n\n/** Fixed size of the trailer's `[ext_type][ext_length]` prefix (2 + 2 BE). */\nexport const MMTP_EXT_TRAILER_HEADER_SIZE = 4\n\n/** Result of extracting the detached ALTA authenticator from an MMTP packet. */\nexport interface AltaTrailer {\n /** Authenticator bytes (no ext_type/ext_length prefix, no trailing padding). */\n auth: Uint8Array\n /**\n * Wire-packet byte offset where the signed payload ends, i.e. where the\n * trailer begins. Caller uses this to produce\n * `payload = wirePacket.subarray(0, signedEnd)` for verify_detached.\n */\n signedEnd: number\n}\n\n/**\n * Extract the detached ALTA authenticator trailer from an MMTP source packet.\n *\n * Wire layout:\n * `[MMTP hdr][length(2)][remaining MPU hdr(6)][DU?][media][TRAILER][SS_ID?]`\n * `payload_length = 6 + DU bytes + media bytes`\n *\n * Returns `null` when:\n * - the packet is too small / malformed,\n * - `payload_length` cannot contain the remaining six MPU-header bytes,\n * - the declared payload overruns the packet,\n * - no bytes remain between `payload_length` and the SS_ID,\n * - the trailer's `ext_type` isn't {@link MMTP_EXT_TYPE_ALTA_AUTH},\n * - the declared `ext_length` would extend past the SS_ID boundary.\n *\n * Non-MPU packets (repair, signaling) always return `null`.\n *\n * @param options.skipFecId When true, do NOT reserve 4 trailing bytes for the\n * Source FEC Payload ID. Use on FEC-recovered symbols where the PID has\n * already been stripped upstream.\n */\nexport function extractAltaTrailer(\n mmtpPacket: Uint8Array,\n options?: { skipFecId?: boolean },\n): AltaTrailer | null {\n if (mmtpPacket.length < MMTP_HEADER_SIZE + MPU_HEADER_SIZE) return null\n // ISO/IEC 23008-1:2023 Figure 8 places V=0 packet_type in the low six bits.\n const packetType = mmtpPacket[1]! & 0x3f\n if (packetType !== PACKET_TYPE_MPU) return null\n\n const fecType = (mmtpPacket[0]! >> 3) & 0x03\n let headerLength = MMTP_HEADER_SIZE\n if (fecType < 2 && (mmtpPacket[0]! & 0x20) !== 0) headerLength += 4\n if (fecType < 2 && (mmtpPacket[0]! & 0x02) !== 0) {\n if (headerLength + 4 > mmtpPacket.length) return null\n const extensionLength = new DataView(\n mmtpPacket.buffer,\n mmtpPacket.byteOffset + headerLength + 2,\n 2,\n ).getUint16(0, false)\n headerLength += 4 + extensionLength\n }\n if (headerLength + MPU_HEADER_SIZE > mmtpPacket.length) return null\n\n const payload = mmtpPacket.subarray(headerLength)\n\n // Inline a minimal MPU-header read \u2014 only the fields extractAltaTrailer needs.\n // payloadLength: 16 BE at bytes 0-1.\n // Byte 2 packs FT(4) | T(1) | FI(2) | A(1).\n const payloadLength = (payload[0]! << 8) | payload[1]!\n const flagsByte = payload[2]!\n const fragmentType = (flagsByte >> 4) & 0x0f\n const timed = ((flagsByte >> 3) & 0x01) === 1\n const fragmentationIndicator = (flagsByte >> 1) & 0x03\n\n // Only MFU packets (FT=2) carry the detached trailer.\n if (fragmentType !== 2) return null\n\n // FI=0/1 structurally carries a DU header; FI=2/3 does not. Require at\n // least one media byte after that deterministic layout so the bytes at the\n // declared boundary cannot impersonate ALTA for a header-only fragment.\n const dataUnitHeaderSize = fragmentationIndicator <= 1\n ? (timed ? TIMED_MFU_DU_HEADER_SIZE : NONTIMED_MFU_DU_HEADER_SIZE)\n : 0\n if (payloadLength <= MPU_HEADER_REMAINDER_SIZE + dataUnitHeaderSize) return null\n\n if (fecType > 1) return null\n const hasFecId = !options?.skipFecId && fecType === 1\n const payloadEnd = hasFecId ? payload.length - 4 : payload.length\n const declaredEnd = MPU_LENGTH_FIELD_SIZE + payloadLength\n if (declaredEnd > payloadEnd) return null\n\n if (declaredEnd + MMTP_EXT_TRAILER_HEADER_SIZE > payloadEnd) return null\n const extType = (payload[declaredEnd]! << 8) | payload[declaredEnd + 1]!\n if (extType !== MMTP_EXT_TYPE_ALTA_AUTH) return null\n const extLen = (payload[declaredEnd + 2]! << 8) | payload[declaredEnd + 3]!\n\n const authStart = declaredEnd + MMTP_EXT_TRAILER_HEADER_SIZE\n if (authStart + extLen !== payloadEnd) return null\n\n return {\n auth: payload.subarray(authStart, payloadEnd),\n signedEnd: headerLength + declaredEnd,\n }\n}\n", "import {\n MMTP_EXT_TYPE_ALTA_AUTH as _LEAF_MMTP_EXT_TYPE_ALTA_AUTH,\n MMTP_EXT_TRAILER_HEADER_SIZE as _LEAF_MMTP_EXT_TRAILER_HEADER_SIZE,\n extractAltaTrailer as _leafExtractAltaTrailer,\n type AltaTrailer as _LeafAltaTrailer,\n} from '@blockcast/mmt-alta-parse'\n\n/**\n * @mmt/container - MMTP Parser\n *\n * MMTP (MMT Protocol) packet parser based on ISO/IEC 23008-1:2023 Section 9.2\n *\n * MMTP header format (12 bytes):\n * - Byte 0: V(2) | C(1) | FEC_type(2) | r(1) | X(1) | R(1)\n * - Byte 1: reserved(2) | packet_type(6)\n * - Bytes 2-3: packet_id (big-endian)\n * - Bytes 4-7: timestamp (big-endian, 32-bit)\n * - Bytes 8-11: sequence_number (big-endian)\n *\n * FEC_type values:\n * - 0: No FEC / source packet\n * - 1: FEC mode 0 (source with FEC payload ID)\n * - 2: FEC repair packet mode 0\n * - 3: FEC repair packet mode 1\n *\n * @packageDocumentation\n */\n\nexport interface MMTPHeader {\n version: number\n /** Packet type: 0x00=MPU, 0x02=Signaling, 0x03=Repair */\n packetType: number\n packetId: number\n timestamp: bigint\n sequenceNumber: number\n payloadLength: number\n fecType: number\n /** C flag: header carries a 32-bit packet_counter. */\n packetCounterFlag: boolean\n packetCounter?: number\n /** X flag: header carries an extension header and value. */\n extensionFlag: boolean\n extension?: { type: number; data: Uint8Array }\n /** Byte offset at which the MMTP payload begins. */\n headerLength: number\n /** Random Access Point flag - indicates keyframe (ISO 23008-1, byte 0 bit 0) */\n rapFlag: boolean\n}\n\nexport interface MPUHeader {\n mpuSequenceNumber: number\n fragmentationIndicator: number // FI: 0=complete, 1=first, 2=middle, 3=last\n /** 8-bit MPU fragment_counter from byte 3. */\n fragmentCounter: number\n /** T flag: 1=timed media (14-byte MFU DU header), 0=non-timed (4-byte header) */\n timedFlag: number\n /** A flag: aggregated MPU payload. This strict profile requires A=0. */\n aggregationFlag: boolean\n /** MPU payload_length (bytes 0-1): bytes after the length field, including\n * the remaining six MPU-header bytes and all data-unit bytes. */\n payloadLength: number\n}\n\nexport interface MFUDuHeader {\n movieFragmentSequenceNumber: number\n sampleNumber: number\n offset: number\n priority: number\n dependencyCounter: number\n}\n\nexport interface MMTPPacket {\n header: MMTPHeader\n payload: Uint8Array\n}\n\nexport const MMTP_HEADER_SIZE = 12\nexport const MPU_HEADER_SIZE = 8\nexport const MFU_DU_HEADER_SIZE_TIMED = 14 // T=1: timed media (video/audio samples)\nexport const MFU_DU_HEADER_SIZE_NONTIMED = 4 // T=0: non-timed media (item_id only)\n\nconst MPU_LENGTH_FIELD_SIZE = 2\nconst MPU_HEADER_REMAINDER_SIZE = MPU_HEADER_SIZE - MPU_LENGTH_FIELD_SIZE\nconst SOURCE_FEC_PAYLOAD_ID_SIZE = 4\n\n// MMTP Packet Types (ISO/IEC 23008-1:2023 Section 9.2.2)\nexport const PACKET_TYPE_MPU = 0x00\nexport const PACKET_TYPE_SIGNALING = 0x02\nexport const PACKET_TYPE_REPAIR = 0x03\n\n// AL-FEC Signaling Message ID (ISO/IEC 23008-1:2023 Amendment 1:2025)\nexport const ALFEC_MESSAGE_ID = 0x0203\n\n/**\n * MMTP `packet_id` of the signaling flow, which carries PACKET_TYPE_SIGNALING\n * packets such as the AL-FEC message (ALFEC_MESSAGE_ID). The FFmpeg `moq_mmt`\n * muxer sends that message on this packet_id into the same multicast group as\n * the media, and moqcast-draft (PR #44) makes advertising it for a track a\n * MUST NOT. A track's packet_id is therefore 1..MMTP_PACKET_ID_MAX.\n */\nexport const MMTP_SIGNALING_PACKET_ID = 0\n\n/** Largest value of the 16-bit MMTP `packet_id` header field. */\nexport const MMTP_PACKET_ID_MAX = 0xffff\n\n// FEC Code IDs\nexport const FEC_CODE_XOR = 1\nexport const FEC_CODE_RAPTORQ = 2\n\n/**\n * Parse MMTP header from buffer\n */\nexport function parseMMTPHeader(buffer: Uint8Array): MMTPHeader {\n if (buffer.length < MMTP_HEADER_SIZE) {\n throw new Error(`MMTP packet too small: ${buffer.length} bytes`)\n }\n\n const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength)\n\n const version = (buffer[0]! >> 6) & 0x03\n if (version !== 0) {\n throw new Error(`Unsupported MMTP version: ${version}`)\n }\n\n // Extract FEC type from byte 0, bits 3-4\n const fecType = (buffer[0]! >> 3) & 0x03\n const packetCounterFlag = (buffer[0]! & 0x20) !== 0\n const extensionFlag = (buffer[0]! & 0x02) !== 0\n\n // Extract RAP flag from byte 0, bit 0 (R bit - Random Access Point)\n const rapFlag = (buffer[0]! & 0x01) === 1\n\n // ISO/IEC 23008-1:2023 Figure 8 (V=0) places reserved(2) before\n // packet_type(6), so packet_type occupies the low six bits of byte 1.\n const packetType = buffer[1]! & 0x3F\n\n let offset = 2\n\n // Packet ID (2 bytes, big-endian)\n const packetId = view.getUint16(offset, false)\n offset += 2\n\n // Timestamp (4 bytes, big-endian)\n const timestamp = BigInt(view.getUint32(offset, false))\n offset += 4\n\n // Sequence number (4 bytes, big-endian)\n const sequenceNumber = view.getUint32(offset, false)\n offset += 4\n\n let packetCounter: number | undefined\n if (packetCounterFlag) {\n if (offset + 4 > buffer.length) throw new Error('MMTP packet truncated in packet_counter')\n packetCounter = view.getUint32(offset, false)\n offset += 4\n }\n\n let extension: { type: number; data: Uint8Array } | undefined\n if (extensionFlag) {\n if (offset + 4 > buffer.length) throw new Error('MMTP packet truncated in header extension')\n const type = view.getUint16(offset, false)\n const length = view.getUint16(offset + 2, false)\n offset += 4\n if (offset + length > buffer.length) throw new Error('MMTP packet truncated in header extension value')\n extension = { type, data: buffer.subarray(offset, offset + length) }\n offset += length\n }\n\n return {\n version,\n packetType,\n packetId,\n timestamp,\n sequenceNumber,\n payloadLength: buffer.length - offset,\n fecType,\n packetCounterFlag,\n packetCounter,\n extensionFlag,\n extension,\n headerLength: offset,\n rapFlag,\n }\n}\n\n/**\n * The MMTP `packet_id` of `buffer`, or null exactly when parseMMTPHeader would\n * throw for it (short buffer, V != 0, truncated packet_counter or header\n * extension).\n *\n * This is the per-packet filter a receiver runs before any other work, so it\n * builds no header object, DataView or BigInt timestamp (BLO-36599). Use\n * parseMMTPHeader when any other header field is needed.\n */\nexport function readMMTPPacketId(buffer: Uint8Array): number | null {\n if (buffer.length < MMTP_HEADER_SIZE) return null\n const flags = buffer[0]!\n if (((flags >> 6) & 0x03) !== 0) return null\n let offset = MMTP_HEADER_SIZE\n if ((flags & 0x20) !== 0) {\n if (offset + 4 > buffer.length) return null\n offset += 4\n }\n if ((flags & 0x02) !== 0) {\n if (offset + 4 > buffer.length) return null\n const length = (buffer[offset + 2]! << 8) | buffer[offset + 3]!\n if (offset + 4 + length > buffer.length) return null\n }\n return (buffer[2]! << 8) | buffer[3]!\n}\n\n/**\n * Unwrap MMTP packet to extract payload\n */\nexport function unwrapMMTP(buffer: Uint8Array): MMTPPacket {\n const header = parseMMTPHeader(buffer)\n const payload = buffer.subarray(header.headerLength)\n\n return { header, payload }\n}\n\n/**\n * Parse MPU header from payload\n * ISO/IEC 23008-1:2023 Section 9.2.3.3\n *\n * MPU Header format (8 bytes) as written by FFmpeg/libmmt:\n * - Bytes 0-1: payload_length (16 bits, big-endian)\n * - Byte 2: FT(4) | T(1) | FI(2) | A(1)\n * - Byte 3: fragment_counter (8 bits)\n * - Bytes 4-7: MPU_sequence_number (32 bits, big-endian)\n */\nexport function parseMPUHeader(payload: Uint8Array): MPUHeader {\n if (payload.length < MPU_HEADER_SIZE) {\n throw new Error(`Payload too small for MPU header: ${payload.length} bytes`)\n }\n\n const view = new DataView(payload.buffer, payload.byteOffset, payload.byteLength)\n\n // Bytes 0-1: payload_length (16 bits, big-endian)\n const payloadLength = view.getUint16(0, false)\n\n // Byte 2: FT(4) | T(1) | FI(2) | A(1)\n const flagsByte = payload[2]!\n const timedFlag = (flagsByte >> 3) & 0x01\n const fragmentationIndicator = (flagsByte >> 1) & 0x03\n const aggregationFlag = (flagsByte & 0x01) !== 0\n\n // Byte 3: fragment_counter (8 bits). The high bit is part of the counter in\n // the current libmmt/FFmpeg writer ABI.\n const fragmentCounter = payload[3]!\n\n // Bytes 4-7: MPU_sequence_number (32 bits, big-endian)\n const mpuSequenceNumber = view.getUint32(4, false)\n\n return {\n mpuSequenceNumber,\n fragmentationIndicator,\n fragmentCounter,\n timedFlag,\n aggregationFlag,\n payloadLength,\n }\n}\n\nexport function parseTimedMfuDuHeader(payload: Uint8Array, offset: number): MFUDuHeader | null {\n if (offset < 0 || offset + MFU_DU_HEADER_SIZE_TIMED > payload.length) return null\n const view = new DataView(payload.buffer, payload.byteOffset + offset, MFU_DU_HEADER_SIZE_TIMED)\n return {\n movieFragmentSequenceNumber: view.getUint32(0, false),\n sampleNumber: view.getUint32(4, false),\n offset: view.getUint32(8, false),\n priority: payload[offset + 12]!,\n dependencyCounter: payload[offset + 13]!,\n }\n}\n\n/**\n * Why `extractMFUPayload` refused a packet.\n *\n * These identifiers are part of the public contract: callers surface them in\n * diagnostics, so renaming one is a breaking change.\n */\nexport type MFURejectionGate =\n | 'packet-shorter-than-mmtp-plus-mpu-header'\n | 'mmtp-header-unparseable'\n | 'not-an-mpu-packet'\n | 'payload-shorter-than-mpu-header'\n | 'aggregation-flag-set'\n | 'mpu-payload-length-below-header-remainder'\n | 'mpu-declared-end-exceeds-payload'\n | 'mfu-offset-at-or-past-declared-end'\n | 'source-fec-payload-id-width-mismatch'\n | 'declared-trailers-unresolved'\n\nexport interface MFURejection {\n gate: MFURejectionGate\n /** Human-readable detail, including the byte arithmetic that decided it. */\n detail: string\n}\n\ninterface MfuPayloadLayout {\n hasMfuDuHeader: boolean\n mfuOffset: number\n dataEnd: number\n}\n\nfunction resolveMfuPayloadLayout(\n mpu: MPUHeader,\n fragmentType: number,\n payload: Uint8Array,\n): { layout: MfuPayloadLayout } | { rejection: MFURejection } {\n if (mpu.payloadLength < MPU_HEADER_REMAINDER_SIZE) {\n return {\n rejection: {\n gate: 'mpu-payload-length-below-header-remainder',\n detail: `payload_length=${mpu.payloadLength} < ${MPU_HEADER_REMAINDER_SIZE}`,\n },\n }\n }\n\n // ISO/IEC 23008-1 defines payload_length from immediately after its own\n // two-byte field. It therefore includes the remaining six MPU-header bytes.\n const declaredEnd = MPU_LENGTH_FIELD_SIZE + mpu.payloadLength\n if (declaredEnd > payload.length) {\n return {\n rejection: {\n gate: 'mpu-declared-end-exceeds-payload',\n detail:\n `payload_length=${mpu.payloadLength} implies end=${declaredEnd}, ` +\n `payload is ${payload.length} bytes`,\n },\n }\n }\n\n const hasHeader = fragmentType === 2 && mpu.fragmentationIndicator <= 1\n const mfuHeaderSize = mpu.timedFlag ? MFU_DU_HEADER_SIZE_TIMED : MFU_DU_HEADER_SIZE_NONTIMED\n const mfuOffset = MPU_HEADER_SIZE + (hasHeader ? mfuHeaderSize : 0)\n if (mfuOffset >= declaredEnd) {\n return {\n rejection: {\n gate: 'mfu-offset-at-or-past-declared-end',\n detail: `mfuOffset=${mfuOffset} >= declaredEnd=${declaredEnd}`,\n },\n }\n }\n\n return {\n layout: {\n hasMfuDuHeader: hasHeader,\n mfuOffset,\n dataEnd: declaredEnd,\n },\n }\n}\n\nfunction isEmptyOrExactAltaTrailer(payload: Uint8Array, start: number, end: number): boolean {\n if (start === end) return true\n if (start < 0 || end > payload.length || start + _LEAF_MMTP_EXT_TRAILER_HEADER_SIZE > end) {\n return false\n }\n\n const view = new DataView(\n payload.buffer,\n payload.byteOffset + start,\n end - start,\n )\n if (view.getUint16(0, false) !== _LEAF_MMTP_EXT_TYPE_ALTA_AUTH) return false\n\n const authLength = view.getUint16(2, false)\n return start + _LEAF_MMTP_EXT_TRAILER_HEADER_SIZE + authLength === end\n}\n\ninterface DeclaredTrailerLayout {\n hasFecId: boolean\n}\n\nfunction resolveDeclaredTrailers(\n payload: Uint8Array,\n declaredEnd: number,\n fecType: number,\n options?: { skipFecId?: boolean },\n): DeclaredTrailerLayout | null {\n if (declaredEnd > payload.length) return null\n\n if (options?.skipFecId) {\n return isEmptyOrExactAltaTrailer(payload, declaredEnd, payload.length)\n ? { hasFecId: false }\n : null\n }\n\n if (fecType === 1) {\n const fecStart = payload.length - SOURCE_FEC_PAYLOAD_ID_SIZE\n if (fecStart < declaredEnd || !isEmptyOrExactAltaTrailer(payload, declaredEnd, fecStart)) {\n return null\n }\n return { hasFecId: true }\n }\n if (fecType !== 0) return null\n\n if (isEmptyOrExactAltaTrailer(payload, declaredEnd, payload.length)) {\n return { hasFecId: false }\n }\n\n return null\n}\n\n/**\n * Extract NAL unit payload from MMTP packet\n */\nexport function extractNALUnit(mmtpPacket: Uint8Array): Uint8Array {\n const extraction = extractMFUPayload(mmtpPacket)\n if (!extraction) throw new Error('Malformed MMTP MPU payload')\n return extraction.data\n}\n\n/** Result of extracting MFU payload from an MMTP packet */\nexport interface MFUExtraction {\n /** MMTP header fields */\n header: MMTPHeader\n /** MPU header fields */\n mpu: MPUHeader\n /** Fragment type (FT field, 4 bits): 0=metadata, 2=MFU data */\n fragmentType: number\n /** MFU payload data (video/audio bytes, headers stripped, FEC ID excluded) */\n data: Uint8Array\n /** Parsed timed MFU DU header when present, otherwise null */\n mfuDuHeader: MFUDuHeader | null\n /** Non-timed MFU Item_ID when the 4-byte DU header is present. */\n itemId: number | null\n /** Source FEC Payload ID if present (`FEC_type == 1`), or null */\n fecPayloadId: { ssId: number } | null\n}\n\n/**\n * Extract MFU payload from a raw MMTP packet.\n *\n * Handles all the byte arithmetic for:\n * - MMTP header (12 bytes)\n * - MPU sub-header (8 bytes, payloadLength field)\n * - MFU DU header (14 bytes timed / 4 bytes non-timed, only for FI \u2264 1)\n * - Source FEC Payload ID (4 bytes at end, only when `FEC_type == 1`)\n *\n * `payloadLength` is the ISO length-after-field value: six remaining MPU-header\n * bytes, then the MFU DU header when present, then media. ALTA and Source FEC\n * trailers are outside that declared span and must consume the exact suffix.\n *\n * @param mmtpPacket Raw MMTP packet (from multicast or MoQ)\n * @param options.skipFecId When true, do NOT subtract the 4-byte Source FEC Payload ID\n * from the payload boundary, even if fecType >= 1 in the MMTP header. Use this for\n * FEC-recovered symbols where the FEC PID was already stripped before WASM decoding\n * but the fecType bits in byte 0 still reflect the original packet (FEC Type=1 per\n * draft-ramadan-moq-mmt-00 \u00A73.1).\n * @returns Extracted MFU data + metadata, or null if packet is too small\n */\ninterface MfuPlan {\n header: MMTPHeader\n payload: Uint8Array\n mpu: MPUHeader\n fragmentType: number\n layout: MfuPayloadLayout\n trailerLayout: DeclaredTrailerLayout\n}\n\n/**\n * Single source of truth for whether a packet yields an MFU payload.\n *\n * `extractMFUPayload` and `explainMFURejection` both delegate here, so the\n * reason reported to operators can never drift from the decision the parser\n * actually made.\n */\nfunction planMfuExtraction(\n mmtpPacket: Uint8Array,\n options?: { skipFecId?: boolean },\n parsedHeader?: MMTPHeader,\n): { plan: MfuPlan } | { rejection: MFURejection } {\n if (mmtpPacket.length < MMTP_HEADER_SIZE + MPU_HEADER_SIZE) {\n return {\n rejection: {\n gate: 'packet-shorter-than-mmtp-plus-mpu-header',\n detail: `${mmtpPacket.length} bytes < ${MMTP_HEADER_SIZE + MPU_HEADER_SIZE}`,\n },\n }\n }\n\n let header: MMTPHeader\n if (parsedHeader) {\n header = parsedHeader\n } else {\n try {\n header = parseMMTPHeader(mmtpPacket)\n } catch (error) {\n return {\n rejection: {\n gate: 'mmtp-header-unparseable',\n detail: error instanceof Error ? error.message : String(error),\n },\n }\n }\n }\n if (header.packetType !== PACKET_TYPE_MPU) {\n return {\n rejection: {\n gate: 'not-an-mpu-packet',\n detail: `packetType=0x${header.packetType.toString(16).padStart(2, '0')}`,\n },\n }\n }\n\n const payload = mmtpPacket.subarray(header.headerLength)\n if (payload.length < MPU_HEADER_SIZE) {\n return {\n rejection: {\n gate: 'payload-shorter-than-mpu-header',\n detail: `${payload.length} bytes < ${MPU_HEADER_SIZE}`,\n },\n }\n }\n\n const mpu = parseMPUHeader(payload)\n if (mpu.aggregationFlag) {\n return {\n rejection: { gate: 'aggregation-flag-set', detail: 'strict profile requires A=0' },\n }\n }\n\n // Fragment type from byte 2 upper nibble (not returned by parseMPUHeader)\n const fragmentType = (payload[2]! >> 4) & 0x0f\n\n const layoutResult = resolveMfuPayloadLayout(mpu, fragmentType, payload)\n if ('rejection' in layoutResult) return layoutResult\n const { layout } = layoutResult\n\n const trailerLayout = resolveDeclaredTrailers(\n payload,\n layout.dataEnd,\n header.fecType,\n options,\n )\n if (!trailerLayout) {\n // The most common cause is a producer whose Source FEC Payload ID width\n // disagrees with ISO/IEC 23008-1 \u00A7C.5.2. Name that explicitly rather than\n // leaving operators to infer it from a generic trailer error.\n if (!options?.skipFecId && header.fecType === 1) {\n const observed = payload.length - layout.dataEnd\n if (observed !== SOURCE_FEC_PAYLOAD_ID_SIZE) {\n return {\n rejection: {\n gate: 'source-fec-payload-id-width-mismatch',\n detail:\n `producer wrote a ${observed}-byte Source FEC Payload ID, ` +\n `ISO/IEC 23008-1 \u00A7C.5.2 requires ${SOURCE_FEC_PAYLOAD_ID_SIZE} ` +\n `(payload=${payload.length}, declaredEnd=${layout.dataEnd})`,\n },\n }\n }\n }\n return {\n rejection: {\n gate: 'declared-trailers-unresolved',\n detail:\n `trailer bytes [${layout.dataEnd}, ${payload.length}) are neither empty ` +\n `nor an exact ALTA trailer (fecType=${header.fecType})`,\n },\n }\n }\n\n return { plan: { header, payload, mpu, fragmentType, layout, trailerLayout } }\n}\n\n/**\n * Report why `extractMFUPayload` would refuse this packet, or null if it would\n * accept it.\n *\n * `extractMFUPayload` returns bare `null` on ten distinct conditions, which\n * makes a producer/consumer framing disagreement indistinguishable from a\n * publisher that never started \u2014 both render as silence. Use this wherever\n * that distinction matters: diagnostics, contract tests, and operator-facing\n * logs.\n */\nexport function explainMFURejection(\n mmtpPacket: Uint8Array,\n options?: { skipFecId?: boolean },\n): MFURejection | null {\n const result = planMfuExtraction(mmtpPacket, options)\n return 'rejection' in result ? result.rejection : null\n}\n\nexport function extractMFUPayload(\n mmtpPacket: Uint8Array,\n options?: { skipFecId?: boolean },\n): MFUExtraction | null {\n return extractPlannedMfu(mmtpPacket, planMfuExtraction(mmtpPacket, options))\n}\n\n/**\n * extractMFUPayload for a caller that already holds this packet's parsed MMTP\n * header, so the header is not parsed a second time (BLO-36599).\n *\n * `header` must be `parseMMTPHeader(mmtpPacket)` of this same packet; the\n * result is then identical to `extractMFUPayload(mmtpPacket, options)`.\n */\nexport function extractMFUPayloadWithHeader(\n mmtpPacket: Uint8Array,\n header: MMTPHeader,\n options?: { skipFecId?: boolean },\n): MFUExtraction | null {\n return extractPlannedMfu(mmtpPacket, planMfuExtraction(mmtpPacket, options, header))\n}\n\nfunction extractPlannedMfu(\n mmtpPacket: Uint8Array,\n result: { plan: MfuPlan } | { rejection: MFURejection },\n): MFUExtraction | null {\n if ('rejection' in result) return null\n const { header, payload, mpu, fragmentType, layout, trailerLayout } = result.plan\n\n const mfuDuHeaderPresent = layout.hasMfuDuHeader\n const mfuOffset = layout.mfuOffset\n const dataEnd = layout.dataEnd\n\n const mfuDuHeader =\n mfuDuHeaderPresent && mpu.timedFlag === 1\n ? parseTimedMfuDuHeader(payload, MPU_HEADER_SIZE)\n : null\n const itemId =\n mfuDuHeaderPresent && mpu.timedFlag === 0\n ? new DataView(payload.buffer, payload.byteOffset + MPU_HEADER_SIZE, 4).getUint32(0, false)\n : null\n\n const data = payload.subarray(mfuOffset, dataEnd)\n\n const fecPayloadId = trailerLayout.hasFecId\n ? {\n ssId: new DataView(\n mmtpPacket.buffer,\n mmtpPacket.byteOffset + mmtpPacket.length - SOURCE_FEC_PAYLOAD_ID_SIZE,\n SOURCE_FEC_PAYLOAD_ID_SIZE,\n ).getUint32(0, false),\n }\n : null\n\n return { header, mpu, fragmentType, data, mfuDuHeader, itemId, fecPayloadId }\n}\n\n/**\n * Extract timestamp from MMTP packet\n */\nexport function extractTimestamp(mmtpPacket: Uint8Array): bigint {\n const { header } = unwrapMMTP(mmtpPacket)\n return header.timestamp\n}\n\n/**\n * Check if MMTP packet is an FEC repair packet\n * FEC_type 2 and 3 are repair packets\n */\nexport function isFecRepairPacket(mmtpPacket: Uint8Array): boolean {\n if (mmtpPacket.length < 1) return false\n const fecType = (mmtpPacket[0]! >> 3) & 0x03\n return fecType >= 2\n}\n\n/**\n * Extract Source FEC Payload ID (SS_ID) from the last 4 bytes of an MMTP\n * source packet per ISO/IEC 23008-1:2023 Section C.5.2 Figure C.10.\n *\n * SS_ID is a flat 32-bit monotonic counter (incremented by 1 per packet\n * for ssbg_mode0 and ssbg_mode1). Block membership and ESI use the\n * configured K; repair SS_Start identifies the first SS_ID of the block and\n * SSB_length records the authored source-symbol count:\n * SBN = floor(SS_ID / K), ESI = SS_ID % K\n * SS_Start = SBN * K (first SS_ID of the block)\n *\n * This matches FFmpeg moqenc_mmt.c, the drafts (draft-ramadan-moq-fec \u00A76.2),\n * and ATSC A/331 \u00A78.1.2.5 which defers to ISO 23008-1 Annex C.\n */\nexport function extractFecPayloadId(mmtpPacket: Uint8Array): { ssId: number } | null {\n const header = parseHeaderForSourceFecId(mmtpPacket)\n return header ? sourceFecPayloadIdOf(mmtpPacket, header) : null\n}\n\n/**\n * Return true when FEC_type=1 and the exact suffix after the ISO-declared MPU\n * payload contains a four-byte Source FEC Payload ID.\n */\nexport function hasSourceFecPayloadId(mmtpPacket: Uint8Array): boolean {\n const header = parseHeaderForSourceFecId(mmtpPacket)\n return header !== null && sourceFecPayloadIdOf(mmtpPacket, header) !== null\n}\n\nfunction parseHeaderForSourceFecId(mmtpPacket: Uint8Array): MMTPHeader | null {\n if (mmtpPacket.length < MMTP_HEADER_SIZE + MPU_HEADER_SIZE) return null\n try {\n return parseMMTPHeader(mmtpPacket)\n } catch {\n return null\n }\n}\n\n/**\n * The Source FEC Payload ID of a packet whose MMTP header the caller already\n * parsed, or null when the packet carries none: present only when FEC_type=1\n * and the exact suffix after the ISO-declared MPU payload holds the four-byte\n * ID (ISO/IEC 23008-1 \u00A7C.5.2). Same answer as extractFecPayloadId, without a\n * second header parse (BLO-36599).\n *\n * `header` must be `parseMMTPHeader(mmtpPacket)` of this same packet.\n */\nexport function sourceFecPayloadIdOf(\n mmtpPacket: Uint8Array,\n header: MMTPHeader,\n): { ssId: number } | null {\n if (mmtpPacket.length < MMTP_HEADER_SIZE + MPU_HEADER_SIZE) return null\n if (header.packetType !== PACKET_TYPE_MPU) return null\n\n const payload = mmtpPacket.subarray(header.headerLength)\n if (payload.length < MPU_HEADER_SIZE) return null\n const payloadLength = (payload[0]! << 8) | payload[1]!\n if (payloadLength < MPU_HEADER_REMAINDER_SIZE) return null\n const declaredEnd = MPU_LENGTH_FIELD_SIZE + payloadLength\n if (!resolveDeclaredTrailers(payload, declaredEnd, header.fecType)?.hasFecId) return null\n\n // Flat 4-byte SS_ID: encoder writes block_id * K + symbol_id\n const off = mmtpPacket.length - SOURCE_FEC_PAYLOAD_ID_SIZE\n return {\n ssId:\n ((mmtpPacket[off]! << 24) |\n (mmtpPacket[off + 1]! << 16) |\n (mmtpPacket[off + 2]! << 8) |\n mmtpPacket[off + 3]!) >>>\n 0,\n }\n}\n\n/**\n * MMTP header-extension ext_type assigned to the detached ALTA authenticator\n * trailer (Path 2 / 08-07). Carried in an `[ext_type][ext_length][auth]`\n * block appended after the MPU's declared payload bytes, before the 4-byte\n * Source FEC Payload ID. MMTP X-bit stays 0 \u2014 non-ALTA MMTP receivers treat\n * the trailer as bytes past `payload_length` and skip it (per ISO 23008-1\n * \u00A79.2.2 \"may be discarded without impacting correct processing\").\n */\n// Re-export the ALTA trailer helper from the zero-dep leaf package. The leaf\n// owns the implementation so container and FEC consumers share one parser\n// without creating a package cycle.\nexport const MMTP_EXT_TYPE_ALTA_AUTH = _LEAF_MMTP_EXT_TYPE_ALTA_AUTH\nexport const MMTP_EXT_TRAILER_HEADER_SIZE = _LEAF_MMTP_EXT_TRAILER_HEADER_SIZE\nexport type AltaTrailer = _LeafAltaTrailer\nexport const extractAltaTrailer = _leafExtractAltaTrailer\n\n/** Size of Repair FEC Payload ID per ISO/IEC 23008-1 Section C.5.3 */\nexport const REPAIR_FEC_PID_SIZE = 13\n\n/**\n * Parse the 13-byte Repair FEC Payload ID after the complete dynamic MMTP header.\n * `parsedHeader`, when given, must be `parseMMTPHeader(mmtpPacket)` of this\n * same packet; it spares the second header parse (BLO-36599).\n */\nfunction readRepairFecPayloadIdFields(mmtpPacket: Uint8Array, parsedHeader?: MMTPHeader): {\n packetId: number; ssStart: number; rsbLength: number; rsId: number; ssbLength: number; dataOffset: number\n} | null {\n let header: MMTPHeader\n if (parsedHeader) {\n header = parsedHeader\n } else {\n try {\n header = parseMMTPHeader(mmtpPacket)\n } catch {\n return null\n }\n }\n if (header.packetType !== PACKET_TYPE_REPAIR || header.fecType !== 2) return null\n\n const off = header.headerLength\n if (mmtpPacket.length < off + REPAIR_FEC_PID_SIZE) return null\n const ssStart =\n ((mmtpPacket[off]! << 24) |\n (mmtpPacket[off + 1]! << 16) |\n (mmtpPacket[off + 2]! << 8) |\n mmtpPacket[off + 3]!) >>>\n 0 // 32 bits\n const rsbLength = (mmtpPacket[off + 4]! << 16) | (mmtpPacket[off + 5]! << 8) | mmtpPacket[off + 6]! // 24 bits\n const rsId = (mmtpPacket[off + 7]! << 16) | (mmtpPacket[off + 8]! << 8) | mmtpPacket[off + 9]! // 24 bits\n const ssbLength = (mmtpPacket[off + 10]! << 16) | (mmtpPacket[off + 11]! << 8) | mmtpPacket[off + 12]! // 24 bits\n return {\n packetId: header.packetId,\n ssStart,\n rsbLength,\n rsId,\n ssbLength,\n dataOffset: off + REPAIR_FEC_PID_SIZE,\n }\n}\n\n/**\n * Extract Repair FEC Payload ID from a raw-multicast MMTP repair packet.\n * Per ISO/IEC 23008-1:2023 Section C.5.3 (ssbg_mode0, one-stage, no FFSRP_TS):\n * [MMTP header][SS_Start:4][RSB_length:3][RS_ID:3][SSB_length:3][repair data]\n * OTI is delivered via AL-FEC signaling message (Table C.3), not per-packet.\n *\n * Byte 1 is reserved(2)|packet_type(6) for V=0, so repair type 0x03 is 0x03.\n * Pre-migration high-bits bytes (0x0c) are rejected \u2014 no compatibility decode.\n */\nexport function extractRepairFecPayloadId(mmtpPacket: Uint8Array): {\n ssStart: number; rsbLength: number; rsId: number; ssbLength: number\n} | null {\n const fields = readRepairFecPayloadIdFields(mmtpPacket)\n if (!fields) return null\n return {\n ssStart: fields.ssStart,\n rsbLength: fields.rsbLength,\n rsId: fields.rsId,\n ssbLength: fields.ssbLength,\n }\n}\n\n/** Parsed MoQ repair frame per ISO/IEC 23008-1 Section C.4.3 + C.5.3 */\nexport interface MoqRepairFrame {\n /** MMTP packetId (1=video, 2=audio) \u2014 for routing to correct FEC decoder */\n packetId: number\n /** SS_Start (32 bits): SS_ID of first source symbol in block */\n ssStart: number\n /** RSB_length (24 bits): number of repair symbols (P) */\n rsbLength: number\n /** RS_ID (24 bits): repair symbol index (0-based) */\n rsId: number\n /** SSB_length (24 bits): authored source-symbol count (1..=configured K) */\n ssbLength: number\n /** Repair symbol data after the dynamic MMTP header and 13-byte FEC payload ID. */\n symbolData: Uint8Array\n}\n\n/**\n * Parse a MoQ repair frame (ISO/IEC 23008-1 Annex C format).\n *\n * Wire format per ISO 23008-1 \u00A7C.4.3 + \u00A7C.5.3:\n * MMTP Header: fixed 12 bytes plus C/X optional fields\n * then SS_Start (4 bytes) \u2192 block identifier\n * then RSB_length (3 bytes) \u2192 P\n * then RS_ID (3 bytes) \u2192 ESI = K + RS_ID\n * then SSB_length (3 bytes) \u2192 authored source-symbol count (1..=K)\n * then Repair Symbol Data (T bytes)\n *\n * @param frame - Raw MoQ repair frame bytes\n * @returns Parsed frame or null if invalid\n */\nexport function parseMoqRepairFrame(frame: Uint8Array): MoqRepairFrame | null {\n return moqRepairFrameFrom(frame, readRepairFecPayloadIdFields(frame))\n}\n\n/**\n * parseMoqRepairFrame for a caller that already holds this frame's parsed MMTP\n * header, so the header is not parsed a second time (BLO-36599). The router's\n * multicast repair branch uses it: one repair packet in every K + P otherwise\n * paid two header parses.\n *\n * `header` must be `parseMMTPHeader(frame)` of this same frame; the result is\n * then identical to `parseMoqRepairFrame(frame)`.\n */\nexport function parseMoqRepairFrameWithHeader(\n frame: Uint8Array,\n header: MMTPHeader,\n): MoqRepairFrame | null {\n return moqRepairFrameFrom(frame, readRepairFecPayloadIdFields(frame, header))\n}\n\nfunction moqRepairFrameFrom(\n frame: Uint8Array,\n repairId: ReturnType<typeof readRepairFecPayloadIdFields>,\n): MoqRepairFrame | null {\n if (!repairId) return null\n\n const symbolData = frame.subarray(repairId.dataOffset)\n if (\n repairId.rsbLength === 0 ||\n repairId.ssbLength === 0 ||\n repairId.rsId >= repairId.rsbLength ||\n symbolData.length === 0\n ) return null\n\n return {\n packetId: repairId.packetId,\n ssStart: repairId.ssStart,\n rsbLength: repairId.rsbLength,\n rsId: repairId.rsId,\n ssbLength: repairId.ssbLength,\n symbolData,\n }\n}\n\n// The signalling-message parsers (9.3.4.2 payload header, AL-FEC message,\n// PA/MPT) live in ./signalling.ts.\n", "/**\n * NTP short format (RFC 5905 \u00A76): an unsigned 32-bit, 16.16 fixed-point count\n * of seconds. The MMTP header `timestamp` field (ISO/IEC 23008-1 \u00A79.2.2)\n * carries each packet's UTC sending instant in this format, so the value wraps\n * to 0 every 2^16 s (~18.2 h).\n *\n * Plain numeric order on these values breaks at every wrap: a retained\n * `Math.max` stays at the pre-wrap value and every later instant compares as\n * older. Order send times only with {@link compareNtpShort}, which applies\n * RFC 1982 serial-number arithmetic over the field's width.\n */\n\n/** Width in bits of the MMTP `timestamp` field (NTP short). */\nexport const NTP_SHORT_BITS = 32\n\n/** Number of distinct NTP-short values; the field wraps to 0 here. */\nexport const NTP_SHORT_MODULUS = 2 ** NTP_SHORT_BITS\n\n/**\n * RFC 1982 \u00A73.2's `2^(SERIAL_BITS - 1)`. Two values whose forward distance is\n * below this are ordered; exactly this far apart they are not ordered at all.\n * Two instants must be less than this far apart (2^15 s, ~9.1 h of send time)\n * for the comparison to give their true order.\n */\nexport const NTP_SHORT_HALF_RANGE = 2 ** (NTP_SHORT_BITS - 1)\n\n/**\n * Serial order of one NTP-short value relative to another.\n *\n * `'unordered'` is RFC 1982's undefined case: the values are exactly\n * {@link NTP_SHORT_HALF_RANGE} apart, so neither precedes the other. It is\n * never folded into `'before'` or `'after'`; each caller decides what an\n * unorderable pair means for it.\n */\nexport type NtpShortOrder = 'before' | 'equal' | 'after' | 'unordered'\n\n/** True when `value` is an NTP-short value: an integer in [0, NTP_SHORT_MODULUS). */\nexport function isNtpShort(value: number): boolean {\n return Number.isInteger(value) && value >= 0 && value < NTP_SHORT_MODULUS\n}\n\n/**\n * RFC 1982 serial comparison of NTP-short `a` against `b`: `'after'` when `a`\n * is the later instant, `'before'` when it is the earlier one.\n *\n * The answer is the true order only while the two instants are less than\n * {@link NTP_SHORT_HALF_RANGE} apart. Past that a 32-bit field cannot tell\n * later from earlier, and the result names whichever direction is nearer.\n * This is not a total order (it is not transitive across the whole space), so\n * it must never be used to sort.\n *\n * @throws {RangeError} if either operand is not an NTP-short value\n */\nexport function compareNtpShort(a: number, b: number): NtpShortOrder {\n if (!isNtpShort(a) || !isNtpShort(b)) {\n throw new RangeError(\n `NTP-short values must be integers in [0, 2^${NTP_SHORT_BITS}), got ${String(a)} and ${String(b)}`,\n )\n }\n if (a === b) return 'equal'\n // Forward distance from b to a, reduced into [1, NTP_SHORT_MODULUS). Both\n // operands are below 2^32, so every step is exact in a double.\n const forward = (a - b + NTP_SHORT_MODULUS) % NTP_SHORT_MODULUS\n if (forward < NTP_SHORT_HALF_RANGE) return 'after'\n if (forward > NTP_SHORT_HALF_RANGE) return 'before'\n return 'unordered'\n}\n", "/**\n * FEC block profile (BLO-35597, ISO/IEC 23008-1 Annex C).\n *\n * Every catalog `fec` object that declares an FEC instance names exactly one\n * block profile in `fec.blockProfile`. A receiver applies all of a profile's\n * rules or none of them, so the token is the one explicit per-stream\n * discriminator: it is never defaulted and never inferred from the wire.\n *\n * - `k-padded-v1` (legacy, Blockcast-private): a source symbol is the\n * protected MMTP packet zero-padded to T with no length field; every block\n * is a K-symbol RaptorQ block whose ESIs `SSB_length..K-1` are implicit\n * zero padding; `SS_ID = SBN*K + ESI`, so `SS_Start = SBN*K`; repair\n * ESI = K + RS_ID; the MoQ repair group id is the SBN.\n * - `iso-ssbg1-v1`: ssbg_mode1 (C.3.3): a source symbol is\n * `u16 length || protected packet || zero padding` to T; a block is a\n * K' = SSB_length RaptorQ block (C.5.3) and repair ESI = SSB_length + RS_ID;\n * SS_ID increments by one per FEC source packet and wraps at 2^32 (C.5.2),\n * so a block is located by membership in `[SS_Start, SS_Start + SSB_length)`\n * under RFC 1982 serial arithmetic; the MoQ repair group id is the 62-bit\n * wrap-epoch-extended SS_Start. The AL-FEC coding byte for this profile is\n * 0x14 (one-stage, ssbg_mode1, fec_payload_id_mode 0, Amd 1:2025 Table C.3).\n */\nimport { serial32Add, serial32Distance, serial32Precedes } from './serial-number.js'\n\n/** The two block profiles a catalog `fec.blockProfile` may name. */\nexport type FecBlockProfile = 'k-padded-v1' | 'iso-ssbg1-v1'\n\n/** Every accepted `fec.blockProfile` token, legacy first. */\nexport const FEC_BLOCK_PROFILES: readonly FecBlockProfile[] = Object.freeze([\n 'k-padded-v1',\n 'iso-ssbg1-v1',\n])\n\n/** True for a known block-profile token. */\nexport function isFecBlockProfile(value: unknown): value is FecBlockProfile {\n return value === 'k-padded-v1' || value === 'iso-ssbg1-v1'\n}\n\n/**\n * Read a required `fec.blockProfile`. Throws on an absent or unknown token:\n * there is no default and nothing is inferred, because the two profiles carry\n * identical wire tuples whose repair math differs.\n *\n * @param where the catalog path of the field, for the error message.\n */\nexport function parseFecBlockProfile(value: unknown, where: string): FecBlockProfile {\n if (isFecBlockProfile(value)) return value\n if (value === undefined) {\n throw new Error(\n `${where} is required: name the FEC block profile (\"k-padded-v1\" or \"iso-ssbg1-v1\")`,\n )\n }\n throw new Error(\n `${where} must be \"k-padded-v1\" or \"iso-ssbg1-v1\" (got ${JSON.stringify(value)})`,\n )\n}\n\n/** ISO/IEC 23008-1 C.3.3 ssbg_mode1: the two-octet packet length field. */\nexport const SSBG1_LENGTH_FIELD_BYTES = 2\n\n/** The largest protected packet an ssbg_mode1 symbol of size T can carry. */\nexport function ssbg1MaxPacketLength(symbolSize: number): number {\n return symbolSize - SSBG1_LENGTH_FIELD_BYTES\n}\n\n/** Whether a protected packet of `packetLength` bytes fits one T-byte ssbg_mode1 symbol. */\nexport function ssbg1FitsSymbol(packetLength: number, symbolSize: number): boolean {\n return (\n Number.isSafeInteger(packetLength) &&\n packetLength >= 0 &&\n packetLength <= ssbg1MaxPacketLength(symbolSize) &&\n packetLength <= 0xffff\n )\n}\n\n/**\n * Build one ssbg_mode1 source symbol (C.3.3): the protected packet's length\n * in two octets, network byte order, then the packet, then zero octets up to\n * T. The protected packet is the MMTP packet before its Source FEC Payload ID\n * is appended (C.4.2).\n *\n * @throws when `length + 2 > T`: the packet cannot be protected under this\n * profile, and silently truncating it would corrupt every repair symbol.\n */\nexport function buildSsbg1SourceSymbol(packet: Uint8Array, symbolSize: number): Uint8Array {\n if (!Number.isSafeInteger(symbolSize) || symbolSize <= SSBG1_LENGTH_FIELD_BYTES) {\n throw new Error(`ssbg_mode1 symbol size must exceed ${SSBG1_LENGTH_FIELD_BYTES} (got ${symbolSize})`)\n }\n if (!ssbg1FitsSymbol(packet.byteLength, symbolSize)) {\n throw new Error(\n `ssbg_mode1 source symbol needs ${packet.byteLength} + ${SSBG1_LENGTH_FIELD_BYTES} bytes but T is ${symbolSize}`,\n )\n }\n const symbol = new Uint8Array(symbolSize)\n symbol[0] = (packet.byteLength >>> 8) & 0xff\n symbol[1] = packet.byteLength & 0xff\n symbol.set(packet, SSBG1_LENGTH_FIELD_BYTES)\n return symbol\n}\n\n/**\n * Recover the protected packet from one ssbg_mode1 source symbol: the bytes\n * the length field names. Returns null, never a guess, when the length field\n * points past the symbol or any padding octet is non-zero (C.3.3 padding is\n * all 00h), since either means the recovered symbol is not what was sent.\n */\nexport function trimSsbg1SourceSymbol(symbol: Uint8Array): Uint8Array | null {\n if (symbol.byteLength < SSBG1_LENGTH_FIELD_BYTES) return null\n const length = (symbol[0]! << 8) | symbol[1]!\n const end = SSBG1_LENGTH_FIELD_BYTES + length\n if (end > symbol.byteLength) return null\n for (let i = end; i < symbol.byteLength; i++) {\n if (symbol[i] !== 0) return null\n }\n return symbol.subarray(SSBG1_LENGTH_FIELD_BYTES, end)\n}\n\n/** The SS_ID after the last source symbol of `[ssStart, ssStart + ssbLength)`. */\nexport function isoBlockEnd(ssStart: number, ssbLength: number): number {\n return serial32Add(ssStart, ssbLength)\n}\n\n/**\n * Range membership (C.5.2, C.5.3): whether `ssId` is one of the\n * `ssbLength` consecutive SS_IDs starting at `ssStart`, modulo 2^32.\n */\nexport function isoBlockContains(ssStart: number, ssbLength: number, ssId: number): boolean {\n return serial32Distance(ssStart, ssId) < ssbLength\n}\n\n/**\n * A lower bound on how many blocks the block starting at `a` lies behind the\n * one starting at `b` (0 when `a` does not precede `b`). An iso-ssbg1-v1\n * block holds at most `kMax` source symbols (the catalog `sourceSymbols`, the\n * AL-FEC maximum_k_for_repair_flow), so `n * kMax` SS_IDs always span at least\n * `n` blocks. Every interleave horizon in the receiver (decoder retention,\n * worker retirement, ordered-relay settlement) uses this one bound, so the\n * manager's horizon can never fall inside the decoder's.\n */\nexport function isoBlocksBehindLowerBound(a: number, b: number, kMax: number): number {\n if (!Number.isSafeInteger(kMax) || kMax <= 0) {\n throw new Error(`iso-ssbg1-v1 block horizon needs a positive maximum K (got ${kMax})`)\n }\n return serial32Precedes(a, b) ? Math.floor(serial32Distance(a, b) / kMax) : 0\n}\n", "// Module-local ambient: `multicastFromEnv` reads `process.env` when present\n// (Node, FFI host) and falls back to `{}` in browser contexts. The package\n// targets both, so we don't pull in `@types/node` org-wide \u2014 declaring the\n// minimal surface we touch keeps the dependency graph tight and the\n// published .d.ts free of node-isms.\ndeclare const process: { env: Record<string, string | undefined> } | undefined;\n\n/**\n * Multicast catalog input contract \u2014 validator + builder.\n *\n * Per draft-ramadan-moq-multicast-00 \u00A73 + \u00A74.1, the publisher (FFmpeg,\n * cast bridge, GStreamer, OBS-bridge) populates `multicast.endpoints[]`\n * in the catalog when its output includes a UDP multicast destination.\n *\n * This module is the TypeScript-side validator over a versioned input\n * shape. Sister-language validators (FFmpeg C, cast Rust) target the\n * same invariants \u2014 eventually via a shared JSON Schema + fixtures\n * (BLO-3341 follow-up); today parity is enforced by code review against\n * 1:1 ports. Each publisher's integration MUST also assert that the\n * declared `(sourceAddress, groupAddress, port)` matches what it\n * actually emits \u2014 that's a separate runtime check the helper cannot\n * perform.\n *\n * Validation invariants (per codex review of BLO-3323 v2):\n * - protocol === 'ssm' \u2192 sourceAddress REQUIRED\n * - protocol === 'amt' \u2192 a catalog-level `multicast.networkSource` entry of\n * type \"amt\" REQUIRED; its `relay` is required only for\n * `discovery: \"manual\"` (\"driad\" resolves the relay at runtime).\n * A per-endpoint `relayAddress` input is LIFTED into that entry and is\n * never emitted onto the endpoint.\n * - protocol === 'atsc3' \u2192 a catalog-level `multicast.networkSource` entry of\n * type \"atsc3\" REQUIRED; it carries the frequency, PLP, service id and SLS\n * URI, which is the only tuning information an ATSC3 receiver has.\n * - protocol === 'ssm' | 'asm' \u2192 no `networkSource` entry required: a native\n * join describes no alternate bearer (BLO-37926).\n * - groupAddress is a valid IPv4 multicast group (224.0.0.0/4)\n * or IPv6 multicast group (ff00::/8)\n * - SSM groupAddress MUST be in 232.0.0.0/8 (IPv4) or ff3x::/32 (IPv6),\n * where the IPv6 form requires P=1 + T=1 (the `3` nibble) AND the\n * 16-bit reserved-zero second hextet per RFC 3306 \u00A74 / RFC 4607.\n * - port in 1..65535\n * - tracks non-empty; per-track packetId in 1..65535\n * - packetId 0 (`MMTP_SIGNALING_PACKET_ID`) is the MMTP signaling flow\n * (FFmpeg `moq_mmt` sends the AL-FEC message 0x0203 on it into the same\n * multicast group) and is refused for a track with its own kind,\n * `signaling_packet_id`. The kind string matches cast's\n * `multicast_input.rs` so the two ports report the same reason.\n * - packetIds unique within an endpoint\n * - (sourceAddress, groupAddress, port) tuple unique across endpoints\n * - bandwidth (if present) > 0\n * - validUntil (if present) is a positive unix-ms timestamp\n *\n * @see draft-ramadan-moq-multicast-00 \u00A74.1\n * @see .planning/drafts/blo-3323-multicast-catalog-source-of-truth.md\n */\n\nimport { MMTP_PACKET_ID_MAX, MMTP_SIGNALING_PACKET_ID } from \"@blockcast/mmt-base\";\nimport {\n\ttype AmtDiscovery,\n\ttype MulticastCatalogConfig,\n\ttype MulticastEndpoint,\n\ttype MulticastProtocol,\n\ttype MulticastTrackObject,\n\ttype NetworkSource,\n} from \"./carp-catalog.js\";\n\n/**\n * Strict typed input for one multicast endpoint. The publisher provides\n * this shape (env vars / CLI / config record), the helper validates and\n * lifts it into a `MulticastEndpoint` ready to embed in the catalog.\n */\nexport interface MulticastEndpointInput {\n\treadonly protocol: MulticastProtocol;\n\treadonly sourceAddress?: string;\n\treadonly groupAddress: string;\n\treadonly port: number;\n\t/** New emitters MUST use the object form (name + packetId). */\n\treadonly tracks: ReadonlyArray<MulticastTrackObject>;\n\t/**\n\t * AMT relay for this endpoint.\n\t *\n\t * Publisher-side input only \u2014 it is LIFTED into the catalog-level\n\t * `multicast.networkSource` and is never written onto the emitted endpoint\n\t * (the normative endpoint model carries no relay field). Because\n\t * `networkSource` is catalog-level, a relay that differs from the catalog's\n\t * AMT relay cannot be expressed and is rejected with `conflicting_amt_relay`.\n\t *\n\t * Prefer declaring `MulticastInput.networkSource` directly.\n\t */\n\treadonly relayAddress?: string;\n\t/** Aggregate bandwidth in bits per second. RECOMMENDED per draft \u00A74.1. */\n\treadonly bandwidth?: number;\n}\n\n/**\n * Strict typed input for the catalog-level multicast extension.\n *\n * `version` (monotonic) + `validUntil` (unix ms) support the catalog\n * freshness model: subscribers refetch before expiry, and publishers\n * MUST republish (with bumped `version`) on any endpoint mutation.\n */\nexport interface MulticastInput {\n\treadonly enabled?: boolean;\n\treadonly endpoints: ReadonlyArray<MulticastEndpointInput>;\n\t/**\n\t * Canonical relay/tuning declaration (draft-ramadan-moq-multicast \u00A74.1).\n\t * When omitted, one is derived from `amtFallback` and any endpoint\n\t * `relayAddress`; when present no derivation happens. Either way every entry\n\t * is validated, and an endpoint `relayAddress` it does not carry is\n\t * rejected (`conflicting_amt_relay`). Omit it entirely for native\n\t * multicast: it describes alternate bearers only (BLO-37926).\n\t */\n\treadonly networkSource?: ReadonlyArray<NetworkSource>;\n\t/** @deprecated Lifted into `networkSource`; no longer emitted. */\n\treadonly amtFallback?: { readonly discovery: AmtDiscovery; readonly relayDns?: string };\n\treadonly version?: number;\n\treadonly validUntil?: number;\n}\n\nexport type MulticastInputErrorKind =\n\t| \"empty_endpoints\"\n\t/**\n\t * `endpoints` was not an array at all. Distinct from `empty_endpoints`,\n\t * which reports a well-formed array that declares nothing: a caller\n\t * handling these programmatically needs \"you passed the wrong shape\"\n\t * apart from \"you passed nothing\".\n\t */\n\t| \"invalid_endpoints\"\n\t| \"ssm_requires_source\"\n\t/**\n\t * Deliberately overloaded, unlike its one-site mirror\n\t * `atsc3_requires_tuning`: an AMT relay can be missing at two layers, and\n\t * both mean \"this endpoint names a relay nobody can reach\". Raised for an\n\t * `amt` endpoint with no `networkSource` entry of type `\"amt\"`, and for an\n\t * entry whose `discovery: \"manual\"` names no `relay`. Splitting them would\n\t * make callers handle two kinds for one operator-visible fault.\n\t */\n\t| \"amt_requires_relay\"\n\t/**\n\t * An `atsc3` endpoint with no `networkSource` entry of type `\"atsc3\"`. The\n\t * ATSC3 entry carries the frequency, PLP, service id and SLS URI \u2014 the only\n\t * tuning information there is \u2014 so without it the endpoint names a broadcast\n\t * nobody can tune to. The AMT mirror of this is `amt_requires_relay`; native\n\t * joins (\"ssm\", \"asm\") describe no bearer and are exempt (BLO-37926).\n\t */\n\t| \"atsc3_requires_tuning\"\n\t/**\n\t * An endpoint `relayAddress` is not the relay of any AMT `networkSource`\n\t * entry (another endpoint's relay, `amtFallback.relayDns`, or an explicit\n\t * entry won). `networkSource` is catalog-level, so per-endpoint relays\n\t * cannot be represented; fail closed rather than silently drop one.\n\t */\n\t| \"conflicting_amt_relay\"\n\t/**\n\t * A `networkSource` AMT entry's `discovery` was not `\"driad\"` or `\"manual\"`,\n\t * the schema enum. Checked on explicit and derived entries alike.\n\t */\n\t| \"invalid_amt_discovery\"\n\t/**\n\t * A `networkSource` entry the schema rejects: unknown `type`, a key outside\n\t * its `type`'s shape, a blank `relay`, or an out-of-range ATSC3 field.\n\t */\n\t| \"invalid_network_source\"\n\t| \"invalid_group_address\"\n\t| \"ssm_group_out_of_range\"\n\t| \"invalid_port\"\n\t| \"empty_tracks\"\n\t| \"invalid_track_name\"\n\t| \"invalid_packet_id\"\n\t/**\n\t * A track advertised on `MMTP_SIGNALING_PACKET_ID` (0), the MMTP signaling\n\t * flow. Distinct from `invalid_packet_id` so the reason is legible: 0 is a\n\t * well-formed 16-bit value, it is just not a track's to claim.\n\t */\n\t| \"signaling_packet_id\"\n\t| \"duplicate_packet_id\"\n\t| \"duplicate_endpoint_tuple\"\n\t| \"invalid_bandwidth\"\n\t| \"invalid_valid_until\"\n\t| \"invalid_version\"\n\t| \"invalid_env_shape\";\n\nexport class MulticastInputError extends Error {\n\tconstructor(\n\t\tpublic readonly kind: MulticastInputErrorKind,\n\t\tpublic readonly endpointIndex: number | null,\n\t\tmessage: string,\n\t) {\n\t\tsuper(message);\n\t\tthis.name = \"MulticastInputError\";\n\t}\n}\n\n/**\n * Validate a typed publisher input and lift it into a\n * `MulticastCatalogConfig` ready for `catalog.multicast = \u2026`.\n *\n * Throws `MulticastInputError` on any invariant violation. Fail-fast\n * at config-load time, never at runtime \u2014 silent fallbacks hide bugs\n * upstream (per the project's \"no anti-patterns\" directive).\n */\nexport function multicastFromConfig(input: MulticastInput): MulticastCatalogConfig {\n\t// Both array-valued inputs are checked here, before anything indexes them.\n\t// These helpers are consumed out-of-tree from plain JS (see\n\t// `validateNetworkSourceEntry`), so the declared types are no guarantee \u2014\n\t// and object-instead-of-array is the likely mistake on both: `amtFallback`,\n\t// which `networkSource` replaces, was a bare object. Without these a wrong\n\t// shape escapes as a raw `TypeError` from `.map`, which a caller catching\n\t// `MulticastInputError` does not catch.\n\tif (!Array.isArray(input.endpoints)) {\n\t\tthrow new MulticastInputError(\n\t\t\t\"invalid_endpoints\",\n\t\t\tnull,\n\t\t\t`endpoints must be an array of endpoint objects; got ${typeof input.endpoints}`,\n\t\t);\n\t}\n\tif (input.networkSource !== undefined && !Array.isArray(input.networkSource)) {\n\t\tthrow new MulticastInputError(\n\t\t\t\"invalid_network_source\",\n\t\t\tnull,\n\t\t\t\"networkSource must be an array of entries; got \" +\n\t\t\t\t`${typeof input.networkSource} \u2014 a single entry still goes in a one-element array`,\n\t\t);\n\t}\n\tif (input.endpoints.length === 0) {\n\t\tthrow new MulticastInputError(\n\t\t\t\"empty_endpoints\",\n\t\t\tnull,\n\t\t\t\"endpoints[] must be non-empty \u2014 a multicast field with zero endpoints declares nothing\",\n\t\t);\n\t}\n\tif (input.version !== undefined && (!Number.isInteger(input.version) || input.version < 0)) {\n\t\tthrow new MulticastInputError(\"invalid_version\", null, `version must be a non-negative integer; got ${input.version}`);\n\t}\n\tif (input.validUntil !== undefined) {\n\t\tif (!Number.isFinite(input.validUntil) || input.validUntil <= 0) {\n\t\t\tthrow new MulticastInputError(\n\t\t\t\t\"invalid_valid_until\",\n\t\t\t\tnull,\n\t\t\t\t`validUntil must be a positive unix-ms timestamp; got ${input.validUntil}`,\n\t\t\t);\n\t\t}\n\t}\n\n\tconst seenTuples = new Set<string>();\n\tconst endpoints: MulticastEndpoint[] = input.endpoints.map((ep, idx) => validateEndpoint(ep, idx, seenTuples));\n\n\tconst out: MulticastCatalogConfig = { endpoints };\n\tconst { entries: networkSource, relayFrom } = resolveNetworkSource(input);\n\t// Checked on the resolved array, so an explicit `input.networkSource` gets\n\t// exactly the same checks as one derived from the deprecated inputs.\n\tnetworkSource.forEach(validateNetworkSourceEntry);\n\tif (networkSource.length > 0) out.networkSource = networkSource;\n\n\t// A relay the emitted catalog would not carry fails closed, on both paths:\n\t// `networkSource` is catalog-level, so a per-endpoint relay that differs from\n\t// it (another endpoint's, `amtFallback.relayDns`, or an explicit entry) would\n\t// otherwise be dropped silently and that endpoint joins the wrong relay.\n\t//\n\t// A DRIAD entry does not count: both readers skip it, so its `relay` is a\n\t// discovery hint, not a relay the catalog carries. Without this the derived\n\t// path merges `amtFallback: {discovery: \"driad\"}` with an endpoint's\n\t// `relayAddress` into a driad entry holding that relay, the check passes, and\n\t// the join silently falls through to DRIAD -- where the explicit form of the\n\t// same input already fails closed here.\n\tconst amtRelays = new Set(\n\t\tnetworkSource\n\t\t\t.flatMap((s) => (s.type === \"amt\" && s.discovery !== \"driad\" && s.relay?.trim() ? [s.relay.trim()] : []))\n\t\t\t// Only the first, because it is the only one either reader can\n\t\t\t// deliver: `catalogAmtNetworkSource` returns on it and `findAmtRelay`\n\t\t\t// uses `.find`. Collecting every entry accepts an endpoint naming a\n\t\t\t// second manual entry, which then joins the first -- the same silent\n\t\t\t// wrong-relay join this guard exists to prevent, through the one\n\t\t\t// shape the DRIAD filter does not cover. It also keeps the `(...)`\n\t\t\t// listing below honest. Sliced rather than `.find`ed so the `amt`\n\t\t\t// narrowing stays inside the callback.\n\t\t\t.slice(0, 1),\n\t);\n\tinput.endpoints.forEach((ep, idx) => {\n\t\tconst relay = ep.relayAddress?.trim();\n\t\tif (!relay || amtRelays.has(relay)) return;\n\t\t// Two different causes reach this throw and they need different fixes.\n\t\t// If a DRIAD entry carries this exact relay, the catalog is not missing\n\t\t// it -- the entry is present and demoted, so reporting the set as\n\t\t// \"(none)\" sends the operator looking for an entry they already wrote.\n\t\t// Name the demotion and the two-entry shape that escapes it; both\n\t\t// readers take the first non-DRIAD AMT entry\n\t\t// (`catalog-network-source.ts` `catalogAmtNetworkSource`,\n\t\t// `multicast-catalog-policy.ts` `findAmtRelay`) and\n\t\t// `validateNetworkSourceEntry` has no duplicate-type rule, so a manual\n\t\t// entry beside the DRIAD one is joined while the DRIAD one is kept.\n\t\tconst demoted = networkSource.some(\n\t\t\t(s) => s.type === \"amt\" && s.discovery === \"driad\" && s.relay?.trim() === relay,\n\t\t);\n\t\t// Same \"written down but undeliverable\" family as `demoted`, different\n\t\t// reason and so a different fix: there the entry is skipped for its\n\t\t// discovery, here for its position. Reachable only on the explicit\n\t\t// path -- `resolveNetworkSource` emits at most one entry -- so there is\n\t\t// no provenance clause to add, and saying \"is not the relay of any AMT\n\t\t// entry\" here would be plainly false: the operator can see the entry.\n\t\t// `amtRelays` holds only the first joinable relay, so reaching this\n\t\t// throw at all already means this one is not it.\n\t\tconst shadowed = networkSource.some(\n\t\t\t(s) => s.type === \"amt\" && s.discovery !== \"driad\" && s.relay?.trim() === relay,\n\t\t);\n\t\t// The derived and env paths never wrote `networkSource` at all -- it was\n\t\t// synthesised below in `resolveNetworkSource` -- so naming only that key\n\t\t// would name the one field the operator did not set. This applies to\n\t\t// EVERY cause that reaches this throw, not just the demoted one: the\n\t\t// clause belongs in the shared prefix, or the arm that omits it\n\t\t// reproduces the misdiagnosis the other arm was written to fix.\n\t\t//\n\t\t// The two arms are about different values, so they name different\n\t\t// fields. The demoted arm is about `discovery`: a derived entry is only\n\t\t// `\"driad\"` when `amtFallback.discovery` said so (the default is\n\t\t// `\"manual\"`), so `amtFallback` is always the actionable field there.\n\t\t// The set clause is about the *relay*, which `resolveNetworkSource`\n\t\t// takes from `amtFallback.relayDns` or from the endpoint\n\t\t// `relayAddress` values -- and saying \"amtFallback\" to an operator who\n\t\t// never wrote one is the same defect one level down. Read that source\n\t\t// from `relayFrom` rather than re-inferring it: three review rounds\n\t\t// found a local re-inference here drifting from the rule above it.\n\t\tconst derived = input.networkSource === undefined;\n\t\tconst fromAmtFallback = \", derived from amtFallback/MOQCAST_MULTICAST_AMT_*\";\n\t\tconst fromRelay =\n\t\t\trelayFrom === \"amtFallback.relayDns\"\n\t\t\t\t? fromAmtFallback\n\t\t\t\t: relayFrom === \"endpoint.relayAddress\"\n\t\t\t\t\t? \", derived from the endpoint relayAddress values\"\n\t\t\t\t\t: \"\";\n\t\t// `resolveNetworkSource` lets `relayDns` outrank an endpoint relay, so on\n\t\t// the derived path the set can be non-empty and still not hold this\n\t\t// relay. Blaming per-endpoint representation there is wrong twice over:\n\t\t// there may be only one endpoint, and the field that actually won is\n\t\t// `relayDns`, which is the one the operator can act on. Asking\n\t\t// `amtRelays` instead would miss the case where the winning entry is\n\t\t// `discovery: \"driad\"` and so filtered out of that set entirely.\n\t\tconst relayDns = input.amtFallback?.relayDns?.trim();\n\t\tconst outranked = relayFrom === \"amtFallback.relayDns\" && relayDns !== relay;\n\t\tthrow new MulticastInputError(\n\t\t\t\"conflicting_amt_relay\",\n\t\t\tidx,\n\t\t\t`endpoint[${idx}] relayAddress ${JSON.stringify(relay)} ` +\n\t\t\t\t(demoted\n\t\t\t\t\t? `is carried only by a multicast.networkSource AMT entry with discovery=\"driad\"` +\n\t\t\t\t\t\t`${derived ? fromAmtFallback : \"\"}, ` +\n\t\t\t\t\t\t\"whose relay is a DRIAD discovery hint that both readers skip, so the catalog names \" +\n\t\t\t\t\t\t\"no relay to join; to join it, add a manual entry beside the driad one: \" +\n\t\t\t\t\t\t`[{type:\"amt\",discovery:\"manual\",relay:${JSON.stringify(relay)}},{type:\"amt\",discovery:\"driad\"}]`\n\t\t\t\t\t: shadowed\n\t\t\t\t\t\t? `is carried by a multicast.networkSource AMT entry that is not the first one: ` +\n\t\t\t\t\t\t\t`both readers join the first AMT entry (${[...amtRelays].join(\", \")}) and ignore ` +\n\t\t\t\t\t\t\t\"the rest, so this relay is never joined; keep a single AMT entry, or put this one first\"\n\t\t\t\t\t\t: `is not the relay of any multicast.networkSource AMT entry ` +\n\t\t\t\t\t\t\t`(${[...amtRelays].join(\", \") || \"none\"})${fromRelay}; ` +\n\t\t\t\t\t\t\t(outranked\n\t\t\t\t\t\t\t\t? `amtFallback.relayDns/MOQCAST_MULTICAST_AMT_RELAY_DNS (${relayDns}) outranks an ` +\n\t\t\t\t\t\t\t\t\t\"endpoint relay when networkSource is derived -- drop one, or make them agree\"\n\t\t\t\t\t\t\t\t: \"networkSource is catalog-level and cannot represent per-endpoint relays\")),\n\t\t);\n\t});\n\n\t// A bearer endpoint must name its bearer. The two bearer protocols are\n\t// exactly the two `networkSource` types, so an endpoint's own protocol names\n\t// the entry it requires. Native joins (\"ssm\", \"asm\") describe no bearer and\n\t// are exempt (BLO-37926) \u2014 the endpoint tuple is the whole join.\n\t// Whether an AMT entry needs a relay string (\"manual\" does, \"driad\" does\n\t// not) is per-entry, checked above.\n\tinput.endpoints.forEach((ep, idx) => {\n\t\tif (ep.protocol !== \"amt\" && ep.protocol !== \"atsc3\") return;\n\t\tif (networkSource.some((s) => s.type === ep.protocol)) return;\n\t\tthrow new MulticastInputError(\n\t\t\tep.protocol === \"amt\" ? \"amt_requires_relay\" : \"atsc3_requires_tuning\",\n\t\t\tidx,\n\t\t\t`endpoint[${idx}] protocol=${ep.protocol} requires a multicast.networkSource entry of ` +\n\t\t\t\t`type \"${ep.protocol}\"` +\n\t\t\t\t(ep.protocol === \"atsc3\"\n\t\t\t\t\t? \" carrying frequency, plpId, serviceId and slsUri \u2014 an ATSC3 endpoint is not tunable without them\"\n\t\t\t\t\t: \"\"),\n\t\t);\n\t});\n\n\tif (input.enabled !== undefined) out.enabled = input.enabled;\n\tif (input.version !== undefined) out.version = input.version;\n\tif (input.validUntil !== undefined) out.validUntil = input.validUntil;\n\treturn out;\n}\n\n/**\n * Build the catalog-level `multicast.networkSource` for this input.\n *\n * An explicit `input.networkSource` is authoritative and used as given.\n * Otherwise one AMT entry is derived from the deprecated inputs it replaces:\n * `amtFallback.discovery`/`relayDns` and any endpoint `relayAddress`;\n * `discovery` defaults to `\"manual\"` because a relay named in the catalog is,\n * by definition, not discovered. Endpoint relays that disagree with the result\n * are rejected by `multicastFromConfig`, not resolved here.\n *\n * `relayFrom` reports which deprecated input supplied the derived relay, so\n * that rejection can name a field the operator actually set instead of\n * re-deciding the precedence below at the throw site. It is `null` on the\n * explicit path and when no relay was derived at all.\n */\nfunction resolveNetworkSource(input: MulticastInput): {\n\tentries: NetworkSource[];\n\trelayFrom: \"amtFallback.relayDns\" | \"endpoint.relayAddress\" | null;\n} {\n\tif (input.networkSource !== undefined) {\n\t\treturn { entries: input.networkSource.map((s) => ({ ...s })), relayFrom: null };\n\t}\n\n\tconst relayDns = input.amtFallback?.relayDns?.trim();\n\tconst relay = relayDns || input.endpoints.map((ep) => ep.relayAddress?.trim()).find((r) => !!r);\n\tconst discovery = input.amtFallback?.discovery ?? (relay ? \"manual\" : undefined);\n\tif (discovery === undefined) return { entries: [], relayFrom: null };\n\treturn {\n\t\tentries: [{ type: \"amt\", discovery, ...(relay ? { relay } : {}) }],\n\t\trelayFrom: !relay ? null : relayDns ? \"amtFallback.relayDns\" : \"endpoint.relayAddress\",\n\t};\n}\n\nconst AMT_SOURCE_KEYS = new Set([\"type\", \"discovery\", \"relay\", \"port\"]);\nconst ATSC3_SOURCE_KEYS = new Set([\"type\", \"frequency\", \"plpId\", \"serviceId\", \"slsUri\"]);\n\n/**\n * One `networkSource[]` entry against `catalog.schema.json` `$defs/networkSource`\n * (and moq-rs `NetworkSource`), plus the one semantic rule the schema leaves\n * out: `discovery: \"manual\"` names its relay. These helpers are consumed\n * out-of-tree from plain JS, so the `NetworkSource` type is no guarantee.\n */\nfunction validateNetworkSourceEntry(s: NetworkSource, i: number): void {\n\tconst err = (kind: MulticastInputErrorKind, why: string) =>\n\t\tnew MulticastInputError(kind, null, `networkSource[${i}] ${why}`);\n\tconst keys = s?.type === \"amt\" ? AMT_SOURCE_KEYS : s?.type === \"atsc3\" ? ATSC3_SOURCE_KEYS : undefined;\n\tif (!keys) throw err(\"invalid_network_source\", `type must be \"amt\" or \"atsc3\"; got ${JSON.stringify(s?.type)}`);\n\t// Definedness, not presence: `isMulticastCatalogConfig` in `carp-catalog.ts`\n\t// already chose `=== undefined` over `in` for exactly this reason, and the\n\t// two must agree on what \"set\" means. A key spread in as `undefined`\n\t// (`{...entry, relayDns: undefined}`) is dropped by `JSON.stringify`, so the\n\t// emitted catalog is clean and rejecting it would fail a shape that ships\n\t// correctly.\n\tconst extra = Object.keys(s).filter((k) => !keys.has(k) && (s as Record<string, unknown>)[k] !== undefined);\n\tif (extra.length > 0) throw err(\"invalid_network_source\", `has keys the schema rejects: ${extra.join(\", \")}`);\n\n\tif (s.type === \"amt\") {\n\t\tif (s.discovery !== \"driad\" && s.discovery !== \"manual\") {\n\t\t\tthrow err(\"invalid_amt_discovery\", `discovery must be \"driad\" or \"manual\"; got ${JSON.stringify(s.discovery)}`);\n\t\t}\n\t\tif (s.relay !== undefined) {\n\t\t\t// Canonicalize, don't just check: the conflict guard in\n\t\t\t// `multicastFromConfig` compares `.trim()`ed values, so a padded\n\t\t\t// explicit relay would pass the guard and then ship padded. The\n\t\t\t// derived path (`resolveNetworkSource`) already trims, so trimming\n\t\t\t// here is what makes both paths emit the same canonical string.\n\t\t\t// Entries are copies by then, so this cannot mutate caller input.\n\t\t\tconst relay = typeof s.relay === \"string\" ? s.relay.trim() : \"\";\n\t\t\tif (!relay) {\n\t\t\t\tthrow err(\"invalid_network_source\", `relay must be non-blank when present; got ${JSON.stringify(s.relay)}`);\n\t\t\t}\n\t\t\ts.relay = relay;\n\t\t}\n\t\tif (s.discovery === \"manual\" && s.relay === undefined) {\n\t\t\tthrow err(\"amt_requires_relay\", `discovery=\"manual\" requires a relay (\"driad\" resolves it at runtime)`);\n\t\t}\n\t\tif (s.port !== undefined && (!Number.isInteger(s.port) || s.port < 1 || s.port > 65535)) {\n\t\t\tthrow err(\"invalid_port\", `port must be 1..65535; got ${s.port}`);\n\t\t}\n\t\treturn;\n\t}\n\tconst inRange = (v: number, min: number, max: number) => Number.isInteger(v) && v >= min && v <= max;\n\tif (!inRange(s.frequency, 1, Number.MAX_SAFE_INTEGER)) {\n\t\tthrow err(\"invalid_network_source\", `frequency must be a positive integer; got ${s.frequency}`);\n\t}\n\tif (!inRange(s.plpId, 0, 63)) throw err(\"invalid_network_source\", `plpId must be 0..63; got ${s.plpId}`);\n\tif (!inRange(s.serviceId, 0, 65535)) {\n\t\tthrow err(\"invalid_network_source\", `serviceId must be 0..65535; got ${s.serviceId}`);\n\t}\n\tif (typeof s.slsUri !== \"string\" || !isAbsoluteUri(s.slsUri)) {\n\t\tthrow err(\"invalid_network_source\", `slsUri must be an absolute URI; got ${JSON.stringify(s.slsUri)}`);\n\t}\n}\n\nfunction isAbsoluteUri(value: string): boolean {\n\ttry {\n\t\tnew URL(value);\n\t\treturn true;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\nfunction validateEndpoint(\n\tep: MulticastEndpointInput,\n\tidx: number,\n\tseenTuples: Set<string>,\n): MulticastEndpoint {\n\tif (ep.protocol === \"ssm\" && (!ep.sourceAddress || ep.sourceAddress.length === 0)) {\n\t\tthrow new MulticastInputError(\n\t\t\t\"ssm_requires_source\",\n\t\t\tidx,\n\t\t\t`endpoint[${idx}] protocol=ssm requires sourceAddress`,\n\t\t);\n\t}\n\t// NOTE: `protocol: \"amt\"` no longer requires a per-endpoint relay here. The\n\t// relay is catalog-level (`multicast.networkSource`), which this function\n\t// cannot see; `multicastFromConfig` enforces it once both halves are in hand.\n\n\tif (!isMulticastGroupAddress(ep.groupAddress)) {\n\t\tthrow new MulticastInputError(\n\t\t\t\"invalid_group_address\",\n\t\t\tidx,\n\t\t\t`endpoint[${idx}] groupAddress \"${ep.groupAddress}\" is not a valid multicast address`,\n\t\t);\n\t}\n\tif (ep.protocol === \"ssm\" && !isSsmGroupAddress(ep.groupAddress)) {\n\t\tthrow new MulticastInputError(\n\t\t\t\"ssm_group_out_of_range\",\n\t\t\tidx,\n\t\t\t`endpoint[${idx}] protocol=ssm requires groupAddress in 232.0.0.0/8 (IPv4) or ff3x::/32 (IPv6); got \"${ep.groupAddress}\"`,\n\t\t);\n\t}\n\n\tif (!Number.isInteger(ep.port) || ep.port < 1 || ep.port > 65535) {\n\t\tthrow new MulticastInputError(\"invalid_port\", idx, `endpoint[${idx}] port must be 1..65535; got ${ep.port}`);\n\t}\n\n\tif (ep.tracks.length === 0) {\n\t\tthrow new MulticastInputError(\"empty_tracks\", idx, `endpoint[${idx}] tracks must be non-empty`);\n\t}\n\n\tconst seenPids = new Set<number>();\n\tfor (const t of ep.tracks) {\n\t\tif (typeof t.name !== \"string\" || t.name.length === 0) {\n\t\t\tthrow new MulticastInputError(\n\t\t\t\t\"invalid_track_name\",\n\t\t\t\tidx,\n\t\t\t\t`endpoint[${idx}] track name must be a non-empty string`,\n\t\t\t);\n\t\t}\n\t\tif (t.packetId === MMTP_SIGNALING_PACKET_ID) {\n\t\t\tthrow new MulticastInputError(\n\t\t\t\t\"signaling_packet_id\",\n\t\t\t\tidx,\n\t\t\t\t`endpoint[${idx}] track \"${t.name}\" packetId ${t.packetId} is the MMTP signaling flow ` +\n\t\t\t\t\t`and must not be advertised for a track; packetId must be 1..${MMTP_PACKET_ID_MAX}`,\n\t\t\t);\n\t\t}\n\t\tif (!Number.isInteger(t.packetId) || t.packetId < 1 || t.packetId > MMTP_PACKET_ID_MAX) {\n\t\t\tthrow new MulticastInputError(\n\t\t\t\t\"invalid_packet_id\",\n\t\t\t\tidx,\n\t\t\t\t`endpoint[${idx}] track \"${t.name}\" packetId must be 1..${MMTP_PACKET_ID_MAX}; got ${t.packetId}`,\n\t\t\t);\n\t\t}\n\t\tif (seenPids.has(t.packetId)) {\n\t\t\tthrow new MulticastInputError(\n\t\t\t\t\"duplicate_packet_id\",\n\t\t\t\tidx,\n\t\t\t\t`endpoint[${idx}] duplicate packetId ${t.packetId}`,\n\t\t\t);\n\t\t}\n\t\tseenPids.add(t.packetId);\n\t}\n\n\tif (ep.bandwidth !== undefined && (!Number.isFinite(ep.bandwidth) || ep.bandwidth <= 0)) {\n\t\tthrow new MulticastInputError(\n\t\t\t\"invalid_bandwidth\",\n\t\t\tidx,\n\t\t\t`endpoint[${idx}] bandwidth must be positive; got ${ep.bandwidth}`,\n\t\t);\n\t}\n\n\tconst tupleKey = `${ep.sourceAddress ?? \"\"}|${ep.groupAddress}|${ep.port}`;\n\tif (seenTuples.has(tupleKey)) {\n\t\tthrow new MulticastInputError(\n\t\t\t\"duplicate_endpoint_tuple\",\n\t\t\tidx,\n\t\t\t`endpoint[${idx}] duplicates (sourceAddress, groupAddress, port) tuple \"${tupleKey}\"`,\n\t\t);\n\t}\n\tseenTuples.add(tupleKey);\n\n\tconst out: MulticastEndpoint = {\n\t\tprotocol: ep.protocol,\n\t\tgroupAddress: ep.groupAddress,\n\t\tport: ep.port,\n\t\ttracks: ep.tracks.map((t) => ({ name: t.name, packetId: t.packetId })),\n\t};\n\tif (ep.sourceAddress !== undefined) out.sourceAddress = ep.sourceAddress;\n\t// `relayAddress` is deliberately NOT copied: the relay is lifted to\n\t// catalog-level `multicast.networkSource` by `multicastFromConfig`.\n\tif (ep.bandwidth !== undefined) out.bandwidth = ep.bandwidth;\n\treturn out;\n}\n\nfunction isMulticastGroupAddress(addr: string): boolean {\n\tif (addr.includes(\":\")) {\n\t\t// IPv6: ff00::/8. Prefix-only check; full IPv6 syntax validation\n\t\t// is delegated to the OS bind() call downstream.\n\t\treturn /^ff[0-9a-f]{2}:/i.test(addr);\n\t}\n\tconst m = /^(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})$/.exec(addr);\n\tif (!m) return false;\n\tfor (let i = 1; i <= 4; i++) {\n\t\tconst v = Number(m[i]);\n\t\tif (!Number.isInteger(v) || v < 0 || v > 255) return false;\n\t}\n\tconst first = Number(m[1]);\n\treturn first >= 224 && first <= 239;\n}\n\nfunction isSsmGroupAddress(addr: string): boolean {\n\tif (addr.includes(\":\")) {\n\t\t// SSM IPv6: ff3x::/32 per RFC 3306 \u00A74. Requires P=1 + T=1 (the `3`\n\t\t// nibble) AND the second 16-bit hextet to be all-zero (RFC 3306's\n\t\t// \"prefix length = 0\" \u2192 next 32 bits are 0). The regex matches\n\t\t// `ff3?:0000:` collapsed forms (`ff3e::`, `ff3e:0::`, `ff3e:0:`,\n\t\t// `ff3e:0000:`) and rejects `ff3e:beef::1` etc.\n\t\treturn /^ff3[0-9a-f]:0{0,4}:/i.test(addr);\n\t}\n\tconst m = /^(\\d{1,3})\\./.exec(addr);\n\treturn m !== null && Number(m[1]) === 232;\n}\n\n// Env-var helper (BLO-3337): read MOQCAST_MULTICAST_* keys and route\n// through multicastFromConfig so all per-endpoint invariants apply.\n\n/**\n * Read `MOQCAST_MULTICAST_*` env vars and return a\n * `MulticastCatalogConfig`. Returns `undefined` when no per-endpoint\n * key is set.\n *\n * Env contract \u2014 per-endpoint keys take an optional non-negative index:\n *\n * MOQCAST_MULTICAST[_<idx>]_PROTOCOL=ssm\n * MOQCAST_MULTICAST[_<idx>]_GROUP=232.1.1.1\n * MOQCAST_MULTICAST[_<idx>]_PORT=5004\n * MOQCAST_MULTICAST[_<idx>]_SOURCE=10.0.0.20 (required for ssm)\n * MOQCAST_MULTICAST[_<idx>]_RELAY=amt.example.com (required for amt)\n * MOQCAST_MULTICAST[_<idx>]_TRACKS=video:1,audio:3\n * MOQCAST_MULTICAST[_<idx>]_BANDWIDTH=8000000 (optional, bps)\n *\n * MOQCAST_MULTICAST_AMT_DISCOVERY=driad (catalog-level)\n * MOQCAST_MULTICAST_AMT_RELAY_DNS=relay.example.com (catalog-level)\n *\n * The unindexed form (`MOQCAST_MULTICAST_GROUP`) is exactly index 0;\n * mixing it with explicit `_0_` keys is rejected as ambiguous. Indices\n * need not be contiguous; observed indices are sorted ascending.\n */\nexport function multicastFromEnv(\n\tenv: Record<string, string | undefined> = ((globalThis as { process?: { env?: Record<string, string | undefined> } })\n\t\t.process?.env ?? {}) as Record<string, string | undefined>,\n): MulticastCatalogConfig | undefined {\n\tconst indices = collectMulticastIndices(env);\n\t// An empty value is unset: `FOO=${BAR}` with BAR unset, or an empty\n\t// configMap value, is a templating outcome, not a setting. Normalising\n\t// here gives every test below one notion of \"set\". Stricter than SOURCE\n\t// and RELAY, which gate on truthiness alone and so keep a whitespace-only\n\t// value \u2014 deliberately, as these two decide whether AMT is declared at all.\n\tconst amtDiscovery = env[\"MOQCAST_MULTICAST_AMT_DISCOVERY\"]?.trim() || undefined;\n\tconst amtRelayDns = env[\"MOQCAST_MULTICAST_AMT_RELAY_DNS\"]?.trim() || undefined;\n\t// One predicate, both guards: a key significant enough to reject at zero\n\t// endpoints is not silently discarded once endpoints are present.\n\tconst hasCatalogAmt = amtDiscovery !== undefined || amtRelayDns !== undefined;\n\tif (indices.length === 0) {\n\t\t// Catalog-level AMT keys without per-endpoint keys is a\n\t\t// misconfiguration, not a no-op: `MOQCAST_MULTICAST_AMT_DISCOVERY`\n\t\t// only makes sense alongside endpoints to fall back FOR. Throw\n\t\t// loudly instead of silently dropping the request \u2014 a publisher\n\t\t// that meant to declare AMT fallback shouldn't have its config\n\t\t// disappear without any signal.\n\t\tif (hasCatalogAmt) {\n\t\t\tthrow new MulticastInputError(\n\t\t\t\t\"invalid_env_shape\",\n\t\t\t\tnull,\n\t\t\t\t\"MOQCAST_MULTICAST_AMT_* env keys are set but no per-endpoint keys \" +\n\t\t\t\t\t\"(MOQCAST_MULTICAST_GROUP / _PROTOCOL / _PORT / _TRACKS) were found. \" +\n\t\t\t\t\t\"AMT discovery is a catalog-level fallback for endpoints; declaring it \" +\n\t\t\t\t\t\"with zero endpoints is a misconfiguration.\",\n\t\t\t);\n\t\t}\n\t\treturn undefined;\n\t}\n\n\tconst endpoints: MulticastEndpointInput[] = indices.map((idx) => readEndpointAt(env, idx));\n\n\tconst input: MulticastInput = {\n\t\tendpoints,\n\t\t...(hasCatalogAmt\n\t\t\t? {\n\t\t\t\t\tamtFallback: {\n\t\t\t\t\t\t// `\"manual\"` mirrors `resolveNetworkSource`: a relay named in\n\t\t\t\t\t\t// the catalog is, by definition, not discovered.\n\t\t\t\t\t\tdiscovery: (amtDiscovery ?? \"manual\") as AmtDiscovery,\n\t\t\t\t\t\t...(amtRelayDns ? { relayDns: amtRelayDns } : {}),\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t: {}),\n\t};\n\treturn multicastFromConfig(input);\n}\n\nconst ENV_PER_FIELD = [\"PROTOCOL\", \"GROUP\", \"PORT\", \"SOURCE\", \"RELAY\", \"TRACKS\", \"BANDWIDTH\"];\nconst ENV_CATALOG_LEVEL = new Set([\"AMT_DISCOVERY\", \"AMT_RELAY_DNS\"]);\n\nfunction collectMulticastIndices(env: Record<string, string | undefined>): number[] {\n\tconst indices = new Set<number>();\n\tlet hasUnindexed = false;\n\tlet hasZeroIndexed = false;\n\n\tfor (const [key, val] of Object.entries(env)) {\n\t\tif (val === undefined) continue;\n\t\tif (!key.startsWith(\"MOQCAST_MULTICAST_\")) continue;\n\t\tconst suffix = key.slice(\"MOQCAST_MULTICAST_\".length);\n\t\tif (ENV_CATALOG_LEVEL.has(suffix)) continue;\n\n\t\tconst m = /^(\\d+)_(.+)$/.exec(suffix);\n\t\tif (m) {\n\t\t\tconst idx = Number(m[1]);\n\t\t\tconst field = m[2];\n\t\t\tif (!ENV_PER_FIELD.includes(field)) continue;\n\t\t\tif (idx === 0) hasZeroIndexed = true;\n\t\t\tindices.add(idx);\n\t\t} else if (ENV_PER_FIELD.includes(suffix)) {\n\t\t\thasUnindexed = true;\n\t\t\tindices.add(0);\n\t\t}\n\t}\n\n\tif (hasUnindexed && hasZeroIndexed) {\n\t\tthrow new MulticastInputError(\n\t\t\t\"invalid_env_shape\",\n\t\t\tnull,\n\t\t\t\"MOQCAST_MULTICAST_* env contract: cannot mix unindexed keys (MOQCAST_MULTICAST_GROUP) \" +\n\t\t\t\t\"with explicitly-indexed keys at index 0 (MOQCAST_MULTICAST_0_GROUP) \u2014 they refer to \" +\n\t\t\t\t\"the same endpoint. Use one form consistently.\",\n\t\t);\n\t}\n\n\treturn [...indices].sort((a, b) => a - b);\n}\n\nfunction readEndpointAt(env: Record<string, string | undefined>, idx: number): MulticastEndpointInput {\n\tconst lookup = (field: string): string | undefined => {\n\t\tif (idx === 0) {\n\t\t\treturn env[`MOQCAST_MULTICAST_${field}`] ?? env[`MOQCAST_MULTICAST_0_${field}`];\n\t\t}\n\t\treturn env[`MOQCAST_MULTICAST_${idx}_${field}`];\n\t};\n\n\tconst portStr = lookup(\"PORT\");\n\tconst port = portStr !== undefined ? Number(portStr) : NaN;\n\tconst tracks = parseTracksEnv(lookup(\"TRACKS\"), idx);\n\tconst bandwidthStr = lookup(\"BANDWIDTH\");\n\tconst bandwidth = bandwidthStr !== undefined ? Number(bandwidthStr) : undefined;\n\tconst source = lookup(\"SOURCE\");\n\tconst relay = lookup(\"RELAY\");\n\n\treturn {\n\t\tprotocol: lookup(\"PROTOCOL\") as MulticastProtocol,\n\t\tgroupAddress: lookup(\"GROUP\") ?? \"\",\n\t\tport,\n\t\ttracks,\n\t\t...(source ? { sourceAddress: source } : {}),\n\t\t...(relay ? { relayAddress: relay } : {}),\n\t\t...(bandwidth !== undefined ? { bandwidth } : {}),\n\t};\n}\n\nfunction parseTracksEnv(raw: string | undefined, endpointIdx: number): MulticastTrackObject[] {\n\tif (!raw || raw.length === 0) return [];\n\treturn raw.split(\",\").map((entry, i) => {\n\t\tconst colon = entry.lastIndexOf(\":\");\n\t\tif (colon < 0) {\n\t\t\tthrow new MulticastInputError(\n\t\t\t\t\"invalid_track_name\",\n\t\t\t\tendpointIdx,\n\t\t\t\t`endpoint[${endpointIdx}] TRACKS[${i}] missing \":\" \u2014 expected \"name:packetId\", got ${JSON.stringify(entry)}`,\n\t\t\t);\n\t\t}\n\t\tconst name = entry.slice(0, colon).trim();\n\t\tconst idStr = entry.slice(colon + 1).trim();\n\t\tconst packetId = Number(idStr);\n\t\tif (!name || idStr.length === 0 || !Number.isFinite(packetId) || !Number.isInteger(packetId)) {\n\t\t\tthrow new MulticastInputError(\n\t\t\t\t\"invalid_packet_id\",\n\t\t\t\tendpointIdx,\n\t\t\t\t`endpoint[${endpointIdx}] TRACKS[${i}] entry malformed (need \"name:integer\"): ${JSON.stringify(entry)}`,\n\t\t\t);\n\t\t}\n\t\treturn { name, packetId };\n\t});\n}\n"],
5
- "mappings": "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",
6
- "names": ["MPU_HEADER_SIZE", "MPU_LENGTH_FIELD_SIZE", "MPU_HEADER_REMAINDER_SIZE", "MPU_HEADER_SIZE"]
3
+ "sources": ["../../mmt-base/src/mmtp.ts", "../../mmt-base/src/ntp-short.ts", "../../mmt-base/src/fec-block-profile.ts", "../src/multicast-input.ts"],
4
+ "sourcesContent": ["/**\n * @mmt/container - MMTP Parser\n *\n * MMTP (MMT Protocol) packet parser based on ISO/IEC 23008-1:2023 Section 9.2\n *\n * MMTP header format (12 bytes):\n * - Byte 0: V(2) | C(1) | FEC_type(2) | r(1) | X(1) | R(1)\n * - Byte 1: reserved(2) | packet_type(6)\n * - Bytes 2-3: packet_id (big-endian)\n * - Bytes 4-7: timestamp (big-endian, 32-bit)\n * - Bytes 8-11: sequence_number (big-endian)\n *\n * FEC_type values:\n * - 0: No FEC / source packet\n * - 1: FEC mode 0 (source with FEC payload ID)\n * - 2: FEC repair packet mode 0\n * - 3: FEC repair packet mode 1\n *\n * @packageDocumentation\n */\n\nexport interface MMTPHeader {\n version: number\n /** Packet type: 0x00=MPU, 0x02=Signaling, 0x03=Repair */\n packetType: number\n packetId: number\n timestamp: bigint\n sequenceNumber: number\n payloadLength: number\n fecType: number\n /** C flag: header carries a 32-bit packet_counter. */\n packetCounterFlag: boolean\n packetCounter?: number\n /** X flag: header carries an extension header and value. */\n extensionFlag: boolean\n extension?: { type: number; data: Uint8Array }\n /** Byte offset at which the MMTP payload begins. */\n headerLength: number\n /** Random Access Point flag - indicates keyframe (ISO 23008-1, byte 0 bit 0) */\n rapFlag: boolean\n}\n\nexport interface MPUHeader {\n mpuSequenceNumber: number\n fragmentationIndicator: number // FI: 0=complete, 1=first, 2=middle, 3=last\n /** 8-bit MPU fragment_counter from byte 3. */\n fragmentCounter: number\n /** T flag: 1=timed media (14-byte MFU DU header), 0=non-timed (4-byte header) */\n timedFlag: number\n /** A flag: aggregated MPU payload. This strict profile requires A=0. */\n aggregationFlag: boolean\n /** MPU payload_length (bytes 0-1): bytes after the length field, including\n * the remaining six MPU-header bytes and all data-unit bytes. */\n payloadLength: number\n}\n\nexport interface MFUDuHeader {\n movieFragmentSequenceNumber: number\n sampleNumber: number\n offset: number\n priority: number\n dependencyCounter: number\n}\n\nexport interface MMTPPacket {\n header: MMTPHeader\n payload: Uint8Array\n}\n\nexport const MMTP_HEADER_SIZE = 12\nexport const MPU_HEADER_SIZE = 8\nexport const MFU_DU_HEADER_SIZE_TIMED = 14 // T=1: timed media (video/audio samples)\nexport const MFU_DU_HEADER_SIZE_NONTIMED = 4 // T=0: non-timed media (item_id only)\n\nconst MPU_LENGTH_FIELD_SIZE = 2\nconst MPU_HEADER_REMAINDER_SIZE = MPU_HEADER_SIZE - MPU_LENGTH_FIELD_SIZE\nconst SOURCE_FEC_PAYLOAD_ID_SIZE = 4\n\n// MMTP Packet Types (ISO/IEC 23008-1:2023 Section 9.2.2)\nexport const PACKET_TYPE_MPU = 0x00\nexport const PACKET_TYPE_SIGNALING = 0x02\nexport const PACKET_TYPE_REPAIR = 0x03\n\n// AL-FEC Signaling Message ID (ISO/IEC 23008-1:2023 Amendment 1:2025)\nexport const ALFEC_MESSAGE_ID = 0x0203\n\n/**\n * MMTP `packet_id` of the signaling flow, which carries PACKET_TYPE_SIGNALING\n * packets such as the AL-FEC message (ALFEC_MESSAGE_ID). The FFmpeg `moq_mmt`\n * muxer sends that message on this packet_id into the same multicast group as\n * the media, and moqcast-draft (PR #44) makes advertising it for a track a\n * MUST NOT. A track's packet_id is therefore 1..MMTP_PACKET_ID_MAX.\n */\nexport const MMTP_SIGNALING_PACKET_ID = 0\n\n/** Largest value of the 16-bit MMTP `packet_id` header field. */\nexport const MMTP_PACKET_ID_MAX = 0xffff\n\n// FEC Code IDs\nexport const FEC_CODE_XOR = 1\nexport const FEC_CODE_RAPTORQ = 2\n\n/**\n * Parse MMTP header from buffer\n */\nexport function parseMMTPHeader(buffer: Uint8Array): MMTPHeader {\n if (buffer.length < MMTP_HEADER_SIZE) {\n throw new Error(`MMTP packet too small: ${buffer.length} bytes`)\n }\n\n const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength)\n\n const version = (buffer[0]! >> 6) & 0x03\n if (version !== 0) {\n throw new Error(`Unsupported MMTP version: ${version}`)\n }\n\n // Extract FEC type from byte 0, bits 3-4\n const fecType = (buffer[0]! >> 3) & 0x03\n const packetCounterFlag = (buffer[0]! & 0x20) !== 0\n const extensionFlag = (buffer[0]! & 0x02) !== 0\n\n // Extract RAP flag from byte 0, bit 0 (R bit - Random Access Point)\n const rapFlag = (buffer[0]! & 0x01) === 1\n\n // ISO/IEC 23008-1:2023 Figure 8 (V=0) places reserved(2) before\n // packet_type(6), so packet_type occupies the low six bits of byte 1.\n const packetType = buffer[1]! & 0x3F\n\n let offset = 2\n\n // Packet ID (2 bytes, big-endian)\n const packetId = view.getUint16(offset, false)\n offset += 2\n\n // Timestamp (4 bytes, big-endian)\n const timestamp = BigInt(view.getUint32(offset, false))\n offset += 4\n\n // Sequence number (4 bytes, big-endian)\n const sequenceNumber = view.getUint32(offset, false)\n offset += 4\n\n let packetCounter: number | undefined\n if (packetCounterFlag) {\n if (offset + 4 > buffer.length) throw new Error('MMTP packet truncated in packet_counter')\n packetCounter = view.getUint32(offset, false)\n offset += 4\n }\n\n let extension: { type: number; data: Uint8Array } | undefined\n if (extensionFlag) {\n if (offset + 4 > buffer.length) throw new Error('MMTP packet truncated in header extension')\n const type = view.getUint16(offset, false)\n const length = view.getUint16(offset + 2, false)\n offset += 4\n if (offset + length > buffer.length) throw new Error('MMTP packet truncated in header extension value')\n extension = { type, data: buffer.subarray(offset, offset + length) }\n offset += length\n }\n\n return {\n version,\n packetType,\n packetId,\n timestamp,\n sequenceNumber,\n payloadLength: buffer.length - offset,\n fecType,\n packetCounterFlag,\n packetCounter,\n extensionFlag,\n extension,\n headerLength: offset,\n rapFlag,\n }\n}\n\n/**\n * The MMTP `packet_id` of `buffer`, or null exactly when parseMMTPHeader would\n * throw for it (short buffer, V != 0, truncated packet_counter or header\n * extension).\n *\n * This is the per-packet filter a receiver runs before any other work, so it\n * builds no header object, DataView or BigInt timestamp (BLO-36599). Use\n * parseMMTPHeader when any other header field is needed.\n */\nexport function readMMTPPacketId(buffer: Uint8Array): number | null {\n if (buffer.length < MMTP_HEADER_SIZE) return null\n const flags = buffer[0]!\n if (((flags >> 6) & 0x03) !== 0) return null\n let offset = MMTP_HEADER_SIZE\n if ((flags & 0x20) !== 0) {\n if (offset + 4 > buffer.length) return null\n offset += 4\n }\n if ((flags & 0x02) !== 0) {\n if (offset + 4 > buffer.length) return null\n const length = (buffer[offset + 2]! << 8) | buffer[offset + 3]!\n if (offset + 4 + length > buffer.length) return null\n }\n return (buffer[2]! << 8) | buffer[3]!\n}\n\n/**\n * Unwrap MMTP packet to extract payload\n */\nexport function unwrapMMTP(buffer: Uint8Array): MMTPPacket {\n const header = parseMMTPHeader(buffer)\n const payload = buffer.subarray(header.headerLength)\n\n return { header, payload }\n}\n\n/**\n * Parse MPU header from payload\n * ISO/IEC 23008-1:2023 Section 9.2.3.3\n *\n * MPU Header format (8 bytes) as written by FFmpeg/libmmt:\n * - Bytes 0-1: payload_length (16 bits, big-endian)\n * - Byte 2: FT(4) | T(1) | FI(2) | A(1)\n * - Byte 3: fragment_counter (8 bits)\n * - Bytes 4-7: MPU_sequence_number (32 bits, big-endian)\n */\nexport function parseMPUHeader(payload: Uint8Array): MPUHeader {\n if (payload.length < MPU_HEADER_SIZE) {\n throw new Error(`Payload too small for MPU header: ${payload.length} bytes`)\n }\n\n const view = new DataView(payload.buffer, payload.byteOffset, payload.byteLength)\n\n // Bytes 0-1: payload_length (16 bits, big-endian)\n const payloadLength = view.getUint16(0, false)\n\n // Byte 2: FT(4) | T(1) | FI(2) | A(1)\n const flagsByte = payload[2]!\n const timedFlag = (flagsByte >> 3) & 0x01\n const fragmentationIndicator = (flagsByte >> 1) & 0x03\n const aggregationFlag = (flagsByte & 0x01) !== 0\n\n // Byte 3: fragment_counter (8 bits). The high bit is part of the counter in\n // the current libmmt/FFmpeg writer ABI.\n const fragmentCounter = payload[3]!\n\n // Bytes 4-7: MPU_sequence_number (32 bits, big-endian)\n const mpuSequenceNumber = view.getUint32(4, false)\n\n return {\n mpuSequenceNumber,\n fragmentationIndicator,\n fragmentCounter,\n timedFlag,\n aggregationFlag,\n payloadLength,\n }\n}\n\nexport function parseTimedMfuDuHeader(payload: Uint8Array, offset: number): MFUDuHeader | null {\n if (offset < 0 || offset + MFU_DU_HEADER_SIZE_TIMED > payload.length) return null\n const view = new DataView(payload.buffer, payload.byteOffset + offset, MFU_DU_HEADER_SIZE_TIMED)\n return {\n movieFragmentSequenceNumber: view.getUint32(0, false),\n sampleNumber: view.getUint32(4, false),\n offset: view.getUint32(8, false),\n priority: payload[offset + 12]!,\n dependencyCounter: payload[offset + 13]!,\n }\n}\n\n/**\n * Why `extractMFUPayload` refused a packet.\n *\n * These identifiers are part of the public contract: callers surface them in\n * diagnostics, so renaming one is a breaking change.\n */\nexport type MFURejectionGate =\n | 'packet-shorter-than-mmtp-plus-mpu-header'\n | 'mmtp-header-unparseable'\n | 'not-an-mpu-packet'\n | 'payload-shorter-than-mpu-header'\n | 'aggregation-flag-set'\n | 'mpu-payload-length-below-header-remainder'\n | 'mpu-declared-end-exceeds-payload'\n | 'mfu-offset-at-or-past-declared-end'\n | 'source-fec-payload-id-width-mismatch'\n | 'declared-trailers-unresolved'\n\nexport interface MFURejection {\n gate: MFURejectionGate\n /** Human-readable detail, including the byte arithmetic that decided it. */\n detail: string\n}\n\ninterface MfuPayloadLayout {\n hasMfuDuHeader: boolean\n mfuOffset: number\n dataEnd: number\n}\n\nfunction resolveMfuPayloadLayout(\n mpu: MPUHeader,\n fragmentType: number,\n payload: Uint8Array,\n): { layout: MfuPayloadLayout } | { rejection: MFURejection } {\n if (mpu.payloadLength < MPU_HEADER_REMAINDER_SIZE) {\n return {\n rejection: {\n gate: 'mpu-payload-length-below-header-remainder',\n detail: `payload_length=${mpu.payloadLength} < ${MPU_HEADER_REMAINDER_SIZE}`,\n },\n }\n }\n\n // ISO/IEC 23008-1 defines payload_length from immediately after its own\n // two-byte field. It therefore includes the remaining six MPU-header bytes.\n const declaredEnd = MPU_LENGTH_FIELD_SIZE + mpu.payloadLength\n if (declaredEnd > payload.length) {\n return {\n rejection: {\n gate: 'mpu-declared-end-exceeds-payload',\n detail:\n `payload_length=${mpu.payloadLength} implies end=${declaredEnd}, ` +\n `payload is ${payload.length} bytes`,\n },\n }\n }\n\n const hasHeader = fragmentType === 2 && mpu.fragmentationIndicator <= 1\n const mfuHeaderSize = mpu.timedFlag ? MFU_DU_HEADER_SIZE_TIMED : MFU_DU_HEADER_SIZE_NONTIMED\n const mfuOffset = MPU_HEADER_SIZE + (hasHeader ? mfuHeaderSize : 0)\n if (mfuOffset >= declaredEnd) {\n return {\n rejection: {\n gate: 'mfu-offset-at-or-past-declared-end',\n detail: `mfuOffset=${mfuOffset} >= declaredEnd=${declaredEnd}`,\n },\n }\n }\n\n return {\n layout: {\n hasMfuDuHeader: hasHeader,\n mfuOffset,\n dataEnd: declaredEnd,\n },\n }\n}\n\n// Legacy detached per-packet authenticator suffix `[0xf0a1][len][auth]`. Its\n// producer was removed (BLO-16483) and nothing verifies it any more; provenance\n// is the signed per-group manifest. Receivers still skip an exact suffix so a\n// stale producer degrades to unauthenticated playback instead of dropped\n// packets. ponytail: hard-cut to reject once no deployed producer can emit it.\nconst LEGACY_TRAILER_EXT_TYPE = 0xf0a1\nconst LEGACY_TRAILER_HEADER_SIZE = 4\n\nfunction isEmptyOrExactLegacyTrailer(payload: Uint8Array, start: number, end: number): boolean {\n if (start === end) return true\n if (start < 0 || end > payload.length || start + LEGACY_TRAILER_HEADER_SIZE > end) {\n return false\n }\n\n const view = new DataView(\n payload.buffer,\n payload.byteOffset + start,\n end - start,\n )\n if (view.getUint16(0, false) !== LEGACY_TRAILER_EXT_TYPE) return false\n\n const authLength = view.getUint16(2, false)\n return start + LEGACY_TRAILER_HEADER_SIZE + authLength === end\n}\n\ninterface DeclaredTrailerLayout {\n hasFecId: boolean\n}\n\nfunction resolveDeclaredTrailers(\n payload: Uint8Array,\n declaredEnd: number,\n fecType: number,\n options?: { skipFecId?: boolean },\n): DeclaredTrailerLayout | null {\n if (declaredEnd > payload.length) return null\n\n if (options?.skipFecId) {\n return isEmptyOrExactLegacyTrailer(payload, declaredEnd, payload.length)\n ? { hasFecId: false }\n : null\n }\n\n if (fecType === 1) {\n const fecStart = payload.length - SOURCE_FEC_PAYLOAD_ID_SIZE\n if (fecStart < declaredEnd || !isEmptyOrExactLegacyTrailer(payload, declaredEnd, fecStart)) {\n return null\n }\n return { hasFecId: true }\n }\n if (fecType !== 0) return null\n\n if (isEmptyOrExactLegacyTrailer(payload, declaredEnd, payload.length)) {\n return { hasFecId: false }\n }\n\n return null\n}\n\n/**\n * Extract NAL unit payload from MMTP packet\n */\nexport function extractNALUnit(mmtpPacket: Uint8Array): Uint8Array {\n const extraction = extractMFUPayload(mmtpPacket)\n if (!extraction) throw new Error('Malformed MMTP MPU payload')\n return extraction.data\n}\n\n/** Result of extracting MFU payload from an MMTP packet */\nexport interface MFUExtraction {\n /** MMTP header fields */\n header: MMTPHeader\n /** MPU header fields */\n mpu: MPUHeader\n /** Fragment type (FT field, 4 bits): 0=metadata, 2=MFU data */\n fragmentType: number\n /** MFU payload data (video/audio bytes, headers stripped, FEC ID excluded) */\n data: Uint8Array\n /** Parsed timed MFU DU header when present, otherwise null */\n mfuDuHeader: MFUDuHeader | null\n /** Non-timed MFU Item_ID when the 4-byte DU header is present. */\n itemId: number | null\n /** Source FEC Payload ID if present (`FEC_type == 1`), or null */\n fecPayloadId: { ssId: number } | null\n}\n\n/**\n * Extract MFU payload from a raw MMTP packet.\n *\n * Handles all the byte arithmetic for:\n * - MMTP header (12 bytes)\n * - MPU sub-header (8 bytes, payloadLength field)\n * - MFU DU header (14 bytes timed / 4 bytes non-timed, only for FI \u2264 1)\n * - Source FEC Payload ID (4 bytes at end, only when `FEC_type == 1`)\n *\n * `payloadLength` is the ISO length-after-field value: six remaining MPU-header\n * bytes, then the MFU DU header when present, then media. The legacy\n * authenticator suffix and the Source FEC trailer are outside that declared span and must consume the exact suffix.\n *\n * @param mmtpPacket Raw MMTP packet (from multicast or MoQ)\n * @param options.skipFecId When true, do NOT subtract the 4-byte Source FEC Payload ID\n * from the payload boundary, even if fecType >= 1 in the MMTP header. Use this for\n * FEC-recovered symbols where the FEC PID was already stripped before WASM decoding\n * but the fecType bits in byte 0 still reflect the original packet (FEC Type=1 per\n * draft-ramadan-moq-mmt-00 \u00A73.1).\n * @returns Extracted MFU data + metadata, or null if packet is too small\n */\ninterface MfuPlan {\n header: MMTPHeader\n payload: Uint8Array\n mpu: MPUHeader\n fragmentType: number\n layout: MfuPayloadLayout\n trailerLayout: DeclaredTrailerLayout\n}\n\n/**\n * Single source of truth for whether a packet yields an MFU payload.\n *\n * `extractMFUPayload` and `explainMFURejection` both delegate here, so the\n * reason reported to operators can never drift from the decision the parser\n * actually made.\n */\nfunction planMfuExtraction(\n mmtpPacket: Uint8Array,\n options?: { skipFecId?: boolean },\n parsedHeader?: MMTPHeader,\n): { plan: MfuPlan } | { rejection: MFURejection } {\n if (mmtpPacket.length < MMTP_HEADER_SIZE + MPU_HEADER_SIZE) {\n return {\n rejection: {\n gate: 'packet-shorter-than-mmtp-plus-mpu-header',\n detail: `${mmtpPacket.length} bytes < ${MMTP_HEADER_SIZE + MPU_HEADER_SIZE}`,\n },\n }\n }\n\n let header: MMTPHeader\n if (parsedHeader) {\n header = parsedHeader\n } else {\n try {\n header = parseMMTPHeader(mmtpPacket)\n } catch (error) {\n return {\n rejection: {\n gate: 'mmtp-header-unparseable',\n detail: error instanceof Error ? error.message : String(error),\n },\n }\n }\n }\n if (header.packetType !== PACKET_TYPE_MPU) {\n return {\n rejection: {\n gate: 'not-an-mpu-packet',\n detail: `packetType=0x${header.packetType.toString(16).padStart(2, '0')}`,\n },\n }\n }\n\n const payload = mmtpPacket.subarray(header.headerLength)\n if (payload.length < MPU_HEADER_SIZE) {\n return {\n rejection: {\n gate: 'payload-shorter-than-mpu-header',\n detail: `${payload.length} bytes < ${MPU_HEADER_SIZE}`,\n },\n }\n }\n\n const mpu = parseMPUHeader(payload)\n if (mpu.aggregationFlag) {\n return {\n rejection: { gate: 'aggregation-flag-set', detail: 'strict profile requires A=0' },\n }\n }\n\n // Fragment type from byte 2 upper nibble (not returned by parseMPUHeader)\n const fragmentType = (payload[2]! >> 4) & 0x0f\n\n const layoutResult = resolveMfuPayloadLayout(mpu, fragmentType, payload)\n if ('rejection' in layoutResult) return layoutResult\n const { layout } = layoutResult\n\n const trailerLayout = resolveDeclaredTrailers(\n payload,\n layout.dataEnd,\n header.fecType,\n options,\n )\n if (!trailerLayout) {\n // The most common cause is a producer whose Source FEC Payload ID width\n // disagrees with ISO/IEC 23008-1 \u00A7C.5.2. Name that explicitly rather than\n // leaving operators to infer it from a generic trailer error.\n if (!options?.skipFecId && header.fecType === 1) {\n const observed = payload.length - layout.dataEnd\n if (observed !== SOURCE_FEC_PAYLOAD_ID_SIZE) {\n return {\n rejection: {\n gate: 'source-fec-payload-id-width-mismatch',\n detail:\n `producer wrote a ${observed}-byte Source FEC Payload ID, ` +\n `ISO/IEC 23008-1 \u00A7C.5.2 requires ${SOURCE_FEC_PAYLOAD_ID_SIZE} ` +\n `(payload=${payload.length}, declaredEnd=${layout.dataEnd})`,\n },\n }\n }\n }\n return {\n rejection: {\n gate: 'declared-trailers-unresolved',\n detail:\n `trailer bytes [${layout.dataEnd}, ${payload.length}) are neither empty ` +\n `nor an exact legacy authenticator trailer (fecType=${header.fecType})`,\n },\n }\n }\n\n return { plan: { header, payload, mpu, fragmentType, layout, trailerLayout } }\n}\n\n/**\n * Report why `extractMFUPayload` would refuse this packet, or null if it would\n * accept it.\n *\n * `extractMFUPayload` returns bare `null` on ten distinct conditions, which\n * makes a producer/consumer framing disagreement indistinguishable from a\n * publisher that never started \u2014 both render as silence. Use this wherever\n * that distinction matters: diagnostics, contract tests, and operator-facing\n * logs.\n */\nexport function explainMFURejection(\n mmtpPacket: Uint8Array,\n options?: { skipFecId?: boolean },\n): MFURejection | null {\n const result = planMfuExtraction(mmtpPacket, options)\n return 'rejection' in result ? result.rejection : null\n}\n\nexport function extractMFUPayload(\n mmtpPacket: Uint8Array,\n options?: { skipFecId?: boolean },\n): MFUExtraction | null {\n return extractPlannedMfu(mmtpPacket, planMfuExtraction(mmtpPacket, options))\n}\n\n/**\n * extractMFUPayload for a caller that already holds this packet's parsed MMTP\n * header, so the header is not parsed a second time (BLO-36599).\n *\n * `header` must be `parseMMTPHeader(mmtpPacket)` of this same packet; the\n * result is then identical to `extractMFUPayload(mmtpPacket, options)`.\n */\nexport function extractMFUPayloadWithHeader(\n mmtpPacket: Uint8Array,\n header: MMTPHeader,\n options?: { skipFecId?: boolean },\n): MFUExtraction | null {\n return extractPlannedMfu(mmtpPacket, planMfuExtraction(mmtpPacket, options, header))\n}\n\nfunction extractPlannedMfu(\n mmtpPacket: Uint8Array,\n result: { plan: MfuPlan } | { rejection: MFURejection },\n): MFUExtraction | null {\n if ('rejection' in result) return null\n const { header, payload, mpu, fragmentType, layout, trailerLayout } = result.plan\n\n const mfuDuHeaderPresent = layout.hasMfuDuHeader\n const mfuOffset = layout.mfuOffset\n const dataEnd = layout.dataEnd\n\n const mfuDuHeader =\n mfuDuHeaderPresent && mpu.timedFlag === 1\n ? parseTimedMfuDuHeader(payload, MPU_HEADER_SIZE)\n : null\n const itemId =\n mfuDuHeaderPresent && mpu.timedFlag === 0\n ? new DataView(payload.buffer, payload.byteOffset + MPU_HEADER_SIZE, 4).getUint32(0, false)\n : null\n\n const data = payload.subarray(mfuOffset, dataEnd)\n\n const fecPayloadId = trailerLayout.hasFecId\n ? {\n ssId: new DataView(\n mmtpPacket.buffer,\n mmtpPacket.byteOffset + mmtpPacket.length - SOURCE_FEC_PAYLOAD_ID_SIZE,\n SOURCE_FEC_PAYLOAD_ID_SIZE,\n ).getUint32(0, false),\n }\n : null\n\n return { header, mpu, fragmentType, data, mfuDuHeader, itemId, fecPayloadId }\n}\n\n/**\n * Extract timestamp from MMTP packet\n */\nexport function extractTimestamp(mmtpPacket: Uint8Array): bigint {\n const { header } = unwrapMMTP(mmtpPacket)\n return header.timestamp\n}\n\n/**\n * Check if MMTP packet is an FEC repair packet\n * FEC_type 2 and 3 are repair packets\n */\nexport function isFecRepairPacket(mmtpPacket: Uint8Array): boolean {\n if (mmtpPacket.length < 1) return false\n const fecType = (mmtpPacket[0]! >> 3) & 0x03\n return fecType >= 2\n}\n\n/**\n * Extract Source FEC Payload ID (SS_ID) from the last 4 bytes of an MMTP\n * source packet per ISO/IEC 23008-1:2023 Section C.5.2 Figure C.10.\n *\n * SS_ID is a flat 32-bit monotonic counter (incremented by 1 per packet\n * for ssbg_mode0 and ssbg_mode1). Block membership and ESI use the\n * configured K; repair SS_Start identifies the first SS_ID of the block and\n * SSB_length records the authored source-symbol count:\n * SBN = floor(SS_ID / K), ESI = SS_ID % K\n * SS_Start = SBN * K (first SS_ID of the block)\n *\n * This matches FFmpeg moqenc_mmt.c, the drafts (draft-ramadan-moq-fec \u00A76.2),\n * and ATSC A/331 \u00A78.1.2.5 which defers to ISO 23008-1 Annex C.\n */\nexport function extractFecPayloadId(mmtpPacket: Uint8Array): { ssId: number } | null {\n const header = parseHeaderForSourceFecId(mmtpPacket)\n return header ? sourceFecPayloadIdOf(mmtpPacket, header) : null\n}\n\n/**\n * Return true when FEC_type=1 and the exact suffix after the ISO-declared MPU\n * payload contains a four-byte Source FEC Payload ID.\n */\nexport function hasSourceFecPayloadId(mmtpPacket: Uint8Array): boolean {\n const header = parseHeaderForSourceFecId(mmtpPacket)\n return header !== null && sourceFecPayloadIdOf(mmtpPacket, header) !== null\n}\n\nfunction parseHeaderForSourceFecId(mmtpPacket: Uint8Array): MMTPHeader | null {\n if (mmtpPacket.length < MMTP_HEADER_SIZE + MPU_HEADER_SIZE) return null\n try {\n return parseMMTPHeader(mmtpPacket)\n } catch {\n return null\n }\n}\n\n/**\n * The Source FEC Payload ID of a packet whose MMTP header the caller already\n * parsed, or null when the packet carries none: present only when FEC_type=1\n * and the exact suffix after the ISO-declared MPU payload holds the four-byte\n * ID (ISO/IEC 23008-1 \u00A7C.5.2). Same answer as extractFecPayloadId, without a\n * second header parse (BLO-36599).\n *\n * `header` must be `parseMMTPHeader(mmtpPacket)` of this same packet.\n */\nexport function sourceFecPayloadIdOf(\n mmtpPacket: Uint8Array,\n header: MMTPHeader,\n): { ssId: number } | null {\n if (mmtpPacket.length < MMTP_HEADER_SIZE + MPU_HEADER_SIZE) return null\n if (header.packetType !== PACKET_TYPE_MPU) return null\n\n const payload = mmtpPacket.subarray(header.headerLength)\n if (payload.length < MPU_HEADER_SIZE) return null\n const payloadLength = (payload[0]! << 8) | payload[1]!\n if (payloadLength < MPU_HEADER_REMAINDER_SIZE) return null\n const declaredEnd = MPU_LENGTH_FIELD_SIZE + payloadLength\n if (!resolveDeclaredTrailers(payload, declaredEnd, header.fecType)?.hasFecId) return null\n\n // Flat 4-byte SS_ID: encoder writes block_id * K + symbol_id\n const off = mmtpPacket.length - SOURCE_FEC_PAYLOAD_ID_SIZE\n return {\n ssId:\n ((mmtpPacket[off]! << 24) |\n (mmtpPacket[off + 1]! << 16) |\n (mmtpPacket[off + 2]! << 8) |\n mmtpPacket[off + 3]!) >>>\n 0,\n }\n}\n\n/** Size of Repair FEC Payload ID per ISO/IEC 23008-1 Section C.5.3 */\nexport const REPAIR_FEC_PID_SIZE = 13\n\n/**\n * Parse the 13-byte Repair FEC Payload ID after the complete dynamic MMTP header.\n * `parsedHeader`, when given, must be `parseMMTPHeader(mmtpPacket)` of this\n * same packet; it spares the second header parse (BLO-36599).\n */\nfunction readRepairFecPayloadIdFields(mmtpPacket: Uint8Array, parsedHeader?: MMTPHeader): {\n packetId: number; ssStart: number; rsbLength: number; rsId: number; ssbLength: number; dataOffset: number\n} | null {\n let header: MMTPHeader\n if (parsedHeader) {\n header = parsedHeader\n } else {\n try {\n header = parseMMTPHeader(mmtpPacket)\n } catch {\n return null\n }\n }\n if (header.packetType !== PACKET_TYPE_REPAIR || header.fecType !== 2) return null\n\n const off = header.headerLength\n if (mmtpPacket.length < off + REPAIR_FEC_PID_SIZE) return null\n const ssStart =\n ((mmtpPacket[off]! << 24) |\n (mmtpPacket[off + 1]! << 16) |\n (mmtpPacket[off + 2]! << 8) |\n mmtpPacket[off + 3]!) >>>\n 0 // 32 bits\n const rsbLength = (mmtpPacket[off + 4]! << 16) | (mmtpPacket[off + 5]! << 8) | mmtpPacket[off + 6]! // 24 bits\n const rsId = (mmtpPacket[off + 7]! << 16) | (mmtpPacket[off + 8]! << 8) | mmtpPacket[off + 9]! // 24 bits\n const ssbLength = (mmtpPacket[off + 10]! << 16) | (mmtpPacket[off + 11]! << 8) | mmtpPacket[off + 12]! // 24 bits\n return {\n packetId: header.packetId,\n ssStart,\n rsbLength,\n rsId,\n ssbLength,\n dataOffset: off + REPAIR_FEC_PID_SIZE,\n }\n}\n\n/**\n * Extract Repair FEC Payload ID from a raw-multicast MMTP repair packet.\n * Per ISO/IEC 23008-1:2023 Section C.5.3 (ssbg_mode0, one-stage, no FFSRP_TS):\n * [MMTP header][SS_Start:4][RSB_length:3][RS_ID:3][SSB_length:3][repair data]\n * OTI is delivered via AL-FEC signaling message (Table C.3), not per-packet.\n *\n * Byte 1 is reserved(2)|packet_type(6) for V=0, so repair type 0x03 is 0x03.\n * Pre-migration high-bits bytes (0x0c) are rejected \u2014 no compatibility decode.\n */\nexport function extractRepairFecPayloadId(mmtpPacket: Uint8Array): {\n ssStart: number; rsbLength: number; rsId: number; ssbLength: number\n} | null {\n const fields = readRepairFecPayloadIdFields(mmtpPacket)\n if (!fields) return null\n return {\n ssStart: fields.ssStart,\n rsbLength: fields.rsbLength,\n rsId: fields.rsId,\n ssbLength: fields.ssbLength,\n }\n}\n\n/** Parsed MoQ repair frame per ISO/IEC 23008-1 Section C.4.3 + C.5.3 */\nexport interface MoqRepairFrame {\n /** MMTP packetId (1=video, 2=audio) \u2014 for routing to correct FEC decoder */\n packetId: number\n /** SS_Start (32 bits): SS_ID of first source symbol in block */\n ssStart: number\n /** RSB_length (24 bits): number of repair symbols (P) */\n rsbLength: number\n /** RS_ID (24 bits): repair symbol index (0-based) */\n rsId: number\n /** SSB_length (24 bits): authored source-symbol count (1..=configured K) */\n ssbLength: number\n /** Repair symbol data after the dynamic MMTP header and 13-byte FEC payload ID. */\n symbolData: Uint8Array\n}\n\n/**\n * Parse a MoQ repair frame (ISO/IEC 23008-1 Annex C format).\n *\n * Wire format per ISO 23008-1 \u00A7C.4.3 + \u00A7C.5.3:\n * MMTP Header: fixed 12 bytes plus C/X optional fields\n * then SS_Start (4 bytes) \u2192 block identifier\n * then RSB_length (3 bytes) \u2192 P\n * then RS_ID (3 bytes) \u2192 ESI = K + RS_ID\n * then SSB_length (3 bytes) \u2192 authored source-symbol count (1..=K)\n * then Repair Symbol Data (T bytes)\n *\n * @param frame - Raw MoQ repair frame bytes\n * @returns Parsed frame or null if invalid\n */\nexport function parseMoqRepairFrame(frame: Uint8Array): MoqRepairFrame | null {\n return moqRepairFrameFrom(frame, readRepairFecPayloadIdFields(frame))\n}\n\n/**\n * parseMoqRepairFrame for a caller that already holds this frame's parsed MMTP\n * header, so the header is not parsed a second time (BLO-36599). The router's\n * multicast repair branch uses it: one repair packet in every K + P otherwise\n * paid two header parses.\n *\n * `header` must be `parseMMTPHeader(frame)` of this same frame; the result is\n * then identical to `parseMoqRepairFrame(frame)`.\n */\nexport function parseMoqRepairFrameWithHeader(\n frame: Uint8Array,\n header: MMTPHeader,\n): MoqRepairFrame | null {\n return moqRepairFrameFrom(frame, readRepairFecPayloadIdFields(frame, header))\n}\n\nfunction moqRepairFrameFrom(\n frame: Uint8Array,\n repairId: ReturnType<typeof readRepairFecPayloadIdFields>,\n): MoqRepairFrame | null {\n if (!repairId) return null\n\n const symbolData = frame.subarray(repairId.dataOffset)\n if (\n repairId.rsbLength === 0 ||\n repairId.ssbLength === 0 ||\n repairId.rsId >= repairId.rsbLength ||\n symbolData.length === 0\n ) return null\n\n return {\n packetId: repairId.packetId,\n ssStart: repairId.ssStart,\n rsbLength: repairId.rsbLength,\n rsId: repairId.rsId,\n ssbLength: repairId.ssbLength,\n symbolData,\n }\n}\n\n// The signalling-message parsers (9.3.4.2 payload header, AL-FEC message,\n// PA/MPT) live in ./signalling.ts.\n", "/**\n * NTP short format (RFC 5905 \u00A76): an unsigned 32-bit, 16.16 fixed-point count\n * of seconds. The MMTP header `timestamp` field (ISO/IEC 23008-1 \u00A79.2.2)\n * carries each packet's UTC sending instant in this format, so the value wraps\n * to 0 every 2^16 s (~18.2 h).\n *\n * Plain numeric order on these values breaks at every wrap: a retained\n * `Math.max` stays at the pre-wrap value and every later instant compares as\n * older. Order send times only with {@link compareNtpShort}, which applies\n * RFC 1982 serial-number arithmetic over the field's width.\n */\n\n/** Width in bits of the MMTP `timestamp` field (NTP short). */\nexport const NTP_SHORT_BITS = 32\n\n/** Number of distinct NTP-short values; the field wraps to 0 here. */\nexport const NTP_SHORT_MODULUS = 2 ** NTP_SHORT_BITS\n\n/**\n * RFC 1982 \u00A73.2's `2^(SERIAL_BITS - 1)`. Two values whose forward distance is\n * below this are ordered; exactly this far apart they are not ordered at all.\n * Two instants must be less than this far apart (2^15 s, ~9.1 h of send time)\n * for the comparison to give their true order.\n */\nexport const NTP_SHORT_HALF_RANGE = 2 ** (NTP_SHORT_BITS - 1)\n\n/**\n * Serial order of one NTP-short value relative to another.\n *\n * `'unordered'` is RFC 1982's undefined case: the values are exactly\n * {@link NTP_SHORT_HALF_RANGE} apart, so neither precedes the other. It is\n * never folded into `'before'` or `'after'`; each caller decides what an\n * unorderable pair means for it.\n */\nexport type NtpShortOrder = 'before' | 'equal' | 'after' | 'unordered'\n\n/** True when `value` is an NTP-short value: an integer in [0, NTP_SHORT_MODULUS). */\nexport function isNtpShort(value: number): boolean {\n return Number.isInteger(value) && value >= 0 && value < NTP_SHORT_MODULUS\n}\n\n/**\n * RFC 1982 serial comparison of NTP-short `a` against `b`: `'after'` when `a`\n * is the later instant, `'before'` when it is the earlier one.\n *\n * The answer is the true order only while the two instants are less than\n * {@link NTP_SHORT_HALF_RANGE} apart. Past that a 32-bit field cannot tell\n * later from earlier, and the result names whichever direction is nearer.\n * This is not a total order (it is not transitive across the whole space), so\n * it must never be used to sort.\n *\n * @throws {RangeError} if either operand is not an NTP-short value\n */\nexport function compareNtpShort(a: number, b: number): NtpShortOrder {\n if (!isNtpShort(a) || !isNtpShort(b)) {\n throw new RangeError(\n `NTP-short values must be integers in [0, 2^${NTP_SHORT_BITS}), got ${String(a)} and ${String(b)}`,\n )\n }\n if (a === b) return 'equal'\n // Forward distance from b to a, reduced into [1, NTP_SHORT_MODULUS). Both\n // operands are below 2^32, so every step is exact in a double.\n const forward = (a - b + NTP_SHORT_MODULUS) % NTP_SHORT_MODULUS\n if (forward < NTP_SHORT_HALF_RANGE) return 'after'\n if (forward > NTP_SHORT_HALF_RANGE) return 'before'\n return 'unordered'\n}\n", "/**\n * FEC block profile (BLO-35597, ISO/IEC 23008-1 Annex C).\n *\n * Every catalog `fec` object that declares an FEC instance names its block\n * profile in `fec.blockProfile`. A receiver applies all of a profile's rules\n * or none of them, so the token is the one explicit per-stream discriminator:\n * it is never defaulted and never inferred from the wire.\n *\n * draft-ramadan-moq-fec \u00A75.1 defines exactly one value, and it is the only\n * profile this library implements:\n *\n * - `iso-ssbg1-v1`: ssbg_mode1 (C.3.3): a source symbol is\n * `u16 length || protected packet || zero padding` to T; a block is a\n * K' = SSB_length RaptorQ block (C.5.3) and repair ESI = SSB_length + RS_ID;\n * SS_ID increments by one per FEC source packet and wraps at 2^32 (C.5.2),\n * so a block is located by membership in `[SS_Start, SS_Start + SSB_length)`\n * under RFC 1982 serial arithmetic; the MoQ repair group id is the 62-bit\n * wrap-epoch-extended SS_Start. The AL-FEC coding byte for this profile is\n * 0x14 (one-stage, ssbg_mode1, fec_payload_id_mode 0, Amd 1:2025 Table C.3).\n *\n * A track whose `blockProfile` is absent or names any other token (the\n * retired Blockcast-private `k-padded-v1` included) has no FEC: a receiver\n * consumes its source without FEC and joins none of its repair tracks\n * (draft-ramadan-moq-fec \u00A75.1). `parseFecBlockProfile` throws for such a\n * token; the per-track catalog resolver turns the same rule into a typed\n * per-track \"FEC unavailable\" outcome instead of failing the session.\n */\nimport { serial32Add, serial32Distance, serial32Precedes } from './serial-number.js'\n\n/** The block profile a catalog `fec.blockProfile` may name: the one this library implements. */\nexport type FecBlockProfile = 'iso-ssbg1-v1'\n\n/** Every accepted `fec.blockProfile` token. */\nexport const FEC_BLOCK_PROFILES: readonly FecBlockProfile[] = Object.freeze([\n 'iso-ssbg1-v1',\n])\n\n/** True for a block-profile token this library implements. */\nexport function isFecBlockProfile(value: unknown): value is FecBlockProfile {\n return value === 'iso-ssbg1-v1'\n}\n\n/**\n * Read a required `fec.blockProfile`. Throws on an absent token and on any\n * token other than `iso-ssbg1-v1`: there is no default and nothing is\n * inferred from the wire (draft-ramadan-moq-fec \u00A75.1).\n *\n * @param where the catalog path of the field, for the error message.\n */\nexport function parseFecBlockProfile(value: unknown, where: string): FecBlockProfile {\n if (isFecBlockProfile(value)) return value\n if (value === undefined) {\n throw new Error(`${where} is required: name the FEC block profile (\"iso-ssbg1-v1\")`)\n }\n throw new Error(`${where} must be \"iso-ssbg1-v1\" (got ${JSON.stringify(value)})`)\n}\n\n/** ISO/IEC 23008-1 C.3.3 ssbg_mode1: the two-octet packet length field. */\nexport const SSBG1_LENGTH_FIELD_BYTES = 2\n\n/** The largest protected packet an ssbg_mode1 symbol of size T can carry. */\nexport function ssbg1MaxPacketLength(symbolSize: number): number {\n return symbolSize - SSBG1_LENGTH_FIELD_BYTES\n}\n\n/** Whether a protected packet of `packetLength` bytes fits one T-byte ssbg_mode1 symbol. */\nexport function ssbg1FitsSymbol(packetLength: number, symbolSize: number): boolean {\n return (\n Number.isSafeInteger(packetLength) &&\n packetLength >= 0 &&\n packetLength <= ssbg1MaxPacketLength(symbolSize) &&\n packetLength <= 0xffff\n )\n}\n\n/**\n * Build one ssbg_mode1 source symbol (C.3.3): the protected packet's length\n * in two octets, network byte order, then the packet, then zero octets up to\n * T. The protected packet is the MMTP packet before its Source FEC Payload ID\n * is appended (C.4.2).\n *\n * @throws when `length + 2 > T`: the packet cannot be protected under this\n * profile, and silently truncating it would corrupt every repair symbol.\n */\nexport function buildSsbg1SourceSymbol(packet: Uint8Array, symbolSize: number): Uint8Array {\n if (!Number.isSafeInteger(symbolSize) || symbolSize <= SSBG1_LENGTH_FIELD_BYTES) {\n throw new Error(`ssbg_mode1 symbol size must exceed ${SSBG1_LENGTH_FIELD_BYTES} (got ${symbolSize})`)\n }\n if (!ssbg1FitsSymbol(packet.byteLength, symbolSize)) {\n throw new Error(\n `ssbg_mode1 source symbol needs ${packet.byteLength} + ${SSBG1_LENGTH_FIELD_BYTES} bytes but T is ${symbolSize}`,\n )\n }\n const symbol = new Uint8Array(symbolSize)\n symbol[0] = (packet.byteLength >>> 8) & 0xff\n symbol[1] = packet.byteLength & 0xff\n symbol.set(packet, SSBG1_LENGTH_FIELD_BYTES)\n return symbol\n}\n\n/**\n * Recover the protected packet from one ssbg_mode1 source symbol: the bytes\n * the length field names. Returns null, never a guess, when the length field\n * points past the symbol or any padding octet is non-zero (C.3.3 padding is\n * all 00h), since either means the recovered symbol is not what was sent.\n */\nexport function trimSsbg1SourceSymbol(symbol: Uint8Array): Uint8Array | null {\n if (symbol.byteLength < SSBG1_LENGTH_FIELD_BYTES) return null\n const length = (symbol[0]! << 8) | symbol[1]!\n const end = SSBG1_LENGTH_FIELD_BYTES + length\n if (end > symbol.byteLength) return null\n for (let i = end; i < symbol.byteLength; i++) {\n if (symbol[i] !== 0) return null\n }\n return symbol.subarray(SSBG1_LENGTH_FIELD_BYTES, end)\n}\n\n/** The SS_ID after the last source symbol of `[ssStart, ssStart + ssbLength)`. */\nexport function isoBlockEnd(ssStart: number, ssbLength: number): number {\n return serial32Add(ssStart, ssbLength)\n}\n\n/**\n * Range membership (C.5.2, C.5.3): whether `ssId` is one of the\n * `ssbLength` consecutive SS_IDs starting at `ssStart`, modulo 2^32.\n */\nexport function isoBlockContains(ssStart: number, ssbLength: number, ssId: number): boolean {\n return serial32Distance(ssStart, ssId) < ssbLength\n}\n\n/**\n * A lower bound on how many blocks the block starting at `a` lies behind the\n * one starting at `b` (0 when `a` does not precede `b`). An iso-ssbg1-v1\n * block holds at most `kMax` source symbols (the catalog `sourceSymbols`, the\n * AL-FEC maximum_k_for_repair_flow), so `n * kMax` SS_IDs always span at least\n * `n` blocks. Every interleave horizon in the receiver (decoder retention,\n * worker retirement, ordered-relay settlement) uses this one bound, so the\n * manager's horizon can never fall inside the decoder's.\n */\nexport function isoBlocksBehindLowerBound(a: number, b: number, kMax: number): number {\n if (!Number.isSafeInteger(kMax) || kMax <= 0) {\n throw new Error(`iso-ssbg1-v1 block horizon needs a positive maximum K (got ${kMax})`)\n }\n return serial32Precedes(a, b) ? Math.floor(serial32Distance(a, b) / kMax) : 0\n}\n", "// Module-local ambient: `multicastFromEnv` reads `process.env` when present\n// (Node, FFI host) and falls back to `{}` in browser contexts. The package\n// targets both, so we don't pull in `@types/node` org-wide \u2014 declaring the\n// minimal surface we touch keeps the dependency graph tight and the\n// published .d.ts free of node-isms.\ndeclare const process: { env: Record<string, string | undefined> } | undefined;\n\n/**\n * Multicast catalog input contract \u2014 validator + builder.\n *\n * Per draft-ramadan-moq-multicast-00 \u00A73 + \u00A74.1, the publisher (FFmpeg,\n * cast bridge, GStreamer, OBS-bridge) populates `multicast.endpoints[]`\n * in the catalog when its output includes a UDP multicast destination.\n *\n * This module is the TypeScript-side validator over a versioned input\n * shape. Sister-language validators (FFmpeg C, cast Rust) target the\n * same invariants \u2014 eventually via a shared JSON Schema + fixtures\n * (BLO-3341 follow-up); today parity is enforced by code review against\n * 1:1 ports. Each publisher's integration MUST also assert that the\n * declared `(sourceAddress, groupAddress, port)` matches what it\n * actually emits \u2014 that's a separate runtime check the helper cannot\n * perform.\n *\n * Validation invariants (per codex review of BLO-3323 v2):\n * - protocol === 'ssm' \u2192 sourceAddress REQUIRED\n * - protocol === 'amt' \u2192 a catalog-level `multicast.networkSource` entry of\n * type \"amt\" REQUIRED; its `relay` is required only for\n * `discovery: \"manual\"` (\"driad\" resolves the relay at runtime).\n * A per-endpoint `relayAddress` input is LIFTED into that entry and is\n * never emitted onto the endpoint.\n * - protocol === 'atsc3' \u2192 a catalog-level `multicast.networkSource` entry of\n * type \"atsc3\" REQUIRED; it carries the frequency, PLP, service id and SLS\n * URI, which is the only tuning information an ATSC3 receiver has.\n * - protocol === 'ssm' | 'asm' \u2192 no `networkSource` entry required: a native\n * join describes no alternate bearer (BLO-37926).\n * - groupAddress is a valid IPv4 multicast group (224.0.0.0/4)\n * or IPv6 multicast group (ff00::/8)\n * - SSM groupAddress MUST be in 232.0.0.0/8 (IPv4) or ff3x::/32 (IPv6),\n * where the IPv6 form requires P=1 + T=1 (the `3` nibble) AND the\n * 16-bit reserved-zero second hextet per RFC 3306 \u00A74 / RFC 4607.\n * - port in 1..65535\n * - tracks non-empty; per-track packetId in 1..65535\n * - packetId 0 (`MMTP_SIGNALING_PACKET_ID`) is the MMTP signaling flow\n * (FFmpeg `moq_mmt` sends the AL-FEC message 0x0203 on it into the same\n * multicast group) and is refused for a track with its own kind,\n * `signaling_packet_id`. The kind string matches cast's\n * `multicast_input.rs` so the two ports report the same reason.\n * - packetIds unique within an endpoint\n * - (sourceAddress, groupAddress, port) tuple unique across endpoints\n * - bandwidth (if present) > 0\n * - validUntil (if present) is a positive unix-ms timestamp\n *\n * @see draft-ramadan-moq-multicast-00 \u00A74.1\n * @see .planning/drafts/blo-3323-multicast-catalog-source-of-truth.md\n */\n\nimport { MMTP_PACKET_ID_MAX, MMTP_SIGNALING_PACKET_ID } from \"@blockcast/mmt-base\";\nimport {\n\ttype AmtDiscovery,\n\ttype MulticastCatalogConfig,\n\ttype MulticastEndpoint,\n\ttype MulticastProtocol,\n\ttype MulticastTrackObject,\n\ttype NetworkSource,\n} from \"./carp-catalog.js\";\n\n/**\n * Strict typed input for one multicast endpoint. The publisher provides\n * this shape (env vars / CLI / config record), the helper validates and\n * lifts it into a `MulticastEndpoint` ready to embed in the catalog.\n */\nexport interface MulticastEndpointInput {\n\treadonly protocol: MulticastProtocol;\n\treadonly sourceAddress?: string;\n\treadonly groupAddress: string;\n\treadonly port: number;\n\t/** New emitters MUST use the object form (name + packetId). */\n\treadonly tracks: ReadonlyArray<MulticastTrackObject>;\n\t/**\n\t * AMT relay for this endpoint.\n\t *\n\t * Publisher-side input only \u2014 it is LIFTED into the catalog-level\n\t * `multicast.networkSource` and is never written onto the emitted endpoint\n\t * (the normative endpoint model carries no relay field). Because\n\t * `networkSource` is catalog-level, a relay that differs from the catalog's\n\t * AMT relay cannot be expressed and is rejected with `conflicting_amt_relay`.\n\t *\n\t * Prefer declaring `MulticastInput.networkSource` directly.\n\t */\n\treadonly relayAddress?: string;\n\t/** Aggregate bandwidth in bits per second. RECOMMENDED per draft \u00A74.1. */\n\treadonly bandwidth?: number;\n}\n\n/**\n * Strict typed input for the catalog-level multicast extension.\n *\n * `version` (monotonic) + `validUntil` (unix ms) support the catalog\n * freshness model: subscribers refetch before expiry, and publishers\n * MUST republish (with bumped `version`) on any endpoint mutation.\n */\nexport interface MulticastInput {\n\treadonly enabled?: boolean;\n\treadonly endpoints: ReadonlyArray<MulticastEndpointInput>;\n\t/**\n\t * Canonical relay/tuning declaration (draft-ramadan-moq-multicast \u00A74.1).\n\t * When omitted, one is derived from `amtFallback` and any endpoint\n\t * `relayAddress`; when present no derivation happens. Either way every entry\n\t * is validated, and an endpoint `relayAddress` it does not carry is\n\t * rejected (`conflicting_amt_relay`). Omit it entirely for native\n\t * multicast: it describes alternate bearers only (BLO-37926).\n\t */\n\treadonly networkSource?: ReadonlyArray<NetworkSource>;\n\t/** @deprecated Lifted into `networkSource`; no longer emitted. */\n\treadonly amtFallback?: { readonly discovery: AmtDiscovery; readonly relayDns?: string };\n\treadonly version?: number;\n\treadonly validUntil?: number;\n}\n\nexport type MulticastInputErrorKind =\n\t| \"empty_endpoints\"\n\t/**\n\t * `endpoints` was not an array at all. Distinct from `empty_endpoints`,\n\t * which reports a well-formed array that declares nothing: a caller\n\t * handling these programmatically needs \"you passed the wrong shape\"\n\t * apart from \"you passed nothing\".\n\t */\n\t| \"invalid_endpoints\"\n\t| \"ssm_requires_source\"\n\t/**\n\t * Deliberately overloaded, unlike its one-site mirror\n\t * `atsc3_requires_tuning`: an AMT relay can be missing at two layers, and\n\t * both mean \"this endpoint names a relay nobody can reach\". Raised for an\n\t * `amt` endpoint with no `networkSource` entry of type `\"amt\"`, and for an\n\t * entry whose `discovery: \"manual\"` names no `relay`. Splitting them would\n\t * make callers handle two kinds for one operator-visible fault.\n\t */\n\t| \"amt_requires_relay\"\n\t/**\n\t * An `atsc3` endpoint with no `networkSource` entry of type `\"atsc3\"`. The\n\t * ATSC3 entry carries the frequency, PLP, service id and SLS URI \u2014 the only\n\t * tuning information there is \u2014 so without it the endpoint names a broadcast\n\t * nobody can tune to. The AMT mirror of this is `amt_requires_relay`; native\n\t * joins (\"ssm\", \"asm\") describe no bearer and are exempt (BLO-37926).\n\t */\n\t| \"atsc3_requires_tuning\"\n\t/**\n\t * An endpoint `relayAddress` is not the relay of any AMT `networkSource`\n\t * entry (another endpoint's relay, `amtFallback.relayDns`, or an explicit\n\t * entry won). `networkSource` is catalog-level, so per-endpoint relays\n\t * cannot be represented; fail closed rather than silently drop one.\n\t */\n\t| \"conflicting_amt_relay\"\n\t/**\n\t * A `networkSource` AMT entry's `discovery` was not `\"driad\"` or `\"manual\"`,\n\t * the schema enum. Checked on explicit and derived entries alike.\n\t */\n\t| \"invalid_amt_discovery\"\n\t/**\n\t * A `networkSource` entry the schema rejects: unknown `type`, a key outside\n\t * its `type`'s shape, a blank `relay`, or an out-of-range ATSC3 field.\n\t */\n\t| \"invalid_network_source\"\n\t| \"invalid_group_address\"\n\t| \"ssm_group_out_of_range\"\n\t| \"invalid_port\"\n\t| \"empty_tracks\"\n\t| \"invalid_track_name\"\n\t| \"invalid_packet_id\"\n\t/**\n\t * A track advertised on `MMTP_SIGNALING_PACKET_ID` (0), the MMTP signaling\n\t * flow. Distinct from `invalid_packet_id` so the reason is legible: 0 is a\n\t * well-formed 16-bit value, it is just not a track's to claim.\n\t */\n\t| \"signaling_packet_id\"\n\t| \"duplicate_packet_id\"\n\t| \"duplicate_endpoint_tuple\"\n\t| \"invalid_bandwidth\"\n\t| \"invalid_valid_until\"\n\t| \"invalid_version\"\n\t| \"invalid_env_shape\";\n\nexport class MulticastInputError extends Error {\n\tconstructor(\n\t\tpublic readonly kind: MulticastInputErrorKind,\n\t\tpublic readonly endpointIndex: number | null,\n\t\tmessage: string,\n\t) {\n\t\tsuper(message);\n\t\tthis.name = \"MulticastInputError\";\n\t}\n}\n\n/**\n * Validate a typed publisher input and lift it into a\n * `MulticastCatalogConfig` ready for `catalog.multicast = \u2026`.\n *\n * Throws `MulticastInputError` on any invariant violation. Fail-fast\n * at config-load time, never at runtime \u2014 silent fallbacks hide bugs\n * upstream (per the project's \"no anti-patterns\" directive).\n */\nexport function multicastFromConfig(input: MulticastInput): MulticastCatalogConfig {\n\t// Both array-valued inputs are checked here, before anything indexes them.\n\t// These helpers are consumed out-of-tree from plain JS (see\n\t// `validateNetworkSourceEntry`), so the declared types are no guarantee \u2014\n\t// and object-instead-of-array is the likely mistake on both: `amtFallback`,\n\t// which `networkSource` replaces, was a bare object. Without these a wrong\n\t// shape escapes as a raw `TypeError` from `.map`, which a caller catching\n\t// `MulticastInputError` does not catch.\n\tif (!Array.isArray(input.endpoints)) {\n\t\tthrow new MulticastInputError(\n\t\t\t\"invalid_endpoints\",\n\t\t\tnull,\n\t\t\t`endpoints must be an array of endpoint objects; got ${typeof input.endpoints}`,\n\t\t);\n\t}\n\tif (input.networkSource !== undefined && !Array.isArray(input.networkSource)) {\n\t\tthrow new MulticastInputError(\n\t\t\t\"invalid_network_source\",\n\t\t\tnull,\n\t\t\t\"networkSource must be an array of entries; got \" +\n\t\t\t\t`${typeof input.networkSource} \u2014 a single entry still goes in a one-element array`,\n\t\t);\n\t}\n\tif (input.endpoints.length === 0) {\n\t\tthrow new MulticastInputError(\n\t\t\t\"empty_endpoints\",\n\t\t\tnull,\n\t\t\t\"endpoints[] must be non-empty \u2014 a multicast field with zero endpoints declares nothing\",\n\t\t);\n\t}\n\tif (input.version !== undefined && (!Number.isInteger(input.version) || input.version < 0)) {\n\t\tthrow new MulticastInputError(\"invalid_version\", null, `version must be a non-negative integer; got ${input.version}`);\n\t}\n\tif (input.validUntil !== undefined) {\n\t\tif (!Number.isFinite(input.validUntil) || input.validUntil <= 0) {\n\t\t\tthrow new MulticastInputError(\n\t\t\t\t\"invalid_valid_until\",\n\t\t\t\tnull,\n\t\t\t\t`validUntil must be a positive unix-ms timestamp; got ${input.validUntil}`,\n\t\t\t);\n\t\t}\n\t}\n\n\tconst seenTuples = new Set<string>();\n\tconst endpoints: MulticastEndpoint[] = input.endpoints.map((ep, idx) => validateEndpoint(ep, idx, seenTuples));\n\n\tconst out: MulticastCatalogConfig = { endpoints };\n\tconst { entries: networkSource, relayFrom } = resolveNetworkSource(input);\n\t// Checked on the resolved array, so an explicit `input.networkSource` gets\n\t// exactly the same checks as one derived from the deprecated inputs.\n\tnetworkSource.forEach(validateNetworkSourceEntry);\n\tif (networkSource.length > 0) out.networkSource = networkSource;\n\n\t// A relay the emitted catalog would not carry fails closed, on both paths:\n\t// `networkSource` is catalog-level, so a per-endpoint relay that differs from\n\t// it (another endpoint's, `amtFallback.relayDns`, or an explicit entry) would\n\t// otherwise be dropped silently and that endpoint joins the wrong relay.\n\t//\n\t// A DRIAD entry does not count: both readers skip it, so its `relay` is a\n\t// discovery hint, not a relay the catalog carries. Without this the derived\n\t// path merges `amtFallback: {discovery: \"driad\"}` with an endpoint's\n\t// `relayAddress` into a driad entry holding that relay, the check passes, and\n\t// the join silently falls through to DRIAD -- where the explicit form of the\n\t// same input already fails closed here.\n\tconst amtRelays = new Set(\n\t\tnetworkSource\n\t\t\t.flatMap((s) => (s.type === \"amt\" && s.discovery !== \"driad\" && s.relay?.trim() ? [s.relay.trim()] : []))\n\t\t\t// Only the first, because it is the only one either reader can\n\t\t\t// deliver: `catalogAmtNetworkSource` returns on it and `findAmtRelay`\n\t\t\t// uses `.find`. Collecting every entry accepts an endpoint naming a\n\t\t\t// second manual entry, which then joins the first -- the same silent\n\t\t\t// wrong-relay join this guard exists to prevent, through the one\n\t\t\t// shape the DRIAD filter does not cover. It also keeps the `(...)`\n\t\t\t// listing below honest. Sliced rather than `.find`ed so the `amt`\n\t\t\t// narrowing stays inside the callback.\n\t\t\t.slice(0, 1),\n\t);\n\tinput.endpoints.forEach((ep, idx) => {\n\t\tconst relay = ep.relayAddress?.trim();\n\t\tif (!relay || amtRelays.has(relay)) return;\n\t\t// Two different causes reach this throw and they need different fixes.\n\t\t// If a DRIAD entry carries this exact relay, the catalog is not missing\n\t\t// it -- the entry is present and demoted, so reporting the set as\n\t\t// \"(none)\" sends the operator looking for an entry they already wrote.\n\t\t// Name the demotion and the two-entry shape that escapes it; both\n\t\t// readers take the first non-DRIAD AMT entry\n\t\t// (`catalog-network-source.ts` `catalogAmtNetworkSource`,\n\t\t// `multicast-catalog-policy.ts` `findAmtRelay`) and\n\t\t// `validateNetworkSourceEntry` has no duplicate-type rule, so a manual\n\t\t// entry beside the DRIAD one is joined while the DRIAD one is kept.\n\t\tconst demoted = networkSource.some(\n\t\t\t(s) => s.type === \"amt\" && s.discovery === \"driad\" && s.relay?.trim() === relay,\n\t\t);\n\t\t// Same \"written down but undeliverable\" family as `demoted`, different\n\t\t// reason and so a different fix: there the entry is skipped for its\n\t\t// discovery, here for its position. Reachable only on the explicit\n\t\t// path -- `resolveNetworkSource` emits at most one entry -- so there is\n\t\t// no provenance clause to add, and saying \"is not the relay of any AMT\n\t\t// entry\" here would be plainly false: the operator can see the entry.\n\t\t// `amtRelays` holds only the first joinable relay, so reaching this\n\t\t// throw at all already means this one is not it.\n\t\tconst shadowed = networkSource.some(\n\t\t\t(s) => s.type === \"amt\" && s.discovery !== \"driad\" && s.relay?.trim() === relay,\n\t\t);\n\t\t// The derived and env paths never wrote `networkSource` at all -- it was\n\t\t// synthesised below in `resolveNetworkSource` -- so naming only that key\n\t\t// would name the one field the operator did not set. This applies to\n\t\t// EVERY cause that reaches this throw, not just the demoted one: the\n\t\t// clause belongs in the shared prefix, or the arm that omits it\n\t\t// reproduces the misdiagnosis the other arm was written to fix.\n\t\t//\n\t\t// The two arms are about different values, so they name different\n\t\t// fields. The demoted arm is about `discovery`: a derived entry is only\n\t\t// `\"driad\"` when `amtFallback.discovery` said so (the default is\n\t\t// `\"manual\"`), so `amtFallback` is always the actionable field there.\n\t\t// The set clause is about the *relay*, which `resolveNetworkSource`\n\t\t// takes from `amtFallback.relayDns` or from the endpoint\n\t\t// `relayAddress` values -- and saying \"amtFallback\" to an operator who\n\t\t// never wrote one is the same defect one level down. Read that source\n\t\t// from `relayFrom` rather than re-inferring it: three review rounds\n\t\t// found a local re-inference here drifting from the rule above it.\n\t\tconst derived = input.networkSource === undefined;\n\t\tconst fromAmtFallback = \", derived from amtFallback/MOQCAST_MULTICAST_AMT_*\";\n\t\tconst fromRelay =\n\t\t\trelayFrom === \"amtFallback.relayDns\"\n\t\t\t\t? fromAmtFallback\n\t\t\t\t: relayFrom === \"endpoint.relayAddress\"\n\t\t\t\t\t? \", derived from the endpoint relayAddress values\"\n\t\t\t\t\t: \"\";\n\t\t// `resolveNetworkSource` lets `relayDns` outrank an endpoint relay, so on\n\t\t// the derived path the set can be non-empty and still not hold this\n\t\t// relay. Blaming per-endpoint representation there is wrong twice over:\n\t\t// there may be only one endpoint, and the field that actually won is\n\t\t// `relayDns`, which is the one the operator can act on. Asking\n\t\t// `amtRelays` instead would miss the case where the winning entry is\n\t\t// `discovery: \"driad\"` and so filtered out of that set entirely.\n\t\tconst relayDns = input.amtFallback?.relayDns?.trim();\n\t\tconst outranked = relayFrom === \"amtFallback.relayDns\" && relayDns !== relay;\n\t\tthrow new MulticastInputError(\n\t\t\t\"conflicting_amt_relay\",\n\t\t\tidx,\n\t\t\t`endpoint[${idx}] relayAddress ${JSON.stringify(relay)} ` +\n\t\t\t\t(demoted\n\t\t\t\t\t? `is carried only by a multicast.networkSource AMT entry with discovery=\"driad\"` +\n\t\t\t\t\t\t`${derived ? fromAmtFallback : \"\"}, ` +\n\t\t\t\t\t\t\"whose relay is a DRIAD discovery hint that both readers skip, so the catalog names \" +\n\t\t\t\t\t\t\"no relay to join; to join it, add a manual entry beside the driad one: \" +\n\t\t\t\t\t\t`[{type:\"amt\",discovery:\"manual\",relay:${JSON.stringify(relay)}},{type:\"amt\",discovery:\"driad\"}]`\n\t\t\t\t\t: shadowed\n\t\t\t\t\t\t? `is carried by a multicast.networkSource AMT entry that is not the first one: ` +\n\t\t\t\t\t\t\t`both readers join the first AMT entry (${[...amtRelays].join(\", \")}) and ignore ` +\n\t\t\t\t\t\t\t\"the rest, so this relay is never joined; keep a single AMT entry, or put this one first\"\n\t\t\t\t\t\t: `is not the relay of any multicast.networkSource AMT entry ` +\n\t\t\t\t\t\t\t`(${[...amtRelays].join(\", \") || \"none\"})${fromRelay}; ` +\n\t\t\t\t\t\t\t(outranked\n\t\t\t\t\t\t\t\t? `amtFallback.relayDns/MOQCAST_MULTICAST_AMT_RELAY_DNS (${relayDns}) outranks an ` +\n\t\t\t\t\t\t\t\t\t\"endpoint relay when networkSource is derived -- drop one, or make them agree\"\n\t\t\t\t\t\t\t\t: \"networkSource is catalog-level and cannot represent per-endpoint relays\")),\n\t\t);\n\t});\n\n\t// A bearer endpoint must name its bearer. The two bearer protocols are\n\t// exactly the two `networkSource` types, so an endpoint's own protocol names\n\t// the entry it requires. Native joins (\"ssm\", \"asm\") describe no bearer and\n\t// are exempt (BLO-37926) \u2014 the endpoint tuple is the whole join.\n\t// Whether an AMT entry needs a relay string (\"manual\" does, \"driad\" does\n\t// not) is per-entry, checked above.\n\tinput.endpoints.forEach((ep, idx) => {\n\t\tif (ep.protocol !== \"amt\" && ep.protocol !== \"atsc3\") return;\n\t\tif (networkSource.some((s) => s.type === ep.protocol)) return;\n\t\tthrow new MulticastInputError(\n\t\t\tep.protocol === \"amt\" ? \"amt_requires_relay\" : \"atsc3_requires_tuning\",\n\t\t\tidx,\n\t\t\t`endpoint[${idx}] protocol=${ep.protocol} requires a multicast.networkSource entry of ` +\n\t\t\t\t`type \"${ep.protocol}\"` +\n\t\t\t\t(ep.protocol === \"atsc3\"\n\t\t\t\t\t? \" carrying frequency, plpId, serviceId and slsUri \u2014 an ATSC3 endpoint is not tunable without them\"\n\t\t\t\t\t: \"\"),\n\t\t);\n\t});\n\n\tif (input.enabled !== undefined) out.enabled = input.enabled;\n\tif (input.version !== undefined) out.version = input.version;\n\tif (input.validUntil !== undefined) out.validUntil = input.validUntil;\n\treturn out;\n}\n\n/**\n * Build the catalog-level `multicast.networkSource` for this input.\n *\n * An explicit `input.networkSource` is authoritative and used as given.\n * Otherwise one AMT entry is derived from the deprecated inputs it replaces:\n * `amtFallback.discovery`/`relayDns` and any endpoint `relayAddress`;\n * `discovery` defaults to `\"manual\"` because a relay named in the catalog is,\n * by definition, not discovered. Endpoint relays that disagree with the result\n * are rejected by `multicastFromConfig`, not resolved here.\n *\n * `relayFrom` reports which deprecated input supplied the derived relay, so\n * that rejection can name a field the operator actually set instead of\n * re-deciding the precedence below at the throw site. It is `null` on the\n * explicit path and when no relay was derived at all.\n */\nfunction resolveNetworkSource(input: MulticastInput): {\n\tentries: NetworkSource[];\n\trelayFrom: \"amtFallback.relayDns\" | \"endpoint.relayAddress\" | null;\n} {\n\tif (input.networkSource !== undefined) {\n\t\treturn { entries: input.networkSource.map((s) => ({ ...s })), relayFrom: null };\n\t}\n\n\tconst relayDns = input.amtFallback?.relayDns?.trim();\n\tconst relay = relayDns || input.endpoints.map((ep) => ep.relayAddress?.trim()).find((r) => !!r);\n\tconst discovery = input.amtFallback?.discovery ?? (relay ? \"manual\" : undefined);\n\tif (discovery === undefined) return { entries: [], relayFrom: null };\n\treturn {\n\t\tentries: [{ type: \"amt\", discovery, ...(relay ? { relay } : {}) }],\n\t\trelayFrom: !relay ? null : relayDns ? \"amtFallback.relayDns\" : \"endpoint.relayAddress\",\n\t};\n}\n\nconst AMT_SOURCE_KEYS = new Set([\"type\", \"discovery\", \"relay\", \"port\"]);\nconst ATSC3_SOURCE_KEYS = new Set([\"type\", \"frequency\", \"plpId\", \"serviceId\", \"slsUri\"]);\n\n/**\n * One `networkSource[]` entry against `catalog.schema.json` `$defs/networkSource`\n * (and moq-rs `NetworkSource`), plus the one semantic rule the schema leaves\n * out: `discovery: \"manual\"` names its relay. These helpers are consumed\n * out-of-tree from plain JS, so the `NetworkSource` type is no guarantee.\n */\nfunction validateNetworkSourceEntry(s: NetworkSource, i: number): void {\n\tconst err = (kind: MulticastInputErrorKind, why: string) =>\n\t\tnew MulticastInputError(kind, null, `networkSource[${i}] ${why}`);\n\tconst keys = s?.type === \"amt\" ? AMT_SOURCE_KEYS : s?.type === \"atsc3\" ? ATSC3_SOURCE_KEYS : undefined;\n\tif (!keys) throw err(\"invalid_network_source\", `type must be \"amt\" or \"atsc3\"; got ${JSON.stringify(s?.type)}`);\n\t// Definedness, not presence: `isMulticastCatalogConfig` in `carp-catalog.ts`\n\t// already chose `=== undefined` over `in` for exactly this reason, and the\n\t// two must agree on what \"set\" means. A key spread in as `undefined`\n\t// (`{...entry, relayDns: undefined}`) is dropped by `JSON.stringify`, so the\n\t// emitted catalog is clean and rejecting it would fail a shape that ships\n\t// correctly.\n\tconst extra = Object.keys(s).filter((k) => !keys.has(k) && (s as Record<string, unknown>)[k] !== undefined);\n\tif (extra.length > 0) throw err(\"invalid_network_source\", `has keys the schema rejects: ${extra.join(\", \")}`);\n\n\tif (s.type === \"amt\") {\n\t\tif (s.discovery !== \"driad\" && s.discovery !== \"manual\") {\n\t\t\tthrow err(\"invalid_amt_discovery\", `discovery must be \"driad\" or \"manual\"; got ${JSON.stringify(s.discovery)}`);\n\t\t}\n\t\tif (s.relay !== undefined) {\n\t\t\t// Canonicalize, don't just check: the conflict guard in\n\t\t\t// `multicastFromConfig` compares `.trim()`ed values, so a padded\n\t\t\t// explicit relay would pass the guard and then ship padded. The\n\t\t\t// derived path (`resolveNetworkSource`) already trims, so trimming\n\t\t\t// here is what makes both paths emit the same canonical string.\n\t\t\t// Entries are copies by then, so this cannot mutate caller input.\n\t\t\tconst relay = typeof s.relay === \"string\" ? s.relay.trim() : \"\";\n\t\t\tif (!relay) {\n\t\t\t\tthrow err(\"invalid_network_source\", `relay must be non-blank when present; got ${JSON.stringify(s.relay)}`);\n\t\t\t}\n\t\t\ts.relay = relay;\n\t\t}\n\t\tif (s.discovery === \"manual\" && s.relay === undefined) {\n\t\t\tthrow err(\"amt_requires_relay\", `discovery=\"manual\" requires a relay (\"driad\" resolves it at runtime)`);\n\t\t}\n\t\tif (s.port !== undefined && (!Number.isInteger(s.port) || s.port < 1 || s.port > 65535)) {\n\t\t\tthrow err(\"invalid_port\", `port must be 1..65535; got ${s.port}`);\n\t\t}\n\t\treturn;\n\t}\n\tconst inRange = (v: number, min: number, max: number) => Number.isInteger(v) && v >= min && v <= max;\n\tif (!inRange(s.frequency, 1, Number.MAX_SAFE_INTEGER)) {\n\t\tthrow err(\"invalid_network_source\", `frequency must be a positive integer; got ${s.frequency}`);\n\t}\n\tif (!inRange(s.plpId, 0, 63)) throw err(\"invalid_network_source\", `plpId must be 0..63; got ${s.plpId}`);\n\tif (!inRange(s.serviceId, 0, 65535)) {\n\t\tthrow err(\"invalid_network_source\", `serviceId must be 0..65535; got ${s.serviceId}`);\n\t}\n\tif (typeof s.slsUri !== \"string\" || !isAbsoluteUri(s.slsUri)) {\n\t\tthrow err(\"invalid_network_source\", `slsUri must be an absolute URI; got ${JSON.stringify(s.slsUri)}`);\n\t}\n}\n\nfunction isAbsoluteUri(value: string): boolean {\n\ttry {\n\t\tnew URL(value);\n\t\treturn true;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\nfunction validateEndpoint(\n\tep: MulticastEndpointInput,\n\tidx: number,\n\tseenTuples: Set<string>,\n): MulticastEndpoint {\n\tif (ep.protocol === \"ssm\" && (!ep.sourceAddress || ep.sourceAddress.length === 0)) {\n\t\tthrow new MulticastInputError(\n\t\t\t\"ssm_requires_source\",\n\t\t\tidx,\n\t\t\t`endpoint[${idx}] protocol=ssm requires sourceAddress`,\n\t\t);\n\t}\n\t// NOTE: `protocol: \"amt\"` no longer requires a per-endpoint relay here. The\n\t// relay is catalog-level (`multicast.networkSource`), which this function\n\t// cannot see; `multicastFromConfig` enforces it once both halves are in hand.\n\n\tif (!isMulticastGroupAddress(ep.groupAddress)) {\n\t\tthrow new MulticastInputError(\n\t\t\t\"invalid_group_address\",\n\t\t\tidx,\n\t\t\t`endpoint[${idx}] groupAddress \"${ep.groupAddress}\" is not a valid multicast address`,\n\t\t);\n\t}\n\tif (ep.protocol === \"ssm\" && !isSsmGroupAddress(ep.groupAddress)) {\n\t\tthrow new MulticastInputError(\n\t\t\t\"ssm_group_out_of_range\",\n\t\t\tidx,\n\t\t\t`endpoint[${idx}] protocol=ssm requires groupAddress in 232.0.0.0/8 (IPv4) or ff3x::/32 (IPv6); got \"${ep.groupAddress}\"`,\n\t\t);\n\t}\n\n\tif (!Number.isInteger(ep.port) || ep.port < 1 || ep.port > 65535) {\n\t\tthrow new MulticastInputError(\"invalid_port\", idx, `endpoint[${idx}] port must be 1..65535; got ${ep.port}`);\n\t}\n\n\tif (ep.tracks.length === 0) {\n\t\tthrow new MulticastInputError(\"empty_tracks\", idx, `endpoint[${idx}] tracks must be non-empty`);\n\t}\n\n\tconst seenPids = new Set<number>();\n\tfor (const t of ep.tracks) {\n\t\tif (typeof t.name !== \"string\" || t.name.length === 0) {\n\t\t\tthrow new MulticastInputError(\n\t\t\t\t\"invalid_track_name\",\n\t\t\t\tidx,\n\t\t\t\t`endpoint[${idx}] track name must be a non-empty string`,\n\t\t\t);\n\t\t}\n\t\tif (t.packetId === MMTP_SIGNALING_PACKET_ID) {\n\t\t\tthrow new MulticastInputError(\n\t\t\t\t\"signaling_packet_id\",\n\t\t\t\tidx,\n\t\t\t\t`endpoint[${idx}] track \"${t.name}\" packetId ${t.packetId} is the MMTP signaling flow ` +\n\t\t\t\t\t`and must not be advertised for a track; packetId must be 1..${MMTP_PACKET_ID_MAX}`,\n\t\t\t);\n\t\t}\n\t\tif (!Number.isInteger(t.packetId) || t.packetId < 1 || t.packetId > MMTP_PACKET_ID_MAX) {\n\t\t\tthrow new MulticastInputError(\n\t\t\t\t\"invalid_packet_id\",\n\t\t\t\tidx,\n\t\t\t\t`endpoint[${idx}] track \"${t.name}\" packetId must be 1..${MMTP_PACKET_ID_MAX}; got ${t.packetId}`,\n\t\t\t);\n\t\t}\n\t\tif (seenPids.has(t.packetId)) {\n\t\t\tthrow new MulticastInputError(\n\t\t\t\t\"duplicate_packet_id\",\n\t\t\t\tidx,\n\t\t\t\t`endpoint[${idx}] duplicate packetId ${t.packetId}`,\n\t\t\t);\n\t\t}\n\t\tseenPids.add(t.packetId);\n\t}\n\n\tif (ep.bandwidth !== undefined && (!Number.isFinite(ep.bandwidth) || ep.bandwidth <= 0)) {\n\t\tthrow new MulticastInputError(\n\t\t\t\"invalid_bandwidth\",\n\t\t\tidx,\n\t\t\t`endpoint[${idx}] bandwidth must be positive; got ${ep.bandwidth}`,\n\t\t);\n\t}\n\n\tconst tupleKey = `${ep.sourceAddress ?? \"\"}|${ep.groupAddress}|${ep.port}`;\n\tif (seenTuples.has(tupleKey)) {\n\t\tthrow new MulticastInputError(\n\t\t\t\"duplicate_endpoint_tuple\",\n\t\t\tidx,\n\t\t\t`endpoint[${idx}] duplicates (sourceAddress, groupAddress, port) tuple \"${tupleKey}\"`,\n\t\t);\n\t}\n\tseenTuples.add(tupleKey);\n\n\tconst out: MulticastEndpoint = {\n\t\tprotocol: ep.protocol,\n\t\tgroupAddress: ep.groupAddress,\n\t\tport: ep.port,\n\t\ttracks: ep.tracks.map((t) => ({ name: t.name, packetId: t.packetId })),\n\t};\n\tif (ep.sourceAddress !== undefined) out.sourceAddress = ep.sourceAddress;\n\t// `relayAddress` is deliberately NOT copied: the relay is lifted to\n\t// catalog-level `multicast.networkSource` by `multicastFromConfig`.\n\tif (ep.bandwidth !== undefined) out.bandwidth = ep.bandwidth;\n\treturn out;\n}\n\nfunction isMulticastGroupAddress(addr: string): boolean {\n\tif (addr.includes(\":\")) {\n\t\t// IPv6: ff00::/8. Prefix-only check; full IPv6 syntax validation\n\t\t// is delegated to the OS bind() call downstream.\n\t\treturn /^ff[0-9a-f]{2}:/i.test(addr);\n\t}\n\tconst m = /^(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})$/.exec(addr);\n\tif (!m) return false;\n\tfor (let i = 1; i <= 4; i++) {\n\t\tconst v = Number(m[i]);\n\t\tif (!Number.isInteger(v) || v < 0 || v > 255) return false;\n\t}\n\tconst first = Number(m[1]);\n\treturn first >= 224 && first <= 239;\n}\n\nfunction isSsmGroupAddress(addr: string): boolean {\n\tif (addr.includes(\":\")) {\n\t\t// SSM IPv6: ff3x::/32 per RFC 3306 \u00A74. Requires P=1 + T=1 (the `3`\n\t\t// nibble) AND the second 16-bit hextet to be all-zero (RFC 3306's\n\t\t// \"prefix length = 0\" \u2192 next 32 bits are 0). The regex matches\n\t\t// `ff3?:0000:` collapsed forms (`ff3e::`, `ff3e:0::`, `ff3e:0:`,\n\t\t// `ff3e:0000:`) and rejects `ff3e:beef::1` etc.\n\t\treturn /^ff3[0-9a-f]:0{0,4}:/i.test(addr);\n\t}\n\tconst m = /^(\\d{1,3})\\./.exec(addr);\n\treturn m !== null && Number(m[1]) === 232;\n}\n\n// Env-var helper (BLO-3337): read MOQCAST_MULTICAST_* keys and route\n// through multicastFromConfig so all per-endpoint invariants apply.\n\n/**\n * Read `MOQCAST_MULTICAST_*` env vars and return a\n * `MulticastCatalogConfig`. Returns `undefined` when no per-endpoint\n * key is set.\n *\n * Env contract \u2014 per-endpoint keys take an optional non-negative index:\n *\n * MOQCAST_MULTICAST[_<idx>]_PROTOCOL=ssm\n * MOQCAST_MULTICAST[_<idx>]_GROUP=232.1.1.1\n * MOQCAST_MULTICAST[_<idx>]_PORT=5004\n * MOQCAST_MULTICAST[_<idx>]_SOURCE=10.0.0.20 (required for ssm)\n * MOQCAST_MULTICAST[_<idx>]_RELAY=amt.example.com (required for amt)\n * MOQCAST_MULTICAST[_<idx>]_TRACKS=video:1,audio:3\n * MOQCAST_MULTICAST[_<idx>]_BANDWIDTH=8000000 (optional, bps)\n *\n * MOQCAST_MULTICAST_AMT_DISCOVERY=driad (catalog-level)\n * MOQCAST_MULTICAST_AMT_RELAY_DNS=relay.example.com (catalog-level)\n *\n * The unindexed form (`MOQCAST_MULTICAST_GROUP`) is exactly index 0;\n * mixing it with explicit `_0_` keys is rejected as ambiguous. Indices\n * need not be contiguous; observed indices are sorted ascending.\n */\nexport function multicastFromEnv(\n\tenv: Record<string, string | undefined> = ((globalThis as { process?: { env?: Record<string, string | undefined> } })\n\t\t.process?.env ?? {}) as Record<string, string | undefined>,\n): MulticastCatalogConfig | undefined {\n\tconst indices = collectMulticastIndices(env);\n\t// An empty value is unset: `FOO=${BAR}` with BAR unset, or an empty\n\t// configMap value, is a templating outcome, not a setting. Normalising\n\t// here gives every test below one notion of \"set\". Stricter than SOURCE\n\t// and RELAY, which gate on truthiness alone and so keep a whitespace-only\n\t// value \u2014 deliberately, as these two decide whether AMT is declared at all.\n\tconst amtDiscovery = env[\"MOQCAST_MULTICAST_AMT_DISCOVERY\"]?.trim() || undefined;\n\tconst amtRelayDns = env[\"MOQCAST_MULTICAST_AMT_RELAY_DNS\"]?.trim() || undefined;\n\t// One predicate, both guards: a key significant enough to reject at zero\n\t// endpoints is not silently discarded once endpoints are present.\n\tconst hasCatalogAmt = amtDiscovery !== undefined || amtRelayDns !== undefined;\n\tif (indices.length === 0) {\n\t\t// Catalog-level AMT keys without per-endpoint keys is a\n\t\t// misconfiguration, not a no-op: `MOQCAST_MULTICAST_AMT_DISCOVERY`\n\t\t// only makes sense alongside endpoints to fall back FOR. Throw\n\t\t// loudly instead of silently dropping the request \u2014 a publisher\n\t\t// that meant to declare AMT fallback shouldn't have its config\n\t\t// disappear without any signal.\n\t\tif (hasCatalogAmt) {\n\t\t\tthrow new MulticastInputError(\n\t\t\t\t\"invalid_env_shape\",\n\t\t\t\tnull,\n\t\t\t\t\"MOQCAST_MULTICAST_AMT_* env keys are set but no per-endpoint keys \" +\n\t\t\t\t\t\"(MOQCAST_MULTICAST_GROUP / _PROTOCOL / _PORT / _TRACKS) were found. \" +\n\t\t\t\t\t\"AMT discovery is a catalog-level fallback for endpoints; declaring it \" +\n\t\t\t\t\t\"with zero endpoints is a misconfiguration.\",\n\t\t\t);\n\t\t}\n\t\treturn undefined;\n\t}\n\n\tconst endpoints: MulticastEndpointInput[] = indices.map((idx) => readEndpointAt(env, idx));\n\n\tconst input: MulticastInput = {\n\t\tendpoints,\n\t\t...(hasCatalogAmt\n\t\t\t? {\n\t\t\t\t\tamtFallback: {\n\t\t\t\t\t\t// `\"manual\"` mirrors `resolveNetworkSource`: a relay named in\n\t\t\t\t\t\t// the catalog is, by definition, not discovered.\n\t\t\t\t\t\tdiscovery: (amtDiscovery ?? \"manual\") as AmtDiscovery,\n\t\t\t\t\t\t...(amtRelayDns ? { relayDns: amtRelayDns } : {}),\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t: {}),\n\t};\n\treturn multicastFromConfig(input);\n}\n\nconst ENV_PER_FIELD = [\"PROTOCOL\", \"GROUP\", \"PORT\", \"SOURCE\", \"RELAY\", \"TRACKS\", \"BANDWIDTH\"];\nconst ENV_CATALOG_LEVEL = new Set([\"AMT_DISCOVERY\", \"AMT_RELAY_DNS\"]);\n\nfunction collectMulticastIndices(env: Record<string, string | undefined>): number[] {\n\tconst indices = new Set<number>();\n\tlet hasUnindexed = false;\n\tlet hasZeroIndexed = false;\n\n\tfor (const [key, val] of Object.entries(env)) {\n\t\tif (val === undefined) continue;\n\t\tif (!key.startsWith(\"MOQCAST_MULTICAST_\")) continue;\n\t\tconst suffix = key.slice(\"MOQCAST_MULTICAST_\".length);\n\t\tif (ENV_CATALOG_LEVEL.has(suffix)) continue;\n\n\t\tconst m = /^(\\d+)_(.+)$/.exec(suffix);\n\t\tif (m) {\n\t\t\tconst idx = Number(m[1]);\n\t\t\tconst field = m[2];\n\t\t\tif (!ENV_PER_FIELD.includes(field)) continue;\n\t\t\tif (idx === 0) hasZeroIndexed = true;\n\t\t\tindices.add(idx);\n\t\t} else if (ENV_PER_FIELD.includes(suffix)) {\n\t\t\thasUnindexed = true;\n\t\t\tindices.add(0);\n\t\t}\n\t}\n\n\tif (hasUnindexed && hasZeroIndexed) {\n\t\tthrow new MulticastInputError(\n\t\t\t\"invalid_env_shape\",\n\t\t\tnull,\n\t\t\t\"MOQCAST_MULTICAST_* env contract: cannot mix unindexed keys (MOQCAST_MULTICAST_GROUP) \" +\n\t\t\t\t\"with explicitly-indexed keys at index 0 (MOQCAST_MULTICAST_0_GROUP) \u2014 they refer to \" +\n\t\t\t\t\"the same endpoint. Use one form consistently.\",\n\t\t);\n\t}\n\n\treturn [...indices].sort((a, b) => a - b);\n}\n\nfunction readEndpointAt(env: Record<string, string | undefined>, idx: number): MulticastEndpointInput {\n\tconst lookup = (field: string): string | undefined => {\n\t\tif (idx === 0) {\n\t\t\treturn env[`MOQCAST_MULTICAST_${field}`] ?? env[`MOQCAST_MULTICAST_0_${field}`];\n\t\t}\n\t\treturn env[`MOQCAST_MULTICAST_${idx}_${field}`];\n\t};\n\n\tconst portStr = lookup(\"PORT\");\n\tconst port = portStr !== undefined ? Number(portStr) : NaN;\n\tconst tracks = parseTracksEnv(lookup(\"TRACKS\"), idx);\n\tconst bandwidthStr = lookup(\"BANDWIDTH\");\n\tconst bandwidth = bandwidthStr !== undefined ? Number(bandwidthStr) : undefined;\n\tconst source = lookup(\"SOURCE\");\n\tconst relay = lookup(\"RELAY\");\n\n\treturn {\n\t\tprotocol: lookup(\"PROTOCOL\") as MulticastProtocol,\n\t\tgroupAddress: lookup(\"GROUP\") ?? \"\",\n\t\tport,\n\t\ttracks,\n\t\t...(source ? { sourceAddress: source } : {}),\n\t\t...(relay ? { relayAddress: relay } : {}),\n\t\t...(bandwidth !== undefined ? { bandwidth } : {}),\n\t};\n}\n\nfunction parseTracksEnv(raw: string | undefined, endpointIdx: number): MulticastTrackObject[] {\n\tif (!raw || raw.length === 0) return [];\n\treturn raw.split(\",\").map((entry, i) => {\n\t\tconst colon = entry.lastIndexOf(\":\");\n\t\tif (colon < 0) {\n\t\t\tthrow new MulticastInputError(\n\t\t\t\t\"invalid_track_name\",\n\t\t\t\tendpointIdx,\n\t\t\t\t`endpoint[${endpointIdx}] TRACKS[${i}] missing \":\" \u2014 expected \"name:packetId\", got ${JSON.stringify(entry)}`,\n\t\t\t);\n\t\t}\n\t\tconst name = entry.slice(0, colon).trim();\n\t\tconst idStr = entry.slice(colon + 1).trim();\n\t\tconst packetId = Number(idStr);\n\t\tif (!name || idStr.length === 0 || !Number.isFinite(packetId) || !Number.isInteger(packetId)) {\n\t\t\tthrow new MulticastInputError(\n\t\t\t\t\"invalid_packet_id\",\n\t\t\t\tendpointIdx,\n\t\t\t\t`endpoint[${endpointIdx}] TRACKS[${i}] entry malformed (need \"name:integer\"): ${JSON.stringify(entry)}`,\n\t\t\t);\n\t\t}\n\t\treturn { name, packetId };\n\t});\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
7
  }
@@ -18,17 +18,16 @@ import type { MfuReassemblerBackend } from "./mfu-reassembler.js";
18
18
  import type { MfuReassemblyPolicy } from "./mfu-reassembly-policy.js";
19
19
  import type { ManifestObjectIdentity, ManifestVerifier } from "@blockcast/mmt-manifest-verify";
20
20
  import type { BlockAssemblerConfig, CmafBlockSample } from "./cmaf-block-assembler.js";
21
+ import { type TrackFecUnavailable } from "./catalog-fec-params.js";
21
22
  export interface MulticastEndpointConfig {
22
23
  sourceAddress: string;
23
24
  groupAddress: string;
24
25
  port: number;
25
26
  /** Explicit gateway transport policy. Omitted means gateway-selected auto mode. */
26
27
  mode?: 'auto' | 'native' | 'tunnel';
27
- /** AMT relay hostname/IP from catalog endpoint. */
28
- relayAddress?: string;
29
28
  /** AMT relay port from catalog endpoint. */
30
29
  relayPort?: number;
31
- /** AMT relay hostname/IP from player/UI overrides. Wins over `relayAddress`. */
30
+ /** AMT relay hostname/IP: the player/UI override, or the catalog's `multicast.networkSource` AMT relay. */
32
31
  amtRelay?: string;
33
32
  /** AMT relay port from player/UI overrides. Wins over `relayPort`. */
34
33
  amtRelayPort?: number;
@@ -37,8 +36,9 @@ export interface MulticastEndpointConfig {
37
36
  /** Exact catalog tracks carried by this physical multicast endpoint. */
38
37
  tracks?: MmtpIngressContext["tracks"];
39
38
  /**
40
- * The catalog `fec.blockProfile` (BLO-35597): `k-padded-v1` or
41
- * `iso-ssbg1-v1`. Required for per-track FEC configuration; never defaulted.
39
+ * The catalog `fec.blockProfile` (BLO-35597): `iso-ssbg1-v1`, the only
40
+ * profile (the retired `k-padded-v1` token is refused). Required for
41
+ * per-track FEC configuration; never defaulted.
42
42
  */
43
43
  fecBlockProfile?: FecBlockProfile;
44
44
  fecK?: number;
@@ -510,13 +510,14 @@ export declare class MulticastManager {
510
510
  onAudioFrame(cb: FrameCallback): void;
511
511
  onTrackFrame(packetId: number, cb: FrameCallback): void;
512
512
  onTrackPacket(packetId: number, cb: TrackPacketCallback): void;
513
+ /** @deprecated Use onTrackInit(packetId, cb); see MmtpRouter.onInitSegment. */
513
514
  onInitSegment(cb: (data: Uint8Array) => void): void;
514
515
  onTrackInit(packetId: number, cb: (data: Uint8Array) => void): void;
515
516
  onFecBlockStarted(cb: (sbn: number, packetId: number) => void): void;
516
517
  /** Unrecoverable-FEC-block signal. Players drive decoder recovery from this
517
518
  * (a starved decoder cannot resync on its own once the block is abandoned). */
518
519
  onFecBlockFailed(cb: (sbn: number, packetId: number, catalogTrackName?: string) => void): void;
519
- enableMseAssembler(packetId: number, config: Omit<BlockAssemblerConfig, "k">, catalogTrackName?: string): import("./cmaf-block-assembler.js").CmafBlockAssembler;
520
+ enableMseAssembler(packetId: number, config: BlockAssemblerConfig, catalogTrackName?: string): import("./cmaf-block-assembler.js").CmafBlockAssembler;
520
521
  onMseSegment(packetId: number, cb: (segment: Uint8Array) => void, catalogTrackName?: string): void;
521
522
  onMseSample(packetId: number, cb: (sample: CmafBlockSample) => void, catalogTrackName?: string): void;
522
523
  onMseInitSegment(packetId: number, cb: (init: Uint8Array) => void, catalogTrackName?: string): void;
@@ -586,11 +587,38 @@ export declare class MulticastManager {
586
587
  * D-17: Builds altGroup → track-name index for getSwitchingSet().
587
588
  * D-26: Registers packetId → TrackRole on MmtpRouter for block-complete fanout.
588
589
  *
590
+ * FEC availability is per track (draft-ramadan-moq-fec §5.1). A media track
591
+ * whose `fec` names no implemented `blockProfile` (only `iso-ssbg1-v1` is
592
+ * implemented; the retired `k-padded-v1` is not), or an `iso-ssbg1-v1` span
593
+ * it cannot use, is consumed without FEC: it gets no decoder, and none of
594
+ * its repair tracks (layer 0 by `fec.repairTrack`, further layers and the
595
+ * overlay by `depends`) is given an owner, so the router drops their packets
596
+ * before any decoder. Those legs keep the `repair` role and their owner's
597
+ * switching set. Neither the call nor any other track's FEC fails on that
598
+ * account; `trackFecUnavailable(name)` reports the reason. FEC a previous
599
+ * call configured on a packetId this call does not is released
600
+ * (FECManager.releasePacket); frames its release gate held are delivered in
601
+ * order, each gap stalled. FEC wired by hand (setFecDecoder) is left alone.
602
+ * Geometry a decoder could not configure (K, P, T, `repairTrack`,
603
+ * algorithm) still throws.
604
+ *
589
605
  * @param catalog - Multi-track catalog (tracks array with name, altGroup, fec, packetId).
590
606
  * @param wasmDecoderSource - One WASM FEC decoder for a single FEC stream, or
591
607
  * a factory returning a distinct decoder per stream.
592
608
  */
593
609
  configureMultiTrack(catalog: MultiTrackCatalog, wasmDecoderSource?: WasmFecDecoder | WasmFecDecoderFactory): void;
610
+ /**
611
+ * Why `configureMultiTrack` consumes the named catalog track without FEC
612
+ * although its `fec` block declares one (draft-ramadan-moq-fec §5.1): no
613
+ * implemented `blockProfile` (`track_fec_block_profile_unimplemented`, e.g.
614
+ * the retired `k-padded-v1`; `track_fec_block_profile` when absent), or an
615
+ * invalid `iso-ssbg1-v1` span (`track_fec`). Absent/zero spans keep in-band
616
+ * decoding enabled. The same reason the hang
617
+ * resolver reports (`catalogFecUnavailable`). Undefined for every other
618
+ * track, and for every track before the first call; each call replaces the
619
+ * previous call's answers.
620
+ */
621
+ trackFecUnavailable(trackName: string): TrackFecUnavailable | undefined;
594
622
  /**
595
623
  * Return the track names belonging to the given altGroup switching set.
596
624
  * D-17: altGroup groups base+delta+repair into one switching set.