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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (678) hide show
  1. package/README.md +432 -0
  2. package/dist/.build-stamp +0 -0
  3. package/dist/__tests__/test-utils.d.ts +71 -0
  4. package/dist/__tests__/test-utils.d.ts.map +1 -0
  5. package/dist/align.d.ts +148 -0
  6. package/dist/align.d.ts.map +1 -0
  7. package/dist/align.js +88 -0
  8. package/dist/align.js.map +7 -0
  9. package/dist/audio-init-parser.d.ts +66 -0
  10. package/dist/audio-init-parser.d.ts.map +1 -0
  11. package/dist/audio-init-parser.js +118 -0
  12. package/dist/audio-init-parser.js.map +7 -0
  13. package/dist/audio-ring-buffer.d.ts +50 -0
  14. package/dist/audio-ring-buffer.d.ts.map +1 -0
  15. package/dist/audio-ring-buffer.js +93 -0
  16. package/dist/audio-ring-buffer.js.map +7 -0
  17. package/dist/authored-timing.d.ts +32 -0
  18. package/dist/authored-timing.d.ts.map +1 -0
  19. package/dist/authored-timing.js +32 -0
  20. package/dist/authored-timing.js.map +7 -0
  21. package/dist/browser-runtime.js +17109 -0
  22. package/dist/browser-runtime.js.map +7 -0
  23. package/dist/carp-catalog.d.ts +318 -0
  24. package/dist/carp-catalog.d.ts.map +1 -0
  25. package/dist/carp-catalog.js +549 -0
  26. package/dist/carp-catalog.js.map +7 -0
  27. package/dist/catalog-fec-params.d.ts +82 -0
  28. package/dist/catalog-fec-params.d.ts.map +1 -0
  29. package/dist/catalog-fec-params.js +66 -0
  30. package/dist/catalog-fec-params.js.map +7 -0
  31. package/dist/catalog-network-source.d.ts +19 -0
  32. package/dist/catalog-network-source.d.ts.map +1 -0
  33. package/dist/catalog-network-source.js +22 -0
  34. package/dist/catalog-network-source.js.map +7 -0
  35. package/dist/catalog-validator.d.ts +18 -0
  36. package/dist/catalog-validator.d.ts.map +1 -0
  37. package/dist/catalog-validator.js +7747 -0
  38. package/dist/catalog-validator.js.map +7 -0
  39. package/dist/cmaf-block-assembler.d.ts +168 -0
  40. package/dist/cmaf-block-assembler.d.ts.map +1 -0
  41. package/dist/cmaf-block-assembler.js +1755 -0
  42. package/dist/cmaf-block-assembler.js.map +7 -0
  43. package/dist/cmaf-group-batcher.d.ts +38 -0
  44. package/dist/cmaf-group-batcher.d.ts.map +1 -0
  45. package/dist/cmaf-group-batcher.js +195 -0
  46. package/dist/cmaf-group-batcher.js.map +7 -0
  47. package/dist/cmcd-producer.d.ts +104 -0
  48. package/dist/cmcd-producer.d.ts.map +1 -0
  49. package/dist/cmcd-producer.js +207 -0
  50. package/dist/cmcd-producer.js.map +7 -0
  51. package/dist/codec-utils.d.ts +235 -0
  52. package/dist/codec-utils.d.ts.map +1 -0
  53. package/dist/codec-utils.js +1225 -0
  54. package/dist/codec-utils.js.map +7 -0
  55. package/dist/fec-codec.d.ts +32 -0
  56. package/dist/fec-codec.d.ts.map +1 -0
  57. package/dist/fec-codec.js +47 -0
  58. package/dist/fec-codec.js.map +7 -0
  59. package/dist/fec-manager.d.ts +266 -0
  60. package/dist/fec-manager.d.ts.map +1 -0
  61. package/dist/fec-manager.js +1447 -0
  62. package/dist/fec-manager.js.map +7 -0
  63. package/dist/fec-starvation-detector.d.ts +59 -0
  64. package/dist/fec-starvation-detector.d.ts.map +1 -0
  65. package/dist/fec-starvation-detector.js +75 -0
  66. package/dist/fec-starvation-detector.js.map +7 -0
  67. package/dist/fec-worker-setup.d.ts +76 -0
  68. package/dist/fec-worker-setup.d.ts.map +1 -0
  69. package/dist/fec-worker-setup.js +603 -0
  70. package/dist/fec-worker-setup.js.map +7 -0
  71. package/dist/fmp4-completion.d.ts +30 -0
  72. package/dist/fmp4-completion.d.ts.map +1 -0
  73. package/dist/fmp4-completion.js +21 -0
  74. package/dist/fmp4-completion.js.map +7 -0
  75. package/dist/fmp4-reader.d.ts +37 -0
  76. package/dist/fmp4-reader.d.ts.map +1 -0
  77. package/dist/fmp4-reader.js +100 -0
  78. package/dist/fmp4-reader.js.map +7 -0
  79. package/dist/index.d.ts +47 -0
  80. package/dist/index.d.ts.map +1 -0
  81. package/dist/index.js +330 -0
  82. package/dist/index.js.map +7 -0
  83. package/dist/iso-bmff-timing.d.ts +47 -0
  84. package/dist/iso-bmff-timing.d.ts.map +1 -0
  85. package/dist/iso-bmff-timing.js +526 -0
  86. package/dist/iso-bmff-timing.js.map +7 -0
  87. package/dist/loc.d.ts +68 -0
  88. package/dist/loc.d.ts.map +1 -0
  89. package/dist/loc.js +143 -0
  90. package/dist/loc.js.map +7 -0
  91. package/dist/manifest-auth-plan.d.ts +30 -0
  92. package/dist/manifest-auth-plan.d.ts.map +1 -0
  93. package/dist/manifest-auth-plan.js +1267 -0
  94. package/dist/manifest-auth-plan.js.map +7 -0
  95. package/dist/media-router.d.ts +338 -0
  96. package/dist/media-router.d.ts.map +1 -0
  97. package/dist/media-router.js +802 -0
  98. package/dist/media-router.js.map +7 -0
  99. package/dist/mfu-reassembler.d.ts +264 -0
  100. package/dist/mfu-reassembler.d.ts.map +1 -0
  101. package/dist/mfu-reassembler.js +2668 -0
  102. package/dist/mfu-reassembler.js.map +7 -0
  103. package/dist/mfu-reassembly-policy.d.ts +24 -0
  104. package/dist/mfu-reassembly-policy.d.ts.map +1 -0
  105. package/dist/mfu-reassembly-policy.js +99 -0
  106. package/dist/mfu-reassembly-policy.js.map +7 -0
  107. package/dist/mmtp-container-parser.d.ts +304 -0
  108. package/dist/mmtp-container-parser.d.ts.map +1 -0
  109. package/dist/mmtp-container-parser.js +5378 -0
  110. package/dist/mmtp-container-parser.js.map +7 -0
  111. package/dist/mmtp-container-parser.no-modules.js +7853 -0
  112. package/dist/mmtp-container-parser.no-modules.js.map +7 -0
  113. package/dist/mmtp-decode-pipeline.d.ts +96 -0
  114. package/dist/mmtp-decode-pipeline.d.ts.map +1 -0
  115. package/dist/mmtp-decode-pipeline.js +5589 -0
  116. package/dist/mmtp-decode-pipeline.js.map +7 -0
  117. package/dist/mmtp-router.d.ts +383 -0
  118. package/dist/mmtp-router.d.ts.map +1 -0
  119. package/dist/mmtp-router.js +8384 -0
  120. package/dist/mmtp-router.js.map +7 -0
  121. package/dist/mmtp-to-cmaf.d.ts +166 -0
  122. package/dist/mmtp-to-cmaf.d.ts.map +1 -0
  123. package/dist/mmtp-to-cmaf.js +781 -0
  124. package/dist/mmtp-to-cmaf.js.map +7 -0
  125. package/dist/multicast-auth-catalog.d.ts +115 -0
  126. package/dist/multicast-auth-catalog.d.ts.map +1 -0
  127. package/dist/multicast-auth-catalog.js +202 -0
  128. package/dist/multicast-auth-catalog.js.map +7 -0
  129. package/dist/multicast-input.d.ts +107 -0
  130. package/dist/multicast-input.d.ts.map +1 -0
  131. package/dist/multicast-input.js +256 -0
  132. package/dist/multicast-input.js.map +7 -0
  133. package/dist/multicast-manager.d.ts +330 -0
  134. package/dist/multicast-manager.d.ts.map +1 -0
  135. package/dist/multicast-manager.js +11032 -0
  136. package/dist/multicast-manager.js.map +7 -0
  137. package/dist/multicast-transport.d.ts +62 -0
  138. package/dist/multicast-transport.d.ts.map +1 -0
  139. package/dist/multicast-transport.js +338 -0
  140. package/dist/multicast-transport.js.map +7 -0
  141. package/dist/multicast-unwrap.d.ts +57 -0
  142. package/dist/multicast-unwrap.d.ts.map +1 -0
  143. package/dist/multicast-unwrap.js +384 -0
  144. package/dist/multicast-unwrap.js.map +7 -0
  145. package/dist/no-modules.d.ts +9 -0
  146. package/dist/no-modules.d.ts.map +1 -0
  147. package/dist/no-modules.js +7819 -0
  148. package/dist/no-modules.js.map +7 -0
  149. package/dist/phase14-catalog-override.d.ts +126 -0
  150. package/dist/phase14-catalog-override.d.ts.map +1 -0
  151. package/dist/phase14-catalog-override.js +155 -0
  152. package/dist/phase14-catalog-override.js.map +7 -0
  153. package/dist/repair-fetch.d.ts +58 -0
  154. package/dist/repair-fetch.d.ts.map +1 -0
  155. package/dist/repair-fetch.js +117 -0
  156. package/dist/repair-fetch.js.map +7 -0
  157. package/dist/sample-combiner.d.ts +182 -0
  158. package/dist/sample-combiner.d.ts.map +1 -0
  159. package/dist/sample-combiner.js +571 -0
  160. package/dist/sample-combiner.js.map +7 -0
  161. package/dist/signaling.d.ts +29 -0
  162. package/dist/signaling.d.ts.map +1 -0
  163. package/dist/signaling.js +232 -0
  164. package/dist/signaling.js.map +7 -0
  165. package/dist/source-auth.d.ts +2 -0
  166. package/dist/source-auth.d.ts.map +1 -0
  167. package/dist/source-auth.js +1220 -0
  168. package/dist/source-auth.js.map +7 -0
  169. package/dist/switching-set-alignment.d.ts +61 -0
  170. package/dist/switching-set-alignment.d.ts.map +1 -0
  171. package/dist/switching-set-alignment.js +85 -0
  172. package/dist/switching-set-alignment.js.map +7 -0
  173. package/dist/timestamp-codec.d.ts +34 -0
  174. package/dist/timestamp-codec.d.ts.map +1 -0
  175. package/dist/timestamp-codec.js +456 -0
  176. package/dist/timestamp-codec.js.map +7 -0
  177. package/dist/track-pipeline-setup.d.ts +72 -0
  178. package/dist/track-pipeline-setup.d.ts.map +1 -0
  179. package/dist/track-pipeline-setup.js +306 -0
  180. package/dist/track-pipeline-setup.js.map +7 -0
  181. package/dist/track-pipeline.d.ts +121 -0
  182. package/dist/track-pipeline.d.ts.map +1 -0
  183. package/dist/track-pipeline.js +229 -0
  184. package/dist/track-pipeline.js.map +7 -0
  185. package/dist/track-session.d.ts +269 -0
  186. package/dist/track-session.d.ts.map +1 -0
  187. package/dist/track-session.js +597 -0
  188. package/dist/track-session.js.map +7 -0
  189. package/dist/transport-pool.d.ts +69 -0
  190. package/dist/transport-pool.d.ts.map +1 -0
  191. package/dist/transport-pool.js +88 -0
  192. package/dist/transport-pool.js.map +7 -0
  193. package/dist/wasm-mfu-reassembler.d.ts +81 -0
  194. package/dist/wasm-mfu-reassembler.d.ts.map +1 -0
  195. package/dist/wasm-mfu-reassembler.js +1915 -0
  196. package/dist/wasm-mfu-reassembler.js.map +7 -0
  197. package/dist/wasm-mmtp-container-parser.d.ts +10 -0
  198. package/dist/wasm-mmtp-container-parser.d.ts.map +1 -0
  199. package/dist/wasm-mmtp-container-parser.js +5635 -0
  200. package/dist/wasm-mmtp-container-parser.js.map +7 -0
  201. package/node_modules/@blockcast/fec-worker/README.md +213 -0
  202. package/node_modules/@blockcast/fec-worker/dist/.build-stamp +0 -0
  203. package/node_modules/@blockcast/fec-worker/dist/fec-worker-client.d.ts +80 -0
  204. package/node_modules/@blockcast/fec-worker/dist/fec-worker-client.d.ts.map +1 -0
  205. package/node_modules/@blockcast/fec-worker/dist/fec-worker-client.js +270 -0
  206. package/node_modules/@blockcast/fec-worker/dist/fec-worker-client.js.map +1 -0
  207. package/node_modules/@blockcast/fec-worker/dist/fec-worker-types.d.ts +252 -0
  208. package/node_modules/@blockcast/fec-worker/dist/fec-worker-types.d.ts.map +1 -0
  209. package/node_modules/@blockcast/fec-worker/dist/fec-worker-types.js +11 -0
  210. package/node_modules/@blockcast/fec-worker/dist/fec-worker-types.js.map +1 -0
  211. package/node_modules/@blockcast/fec-worker/dist/fec-worker.d.ts +14 -0
  212. package/node_modules/@blockcast/fec-worker/dist/fec-worker.d.ts.map +1 -0
  213. package/node_modules/@blockcast/fec-worker/dist/fec-worker.js +565 -0
  214. package/node_modules/@blockcast/fec-worker/dist/fec-worker.js.map +7 -0
  215. package/node_modules/@blockcast/fec-worker/dist/index.d.ts +10 -0
  216. package/node_modules/@blockcast/fec-worker/dist/index.d.ts.map +1 -0
  217. package/node_modules/@blockcast/fec-worker/dist/index.js +8 -0
  218. package/node_modules/@blockcast/fec-worker/dist/index.js.map +1 -0
  219. package/node_modules/@blockcast/fec-worker/dist/shred-fec-worker.d.ts +20 -0
  220. package/node_modules/@blockcast/fec-worker/dist/shred-fec-worker.d.ts.map +1 -0
  221. package/node_modules/@blockcast/fec-worker/dist/shred-fec-worker.js +349 -0
  222. package/node_modules/@blockcast/fec-worker/dist/shred-fec-worker.js.map +7 -0
  223. package/node_modules/@blockcast/fec-worker/dist/shred-worker-client.d.ts +104 -0
  224. package/node_modules/@blockcast/fec-worker/dist/shred-worker-client.d.ts.map +1 -0
  225. package/node_modules/@blockcast/fec-worker/dist/shred-worker-client.js +181 -0
  226. package/node_modules/@blockcast/fec-worker/dist/shred-worker-client.js.map +1 -0
  227. package/node_modules/@blockcast/fec-worker/dist/shred-worker-types.d.ts +179 -0
  228. package/node_modules/@blockcast/fec-worker/dist/shred-worker-types.d.ts.map +1 -0
  229. package/node_modules/@blockcast/fec-worker/dist/shred-worker-types.js +47 -0
  230. package/node_modules/@blockcast/fec-worker/dist/shred-worker-types.js.map +1 -0
  231. package/node_modules/@blockcast/fec-worker/dist/transfer.d.ts +9 -0
  232. package/node_modules/@blockcast/fec-worker/dist/transfer.d.ts.map +1 -0
  233. package/node_modules/@blockcast/fec-worker/dist/transfer.js +13 -0
  234. package/node_modules/@blockcast/fec-worker/dist/transfer.js.map +1 -0
  235. package/node_modules/@blockcast/fec-worker/package.json +59 -0
  236. package/node_modules/@blockcast/fec-worker/src/fec-worker-client.ts +309 -0
  237. package/node_modules/@blockcast/fec-worker/src/fec-worker-types.ts +268 -0
  238. package/node_modules/@blockcast/fec-worker/src/fec-worker.ts +1048 -0
  239. package/node_modules/@blockcast/fec-worker/src/index.ts +32 -0
  240. package/node_modules/@blockcast/fec-worker/src/shred-fec-worker.ts +558 -0
  241. package/node_modules/@blockcast/fec-worker/src/shred-worker-client.ts +222 -0
  242. package/node_modules/@blockcast/fec-worker/src/shred-worker-types.ts +194 -0
  243. package/node_modules/@blockcast/fec-worker/src/transfer.ts +12 -0
  244. package/node_modules/@blockcast/mmt-alta-parse/README.md +181 -0
  245. package/node_modules/@blockcast/mmt-alta-parse/dist/.build-stamp +0 -0
  246. package/node_modules/@blockcast/mmt-alta-parse/dist/index.d.ts +57 -0
  247. package/node_modules/@blockcast/mmt-alta-parse/dist/index.d.ts.map +1 -0
  248. package/node_modules/@blockcast/mmt-alta-parse/dist/index.js +115 -0
  249. package/node_modules/@blockcast/mmt-alta-parse/dist/index.js.map +1 -0
  250. package/node_modules/@blockcast/mmt-alta-parse/package.json +50 -0
  251. package/node_modules/@blockcast/mmt-alta-parse/src/index.ts +136 -0
  252. package/node_modules/@blockcast/mmt-base/README.md +120 -0
  253. package/node_modules/@blockcast/mmt-base/dist/.build-stamp +0 -0
  254. package/node_modules/@blockcast/mmt-base/dist/index.d.ts +2 -0
  255. package/node_modules/@blockcast/mmt-base/dist/index.d.ts.map +1 -0
  256. package/node_modules/@blockcast/mmt-base/dist/index.js +2 -0
  257. package/node_modules/@blockcast/mmt-base/dist/index.js.map +1 -0
  258. package/node_modules/@blockcast/mmt-base/dist/mmtp.d.ts +275 -0
  259. package/node_modules/@blockcast/mmt-base/dist/mmtp.d.ts.map +1 -0
  260. package/node_modules/@blockcast/mmt-base/dist/mmtp.js +644 -0
  261. package/node_modules/@blockcast/mmt-base/dist/mmtp.js.map +1 -0
  262. package/node_modules/@blockcast/mmt-base/package.json +49 -0
  263. package/node_modules/@blockcast/mmt-base/src/index.ts +1 -0
  264. package/node_modules/@blockcast/mmt-base/src/mmtp.ts +898 -0
  265. package/node_modules/@blockcast/mmt-fec/README.md +270 -0
  266. package/node_modules/@blockcast/mmt-fec/dist/.build-stamp +0 -0
  267. package/node_modules/@blockcast/mmt-fec/dist/block-manager.d.ts +80 -0
  268. package/node_modules/@blockcast/mmt-fec/dist/block-manager.d.ts.map +1 -0
  269. package/node_modules/@blockcast/mmt-fec/dist/block-manager.js +294 -0
  270. package/node_modules/@blockcast/mmt-fec/dist/block-manager.js.map +1 -0
  271. package/node_modules/@blockcast/mmt-fec/dist/block-processor.d.ts +72 -0
  272. package/node_modules/@blockcast/mmt-fec/dist/block-processor.d.ts.map +1 -0
  273. package/node_modules/@blockcast/mmt-fec/dist/block-processor.js +192 -0
  274. package/node_modules/@blockcast/mmt-fec/dist/block-processor.js.map +1 -0
  275. package/node_modules/@blockcast/mmt-fec/dist/cmaf-recovery.d.ts +46 -0
  276. package/node_modules/@blockcast/mmt-fec/dist/cmaf-recovery.d.ts.map +1 -0
  277. package/node_modules/@blockcast/mmt-fec/dist/cmaf-recovery.js +96 -0
  278. package/node_modules/@blockcast/mmt-fec/dist/cmaf-recovery.js.map +1 -0
  279. package/node_modules/@blockcast/mmt-fec/dist/cmaf-symbols.d.ts +33 -0
  280. package/node_modules/@blockcast/mmt-fec/dist/cmaf-symbols.d.ts.map +1 -0
  281. package/node_modules/@blockcast/mmt-fec/dist/cmaf-symbols.js +57 -0
  282. package/node_modules/@blockcast/mmt-fec/dist/cmaf-symbols.js.map +1 -0
  283. package/node_modules/@blockcast/mmt-fec/dist/fec-block-mapper.d.ts +185 -0
  284. package/node_modules/@blockcast/mmt-fec/dist/fec-block-mapper.d.ts.map +1 -0
  285. package/node_modules/@blockcast/mmt-fec/dist/fec-block-mapper.js +273 -0
  286. package/node_modules/@blockcast/mmt-fec/dist/fec-block-mapper.js.map +1 -0
  287. package/node_modules/@blockcast/mmt-fec/dist/index.d.ts +22 -0
  288. package/node_modules/@blockcast/mmt-fec/dist/index.d.ts.map +1 -0
  289. package/node_modules/@blockcast/mmt-fec/dist/index.js +22 -0
  290. package/node_modules/@blockcast/mmt-fec/dist/index.js.map +1 -0
  291. package/node_modules/@blockcast/mmt-fec/dist/loc-symbols.d.ts +20 -0
  292. package/node_modules/@blockcast/mmt-fec/dist/loc-symbols.d.ts.map +1 -0
  293. package/node_modules/@blockcast/mmt-fec/dist/loc-symbols.js +28 -0
  294. package/node_modules/@blockcast/mmt-fec/dist/loc-symbols.js.map +1 -0
  295. package/node_modules/@blockcast/mmt-fec/dist/oti.d.ts +174 -0
  296. package/node_modules/@blockcast/mmt-fec/dist/oti.d.ts.map +1 -0
  297. package/node_modules/@blockcast/mmt-fec/dist/oti.js +256 -0
  298. package/node_modules/@blockcast/mmt-fec/dist/oti.js.map +1 -0
  299. package/node_modules/@blockcast/mmt-fec/dist/reorder-buffer.d.ts +41 -0
  300. package/node_modules/@blockcast/mmt-fec/dist/reorder-buffer.d.ts.map +1 -0
  301. package/node_modules/@blockcast/mmt-fec/dist/reorder-buffer.js +81 -0
  302. package/node_modules/@blockcast/mmt-fec/dist/reorder-buffer.js.map +1 -0
  303. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-alta-parse/README.md +181 -0
  304. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-alta-parse/dist/.build-stamp +0 -0
  305. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-alta-parse/dist/index.d.ts +57 -0
  306. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-alta-parse/dist/index.d.ts.map +1 -0
  307. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-alta-parse/dist/index.js +115 -0
  308. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-alta-parse/dist/index.js.map +1 -0
  309. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-alta-parse/package.json +50 -0
  310. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-alta-parse/src/index.ts +136 -0
  311. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/README.md +120 -0
  312. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/.build-stamp +0 -0
  313. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/index.d.ts +2 -0
  314. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/index.d.ts.map +1 -0
  315. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/index.js +2 -0
  316. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/index.js.map +1 -0
  317. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/mmtp.d.ts +275 -0
  318. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/mmtp.d.ts.map +1 -0
  319. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/mmtp.js +644 -0
  320. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/mmtp.js.map +1 -0
  321. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/package.json +49 -0
  322. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/src/index.ts +1 -0
  323. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/src/mmtp.ts +898 -0
  324. package/node_modules/@blockcast/mmt-fec/package.json +79 -0
  325. package/node_modules/@blockcast/mmt-fec/src/block-manager.ts +369 -0
  326. package/node_modules/@blockcast/mmt-fec/src/block-processor.ts +277 -0
  327. package/node_modules/@blockcast/mmt-fec/src/cmaf-recovery.ts +127 -0
  328. package/node_modules/@blockcast/mmt-fec/src/cmaf-symbols.ts +68 -0
  329. package/node_modules/@blockcast/mmt-fec/src/fec-block-mapper.ts +332 -0
  330. package/node_modules/@blockcast/mmt-fec/src/index.ts +77 -0
  331. package/node_modules/@blockcast/mmt-fec/src/loc-symbols.ts +34 -0
  332. package/node_modules/@blockcast/mmt-fec/src/oti.ts +319 -0
  333. package/node_modules/@blockcast/mmt-fec/src/reorder-buffer.ts +101 -0
  334. package/node_modules/@blockcast/mmt-manifest-verify/dist/.build-stamp +0 -0
  335. package/node_modules/@blockcast/mmt-manifest-verify/dist/admission.d.ts +28 -0
  336. package/node_modules/@blockcast/mmt-manifest-verify/dist/admission.d.ts.map +1 -0
  337. package/node_modules/@blockcast/mmt-manifest-verify/dist/admission.js +128 -0
  338. package/node_modules/@blockcast/mmt-manifest-verify/dist/admission.js.map +1 -0
  339. package/node_modules/@blockcast/mmt-manifest-verify/dist/index.d.ts +4 -0
  340. package/node_modules/@blockcast/mmt-manifest-verify/dist/index.d.ts.map +1 -0
  341. package/node_modules/@blockcast/mmt-manifest-verify/dist/index.js +4 -0
  342. package/node_modules/@blockcast/mmt-manifest-verify/dist/index.js.map +1 -0
  343. package/node_modules/@blockcast/mmt-manifest-verify/dist/source-auth.d.ts +37 -0
  344. package/node_modules/@blockcast/mmt-manifest-verify/dist/source-auth.d.ts.map +1 -0
  345. package/node_modules/@blockcast/mmt-manifest-verify/dist/source-auth.js +179 -0
  346. package/node_modules/@blockcast/mmt-manifest-verify/dist/source-auth.js.map +1 -0
  347. package/node_modules/@blockcast/mmt-manifest-verify/dist/verifier.d.ts +90 -0
  348. package/node_modules/@blockcast/mmt-manifest-verify/dist/verifier.d.ts.map +1 -0
  349. package/node_modules/@blockcast/mmt-manifest-verify/dist/verifier.js +462 -0
  350. package/node_modules/@blockcast/mmt-manifest-verify/dist/verifier.js.map +1 -0
  351. package/node_modules/@blockcast/mmt-manifest-verify/package.json +45 -0
  352. package/node_modules/@blockcast/mmt-manifest-verify/src/admission.ts +171 -0
  353. package/node_modules/@blockcast/mmt-manifest-verify/src/index.ts +3 -0
  354. package/node_modules/@blockcast/mmt-manifest-verify/src/source-auth.ts +230 -0
  355. package/node_modules/@blockcast/mmt-manifest-verify/src/verifier.ts +636 -0
  356. package/node_modules/@noble/hashes/LICENSE +21 -0
  357. package/node_modules/@noble/hashes/README.md +521 -0
  358. package/node_modules/@noble/hashes/_assert.d.ts +17 -0
  359. package/node_modules/@noble/hashes/_assert.d.ts.map +1 -0
  360. package/node_modules/@noble/hashes/_assert.js +18 -0
  361. package/node_modules/@noble/hashes/_assert.js.map +1 -0
  362. package/node_modules/@noble/hashes/_blake.d.ts +14 -0
  363. package/node_modules/@noble/hashes/_blake.d.ts.map +1 -0
  364. package/node_modules/@noble/hashes/_blake.js +50 -0
  365. package/node_modules/@noble/hashes/_blake.js.map +1 -0
  366. package/node_modules/@noble/hashes/_md.d.ts +51 -0
  367. package/node_modules/@noble/hashes/_md.d.ts.map +1 -0
  368. package/node_modules/@noble/hashes/_md.js +162 -0
  369. package/node_modules/@noble/hashes/_md.js.map +1 -0
  370. package/node_modules/@noble/hashes/_u64.d.ts +55 -0
  371. package/node_modules/@noble/hashes/_u64.d.ts.map +1 -0
  372. package/node_modules/@noble/hashes/_u64.js +90 -0
  373. package/node_modules/@noble/hashes/_u64.js.map +1 -0
  374. package/node_modules/@noble/hashes/argon2.d.ts +32 -0
  375. package/node_modules/@noble/hashes/argon2.d.ts.map +1 -0
  376. package/node_modules/@noble/hashes/argon2.js +401 -0
  377. package/node_modules/@noble/hashes/argon2.js.map +1 -0
  378. package/node_modules/@noble/hashes/blake1.d.ts +106 -0
  379. package/node_modules/@noble/hashes/blake1.d.ts.map +1 -0
  380. package/node_modules/@noble/hashes/blake1.js +459 -0
  381. package/node_modules/@noble/hashes/blake1.js.map +1 -0
  382. package/node_modules/@noble/hashes/blake2.d.ts +116 -0
  383. package/node_modules/@noble/hashes/blake2.d.ts.map +1 -0
  384. package/node_modules/@noble/hashes/blake2.js +420 -0
  385. package/node_modules/@noble/hashes/blake2.js.map +1 -0
  386. package/node_modules/@noble/hashes/blake2b.d.ts +11 -0
  387. package/node_modules/@noble/hashes/blake2b.d.ts.map +1 -0
  388. package/node_modules/@noble/hashes/blake2b.js +14 -0
  389. package/node_modules/@noble/hashes/blake2b.js.map +1 -0
  390. package/node_modules/@noble/hashes/blake2s.d.ts +20 -0
  391. package/node_modules/@noble/hashes/blake2s.d.ts.map +1 -0
  392. package/node_modules/@noble/hashes/blake2s.js +24 -0
  393. package/node_modules/@noble/hashes/blake2s.js.map +1 -0
  394. package/node_modules/@noble/hashes/blake3.d.ts +54 -0
  395. package/node_modules/@noble/hashes/blake3.d.ts.map +1 -0
  396. package/node_modules/@noble/hashes/blake3.js +255 -0
  397. package/node_modules/@noble/hashes/blake3.js.map +1 -0
  398. package/node_modules/@noble/hashes/crypto.d.ts +2 -0
  399. package/node_modules/@noble/hashes/crypto.d.ts.map +1 -0
  400. package/node_modules/@noble/hashes/crypto.js +5 -0
  401. package/node_modules/@noble/hashes/crypto.js.map +1 -0
  402. package/node_modules/@noble/hashes/cryptoNode.d.ts +2 -0
  403. package/node_modules/@noble/hashes/cryptoNode.d.ts.map +1 -0
  404. package/node_modules/@noble/hashes/cryptoNode.js +18 -0
  405. package/node_modules/@noble/hashes/cryptoNode.js.map +1 -0
  406. package/node_modules/@noble/hashes/eskdf.d.ts +47 -0
  407. package/node_modules/@noble/hashes/eskdf.d.ts.map +1 -0
  408. package/node_modules/@noble/hashes/eskdf.js +166 -0
  409. package/node_modules/@noble/hashes/eskdf.js.map +1 -0
  410. package/node_modules/@noble/hashes/esm/_assert.d.ts +17 -0
  411. package/node_modules/@noble/hashes/esm/_assert.d.ts.map +1 -0
  412. package/node_modules/@noble/hashes/esm/_assert.js +15 -0
  413. package/node_modules/@noble/hashes/esm/_assert.js.map +1 -0
  414. package/node_modules/@noble/hashes/esm/_blake.d.ts +14 -0
  415. package/node_modules/@noble/hashes/esm/_blake.d.ts.map +1 -0
  416. package/node_modules/@noble/hashes/esm/_blake.js +45 -0
  417. package/node_modules/@noble/hashes/esm/_blake.js.map +1 -0
  418. package/node_modules/@noble/hashes/esm/_md.d.ts +51 -0
  419. package/node_modules/@noble/hashes/esm/_md.d.ts.map +1 -0
  420. package/node_modules/@noble/hashes/esm/_md.js +155 -0
  421. package/node_modules/@noble/hashes/esm/_md.js.map +1 -0
  422. package/node_modules/@noble/hashes/esm/_u64.d.ts +55 -0
  423. package/node_modules/@noble/hashes/esm/_u64.d.ts.map +1 -0
  424. package/node_modules/@noble/hashes/esm/_u64.js +67 -0
  425. package/node_modules/@noble/hashes/esm/_u64.js.map +1 -0
  426. package/node_modules/@noble/hashes/esm/argon2.d.ts +32 -0
  427. package/node_modules/@noble/hashes/esm/argon2.d.ts.map +1 -0
  428. package/node_modules/@noble/hashes/esm/argon2.js +392 -0
  429. package/node_modules/@noble/hashes/esm/argon2.js.map +1 -0
  430. package/node_modules/@noble/hashes/esm/blake1.d.ts +106 -0
  431. package/node_modules/@noble/hashes/esm/blake1.d.ts.map +1 -0
  432. package/node_modules/@noble/hashes/esm/blake1.js +452 -0
  433. package/node_modules/@noble/hashes/esm/blake1.js.map +1 -0
  434. package/node_modules/@noble/hashes/esm/blake2.d.ts +116 -0
  435. package/node_modules/@noble/hashes/esm/blake2.d.ts.map +1 -0
  436. package/node_modules/@noble/hashes/esm/blake2.js +413 -0
  437. package/node_modules/@noble/hashes/esm/blake2.js.map +1 -0
  438. package/node_modules/@noble/hashes/esm/blake2b.d.ts +11 -0
  439. package/node_modules/@noble/hashes/esm/blake2b.d.ts.map +1 -0
  440. package/node_modules/@noble/hashes/esm/blake2b.js +11 -0
  441. package/node_modules/@noble/hashes/esm/blake2b.js.map +1 -0
  442. package/node_modules/@noble/hashes/esm/blake2s.d.ts +20 -0
  443. package/node_modules/@noble/hashes/esm/blake2s.d.ts.map +1 -0
  444. package/node_modules/@noble/hashes/esm/blake2s.js +21 -0
  445. package/node_modules/@noble/hashes/esm/blake2s.js.map +1 -0
  446. package/node_modules/@noble/hashes/esm/blake3.d.ts +54 -0
  447. package/node_modules/@noble/hashes/esm/blake3.d.ts.map +1 -0
  448. package/node_modules/@noble/hashes/esm/blake3.js +251 -0
  449. package/node_modules/@noble/hashes/esm/blake3.js.map +1 -0
  450. package/node_modules/@noble/hashes/esm/crypto.d.ts +2 -0
  451. package/node_modules/@noble/hashes/esm/crypto.d.ts.map +1 -0
  452. package/node_modules/@noble/hashes/esm/crypto.js +2 -0
  453. package/node_modules/@noble/hashes/esm/crypto.js.map +1 -0
  454. package/node_modules/@noble/hashes/esm/cryptoNode.d.ts +2 -0
  455. package/node_modules/@noble/hashes/esm/cryptoNode.d.ts.map +1 -0
  456. package/node_modules/@noble/hashes/esm/cryptoNode.js +15 -0
  457. package/node_modules/@noble/hashes/esm/cryptoNode.js.map +1 -0
  458. package/node_modules/@noble/hashes/esm/eskdf.d.ts +47 -0
  459. package/node_modules/@noble/hashes/esm/eskdf.d.ts.map +1 -0
  460. package/node_modules/@noble/hashes/esm/eskdf.js +160 -0
  461. package/node_modules/@noble/hashes/esm/eskdf.js.map +1 -0
  462. package/node_modules/@noble/hashes/esm/hkdf.d.ts +36 -0
  463. package/node_modules/@noble/hashes/esm/hkdf.d.ts.map +1 -0
  464. package/node_modules/@noble/hashes/esm/hkdf.js +82 -0
  465. package/node_modules/@noble/hashes/esm/hkdf.js.map +1 -0
  466. package/node_modules/@noble/hashes/esm/hmac.d.ts +35 -0
  467. package/node_modules/@noble/hashes/esm/hmac.d.ts.map +1 -0
  468. package/node_modules/@noble/hashes/esm/hmac.js +86 -0
  469. package/node_modules/@noble/hashes/esm/hmac.js.map +1 -0
  470. package/node_modules/@noble/hashes/esm/index.d.ts +2 -0
  471. package/node_modules/@noble/hashes/esm/index.d.ts.map +1 -0
  472. package/node_modules/@noble/hashes/esm/index.js +33 -0
  473. package/node_modules/@noble/hashes/esm/index.js.map +1 -0
  474. package/node_modules/@noble/hashes/esm/legacy.d.ts +71 -0
  475. package/node_modules/@noble/hashes/esm/legacy.d.ts.map +1 -0
  476. package/node_modules/@noble/hashes/esm/legacy.js +281 -0
  477. package/node_modules/@noble/hashes/esm/legacy.js.map +1 -0
  478. package/node_modules/@noble/hashes/esm/package.json +10 -0
  479. package/node_modules/@noble/hashes/esm/pbkdf2.d.ts +23 -0
  480. package/node_modules/@noble/hashes/esm/pbkdf2.d.ts.map +1 -0
  481. package/node_modules/@noble/hashes/esm/pbkdf2.js +97 -0
  482. package/node_modules/@noble/hashes/esm/pbkdf2.js.map +1 -0
  483. package/node_modules/@noble/hashes/esm/ripemd160.d.ts +13 -0
  484. package/node_modules/@noble/hashes/esm/ripemd160.d.ts.map +1 -0
  485. package/node_modules/@noble/hashes/esm/ripemd160.js +13 -0
  486. package/node_modules/@noble/hashes/esm/ripemd160.js.map +1 -0
  487. package/node_modules/@noble/hashes/esm/scrypt.d.ts +34 -0
  488. package/node_modules/@noble/hashes/esm/scrypt.d.ts.map +1 -0
  489. package/node_modules/@noble/hashes/esm/scrypt.js +228 -0
  490. package/node_modules/@noble/hashes/esm/scrypt.js.map +1 -0
  491. package/node_modules/@noble/hashes/esm/sha1.d.ts +11 -0
  492. package/node_modules/@noble/hashes/esm/sha1.d.ts.map +1 -0
  493. package/node_modules/@noble/hashes/esm/sha1.js +11 -0
  494. package/node_modules/@noble/hashes/esm/sha1.js.map +1 -0
  495. package/node_modules/@noble/hashes/esm/sha2.d.ts +159 -0
  496. package/node_modules/@noble/hashes/esm/sha2.d.ts.map +1 -0
  497. package/node_modules/@noble/hashes/esm/sha2.js +375 -0
  498. package/node_modules/@noble/hashes/esm/sha2.js.map +1 -0
  499. package/node_modules/@noble/hashes/esm/sha256.d.ts +20 -0
  500. package/node_modules/@noble/hashes/esm/sha256.d.ts.map +1 -0
  501. package/node_modules/@noble/hashes/esm/sha256.js +20 -0
  502. package/node_modules/@noble/hashes/esm/sha256.js.map +1 -0
  503. package/node_modules/@noble/hashes/esm/sha3-addons.d.ts +142 -0
  504. package/node_modules/@noble/hashes/esm/sha3-addons.d.ts.map +1 -0
  505. package/node_modules/@noble/hashes/esm/sha3-addons.js +393 -0
  506. package/node_modules/@noble/hashes/esm/sha3-addons.js.map +1 -0
  507. package/node_modules/@noble/hashes/esm/sha3.d.ts +53 -0
  508. package/node_modules/@noble/hashes/esm/sha3.d.ts.map +1 -0
  509. package/node_modules/@noble/hashes/esm/sha3.js +234 -0
  510. package/node_modules/@noble/hashes/esm/sha3.js.map +1 -0
  511. package/node_modules/@noble/hashes/esm/sha512.d.ts +26 -0
  512. package/node_modules/@noble/hashes/esm/sha512.d.ts.map +1 -0
  513. package/node_modules/@noble/hashes/esm/sha512.js +26 -0
  514. package/node_modules/@noble/hashes/esm/sha512.js.map +1 -0
  515. package/node_modules/@noble/hashes/esm/utils.d.ts +161 -0
  516. package/node_modules/@noble/hashes/esm/utils.d.ts.map +1 -0
  517. package/node_modules/@noble/hashes/esm/utils.js +281 -0
  518. package/node_modules/@noble/hashes/esm/utils.js.map +1 -0
  519. package/node_modules/@noble/hashes/hkdf.d.ts +36 -0
  520. package/node_modules/@noble/hashes/hkdf.d.ts.map +1 -0
  521. package/node_modules/@noble/hashes/hkdf.js +88 -0
  522. package/node_modules/@noble/hashes/hkdf.js.map +1 -0
  523. package/node_modules/@noble/hashes/hmac.d.ts +35 -0
  524. package/node_modules/@noble/hashes/hmac.d.ts.map +1 -0
  525. package/node_modules/@noble/hashes/hmac.js +91 -0
  526. package/node_modules/@noble/hashes/hmac.js.map +1 -0
  527. package/node_modules/@noble/hashes/index.d.ts +1 -0
  528. package/node_modules/@noble/hashes/index.d.ts.map +1 -0
  529. package/node_modules/@noble/hashes/index.js +33 -0
  530. package/node_modules/@noble/hashes/index.js.map +1 -0
  531. package/node_modules/@noble/hashes/legacy.d.ts +71 -0
  532. package/node_modules/@noble/hashes/legacy.d.ts.map +1 -0
  533. package/node_modules/@noble/hashes/legacy.js +287 -0
  534. package/node_modules/@noble/hashes/legacy.js.map +1 -0
  535. package/node_modules/@noble/hashes/package.json +266 -0
  536. package/node_modules/@noble/hashes/pbkdf2.d.ts +23 -0
  537. package/node_modules/@noble/hashes/pbkdf2.d.ts.map +1 -0
  538. package/node_modules/@noble/hashes/pbkdf2.js +101 -0
  539. package/node_modules/@noble/hashes/pbkdf2.js.map +1 -0
  540. package/node_modules/@noble/hashes/ripemd160.d.ts +13 -0
  541. package/node_modules/@noble/hashes/ripemd160.d.ts.map +1 -0
  542. package/node_modules/@noble/hashes/ripemd160.js +16 -0
  543. package/node_modules/@noble/hashes/ripemd160.js.map +1 -0
  544. package/node_modules/@noble/hashes/scrypt.d.ts +34 -0
  545. package/node_modules/@noble/hashes/scrypt.d.ts.map +1 -0
  546. package/node_modules/@noble/hashes/scrypt.js +232 -0
  547. package/node_modules/@noble/hashes/scrypt.js.map +1 -0
  548. package/node_modules/@noble/hashes/sha1.d.ts +11 -0
  549. package/node_modules/@noble/hashes/sha1.d.ts.map +1 -0
  550. package/node_modules/@noble/hashes/sha1.js +14 -0
  551. package/node_modules/@noble/hashes/sha1.js.map +1 -0
  552. package/node_modules/@noble/hashes/sha2.d.ts +159 -0
  553. package/node_modules/@noble/hashes/sha2.d.ts.map +1 -0
  554. package/node_modules/@noble/hashes/sha2.js +384 -0
  555. package/node_modules/@noble/hashes/sha2.js.map +1 -0
  556. package/node_modules/@noble/hashes/sha256.d.ts +20 -0
  557. package/node_modules/@noble/hashes/sha256.d.ts.map +1 -0
  558. package/node_modules/@noble/hashes/sha256.js +23 -0
  559. package/node_modules/@noble/hashes/sha256.js.map +1 -0
  560. package/node_modules/@noble/hashes/sha3-addons.d.ts +142 -0
  561. package/node_modules/@noble/hashes/sha3-addons.d.ts.map +1 -0
  562. package/node_modules/@noble/hashes/sha3-addons.js +402 -0
  563. package/node_modules/@noble/hashes/sha3-addons.js.map +1 -0
  564. package/node_modules/@noble/hashes/sha3.d.ts +53 -0
  565. package/node_modules/@noble/hashes/sha3.d.ts.map +1 -0
  566. package/node_modules/@noble/hashes/sha3.js +239 -0
  567. package/node_modules/@noble/hashes/sha3.js.map +1 -0
  568. package/node_modules/@noble/hashes/sha512.d.ts +26 -0
  569. package/node_modules/@noble/hashes/sha512.d.ts.map +1 -0
  570. package/node_modules/@noble/hashes/sha512.js +29 -0
  571. package/node_modules/@noble/hashes/sha512.js.map +1 -0
  572. package/node_modules/@noble/hashes/src/_assert.ts +22 -0
  573. package/node_modules/@noble/hashes/src/_blake.ts +50 -0
  574. package/node_modules/@noble/hashes/src/_md.ts +176 -0
  575. package/node_modules/@noble/hashes/src/_u64.ts +91 -0
  576. package/node_modules/@noble/hashes/src/argon2.ts +497 -0
  577. package/node_modules/@noble/hashes/src/blake1.ts +534 -0
  578. package/node_modules/@noble/hashes/src/blake2.ts +486 -0
  579. package/node_modules/@noble/hashes/src/blake2b.ts +10 -0
  580. package/node_modules/@noble/hashes/src/blake2s.ts +20 -0
  581. package/node_modules/@noble/hashes/src/blake3.ts +272 -0
  582. package/node_modules/@noble/hashes/src/crypto.ts +9 -0
  583. package/node_modules/@noble/hashes/src/cryptoNode.ts +15 -0
  584. package/node_modules/@noble/hashes/src/eskdf.ts +187 -0
  585. package/node_modules/@noble/hashes/src/hkdf.ts +88 -0
  586. package/node_modules/@noble/hashes/src/hmac.ts +94 -0
  587. package/node_modules/@noble/hashes/src/index.ts +31 -0
  588. package/node_modules/@noble/hashes/src/legacy.ts +293 -0
  589. package/node_modules/@noble/hashes/src/pbkdf2.ts +122 -0
  590. package/node_modules/@noble/hashes/src/ripemd160.ts +12 -0
  591. package/node_modules/@noble/hashes/src/scrypt.ts +257 -0
  592. package/node_modules/@noble/hashes/src/sha1.ts +10 -0
  593. package/node_modules/@noble/hashes/src/sha2.ts +402 -0
  594. package/node_modules/@noble/hashes/src/sha256.ts +24 -0
  595. package/node_modules/@noble/hashes/src/sha3-addons.ts +499 -0
  596. package/node_modules/@noble/hashes/src/sha3.ts +258 -0
  597. package/node_modules/@noble/hashes/src/sha512.ts +34 -0
  598. package/node_modules/@noble/hashes/src/utils.ts +395 -0
  599. package/node_modules/@noble/hashes/utils.d.ts +161 -0
  600. package/node_modules/@noble/hashes/utils.d.ts.map +1 -0
  601. package/node_modules/@noble/hashes/utils.js +313 -0
  602. package/node_modules/@noble/hashes/utils.js.map +1 -0
  603. package/package.json +203 -0
  604. package/schemas/catalog.schema.json +283 -0
  605. package/src/align.ts +235 -0
  606. package/src/audio-init-parser.ts +189 -0
  607. package/src/audio-ring-buffer.ts +120 -0
  608. package/src/authored-timing.ts +63 -0
  609. package/src/carp-catalog.ts +776 -0
  610. package/src/catalog-fec-params.ts +167 -0
  611. package/src/catalog-network-source.ts +49 -0
  612. package/src/catalog-validator.ts +34 -0
  613. package/src/cmaf-block-assembler.ts +1058 -0
  614. package/src/cmaf-group-batcher.ts +104 -0
  615. package/src/cmcd-producer.ts +329 -0
  616. package/src/codec-utils.ts +1539 -0
  617. package/src/fec-codec.ts +61 -0
  618. package/src/fec-manager.ts +1939 -0
  619. package/src/fec-starvation-detector.ts +128 -0
  620. package/src/fec-worker-setup.ts +177 -0
  621. package/src/fmp4-completion.ts +47 -0
  622. package/src/fmp4-reader.ts +112 -0
  623. package/src/index.ts +443 -0
  624. package/src/iso-bmff-timing.ts +688 -0
  625. package/src/loc.ts +181 -0
  626. package/src/manifest-auth-plan.ts +107 -0
  627. package/src/media-router.ts +1185 -0
  628. package/src/mfu-reassembler.ts +2842 -0
  629. package/src/mfu-reassembly-policy.ts +134 -0
  630. package/src/mmtp-container-parser.ts +1498 -0
  631. package/src/mmtp-decode-pipeline.ts +315 -0
  632. package/src/mmtp-router.ts +3027 -0
  633. package/src/mmtp-to-cmaf.ts +956 -0
  634. package/src/multicast-auth-catalog.ts +340 -0
  635. package/src/multicast-input.ts +439 -0
  636. package/src/multicast-manager.ts +1503 -0
  637. package/src/multicast-transport.ts +320 -0
  638. package/src/multicast-unwrap.ts +132 -0
  639. package/src/no-modules.ts +20 -0
  640. package/src/phase14-catalog-override.ts +336 -0
  641. package/src/repair-fetch.ts +217 -0
  642. package/src/sample-combiner.ts +846 -0
  643. package/src/signaling.ts +78 -0
  644. package/src/source-auth.ts +1 -0
  645. package/src/switching-set-alignment.ts +109 -0
  646. package/src/timestamp-codec.ts +265 -0
  647. package/src/track-pipeline-setup.ts +189 -0
  648. package/src/track-pipeline.ts +332 -0
  649. package/src/track-session.ts +778 -0
  650. package/src/transport-pool.ts +128 -0
  651. package/src/wasm-mfu-reassembler.ts +372 -0
  652. package/src/wasm-mmtp-container-parser.ts +23 -0
  653. package/test-vectors/align.json +199 -0
  654. package/test-vectors/catalog/README.md +25 -0
  655. package/test-vectors/catalog/negative/legacy-selection-params.json +15 -0
  656. package/test-vectors/catalog/negative/missing-mmtp-fields.json +12 -0
  657. package/test-vectors/catalog/negative/multicast-auth-padded-key.json +37 -0
  658. package/test-vectors/catalog/negative/multicast-auth-reused-track-format.json +53 -0
  659. package/test-vectors/catalog/negative/multicast-auth-trailing-pad-bits.json +37 -0
  660. package/test-vectors/catalog/negative/multicast-endpoint-missing-protocol-and-source.json +24 -0
  661. package/test-vectors/catalog/negative/network-source-object.json +25 -0
  662. package/test-vectors/catalog/negative/raptorq-unaligned-symbol.json +26 -0
  663. package/test-vectors/catalog/negative/repair-track-legacy-shape.json +18 -0
  664. package/test-vectors/catalog/negative/track-role-empty.json +16 -0
  665. package/test-vectors/catalog/negative/track-role-non-string.json +16 -0
  666. package/test-vectors/catalog/positive/fec-multicast.json +51 -0
  667. package/test-vectors/catalog/positive/flat-av.json +30 -0
  668. package/test-vectors/catalog/positive/multicast-auth-rotation.json +55 -0
  669. package/test-vectors/catalog/positive/ticks-and-mixed-timescales.json +39 -0
  670. package/test-vectors/catalog/positive/track-role.json +49 -0
  671. package/test-vectors/catalog/releases/moqcast-native-v16-v1/catalog.json +65 -0
  672. package/test-vectors/catalog/releases/moqcast-native-v16-v1.archive.sha256 +1 -0
  673. package/test-vectors/catalog-multicast-auth-v1.json +26 -0
  674. package/test-vectors/catalog-validation.json +148 -0
  675. package/test-vectors/catalog-validator-public-api.mjs +25 -0
  676. package/test-vectors/generate-source-auth-fixture.mjs +114 -0
  677. package/test-vectors/source-auth-v1.json +61 -0
  678. package/test-vectors/subsample-placement-v1.json +230 -0
@@ -0,0 +1,1185 @@
1
+ /**
2
+ * MediaRouter — source arbitration for MoQ ↔ multicast switching.
3
+ *
4
+ * Both moq-lib and moqtail wire their MoQ and multicast frame outputs
5
+ * through this router. The router decides which source reaches the decoder:
6
+ *
7
+ * - MoQ frames pass through while multicast is inactive.
8
+ * - Multicast video frames are suppressed until the first keyframe.
9
+ * - Once multicast has both a video keyframe and audio, takeover is requested.
10
+ * - MoQ is suppressed only after the source-switch callback completes.
11
+ * - failbackToMoq() reverses the switch immediately.
12
+ *
13
+ * Per-track setTrackSource() allows individual tracks to be wired to a
14
+ * specific transport source (video=multicast, audio=moq, overlay=amt) without
15
+ * changing the binary MoQ↔multicast switching for other tracks.
16
+ *
17
+ * notifyGroupBoundary() is an observer hook: callers report the
18
+ * packet-stamped groupId + media time for each group, and the router
19
+ * cross-checks against the shared `align.ts` formula. Drift is emitted via
20
+ * onGroupDrift so the player can bump the CMCD `x-bc-switch-drift-us` /
21
+ * `x-bc-switch-miscount` counters. Observation only — does NOT alter routing.
22
+ */
23
+
24
+ import { groupIdForTicks, driftCheck } from './align.js'
25
+ import { computeFecDeliveryWindowMs } from './track-session.js'
26
+
27
+ export type FrameMeta = {
28
+ timestamp: number
29
+ keyframe: boolean
30
+ packetId?: number
31
+ packetSequenceNumber?: number
32
+ source?: 'moq' | 'multicast' | 'repair'
33
+ recovered?: boolean
34
+ sourceDiscontinuity?: boolean
35
+ }
36
+ export type FrameKind = 'video' | 'audio'
37
+ export type SourceKind = 'moq' | 'multicast'
38
+
39
+ /**
40
+ * Tick-space timing for one group boundary — the exact, authoritative form.
41
+ *
42
+ * These are the integers the catalog already carries (`groupDurationTicks`,
43
+ * `timescale`) plus the packet's raw timestamp in the same units. Passing
44
+ * them straight through keeps the whole drift check in integer math.
45
+ *
46
+ * Prefer this over the seconds overload: `groupDurationTicks / timescale`
47
+ * is not exactly representable for the common non-integer-second cadences
48
+ * (33333@90k, 1001@30k, 1024@44.1k), and the seconds path has to quantize
49
+ * to whole nanoseconds — which shifts `expected` by a whole group from the
50
+ * second boundary onward and reports drift on a perfectly aligned stream.
51
+ */
52
+ export interface GroupBoundaryTicks {
53
+ /** Packet media timestamp in `timescale` units. Integer; negatives clamp to 0. */
54
+ readonly mediaTimeTicks: number
55
+ /** Group duration in `timescale` units. Positive integer. */
56
+ readonly groupDurationTicks: number
57
+ /**
58
+ * Catalog timescale in Hz. Positive integer — **validated, and a hard gate**:
59
+ * a non-integer or non-positive value skips the drift check entirely rather
60
+ * than reporting against it. It does not feed the groupId computation (that
61
+ * is `mediaTimeTicks / groupDurationTicks` alone), but it does derive the
62
+ * `mediaTimeSec` / `groupDurationSec` on every GroupDriftEvent — so letting a
63
+ * bad value through would push NaN/Infinity into CMCD `x-bc-switch-drift-us`,
64
+ * which is worse than skipping.
65
+ */
66
+ readonly timescale: number
67
+ }
68
+
69
+ /** Payload emitted by MediaRouter.onGroupDrift when a packet-stamped groupId
70
+ * disagrees with the shared `align.ts` formula. */
71
+ export interface GroupDriftEvent {
72
+ trackName: string
73
+ expectedGroupId: number
74
+ actualGroupId: number
75
+ /** `actual - expected` (positive = ahead, negative = behind). */
76
+ deltaGroups: number
77
+ /** Seconds view, kept for CMCD consumers. Derived when the caller used ticks. */
78
+ mediaTimeSec: number
79
+ groupDurationSec: number
80
+ /** Exact tick-space inputs. Present only when the caller used the ticks form. */
81
+ timing?: GroupBoundaryTicks
82
+ }
83
+
84
+ /** Normalized output of the two notifyGroupBoundary input forms. */
85
+ type ResolvedBoundary = {
86
+ readonly expected: number
87
+ readonly mediaTimeSec: number
88
+ readonly groupDurationSec: number
89
+ readonly timing?: GroupBoundaryTicks
90
+ }
91
+
92
+ /**
93
+ * Distinct failure modes on the notifyGroupBoundary drift path. Each gets its
94
+ * own bounded warn budget (see `#skipWithWarning`), so a stream that trips one
95
+ * cannot permanently silence the diagnostic for a genuinely different one.
96
+ *
97
+ * Deliberately a closed union rather than `string`: the tag is the dedup key,
98
+ * and a typo would silently mint a second budget for an existing reason.
99
+ */
100
+ type GroupAlignSkipReason =
101
+ | 'invalid-tick-inputs'
102
+ // Currently UNREACHABLE, kept as defence in depth — see the catch in
103
+ // #resolveTicks. Do not write a test chasing it.
104
+ | 'ticks-formula-threw'
105
+ | 'invalid-seconds-inputs'
106
+ | 'sub-nanosecond-duration'
107
+ | 'media-time-overflow'
108
+ | 'seconds-formula-threw'
109
+ | 'callback-threw'
110
+
111
+ /**
112
+ * Warns allowed per `GroupAlignSkipReason` for the lifetime of a router.
113
+ *
114
+ * Not a single flat number (PCL-2211, #329 review). The six resolver reasons
115
+ * are closed, deterministic conditions: once you have seen
116
+ * `sub-nanosecond-duration` on this router, the second occurrence carries no
117
+ * new information, so one slot is the right budget and repeated malformed
118
+ * catalog input stays silent.
119
+ *
120
+ * `callback-threw` is the opposite. It wraps arbitrary downstream consumer
121
+ * code and interpolates `err.message` *because the error varies*. Under one
122
+ * slot, a consumer that throws at startup permanently hides an unrelated
123
+ * exception later — including one from a *replacement* consumer registered
124
+ * through `onGroupDrift()`, which is a public setter with no budget reset.
125
+ * That is the same argument `#skipWithWarning` makes against a single shared
126
+ * boolean, one level down. A small cap bounds the flood, at the cost of
127
+ * silencing distinct errors past the cap: three throws at startup exhaust
128
+ * `callback-threw`, and a replacement consumer's unrelated exception is then
129
+ * silenced permanently — the same failure as the one-slot budget, moved from
130
+ * the 1st throw to the 4th. That residual is the inherent price of a bounded
131
+ * budget and is accepted deliberately; the alternative is an unbounded warn.
132
+ *
133
+ * Deliberately NOT a reset on `onGroupDrift()` re-registration: that setter is
134
+ * public and unthrottled, so a caller re-registering per boundary would
135
+ * refresh the budget every time and restore the exact flood this prevents.
136
+ *
137
+ * A total `Record` rather than a lookup with a default: the union is closed, so
138
+ * a new reason that forgets to declare its budget is a compile error — the
139
+ * same typo-safety rationale that makes the union closed in the first place.
140
+ */
141
+ const GROUP_ALIGN_WARN_BUDGET: Record<GroupAlignSkipReason, number> = {
142
+ 'invalid-tick-inputs': 1,
143
+ 'ticks-formula-threw': 1,
144
+ 'invalid-seconds-inputs': 1,
145
+ 'sub-nanosecond-duration': 1,
146
+ 'media-time-overflow': 1,
147
+ 'seconds-formula-threw': 1,
148
+ 'callback-threw': 3,
149
+ }
150
+
151
+ type FrameCb = (data: Uint8Array, meta: FrameMeta) => void
152
+ type SwitchCb = (source: SourceKind) => void | Promise<void>
153
+
154
+ export interface MediaRouterOptions {
155
+ /** Set false for video-only streams. Default: true. */
156
+ requireAudio?: boolean
157
+ /**
158
+ * Audio fallback timeout (ms): force multicast activation if video keyframe
159
+ * arrived but audio hasn't within this window. Catalog-derived via
160
+ * computeFecDeliveryWindowMs (the single FEC recovery-window source of truth).
161
+ * Omit to disable the independent audio-fallback timer (no magic default).
162
+ */
163
+ audioFallbackMs?: number
164
+ /**
165
+ * Number of binary-path frames to allow through during a crossfade transition.
166
+ * Derive from FEC block size (e.g., ceil(K / groupsPerBlock)). Default: 10.
167
+ */
168
+ crossfadeFrames?: number
169
+ /**
170
+ * Max pre-boundary overlay frames to buffer before dropping.
171
+ * Derive from FEC config (e.g., ceil(K * 1.5)). Default: 128.
172
+ */
173
+ maxOverlayBuffer?: number
174
+ /**
175
+ * Max concurrent in-flight repairs. Prevents relay SUBSCRIBE flood (T-13-01 mitigation).
176
+ * Default: 2.
177
+ */
178
+ maxRepairInFlight?: number
179
+ /**
180
+ * Allow multicast takeover from a delta frame after the player has confirmed
181
+ * that MoQ already seeded the same decoder timeline with a video keyframe.
182
+ * Default false: decoder-reset paths still need multicast to provide a RAP.
183
+ */
184
+ allowDeltaMulticastTakeoverAfterMoqKeyframe?: boolean
185
+ }
186
+
187
+ /**
188
+ * A per-track source that emits frames independently of the binary MoQ/multicast path.
189
+ * Used by setTrackSource() to wire specific tracks to specific transport sources.
190
+ */
191
+ export interface TrackSource {
192
+ name: string
193
+ /**
194
+ * Register a frame callback. Returns an unsubscribe function.
195
+ * MediaRouter calls unsubscribe() when clearTrackSource() is called.
196
+ */
197
+ onFrame(cb: (data: Uint8Array, meta: FrameMeta) => void): () => void
198
+ /**
199
+ * Optional error callback for when the source fails.
200
+ * MediaRouter uses this to trigger failback to binary switching for this track.
201
+ */
202
+ onError?(cb: () => void): () => void
203
+ /** Optional dispose hook called when clearTrackSource() is called. */
204
+ dispose?(): void
205
+ }
206
+
207
+ /**
208
+ * Thrown when setTrackSource() is called for a track that already has a source
209
+ * without an intervening clearTrackSource(). Prevents silent concurrent-assignment
210
+ * races (T-02-16 mitigation).
211
+ */
212
+ export class TrackSourceConflictError extends Error {
213
+ constructor(public readonly trackName: string) {
214
+ super(`Track source already assigned for '${trackName}'; call clearTrackSource first`)
215
+ this.name = 'TrackSourceConflictError'
216
+ }
217
+ }
218
+
219
+ /**
220
+ * Migration policy for setTrackSource() transport switching.
221
+ *
222
+ * - 'crossfade': Overlap old (binary) and new source briefly (CROSSFADE_WINDOW_FRAMES),
223
+ * then suppress the binary path. Default for audio.
224
+ * - 'drop': Cut to new source immediately. Binary path suppressed at once.
225
+ * Suitable for latency-critical tracks.
226
+ * - 'wait': Defer migration until first FEC block boundary (same as overlay=true).
227
+ * Default for video — avoids decoder reset mid-block.
228
+ */
229
+ export type MigrationPolicy = 'crossfade' | 'drop' | 'wait'
230
+
231
+ /** Default crossfade window — override via MediaRouterOptions.crossfadeFrames. */
232
+ const DEFAULT_CROSSFADE_FRAMES = 10
233
+
234
+ /** Options passed to setTrackSource(). */
235
+ export interface SetTrackSourceOptions {
236
+ /**
237
+ * If true, this source is an overlay track that must wait for the first FEC
238
+ * block boundary signal (via notifyFecBlockBoundary()) before emitting frames.
239
+ * Frames arriving before the first boundary are buffered up to MAX_OVERLAY_BUFFER.
240
+ *
241
+ * Backwards compat: equivalent to policy: 'wait'.
242
+ */
243
+ overlay?: boolean
244
+ /**
245
+ * Migration policy for the transport switch. Overrides `overlay` when provided.
246
+ * See MigrationPolicy for semantics.
247
+ */
248
+ policy?: MigrationPolicy
249
+ }
250
+
251
+ /** Default overlay buffer cap — override via MediaRouterOptions.maxOverlayBuffer. */
252
+ const DEFAULT_MAX_OVERLAY_BUFFER = 128
253
+
254
+ /** Internal per-track source record. */
255
+ interface TrackSourceRecord {
256
+ source: TrackSource
257
+ unsubscribeFrame: () => void
258
+ unsubscribeError?: () => void
259
+ emitCount: number
260
+ }
261
+
262
+ export class MediaRouter {
263
+ #active: SourceKind = 'moq'
264
+ #multicastVideoKeyed = false
265
+ #multicastAudioReceived = false
266
+ #moqVideoKeyframeEmitted = false
267
+ #requireAudio: boolean
268
+ #suppressed = false // true after multicast confirmed — suppresses MoQ
269
+ #audioFallbackTimer: ReturnType<typeof setTimeout> | null = null
270
+
271
+ // Configurable constants (from catalog/FEC config, not magic numbers)
272
+ readonly #audioFallbackMs: number
273
+ readonly #crossfadeFrames: number
274
+ readonly #maxOverlayBuffer: number
275
+ readonly #allowDeltaMulticastTakeoverAfterMoqKeyframe: boolean
276
+
277
+ // ── Keyframe repair state ────────────────────────────────────────
278
+ /** Groups currently being repaired — idempotent guard per Pitfall 2 (T-13-01 mitigation) */
279
+ #repairInFlight = new Set<number>()
280
+ /** Max concurrent repairs — prevents relay SUBSCRIBE flood (T-13-01 mitigation) */
281
+ readonly #maxRepairInFlight: number
282
+ /** Callback to request MoQ subscribe for a specific group (wired by player) */
283
+ #onRepairRequest: ((expectedGroupId: number, trackName: string) => void) | null = null
284
+
285
+ #onVideoFrame: FrameCb | null = null
286
+ #onAudioFrame: FrameCb | null = null
287
+ #onOverlayFrame: FrameCb | null = null
288
+ #onSourceSwitch: SwitchCb | null = null
289
+ #sourceSwitchReady: Promise<void> | null = null
290
+ #sourceSwitchToken = 0
291
+ #pendingMulticastToken: number | null = null
292
+ #pendingMulticastVideo: { data: Uint8Array; meta: FrameMeta } | null = null
293
+ #pendingMulticastAudio: { data: Uint8Array; meta: FrameMeta } | null = null
294
+
295
+ // Per-track source state
296
+ #trackSources = new Map<string, TrackSourceRecord>()
297
+
298
+ // Track type registry: trackName → { type, source }.
299
+ // Init data / inline signaling is authoritative until catalog updates.
300
+ // Priority: 'init' > 'catalog' > 'name' (backward compat fallback).
301
+ #trackTypes = new Map<string, { type: FrameKind; source: 'catalog' | 'init' }>()
302
+
303
+ // Crossfade state: maps trackName → remaining binary-path frames allowed during window
304
+ #crossfadeCounters = new Map<string, number>()
305
+
306
+ // Overlay alignment state
307
+ #overlayBoundaryReached = false
308
+ #overlayBuffer: Array<{ data: Uint8Array; meta: FrameMeta }> = []
309
+
310
+ // ── Phase 14: deltaActive state machine (D-11..D-14) ─────────────
311
+ // No timers — state transitions driven entirely by notifyDeltaBlock() callbacks.
312
+ // All thresholds derived from catalog fields or throw at configureDelta() time.
313
+ #deltaConfigured = false
314
+ #deltaActive: boolean = true
315
+ #deltaWindow: boolean[] = [] // ring buffer: true = block failed
316
+ #deltaWindowSize: number = 0
317
+ #deltaFailThresholdRatio: number = 0 // P/K
318
+ #deltaRecoverThresholdRatio: number = 0 // (P/K)/2 — hysteresis (D-13)
319
+ #deltaProbeIntervalMs: number = 0
320
+ #lastProbeAt: number = 0
321
+ #onDeltaFallbackCb: (() => void) | null = null
322
+ #onDeltaRecoverCb: (() => void) | null = null
323
+
324
+ constructor(options?: MediaRouterOptions) {
325
+ this.#requireAudio = options?.requireAudio ?? true
326
+ // Catalog-derived — the caller passes an audioFallbackMs computed from the
327
+ // FEC recovery window (computeFecDeliveryWindowMs). No magic default: when
328
+ // absent, the independent audio-fallback timer is disabled (0) and
329
+ // activation waits for real audio arrival.
330
+ this.#audioFallbackMs = options?.audioFallbackMs ?? 0
331
+ this.#crossfadeFrames = options?.crossfadeFrames ?? DEFAULT_CROSSFADE_FRAMES
332
+ this.#maxOverlayBuffer = options?.maxOverlayBuffer ?? DEFAULT_MAX_OVERLAY_BUFFER
333
+ this.#maxRepairInFlight = options?.maxRepairInFlight ?? 2
334
+ this.#allowDeltaMulticastTakeoverAfterMoqKeyframe =
335
+ options?.allowDeltaMulticastTakeoverAfterMoqKeyframe ?? false
336
+ }
337
+
338
+ onVideoFrame(cb: FrameCb) { this.#onVideoFrame = cb }
339
+ onAudioFrame(cb: FrameCb) { this.#onAudioFrame = cb }
340
+ onOverlayFrame(cb: FrameCb) { this.#onOverlayFrame = cb }
341
+ onSourceSwitch(cb: SwitchCb) { this.#onSourceSwitch = cb }
342
+
343
+ // ── Group-alignment observer (align.ts formula) ──────────────────
344
+ // Observation only — does NOT alter routing. Producers are wired in
345
+ // follow-up PRs; when they land they will call
346
+ // CmcdProducer.incrementSwitchMiscount() + emitSwitchDrift() from the
347
+ // onGroupDrift callback so divergence between packet-stamped groupIds
348
+ // and the shared formula surfaces on `x-bc-switch-drift-us` /
349
+ // `x-bc-switch-miscount`. Canonical Go impl: Blockcast/multicast
350
+ // cmd/caddy/sender/group_align.go (commit 104eeff). Canonical Rust impl:
351
+ // moqtail/moqtail apps/relay/src/server/abr.rs (branch
352
+ // experimental/server-side-abr, commit 39accbf).
353
+ #onGroupDrift: ((evt: GroupDriftEvent) => void) | null = null
354
+ #groupAlignWarnCounts = new Map<GroupAlignSkipReason, number>()
355
+ onGroupDrift(cb: (evt: GroupDriftEvent) => void): void {
356
+ this.#onGroupDrift = cb
357
+ }
358
+
359
+ /**
360
+ * Report a group boundary observed on an incoming track. Cross-checks the
361
+ * packet-stamped groupId against the shared `align.ts` formula and fires
362
+ * the onGroupDrift callback when they disagree.
363
+ *
364
+ * Two input forms:
365
+ * - **Ticks (preferred).** `notifyGroupBoundary(track, id, { mediaTimeTicks,
366
+ * groupDurationTicks, timescale })` — pure integer math via
367
+ * `align.groupIdForTicks`, no rounding anywhere.
368
+ * - **Seconds (deprecated).** The 4-argument form. Quantizes both operands
369
+ * to whole nanoseconds, which is exact only when `groupDurationSec` is a
370
+ * whole number of nanoseconds. It is not for 33333@90k, 1001@30k or
371
+ * 1024@44.1k, and there `expected` diverges by a whole group from the
372
+ * second boundary onward — a false drift event on every subsequent
373
+ * group, and a systematically wrong CMCD `x-bc-switch-drift-us`.
374
+ *
375
+ * No-op when no onGroupDrift callback is registered (avoid unnecessary work
376
+ * on hot paths).
377
+ *
378
+ * Hot-path robustness (does NOT throw on bad inputs — the ingest stream
379
+ * must not be torn down by a malformed catalog or a disposed producer):
380
+ * - Invalid duration (NaN / <=0) or non-finite media time → warn + early
381
+ * return (same convention as audioFallbackMs).
382
+ * - Any throw out of the align formula (e.g. the safe-integer overflow
383
+ * guard at ~104 days of media time on the seconds path) is caught +
384
+ * logged so an unusually long-running stream cannot tear down ingest.
385
+ * - Callback throws are caught + logged; ingest continues.
386
+ * - Skip warnings are coalesced **per failure mode** for the lifetime
387
+ * of the router (never reset, including across `dispose()`), so a hot
388
+ * path cannot flood the log while a genuinely new failure mode still
389
+ * gets heard. Deterministic modes get one line; a throwing consumer
390
+ * callback gets a small budget because its error varies. See
391
+ * `#skipWithWarning` and `GROUP_ALIGN_WARN_BUDGET`.
392
+ *
393
+ * Init-track filter: media time === 0 with a non-zero stamped groupId is
394
+ * treated as an init/ambient track emitting pre-PTS-zero packets and is
395
+ * skipped (matches Go's base-clamp intent without producing spurious
396
+ * drift).
397
+ *
398
+ * NOTE: do NOT call from the repair path (feedRepair). Repair frames carry
399
+ * the groupId they are recovering for, not a freshly-stamped one.
400
+ */
401
+ notifyGroupBoundary(
402
+ trackName: string,
403
+ packetGroupId: number,
404
+ timing: GroupBoundaryTicks,
405
+ ): void
406
+ /**
407
+ * @deprecated Pass a {@link GroupBoundaryTicks} instead. The seconds form
408
+ * rounds to whole nanoseconds and reports phantom drift on catalogs whose
409
+ * `groupDurationTicks / timescale` is not ns-exact.
410
+ */
411
+ notifyGroupBoundary(
412
+ trackName: string,
413
+ packetGroupId: number,
414
+ mediaTimeSec: number,
415
+ groupDurationSec: number,
416
+ ): void
417
+ notifyGroupBoundary(
418
+ trackName: string,
419
+ packetGroupId: number,
420
+ timingOrMediaTimeSec: GroupBoundaryTicks | number,
421
+ groupDurationSec?: number,
422
+ ): void {
423
+ if (!this.#onGroupDrift) return
424
+
425
+ const resolved =
426
+ typeof timingOrMediaTimeSec === 'object' && timingOrMediaTimeSec !== null
427
+ ? this.#resolveTicks(trackName, timingOrMediaTimeSec)
428
+ : this.#resolveSeconds(
429
+ trackName,
430
+ timingOrMediaTimeSec as number,
431
+ groupDurationSec as number,
432
+ )
433
+ if (!resolved) return
434
+
435
+ // Init/ambient track heuristic: mediaTime=0 with stamped groupId !== 0
436
+ // is a signaling packet pre-PTS-zero, not a real drift. Skipping here
437
+ // avoids every stream opening with a false drift event.
438
+ if (resolved.mediaTimeSec === 0 && packetGroupId !== 0) return
439
+
440
+ const expected = resolved.expected
441
+ const result = driftCheck(expected, packetGroupId)
442
+ if (result.kind === 'aligned') return
443
+
444
+ try {
445
+ this.#onGroupDrift({
446
+ trackName,
447
+ expectedGroupId: expected,
448
+ actualGroupId: packetGroupId,
449
+ deltaGroups: result.deltaGroups,
450
+ mediaTimeSec: resolved.mediaTimeSec,
451
+ groupDurationSec: resolved.groupDurationSec,
452
+ ...(resolved.timing ? { timing: resolved.timing } : {}),
453
+ })
454
+ } catch (err) {
455
+ // A disposed CmcdProducer (or any downstream consumer) throwing here
456
+ // must NOT tear down the MoQ ingest stream. Log and continue.
457
+ //
458
+ // Routed through the same per-reason budget as the resolver skips: this
459
+ // fires once per DRIFT EVENT, so a consumer that throws on every
460
+ // boundary would otherwise flood at boundary rate — exactly the failure
461
+ // mode #skipWithWarning exists to prevent, and exactly the disposed-
462
+ // producer scenario this comment already cites (#329 review).
463
+ this.#skipWithWarning(
464
+ 'callback-threw',
465
+ `[MediaRouter] onGroupDrift callback threw (track=${trackName} expected=${expected} actual=${packetGroupId}); continuing: ${(err as Error)?.message ?? err}`,
466
+ )
467
+ }
468
+ }
469
+
470
+ /**
471
+ * Warn at most `GROUP_ALIGN_WARN_BUDGET[reason]` times **per reason**, across
472
+ * every skip and post-emit failure on the drift path.
473
+ *
474
+ * Note the two call shapes: the resolvers call this *instead of* computing a
475
+ * groupId, and return its `null` to skip the drift check; the `onGroupDrift`
476
+ * catch calls it after the check already ran and the event was already
477
+ * emitted, so there only the warn is coalesced and the `null` is discarded.
478
+ *
479
+ * Keyed on a short stable `reason` tag, NOT on the formatted message: the
480
+ * messages interpolate track name and operand values, so message-keying
481
+ * would hand a malformed stream a fresh warn slot on every boundary — the
482
+ * log flood this coalescing exists to prevent.
483
+ *
484
+ * Per-reason rather than one shared boolean (PCL-2211, #329 review): the
485
+ * budget is never reset — not by `dispose()`, not on source switch — so a
486
+ * single flag means the FIRST malformed boundary of a long-running stream
487
+ * permanently silences every later, genuinely different failure mode. The
488
+ * throw/overflow diagnostics carry the operand values (`ticks=`, `durTicks=`,
489
+ * `err.message`) an incident responder actually needs, and under one shared
490
+ * flag those are precisely the ones most likely to be already spent by an
491
+ * unrelated NaN at catalog load.
492
+ *
493
+ * A closed, bounded set of reasons times a small per-reason budget is still
494
+ * bounded, so this cannot flood. See `GROUP_ALIGN_WARN_BUDGET` for why the
495
+ * budget is not uniform.
496
+ */
497
+ #skipWithWarning(reason: GroupAlignSkipReason, message: string): null {
498
+ const seen = this.#groupAlignWarnCounts.get(reason) ?? 0
499
+ if (seen < GROUP_ALIGN_WARN_BUDGET[reason]) {
500
+ this.#groupAlignWarnCounts.set(reason, seen + 1)
501
+ console.warn(message)
502
+ }
503
+ return null
504
+ }
505
+
506
+ /**
507
+ * Exact path: integers straight from the catalog, no rounding. `mediaTimeSec`
508
+ * / `groupDurationSec` are derived for reporting only and never feed the
509
+ * groupId computation.
510
+ */
511
+ #resolveTicks(
512
+ trackName: string,
513
+ timing: GroupBoundaryTicks,
514
+ ): ResolvedBoundary | null {
515
+ const { mediaTimeTicks, groupDurationTicks, timescale } = timing
516
+ if (
517
+ !Number.isInteger(mediaTimeTicks) ||
518
+ !Number.isInteger(groupDurationTicks) ||
519
+ groupDurationTicks <= 0 ||
520
+ !Number.isInteger(timescale) ||
521
+ timescale <= 0
522
+ ) {
523
+ return this.#skipWithWarning(
524
+ 'invalid-tick-inputs',
525
+ `[MediaRouter] notifyGroupBoundary invalid tick inputs (track=${trackName} mediaTimeTicks=${mediaTimeTicks} groupDurationTicks=${groupDurationTicks} timescale=${timescale}); skipping drift check. Pre-validate at catalog load time.`,
526
+ )
527
+ }
528
+ try {
529
+ return {
530
+ expected: groupIdForTicks(mediaTimeTicks, groupDurationTicks),
531
+ mediaTimeSec: mediaTimeTicks / timescale,
532
+ groupDurationSec: groupDurationTicks / timescale,
533
+ // Snapshot, not the caller's reference: a producer that reuses one
534
+ // scratch object per boundary would otherwise retroactively mutate
535
+ // every GroupDriftEvent it has already emitted. Three integers.
536
+ timing: { mediaTimeTicks, groupDurationTicks, timescale },
537
+ }
538
+ } catch (err) {
539
+ // UNREACHABLE as of today, and deliberately kept. groupIdForTicks throws
540
+ // on exactly two conditions — !Number.isInteger(ticks) and
541
+ // !Number.isInteger(groupDurationTicks) || groupDurationTicks <= 0 — and
542
+ // the guard above pre-rejects both; Math.floor(a / b) does not throw.
543
+ //
544
+ // Kept because align.ts is a separate module that can grow a new
545
+ // precondition (a safe-integer check, say) without this file changing,
546
+ // and the no-throw contract must hold on that day too. Nobody should
547
+ // write a test chasing this branch — see BLO-21176 note.
548
+ //
549
+ // NOTE the asymmetry this implies: the seconds path is the only one that
550
+ // can actually reach its catch, because durTicks <= 0 cannot catch
551
+ // Infinity and Number.isSafeInteger guards only `ticks`. That is the real
552
+ // shape of the AC#5a bug, not an oversight in this resolver.
553
+ return this.#skipWithWarning(
554
+ 'ticks-formula-threw',
555
+ `[MediaRouter] notifyGroupBoundary: groupIdForTicks threw (track=${trackName} mediaTimeTicks=${mediaTimeTicks} groupDurationTicks=${groupDurationTicks}); skipping drift check: ${(err as Error)?.message ?? err}`,
556
+ )
557
+ }
558
+ }
559
+
560
+ /**
561
+ * Legacy seconds path. Quantizes to a synthetic 1e9 Hz timescale — the exact
562
+ * behaviour the deprecated `align.groupIdFor` wrapper had, reproduced here so
563
+ * that wrapper has no non-test callers left and can be deleted (BLO-21176).
564
+ *
565
+ * Inherits that wrapper's ns-rounding error; see the `@deprecated` overload.
566
+ * Delete this branch once every caller passes ticks.
567
+ */
568
+ #resolveSeconds(
569
+ trackName: string,
570
+ mediaTimeSec: number,
571
+ groupDurationSec: number,
572
+ ): ResolvedBoundary | null {
573
+ if (
574
+ !Number.isFinite(groupDurationSec) ||
575
+ groupDurationSec <= 0 ||
576
+ !Number.isFinite(mediaTimeSec)
577
+ ) {
578
+ return this.#skipWithWarning(
579
+ 'invalid-seconds-inputs',
580
+ `[MediaRouter] notifyGroupBoundary invalid inputs (track=${trackName} mediaTimeSec=${mediaTimeSec} groupDurationSec=${groupDurationSec}); skipping drift check. Pre-validate at catalog load time.`,
581
+ )
582
+ }
583
+ // 1e9 Hz keeps both operands inside MAX_SAFE_INTEGER for ~104 days of
584
+ // media time. Past that Math.round yields a non-safe integer that
585
+ // Number.isInteger still accepts, so bail explicitly rather than
586
+ // silently quantizing — ingest must survive either way.
587
+ const TS = 1_000_000_000
588
+ const ticks = mediaTimeSec < 0 ? 0 : Math.round(mediaTimeSec * TS)
589
+ const durTicks = Math.round(groupDurationSec * TS)
590
+ if (durTicks <= 0) {
591
+ return this.#skipWithWarning(
592
+ 'sub-nanosecond-duration',
593
+ `[MediaRouter] notifyGroupBoundary: groupDurationSec=${groupDurationSec} rounds to <1 tick at 1e9 Hz (track=${trackName}); skipping drift check. Pass GroupBoundaryTicks for sub-nanosecond durations.`,
594
+ )
595
+ }
596
+ if (!Number.isSafeInteger(ticks)) {
597
+ return this.#skipWithWarning(
598
+ 'media-time-overflow',
599
+ `[MediaRouter] notifyGroupBoundary: mediaTimeSec=${mediaTimeSec} overflows safe-integer ticks at 1e9 Hz (>~104 days, track=${trackName}); skipping drift check. Pass GroupBoundaryTicks with the catalog timescale.`,
600
+ )
601
+ }
602
+ // Same try as #resolveTicks: a finite groupDurationSec can still overflow
603
+ // the * 1e9 multiply to Infinity, which clears the durTicks <= 0 bail above
604
+ // but fails Number.isInteger inside groupIdForTicks. Without this catch the
605
+ // throw escapes notifyGroupBoundary and breaks its documented no-throw
606
+ // contract — the one path #resolveTicks already guards and this did not.
607
+ try {
608
+ return {
609
+ expected: groupIdForTicks(ticks, durTicks),
610
+ mediaTimeSec,
611
+ groupDurationSec,
612
+ }
613
+ } catch (err) {
614
+ return this.#skipWithWarning(
615
+ 'seconds-formula-threw',
616
+ `[MediaRouter] notifyGroupBoundary: groupIdForTicks threw (track=${trackName} mediaTimeSec=${mediaTimeSec} groupDurationSec=${groupDurationSec} ticks=${ticks} durTicks=${durTicks}); skipping drift check: ${(err as Error)?.message ?? err}`,
617
+ )
618
+ }
619
+ }
620
+
621
+ get activeSource(): SourceKind { return this.#active }
622
+
623
+ // ── Keyframe repair API ──────────────────────────────────────────
624
+
625
+ /** Register callback for keyframe repair requests. Player wires this to issue MoQ SUBSCRIBE. */
626
+ onRepairRequest(cb: (expectedGroupId: number, trackName: string) => void): void {
627
+ this.#onRepairRequest = cb
628
+ }
629
+
630
+ /**
631
+ * Request repair for a lost keyframe group. Only fires when activeSource is 'multicast'.
632
+ * Idempotent per groupId — duplicate calls for the same group are suppressed.
633
+ * @param expectedGroupId - The group ID expected but not received
634
+ * @param trackName - Track name for the SUBSCRIBE (e.g. 'video'). Default: 'video'.
635
+ */
636
+ repairLostGroup(expectedGroupId: number, trackName = 'video'): void {
637
+ if (this.#active !== 'multicast') return // only repair on multicast path
638
+ if (this.#repairInFlight.has(expectedGroupId)) return // idempotent
639
+ if (this.#repairInFlight.size >= this.#maxRepairInFlight) {
640
+ console.warn(
641
+ `[MediaRouter] max repair in flight (${this.#maxRepairInFlight}), dropping repair for group ${expectedGroupId}`,
642
+ )
643
+ return
644
+ }
645
+ this.#repairInFlight.add(expectedGroupId)
646
+ this.#onRepairRequest?.(expectedGroupId, trackName)
647
+ }
648
+
649
+ /**
650
+ * Feed a recovered keyframe frame from the repair path.
651
+ * Bypasses source arbitration entirely — goes directly to decoder callbacks.
652
+ * Does NOT change activeSource. Does NOT trigger failbackToMoq.
653
+ */
654
+ feedRepair(data: Uint8Array, meta: FrameMeta, kind: FrameKind): void {
655
+ // Repair only makes sense when multicast is active.
656
+ // Silently drop if MoQ became active (race with failbackToMoq) to avoid
657
+ // injecting stale-timestamp frames into the MoQ decoder path (WR-02).
658
+ if (this.#active !== 'multicast') return
659
+ const routedMeta = { ...meta, source: 'repair' as const, recovered: true }
660
+ if (kind === 'video') this.#onVideoFrame?.(data, routedMeta)
661
+ else this.#onAudioFrame?.(data, routedMeta)
662
+ }
663
+
664
+ /**
665
+ * Clear repair-in-flight state for a group (called after repair completes or times out).
666
+ */
667
+ clearRepairInFlight(groupId: number): void {
668
+ this.#repairInFlight.delete(groupId)
669
+ }
670
+
671
+ // ── Phase 14: deltaActive state machine API ──────────────────────
672
+
673
+ /**
674
+ * Configure the delta-track fallback state machine from catalog parameters.
675
+ *
676
+ * D-11: Thresholds derive from fec.sourceSymbols, fec.repairSymbols, framerate.
677
+ * D-13: Probe interval derives from deltaProbeIntervalMs or interleaveDepthMs*2.
678
+ * CLAUDE.md: error on bad init — throws if required inputs are absent.
679
+ *
680
+ * Calling again resets deltaActive=true and clears the window buffer (stream restart).
681
+ */
682
+ configureDelta(cfg: {
683
+ framerate: number
684
+ sourceSymbols: number
685
+ repairSymbols: number
686
+ deltaFailWindowBlocks?: number
687
+ deltaProbeIntervalMs?: number
688
+ interleaveDepthMs?: number
689
+ }): void {
690
+ if (typeof cfg.framerate !== 'number' || !Number.isFinite(cfg.framerate) || cfg.framerate <= 0) {
691
+ throw new Error('[MediaRouter] configureDelta: framerate required and must be > 0')
692
+ }
693
+ if (typeof cfg.sourceSymbols !== 'number' || cfg.sourceSymbols <= 0) {
694
+ throw new Error('[MediaRouter] configureDelta: sourceSymbols (K_delta) required and must be > 0')
695
+ }
696
+ if (typeof cfg.repairSymbols !== 'number' || cfg.repairSymbols <= 0) {
697
+ throw new Error('[MediaRouter] configureDelta: repairSymbols (P_delta) required and must be > 0')
698
+ }
699
+ if (cfg.deltaProbeIntervalMs === undefined && cfg.interleaveDepthMs === undefined) {
700
+ throw new Error(
701
+ '[MediaRouter] configureDelta: either deltaProbeIntervalMs or interleaveDepthMs required for probe interval derivation'
702
+ )
703
+ }
704
+
705
+ // Derivations — no magic floors, no hardcoded defaults (CLAUDE.md).
706
+ this.#deltaWindowSize = cfg.deltaFailWindowBlocks ?? Math.ceil(cfg.framerate)
707
+ this.#deltaFailThresholdRatio = cfg.repairSymbols / cfg.sourceSymbols
708
+ this.#deltaRecoverThresholdRatio = this.#deltaFailThresholdRatio / 2
709
+ // Probe interval routes through the ONE FEC recovery-window derivation
710
+ // (computeFecDeliveryWindowMs). Interleave-span + jitter margin only — the
711
+ // K-cycle production horizon (mediaUnitsPerSecond) is deliberately omitted:
712
+ // this is a re-subscribe cadence, not a block-recovery deadline. With no
713
+ // catalog jitterBufferMs the margin defaults to one interleave window, so
714
+ // the value equals the retired interleaveDepthMs*2 but is now catalog-tunable.
715
+ this.#deltaProbeIntervalMs = cfg.deltaProbeIntervalMs ?? computeFecDeliveryWindowMs({
716
+ interleaveDepthMs: cfg.interleaveDepthMs!,
717
+ sourceSymbols: cfg.sourceSymbols,
718
+ })
719
+
720
+ // Reset state — useful for stream restart.
721
+ this.#deltaActive = true
722
+ this.#deltaWindow = []
723
+ this.#lastProbeAt = 0
724
+ this.#deltaConfigured = true
725
+ }
726
+
727
+ /**
728
+ * Notify the state machine of a completed FEC block for the delta track.
729
+ *
730
+ * @param failed - true if the block was unrecoverable (FEC failure)
731
+ * @param nowMs - caller-injected monotonic ms (no internal timer — D-13, CLAUDE.md)
732
+ *
733
+ * If configureDelta() has not been called, this is a silent no-op (pre-catalog state).
734
+ */
735
+ notifyDeltaBlock(failed: boolean, nowMs: number): void {
736
+ if (!this.#deltaConfigured) return
737
+
738
+ // Maintain sliding window — ring buffer semantics.
739
+ this.#deltaWindow.push(failed)
740
+ if (this.#deltaWindow.length > this.#deltaWindowSize) {
741
+ this.#deltaWindow.shift()
742
+ }
743
+
744
+ // Window not yet full — insufficient data for decision.
745
+ if (this.#deltaWindow.length < this.#deltaWindowSize) return
746
+
747
+ const failedCount = this.#deltaWindow.filter(x => x).length
748
+ const ratio = failedCount / this.#deltaWindowSize
749
+
750
+ if (this.#deltaActive) {
751
+ // Fallback transition: active → inactive (T-14-03: fires exactly once)
752
+ if (ratio > this.#deltaFailThresholdRatio) {
753
+ this.#deltaActive = false
754
+ this.#lastProbeAt = nowMs
755
+ this.#onDeltaFallbackCb?.()
756
+ }
757
+ } else {
758
+ // Recovery transition: inactive → active (after probe interval + low ratio)
759
+ if (
760
+ (nowMs - this.#lastProbeAt) >= this.#deltaProbeIntervalMs &&
761
+ ratio < this.#deltaRecoverThresholdRatio
762
+ ) {
763
+ this.#deltaActive = true
764
+ this.#lastProbeAt = nowMs
765
+ this.#onDeltaRecoverCb?.()
766
+ }
767
+ }
768
+ }
769
+
770
+ /** Register callback fired when delta track degrades past threshold (D-12). */
771
+ onDeltaFallback(cb: () => void): void {
772
+ this.#onDeltaFallbackCb = cb
773
+ }
774
+
775
+ /** Register callback fired when delta track recovers below recovery threshold (D-13). */
776
+ onDeltaRecover(cb: () => void): void {
777
+ this.#onDeltaRecoverCb = cb
778
+ }
779
+
780
+ /** Whether the delta track is currently considered healthy (true = active). */
781
+ get deltaActive(): boolean {
782
+ return this.#deltaActive
783
+ }
784
+
785
+ // ── Test-visible accessors for threshold verification ────────────
786
+ // These expose internal derived values for test assertions (Tests 3,4,5,6,7).
787
+ // Prefixed with 'delta' to namespace clearly within the public API surface.
788
+
789
+ /** Derived window size — exposed for test assertions (Test 3, 4). */
790
+ get deltaWindowSize(): number { return this.#deltaWindowSize }
791
+ /** Derived fail threshold ratio (P/K) — exposed for test assertions (Test 6). */
792
+ get deltaFailThresholdRatio(): number { return this.#deltaFailThresholdRatio }
793
+ /** Derived recovery threshold ratio ((P/K)/2) — exposed for test assertions (Test 7). */
794
+ get deltaRecoverThresholdRatio(): number { return this.#deltaRecoverThresholdRatio }
795
+ /** Derived probe interval ms — exposed for test assertions (Test 5a). */
796
+ get deltaProbeIntervalMs(): number { return this.#deltaProbeIntervalMs }
797
+
798
+ /**
799
+ * Register a track name → type mapping from catalog.
800
+ * Catalog is the default authority. Init data / inline signaling
801
+ * takes precedence until the next catalog update.
802
+ */
803
+ registerTrackType(trackName: string, type: FrameKind): void {
804
+ // Catalog overwrites any previous registration (including init).
805
+ this.#trackTypes.set(trackName, { type, source: 'catalog' })
806
+ }
807
+
808
+ /**
809
+ * Register a track name → type from init data / inline signaling (MMTP FT=0).
810
+ * Authoritative until the next catalog update per §5.3.
811
+ * Does NOT overwrite an existing catalog registration.
812
+ */
813
+ registerTrackTypeFromInit(trackName: string, type: FrameKind): void {
814
+ const existing = this.#trackTypes.get(trackName)
815
+ if (existing?.source === 'catalog') return // catalog already set — don't downgrade
816
+ this.#trackTypes.set(trackName, { type, source: 'init' })
817
+ }
818
+
819
+ /**
820
+ * Assign a per-track source. Frames from this source bypass the binary
821
+ * MoQ↔multicast switching for this track only.
822
+ *
823
+ * Throws TrackSourceConflictError if a source is already assigned for this
824
+ * track without a clearTrackSource() in between.
825
+ */
826
+ setTrackSource(trackName: string, source: TrackSource, opts?: SetTrackSourceOptions): void {
827
+ if (this.#trackSources.has(trackName)) {
828
+ throw new TrackSourceConflictError(trackName)
829
+ }
830
+
831
+ // Resolve effective policy. opts.policy takes precedence; overlay=true is legacy 'wait'.
832
+ const effectivePolicy: MigrationPolicy | undefined =
833
+ opts?.policy ?? (opts?.overlay ? 'wait' : undefined)
834
+
835
+ // For crossfade: open the binary-path window immediately so the first per-track frame
836
+ // triggers the window (counter starts before the frame callback is wired).
837
+ if (effectivePolicy === 'crossfade') {
838
+ this.#crossfadeCounters.set(trackName, this.#crossfadeFrames)
839
+ }
840
+
841
+ const record: TrackSourceRecord = {
842
+ source,
843
+ unsubscribeFrame: () => {},
844
+ emitCount: 0,
845
+ }
846
+
847
+ // Wire the frame callback
848
+ record.unsubscribeFrame = source.onFrame((data, meta) => {
849
+ record.emitCount++
850
+ if (effectivePolicy === 'wait') {
851
+ this.#handleOverlayFrame(data, meta)
852
+ } else {
853
+ // 'drop', 'crossfade', or undefined: emit immediately
854
+ this.#emitForTrack(trackName, data, meta)
855
+ }
856
+ })
857
+
858
+ // Wire the optional error callback — failback to binary switching
859
+ if (source.onError) {
860
+ record.unsubscribeError = source.onError(() => {
861
+ this.clearTrackSource(trackName)
862
+ })
863
+ }
864
+
865
+ this.#trackSources.set(trackName, record)
866
+ }
867
+
868
+ /**
869
+ * Remove the per-track source for the given track.
870
+ * Binary switching resumes for this track after this call.
871
+ */
872
+ clearTrackSource(trackName: string): void {
873
+ const record = this.#trackSources.get(trackName)
874
+ if (!record) return
875
+
876
+ record.unsubscribeFrame()
877
+ record.unsubscribeError?.()
878
+ record.source.dispose?.()
879
+ this.#trackSources.delete(trackName)
880
+
881
+ // Clean up crossfade window if one was active for this track
882
+ this.#crossfadeCounters.delete(trackName)
883
+
884
+ // Reset overlay buffer if removing a non-media track (overlay/unknown)
885
+ const reg = this.#trackTypes.get(trackName)
886
+ const isMedia = reg?.type === 'video' || reg?.type === 'audio' ||
887
+ (reg === undefined && (trackName === 'video' || trackName === 'audio'))
888
+ if (!isMedia) {
889
+ this.#overlayBoundaryReached = false
890
+ this.#overlayBuffer = []
891
+ }
892
+ }
893
+
894
+ /**
895
+ * Signal that a FEC block boundary has been reached on the base track.
896
+ * Overlay tracks waiting for alignment flush their buffered frames now.
897
+ */
898
+ notifyFecBlockBoundary(): void {
899
+ if (this.#overlayBoundaryReached) return
900
+ this.#overlayBoundaryReached = true
901
+
902
+ // Flush buffered overlay frames
903
+ const buffered = this.#overlayBuffer.splice(0)
904
+ for (const { data, meta } of buffered) {
905
+ this.#onOverlayFrame?.(data, meta)
906
+ }
907
+ }
908
+
909
+ /**
910
+ * Return the number of frames emitted from the per-track source for a given
911
+ * track. Used in test-mode to verify per-track routing histogram.
912
+ * Returns 0 if no per-track source is active for the track.
913
+ */
914
+ trackSourceEmitCount(trackName: string): number {
915
+ return this.#trackSources.get(trackName)?.emitCount ?? 0
916
+ }
917
+
918
+ /** Feed a frame from the MoQ path. Suppressed when multicast is active OR a per-track source owns this track. */
919
+ feedMoq(data: Uint8Array, meta: FrameMeta, kind: FrameKind) {
920
+ // Per-track source takes precedence; allow through during crossfade window
921
+ if (this.#trackSources.has(kind)) {
922
+ const remaining = this.#crossfadeCounters.get(kind)
923
+ if (remaining && remaining > 0) {
924
+ // Decrement crossfade counter and allow this frame through
925
+ const next = remaining - 1
926
+ if (next === 0) {
927
+ this.#crossfadeCounters.delete(kind)
928
+ } else {
929
+ this.#crossfadeCounters.set(kind, next)
930
+ }
931
+ // Fall through to emit below (no return)
932
+ } else {
933
+ return // suppressed — no crossfade window active
934
+ }
935
+ }
936
+ if (this.#suppressed) return
937
+ // Audio suppression is handled by SessionManager onSuppress callback —
938
+ // do NOT independently suppress based on multicastAudioReceived.
939
+ // The state machine decides when to switch; the router just routes.
940
+ const routedMeta = { ...meta, source: 'moq' as const }
941
+ if (kind === 'video') this.#onVideoFrame?.(data, routedMeta)
942
+ else this.#onAudioFrame?.(data, routedMeta)
943
+ }
944
+
945
+ /** Feed a frame from the multicast path. Video waits for keyframe before activating. */
946
+ feedMulticast(data: Uint8Array, meta: FrameMeta, kind: FrameKind) {
947
+ // Per-track source takes precedence; allow through during crossfade window
948
+ if (this.#trackSources.has(kind)) {
949
+ const remaining = this.#crossfadeCounters.get(kind)
950
+ if (remaining && remaining > 0) {
951
+ const next = remaining - 1
952
+ if (next === 0) {
953
+ this.#crossfadeCounters.delete(kind)
954
+ } else {
955
+ this.#crossfadeCounters.set(kind, next)
956
+ }
957
+ // Fall through to emit below (no return)
958
+ } else {
959
+ return // suppressed
960
+ }
961
+ }
962
+
963
+ if (kind === 'video') {
964
+ if (!this.#multicastVideoKeyed) {
965
+ const moqSeededDelta =
966
+ !meta.keyframe &&
967
+ this.#allowDeltaMulticastTakeoverAfterMoqKeyframe &&
968
+ this.#moqVideoKeyframeEmitted
969
+ if (!meta.keyframe && !moqSeededDelta) return // drop until first safe decode point
970
+ this.#multicastVideoKeyed = true
971
+ const pendingMeta: FrameMeta = { ...meta, source: 'multicast' }
972
+ if (!moqSeededDelta) pendingMeta.sourceDiscontinuity = true
973
+ this.#pendingMulticastVideo = {
974
+ data: data.slice(),
975
+ meta: pendingMeta,
976
+ }
977
+ // If audio is required but hasn't arrived yet, start a fallback timer.
978
+ // Audio FEC blocks may recover after video — don't block activation forever.
979
+ // Only arms when a catalog-derived audioFallbackMs was supplied (>0).
980
+ if (this.#requireAudio && this.#audioFallbackMs > 0 && !this.#multicastAudioReceived && !this.#audioFallbackTimer) {
981
+ this.#audioFallbackTimer = setTimeout(() => {
982
+ if (!this.#multicastAudioReceived && this.#active !== 'multicast') {
983
+ this.#multicastAudioReceived = true // force
984
+ // Also release the MoQ-audio-warmup gate so the fallback actually
985
+ // activates — otherwise #tryActivateMulticast would still bail.
986
+ this.#moqAudioWarmed = true
987
+ this.#tryActivateMulticast()
988
+ }
989
+ }, this.#audioFallbackMs)
990
+ }
991
+ } else if (this.#active !== 'multicast') {
992
+ // Do not feed video into the multicast decoder while the source switch
993
+ // is still pending. Keep the newest keyframe as the post-activation
994
+ // decoder opener and drop deltas until takeover commits.
995
+ if (meta.keyframe) {
996
+ this.#pendingMulticastVideo = {
997
+ data: data.slice(),
998
+ meta: { ...meta, source: 'multicast', sourceDiscontinuity: true },
999
+ }
1000
+ }
1001
+ this.#tryActivateMulticast()
1002
+ return
1003
+ }
1004
+ if (this.#active === 'multicast') this.#onVideoFrame?.(data, { ...meta, source: 'multicast' })
1005
+ this.#tryActivateMulticast()
1006
+ } else {
1007
+ this.#multicastAudioReceived = true
1008
+ if (this.#audioFallbackTimer) {
1009
+ clearTimeout(this.#audioFallbackTimer)
1010
+ this.#audioFallbackTimer = null
1011
+ }
1012
+ // BLO-3035 layer 5: in a multicast-first scenario (no MoQ unicast audio
1013
+ // source) `notifyMoqAudioEmit()` is never called and the audio-fallback
1014
+ // timer's force-set is bypassed (the timer's inner `!multicastAudioReceived`
1015
+ // check returns early because audio already arrived). Without this gate
1016
+ // `#moqAudioWarmed` stays false forever and `#tryActivateMulticast` never
1017
+ // fires `onSourceSwitch('multicast')` — the player can't know to emit
1018
+ // `__MULTICAST_TAKEOVER_TIME`. Treat the arrival of multicast audio as
1019
+ // sufficient evidence to release the warmup gate. For the MoQ→multicast
1020
+ // handoff case `notifyMoqAudioEmit()` will already have set this flag, so
1021
+ // the assignment is idempotent.
1022
+ if (!this.#moqAudioWarmed) this.#moqAudioWarmed = true
1023
+ // Activate BEFORE delivering the frame — the source switch callback
1024
+ // (e.g. ring buffer reset) must fire before the first multicast audio
1025
+ // frame reaches the backend. Otherwise the NTP timestamp hits the
1026
+ // MoQ-era ring buffer base → overflow → silence gap.
1027
+ this.#tryActivateMulticast()
1028
+ // Only emit multicast audio AFTER takeover succeeds. Before takeover,
1029
+ // MoQ audio is still flowing through feedMoq → #onAudioFrame; emitting
1030
+ // multicast audio too would double-feed the backend (2x audio frames,
1031
+ // pitch wobble and distortion). Post-takeover, feedMoq is suppressed
1032
+ // (#suppressed=true) so only multicast audio reaches the backend.
1033
+ if (this.#active === 'multicast') {
1034
+ this.#onAudioFrame?.(data, { ...meta, source: 'multicast' })
1035
+ } else if (this.#pendingMulticastToken !== null) {
1036
+ this.#pendingMulticastAudio = { data: data.slice(), meta: { ...meta, source: 'multicast' } }
1037
+ }
1038
+ }
1039
+ }
1040
+
1041
+ /**
1042
+ * MoQ unicast audio warmup gate. When audio is required, multicast takeover
1043
+ * waits until the MoQ unicast audio decoder has emitted at least one frame
1044
+ * (so downstream samples-before-takeover counters observe nonzero) or until
1045
+ * the existing audio-fallback timer forces the flag true.
1046
+ */
1047
+ #moqAudioWarmed = false
1048
+
1049
+ /** Called by the player when the MoQ unicast audio decoder emits first frame. */
1050
+ public notifyMoqAudioEmit(): void {
1051
+ if (this.#moqAudioWarmed) return
1052
+ this.#moqAudioWarmed = true
1053
+ this.#tryActivateMulticast()
1054
+ }
1055
+
1056
+ /**
1057
+ * Called by the player after a MoQ video keyframe has reached the active
1058
+ * decoder path. Used only by MSE/MMTP continuity switching; decoder-reset
1059
+ * paths leave allowDeltaMulticastTakeoverAfterMoqKeyframe disabled.
1060
+ */
1061
+ public notifyMoqVideoKeyframeEmit(): void {
1062
+ this.#moqVideoKeyframeEmitted = true
1063
+ this.#tryActivateMulticast()
1064
+ }
1065
+
1066
+ #tryActivateMulticast() {
1067
+ if (this.#active === 'multicast') return
1068
+ if (this.#pendingMulticastToken !== null) return
1069
+ if (!this.#multicastVideoKeyed) return
1070
+ if (this.#requireAudio && !this.#multicastAudioReceived) return
1071
+ if (this.#requireAudio && !this.#moqAudioWarmed) return
1072
+ const token = ++this.#sourceSwitchToken
1073
+ const maybeReady = this.#onSourceSwitch?.('multicast')
1074
+ if (!maybeReady || typeof (maybeReady as Promise<void>).then !== 'function') {
1075
+ this.#commitMulticastActivation(token)
1076
+ return
1077
+ }
1078
+ const ready = Promise.resolve(maybeReady)
1079
+ this.#pendingMulticastToken = token
1080
+ this.#sourceSwitchReady = ready
1081
+ void ready.then(
1082
+ () => this.#commitMulticastActivation(token),
1083
+ err => this.#rejectMulticastActivation(token, err),
1084
+ ).finally(() => {
1085
+ if (this.#sourceSwitchReady === ready) this.#sourceSwitchReady = null
1086
+ })
1087
+ }
1088
+
1089
+ #commitMulticastActivation(token: number) {
1090
+ if (token !== this.#sourceSwitchToken) return
1091
+ this.#active = 'multicast'
1092
+ this.#suppressed = true
1093
+ this.#pendingMulticastToken = null
1094
+ const video = this.#pendingMulticastVideo
1095
+ this.#pendingMulticastVideo = null
1096
+ const audio = this.#pendingMulticastAudio
1097
+ this.#pendingMulticastAudio = null
1098
+ if (video) this.#onVideoFrame?.(video.data, video.meta)
1099
+ if (audio) this.#onAudioFrame?.(audio.data, audio.meta)
1100
+ }
1101
+
1102
+ #rejectMulticastActivation(token: number, err: unknown) {
1103
+ if (token !== this.#sourceSwitchToken) return
1104
+ if (this.#pendingMulticastToken === token) this.#pendingMulticastToken = null
1105
+ this.#pendingMulticastVideo = null
1106
+ this.#pendingMulticastAudio = null
1107
+ console.warn('[MediaRouter] onSourceSwitch(multicast) failed', err)
1108
+ }
1109
+
1110
+ /** Switch back to MoQ immediately (called on multicast failback). */
1111
+ failbackToMoq() {
1112
+ const hadPendingMulticast = this.#pendingMulticastToken !== null
1113
+ if (this.#active === 'moq' && !hadPendingMulticast) return // already on MoQ, no-op
1114
+ ++this.#sourceSwitchToken
1115
+ this.#active = 'moq'
1116
+ this.#suppressed = false
1117
+ this.#pendingMulticastToken = null
1118
+ this.#pendingMulticastVideo = null
1119
+ this.#pendingMulticastAudio = null
1120
+ this.#sourceSwitchReady = null
1121
+ this.#multicastVideoKeyed = false
1122
+ this.#multicastAudioReceived = false
1123
+ this.#moqVideoKeyframeEmitted = false
1124
+ // A pending audio-fallback timer belongs to the abandoned takeover attempt.
1125
+ // Left running it would force-set the audio flags against the next attempt's
1126
+ // fresh state (BLO-9413 failback coherence — every takeover-era flag/timer
1127
+ // unwinds on this edge).
1128
+ if (this.#audioFallbackTimer) {
1129
+ clearTimeout(this.#audioFallbackTimer)
1130
+ this.#audioFallbackTimer = null
1131
+ }
1132
+ this.#onSourceSwitch?.('moq')
1133
+ }
1134
+
1135
+ /** Dispose timers and per-track sources. Call on teardown to prevent
1136
+ * stale timer callbacks firing into garbage-collected closures (WR-03). */
1137
+ dispose(): void {
1138
+ if (this.#audioFallbackTimer) {
1139
+ clearTimeout(this.#audioFallbackTimer)
1140
+ this.#audioFallbackTimer = null
1141
+ }
1142
+ for (const [name] of this.#trackSources) {
1143
+ this.clearTrackSource(name)
1144
+ }
1145
+ }
1146
+
1147
+ // ── Private helpers ──────────────────────────────────────────────────────
1148
+
1149
+ /** Emit a frame on the correct per-track callback using the type registry.
1150
+ * Falls back to name match if no type registered (backward compat). */
1151
+ #emitForTrack(trackName: string, data: Uint8Array, meta: FrameMeta): void {
1152
+ // Prefer explicit type registry (catalog or init data)
1153
+ const reg = this.#trackTypes.get(trackName)
1154
+ if (reg?.type === 'video') {
1155
+ this.#onVideoFrame?.(data, meta)
1156
+ } else if (reg?.type === 'audio') {
1157
+ this.#onAudioFrame?.(data, meta)
1158
+ } else if (reg !== undefined) {
1159
+ // Registered but unknown type — overlay
1160
+ this.#onOverlayFrame?.(data, meta)
1161
+ } else {
1162
+ // Not registered — fall back to name match (backward compat for tests/legacy)
1163
+ if (trackName === 'video') this.#onVideoFrame?.(data, meta)
1164
+ else if (trackName === 'audio') this.#onAudioFrame?.(data, meta)
1165
+ else this.#onOverlayFrame?.(data, meta)
1166
+ }
1167
+ }
1168
+
1169
+ /** Handle an overlay frame: buffer until first FEC block boundary, then pass through. */
1170
+ #handleOverlayFrame(data: Uint8Array, meta: FrameMeta): void {
1171
+ if (this.#overlayBoundaryReached) {
1172
+ this.#onOverlayFrame?.(data, meta)
1173
+ return
1174
+ }
1175
+
1176
+ // Buffer until boundary — enforcing cap for T-02-17 DoS mitigation
1177
+ if (this.#overlayBuffer.length >= this.#maxOverlayBuffer) {
1178
+ console.warn(
1179
+ `[MediaRouter] overlay buffer full (${this.#maxOverlayBuffer} frames); dropping frame ts=${meta.timestamp}`
1180
+ )
1181
+ return
1182
+ }
1183
+ this.#overlayBuffer.push({ data, meta })
1184
+ }
1185
+ }