@blockcast/mmt-container 0.1.0-main.0399efef1f07

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (694) hide show
  1. package/README.md +432 -0
  2. package/dist/.build-stamp +0 -0
  3. package/dist/__tests__/test-utils.d.ts +71 -0
  4. package/dist/__tests__/test-utils.d.ts.map +1 -0
  5. package/dist/align.d.ts +152 -0
  6. package/dist/align.d.ts.map +1 -0
  7. package/dist/align.js +88 -0
  8. package/dist/align.js.map +7 -0
  9. package/dist/audio-init-parser.d.ts +66 -0
  10. package/dist/audio-init-parser.d.ts.map +1 -0
  11. package/dist/audio-init-parser.js +118 -0
  12. package/dist/audio-init-parser.js.map +7 -0
  13. package/dist/audio-ring-buffer.d.ts +50 -0
  14. package/dist/audio-ring-buffer.d.ts.map +1 -0
  15. package/dist/audio-ring-buffer.js +93 -0
  16. package/dist/audio-ring-buffer.js.map +7 -0
  17. package/dist/authored-timing.d.ts +32 -0
  18. package/dist/authored-timing.d.ts.map +1 -0
  19. package/dist/authored-timing.js +32 -0
  20. package/dist/authored-timing.js.map +7 -0
  21. package/dist/browser-runtime.js +20087 -0
  22. package/dist/browser-runtime.js.map +7 -0
  23. package/dist/carp-catalog.d.ts +318 -0
  24. package/dist/carp-catalog.d.ts.map +1 -0
  25. package/dist/carp-catalog.js +873 -0
  26. package/dist/carp-catalog.js.map +7 -0
  27. package/dist/catalog-fec-params.d.ts +202 -0
  28. package/dist/catalog-fec-params.d.ts.map +1 -0
  29. package/dist/catalog-fec-params.js +568 -0
  30. package/dist/catalog-fec-params.js.map +7 -0
  31. package/dist/catalog-network-source.d.ts +19 -0
  32. package/dist/catalog-network-source.d.ts.map +1 -0
  33. package/dist/catalog-network-source.js +22 -0
  34. package/dist/catalog-network-source.js.map +7 -0
  35. package/dist/catalog-validator.d.ts +18 -0
  36. package/dist/catalog-validator.d.ts.map +1 -0
  37. package/dist/catalog-validator.js +7747 -0
  38. package/dist/catalog-validator.js.map +7 -0
  39. package/dist/cmaf-block-assembler.d.ts +217 -0
  40. package/dist/cmaf-block-assembler.d.ts.map +1 -0
  41. package/dist/cmaf-block-assembler.js +1769 -0
  42. package/dist/cmaf-block-assembler.js.map +7 -0
  43. package/dist/cmaf-group-batcher.d.ts +38 -0
  44. package/dist/cmaf-group-batcher.d.ts.map +1 -0
  45. package/dist/cmaf-group-batcher.js +195 -0
  46. package/dist/cmaf-group-batcher.js.map +7 -0
  47. package/dist/cmcd-producer.d.ts +104 -0
  48. package/dist/cmcd-producer.d.ts.map +1 -0
  49. package/dist/cmcd-producer.js +207 -0
  50. package/dist/cmcd-producer.js.map +7 -0
  51. package/dist/codec-utils.d.ts +248 -0
  52. package/dist/codec-utils.d.ts.map +1 -0
  53. package/dist/codec-utils.js +1268 -0
  54. package/dist/codec-utils.js.map +7 -0
  55. package/dist/fec-codec.d.ts +32 -0
  56. package/dist/fec-codec.d.ts.map +1 -0
  57. package/dist/fec-codec.js +47 -0
  58. package/dist/fec-codec.js.map +7 -0
  59. package/dist/fec-manager.d.ts +307 -0
  60. package/dist/fec-manager.d.ts.map +1 -0
  61. package/dist/fec-manager.js +1530 -0
  62. package/dist/fec-manager.js.map +7 -0
  63. package/dist/fec-starvation-detector.d.ts +59 -0
  64. package/dist/fec-starvation-detector.d.ts.map +1 -0
  65. package/dist/fec-starvation-detector.js +75 -0
  66. package/dist/fec-starvation-detector.js.map +7 -0
  67. package/dist/fec-worker-setup.d.ts +88 -0
  68. package/dist/fec-worker-setup.d.ts.map +1 -0
  69. package/dist/fec-worker-setup.js +614 -0
  70. package/dist/fec-worker-setup.js.map +7 -0
  71. package/dist/fmp4-completion.d.ts +30 -0
  72. package/dist/fmp4-completion.d.ts.map +1 -0
  73. package/dist/fmp4-completion.js +21 -0
  74. package/dist/fmp4-completion.js.map +7 -0
  75. package/dist/fmp4-reader.d.ts +37 -0
  76. package/dist/fmp4-reader.d.ts.map +1 -0
  77. package/dist/fmp4-reader.js +100 -0
  78. package/dist/fmp4-reader.js.map +7 -0
  79. package/dist/index.d.ts +47 -0
  80. package/dist/index.d.ts.map +1 -0
  81. package/dist/index.js +380 -0
  82. package/dist/index.js.map +7 -0
  83. package/dist/iso-bmff-timing.d.ts +78 -0
  84. package/dist/iso-bmff-timing.d.ts.map +1 -0
  85. package/dist/iso-bmff-timing.js +618 -0
  86. package/dist/iso-bmff-timing.js.map +7 -0
  87. package/dist/loc.d.ts +68 -0
  88. package/dist/loc.d.ts.map +1 -0
  89. package/dist/loc.js +143 -0
  90. package/dist/loc.js.map +7 -0
  91. package/dist/manifest-auth-plan.d.ts +30 -0
  92. package/dist/manifest-auth-plan.d.ts.map +1 -0
  93. package/dist/manifest-auth-plan.js +1285 -0
  94. package/dist/manifest-auth-plan.js.map +7 -0
  95. package/dist/media-router.d.ts +251 -0
  96. package/dist/media-router.d.ts.map +1 -0
  97. package/dist/media-router.js +624 -0
  98. package/dist/media-router.js.map +7 -0
  99. package/dist/mfu-reassembler.d.ts +396 -0
  100. package/dist/mfu-reassembler.d.ts.map +1 -0
  101. package/dist/mfu-reassembler.js +3085 -0
  102. package/dist/mfu-reassembler.js.map +7 -0
  103. package/dist/mfu-reassembly-policy.d.ts +24 -0
  104. package/dist/mfu-reassembly-policy.d.ts.map +1 -0
  105. package/dist/mfu-reassembly-policy.js +99 -0
  106. package/dist/mfu-reassembly-policy.js.map +7 -0
  107. package/dist/mmtp-container-parser.d.ts +434 -0
  108. package/dist/mmtp-container-parser.d.ts.map +1 -0
  109. package/dist/mmtp-container-parser.js +6164 -0
  110. package/dist/mmtp-container-parser.js.map +7 -0
  111. package/dist/mmtp-container-parser.no-modules.js +8999 -0
  112. package/dist/mmtp-container-parser.no-modules.js.map +7 -0
  113. package/dist/mmtp-decode-pipeline.d.ts +132 -0
  114. package/dist/mmtp-decode-pipeline.d.ts.map +1 -0
  115. package/dist/mmtp-decode-pipeline.js +6413 -0
  116. package/dist/mmtp-decode-pipeline.js.map +7 -0
  117. package/dist/mmtp-router.d.ts +390 -0
  118. package/dist/mmtp-router.d.ts.map +1 -0
  119. package/dist/mmtp-router.js +9242 -0
  120. package/dist/mmtp-router.js.map +7 -0
  121. package/dist/mmtp-to-cmaf.d.ts +181 -0
  122. package/dist/mmtp-to-cmaf.d.ts.map +1 -0
  123. package/dist/mmtp-to-cmaf.js +802 -0
  124. package/dist/mmtp-to-cmaf.js.map +7 -0
  125. package/dist/multicast-auth-catalog.d.ts +115 -0
  126. package/dist/multicast-auth-catalog.d.ts.map +1 -0
  127. package/dist/multicast-auth-catalog.js +202 -0
  128. package/dist/multicast-auth-catalog.js.map +7 -0
  129. package/dist/multicast-input.d.ts +107 -0
  130. package/dist/multicast-input.d.ts.map +1 -0
  131. package/dist/multicast-input.js +256 -0
  132. package/dist/multicast-input.js.map +7 -0
  133. package/dist/multicast-manager.d.ts +369 -0
  134. package/dist/multicast-manager.d.ts.map +1 -0
  135. package/dist/multicast-manager.js +13400 -0
  136. package/dist/multicast-manager.js.map +7 -0
  137. package/dist/multicast-transport.d.ts +61 -0
  138. package/dist/multicast-transport.d.ts.map +1 -0
  139. package/dist/multicast-transport.js +1665 -0
  140. package/dist/multicast-transport.js.map +7 -0
  141. package/dist/multicast-unwrap.d.ts +57 -0
  142. package/dist/multicast-unwrap.d.ts.map +1 -0
  143. package/dist/multicast-unwrap.js +384 -0
  144. package/dist/multicast-unwrap.js.map +7 -0
  145. package/dist/no-modules.d.ts +9 -0
  146. package/dist/no-modules.d.ts.map +1 -0
  147. package/dist/no-modules.js +8965 -0
  148. package/dist/no-modules.js.map +7 -0
  149. package/dist/phase14-catalog-override.d.ts +126 -0
  150. package/dist/phase14-catalog-override.d.ts.map +1 -0
  151. package/dist/phase14-catalog-override.js +155 -0
  152. package/dist/phase14-catalog-override.js.map +7 -0
  153. package/dist/repair-fetch.d.ts +58 -0
  154. package/dist/repair-fetch.d.ts.map +1 -0
  155. package/dist/repair-fetch.js +127 -0
  156. package/dist/repair-fetch.js.map +7 -0
  157. package/dist/repair-layers.d.ts +141 -0
  158. package/dist/repair-layers.d.ts.map +1 -0
  159. package/dist/repair-layers.js +303 -0
  160. package/dist/repair-layers.js.map +7 -0
  161. package/dist/sample-combiner.d.ts +212 -0
  162. package/dist/sample-combiner.d.ts.map +1 -0
  163. package/dist/sample-combiner.js +616 -0
  164. package/dist/sample-combiner.js.map +7 -0
  165. package/dist/signaling.d.ts +29 -0
  166. package/dist/signaling.d.ts.map +1 -0
  167. package/dist/signaling.js +232 -0
  168. package/dist/signaling.js.map +7 -0
  169. package/dist/source-auth.d.ts +2 -0
  170. package/dist/source-auth.d.ts.map +1 -0
  171. package/dist/source-auth.js +1238 -0
  172. package/dist/source-auth.js.map +7 -0
  173. package/dist/switching-set-alignment.d.ts +61 -0
  174. package/dist/switching-set-alignment.d.ts.map +1 -0
  175. package/dist/switching-set-alignment.js +85 -0
  176. package/dist/switching-set-alignment.js.map +7 -0
  177. package/dist/timestamp-codec.d.ts +34 -0
  178. package/dist/timestamp-codec.d.ts.map +1 -0
  179. package/dist/timestamp-codec.js +456 -0
  180. package/dist/timestamp-codec.js.map +7 -0
  181. package/dist/track-pipeline-setup.d.ts +72 -0
  182. package/dist/track-pipeline-setup.d.ts.map +1 -0
  183. package/dist/track-pipeline-setup.js +306 -0
  184. package/dist/track-pipeline-setup.js.map +7 -0
  185. package/dist/track-pipeline.d.ts +121 -0
  186. package/dist/track-pipeline.d.ts.map +1 -0
  187. package/dist/track-pipeline.js +229 -0
  188. package/dist/track-pipeline.js.map +7 -0
  189. package/dist/track-session.d.ts +398 -0
  190. package/dist/track-session.d.ts.map +1 -0
  191. package/dist/track-session.js +767 -0
  192. package/dist/track-session.js.map +7 -0
  193. package/dist/transport-pool.d.ts +69 -0
  194. package/dist/transport-pool.d.ts.map +1 -0
  195. package/dist/transport-pool.js +88 -0
  196. package/dist/transport-pool.js.map +7 -0
  197. package/dist/wasm-mfu-reassembler.d.ts +92 -0
  198. package/dist/wasm-mfu-reassembler.d.ts.map +1 -0
  199. package/dist/wasm-mfu-reassembler.js +2139 -0
  200. package/dist/wasm-mfu-reassembler.js.map +7 -0
  201. package/dist/wasm-mmtp-container-parser.d.ts +10 -0
  202. package/dist/wasm-mmtp-container-parser.d.ts.map +1 -0
  203. package/dist/wasm-mmtp-container-parser.js +6418 -0
  204. package/dist/wasm-mmtp-container-parser.js.map +7 -0
  205. package/node_modules/@blockcast/fec-worker/README.md +213 -0
  206. package/node_modules/@blockcast/fec-worker/dist/.build-stamp +0 -0
  207. package/node_modules/@blockcast/fec-worker/dist/fec-worker-client.d.ts +87 -0
  208. package/node_modules/@blockcast/fec-worker/dist/fec-worker-client.d.ts.map +1 -0
  209. package/node_modules/@blockcast/fec-worker/dist/fec-worker-client.js +279 -0
  210. package/node_modules/@blockcast/fec-worker/dist/fec-worker-client.js.map +1 -0
  211. package/node_modules/@blockcast/fec-worker/dist/fec-worker-types.d.ts +252 -0
  212. package/node_modules/@blockcast/fec-worker/dist/fec-worker-types.d.ts.map +1 -0
  213. package/node_modules/@blockcast/fec-worker/dist/fec-worker-types.js +11 -0
  214. package/node_modules/@blockcast/fec-worker/dist/fec-worker-types.js.map +1 -0
  215. package/node_modules/@blockcast/fec-worker/dist/fec-worker.d.ts +14 -0
  216. package/node_modules/@blockcast/fec-worker/dist/fec-worker.d.ts.map +1 -0
  217. package/node_modules/@blockcast/fec-worker/dist/fec-worker.js +565 -0
  218. package/node_modules/@blockcast/fec-worker/dist/fec-worker.js.map +7 -0
  219. package/node_modules/@blockcast/fec-worker/dist/index.d.ts +10 -0
  220. package/node_modules/@blockcast/fec-worker/dist/index.d.ts.map +1 -0
  221. package/node_modules/@blockcast/fec-worker/dist/index.js +8 -0
  222. package/node_modules/@blockcast/fec-worker/dist/index.js.map +1 -0
  223. package/node_modules/@blockcast/fec-worker/dist/shred-fec-worker.d.ts +20 -0
  224. package/node_modules/@blockcast/fec-worker/dist/shred-fec-worker.d.ts.map +1 -0
  225. package/node_modules/@blockcast/fec-worker/dist/shred-fec-worker.js +349 -0
  226. package/node_modules/@blockcast/fec-worker/dist/shred-fec-worker.js.map +7 -0
  227. package/node_modules/@blockcast/fec-worker/dist/shred-worker-client.d.ts +104 -0
  228. package/node_modules/@blockcast/fec-worker/dist/shred-worker-client.d.ts.map +1 -0
  229. package/node_modules/@blockcast/fec-worker/dist/shred-worker-client.js +181 -0
  230. package/node_modules/@blockcast/fec-worker/dist/shred-worker-client.js.map +1 -0
  231. package/node_modules/@blockcast/fec-worker/dist/shred-worker-types.d.ts +179 -0
  232. package/node_modules/@blockcast/fec-worker/dist/shred-worker-types.d.ts.map +1 -0
  233. package/node_modules/@blockcast/fec-worker/dist/shred-worker-types.js +47 -0
  234. package/node_modules/@blockcast/fec-worker/dist/shred-worker-types.js.map +1 -0
  235. package/node_modules/@blockcast/fec-worker/dist/transfer.d.ts +9 -0
  236. package/node_modules/@blockcast/fec-worker/dist/transfer.d.ts.map +1 -0
  237. package/node_modules/@blockcast/fec-worker/dist/transfer.js +13 -0
  238. package/node_modules/@blockcast/fec-worker/dist/transfer.js.map +1 -0
  239. package/node_modules/@blockcast/fec-worker/package.json +59 -0
  240. package/node_modules/@blockcast/fec-worker/src/fec-worker-client.ts +319 -0
  241. package/node_modules/@blockcast/fec-worker/src/fec-worker-types.ts +268 -0
  242. package/node_modules/@blockcast/fec-worker/src/fec-worker.ts +1048 -0
  243. package/node_modules/@blockcast/fec-worker/src/index.ts +32 -0
  244. package/node_modules/@blockcast/fec-worker/src/shred-fec-worker.ts +558 -0
  245. package/node_modules/@blockcast/fec-worker/src/shred-worker-client.ts +222 -0
  246. package/node_modules/@blockcast/fec-worker/src/shred-worker-types.ts +194 -0
  247. package/node_modules/@blockcast/fec-worker/src/transfer.ts +12 -0
  248. package/node_modules/@blockcast/mmt-alta-parse/README.md +181 -0
  249. package/node_modules/@blockcast/mmt-alta-parse/dist/.build-stamp +0 -0
  250. package/node_modules/@blockcast/mmt-alta-parse/dist/index.d.ts +57 -0
  251. package/node_modules/@blockcast/mmt-alta-parse/dist/index.d.ts.map +1 -0
  252. package/node_modules/@blockcast/mmt-alta-parse/dist/index.js +115 -0
  253. package/node_modules/@blockcast/mmt-alta-parse/dist/index.js.map +1 -0
  254. package/node_modules/@blockcast/mmt-alta-parse/package.json +50 -0
  255. package/node_modules/@blockcast/mmt-alta-parse/src/index.ts +136 -0
  256. package/node_modules/@blockcast/mmt-base/README.md +120 -0
  257. package/node_modules/@blockcast/mmt-base/dist/.build-stamp +0 -0
  258. package/node_modules/@blockcast/mmt-base/dist/index.d.ts +2 -0
  259. package/node_modules/@blockcast/mmt-base/dist/index.d.ts.map +1 -0
  260. package/node_modules/@blockcast/mmt-base/dist/index.js +2 -0
  261. package/node_modules/@blockcast/mmt-base/dist/index.js.map +1 -0
  262. package/node_modules/@blockcast/mmt-base/dist/mmtp.d.ts +276 -0
  263. package/node_modules/@blockcast/mmt-base/dist/mmtp.d.ts.map +1 -0
  264. package/node_modules/@blockcast/mmt-base/dist/mmtp.js +645 -0
  265. package/node_modules/@blockcast/mmt-base/dist/mmtp.js.map +1 -0
  266. package/node_modules/@blockcast/mmt-base/package.json +49 -0
  267. package/node_modules/@blockcast/mmt-base/src/index.ts +1 -0
  268. package/node_modules/@blockcast/mmt-base/src/mmtp.ts +899 -0
  269. package/node_modules/@blockcast/mmt-fec/README.md +270 -0
  270. package/node_modules/@blockcast/mmt-fec/dist/.build-stamp +0 -0
  271. package/node_modules/@blockcast/mmt-fec/dist/block-manager.d.ts +82 -0
  272. package/node_modules/@blockcast/mmt-fec/dist/block-manager.d.ts.map +1 -0
  273. package/node_modules/@blockcast/mmt-fec/dist/block-manager.js +296 -0
  274. package/node_modules/@blockcast/mmt-fec/dist/block-manager.js.map +1 -0
  275. package/node_modules/@blockcast/mmt-fec/dist/block-processor.d.ts +72 -0
  276. package/node_modules/@blockcast/mmt-fec/dist/block-processor.d.ts.map +1 -0
  277. package/node_modules/@blockcast/mmt-fec/dist/block-processor.js +192 -0
  278. package/node_modules/@blockcast/mmt-fec/dist/block-processor.js.map +1 -0
  279. package/node_modules/@blockcast/mmt-fec/dist/cmaf-recovery.d.ts +46 -0
  280. package/node_modules/@blockcast/mmt-fec/dist/cmaf-recovery.d.ts.map +1 -0
  281. package/node_modules/@blockcast/mmt-fec/dist/cmaf-recovery.js +96 -0
  282. package/node_modules/@blockcast/mmt-fec/dist/cmaf-recovery.js.map +1 -0
  283. package/node_modules/@blockcast/mmt-fec/dist/cmaf-symbols.d.ts +33 -0
  284. package/node_modules/@blockcast/mmt-fec/dist/cmaf-symbols.d.ts.map +1 -0
  285. package/node_modules/@blockcast/mmt-fec/dist/cmaf-symbols.js +57 -0
  286. package/node_modules/@blockcast/mmt-fec/dist/cmaf-symbols.js.map +1 -0
  287. package/node_modules/@blockcast/mmt-fec/dist/fec-block-mapper.d.ts +185 -0
  288. package/node_modules/@blockcast/mmt-fec/dist/fec-block-mapper.d.ts.map +1 -0
  289. package/node_modules/@blockcast/mmt-fec/dist/fec-block-mapper.js +273 -0
  290. package/node_modules/@blockcast/mmt-fec/dist/fec-block-mapper.js.map +1 -0
  291. package/node_modules/@blockcast/mmt-fec/dist/index.d.ts +22 -0
  292. package/node_modules/@blockcast/mmt-fec/dist/index.d.ts.map +1 -0
  293. package/node_modules/@blockcast/mmt-fec/dist/index.js +22 -0
  294. package/node_modules/@blockcast/mmt-fec/dist/index.js.map +1 -0
  295. package/node_modules/@blockcast/mmt-fec/dist/loc-symbols.d.ts +20 -0
  296. package/node_modules/@blockcast/mmt-fec/dist/loc-symbols.d.ts.map +1 -0
  297. package/node_modules/@blockcast/mmt-fec/dist/loc-symbols.js +28 -0
  298. package/node_modules/@blockcast/mmt-fec/dist/loc-symbols.js.map +1 -0
  299. package/node_modules/@blockcast/mmt-fec/dist/oti.d.ts +172 -0
  300. package/node_modules/@blockcast/mmt-fec/dist/oti.d.ts.map +1 -0
  301. package/node_modules/@blockcast/mmt-fec/dist/oti.js +254 -0
  302. package/node_modules/@blockcast/mmt-fec/dist/oti.js.map +1 -0
  303. package/node_modules/@blockcast/mmt-fec/dist/reorder-buffer.d.ts +41 -0
  304. package/node_modules/@blockcast/mmt-fec/dist/reorder-buffer.d.ts.map +1 -0
  305. package/node_modules/@blockcast/mmt-fec/dist/reorder-buffer.js +81 -0
  306. package/node_modules/@blockcast/mmt-fec/dist/reorder-buffer.js.map +1 -0
  307. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-alta-parse/README.md +181 -0
  308. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-alta-parse/dist/.build-stamp +0 -0
  309. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-alta-parse/dist/index.d.ts +57 -0
  310. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-alta-parse/dist/index.d.ts.map +1 -0
  311. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-alta-parse/dist/index.js +115 -0
  312. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-alta-parse/dist/index.js.map +1 -0
  313. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-alta-parse/package.json +50 -0
  314. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-alta-parse/src/index.ts +136 -0
  315. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/README.md +120 -0
  316. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/.build-stamp +0 -0
  317. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/index.d.ts +2 -0
  318. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/index.d.ts.map +1 -0
  319. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/index.js +2 -0
  320. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/index.js.map +1 -0
  321. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/mmtp.d.ts +276 -0
  322. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/mmtp.d.ts.map +1 -0
  323. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/mmtp.js +645 -0
  324. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/mmtp.js.map +1 -0
  325. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/package.json +49 -0
  326. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/src/index.ts +1 -0
  327. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/src/mmtp.ts +899 -0
  328. package/node_modules/@blockcast/mmt-fec/package.json +79 -0
  329. package/node_modules/@blockcast/mmt-fec/src/block-manager.ts +371 -0
  330. package/node_modules/@blockcast/mmt-fec/src/block-processor.ts +277 -0
  331. package/node_modules/@blockcast/mmt-fec/src/cmaf-recovery.ts +127 -0
  332. package/node_modules/@blockcast/mmt-fec/src/cmaf-symbols.ts +68 -0
  333. package/node_modules/@blockcast/mmt-fec/src/fec-block-mapper.ts +332 -0
  334. package/node_modules/@blockcast/mmt-fec/src/index.ts +77 -0
  335. package/node_modules/@blockcast/mmt-fec/src/loc-symbols.ts +34 -0
  336. package/node_modules/@blockcast/mmt-fec/src/oti.ts +317 -0
  337. package/node_modules/@blockcast/mmt-fec/src/reorder-buffer.ts +101 -0
  338. package/node_modules/@blockcast/mmt-manifest-verify/dist/.build-stamp +0 -0
  339. package/node_modules/@blockcast/mmt-manifest-verify/dist/admission.d.ts +28 -0
  340. package/node_modules/@blockcast/mmt-manifest-verify/dist/admission.d.ts.map +1 -0
  341. package/node_modules/@blockcast/mmt-manifest-verify/dist/admission.js +128 -0
  342. package/node_modules/@blockcast/mmt-manifest-verify/dist/admission.js.map +1 -0
  343. package/node_modules/@blockcast/mmt-manifest-verify/dist/index.d.ts +4 -0
  344. package/node_modules/@blockcast/mmt-manifest-verify/dist/index.d.ts.map +1 -0
  345. package/node_modules/@blockcast/mmt-manifest-verify/dist/index.js +4 -0
  346. package/node_modules/@blockcast/mmt-manifest-verify/dist/index.js.map +1 -0
  347. package/node_modules/@blockcast/mmt-manifest-verify/dist/source-auth.d.ts +37 -0
  348. package/node_modules/@blockcast/mmt-manifest-verify/dist/source-auth.d.ts.map +1 -0
  349. package/node_modules/@blockcast/mmt-manifest-verify/dist/source-auth.js +179 -0
  350. package/node_modules/@blockcast/mmt-manifest-verify/dist/source-auth.js.map +1 -0
  351. package/node_modules/@blockcast/mmt-manifest-verify/dist/verifier.d.ts +99 -0
  352. package/node_modules/@blockcast/mmt-manifest-verify/dist/verifier.d.ts.map +1 -0
  353. package/node_modules/@blockcast/mmt-manifest-verify/dist/verifier.js +492 -0
  354. package/node_modules/@blockcast/mmt-manifest-verify/dist/verifier.js.map +1 -0
  355. package/node_modules/@blockcast/mmt-manifest-verify/package.json +45 -0
  356. package/node_modules/@blockcast/mmt-manifest-verify/src/admission.ts +171 -0
  357. package/node_modules/@blockcast/mmt-manifest-verify/src/index.ts +3 -0
  358. package/node_modules/@blockcast/mmt-manifest-verify/src/source-auth.ts +230 -0
  359. package/node_modules/@blockcast/mmt-manifest-verify/src/verifier.ts +669 -0
  360. package/node_modules/@noble/hashes/LICENSE +21 -0
  361. package/node_modules/@noble/hashes/README.md +521 -0
  362. package/node_modules/@noble/hashes/_assert.d.ts +17 -0
  363. package/node_modules/@noble/hashes/_assert.d.ts.map +1 -0
  364. package/node_modules/@noble/hashes/_assert.js +18 -0
  365. package/node_modules/@noble/hashes/_assert.js.map +1 -0
  366. package/node_modules/@noble/hashes/_blake.d.ts +14 -0
  367. package/node_modules/@noble/hashes/_blake.d.ts.map +1 -0
  368. package/node_modules/@noble/hashes/_blake.js +50 -0
  369. package/node_modules/@noble/hashes/_blake.js.map +1 -0
  370. package/node_modules/@noble/hashes/_md.d.ts +51 -0
  371. package/node_modules/@noble/hashes/_md.d.ts.map +1 -0
  372. package/node_modules/@noble/hashes/_md.js +162 -0
  373. package/node_modules/@noble/hashes/_md.js.map +1 -0
  374. package/node_modules/@noble/hashes/_u64.d.ts +55 -0
  375. package/node_modules/@noble/hashes/_u64.d.ts.map +1 -0
  376. package/node_modules/@noble/hashes/_u64.js +90 -0
  377. package/node_modules/@noble/hashes/_u64.js.map +1 -0
  378. package/node_modules/@noble/hashes/argon2.d.ts +32 -0
  379. package/node_modules/@noble/hashes/argon2.d.ts.map +1 -0
  380. package/node_modules/@noble/hashes/argon2.js +401 -0
  381. package/node_modules/@noble/hashes/argon2.js.map +1 -0
  382. package/node_modules/@noble/hashes/blake1.d.ts +106 -0
  383. package/node_modules/@noble/hashes/blake1.d.ts.map +1 -0
  384. package/node_modules/@noble/hashes/blake1.js +459 -0
  385. package/node_modules/@noble/hashes/blake1.js.map +1 -0
  386. package/node_modules/@noble/hashes/blake2.d.ts +116 -0
  387. package/node_modules/@noble/hashes/blake2.d.ts.map +1 -0
  388. package/node_modules/@noble/hashes/blake2.js +420 -0
  389. package/node_modules/@noble/hashes/blake2.js.map +1 -0
  390. package/node_modules/@noble/hashes/blake2b.d.ts +11 -0
  391. package/node_modules/@noble/hashes/blake2b.d.ts.map +1 -0
  392. package/node_modules/@noble/hashes/blake2b.js +14 -0
  393. package/node_modules/@noble/hashes/blake2b.js.map +1 -0
  394. package/node_modules/@noble/hashes/blake2s.d.ts +20 -0
  395. package/node_modules/@noble/hashes/blake2s.d.ts.map +1 -0
  396. package/node_modules/@noble/hashes/blake2s.js +24 -0
  397. package/node_modules/@noble/hashes/blake2s.js.map +1 -0
  398. package/node_modules/@noble/hashes/blake3.d.ts +54 -0
  399. package/node_modules/@noble/hashes/blake3.d.ts.map +1 -0
  400. package/node_modules/@noble/hashes/blake3.js +255 -0
  401. package/node_modules/@noble/hashes/blake3.js.map +1 -0
  402. package/node_modules/@noble/hashes/crypto.d.ts +2 -0
  403. package/node_modules/@noble/hashes/crypto.d.ts.map +1 -0
  404. package/node_modules/@noble/hashes/crypto.js +5 -0
  405. package/node_modules/@noble/hashes/crypto.js.map +1 -0
  406. package/node_modules/@noble/hashes/cryptoNode.d.ts +2 -0
  407. package/node_modules/@noble/hashes/cryptoNode.d.ts.map +1 -0
  408. package/node_modules/@noble/hashes/cryptoNode.js +18 -0
  409. package/node_modules/@noble/hashes/cryptoNode.js.map +1 -0
  410. package/node_modules/@noble/hashes/eskdf.d.ts +47 -0
  411. package/node_modules/@noble/hashes/eskdf.d.ts.map +1 -0
  412. package/node_modules/@noble/hashes/eskdf.js +166 -0
  413. package/node_modules/@noble/hashes/eskdf.js.map +1 -0
  414. package/node_modules/@noble/hashes/esm/_assert.d.ts +17 -0
  415. package/node_modules/@noble/hashes/esm/_assert.d.ts.map +1 -0
  416. package/node_modules/@noble/hashes/esm/_assert.js +15 -0
  417. package/node_modules/@noble/hashes/esm/_assert.js.map +1 -0
  418. package/node_modules/@noble/hashes/esm/_blake.d.ts +14 -0
  419. package/node_modules/@noble/hashes/esm/_blake.d.ts.map +1 -0
  420. package/node_modules/@noble/hashes/esm/_blake.js +45 -0
  421. package/node_modules/@noble/hashes/esm/_blake.js.map +1 -0
  422. package/node_modules/@noble/hashes/esm/_md.d.ts +51 -0
  423. package/node_modules/@noble/hashes/esm/_md.d.ts.map +1 -0
  424. package/node_modules/@noble/hashes/esm/_md.js +155 -0
  425. package/node_modules/@noble/hashes/esm/_md.js.map +1 -0
  426. package/node_modules/@noble/hashes/esm/_u64.d.ts +55 -0
  427. package/node_modules/@noble/hashes/esm/_u64.d.ts.map +1 -0
  428. package/node_modules/@noble/hashes/esm/_u64.js +67 -0
  429. package/node_modules/@noble/hashes/esm/_u64.js.map +1 -0
  430. package/node_modules/@noble/hashes/esm/argon2.d.ts +32 -0
  431. package/node_modules/@noble/hashes/esm/argon2.d.ts.map +1 -0
  432. package/node_modules/@noble/hashes/esm/argon2.js +392 -0
  433. package/node_modules/@noble/hashes/esm/argon2.js.map +1 -0
  434. package/node_modules/@noble/hashes/esm/blake1.d.ts +106 -0
  435. package/node_modules/@noble/hashes/esm/blake1.d.ts.map +1 -0
  436. package/node_modules/@noble/hashes/esm/blake1.js +452 -0
  437. package/node_modules/@noble/hashes/esm/blake1.js.map +1 -0
  438. package/node_modules/@noble/hashes/esm/blake2.d.ts +116 -0
  439. package/node_modules/@noble/hashes/esm/blake2.d.ts.map +1 -0
  440. package/node_modules/@noble/hashes/esm/blake2.js +413 -0
  441. package/node_modules/@noble/hashes/esm/blake2.js.map +1 -0
  442. package/node_modules/@noble/hashes/esm/blake2b.d.ts +11 -0
  443. package/node_modules/@noble/hashes/esm/blake2b.d.ts.map +1 -0
  444. package/node_modules/@noble/hashes/esm/blake2b.js +11 -0
  445. package/node_modules/@noble/hashes/esm/blake2b.js.map +1 -0
  446. package/node_modules/@noble/hashes/esm/blake2s.d.ts +20 -0
  447. package/node_modules/@noble/hashes/esm/blake2s.d.ts.map +1 -0
  448. package/node_modules/@noble/hashes/esm/blake2s.js +21 -0
  449. package/node_modules/@noble/hashes/esm/blake2s.js.map +1 -0
  450. package/node_modules/@noble/hashes/esm/blake3.d.ts +54 -0
  451. package/node_modules/@noble/hashes/esm/blake3.d.ts.map +1 -0
  452. package/node_modules/@noble/hashes/esm/blake3.js +251 -0
  453. package/node_modules/@noble/hashes/esm/blake3.js.map +1 -0
  454. package/node_modules/@noble/hashes/esm/crypto.d.ts +2 -0
  455. package/node_modules/@noble/hashes/esm/crypto.d.ts.map +1 -0
  456. package/node_modules/@noble/hashes/esm/crypto.js +2 -0
  457. package/node_modules/@noble/hashes/esm/crypto.js.map +1 -0
  458. package/node_modules/@noble/hashes/esm/cryptoNode.d.ts +2 -0
  459. package/node_modules/@noble/hashes/esm/cryptoNode.d.ts.map +1 -0
  460. package/node_modules/@noble/hashes/esm/cryptoNode.js +15 -0
  461. package/node_modules/@noble/hashes/esm/cryptoNode.js.map +1 -0
  462. package/node_modules/@noble/hashes/esm/eskdf.d.ts +47 -0
  463. package/node_modules/@noble/hashes/esm/eskdf.d.ts.map +1 -0
  464. package/node_modules/@noble/hashes/esm/eskdf.js +160 -0
  465. package/node_modules/@noble/hashes/esm/eskdf.js.map +1 -0
  466. package/node_modules/@noble/hashes/esm/hkdf.d.ts +36 -0
  467. package/node_modules/@noble/hashes/esm/hkdf.d.ts.map +1 -0
  468. package/node_modules/@noble/hashes/esm/hkdf.js +82 -0
  469. package/node_modules/@noble/hashes/esm/hkdf.js.map +1 -0
  470. package/node_modules/@noble/hashes/esm/hmac.d.ts +35 -0
  471. package/node_modules/@noble/hashes/esm/hmac.d.ts.map +1 -0
  472. package/node_modules/@noble/hashes/esm/hmac.js +86 -0
  473. package/node_modules/@noble/hashes/esm/hmac.js.map +1 -0
  474. package/node_modules/@noble/hashes/esm/index.d.ts +2 -0
  475. package/node_modules/@noble/hashes/esm/index.d.ts.map +1 -0
  476. package/node_modules/@noble/hashes/esm/index.js +33 -0
  477. package/node_modules/@noble/hashes/esm/index.js.map +1 -0
  478. package/node_modules/@noble/hashes/esm/legacy.d.ts +71 -0
  479. package/node_modules/@noble/hashes/esm/legacy.d.ts.map +1 -0
  480. package/node_modules/@noble/hashes/esm/legacy.js +281 -0
  481. package/node_modules/@noble/hashes/esm/legacy.js.map +1 -0
  482. package/node_modules/@noble/hashes/esm/package.json +10 -0
  483. package/node_modules/@noble/hashes/esm/pbkdf2.d.ts +23 -0
  484. package/node_modules/@noble/hashes/esm/pbkdf2.d.ts.map +1 -0
  485. package/node_modules/@noble/hashes/esm/pbkdf2.js +97 -0
  486. package/node_modules/@noble/hashes/esm/pbkdf2.js.map +1 -0
  487. package/node_modules/@noble/hashes/esm/ripemd160.d.ts +13 -0
  488. package/node_modules/@noble/hashes/esm/ripemd160.d.ts.map +1 -0
  489. package/node_modules/@noble/hashes/esm/ripemd160.js +13 -0
  490. package/node_modules/@noble/hashes/esm/ripemd160.js.map +1 -0
  491. package/node_modules/@noble/hashes/esm/scrypt.d.ts +34 -0
  492. package/node_modules/@noble/hashes/esm/scrypt.d.ts.map +1 -0
  493. package/node_modules/@noble/hashes/esm/scrypt.js +228 -0
  494. package/node_modules/@noble/hashes/esm/scrypt.js.map +1 -0
  495. package/node_modules/@noble/hashes/esm/sha1.d.ts +11 -0
  496. package/node_modules/@noble/hashes/esm/sha1.d.ts.map +1 -0
  497. package/node_modules/@noble/hashes/esm/sha1.js +11 -0
  498. package/node_modules/@noble/hashes/esm/sha1.js.map +1 -0
  499. package/node_modules/@noble/hashes/esm/sha2.d.ts +159 -0
  500. package/node_modules/@noble/hashes/esm/sha2.d.ts.map +1 -0
  501. package/node_modules/@noble/hashes/esm/sha2.js +375 -0
  502. package/node_modules/@noble/hashes/esm/sha2.js.map +1 -0
  503. package/node_modules/@noble/hashes/esm/sha256.d.ts +20 -0
  504. package/node_modules/@noble/hashes/esm/sha256.d.ts.map +1 -0
  505. package/node_modules/@noble/hashes/esm/sha256.js +20 -0
  506. package/node_modules/@noble/hashes/esm/sha256.js.map +1 -0
  507. package/node_modules/@noble/hashes/esm/sha3-addons.d.ts +142 -0
  508. package/node_modules/@noble/hashes/esm/sha3-addons.d.ts.map +1 -0
  509. package/node_modules/@noble/hashes/esm/sha3-addons.js +393 -0
  510. package/node_modules/@noble/hashes/esm/sha3-addons.js.map +1 -0
  511. package/node_modules/@noble/hashes/esm/sha3.d.ts +53 -0
  512. package/node_modules/@noble/hashes/esm/sha3.d.ts.map +1 -0
  513. package/node_modules/@noble/hashes/esm/sha3.js +234 -0
  514. package/node_modules/@noble/hashes/esm/sha3.js.map +1 -0
  515. package/node_modules/@noble/hashes/esm/sha512.d.ts +26 -0
  516. package/node_modules/@noble/hashes/esm/sha512.d.ts.map +1 -0
  517. package/node_modules/@noble/hashes/esm/sha512.js +26 -0
  518. package/node_modules/@noble/hashes/esm/sha512.js.map +1 -0
  519. package/node_modules/@noble/hashes/esm/utils.d.ts +161 -0
  520. package/node_modules/@noble/hashes/esm/utils.d.ts.map +1 -0
  521. package/node_modules/@noble/hashes/esm/utils.js +281 -0
  522. package/node_modules/@noble/hashes/esm/utils.js.map +1 -0
  523. package/node_modules/@noble/hashes/hkdf.d.ts +36 -0
  524. package/node_modules/@noble/hashes/hkdf.d.ts.map +1 -0
  525. package/node_modules/@noble/hashes/hkdf.js +88 -0
  526. package/node_modules/@noble/hashes/hkdf.js.map +1 -0
  527. package/node_modules/@noble/hashes/hmac.d.ts +35 -0
  528. package/node_modules/@noble/hashes/hmac.d.ts.map +1 -0
  529. package/node_modules/@noble/hashes/hmac.js +91 -0
  530. package/node_modules/@noble/hashes/hmac.js.map +1 -0
  531. package/node_modules/@noble/hashes/index.d.ts +1 -0
  532. package/node_modules/@noble/hashes/index.d.ts.map +1 -0
  533. package/node_modules/@noble/hashes/index.js +33 -0
  534. package/node_modules/@noble/hashes/index.js.map +1 -0
  535. package/node_modules/@noble/hashes/legacy.d.ts +71 -0
  536. package/node_modules/@noble/hashes/legacy.d.ts.map +1 -0
  537. package/node_modules/@noble/hashes/legacy.js +287 -0
  538. package/node_modules/@noble/hashes/legacy.js.map +1 -0
  539. package/node_modules/@noble/hashes/package.json +266 -0
  540. package/node_modules/@noble/hashes/pbkdf2.d.ts +23 -0
  541. package/node_modules/@noble/hashes/pbkdf2.d.ts.map +1 -0
  542. package/node_modules/@noble/hashes/pbkdf2.js +101 -0
  543. package/node_modules/@noble/hashes/pbkdf2.js.map +1 -0
  544. package/node_modules/@noble/hashes/ripemd160.d.ts +13 -0
  545. package/node_modules/@noble/hashes/ripemd160.d.ts.map +1 -0
  546. package/node_modules/@noble/hashes/ripemd160.js +16 -0
  547. package/node_modules/@noble/hashes/ripemd160.js.map +1 -0
  548. package/node_modules/@noble/hashes/scrypt.d.ts +34 -0
  549. package/node_modules/@noble/hashes/scrypt.d.ts.map +1 -0
  550. package/node_modules/@noble/hashes/scrypt.js +232 -0
  551. package/node_modules/@noble/hashes/scrypt.js.map +1 -0
  552. package/node_modules/@noble/hashes/sha1.d.ts +11 -0
  553. package/node_modules/@noble/hashes/sha1.d.ts.map +1 -0
  554. package/node_modules/@noble/hashes/sha1.js +14 -0
  555. package/node_modules/@noble/hashes/sha1.js.map +1 -0
  556. package/node_modules/@noble/hashes/sha2.d.ts +159 -0
  557. package/node_modules/@noble/hashes/sha2.d.ts.map +1 -0
  558. package/node_modules/@noble/hashes/sha2.js +384 -0
  559. package/node_modules/@noble/hashes/sha2.js.map +1 -0
  560. package/node_modules/@noble/hashes/sha256.d.ts +20 -0
  561. package/node_modules/@noble/hashes/sha256.d.ts.map +1 -0
  562. package/node_modules/@noble/hashes/sha256.js +23 -0
  563. package/node_modules/@noble/hashes/sha256.js.map +1 -0
  564. package/node_modules/@noble/hashes/sha3-addons.d.ts +142 -0
  565. package/node_modules/@noble/hashes/sha3-addons.d.ts.map +1 -0
  566. package/node_modules/@noble/hashes/sha3-addons.js +402 -0
  567. package/node_modules/@noble/hashes/sha3-addons.js.map +1 -0
  568. package/node_modules/@noble/hashes/sha3.d.ts +53 -0
  569. package/node_modules/@noble/hashes/sha3.d.ts.map +1 -0
  570. package/node_modules/@noble/hashes/sha3.js +239 -0
  571. package/node_modules/@noble/hashes/sha3.js.map +1 -0
  572. package/node_modules/@noble/hashes/sha512.d.ts +26 -0
  573. package/node_modules/@noble/hashes/sha512.d.ts.map +1 -0
  574. package/node_modules/@noble/hashes/sha512.js +29 -0
  575. package/node_modules/@noble/hashes/sha512.js.map +1 -0
  576. package/node_modules/@noble/hashes/src/_assert.ts +22 -0
  577. package/node_modules/@noble/hashes/src/_blake.ts +50 -0
  578. package/node_modules/@noble/hashes/src/_md.ts +176 -0
  579. package/node_modules/@noble/hashes/src/_u64.ts +91 -0
  580. package/node_modules/@noble/hashes/src/argon2.ts +497 -0
  581. package/node_modules/@noble/hashes/src/blake1.ts +534 -0
  582. package/node_modules/@noble/hashes/src/blake2.ts +486 -0
  583. package/node_modules/@noble/hashes/src/blake2b.ts +10 -0
  584. package/node_modules/@noble/hashes/src/blake2s.ts +20 -0
  585. package/node_modules/@noble/hashes/src/blake3.ts +272 -0
  586. package/node_modules/@noble/hashes/src/crypto.ts +9 -0
  587. package/node_modules/@noble/hashes/src/cryptoNode.ts +15 -0
  588. package/node_modules/@noble/hashes/src/eskdf.ts +187 -0
  589. package/node_modules/@noble/hashes/src/hkdf.ts +88 -0
  590. package/node_modules/@noble/hashes/src/hmac.ts +94 -0
  591. package/node_modules/@noble/hashes/src/index.ts +31 -0
  592. package/node_modules/@noble/hashes/src/legacy.ts +293 -0
  593. package/node_modules/@noble/hashes/src/pbkdf2.ts +122 -0
  594. package/node_modules/@noble/hashes/src/ripemd160.ts +12 -0
  595. package/node_modules/@noble/hashes/src/scrypt.ts +257 -0
  596. package/node_modules/@noble/hashes/src/sha1.ts +10 -0
  597. package/node_modules/@noble/hashes/src/sha2.ts +402 -0
  598. package/node_modules/@noble/hashes/src/sha256.ts +24 -0
  599. package/node_modules/@noble/hashes/src/sha3-addons.ts +499 -0
  600. package/node_modules/@noble/hashes/src/sha3.ts +258 -0
  601. package/node_modules/@noble/hashes/src/sha512.ts +34 -0
  602. package/node_modules/@noble/hashes/src/utils.ts +395 -0
  603. package/node_modules/@noble/hashes/utils.d.ts +161 -0
  604. package/node_modules/@noble/hashes/utils.d.ts.map +1 -0
  605. package/node_modules/@noble/hashes/utils.js +313 -0
  606. package/node_modules/@noble/hashes/utils.js.map +1 -0
  607. package/package.json +203 -0
  608. package/schemas/catalog.schema.json +360 -0
  609. package/src/align.ts +239 -0
  610. package/src/audio-init-parser.ts +189 -0
  611. package/src/audio-ring-buffer.ts +120 -0
  612. package/src/authored-timing.ts +63 -0
  613. package/src/carp-catalog.ts +845 -0
  614. package/src/catalog-fec-params.ts +535 -0
  615. package/src/catalog-network-source.ts +49 -0
  616. package/src/catalog-validator.ts +34 -0
  617. package/src/cmaf-block-assembler.ts +1167 -0
  618. package/src/cmaf-group-batcher.ts +104 -0
  619. package/src/cmcd-producer.ts +334 -0
  620. package/src/codec-utils.ts +1611 -0
  621. package/src/fec-codec.ts +61 -0
  622. package/src/fec-manager.ts +2081 -0
  623. package/src/fec-starvation-detector.ts +128 -0
  624. package/src/fec-worker-setup.ts +213 -0
  625. package/src/fmp4-completion.ts +47 -0
  626. package/src/fmp4-reader.ts +112 -0
  627. package/src/index.ts +486 -0
  628. package/src/iso-bmff-timing.ts +813 -0
  629. package/src/loc.ts +181 -0
  630. package/src/manifest-auth-plan.ts +107 -0
  631. package/src/media-router.ts +795 -0
  632. package/src/mfu-reassembler.ts +3741 -0
  633. package/src/mfu-reassembly-policy.ts +134 -0
  634. package/src/mmtp-container-parser.ts +1836 -0
  635. package/src/mmtp-decode-pipeline.ts +389 -0
  636. package/src/mmtp-router.ts +3136 -0
  637. package/src/mmtp-to-cmaf.ts +1024 -0
  638. package/src/multicast-auth-catalog.ts +340 -0
  639. package/src/multicast-input.ts +439 -0
  640. package/src/multicast-manager.ts +1720 -0
  641. package/src/multicast-transport.ts +321 -0
  642. package/src/multicast-unwrap.ts +132 -0
  643. package/src/no-modules.ts +20 -0
  644. package/src/phase14-catalog-override.ts +336 -0
  645. package/src/repair-fetch.ts +238 -0
  646. package/src/repair-layers.ts +490 -0
  647. package/src/sample-combiner.ts +938 -0
  648. package/src/signaling.ts +78 -0
  649. package/src/source-auth.ts +1 -0
  650. package/src/switching-set-alignment.ts +109 -0
  651. package/src/timestamp-codec.ts +265 -0
  652. package/src/track-pipeline-setup.ts +189 -0
  653. package/src/track-pipeline.ts +332 -0
  654. package/src/track-session.ts +994 -0
  655. package/src/transport-pool.ts +128 -0
  656. package/src/wasm-mfu-reassembler.ts +389 -0
  657. package/src/wasm-mmtp-container-parser.ts +23 -0
  658. package/test-vectors/align.json +199 -0
  659. package/test-vectors/catalog/README.md +40 -0
  660. package/test-vectors/catalog/negative/fec-enhancement-repair-removed.json +27 -0
  661. package/test-vectors/catalog/negative/fec-repair-layer-geometry.json +32 -0
  662. package/test-vectors/catalog/negative/fec-repair-layer-missing-symbols.json +30 -0
  663. package/test-vectors/catalog/negative/fec-repair-multiple-depends.json +31 -0
  664. package/test-vectors/catalog/negative/fec-repair-overlay-with-layer.json +33 -0
  665. package/test-vectors/catalog/negative/fec-repair-priority-out-of-band.json +23 -0
  666. package/test-vectors/catalog/negative/fec-repair-source-symbols-without-scope.json +32 -0
  667. package/test-vectors/catalog/negative/legacy-selection-params.json +15 -0
  668. package/test-vectors/catalog/negative/media-track-repair-layer.json +16 -0
  669. package/test-vectors/catalog/negative/missing-mmtp-fields.json +12 -0
  670. package/test-vectors/catalog/negative/multicast-auth-padded-key.json +37 -0
  671. package/test-vectors/catalog/negative/multicast-auth-reused-track-format.json +53 -0
  672. package/test-vectors/catalog/negative/multicast-auth-trailing-pad-bits.json +37 -0
  673. package/test-vectors/catalog/negative/multicast-endpoint-missing-protocol-and-source.json +24 -0
  674. package/test-vectors/catalog/negative/network-source-object.json +25 -0
  675. package/test-vectors/catalog/negative/raptorq-source-symbols-over-max.json +23 -0
  676. package/test-vectors/catalog/negative/raptorq-unaligned-symbol.json +26 -0
  677. package/test-vectors/catalog/negative/repair-track-legacy-shape.json +19 -0
  678. package/test-vectors/catalog/negative/track-role-empty.json +16 -0
  679. package/test-vectors/catalog/negative/track-role-non-string.json +16 -0
  680. package/test-vectors/catalog/positive/fec-layered-repair.json +104 -0
  681. package/test-vectors/catalog/positive/fec-multicast.json +51 -0
  682. package/test-vectors/catalog/positive/flat-av.json +30 -0
  683. package/test-vectors/catalog/positive/multicast-auth-rotation.json +55 -0
  684. package/test-vectors/catalog/positive/ticks-and-mixed-timescales.json +39 -0
  685. package/test-vectors/catalog/positive/track-role.json +49 -0
  686. package/test-vectors/catalog/releases/moqcast-native-v16-v1/catalog.json +65 -0
  687. package/test-vectors/catalog/releases/moqcast-native-v16-v1.archive.sha256 +1 -0
  688. package/test-vectors/catalog-multicast-auth-v1.json +26 -0
  689. package/test-vectors/catalog-validation.json +148 -0
  690. package/test-vectors/catalog-validator-public-api.mjs +25 -0
  691. package/test-vectors/generate-source-auth-fixture.mjs +114 -0
  692. package/test-vectors/source-auth-v1.json +61 -0
  693. package/test-vectors/source-contiguity-v1.json +131 -0
  694. package/test-vectors/subsample-placement-v1.json +230 -0
@@ -0,0 +1,3136 @@
1
+ /**
2
+ * MMTP packet processing and frame routing layer.
3
+ *
4
+ * Delegates MMTP header parsing, MFU extraction, reassembly, and keyframe
5
+ * detection to MmtpContainerParser while owning FEC orchestration, sync
6
+ * gating, CMAF assembly, and callback routing.
7
+ *
8
+ * Extracted from MulticastManager — this module owns MMTP-layer concerns:
9
+ * FEC symbol feeding, sync gating, MSE segment assembly, and frame routing.
10
+ * It does NOT own network I/O, watchdog timers, or multicast join/leave.
11
+ *
12
+ * @packageDocumentation
13
+ */
14
+
15
+ import { parseMMTPHeader } from "@blockcast/mmt-base";
16
+ import { isMfuReassemblyError } from "./mfu-reassembler.js";
17
+ import type {
18
+ MfuReassemblerBackend,
19
+ MmtpSourcePath,
20
+ MmtpDeliveryProvenance,
21
+ } from "./mfu-reassembler.js";
22
+ import type { MfuReassemblyPolicy } from "./mfu-reassembly-policy.js";
23
+ import { mmtpTimestampToMicros } from "./codec-utils.js";
24
+ import { scaleAuthoredTiming, type AuthoredTiming } from "./authored-timing.js";
25
+ import {
26
+ processFecBlock,
27
+ processFecSymbol,
28
+ parseRepairPacket,
29
+ FecBlockMapper,
30
+ type ProcessFecBlockResult,
31
+ } from "@blockcast/mmt-fec";
32
+ import {
33
+ CmafBlockAssembler,
34
+ type BlockAssemblerConfig,
35
+ type CmafBlockSample,
36
+ } from "./cmaf-block-assembler.js";
37
+ import { MmtpContainerParser, isMfuKeyframeMetadata } from "./mmtp-container-parser.js";
38
+ import { type TrackRole } from "./carp-catalog.js";
39
+ import type {
40
+ ManifestAuthMode,
41
+ ManifestObjectIdentity,
42
+ ManifestTrackContext,
43
+ ManifestVerifier,
44
+ } from "@blockcast/mmt-manifest-verify";
45
+
46
+ // ─── Constants ─────────────────────────────────────────────────────
47
+
48
+ /**
49
+ * Keyframe loss timeout slack factor.
50
+ *
51
+ * Derivation: The keyframe loss timer must fire after one full keyframe group
52
+ * has been missed. Group duration must come from explicit catalog timing; FEC
53
+ * sourceSymbols is a block width, not a GOP length. Multicast jitter means the
54
+ * last packet of a group may arrive up to ~0.5 group durations late (network
55
+ * reordering + FEC recovery latency). The total timeout is therefore:
56
+ *
57
+ * timeout = group_duration * (1.0 + 0.5_jitter) = group_duration * 1.5
58
+ *
59
+ * The 0.5 jitter margin covers: (a) FEC interleave recovery window (up to D
60
+ * frames), (b) network reordering within one interleave depth, and (c) OS
61
+ * socket buffer draining latency. This is tight enough to detect loss within
62
+ * one missing group while avoiding false positives from normal jitter.
63
+ */
64
+ export const KEYFRAME_LOST_TIMEOUT_SLACK = 1.5;
65
+
66
+ function isRouterDebugEnabled(): boolean {
67
+ const globalDebug = (globalThis as { __MMTP_ROUTER_DEBUG?: unknown }).__MMTP_ROUTER_DEBUG;
68
+ if (globalDebug === true || globalDebug === "true" || globalDebug === "1") return true;
69
+ const env = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process
70
+ ?.env;
71
+ return env?.["MMTP_ROUTER_DEBUG"] === "1" || env?.["MMTP_ROUTER_DEBUG"] === "true";
72
+ }
73
+ function debugRouter(message: string): void {
74
+ if (isRouterDebugEnabled()) console.debug(message);
75
+ }
76
+
77
+ // ─── Types ──────────────────────────────────────────────────────────
78
+
79
+ export type RoutedFrameMeta = {
80
+ /** Authored ISO-BMFF presentation timestamp, in microseconds. */
81
+ timestamp: number;
82
+ /**
83
+ * Raw MMTP short-NTP header timestamp. This is transport timing metadata
84
+ * only; it is deliberately separate from the authored media timestamp.
85
+ */
86
+ mmtpTimestamp?: bigint;
87
+ keyframe: boolean;
88
+ /** Exact ISO-BMFF timing in the media track's native timescale. */
89
+ authoredTiming?: AuthoredTiming;
90
+ /** Transport that supplied the accepted FI=1 head (or complete FI=0 frame). */
91
+ source?: MmtpSourcePath;
92
+ /**
93
+ * Whole-frame delivery verdict folded across every contributing fragment.
94
+ *
95
+ * Distinct from `source` on purpose. `source` answers a routing question —
96
+ * which transport's copy is this — and deliberately lets the head fragment
97
+ * decide. This answers an evidence question: did every byte of this frame
98
+ * physically arrive over multicast? A frame can be routed as 'multicast'
99
+ * and still be 'mixed'.
100
+ */
101
+ deliveryProvenance?: MmtpDeliveryProvenance;
102
+ packetId?: number;
103
+ packetSequenceNumber?: number;
104
+ /** Every authoritative MMTP source-packet sequence that formed this frame. */
105
+ packetSequenceNumbers?: number[];
106
+ /** Subset of packetSequenceNumbers reconstructed by FEC. */
107
+ repairedPacketSequenceNumbers?: number[];
108
+ /** Native MMTP MPU group coordinate. */
109
+ mpuSequenceNumber?: number;
110
+ /**
111
+ * Timed-MFU sample identity `(movie_fragment_sequence_number, sample_number)`
112
+ * from the MFU data-unit header. Absent when the MFU header is untimed.
113
+ */
114
+ movieFragmentSequenceNumber?: number;
115
+ sampleNumber?: number;
116
+ /** Immutable catalog identity supplied by the owning ingress subscription. */
117
+ catalogTrackName?: string;
118
+ recovered?: boolean;
119
+ /**
120
+ * True when this frame opens playback after the router suppressed earlier
121
+ * video deltas while waiting for a sync keyframe. Downstream decoders can
122
+ * use this to treat the following dependency chain as provisional.
123
+ */
124
+ sourceDiscontinuity?: boolean;
125
+ };
126
+
127
+ export interface MmtpIngressContext {
128
+ tracks: readonly Readonly<{ name: string; packetId: number }>[];
129
+ }
130
+
131
+ function catalogTrackNameForPacket(
132
+ ingress: MmtpIngressContext | undefined,
133
+ packetId: number,
134
+ ): string | undefined {
135
+ if (!ingress) return undefined;
136
+ const names = new Set(
137
+ ingress.tracks
138
+ .filter((track) => track.packetId === packetId && track.name.length > 0)
139
+ .map((track) => track.name),
140
+ );
141
+ return names.size === 1 ? names.values().next().value : undefined;
142
+ }
143
+
144
+ function hasAmbiguousCatalogTrackName(
145
+ ingress: MmtpIngressContext | undefined,
146
+ packetId: number,
147
+ ): boolean {
148
+ if (!ingress) return false;
149
+ const names = new Set(
150
+ ingress.tracks
151
+ .filter((track) => track.packetId === packetId && track.name.length > 0)
152
+ .map((track) => track.name),
153
+ );
154
+ return names.size > 1;
155
+ }
156
+
157
+ function ingressHasPacketId(
158
+ ingress: MmtpIngressContext | undefined,
159
+ packetId: number,
160
+ ): boolean {
161
+ // Direct/legacy callers have no catalog-backed ingress policy to enforce.
162
+ // MulticastManager supplies a populated context whenever packetIds are selected.
163
+ return ingress === undefined || ingress.tracks.some((track) => track.packetId === packetId);
164
+ }
165
+
166
+ /** Callback for decoded video/audio frames. */
167
+ export type FrameCallback = (data: Uint8Array, meta: RoutedFrameMeta) => void;
168
+
169
+ /** Metadata accompanying an accepted raw MMTP source packet. */
170
+ export type RoutedPacketMeta = {
171
+ packetId: number;
172
+ packetSequenceNumber: number;
173
+ /** True when FEC reconstructed this source packet. */
174
+ recovered: boolean;
175
+ /** True when the packet has no trailing Source FEC Payload ID. */
176
+ skipFecPayloadId: boolean;
177
+ /** Immutable catalog identity supplied by the owning ingress subscription. */
178
+ catalogTrackName?: string;
179
+ /**
180
+ * Strict provenance for the fragments this packet produces. Set for
181
+ * FEC-recovered packets, which carry no `source` of their own but inherit
182
+ * their FEC block's symbol provenance.
183
+ */
184
+ deliveryProvenance?: MmtpDeliveryProvenance;
185
+ };
186
+
187
+ /** Callback for accepted raw MMTP source packets. */
188
+ export type TrackPacketCallback = (packet: Uint8Array, meta: RoutedPacketMeta) => void;
189
+
190
+ type PendingRecoveredVideoFrame = {
191
+ data: Uint8Array;
192
+ meta: RoutedFrameMeta;
193
+ };
194
+
195
+ type PendingRecoveredAudioFrame = {
196
+ data: Uint8Array;
197
+ meta: RoutedFrameMeta;
198
+ };
199
+
200
+ type DirectAudioCursor = {
201
+ timestamp: number;
202
+ sequenceNumber: number;
203
+ };
204
+
205
+ const DIRECT_RECOVERED_VIDEO_QUEUE_LIMIT = 512;
206
+ const DIRECT_RECOVERED_AUDIO_QUEUE_LIMIT = 512;
207
+
208
+ type ParserDiagnostics = {
209
+ packets: number;
210
+ frames: number;
211
+ initSegments: number;
212
+ noFragment: number;
213
+ fragments: number;
214
+ reassembled: number;
215
+ pendingBuffers: number;
216
+ pendingFragments: number;
217
+ pendingSamples?: unknown[];
218
+ lastPacketLength: number;
219
+ lastSequenceNumber: number;
220
+ lastFecSsId?: number;
221
+ lastFrameTimestamp?: number;
222
+ directAudioSourceEmitted: number;
223
+ directAudioSourceRegressions: number;
224
+ directAudioRecoveredQueued: number;
225
+ directAudioRecoveredDrained: number;
226
+ directAudioRecoveredStaleDropped: number;
227
+ };
228
+
229
+ /**
230
+ * FECManager interface — the contract MmtpRouter expects for FEC
231
+ * source/repair symbol processing and block lifecycle events.
232
+ *
233
+ * Concrete implementation: `FECManager` class in `./fec-manager.ts`.
234
+ * MmtpRouter only interacts with FEC through this interface.
235
+ */
236
+ export interface FECManagerLike {
237
+ /** Feed a source symbol to the FEC decoder for a given track.
238
+ * Returns recovered block bytes if recovery completes, else null.
239
+ *
240
+ * Optional `alta` carries the detached ALTA authenticator extracted from
241
+ * the MMTP header-extension trailer (08-07 Path 2) — present on signed
242
+ * source packets, absent on relay-reassembled / unsigned streams. When
243
+ * provided, the fec-worker runs verify_detached instead of counting the
244
+ * symbol as `altaPending`. */
245
+ feedSourceSymbol(
246
+ packetId: number,
247
+ ssId: number,
248
+ symbolData: Uint8Array,
249
+ alta?: { auth: Uint8Array; payloadEnd: number },
250
+ catalogTrackName?: string,
251
+ ): Uint8Array | null;
252
+
253
+ /** Feed a repair symbol to the FEC decoder for a given track.
254
+ * Returns recovered block bytes if recovery completes, else null. */
255
+ feedRepairSymbol(
256
+ packetId: number,
257
+ ssStart: number,
258
+ ssbLength: number,
259
+ rsId: number,
260
+ repairData: Uint8Array,
261
+ catalogTrackName?: string,
262
+ ): Uint8Array | null;
263
+
264
+ /** Record a transport symbol for first-arrival-wins dedup and path counters. */
265
+ feedSymbolFromSource(
266
+ packetId: number,
267
+ sbn: number,
268
+ esi: number,
269
+ data: Uint8Array,
270
+ source: "multicast" | "moq",
271
+ catalogTrackName?: string,
272
+ ): boolean;
273
+
274
+ /** Strict provenance for a block: 'multicast' only when no symbol arrived
275
+ * over MoQ. Recovered bytes inherit it, since a recovered symbol is a
276
+ * function of every symbol that entered the block. */
277
+ blockDeliveryProvenance?(
278
+ sbn: number,
279
+ packetId: number,
280
+ catalogTrackName?: string,
281
+ ): MmtpDeliveryProvenance;
282
+
283
+ /** Track a source symbol arrival for block completion counting.
284
+ * Refreshes the inactivity deadline and marks clean when K arrive. */
285
+ trackBlock(
286
+ sbn: number,
287
+ packetId: number,
288
+ catalogTrackName?: string,
289
+ esi?: number,
290
+ ): void;
291
+
292
+ /** Refresh the block inactivity deadline after accepted repair progress. */
293
+ noteBlockActivity?(sbn: number, packetId: number, catalogTrackName?: string): void;
294
+
295
+ /** Cancel block timeout (block completed via clean or FEC recovery). */
296
+ cancelBlockTimeout(sbn: number, packetId?: number, catalogTrackName?: string): void;
297
+
298
+ /** Check if a block is already completed (clean or recovered). */
299
+ isBlockCompleted(sbn: number, packetId?: number, catalogTrackName?: string): boolean;
300
+
301
+ /** Mark a block as completed externally (after routing recovered data). */
302
+ markBlockCompleted(sbn: number, packetId?: number, catalogTrackName?: string): void;
303
+
304
+ /** Increment blocksRecovered stat (called after routing recovery). */
305
+ countRecovery(packetId?: number): void;
306
+
307
+ /** FEC source symbol count K per block. */
308
+ readonly k: number;
309
+ kForPacket?(packetId: number): number;
310
+
311
+ /** FEC symbol size T in bytes (0 = unknown / no FEC). */
312
+ readonly symbolSize: number;
313
+ symbolSizeForPacket?(packetId: number): number;
314
+
315
+ /** Update framerate — used to refine FEC block timeout. */
316
+ setFramerate(fps: number): void;
317
+
318
+ /** Per-block state snapshot for FecBlockGrid visualization. */
319
+ getBlockStates(): Array<{
320
+ sbn: number;
321
+ packetId?: number;
322
+ state: "collecting" | "complete" | "failed";
323
+ sourceCount: number;
324
+ k: number;
325
+ progress: number;
326
+ }>;
327
+
328
+ /** FEC statistics snapshot. */
329
+ readonly fecStats: {
330
+ sourcePackets: number;
331
+ repairPackets: number;
332
+ blocksClean: number;
333
+ blocksRecovered: number;
334
+ blocksFailed: number;
335
+ greenFillFrames: number;
336
+ };
337
+
338
+ // ── Event hooks (set by MmtpRouter in constructor) ──
339
+
340
+ /** Called when a block receives its first source symbol. */
341
+ onBlockStarted:
342
+ | ((sbn: number, packetId: number, catalogTrackName?: string) => void)
343
+ | null;
344
+
345
+ /** Called when a block completes cleanly (all K sources arrived). */
346
+ onBlockComplete:
347
+ | ((sbn: number, packetId: number, catalogTrackName?: string) => void)
348
+ | null;
349
+
350
+ /** Called when a block times out (unrecoverable). */
351
+ onBlockFailed:
352
+ | ((sbn: number, packetId: number, catalogTrackName?: string) => void)
353
+ | null;
354
+
355
+ /** Dispose timers and state. */
356
+ dispose(): void;
357
+ }
358
+
359
+ // ─── MmtpRouter ─────────────────────────────────────────────────────
360
+
361
+ export interface ManifestIdentityResolutionInput {
362
+ packetId: number;
363
+ sbn: number;
364
+ esi: number;
365
+ catalogTrackName?: string;
366
+ recovered: boolean;
367
+ sourceSymbolFormat: "full-mmtp-packet" | "fec-padded-recovery";
368
+ }
369
+
370
+ export type ManifestIdentityResolver = (
371
+ input: ManifestIdentityResolutionInput,
372
+ ) => ManifestObjectIdentity | null;
373
+
374
+ export class MmtpRouter {
375
+ readonly #parser: MmtpContainerParser;
376
+ // Source AND FEC-recovered MFU fragments share ONE reassembler — the parser's
377
+ // (see #parser.reassembler). A keyframe split between source-received and
378
+ // FEC-recovered fragments must reassemble in a single buffer (BLO-8151).
379
+ readonly #fecManager: FECManagerLike | null;
380
+ #defaultFecMapper: FecBlockMapper | null;
381
+ #fecMappersByPacketId = new Map<number, FecBlockMapper>();
382
+ readonly #manifestVerifier: ManifestVerifier | null;
383
+ readonly #ownsManifestVerifier: boolean;
384
+ readonly #resolveManifestIdentity:
385
+ | ((
386
+ packetId: number,
387
+ sbn: number,
388
+ esi: number,
389
+ catalogTrackName?: string,
390
+ ) => ManifestObjectIdentity | null)
391
+ | null;
392
+ readonly #resolveManifestIdentityForRoute: ManifestIdentityResolver | null;
393
+ #manifestRecoveryQueues = new Map<
394
+ string,
395
+ Array<{
396
+ blockId: number;
397
+ result: ProcessFecBlockResult;
398
+ ownerPacketId: number;
399
+ catalogTrackName?: string;
400
+ }>
401
+ >();
402
+
403
+ // Temp state set by #processMMTP before calling parser.feed(),
404
+ // read by the parser's onFrame callback for CMAF assembly + dedup.
405
+ #currentFecInfo:
406
+ | { sbn: number; esi: number; oversized?: boolean }
407
+ | undefined;
408
+ #currentSeqNum = 0;
409
+
410
+ // Track type registry: which packetIds are video vs audio (from catalog)
411
+ #videoPacketIds = new Set<number>();
412
+ #audioPacketIds = new Set<number>();
413
+ #catalogTrackNamesByPacketId = new Map<number, Set<string>>();
414
+
415
+ // Per-track codec string for keyframe detection (e.g. 'avc1.42001e', 'av01.0.04M.08')
416
+ #trackCodecs = new Map<number, string>();
417
+
418
+ // Per-track frame callbacks keyed by packetId — used for ABR / multi-track routing
419
+ #trackCallbacks = new Map<
420
+ number,
421
+ FrameCallback
422
+ >();
423
+ #trackPacketCallbacks = new Map<number, TrackPacketCallback>();
424
+ // Cached resolved callbacks: per-track + legacy composite per packetId.
425
+ // Invalidated when any callback is registered (onTrackFrame, onVideoFrame, onAudioFrame).
426
+ #resolvedCallbacks = new Map<number, FrameCallback>();
427
+
428
+ // Per-ingress-stream sync flag: suppress direct frame emission until the first complete
429
+ // FEC block confirms we've joined mid-stream cleanly. Prevents corrupt partial
430
+ // frames from reaching the decoder on multicast mid-stream join. Catalog
431
+ // identity keeps renditions that reuse a packet ID from opening each other's gate.
432
+ #trackSynced = new Set<string>();
433
+ #droppedPreSyncVideo = new Set<string>();
434
+
435
+ // Dedup: packetId-scoped packetSequenceNumbers already received via direct source path.
436
+ // Recovered FEC blocks contain all K source symbols, so skip direct-path
437
+ // duplicates even when the source frame is still buffered by parser reordering.
438
+ #sourcePktSeqs = new Set<string>();
439
+ #routedPacketSeqs = new Set<string>();
440
+
441
+ // Live WebCodecs callbacks cannot accept late recovered inter-frame video.
442
+ // MSE recovery routes through CmafBlockAssembler and can reorder by block; the
443
+ // direct callback path has to preserve decode order before frames leave libmmt.
444
+ #latestVideoSeqByStream = new Map<string, number>();
445
+ #lastDirectSourceVideoTsByStream = new Map<string, number>();
446
+ #lastDirectEmittedVideoTsByStream = new Map<string, number>();
447
+ #pendingDirectRecoveredVideoByStream = new Map<string, Map<string, PendingRecoveredVideoFrame>>();
448
+ // AudioDecoder input must remain monotonic too. A repair that completes after
449
+ // the live source cursor passed its AAC access unit is useful for FEC
450
+ // accounting, but injecting it into the decoder can stall PCM output.
451
+ #lastDirectSourceAudioCursorByStream = new Map<string, DirectAudioCursor>();
452
+ #lastDirectEmittedAudioCursorByStream = new Map<string, DirectAudioCursor>();
453
+ #pendingDirectRecoveredAudioByStream = new Map<string, Map<string, PendingRecoveredAudioFrame>>();
454
+
455
+ // Per-track CMAF block assemblers for MSE mode. When active, frames are buffered per-SBN
456
+ // and emitted as multi-sample CMAF segments on block completion instead of per-frame.
457
+ #mseAssemblers = new Map<string, CmafBlockAssembler>();
458
+
459
+ // Callbacks (legacy — used when per-track callbacks are not registered)
460
+ #onVideoFrame: FrameCallback | null = null;
461
+ #onAudioFrame: FrameCallback | null = null;
462
+
463
+ // Init segment (ftyp+moov) from MMTP MPU metadata — needed for MSE SourceBuffer
464
+ #onInitSegment: ((data: Uint8Array) => void) | null = null;
465
+
466
+ // Per-track init segment callbacks keyed by packetId
467
+ #trackInitCallbacks = new Map<number, (data: Uint8Array) => void>();
468
+ // Track whether init has been delivered per physical stream — prevent duplicates
469
+ #trackInitDelivered = new Set<string>();
470
+
471
+ // FEC block lifecycle — backend decides failed handling: WebCodecs=freeze, MSE=green fill
472
+ #onBlockStarted: ((sbn: number, packetId: number) => void) | null = null;
473
+ /** Subscriber for unrecoverable FEC blocks. Mirrors #onBlockStarted so
474
+ * players can drive decoder recovery when a block times out; the internal
475
+ * MSE/delta-role handling below stays independent of this subscriber. */
476
+ #onBlockFailed:
477
+ | ((sbn: number, packetId: number, catalogTrackName?: string) => void)
478
+ | null = null;
479
+ // ── Phase 14: per-role packetId dispatch (D-26, D-27, D-28) ───────
480
+ /** Maps packetId → TrackRole. Default: 'single' when absent (legacy path). */
481
+ #packetIdRoles: Map<number, TrackRole> = new Map();
482
+ /** Explicit catalog ownership for dedicated repair-track packet IDs. */
483
+ #repairOwnersByPacketId = new Map<number, Set<number>>();
484
+ /** Callback fired when a delta-track FEC block completes or fails. */
485
+ #onDeltaBlockCompleteCb:
486
+ | ((packetId: number, failed: boolean) => void)
487
+ | null = null;
488
+
489
+ // Debug counters
490
+ #pidCounts: Record<number, number> = {};
491
+ #repairPidCounts: Record<number, number> = {};
492
+ #moqRepairPidCounts: Record<number, number> = {};
493
+ #moqRepairOwnerCounts: Record<number, number> = {};
494
+ #parserDiagnostics = new Map<string, ParserDiagnostics>();
495
+
496
+ // ── Keyframe loss detection state ───────────────────────────────
497
+ /** Catalog-derived keyframe loss timeout (ms). null = not configured (throws on video track registration). */
498
+ #keyframeLostTimeoutMs: number | null;
499
+ /** Active keyframe loss timer handle. Single timer per video track — cleared on each rapFlag and on dispose(). */
500
+ #keyframeLostTimer: ReturnType<typeof setTimeout> | null = null;
501
+ /** Monotonic counter incremented on each emitted video keyframe. Tracks current GOP position. */
502
+ #lastKeyframeGroupId = 0;
503
+ /** Callback fired when no complete keyframe is emitted within keyframeLostTimeoutMs. Argument: next expected groupId. */
504
+ #onKeyframeLost: ((expectedGroupId: number) => void) | null = null;
505
+
506
+ #disposed = false;
507
+
508
+ constructor(config?: {
509
+ fecManager?: FECManagerLike;
510
+ fecMapper?: FecBlockMapper;
511
+ /** MFU reassembly timeout (ms) for FEC recovery path. Derive from FEC interleaveDepthMs. */
512
+ mfuTimeoutMs?: number;
513
+ reassemblyPolicy?: MfuReassemblyPolicy;
514
+ reassembler?: MfuReassemblerBackend;
515
+ maxBufferedMfus?: number;
516
+ /** Catalog-derived keyframe loss timeout (ms). Derive from explicit
517
+ * keyframe/group duration; do not use FEC sourceSymbols as GOP length. */
518
+ keyframeLostTimeoutMs?: number;
519
+ manifestVerifier?: ManifestVerifier;
520
+ /** Existing behavior is router ownership; shared browser plans opt into caller ownership. */
521
+ manifestVerifierOwnership?: "caller" | "router";
522
+ resolveManifestIdentity?: (
523
+ packetId: number,
524
+ sbn: number,
525
+ esi: number,
526
+ catalogTrackName?: string,
527
+ ) => ManifestObjectIdentity | null;
528
+ resolveManifestIdentityForRoute?: ManifestIdentityResolver;
529
+ /** Internal terminal-failure sink owned by the composing transport. */
530
+ onBlockFailed?: (
531
+ sbn: number,
532
+ packetId: number,
533
+ catalogTrackName?: string,
534
+ ) => void;
535
+ }) {
536
+ this.#keyframeLostTimeoutMs = config?.keyframeLostTimeoutMs ?? null;
537
+ this.#fecManager = config?.fecManager ?? null;
538
+ this.#defaultFecMapper = config?.fecMapper ?? null;
539
+ this.#manifestVerifier = config?.manifestVerifier ?? null;
540
+ this.#ownsManifestVerifier = config?.manifestVerifierOwnership !== "caller";
541
+ this.#resolveManifestIdentity = config?.resolveManifestIdentity ?? null;
542
+ this.#resolveManifestIdentityForRoute = config?.resolveManifestIdentityForRoute ?? null;
543
+
544
+ // Create MMTP parser — handles MFU extraction, reassembly, keyframe detection.
545
+ // Its MFUReassembler is the single reassembler for BOTH the source path
546
+ // (parser.feed) and the FEC-recovery path (#routeRecoveredBlock), so a frame
547
+ // split across both completes (BLO-8151). mfuTimeoutMs threads the
548
+ // catalog-derived timeout through.
549
+ this.#parser = new MmtpContainerParser({
550
+ mfuTimeoutMs: config?.mfuTimeoutMs,
551
+ reassemblyPolicy: config?.reassemblyPolicy,
552
+ reassembler: config?.reassembler,
553
+ maxBufferedMfus: config?.maxBufferedMfus,
554
+ });
555
+
556
+ // Wire parser init callback → router's init delivery (dedup).
557
+ this.#parser.onInit((data, packetId, catalogTrackName) => {
558
+ this.#diagFor(packetId, catalogTrackName).initSegments++;
559
+ this.#deliverInit(packetId, data, catalogTrackName);
560
+ });
561
+
562
+ // Wire parser frame callback → router's callback routing + CMAF assembly.
563
+ this.#parser.onFrame((data, meta) => {
564
+ const packetId = meta.packetId;
565
+ const diag = this.#diagFor(packetId, meta.catalogTrackName);
566
+ diag.frames++;
567
+ diag.lastFrameTimestamp = meta.timestamp;
568
+ const isVideo = this.#classifyPacketId(packetId) === true;
569
+ const frameMeta: RoutedFrameMeta = {
570
+ timestamp: meta.timestamp,
571
+ ...(meta.mmtpTimestamp === undefined
572
+ ? {}
573
+ : { mmtpTimestamp: meta.mmtpTimestamp }),
574
+ keyframe: isVideo ? meta.keyframe : true,
575
+ packetId,
576
+ packetSequenceNumber: meta.packetSequenceNumber ?? this.#currentSeqNum,
577
+ ...(meta.authoredTiming === undefined
578
+ ? {}
579
+ : { authoredTiming: meta.authoredTiming }),
580
+ ...(meta.packetSequenceNumbers === undefined
581
+ ? {}
582
+ : { packetSequenceNumbers: meta.packetSequenceNumbers }),
583
+ ...(meta.repairedPacketSequenceNumbers === undefined
584
+ ? {}
585
+ : { repairedPacketSequenceNumbers: meta.repairedPacketSequenceNumbers }),
586
+ ...(meta.mpuSequenceNumber === undefined
587
+ ? {}
588
+ : { mpuSequenceNumber: meta.mpuSequenceNumber }),
589
+ ...(meta.movieFragmentSequenceNumber === undefined
590
+ ? {}
591
+ : { movieFragmentSequenceNumber: meta.movieFragmentSequenceNumber }),
592
+ ...(meta.sampleNumber === undefined
593
+ ? {}
594
+ : { sampleNumber: meta.sampleNumber }),
595
+ ...(meta.catalogTrackName === undefined
596
+ ? {}
597
+ : { catalogTrackName: meta.catalogTrackName }),
598
+ };
599
+ if (meta.source) frameMeta.source = meta.source;
600
+ // Forward whenever present, not when truthy: 'unknown' is a verdict the
601
+ // parser reached, not a value to drop.
602
+ if (meta.deliveryProvenance !== undefined)
603
+ frameMeta.deliveryProvenance = meta.deliveryProvenance;
604
+ if (meta.recovered) frameMeta.recovered = true;
605
+ const sourceDiscontinuity = this.#opensFecSync(
606
+ packetId,
607
+ frameMeta.keyframe,
608
+ frameMeta.catalogTrackName,
609
+ );
610
+
611
+ // Route to MSE assembler if active for this track.
612
+ const assembler = this.#mseAssemblerFor(packetId, frameMeta.catalogTrackName);
613
+ if (assembler) {
614
+ const fecInfo = this.#currentFecInfo;
615
+ if (fecInfo && !fecInfo.oversized) {
616
+ assembler.addFrame(
617
+ fecInfo.sbn,
618
+ fecInfo.esi,
619
+ data,
620
+ frameMeta.timestamp,
621
+ frameMeta.keyframe,
622
+ frameMeta.catalogTrackName,
623
+ frameMeta.mpuSequenceNumber,
624
+ frameMeta.packetSequenceNumber,
625
+ frameMeta.packetSequenceNumbers,
626
+ frameMeta.repairedPacketSequenceNumbers,
627
+ frameMeta.authoredTiming,
628
+ frameMeta.deliveryProvenance,
629
+ frameMeta.mmtpTimestamp,
630
+ frameMeta.movieFragmentSequenceNumber,
631
+ frameMeta.sampleNumber,
632
+ );
633
+ } else {
634
+ assembler.addBypassFrame(
635
+ data,
636
+ frameMeta.timestamp,
637
+ frameMeta.keyframe,
638
+ frameMeta.catalogTrackName,
639
+ frameMeta.mpuSequenceNumber,
640
+ frameMeta.packetSequenceNumber,
641
+ frameMeta.packetSequenceNumbers,
642
+ frameMeta.repairedPacketSequenceNumbers,
643
+ frameMeta.authoredTiming,
644
+ frameMeta.deliveryProvenance,
645
+ frameMeta.mmtpTimestamp,
646
+ frameMeta.movieFragmentSequenceNumber,
647
+ frameMeta.sampleNumber,
648
+ );
649
+ }
650
+ }
651
+
652
+ if (!this.#shouldEmitFrame(
653
+ packetId,
654
+ frameMeta.keyframe,
655
+ frameMeta.catalogTrackName,
656
+ )) return;
657
+ if (sourceDiscontinuity) frameMeta.sourceDiscontinuity = true;
658
+
659
+ this.#noteEmittedKeyframe(packetId, frameMeta.keyframe);
660
+
661
+ // The emitted-sequence high-water mark (#noteEmittedVideoSeq) is recorded
662
+ // at the actual emit inside #emitDirectVideo, not here — a recovered frame
663
+ // can be queued ahead of the source cursor, so it must track real emits.
664
+
665
+ // Route to per-track or legacy callback. Even when an MSE block-assembler
666
+ // is registered, this callback still fires — downstream consumers
667
+ // (`mc.onVideoFrame` → takeover detection, multicast frame counter,
668
+ // `confirmVideo`/`confirmAudio`) need every reassembled frame. The
669
+ // SourceBuffer-write side of this callback path is suppressed inside
670
+ // `MseBackend.feedVideo`/`feedAudio` when the assembler owns the track,
671
+ // which is configured via `MseBackend.setAssemblerOwns('video' | 'audio')`.
672
+ // Prevents the dual-CMAF-timeline collision (BLO-3035 layer 3).
673
+ const cb = this.#getFrameCallback(packetId);
674
+ if (isVideo && !assembler) {
675
+ if (frameMeta.recovered) {
676
+ this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb);
677
+ } else {
678
+ this.#routeDirectSourceVideo(packetId, data, frameMeta, cb);
679
+ }
680
+ } else if (!isVideo && !assembler) {
681
+ if (frameMeta.recovered) {
682
+ this.#routeDirectRecoveredAudio(packetId, data, frameMeta, cb);
683
+ } else {
684
+ this.#routeDirectSourceAudio(packetId, data, frameMeta, cb);
685
+ }
686
+ } else {
687
+ cb?.(data, frameMeta);
688
+ }
689
+ });
690
+
691
+ // Wire up FECManager block lifecycle events.
692
+ // NOTE: Recovery routing is handled from feedSourceSymbol/feedRepairSymbol
693
+ // return values in #processMMTP — no onBlockRecovered event needed.
694
+ if (this.#fecManager) {
695
+ this.#fecManager.onBlockStarted = (
696
+ sbn: number,
697
+ packetId: number,
698
+ _catalogTrackName?: string,
699
+ ) => {
700
+ this.#onBlockStarted?.(sbn, packetId);
701
+ };
702
+
703
+ this.#fecManager.onBlockComplete = (
704
+ sbn: number,
705
+ packetId: number,
706
+ catalogTrackName?: string,
707
+ ) => {
708
+ this.#mseAssemblerFor(packetId, catalogTrackName)?.completeBlock(sbn);
709
+ // Phase 14 D-26: fan-out to delta callback when delta-role packetId completes.
710
+ if (
711
+ this.#isCatalogTrackUnambiguous(packetId, catalogTrackName) &&
712
+ this.#packetIdRoles.get(packetId) === "delta"
713
+ ) {
714
+ this.#onDeltaBlockCompleteCb?.(packetId, false); // failed=false
715
+ }
716
+ };
717
+
718
+ this.#fecManager.onBlockFailed = (
719
+ sbn: number,
720
+ packetId: number,
721
+ catalogTrackName?: string,
722
+ ) => {
723
+ this.#mseAssemblerFor(packetId, catalogTrackName)?.failBlock(sbn);
724
+ if (
725
+ this.#isCatalogTrackUnambiguous(packetId, catalogTrackName) &&
726
+ this.#packetIdRoles.get(packetId) === "delta"
727
+ ) {
728
+ this.#onDeltaBlockCompleteCb?.(packetId, true); // failed=true
729
+ }
730
+ config?.onBlockFailed?.(sbn, packetId, catalogTrackName);
731
+ this.#onBlockFailed?.(sbn, packetId, catalogTrackName);
732
+ };
733
+ }
734
+ }
735
+
736
+ // ── Accessors ───────────────────────────────────────────────────
737
+
738
+ get fecManager(): FECManagerLike | null {
739
+ return this.#fecManager;
740
+ }
741
+
742
+ get reassembler(): MfuReassemblerBackend {
743
+ return this.#parser.reassembler;
744
+ }
745
+
746
+ getDiagnostics(): Record<number, ParserDiagnostics> {
747
+ const out: Record<number, ParserDiagnostics> = {};
748
+ for (const [key, diag] of this.#parserDiagnostics) {
749
+ const packetId = this.#packetIdFromStreamKey(key);
750
+ const existing = out[packetId];
751
+ if (!existing) {
752
+ out[packetId] = { ...diag };
753
+ continue;
754
+ }
755
+ const latest =
756
+ diag.lastSequenceNumber >= existing.lastSequenceNumber ? diag : existing;
757
+ out[packetId] = {
758
+ packets: existing.packets + diag.packets,
759
+ frames: existing.frames + diag.frames,
760
+ initSegments: existing.initSegments + diag.initSegments,
761
+ noFragment: existing.noFragment + diag.noFragment,
762
+ fragments: existing.fragments + diag.fragments,
763
+ reassembled: existing.reassembled + diag.reassembled,
764
+ pendingBuffers: existing.pendingBuffers + diag.pendingBuffers,
765
+ pendingFragments: existing.pendingFragments + diag.pendingFragments,
766
+ directAudioSourceEmitted:
767
+ existing.directAudioSourceEmitted + diag.directAudioSourceEmitted,
768
+ directAudioSourceRegressions:
769
+ existing.directAudioSourceRegressions + diag.directAudioSourceRegressions,
770
+ directAudioRecoveredQueued:
771
+ existing.directAudioRecoveredQueued + diag.directAudioRecoveredQueued,
772
+ directAudioRecoveredDrained:
773
+ existing.directAudioRecoveredDrained + diag.directAudioRecoveredDrained,
774
+ directAudioRecoveredStaleDropped:
775
+ existing.directAudioRecoveredStaleDropped + diag.directAudioRecoveredStaleDropped,
776
+ pendingSamples: [
777
+ ...((existing.pendingSamples as unknown[]) ?? []),
778
+ ...((diag.pendingSamples as unknown[]) ?? []),
779
+ ],
780
+ lastPacketLength: latest.lastPacketLength,
781
+ lastSequenceNumber: latest.lastSequenceNumber,
782
+ ...(latest.lastFecSsId === undefined ? {} : { lastFecSsId: latest.lastFecSsId }),
783
+ ...(latest.lastFrameTimestamp === undefined
784
+ ? {}
785
+ : { lastFrameTimestamp: latest.lastFrameTimestamp }),
786
+ };
787
+ }
788
+ return out;
789
+ }
790
+
791
+ // ── Track configuration ─────────────────────────────────────────
792
+
793
+ /** Register callback for keyframe loss detection.
794
+ * Fires when no complete keyframe is emitted within keyframeLostTimeoutMs.
795
+ * Argument: next expected groupId (lastKeyframeGroupId + 1). */
796
+ onKeyframeLost(cb: (expectedGroupId: number) => void): void {
797
+ this.#onKeyframeLost = cb;
798
+ }
799
+
800
+ /**
801
+ * Set the keyframe loss timeout after construction.
802
+ * Must be called BEFORE setPacketIdType('video').
803
+ * Derive from explicit keyframe/group duration:
804
+ * Math.ceil(groupDurationMs * KEYFRAME_LOST_TIMEOUT_SLACK).
805
+ */
806
+ setKeyframeLostTimeoutMs(ms: number): void {
807
+ this.#keyframeLostTimeoutMs = ms;
808
+ }
809
+
810
+ /** Update MFU reassembly timeout. Source and FEC-recovery paths share the
811
+ * parser's single reassembler (BLO-8151), so one update covers both. */
812
+ setMfuTimeoutMs(ms: number): void {
813
+ if (ms <= 0) throw new Error("[MmtpRouter] mfuTimeoutMs must be > 0");
814
+ this.#parser.setMfuTimeoutMs(ms);
815
+ }
816
+
817
+ /** Update parser frame reordering for unordered multicast delivery.
818
+ * Callers derive this from the catalog FEC block span (§5.1 — the
819
+ * interleave value IS the span): positive round-half-up of span / frameMs. */
820
+ setReorderWindow(windowSize: number): void {
821
+ this.#parser.setReorderWindow(windowSize);
822
+ }
823
+
824
+ /** Register MMTP packetId → track type mapping.
825
+ * Call BEFORE packets arrive to ensure correct routing from the first packet.
826
+ * If keyframeLostTimeoutMs is not provided, Phase 13 keyframe repair is
827
+ * disabled (video still routes/decodes normally). framerate is OPTIONAL per
828
+ * draft-ietf-moq-catalogformat-01 §3.2.23, so we can't require the timeout. */
829
+ setPacketIdType(packetId: number, trackType: "video" | "audio"): void {
830
+ if (trackType === "video" && this.#keyframeLostTimeoutMs === null) {
831
+ console.warn(
832
+ "[MmtpRouter] keyframeLostTimeoutMs not configured — Phase 13 keyframe " +
833
+ "repair disabled for this track. Set it via setKeyframeLostTimeoutMs() " +
834
+ "or provide in constructor config to enable: " +
835
+ "Math.ceil(groupDurationMs * KEYFRAME_LOST_TIMEOUT_SLACK).",
836
+ );
837
+ }
838
+ if (trackType === "video") this.#videoPacketIds.add(packetId);
839
+ else this.#audioPacketIds.add(packetId);
840
+ // Sync track info to parser for codec-aware keyframe detection.
841
+ const codec = this.#trackCodecs.get(packetId) ?? "";
842
+ this.#parser.setTrack(packetId, { type: trackType, codec });
843
+ }
844
+
845
+ /** Register codec string for a packetId — enables codec-aware keyframe detection.
846
+ * Without this, only H.264 IDR (NAL type 5) is detected as keyframe fallback.
847
+ * AV1 (OBU_SEQUENCE_HEADER) and VP9 (frame_type bit) require the codec string. */
848
+ setTrackCodec(packetId: number, codec: string): void {
849
+ this.#trackCodecs.set(packetId, codec);
850
+ // Sync to parser. Determine track type from existing registration.
851
+ const trackType = this.#videoPacketIds.has(packetId)
852
+ ? ("video" as const)
853
+ : this.#audioPacketIds.has(packetId)
854
+ ? ("audio" as const)
855
+ : null;
856
+ if (trackType) {
857
+ this.#parser.setTrack(packetId, { type: trackType, codec });
858
+ }
859
+ }
860
+
861
+ // ── Frame callbacks ─────────────────────────────────────────────
862
+
863
+ /** @deprecated Use onTrackFrame(packetId, cb) for per-track routing */
864
+ onVideoFrame(cb: FrameCallback): void {
865
+ this.#onVideoFrame = cb;
866
+ this.#resolvedCallbacks.clear();
867
+ }
868
+
869
+ /** @deprecated Use onTrackFrame(packetId, cb) for per-track routing */
870
+ onAudioFrame(cb: FrameCallback): void {
871
+ this.#onAudioFrame = cb;
872
+ this.#resolvedCallbacks.clear();
873
+ }
874
+
875
+ /** Register a frame callback for a specific MMTP packetId (track).
876
+ * When a legacy callback (onVideoFrame/onAudioFrame) also exists for this
877
+ * packetId's type, both fire: per-track first, then legacy. This ensures
878
+ * element.ts wiring (confirmAudio, feedMulticast) continues working when
879
+ * TrackPipeline registers per-track sources. */
880
+ onTrackFrame(
881
+ packetId: number,
882
+ cb: FrameCallback,
883
+ ): void {
884
+ this.#trackCallbacks.set(packetId, cb);
885
+ this.#resolvedCallbacks.clear();
886
+ }
887
+
888
+ /** Register a raw MMTP source-packet callback for one track.
889
+ *
890
+ * Direct packets retain their original bytes, including a Source FEC Payload
891
+ * ID when present. FEC-recovered packets are the recovered source-symbol
892
+ * bytes and set `skipFecPayloadId` because that trailer is not part of the
893
+ * protected symbol. Repair packets are never delivered here.
894
+ */
895
+ onTrackPacket(packetId: number, cb: TrackPacketCallback): void {
896
+ this.#trackPacketCallbacks.set(packetId, cb);
897
+ }
898
+
899
+ /** Register an init segment callback (ftyp+moov from MMTP MPU metadata). */
900
+ onInitSegment(cb: (data: Uint8Array) => void): void {
901
+ this.#onInitSegment = cb;
902
+ }
903
+
904
+ /** Register an init segment callback for a specific MMTP packetId.
905
+ * Called when FT=0 init arrives via direct source or FEC recovery. */
906
+ onTrackInit(packetId: number, cb: (data: Uint8Array) => void): void {
907
+ this.#trackInitCallbacks.set(packetId, cb);
908
+ }
909
+
910
+ // ── MSE integration ─────────────────────────────────────────────
911
+
912
+ /** Create a CMAF block assembler for MSE mode on a specific track.
913
+ * When active, source frames are buffered per FEC block and emitted as
914
+ * multi-sample CMAF segments instead of per-frame callbacks. */
915
+ enableMseAssembler(
916
+ packetId: number,
917
+ config: Omit<BlockAssemblerConfig, "k">,
918
+ catalogTrackName?: string,
919
+ ): CmafBlockAssembler {
920
+ const k = this.#kForPacket(packetId);
921
+ const assemblerKey = this.#mseAssemblerKey(packetId, catalogTrackName);
922
+ const previous = this.#mseAssemblers.get(assemblerKey);
923
+ if (previous) {
924
+ previous.flush();
925
+ previous.dispose();
926
+ }
927
+ const assembler = new CmafBlockAssembler({ ...config, k });
928
+ this.#mseAssemblers.set(assemblerKey, assembler);
929
+ console.log(
930
+ `[MmtpRouter] MSE assembler enabled for packetId=${packetId}${catalogTrackName === undefined ? "" : ` track=${catalogTrackName}`} (K=${k}, ${config.trackType})`,
931
+ );
932
+ return assembler;
933
+ }
934
+
935
+ /** Register a segment callback for a track's MSE assembler.
936
+ * Called with complete moof+mdat CMAF segments on block completion. */
937
+ onMseSegment(packetId: number, cb: (segment: Uint8Array) => void, catalogTrackName?: string): void {
938
+ const assembler = this.#mseAssemblers.get(this.#mseAssemblerKey(packetId, catalogTrackName));
939
+ if (assembler) assembler.onSegment = cb;
940
+ }
941
+
942
+ /** Register a raw-sample callback for a track's MSE assembler.
943
+ * Use this when the caller already owns the fMP4/MSE timeline and only wants
944
+ * FEC recovery, keyframe gating, and dedup from the assembler. */
945
+ onMseSample(packetId: number, cb: (sample: CmafBlockSample) => void, catalogTrackName?: string): void {
946
+ const assembler = this.#mseAssemblers.get(this.#mseAssemblerKey(packetId, catalogTrackName));
947
+ if (assembler) assembler.onSample = cb;
948
+ }
949
+
950
+ /** Register an init segment callback for a track's MSE assembler. */
951
+ onMseInitSegment(packetId: number, cb: (init: Uint8Array) => void, catalogTrackName?: string): void {
952
+ const assembler = this.#mseAssemblers.get(this.#mseAssemblerKey(packetId, catalogTrackName));
953
+ if (assembler) assembler.onInitSegment = cb;
954
+ }
955
+
956
+ /** Build and emit init segment for a track's MSE assembler from catalog config. */
957
+ emitMseInit(packetId: number, catalogTrackName?: string): void {
958
+ this.#mseAssemblers.get(this.#mseAssemblerKey(packetId, catalogTrackName))?.emitInit();
959
+ }
960
+
961
+ /** Mark a video track as synchronized by an external path.
962
+ * This opens the MMTP/FEC pre-sync gates without waiting for a multicast
963
+ * keyframe, for MSE timelines already seeded by MoQ with the same track. */
964
+ markExternalVideoSync(packetId?: number, catalogTrackName?: string): void {
965
+ const packetIds =
966
+ packetId === undefined
967
+ ? [...this.#videoPacketIds]
968
+ : this.#videoPacketIds.has(packetId)
969
+ ? [packetId]
970
+ : [];
971
+ for (const id of packetIds) {
972
+ const streamKey = this.#mediaStreamKey(id, catalogTrackName);
973
+ this.#trackSynced.add(streamKey);
974
+ this.#droppedPreSyncVideo.delete(streamKey);
975
+ this.#mseAssemblerFor(id, catalogTrackName)?.markVideoSynced();
976
+ }
977
+ }
978
+
979
+ // ── FEC event handler ───────────────────────────────────────────
980
+
981
+ /** Register callback for FEC blocks when the first source symbol arrives. */
982
+ onFecBlockStarted(cb: (sbn: number, packetId: number) => void): void {
983
+ this.#onBlockStarted = cb;
984
+ }
985
+
986
+ /** Register callback for FEC blocks that time out unrecoverably. Players use
987
+ * this to recover a decoder that was starved by the lost block. */
988
+ onFecBlockFailed(
989
+ cb: (sbn: number, packetId: number, catalogTrackName?: string) => void,
990
+ ): void {
991
+ this.#onBlockFailed = cb;
992
+ }
993
+
994
+ // ── Phase 14: per-role packetId dispatch (D-26, D-27, D-28) ────────
995
+
996
+ /**
997
+ * Register a TrackRole for a specific packetId.
998
+ *
999
+ * D-26: Enables per-role block-complete fanout — delta-role packetIds fire
1000
+ * onDeltaBlockComplete when their FEC block completes or fails.
1001
+ * Composite callback preserved: legacy onVideoFrame/onAudioFrame still fire
1002
+ * (MEMORY.md "prevents callback stealing").
1003
+ *
1004
+ * Valid roles: 'base' | 'delta' | 'repair' | 'single'.
1005
+ * Throws on unknown role (error on bad init, CLAUDE.md).
1006
+ */
1007
+ setPacketIdRole(packetId: number, role: TrackRole): void {
1008
+ const VALID_ROLES: readonly TrackRole[] = [
1009
+ "base",
1010
+ "delta",
1011
+ "repair",
1012
+ "single",
1013
+ ];
1014
+ if (!VALID_ROLES.includes(role)) {
1015
+ throw new Error(
1016
+ `[MmtpRouter] setPacketIdRole: unknown role '${role}'. Valid: ${VALID_ROLES.join(", ")}`,
1017
+ );
1018
+ }
1019
+ this.#packetIdRoles.set(packetId, role);
1020
+ }
1021
+
1022
+ /** Replace catalog-derived repair ownership before a catalog reconfiguration. */
1023
+ clearRepairPacketIdOwners(): void {
1024
+ this.#repairOwnersByPacketId.clear();
1025
+ }
1026
+
1027
+ /** Bind one dedicated repair-track PID to its authoritative media PID. */
1028
+ setRepairPacketIdOwner(repairPacketId: number, ownerPacketId: number): void {
1029
+ const owners = this.#repairOwnersByPacketId.get(repairPacketId) ?? new Set<number>();
1030
+ owners.add(ownerPacketId);
1031
+ this.#repairOwnersByPacketId.set(repairPacketId, owners);
1032
+ }
1033
+
1034
+ /**
1035
+ * Register callback fired when a delta-track FEC block completes or fails.
1036
+ *
1037
+ * @param cb - (packetId, failed) — packetId identifies the delta track,
1038
+ * failed=false means clean/recovered, failed=true means unrecoverable.
1039
+ *
1040
+ * Called from both onBlockComplete (failed=false) and onBlockFailed (failed=true)
1041
+ * for every packetId registered with role='delta' via setPacketIdRole().
1042
+ */
1043
+ onDeltaBlockComplete(cb: (packetId: number, failed: boolean) => void): void {
1044
+ this.#onDeltaBlockCompleteCb = cb;
1045
+ }
1046
+
1047
+ // ── Packet processing ───────────────────────────────────────────
1048
+
1049
+ /** Update framerate — delegates to FECManager for timeout refinement. */
1050
+ setFramerate(fps: number): void {
1051
+ this.#fecManager?.setFramerate(fps);
1052
+ }
1053
+
1054
+ /** Set FEC block mapper (created when FEC config becomes known via configureFec). */
1055
+ setFecMapper(mapper: FecBlockMapper, packetId?: number): void {
1056
+ if (packetId == null) {
1057
+ this.#defaultFecMapper = mapper;
1058
+ return;
1059
+ }
1060
+ this.#fecMappersByPacketId.set(packetId, mapper);
1061
+ }
1062
+
1063
+ registerManifestTrack(context: ManifestTrackContext): void {
1064
+ if (!this.#manifestVerifier) throw new Error("manifest verifier is not configured");
1065
+ this.#manifestVerifier.registerTrack(context);
1066
+ }
1067
+
1068
+ feedManifestFrame(trackName: string, frame: Uint8Array): void {
1069
+ this.#manifestVerifier?.ingestManifestFrame(trackName, frame);
1070
+ }
1071
+
1072
+ setManifestAuthMode(mode: ManifestAuthMode): void {
1073
+ this.#manifestVerifier?.setMode(mode);
1074
+ if (mode !== "enforce") {
1075
+ for (const queue of this.#manifestRecoveryQueues.values()) {
1076
+ for (const item of queue) {
1077
+ this.#routeProcessedFecSymbols(
1078
+ item.blockId,
1079
+ item.result,
1080
+ item.ownerPacketId,
1081
+ item.catalogTrackName,
1082
+ );
1083
+ }
1084
+ }
1085
+ this.#manifestRecoveryQueues.clear();
1086
+ }
1087
+ }
1088
+
1089
+ /**
1090
+ * Main entry point: feed a raw MMTP packet for processing.
1091
+ *
1092
+ * Source packets are played immediately AND fed to FECManager for recovery.
1093
+ * Repair packets are fed to FECManager only (no direct playback).
1094
+ */
1095
+ /**
1096
+ * Feed one MMTP source packet.
1097
+ *
1098
+ * `source` is REQUIRED. It used to default to "multicast", which meant any
1099
+ * caller that forgot it silently attributed its bytes to a multicast socket
1100
+ * they may never have crossed — and that attribution is what the physical-
1101
+ * delivery evidence path is built on. Absence is not multicast; say which.
1102
+ */
1103
+ feedPacket(
1104
+ packet: Uint8Array,
1105
+ source: "multicast" | "moq",
1106
+ manifestIdentity?: ManifestObjectIdentity,
1107
+ ingress?: MmtpIngressContext,
1108
+ ): void {
1109
+ if (this.#disposed) return;
1110
+ this.#processMMTP(packet, source, manifestIdentity, false, ingress);
1111
+ }
1112
+
1113
+ /**
1114
+ * Feed a repair packet from MoQ (reliable QUIC) — parses MMTP repair
1115
+ * header and delegates to FECManager for FEC recovery.
1116
+ */
1117
+ feedRepairPacket(
1118
+ packet: Uint8Array,
1119
+ ownerPacketIdHint?: number,
1120
+ catalogTrackName?: string,
1121
+ ): void {
1122
+ if (this.#disposed) return;
1123
+
1124
+ const parsed = parseRepairPacket(packet);
1125
+ if (!parsed) return;
1126
+
1127
+ // A dedicated repair track can carry a packet ID that differs from the
1128
+ // media packet ID. The subscribing track supplies the authoritative owner.
1129
+ const header = packet.length >= 4 ? parseMMTPHeader(packet) : null;
1130
+ const repairPacketId = header?.packetId;
1131
+ if (repairPacketId == null) return;
1132
+
1133
+ const hintedOwner =
1134
+ ownerPacketIdHint != null && this.#classifyPacketId(ownerPacketIdHint) !== null
1135
+ ? ownerPacketIdHint
1136
+ : null;
1137
+ // A MoQ repair subscription owns a media track explicitly. Dedicated
1138
+ // repair PIDs must carry that hint; only same-PID repair is self-describing.
1139
+ const ownerPacketId = hintedOwner ??
1140
+ (this.#classifyPacketId(repairPacketId) !== null ? repairPacketId : null);
1141
+ if (ownerPacketId === null) {
1142
+ debugRouter(
1143
+ `[MmtpRouter] Dropping MoQ repair packet pid=${repairPacketId}: owner hint is required`,
1144
+ );
1145
+ }
1146
+
1147
+ if (ownerPacketId != null && this.#fecManager) {
1148
+ this.#moqRepairPidCounts[repairPacketId] =
1149
+ (this.#moqRepairPidCounts[repairPacketId] || 0) + 1;
1150
+ this.#moqRepairOwnerCounts[ownerPacketId] =
1151
+ (this.#moqRepairOwnerCounts[ownerPacketId] || 0) + 1;
1152
+ const pidCount = this.#moqRepairPidCounts[repairPacketId]!;
1153
+ const ownerCount = this.#moqRepairOwnerCounts[ownerPacketId]!;
1154
+ if (pidCount <= 2 || pidCount % 500 === 0) {
1155
+ console.log(
1156
+ `[MOQ-REPAIR] pid=${repairPacketId} repairOwner=${ownerPacketId} pidCount=${pidCount} ownerCount=${ownerCount} ssbLength=${parsed.ssbLength} len=${packet.length}`,
1157
+ );
1158
+ }
1159
+
1160
+ const repairSbn = this.#deriveRepairSbn(parsed.ssStart, parsed.ssbLength, ownerPacketId);
1161
+ if (repairSbn === null) return;
1162
+ const repairEsi = this.#kForPacket(ownerPacketId) + parsed.rsId;
1163
+
1164
+ if (
1165
+ !this.#feedFecTransportSymbol(
1166
+ ownerPacketId,
1167
+ repairSbn,
1168
+ repairEsi,
1169
+ parsed.repairData,
1170
+ "moq",
1171
+ catalogTrackName,
1172
+ )
1173
+ ) return;
1174
+
1175
+ // NOTE: Do NOT call trackBlock for repair packets (same as multicast repair path).
1176
+ const recovered = catalogTrackName === undefined
1177
+ ? this.#fecManager.feedRepairSymbol(
1178
+ ownerPacketId,
1179
+ parsed.ssStart,
1180
+ parsed.ssbLength,
1181
+ parsed.rsId,
1182
+ parsed.repairData,
1183
+ )
1184
+ : this.#fecManager.feedRepairSymbol(
1185
+ ownerPacketId,
1186
+ parsed.ssStart,
1187
+ parsed.ssbLength,
1188
+ parsed.rsId,
1189
+ parsed.repairData,
1190
+ catalogTrackName,
1191
+ );
1192
+ if (recovered) {
1193
+ const alreadyCompleted = catalogTrackName === undefined
1194
+ ? this.#fecManager.isBlockCompleted(repairSbn, ownerPacketId)
1195
+ : this.#fecManager.isBlockCompleted(repairSbn, ownerPacketId, catalogTrackName);
1196
+ if (catalogTrackName === undefined) {
1197
+ this.#fecManager.cancelBlockTimeout(repairSbn, ownerPacketId);
1198
+ } else {
1199
+ this.#fecManager.cancelBlockTimeout(repairSbn, ownerPacketId, catalogTrackName);
1200
+ }
1201
+ if (!alreadyCompleted) {
1202
+ this.#fecManager.countRecovery(ownerPacketId);
1203
+ if (catalogTrackName === undefined) {
1204
+ this.#fecManager.markBlockCompleted(repairSbn, ownerPacketId);
1205
+ } else {
1206
+ this.#fecManager.markBlockCompleted(repairSbn, ownerPacketId, catalogTrackName);
1207
+ }
1208
+ this.#routeRecoveredBlock(
1209
+ repairSbn,
1210
+ recovered,
1211
+ ownerPacketId,
1212
+ catalogTrackName,
1213
+ );
1214
+ }
1215
+ }
1216
+ }
1217
+ }
1218
+
1219
+ // ── SBN/ESI derivation ──────────────────────────────────────────
1220
+
1221
+ #kForPacket(packetId?: number): number {
1222
+ // Same packet-scoped rule as #deriveSbnEsi: once the packet ID is known,
1223
+ // only its explicitly registered mapper may stand in for the FEC
1224
+ // manager's per-packet K — never the default single-track mapper.
1225
+ if (packetId != null)
1226
+ return (
1227
+ this.#fecManager?.kForPacket?.(packetId) ??
1228
+ this.#fecMappersByPacketId.get(packetId)?.k ??
1229
+ this.#fecManager?.k ??
1230
+ 4
1231
+ );
1232
+ return this.#defaultFecMapper?.k ?? this.#fecManager?.k ?? 4;
1233
+ }
1234
+
1235
+ #symbolSizeForPacket(packetId?: number): number {
1236
+ if (packetId != null) return this.#fecManager?.symbolSizeForPacket?.(packetId) ?? this.#fecManager?.symbolSize ?? 0;
1237
+ return this.#fecManager?.symbolSize ?? 0;
1238
+ }
1239
+
1240
+ /** Derive SBN/ESI from SS_ID using packet-scoped FEC geometry. */
1241
+ #deriveSbnEsi(ssId: number, packetId?: number): { sbn: number; esi: number } {
1242
+ // A default mapper describes legacy single-track geometry. Once a packet
1243
+ // ID is known, only its explicitly registered mapper may override the
1244
+ // FECManager's packet-scoped K. Falling back to the default video mapper
1245
+ // here maps audio source symbols with video K while repair SBNs correctly
1246
+ // use the audio SSB_length, splitting one block across two coordinates.
1247
+ const mapper = packetId == null
1248
+ ? this.#defaultFecMapper
1249
+ : this.#fecMappersByPacketId.get(packetId) ?? null;
1250
+ if (mapper) return mapper.fromSsId(ssId);
1251
+ const k = this.#kForPacket(packetId);
1252
+ return { sbn: Math.floor(ssId / k), esi: ssId % k };
1253
+ }
1254
+
1255
+ #deriveRepairSbn(
1256
+ ssStart: number,
1257
+ ssbLength: number,
1258
+ packetId?: number,
1259
+ ): number | null {
1260
+ const k = this.#kForPacket(packetId);
1261
+ if (ssbLength <= 0 || ssbLength > k || ssStart % k !== 0) return null;
1262
+ return Math.floor(ssStart / k);
1263
+ }
1264
+
1265
+ #resolveRepairOwnerPacketId(
1266
+ packetId: number,
1267
+ ingress?: MmtpIngressContext,
1268
+ ): number | null {
1269
+ if (this.#classifyPacketId(packetId) !== null) return packetId;
1270
+
1271
+ if (this.#packetIdRoles.get(packetId) !== "repair") {
1272
+ debugRouter(
1273
+ `[MmtpRouter] Dropping repair packet pid=${packetId}: physical PID is unregistered`,
1274
+ );
1275
+ return null;
1276
+ }
1277
+
1278
+ const registeredOwners = this.#repairOwnersByPacketId.get(packetId) ?? new Set<number>();
1279
+ const owners = ingress === undefined
1280
+ ? [...registeredOwners]
1281
+ : [...registeredOwners].filter((ownerPacketId) =>
1282
+ ingressHasPacketId(ingress, ownerPacketId));
1283
+ if (owners.length === 1) return owners[0]!;
1284
+
1285
+ debugRouter(
1286
+ `[MmtpRouter] Dropping repair packet pid=${packetId}: catalog owner is ${owners.length === 0 ? "absent" : "ambiguous"}`,
1287
+ );
1288
+ return null;
1289
+ }
1290
+
1291
+ /** Route a recovered FEC block through the parser + dedup pipeline.
1292
+ * Sync WASM decoders return K fixed-width source symbols concatenated. */
1293
+ routeRecoveredBlock(
1294
+ blockId: number,
1295
+ data: Uint8Array,
1296
+ packetId: number,
1297
+ catalogTrackName?: string,
1298
+ ): void {
1299
+ this.#routeRecoveredBlock(blockId, data, packetId, catalogTrackName);
1300
+ }
1301
+
1302
+ /** Route a single recovered source symbol through the parser + dedup pipeline.
1303
+ * Worker-backed decoders emit one padded source symbol per frame event. */
1304
+ routeRecoveredSymbol(
1305
+ blockId: number,
1306
+ data: Uint8Array,
1307
+ packetId: number,
1308
+ esi = 0,
1309
+ catalogTrackName?: string,
1310
+ ): void {
1311
+ const result = processFecSymbol(data, { ownerPacketId: packetId, index: esi });
1312
+ this.#routeVerifiedFecSymbols(blockId, result, packetId, catalogTrackName);
1313
+ }
1314
+
1315
+ // ── Keyframe timer cancellation ─────────────────────────────────
1316
+
1317
+ /** Cancel the active keyframe loss timer.
1318
+ * Call on failbackToMoq to prevent a stale timer from firing
1319
+ * onKeyframeLost after multicast is no longer the active source. */
1320
+ cancelKeyframeLostTimer(): void {
1321
+ if (this.#keyframeLostTimer) {
1322
+ clearTimeout(this.#keyframeLostTimer);
1323
+ this.#keyframeLostTimer = null;
1324
+ }
1325
+ }
1326
+
1327
+ // ── Dispose ─────────────────────────────────────────────────────
1328
+
1329
+ /** Emit parser frames buffered for a finite input before teardown. */
1330
+ flush(): void {
1331
+ if (this.#disposed) return;
1332
+ this.#parser.flush();
1333
+ }
1334
+
1335
+ /** Drop one rendition's parser/router/CMAF epoch without touching peers. */
1336
+ resetTrack(packetId: number, catalogTrackName: string): void {
1337
+ if (this.#disposed) return;
1338
+ this.#parser.resetTrack(packetId, catalogTrackName);
1339
+ const streamKey = this.#mediaStreamKey(packetId, catalogTrackName);
1340
+ this.#trackSynced.delete(streamKey);
1341
+ this.#droppedPreSyncVideo.delete(streamKey);
1342
+ this.#latestVideoSeqByStream.delete(streamKey);
1343
+ this.#lastDirectSourceVideoTsByStream.delete(streamKey);
1344
+ this.#lastDirectEmittedVideoTsByStream.delete(streamKey);
1345
+ this.#pendingDirectRecoveredVideoByStream.delete(streamKey);
1346
+ this.#lastDirectSourceAudioCursorByStream.delete(streamKey);
1347
+ this.#lastDirectEmittedAudioCursorByStream.delete(streamKey);
1348
+ this.#pendingDirectRecoveredAudioByStream.delete(streamKey);
1349
+ this.#manifestRecoveryQueues.delete(streamKey);
1350
+ for (const packetSeqs of [this.#sourcePktSeqs, this.#routedPacketSeqs]) {
1351
+ for (const key of packetSeqs) {
1352
+ try {
1353
+ const [keyPacketId, keyTrackName] = JSON.parse(key);
1354
+ if (keyPacketId === packetId && keyTrackName === catalogTrackName) {
1355
+ packetSeqs.delete(key);
1356
+ }
1357
+ } catch {
1358
+ // Internal keys are JSON tuples; malformed legacy keys do not belong
1359
+ // to a named physical stream and are left untouched.
1360
+ }
1361
+ }
1362
+ }
1363
+ const assemblersToReset = new Set<CmafBlockAssembler>();
1364
+ const exactAssembler = this.#mseAssemblers.get(streamKey);
1365
+ if (exactAssembler) assemblersToReset.add(exactAssembler);
1366
+ const selectedAssembler = this.#mseAssemblerFor(packetId, catalogTrackName);
1367
+ if (selectedAssembler) assemblersToReset.add(selectedAssembler);
1368
+ for (const assembler of assemblersToReset) assembler.reset();
1369
+ this.#trackInitDelivered.delete(streamKey);
1370
+ this.#parserDiagnostics.delete(streamKey);
1371
+ }
1372
+
1373
+ /** Live teardown is drop-only. Finite inputs must opt into a final flush. */
1374
+ dispose(options: { flush?: boolean } = {}): void {
1375
+ if (options.flush) this.flush();
1376
+ this.#disposed = true;
1377
+ this.cancelKeyframeLostTimer();
1378
+ this.#parser.dispose();
1379
+ this.#sourcePktSeqs.clear();
1380
+ this.#routedPacketSeqs.clear();
1381
+ this.#trackPacketCallbacks.clear();
1382
+ this.#latestVideoSeqByStream.clear();
1383
+ this.#lastDirectSourceVideoTsByStream.clear();
1384
+ this.#lastDirectEmittedVideoTsByStream.clear();
1385
+ this.#pendingDirectRecoveredVideoByStream.clear();
1386
+ this.#lastDirectSourceAudioCursorByStream.clear();
1387
+ this.#lastDirectEmittedAudioCursorByStream.clear();
1388
+ this.#pendingDirectRecoveredAudioByStream.clear();
1389
+ this.#trackSynced.clear();
1390
+ this.#droppedPreSyncVideo.clear();
1391
+ this.#trackInitDelivered.clear();
1392
+ this.#repairPidCounts = {};
1393
+ this.#moqRepairPidCounts = {};
1394
+ this.#moqRepairOwnerCounts = {};
1395
+ this.#parserDiagnostics.clear();
1396
+ this.#catalogTrackNamesByPacketId.clear();
1397
+ this.#repairOwnersByPacketId.clear();
1398
+ if (this.#ownsManifestVerifier) this.#manifestVerifier?.dispose();
1399
+ this.#manifestRecoveryQueues.clear();
1400
+ for (const assembler of this.#mseAssemblers.values()) assembler.dispose();
1401
+ this.#mseAssemblers.clear();
1402
+ }
1403
+
1404
+ // ── Private: keyframe loss detection ────────────────────────────
1405
+
1406
+ /** Arm (or re-arm) the keyframe loss timer.
1407
+ * Clears any existing timer and sets a new one for keyframeLostTimeoutMs.
1408
+ * On timeout, fires onKeyframeLost with the next expected groupId. */
1409
+ #armKeyframeLostTimer(): void {
1410
+ if (this.#keyframeLostTimer) {
1411
+ clearTimeout(this.#keyframeLostTimer);
1412
+ }
1413
+ const expectedNext = this.#lastKeyframeGroupId + 1;
1414
+ this.#keyframeLostTimer = setTimeout(() => {
1415
+ this.#keyframeLostTimer = null;
1416
+ this.#onKeyframeLost?.(expectedNext);
1417
+ }, this.#keyframeLostTimeoutMs!);
1418
+ }
1419
+
1420
+ #noteEmittedKeyframe(packetId: number, keyframe: boolean): void {
1421
+ if (!keyframe) return;
1422
+ if (!this.#videoPacketIds.has(packetId)) return;
1423
+ if (this.#keyframeLostTimeoutMs === null) return;
1424
+ this.#lastKeyframeGroupId++;
1425
+ this.#armKeyframeLostTimer();
1426
+ }
1427
+
1428
+ // ── Private: MMTP processing ────────────────────────────────────
1429
+
1430
+ /** Classify a packetId from the catalog. Returns true=video, false=audio, null=unknown (drop). */
1431
+ #classifyPacketId(packetId: number): boolean | null {
1432
+ if (this.#videoPacketIds.has(packetId)) return true;
1433
+ if (this.#audioPacketIds.has(packetId)) return false;
1434
+ return null;
1435
+ }
1436
+
1437
+ /** Resolve callback for a packetId (cached per-packetId, invalidated on registration).
1438
+ * When a per-track callback is registered AND a legacy callback exists,
1439
+ * fires BOTH: per-track first, then legacy. This ensures element.ts wiring
1440
+ * (confirmAudio, feedMulticast) continues to fire even when TrackPipeline
1441
+ * registers per-track sources.
1442
+ * Returns null for unknown packetIds — callers must drop the packet. */
1443
+ #getFrameCallback(packetId: number): FrameCallback | null {
1444
+ const cached = this.#resolvedCallbacks.get(packetId);
1445
+ if (cached) return cached;
1446
+
1447
+ const perTrack = this.#trackCallbacks.get(packetId);
1448
+ const classification = this.#classifyPacketId(packetId);
1449
+ const legacy =
1450
+ classification === true
1451
+ ? this.#onVideoFrame
1452
+ : classification === false
1453
+ ? this.#onAudioFrame
1454
+ : null;
1455
+
1456
+ let resolved: FrameCallback | null;
1457
+ if (perTrack && legacy) {
1458
+ // Composite: fire both so per-track routing AND legacy confirm/feedMulticast work.
1459
+ resolved = (data, meta) => {
1460
+ perTrack(data, meta);
1461
+ legacy(data, meta);
1462
+ };
1463
+ } else {
1464
+ resolved = perTrack ?? legacy ?? null;
1465
+ }
1466
+
1467
+ if (resolved) this.#resolvedCallbacks.set(packetId, resolved);
1468
+ return resolved;
1469
+ }
1470
+
1471
+ /** Deliver init segment for a track (deduped per physical stream). */
1472
+ #deliverInit(packetId: number, data: Uint8Array, catalogTrackName?: string): void {
1473
+ const streamKey = this.#mediaStreamKey(packetId, catalogTrackName);
1474
+ if (this.#trackInitDelivered.has(streamKey)) return;
1475
+ if (data.byteLength <= 8) return;
1476
+ this.#trackInitDelivered.add(streamKey);
1477
+ const cb = this.#trackInitCallbacks.get(packetId);
1478
+ if (cb) {
1479
+ cb(data);
1480
+ } else if (this.#onInitSegment) {
1481
+ this.#onInitSegment(data);
1482
+ }
1483
+ console.log(
1484
+ `[MmtpRouter] Init delivered for packetId=${packetId}${catalogTrackName === undefined ? "" : ` track=${catalogTrackName}`} (${data.byteLength}B)`,
1485
+ );
1486
+ }
1487
+
1488
+ /**
1489
+ * Core MMTP packet processing — source packets → play + FEC, repair → FEC only.
1490
+ * Both source and repair feed FECManager so RaptorQ has enough symbols (source + repair >= K).
1491
+ *
1492
+ * Two-phase parsing via MmtpContainerParser:
1493
+ * 1. parseHeader() → extract packetId + fecPayloadId for FEC feeding
1494
+ * 2. feed() → MFU extraction, reassembly, keyframe detection, emit via callbacks
1495
+ */
1496
+ #processMMTP(
1497
+ packet: Uint8Array,
1498
+ source: "multicast" | "moq",
1499
+ manifestIdentity?: ManifestObjectIdentity,
1500
+ manifestAdmitted = false,
1501
+ ingress?: MmtpIngressContext,
1502
+ ): void {
1503
+ const info = this.#parser.parseHeader(packet);
1504
+ if (!info) return;
1505
+ if (source === "multicast" && !ingressHasPacketId(ingress, info.packetId)) {
1506
+ debugRouter(
1507
+ `[MmtpRouter] Dropping multicast packet pid=${info.packetId}: absent from ingress tracks`,
1508
+ );
1509
+ return;
1510
+ }
1511
+ const packetRole = this.#packetIdRoles.get(info.packetId);
1512
+ if (
1513
+ source === "multicast" &&
1514
+ this.#classifyPacketId(info.packetId) === null &&
1515
+ (!info.isRepair || packetRole !== "repair")
1516
+ ) {
1517
+ debugRouter(
1518
+ `[MmtpRouter] Dropping multicast packet pid=${info.packetId}: physical PID is unregistered`,
1519
+ );
1520
+ return;
1521
+ }
1522
+ if (!info.isRepair && hasAmbiguousCatalogTrackName(ingress, info.packetId)) {
1523
+ return;
1524
+ }
1525
+
1526
+ if (!info.isRepair && this.#manifestVerifier && !manifestAdmitted) {
1527
+ let identity = manifestIdentity;
1528
+ if (!identity && info.fecPayloadId && (this.#resolveManifestIdentityForRoute || this.#resolveManifestIdentity)) {
1529
+ const coordinates = this.#deriveSbnEsi(info.fecPayloadId.ssId, info.packetId);
1530
+ const catalogTrackName = catalogTrackNameForPacket(ingress, info.packetId);
1531
+ identity = (this.#resolveManifestIdentityForRoute
1532
+ ? this.#resolveManifestIdentityForRoute({
1533
+ packetId: info.packetId,
1534
+ sbn: coordinates.sbn,
1535
+ esi: coordinates.esi,
1536
+ ...(catalogTrackName === undefined ? {} : { catalogTrackName }),
1537
+ recovered: false,
1538
+ sourceSymbolFormat: "full-mmtp-packet",
1539
+ })
1540
+ : this.#resolveManifestIdentity!(
1541
+ info.packetId,
1542
+ coordinates.sbn,
1543
+ coordinates.esi,
1544
+ catalogTrackName,
1545
+ )) ?? undefined;
1546
+ }
1547
+ if (identity) {
1548
+ identity = {
1549
+ ...identity,
1550
+ packetId: info.packetId,
1551
+ ...(info.fecPayloadId ? { ssId: info.fecPayloadId.ssId } : {}),
1552
+ };
1553
+ const authenticated = info.fecPayloadId ? packet.subarray(0, packet.length - 4) : packet;
1554
+ this.#manifestVerifier.admitObject(identity, authenticated, () => {
1555
+ if (!this.#disposed) this.#processMMTP(packet, source, identity, true, ingress);
1556
+ });
1557
+ return;
1558
+ }
1559
+ if (
1560
+ this.#manifestVerifier.config.mode === "enforce" &&
1561
+ this.#manifestVerifier.config.onMissingManifest === "stall"
1562
+ ) {
1563
+ console.warn(`[MmtpRouter] manifest identity absent; stalling packetId=${info.packetId}`);
1564
+ return;
1565
+ }
1566
+ }
1567
+
1568
+ if (info.isRepair) {
1569
+ // Repair packet — per ISO 23008-1 §C.5.3, carries (SS_Start, SSB_length, RS_ID).
1570
+ const parsed = parseRepairPacket(packet);
1571
+ if (!parsed) return;
1572
+
1573
+ const ownerPacketId = this.#resolveRepairOwnerPacketId(info.packetId, ingress);
1574
+ if (ownerPacketId != null && this.#fecManager) {
1575
+ if (hasAmbiguousCatalogTrackName(ingress, ownerPacketId)) return;
1576
+ this.#repairPidCounts[info.packetId] = (this.#repairPidCounts[info.packetId] || 0) + 1;
1577
+ const count = this.#repairPidCounts[info.packetId]!;
1578
+ if (count <= 2 || count % 500 === 0) {
1579
+ console.log(
1580
+ `[MC-PKT] pid=${info.packetId} repairOwner=${ownerPacketId} type=${info.packetType} count=${count} ssbLength=${parsed.ssbLength} len=${packet.length}`,
1581
+ );
1582
+ }
1583
+
1584
+ // NOTE: Do NOT call trackBlock for repair packets. Only source symbols
1585
+ // should count toward block completion — repair inflates blocksClean stats
1586
+ // and masks blocks that actually needed FEC recovery.
1587
+ const repairSbn = this.#deriveRepairSbn(parsed.ssStart, parsed.ssbLength, ownerPacketId);
1588
+ if (repairSbn === null) return;
1589
+ const repairEsi = this.#kForPacket(ownerPacketId) + parsed.rsId;
1590
+
1591
+ if (
1592
+ !this.#feedFecTransportSymbol(
1593
+ ownerPacketId,
1594
+ repairSbn,
1595
+ repairEsi,
1596
+ parsed.repairData,
1597
+ source,
1598
+ catalogTrackNameForPacket(ingress, ownerPacketId),
1599
+ )
1600
+ ) return;
1601
+
1602
+ // Repair symbols are block progress too. The deadline measures
1603
+ // inactivity, not total block lifetime.
1604
+ const repairCatalogTrackName = catalogTrackNameForPacket(ingress, ownerPacketId);
1605
+ if (repairCatalogTrackName === undefined) {
1606
+ this.#fecManager.noteBlockActivity?.(repairSbn, ownerPacketId);
1607
+ } else {
1608
+ this.#fecManager.noteBlockActivity?.(
1609
+ repairSbn,
1610
+ ownerPacketId,
1611
+ repairCatalogTrackName,
1612
+ );
1613
+ }
1614
+
1615
+ const recovered = repairCatalogTrackName === undefined
1616
+ ? this.#fecManager.feedRepairSymbol(
1617
+ ownerPacketId,
1618
+ parsed.ssStart,
1619
+ parsed.ssbLength,
1620
+ parsed.rsId,
1621
+ parsed.repairData,
1622
+ )
1623
+ : this.#fecManager.feedRepairSymbol(
1624
+ ownerPacketId,
1625
+ parsed.ssStart,
1626
+ parsed.ssbLength,
1627
+ parsed.rsId,
1628
+ parsed.repairData,
1629
+ repairCatalogTrackName,
1630
+ );
1631
+ if (recovered) {
1632
+ const alreadyCompleted = repairCatalogTrackName === undefined
1633
+ ? this.#fecManager.isBlockCompleted(repairSbn, ownerPacketId)
1634
+ : this.#fecManager.isBlockCompleted(
1635
+ repairSbn,
1636
+ ownerPacketId,
1637
+ repairCatalogTrackName,
1638
+ );
1639
+ if (repairCatalogTrackName === undefined) {
1640
+ this.#fecManager.cancelBlockTimeout(repairSbn, ownerPacketId);
1641
+ } else {
1642
+ this.#fecManager.cancelBlockTimeout(
1643
+ repairSbn,
1644
+ ownerPacketId,
1645
+ repairCatalogTrackName,
1646
+ );
1647
+ }
1648
+ // Skip routing if block was already completed via source path (all K sources
1649
+ // arrived). The parser's feed() already played every frame.
1650
+ if (!alreadyCompleted) {
1651
+ this.#fecManager.countRecovery(ownerPacketId);
1652
+ if (repairCatalogTrackName === undefined) {
1653
+ this.#fecManager.markBlockCompleted(repairSbn, ownerPacketId);
1654
+ } else {
1655
+ this.#fecManager.markBlockCompleted(
1656
+ repairSbn,
1657
+ ownerPacketId,
1658
+ repairCatalogTrackName,
1659
+ );
1660
+ }
1661
+ this.#routeRecoveredBlock(
1662
+ repairSbn,
1663
+ recovered,
1664
+ ownerPacketId,
1665
+ catalogTrackNameForPacket(ingress, ownerPacketId),
1666
+ );
1667
+ }
1668
+ }
1669
+ }
1670
+ } else {
1671
+ // Drop source packets with unknown packetId — not in catalog.
1672
+ if (this.#classifyPacketId(info.packetId) === null) return;
1673
+
1674
+ // Debug: count packets per packetId
1675
+ this.#pidCounts[info.packetId] = (this.#pidCounts[info.packetId] || 0) + 1;
1676
+ const count = this.#pidCounts[info.packetId]!;
1677
+ if (count <= 2 || count % 500 === 0) {
1678
+ console.log(
1679
+ `[MC-PKT] pid=${info.packetId} type=${info.packetType} count=${count} len=${packet.length}`,
1680
+ );
1681
+ }
1682
+
1683
+ // Source packet — parse for playback first, then feed FEC only if the
1684
+ // source block did not complete cleanly. Feeding FEC before parser/feed
1685
+ // lets a source-triggered recovery race ahead of the source timeline.
1686
+ let fecInfo:
1687
+ | { sbn: number; esi: number; oversized?: boolean }
1688
+ | undefined;
1689
+ let sourceFec:
1690
+ | {
1691
+ ssId: number;
1692
+ sbn: number;
1693
+ symbolData: Uint8Array;
1694
+ }
1695
+ | null = null;
1696
+ let sourceSymbolAccepted = false;
1697
+ let sourceSymbolTrackable = false;
1698
+ const catalogTrackName = catalogTrackNameForPacket(ingress, info.packetId);
1699
+ if (catalogTrackName !== undefined) {
1700
+ const names = this.#catalogTrackNamesByPacketId.get(info.packetId) ?? new Set();
1701
+ names.add(catalogTrackName);
1702
+ this.#catalogTrackNamesByPacketId.set(info.packetId, names);
1703
+ }
1704
+
1705
+ if (this.#fecManager && info.fecPayloadId) {
1706
+ const fecId = info.fecPayloadId;
1707
+ const symbolSize = this.#symbolSizeForPacket(info.packetId);
1708
+
1709
+ // The mmtp-packet source symbol protects the complete MMTP packet
1710
+ // before SS_ID is appended, self-delimiting and zero-padded to T.
1711
+ const rawSymbol = packet.subarray(0, packet.length - 4);
1712
+ const oversized = symbolSize > 0 && rawSymbol.length > symbolSize;
1713
+ const coords = this.#deriveSbnEsi(fecId.ssId, info.packetId);
1714
+
1715
+ // NOTE: trackBlock() is deferred to AFTER #feedViaParser (below).
1716
+ // Reason: trackBlock synchronously fires onBlockComplete when the
1717
+ // K-th source symbol arrives, which synchronously calls
1718
+ // CmafBlockAssembler.completeBlock → #flushBlock → emit. If
1719
+ // trackBlock fires BEFORE parser.feed, the K-th packet's
1720
+ // parser.onFrame → assembler.addFrame happens AFTER flushBlock
1721
+ // already drained the block — the K-th frame is stranded in a
1722
+ // newly-allocated empty block that never gets a completeBlock
1723
+ // event again, because that SBN is already in fecManager's
1724
+ // #completedBlocks set (BLO-3035 layer 4).
1725
+ //
1726
+ // Complete-packet symbols larger than T cannot be decoded but still
1727
+ // represent a source-symbol arrival, so trackBlock counts them below.
1728
+
1729
+ if (oversized) {
1730
+ // Oversized packet — not FEC-encoded by FFmpeg, skip FEC feed.
1731
+ // Still emitted via parser.feed() below (direct path).
1732
+ sourceSymbolAccepted = this.#feedFecTransportSymbol(
1733
+ info.packetId,
1734
+ coords.sbn,
1735
+ coords.esi,
1736
+ rawSymbol,
1737
+ source,
1738
+ catalogTrackName,
1739
+ );
1740
+ sourceSymbolTrackable = sourceSymbolAccepted;
1741
+ } else {
1742
+ let symbolData: Uint8Array;
1743
+ if (symbolSize > 0 && rawSymbol.length < symbolSize) {
1744
+ symbolData = new Uint8Array(symbolSize);
1745
+ symbolData.set(rawSymbol);
1746
+ } else {
1747
+ symbolData = rawSymbol;
1748
+ }
1749
+
1750
+ sourceSymbolAccepted = this.#feedFecTransportSymbol(
1751
+ info.packetId,
1752
+ coords.sbn,
1753
+ coords.esi,
1754
+ symbolData,
1755
+ source,
1756
+ catalogTrackName,
1757
+ );
1758
+ sourceSymbolTrackable = sourceSymbolAccepted;
1759
+ if (sourceSymbolAccepted) {
1760
+ sourceFec = { ssId: fecId.ssId, sbn: coords.sbn, symbolData };
1761
+ }
1762
+ }
1763
+
1764
+ // Compute FEC block info for the MSE assembler.
1765
+ fecInfo = { sbn: coords.sbn, esi: coords.esi, oversized };
1766
+ }
1767
+
1768
+ // Parse only the first copy of an FEC source symbol. A late direct copy
1769
+ // of a worker-recovered fragment has the same SBN/ESI and fragment
1770
+ // counter; feeding it again can make the MFU buffer non-contiguous even
1771
+ // when FI=1..3 were already complete. Non-FEC packets still always flow.
1772
+ if (!this.#fecManager || !info.fecPayloadId || sourceSymbolAccepted) {
1773
+ this.#noteSourcePacketSeq(
1774
+ info.packetId,
1775
+ info.sequenceNumber,
1776
+ catalogTrackName,
1777
+ );
1778
+ this.#routeTrackPacket(packet, info.packetId, info.sequenceNumber, {
1779
+ recovered: false,
1780
+ skipFecPayloadId: false,
1781
+ catalogTrackName,
1782
+ });
1783
+ this.#feedViaParser(packet, info, fecInfo, {
1784
+ source,
1785
+ catalogTrackName,
1786
+ });
1787
+ }
1788
+
1789
+ // Deferred trackBlock (BLO-3035 layer 4 race fix). Fire AFTER
1790
+ // #feedViaParser → parser.feed → addFrame, so the K-th source
1791
+ // packet's frame is buffered in CmafBlockAssembler.#blocks BEFORE
1792
+ // trackBlock synchronously triggers onBlockComplete → completeBlock
1793
+ // → #flushBlock. Without this ordering, the K-th frame was lost on
1794
+ // every block — only K-1 frames per block were ever emitted to MSE.
1795
+ // Only first-arriving symbols count toward completion or playback.
1796
+ if (
1797
+ this.#fecManager &&
1798
+ info.fecPayloadId &&
1799
+ sourceSymbolAccepted &&
1800
+ sourceSymbolTrackable
1801
+ ) {
1802
+ const { sbn, esi } = this.#deriveSbnEsi(
1803
+ info.fecPayloadId.ssId,
1804
+ info.packetId,
1805
+ );
1806
+ if (this.#classifyPacketId(info.packetId) !== null) {
1807
+ if (catalogTrackName === undefined) {
1808
+ this.#fecManager.trackBlock(sbn, info.packetId, undefined, esi);
1809
+ } else {
1810
+ this.#fecManager.trackBlock(
1811
+ sbn,
1812
+ info.packetId,
1813
+ catalogTrackName,
1814
+ esi,
1815
+ );
1816
+ }
1817
+ }
1818
+ }
1819
+
1820
+ // Only now feed source symbols into FEC. The decoder must still receive
1821
+ // the K-th source symbol after trackBlock() marks a clean block complete:
1822
+ // Worker-backed decoders use that symbol for their own block lifecycle
1823
+ // and stats. Suppress any synchronous recovered-block return when the
1824
+ // manager already completed the block, because every source frame was
1825
+ // parsed and emitted above.
1826
+ if (this.#fecManager && sourceFec) {
1827
+ const alreadyCompleted = catalogTrackName === undefined
1828
+ ? this.#fecManager.isBlockCompleted(sourceFec.sbn, info.packetId)
1829
+ : this.#fecManager.isBlockCompleted(
1830
+ sourceFec.sbn,
1831
+ info.packetId,
1832
+ catalogTrackName,
1833
+ );
1834
+ const recovered = catalogTrackName === undefined
1835
+ ? this.#fecManager.feedSourceSymbol(
1836
+ info.packetId,
1837
+ sourceFec.ssId,
1838
+ sourceFec.symbolData,
1839
+ undefined,
1840
+ )
1841
+ : this.#fecManager.feedSourceSymbol(
1842
+ info.packetId,
1843
+ sourceFec.ssId,
1844
+ sourceFec.symbolData,
1845
+ undefined,
1846
+ catalogTrackName,
1847
+ );
1848
+ if (recovered) {
1849
+ if (!alreadyCompleted && this.#classifyPacketId(info.packetId) !== null) {
1850
+ if (catalogTrackName === undefined) {
1851
+ this.#fecManager.cancelBlockTimeout(sourceFec.sbn, info.packetId);
1852
+ } else {
1853
+ this.#fecManager.cancelBlockTimeout(
1854
+ sourceFec.sbn,
1855
+ info.packetId,
1856
+ catalogTrackName,
1857
+ );
1858
+ }
1859
+ }
1860
+ // Only route recovered blocks that needed actual repair.
1861
+ if (!alreadyCompleted) {
1862
+ this.#fecManager.countRecovery(info.packetId);
1863
+ if (catalogTrackName === undefined) {
1864
+ this.#fecManager.markBlockCompleted(sourceFec.sbn, info.packetId);
1865
+ } else {
1866
+ this.#fecManager.markBlockCompleted(
1867
+ sourceFec.sbn,
1868
+ info.packetId,
1869
+ catalogTrackName,
1870
+ );
1871
+ }
1872
+ this.#routeRecoveredBlock(
1873
+ sourceFec.sbn,
1874
+ recovered,
1875
+ info.packetId,
1876
+ catalogTrackName,
1877
+ );
1878
+ }
1879
+ }
1880
+ }
1881
+ }
1882
+ }
1883
+
1884
+ /**
1885
+ * Set temp context and delegate to parser.feed() for MFU extraction,
1886
+ * reassembly, keyframe detection, and frame emission via callbacks.
1887
+ */
1888
+ #feedViaParser(
1889
+ packet: Uint8Array,
1890
+ info: {
1891
+ packetId: number;
1892
+ sequenceNumber: number;
1893
+ fecPayloadId?: { ssId: number } | null;
1894
+ },
1895
+ fecInfo?: { sbn: number; esi: number; oversized?: boolean },
1896
+ parserOptions?: {
1897
+ skipFecPayloadId?: boolean;
1898
+ recovered?: boolean;
1899
+ source?: MmtpSourcePath;
1900
+ deliveryProvenance?: MmtpDeliveryProvenance;
1901
+ catalogTrackName?: string;
1902
+ },
1903
+ ): void {
1904
+ const diag = this.#diagFor(info.packetId, parserOptions?.catalogTrackName);
1905
+ const before = this.#parser.reassembler.getStatus();
1906
+ diag.packets++;
1907
+ diag.lastPacketLength = packet.byteLength;
1908
+ diag.lastSequenceNumber = info.sequenceNumber;
1909
+ if (info.fecPayloadId) diag.lastFecSsId = info.fecPayloadId.ssId;
1910
+ this.#currentFecInfo = fecInfo;
1911
+ this.#currentSeqNum = info.sequenceNumber;
1912
+ try {
1913
+ this.#parser.feed(packet, {
1914
+ skipFecPayloadId: parserOptions?.skipFecPayloadId,
1915
+ recovered: parserOptions?.recovered,
1916
+ source: parserOptions?.source,
1917
+ deliveryProvenance: parserOptions?.deliveryProvenance,
1918
+ catalogTrackName: parserOptions?.catalogTrackName,
1919
+ });
1920
+ } finally {
1921
+ this.#currentFecInfo = undefined;
1922
+ }
1923
+ const after = this.#parser.reassembler.getStatus();
1924
+ const fragmentDelta = after.stats.totalFragments - before.stats.totalFragments;
1925
+ const reassembledDelta = after.stats.reassembledMFUs - before.stats.reassembledMFUs;
1926
+ if (fragmentDelta > 0) diag.fragments += fragmentDelta;
1927
+ else diag.noFragment++;
1928
+ if (reassembledDelta > 0) diag.reassembled += reassembledDelta;
1929
+ diag.pendingBuffers = after.bufferedMFUs;
1930
+ diag.pendingFragments = after.pendingFragments;
1931
+ diag.pendingSamples = after.pendingSamples;
1932
+ }
1933
+
1934
+ #feedFecTransportSymbol(
1935
+ packetId: number,
1936
+ sbn: number,
1937
+ esi: number,
1938
+ data: Uint8Array,
1939
+ source: MmtpSourcePath,
1940
+ catalogTrackName?: string,
1941
+ ): boolean {
1942
+ const fecManager = this.#fecManager;
1943
+ if (!fecManager) return false;
1944
+ return catalogTrackName === undefined
1945
+ ? fecManager.feedSymbolFromSource(packetId, sbn, esi, data, source)
1946
+ : fecManager.feedSymbolFromSource(
1947
+ packetId,
1948
+ sbn,
1949
+ esi,
1950
+ data,
1951
+ source,
1952
+ catalogTrackName,
1953
+ );
1954
+ }
1955
+
1956
+ #diagFor(packetId: number, catalogTrackName?: string): ParserDiagnostics {
1957
+ const streamKey = this.#mediaStreamKey(packetId, catalogTrackName);
1958
+ let diag = this.#parserDiagnostics.get(streamKey);
1959
+ if (!diag) {
1960
+ diag = {
1961
+ packets: 0,
1962
+ frames: 0,
1963
+ initSegments: 0,
1964
+ noFragment: 0,
1965
+ fragments: 0,
1966
+ reassembled: 0,
1967
+ pendingBuffers: 0,
1968
+ pendingFragments: 0,
1969
+ lastPacketLength: 0,
1970
+ lastSequenceNumber: 0,
1971
+ directAudioSourceEmitted: 0,
1972
+ directAudioSourceRegressions: 0,
1973
+ directAudioRecoveredQueued: 0,
1974
+ directAudioRecoveredDrained: 0,
1975
+ directAudioRecoveredStaleDropped: 0,
1976
+ };
1977
+ this.#parserDiagnostics.set(streamKey, diag);
1978
+ }
1979
+ return diag;
1980
+ }
1981
+
1982
+ #shouldEmitFrame(
1983
+ packetId: number,
1984
+ keyframe: boolean,
1985
+ catalogTrackName?: string,
1986
+ ): boolean {
1987
+ if (!this.#fecManager) return true;
1988
+ if (!this.#videoPacketIds.has(packetId)) return true;
1989
+ const streamKey = this.#mediaStreamKey(packetId, catalogTrackName);
1990
+ if (this.#trackSynced.has(streamKey)) return true;
1991
+ if (!keyframe) {
1992
+ this.#droppedPreSyncVideo.add(streamKey);
1993
+ return false;
1994
+ }
1995
+ this.#trackSynced.add(streamKey);
1996
+ this.#droppedPreSyncVideo.delete(streamKey);
1997
+ return true;
1998
+ }
1999
+
2000
+ #opensFecSync(
2001
+ packetId: number,
2002
+ keyframe: boolean,
2003
+ catalogTrackName?: string,
2004
+ ): boolean {
2005
+ const streamKey = this.#mediaStreamKey(packetId, catalogTrackName);
2006
+ return !!this.#fecManager &&
2007
+ this.#videoPacketIds.has(packetId) &&
2008
+ !this.#trackSynced.has(streamKey) &&
2009
+ this.#droppedPreSyncVideo.has(streamKey) &&
2010
+ keyframe;
2011
+ }
2012
+
2013
+ #noteEmittedVideoSeq(streamKey: string, packetSequenceNumber: number | undefined): void {
2014
+ if (packetSequenceNumber === undefined || !Number.isFinite(packetSequenceNumber)) return;
2015
+ const latest = this.#latestVideoSeqByStream.get(streamKey);
2016
+ if (latest === undefined || packetSequenceNumber > latest) {
2017
+ this.#latestVideoSeqByStream.set(streamKey, packetSequenceNumber);
2018
+ }
2019
+ }
2020
+
2021
+ #noteSourcePacketSeq(
2022
+ packetId: number,
2023
+ packetSequenceNumber: number | undefined,
2024
+ catalogTrackName?: string,
2025
+ ): void {
2026
+ if (packetSequenceNumber === undefined || !Number.isFinite(packetSequenceNumber)) return;
2027
+ this.#sourcePktSeqs.add(
2028
+ this.#packetSeqKey(packetId, packetSequenceNumber, catalogTrackName),
2029
+ );
2030
+ // Eviction: keep the most recent 2000 entries (covers ~62 blocks at K=32/30fps).
2031
+ // FEC recovery completes within the catalog-derived holdback window, so older
2032
+ // sequence numbers cannot be useful for duplicate suppression.
2033
+ if (this.#sourcePktSeqs.size > 4000) {
2034
+ const iter = this.#sourcePktSeqs.values();
2035
+ for (let i = 0; i < 2000; i++) iter.next();
2036
+ const keep = new Set<string>();
2037
+ for (const v of iter) keep.add(v);
2038
+ this.#sourcePktSeqs = keep;
2039
+ }
2040
+ }
2041
+
2042
+ #routeTrackPacket(
2043
+ packet: Uint8Array,
2044
+ packetId: number,
2045
+ packetSequenceNumber: number,
2046
+ meta: Pick<
2047
+ RoutedPacketMeta,
2048
+ "recovered" | "skipFecPayloadId" | "catalogTrackName" | "deliveryProvenance"
2049
+ >,
2050
+ ): void {
2051
+ if (!Number.isFinite(packetSequenceNumber)) return;
2052
+ const callback = this.#trackPacketCallbacks.get(packetId);
2053
+ if (!callback) return;
2054
+ const key = this.#packetSeqKey(packetId, packetSequenceNumber, meta.catalogTrackName);
2055
+ if (this.#routedPacketSeqs.has(key)) return;
2056
+ this.#routedPacketSeqs.add(key);
2057
+ if (this.#routedPacketSeqs.size > 4000) {
2058
+ const iter = this.#routedPacketSeqs.values();
2059
+ for (let i = 0; i < 2000; i++) iter.next();
2060
+ const keep = new Set<string>();
2061
+ for (const value of iter) keep.add(value);
2062
+ this.#routedPacketSeqs = keep;
2063
+ }
2064
+ callback(packet, {
2065
+ packetId,
2066
+ packetSequenceNumber,
2067
+ recovered: meta.recovered,
2068
+ skipFecPayloadId: meta.skipFecPayloadId,
2069
+ ...(meta.catalogTrackName === undefined
2070
+ ? {}
2071
+ : { catalogTrackName: meta.catalogTrackName }),
2072
+ ...(meta.deliveryProvenance === undefined
2073
+ ? {}
2074
+ : { deliveryProvenance: meta.deliveryProvenance }),
2075
+ });
2076
+ }
2077
+
2078
+ #isStaleRecoveredVideo(
2079
+ packetId: number,
2080
+ packetSequenceNumber: number,
2081
+ catalogTrackName?: string,
2082
+ ): boolean {
2083
+ const latest = this.#latestVideoSeqByStream.get(
2084
+ this.#mediaStreamKey(packetId, catalogTrackName),
2085
+ );
2086
+ return latest !== undefined && packetSequenceNumber <= latest;
2087
+ }
2088
+
2089
+ #mediaStreamKey(packetId: number, catalogTrackName?: string): string {
2090
+ return JSON.stringify([packetId, catalogTrackName ?? null]);
2091
+ }
2092
+
2093
+ #packetIdFromStreamKey(key: string): number {
2094
+ try {
2095
+ const decoded = JSON.parse(key);
2096
+ if (Array.isArray(decoded) && typeof decoded[0] === "number") return decoded[0];
2097
+ } catch {
2098
+ // Internal keys are JSON tuples; malformed legacy keys should not happen.
2099
+ }
2100
+ return 0;
2101
+ }
2102
+
2103
+ #mseAssemblerKey(packetId: number, catalogTrackName?: string): string {
2104
+ return this.#mediaStreamKey(packetId, catalogTrackName);
2105
+ }
2106
+
2107
+ #mseAssemblerFor(
2108
+ packetId: number,
2109
+ catalogTrackName?: string,
2110
+ ): CmafBlockAssembler | undefined {
2111
+ const exact = this.#mseAssemblers.get(
2112
+ this.#mseAssemblerKey(packetId, catalogTrackName),
2113
+ );
2114
+ if (exact || catalogTrackName === undefined) return exact;
2115
+ if (!this.#isCatalogTrackUnambiguous(packetId, catalogTrackName)) return undefined;
2116
+ return this.#mseAssemblers.get(this.#mseAssemblerKey(packetId));
2117
+ }
2118
+
2119
+ #isCatalogTrackUnambiguous(
2120
+ packetId: number,
2121
+ catalogTrackName?: string,
2122
+ ): boolean {
2123
+ if (catalogTrackName === undefined) return true;
2124
+ const names = this.#catalogTrackNamesByPacketId.get(packetId);
2125
+ return names?.size === 1 && names.has(catalogTrackName);
2126
+ }
2127
+
2128
+ #resolveRecoveredTiming(
2129
+ packetId: number,
2130
+ mpuSequenceNumber: number,
2131
+ movieFragmentSequenceNumber: number | undefined,
2132
+ sampleNumber: number | undefined,
2133
+ mmtpTimestamp: bigint,
2134
+ catalogTrackName?: string,
2135
+ ): { authoredTiming?: AuthoredTiming; timestamp: number; mmtpTimestamp: bigint } | null {
2136
+ // Non-timed MFUs do not carry a movie-fragment/sample identity. They are
2137
+ // outside the ISO-BMFF presentation timeline, so retain the raw MMTP
2138
+ // timestamp path used by the untimed router contract.
2139
+ if (movieFragmentSequenceNumber === undefined || sampleNumber === undefined) {
2140
+ return {
2141
+ timestamp: mmtpTimestampToMicros(mmtpTimestamp),
2142
+ mmtpTimestamp,
2143
+ };
2144
+ }
2145
+
2146
+ // Claim the resolver entry before scaling it. The claim is the single
2147
+ // consume operation for a recovered sample; doing lookup → scale → claim
2148
+ // would leave an invalid entry live forever when scaling rejects it.
2149
+ const authoredTiming = this.#parser.consumeAuthoredTimingFor(
2150
+ packetId,
2151
+ mpuSequenceNumber,
2152
+ movieFragmentSequenceNumber,
2153
+ sampleNumber,
2154
+ catalogTrackName,
2155
+ );
2156
+ // Recovery is accepted only when the timed stream's FT=1 metadata names
2157
+ // this exact sample. MMTP send timestamps describe transport arrival, not
2158
+ // the authored presentation timeline, so they are never a fallback.
2159
+ if (authoredTiming === null) return null;
2160
+ const scaled = scaleAuthoredTiming(authoredTiming, 1_000_000);
2161
+ if (scaled === null) return null;
2162
+ return { authoredTiming, timestamp: scaled.timestamp, mmtpTimestamp };
2163
+ }
2164
+
2165
+ #packetSeqKey(
2166
+ packetId: number,
2167
+ packetSequenceNumber: number,
2168
+ catalogTrackName?: string,
2169
+ ): string {
2170
+ return JSON.stringify([packetId, catalogTrackName ?? null, packetSequenceNumber]);
2171
+ }
2172
+
2173
+ #pendingRecoveredKey(meta: RoutedFrameMeta): string {
2174
+ return `${meta.timestamp}:${meta.packetSequenceNumber ?? ""}`;
2175
+ }
2176
+
2177
+ #pendingRecoveredQueue(streamKey: string): Map<string, PendingRecoveredVideoFrame> {
2178
+ let queue = this.#pendingDirectRecoveredVideoByStream.get(streamKey);
2179
+ if (!queue) {
2180
+ queue = new Map();
2181
+ this.#pendingDirectRecoveredVideoByStream.set(streamKey, queue);
2182
+ }
2183
+ return queue;
2184
+ }
2185
+
2186
+ #comparePendingRecoveredVideo(
2187
+ a: PendingRecoveredVideoFrame,
2188
+ b: PendingRecoveredVideoFrame,
2189
+ ): number {
2190
+ if (a.meta.timestamp !== b.meta.timestamp) return a.meta.timestamp - b.meta.timestamp;
2191
+ const aSeq = a.meta.packetSequenceNumber ?? Number.MAX_SAFE_INTEGER;
2192
+ const bSeq = b.meta.packetSequenceNumber ?? Number.MAX_SAFE_INTEGER;
2193
+ return aSeq - bSeq;
2194
+ }
2195
+
2196
+ #emitDirectVideo(
2197
+ packetId: number,
2198
+ data: Uint8Array,
2199
+ meta: RoutedFrameMeta,
2200
+ cb: FrameCallback | null,
2201
+ ): boolean {
2202
+ const streamKey = this.#mediaStreamKey(packetId, meta.catalogTrackName);
2203
+ const lastEmitted = this.#lastDirectEmittedVideoTsByStream.get(streamKey);
2204
+ if (lastEmitted !== undefined && meta.timestamp <= lastEmitted) return false;
2205
+ this.#lastDirectEmittedVideoTsByStream.set(streamKey, meta.timestamp);
2206
+ // Record the emitted sequence number HERE — at the real emit — not when a
2207
+ // recovered frame is queued. #routeDirectRecoveredVideo may enqueue a frame
2208
+ // that is ahead of the source cursor; noting its seq before it drains would
2209
+ // make #isStaleRecoveredVideo drop an earlier gap frame that arrives later
2210
+ // (it would look <= the queued-but-unemitted seq). The high-water mark must
2211
+ // reflect what actually reached the decoder callback.
2212
+ this.#noteEmittedVideoSeq(streamKey, meta.packetSequenceNumber);
2213
+ cb?.(data, meta);
2214
+ return true;
2215
+ }
2216
+
2217
+ #drainDirectRecoveredVideoUpTo(
2218
+ streamKey: string,
2219
+ packetId: number,
2220
+ horizonTs: number,
2221
+ cb: FrameCallback | null,
2222
+ ): void {
2223
+ const queue = this.#pendingDirectRecoveredVideoByStream.get(streamKey);
2224
+ if (!queue || queue.size === 0) return;
2225
+
2226
+ const ready = [...queue.values()]
2227
+ .filter(frame => frame.meta.timestamp <= horizonTs)
2228
+ .sort((a, b) => this.#comparePendingRecoveredVideo(a, b));
2229
+
2230
+ for (const frame of ready) {
2231
+ queue.delete(this.#pendingRecoveredKey(frame.meta));
2232
+ this.#emitDirectVideo(packetId, frame.data, frame.meta, cb);
2233
+ }
2234
+
2235
+ if (queue.size === 0) {
2236
+ this.#pendingDirectRecoveredVideoByStream.delete(streamKey);
2237
+ }
2238
+ }
2239
+
2240
+ #enqueueDirectRecoveredVideo(
2241
+ packetId: number,
2242
+ data: Uint8Array,
2243
+ meta: RoutedFrameMeta,
2244
+ ): void {
2245
+ const streamKey = this.#mediaStreamKey(packetId, meta.catalogTrackName);
2246
+ const lastEmitted = this.#lastDirectEmittedVideoTsByStream.get(streamKey);
2247
+ if (lastEmitted !== undefined && meta.timestamp <= lastEmitted) return;
2248
+
2249
+ const queue = this.#pendingRecoveredQueue(streamKey);
2250
+ queue.set(this.#pendingRecoveredKey(meta), { data, meta: { ...meta } });
2251
+
2252
+ if (queue.size > DIRECT_RECOVERED_VIDEO_QUEUE_LIMIT) {
2253
+ const keep = [...queue.entries()]
2254
+ .sort((a, b) => this.#comparePendingRecoveredVideo(a[1], b[1]))
2255
+ .slice(-Math.floor(DIRECT_RECOVERED_VIDEO_QUEUE_LIMIT / 2));
2256
+ queue.clear();
2257
+ for (const [key, frame] of keep) queue.set(key, frame);
2258
+ }
2259
+ }
2260
+
2261
+ #routeDirectSourceVideo(
2262
+ packetId: number,
2263
+ data: Uint8Array,
2264
+ meta: RoutedFrameMeta,
2265
+ cb: FrameCallback | null,
2266
+ ): void {
2267
+ const streamKey = this.#mediaStreamKey(packetId, meta.catalogTrackName);
2268
+ const lastSourceTs = this.#lastDirectSourceVideoTsByStream.get(streamKey);
2269
+ if (lastSourceTs === undefined || meta.timestamp > lastSourceTs) {
2270
+ this.#lastDirectSourceVideoTsByStream.set(streamKey, meta.timestamp);
2271
+ }
2272
+
2273
+ // Recovered FEC may arrive before the source stream reaches the same media
2274
+ // time. Drain those gap fills immediately before the source frame that proves
2275
+ // the live cursor caught up, preserving the decoder dependency chain.
2276
+ this.#drainDirectRecoveredVideoUpTo(streamKey, packetId, meta.timestamp, cb);
2277
+ this.#emitDirectVideo(packetId, data, meta, cb);
2278
+ }
2279
+
2280
+ #routeDirectRecoveredVideo(
2281
+ packetId: number,
2282
+ data: Uint8Array,
2283
+ meta: RoutedFrameMeta,
2284
+ cb: FrameCallback | null,
2285
+ ): void {
2286
+ const streamKey = this.#mediaStreamKey(packetId, meta.catalogTrackName);
2287
+ this.#enqueueDirectRecoveredVideo(packetId, data, meta);
2288
+ const lastSourceTs = this.#lastDirectSourceVideoTsByStream.get(streamKey);
2289
+ this.#drainDirectRecoveredVideoUpTo(
2290
+ streamKey,
2291
+ packetId,
2292
+ lastSourceTs ?? Number.POSITIVE_INFINITY,
2293
+ cb,
2294
+ );
2295
+ }
2296
+
2297
+ #pendingRecoveredAudioQueue(streamKey: string): Map<string, PendingRecoveredAudioFrame> {
2298
+ let queue = this.#pendingDirectRecoveredAudioByStream.get(streamKey);
2299
+ if (!queue) {
2300
+ queue = new Map();
2301
+ this.#pendingDirectRecoveredAudioByStream.set(streamKey, queue);
2302
+ }
2303
+ return queue;
2304
+ }
2305
+
2306
+ #directAudioCursor(meta: RoutedFrameMeta): DirectAudioCursor {
2307
+ return {
2308
+ timestamp: meta.timestamp,
2309
+ // A recovered frame without transport sequence metadata must drain before
2310
+ // an equal-timestamp source horizon instead of remaining queued forever.
2311
+ sequenceNumber: meta.packetSequenceNumber ?? Number.MIN_SAFE_INTEGER,
2312
+ };
2313
+ }
2314
+
2315
+ #compareDirectAudioCursor(a: DirectAudioCursor, b: DirectAudioCursor): number {
2316
+ if (a.timestamp !== b.timestamp) return a.timestamp - b.timestamp;
2317
+ return a.sequenceNumber - b.sequenceNumber;
2318
+ }
2319
+
2320
+ #emitDirectAudio(
2321
+ packetId: number,
2322
+ data: Uint8Array,
2323
+ meta: RoutedFrameMeta,
2324
+ cb: FrameCallback | null,
2325
+ ): boolean {
2326
+ const streamKey = this.#mediaStreamKey(packetId, meta.catalogTrackName);
2327
+ const cursor = this.#directAudioCursor(meta);
2328
+ const lastEmitted = this.#lastDirectEmittedAudioCursorByStream.get(streamKey);
2329
+ if (
2330
+ lastEmitted !== undefined &&
2331
+ this.#compareDirectAudioCursor(cursor, lastEmitted) <= 0
2332
+ ) return false;
2333
+ this.#lastDirectEmittedAudioCursorByStream.set(streamKey, cursor);
2334
+ cb?.(data, meta);
2335
+ return true;
2336
+ }
2337
+
2338
+ #emitDirectSourceAudio(
2339
+ packetId: number,
2340
+ data: Uint8Array,
2341
+ meta: RoutedFrameMeta,
2342
+ cb: FrameCallback | null,
2343
+ ): void {
2344
+ const streamKey = this.#mediaStreamKey(packetId, meta.catalogTrackName);
2345
+ const cursor = this.#directAudioCursor(meta);
2346
+ const lastEmitted = this.#lastDirectEmittedAudioCursorByStream.get(streamKey);
2347
+ const diag = this.#diagFor(packetId, meta.catalogTrackName);
2348
+ diag.directAudioSourceEmitted++;
2349
+ if (
2350
+ lastEmitted !== undefined &&
2351
+ this.#compareDirectAudioCursor(cursor, lastEmitted) <= 0
2352
+ ) {
2353
+ // Source packets already passed transport/parser validation. Preserve
2354
+ // them even when unordered multicast completion moves the callback
2355
+ // cursor backward; only recovered frames are optional stale gap fills.
2356
+ diag.directAudioSourceRegressions++;
2357
+ } else {
2358
+ this.#lastDirectEmittedAudioCursorByStream.set(streamKey, cursor);
2359
+ }
2360
+ cb?.(data, meta);
2361
+ }
2362
+
2363
+ #drainDirectRecoveredAudioUpTo(
2364
+ streamKey: string,
2365
+ packetId: number,
2366
+ horizon: DirectAudioCursor,
2367
+ cb: FrameCallback | null,
2368
+ ): void {
2369
+ const queue = this.#pendingDirectRecoveredAudioByStream.get(streamKey);
2370
+ if (!queue || queue.size === 0) return;
2371
+
2372
+ const ready = [...queue.values()]
2373
+ .filter(frame => this.#compareDirectAudioCursor(
2374
+ this.#directAudioCursor(frame.meta),
2375
+ horizon,
2376
+ ) <= 0)
2377
+ .sort((a, b) => this.#compareDirectAudioCursor(
2378
+ this.#directAudioCursor(a.meta),
2379
+ this.#directAudioCursor(b.meta),
2380
+ ));
2381
+
2382
+ for (const frame of ready) {
2383
+ queue.delete(this.#pendingRecoveredKey(frame.meta));
2384
+ if (this.#emitDirectAudio(packetId, frame.data, frame.meta, cb)) {
2385
+ this.#diagFor(packetId, frame.meta.catalogTrackName)
2386
+ .directAudioRecoveredDrained++;
2387
+ } else {
2388
+ this.#diagFor(packetId, frame.meta.catalogTrackName)
2389
+ .directAudioRecoveredStaleDropped++;
2390
+ }
2391
+ }
2392
+
2393
+ if (queue.size === 0) {
2394
+ this.#pendingDirectRecoveredAudioByStream.delete(streamKey);
2395
+ }
2396
+ }
2397
+
2398
+ #routeDirectSourceAudio(
2399
+ packetId: number,
2400
+ data: Uint8Array,
2401
+ meta: RoutedFrameMeta,
2402
+ cb: FrameCallback | null,
2403
+ ): void {
2404
+ const streamKey = this.#mediaStreamKey(packetId, meta.catalogTrackName);
2405
+ const cursor = this.#directAudioCursor(meta);
2406
+ const lastSource = this.#lastDirectSourceAudioCursorByStream.get(streamKey);
2407
+ if (
2408
+ lastSource === undefined ||
2409
+ this.#compareDirectAudioCursor(cursor, lastSource) > 0
2410
+ ) {
2411
+ this.#lastDirectSourceAudioCursorByStream.set(streamKey, cursor);
2412
+ }
2413
+ this.#drainDirectRecoveredAudioUpTo(streamKey, packetId, cursor, cb);
2414
+ this.#emitDirectSourceAudio(packetId, data, meta, cb);
2415
+ }
2416
+
2417
+ #routeDirectRecoveredAudio(
2418
+ packetId: number,
2419
+ data: Uint8Array,
2420
+ meta: RoutedFrameMeta,
2421
+ cb: FrameCallback | null,
2422
+ ): void {
2423
+ const streamKey = this.#mediaStreamKey(packetId, meta.catalogTrackName);
2424
+ const cursor = this.#directAudioCursor(meta);
2425
+ const lastEmitted = this.#lastDirectEmittedAudioCursorByStream.get(streamKey);
2426
+ if (
2427
+ lastEmitted !== undefined &&
2428
+ this.#compareDirectAudioCursor(cursor, lastEmitted) <= 0
2429
+ ) {
2430
+ this.#diagFor(packetId, meta.catalogTrackName)
2431
+ .directAudioRecoveredStaleDropped++;
2432
+ return;
2433
+ }
2434
+
2435
+ const queue = this.#pendingRecoveredAudioQueue(streamKey);
2436
+ queue.set(this.#pendingRecoveredKey(meta), { data, meta: { ...meta } });
2437
+ this.#diagFor(packetId, meta.catalogTrackName).directAudioRecoveredQueued++;
2438
+ if (queue.size > DIRECT_RECOVERED_AUDIO_QUEUE_LIMIT) {
2439
+ const keep = [...queue.entries()]
2440
+ .sort((a, b) => this.#compareDirectAudioCursor(
2441
+ this.#directAudioCursor(a[1].meta),
2442
+ this.#directAudioCursor(b[1].meta),
2443
+ ))
2444
+ .slice(-Math.floor(DIRECT_RECOVERED_AUDIO_QUEUE_LIMIT / 2));
2445
+ queue.clear();
2446
+ for (const [key, frame] of keep) queue.set(key, frame);
2447
+ }
2448
+
2449
+ const lastSource = this.#lastDirectSourceAudioCursorByStream.get(streamKey);
2450
+ if (lastSource !== undefined) {
2451
+ this.#drainDirectRecoveredAudioUpTo(streamKey, packetId, lastSource, cb);
2452
+ }
2453
+ }
2454
+
2455
+ // ── Private: FEC recovery routing ───────────────────────────────
2456
+
2457
+ /** Route WASM-recovered block through appropriate path based on container kind. */
2458
+ #routeRecoveredBlock(
2459
+ blockId: number,
2460
+ recovered: Uint8Array,
2461
+ ownerPacketId?: number,
2462
+ catalogTrackName?: string,
2463
+ ): void {
2464
+ // MMTP path: use processFecBlock for MMTP-wrapped recovery.
2465
+ // Recovered symbols are individual MMTP packets that may be fragments
2466
+ // (FI=1/2/3) of a larger MFU. Route through the reassembler so
2467
+ // multi-packet MFUs (e.g., keyframes spanning 15+ packets) are
2468
+ // properly assembled before reaching the decoder.
2469
+ const k = this.#kForPacket(ownerPacketId);
2470
+ const packetSymbolSize = this.#symbolSizeForPacket(ownerPacketId);
2471
+ const symbolSize = packetSymbolSize > 0 ? packetSymbolSize : undefined;
2472
+ const result = processFecBlock(recovered, {
2473
+ k,
2474
+ symbolSize,
2475
+ ownerPacketId,
2476
+ });
2477
+ this.#routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName);
2478
+ }
2479
+
2480
+ #routeVerifiedFecSymbols(
2481
+ blockId: number,
2482
+ result: ProcessFecBlockResult,
2483
+ ownerPacketId?: number,
2484
+ catalogTrackName?: string,
2485
+ ): void {
2486
+ if (!this.#manifestVerifier || ownerPacketId == null) {
2487
+ this.#routeProcessedFecSymbols(blockId, result, ownerPacketId, catalogTrackName);
2488
+ return;
2489
+ }
2490
+ if (!this.#resolveManifestIdentity && !this.#resolveManifestIdentityForRoute) {
2491
+ console.warn(`[MmtpRouter] manifest identity resolver unavailable; recovered media is fail-open for packetId=${ownerPacketId} block=${blockId}`);
2492
+ this.#routeProcessedFecSymbols(blockId, result, ownerPacketId, catalogTrackName);
2493
+ return;
2494
+ }
2495
+ const queueKey = this.#mediaStreamKey(ownerPacketId, catalogTrackName);
2496
+ const queue = this.#manifestRecoveryQueues.get(queueKey) ?? [];
2497
+ queue.push({ blockId, result, ownerPacketId, catalogTrackName });
2498
+ this.#manifestRecoveryQueues.set(queueKey, queue);
2499
+ if (queue.length > 1) return;
2500
+ const drain = (): void => {
2501
+ const next = queue[0];
2502
+ if (!next) {
2503
+ this.#manifestRecoveryQueues.delete(queueKey);
2504
+ return;
2505
+ }
2506
+ this.#admitRecoveredResult(
2507
+ next.blockId,
2508
+ next.result,
2509
+ next.ownerPacketId,
2510
+ next.catalogTrackName,
2511
+ () => {
2512
+ queue.shift();
2513
+ drain();
2514
+ },
2515
+ );
2516
+ };
2517
+ drain();
2518
+ }
2519
+
2520
+ #admitRecoveredResult(
2521
+ blockId: number,
2522
+ result: ProcessFecBlockResult,
2523
+ ownerPacketId: number,
2524
+ catalogTrackName: string | undefined,
2525
+ done: () => void,
2526
+ ): void {
2527
+ const verifier = this.#manifestVerifier;
2528
+ const resolveIdentity = this.#resolveManifestIdentity;
2529
+ const resolveIdentityForRoute = this.#resolveManifestIdentityForRoute;
2530
+ if (!verifier) {
2531
+ this.#routeProcessedFecSymbols(blockId, result, ownerPacketId, catalogTrackName);
2532
+ done();
2533
+ return;
2534
+ }
2535
+ if (!resolveIdentity && !resolveIdentityForRoute) throw new Error("manifest identity resolver disappeared during admission");
2536
+ const symbols = result.symbols;
2537
+ const admitNext = (index: number): void => {
2538
+ const symbol = symbols[index];
2539
+ if (!symbol) {
2540
+ done();
2541
+ return;
2542
+ }
2543
+ const identity = resolveIdentityForRoute
2544
+ ? resolveIdentityForRoute({
2545
+ packetId: ownerPacketId,
2546
+ sbn: blockId,
2547
+ esi: symbol.index,
2548
+ ...(catalogTrackName === undefined ? {} : { catalogTrackName }),
2549
+ recovered: true,
2550
+ sourceSymbolFormat: "fec-padded-recovery",
2551
+ })
2552
+ : resolveIdentity!(ownerPacketId, blockId, symbol.index, catalogTrackName);
2553
+ if (!identity) {
2554
+ console.warn(`[MmtpRouter] manifest identity unresolved; recovered symbol is fail-open for packetId=${ownerPacketId} block=${blockId} esi=${symbol.index}`);
2555
+ this.#routeProcessedFecSymbols(
2556
+ blockId,
2557
+ { ...result, symbols: [symbol] },
2558
+ ownerPacketId,
2559
+ catalogTrackName,
2560
+ );
2561
+ admitNext(index + 1);
2562
+ return;
2563
+ }
2564
+ const admission = verifier.admitObject(
2565
+ {
2566
+ ...identity,
2567
+ packetId: ownerPacketId,
2568
+ ssId: blockId * this.#kForPacket(ownerPacketId) + symbol.index,
2569
+ },
2570
+ symbol.rawSymbol,
2571
+ () => {
2572
+ if (!this.#disposed) {
2573
+ this.#routeProcessedFecSymbols(
2574
+ blockId,
2575
+ { ...result, symbols: [symbol] },
2576
+ ownerPacketId,
2577
+ catalogTrackName,
2578
+ );
2579
+ }
2580
+ admitNext(index + 1);
2581
+ },
2582
+ // A stalled symbol that is later discarded instead of played must still
2583
+ // advance this loop; enforce mode never calls the release callback for
2584
+ // it (BLO-30095).
2585
+ () => admitNext(index + 1),
2586
+ );
2587
+ if (admission.action === "drop") admitNext(index + 1);
2588
+ };
2589
+ admitNext(0);
2590
+ }
2591
+
2592
+ #routeProcessedFecSymbols(
2593
+ blockId: number,
2594
+ result: ProcessFecBlockResult,
2595
+ ownerPacketId?: number,
2596
+ catalogTrackName?: string,
2597
+ ): void {
2598
+ // Route recovered symbols to the track's callback. The ownerPacketId tells us
2599
+ // which track this block belongs to — resolves per-track, then legacy fallback.
2600
+ if (ownerPacketId == null) return; // no track association
2601
+ if (this.#classifyPacketId(ownerPacketId) === null) return; // unknown track
2602
+
2603
+ // A recovered symbol is a function of EVERY symbol that entered its block,
2604
+ // so its provenance is the block's, not the media path's. Repair symbols
2605
+ // routinely arrive over reliable QUIC (`feedRepairPacket`), so a block that
2606
+ // recovered with MoQ help produced bytes that never crossed a multicast
2607
+ // socket. Without this, recovered frames reached the decoder carrying no
2608
+ // attribution at all and were counted as multicast by default.
2609
+ // A FEC manager that cannot report block provenance yields 'unknown', not
2610
+ // a guess: these bytes are then never counted as physical-delivery
2611
+ // evidence. `FECManagerLike` is a pluggable structural interface, so an
2612
+ // implementation without this method must degrade, not throw.
2613
+ const deliveryProvenance: MmtpDeliveryProvenance =
2614
+ this.#fecManager?.blockDeliveryProvenance?.(
2615
+ blockId,
2616
+ ownerPacketId,
2617
+ catalogTrackName,
2618
+ ) ?? 'unknown';
2619
+
2620
+ for (const sym of result.symbols) {
2621
+ this.#routeTrackPacket(sym.rawSymbol, ownerPacketId, sym.packetSequenceNumber, {
2622
+ recovered: true,
2623
+ skipFecPayloadId: true,
2624
+ catalogTrackName,
2625
+ deliveryProvenance,
2626
+ });
2627
+ }
2628
+
2629
+ // Feed recovered metadata before routing data. FT=0 initializes the timing
2630
+ // resolver and FT=1 publishes the sample table that recovered FT=2 data
2631
+ // must resolve against. The parser's callbacks dedupe repeated init
2632
+ // delivery, while keeping this path in lockstep with direct source input.
2633
+ for (const sym of result.symbols) {
2634
+ if (sym.fragmentType !== 0 && sym.fragmentType !== 1) continue;
2635
+ this.#feedViaParser(
2636
+ sym.rawSymbol,
2637
+ { packetId: ownerPacketId, sequenceNumber: sym.packetSequenceNumber },
2638
+ undefined,
2639
+ {
2640
+ skipFecPayloadId: true,
2641
+ recovered: true,
2642
+ ...(catalogTrackName === undefined ? {} : { catalogTrackName }),
2643
+ },
2644
+ );
2645
+ }
2646
+
2647
+ // MSE assembler path: route recovered block as pre-assembled frames (skip FT=0)
2648
+ // BLO-3035 layer 3: recovered symbols are RAW MFU fragments, not complete
2649
+ // frames. A keyframe can span multiple packets (FI=1/2/3); pushing each
2650
+ // fragment as a "frame" produces garbage moof+mdat that fails decode.
2651
+ // Mirror the non-MSE path's logic: FI=0 → emit single-packet MFU directly
2652
+ // (after dedup against source path), FI=1/2/3 → feed reassembler, emit when
2653
+ // the complete frame is available. Take the keyframe bit from the assembled
2654
+ // frame's rapFlag (with `#detectKeyframe` as a bitstream fallback for
2655
+ // publishers that don't set rapFlag) — `sym.isKeyframe` is just rapFlag
2656
+ // from a single fragment's header (see `block-processor.ts` where
2657
+ // `isKeyframe = hdr.rapFlag`) and isn't meaningful for the reassembled-
2658
+ // frame keyframe decision when FI≠0.
2659
+ const assembler = this.#mseAssemblerFor(ownerPacketId, catalogTrackName);
2660
+ if (assembler) {
2661
+ const isVideoTrack = this.#classifyPacketId(ownerPacketId) === true;
2662
+ const frames: Array<{
2663
+ data: Uint8Array;
2664
+ timestamp: number;
2665
+ keyframe: boolean;
2666
+ authoredTiming?: AuthoredTiming;
2667
+ packetSequenceNumber?: number;
2668
+ packetSequenceNumbers?: number[];
2669
+ repairedPacketSequenceNumbers?: number[];
2670
+ mmtpTimestamp?: bigint;
2671
+ mpuSequenceNumber?: number;
2672
+ movieFragmentSequenceNumber?: number;
2673
+ sampleNumber?: number;
2674
+ catalogTrackName?: string;
2675
+ deliveryProvenance?: MmtpDeliveryProvenance;
2676
+ sourceDiscontinuity?: boolean;
2677
+ }> = [];
2678
+ let dedupedAgainstSource = 0;
2679
+ let droppedBeforeSync = 0;
2680
+ for (const sym of result.symbols) {
2681
+ if (sym.fragmentType === 0 || sym.fragmentType === 1) continue;
2682
+ // FT=0/FT=1 are MPU metadata (initialization/movie fragment), already
2683
+ // consumed above to seed the authored timing resolver. Only FT=2 MFU
2684
+ // payloads can become media samples here.
2685
+ const fi = sym.fragment.fragmentationIndicator;
2686
+ if (fi === 0) {
2687
+ // Single-packet MFU. Dedup against source path so frames aren't
2688
+ // emitted twice. The direct source frame may still be buffered by the
2689
+ // parser reorder window, so use source-received sequence numbers.
2690
+ if (this.#sourcePktSeqs.has(
2691
+ this.#packetSeqKey(ownerPacketId, sym.packetSequenceNumber, catalogTrackName),
2692
+ )) {
2693
+ dedupedAgainstSource++;
2694
+ continue;
2695
+ }
2696
+ const keyframe = isVideoTrack
2697
+ ? this.#detectKeyframe(
2698
+ ownerPacketId,
2699
+ sym.fragment.data,
2700
+ isMfuKeyframeMetadata(sym.fragment.rapFlag, sym.fragment.priority),
2701
+ )
2702
+ : sym.fragment.rapFlag;
2703
+ const sourceDiscontinuity = this.#opensFecSync(
2704
+ ownerPacketId,
2705
+ keyframe,
2706
+ catalogTrackName,
2707
+ );
2708
+ if (!this.#shouldEmitFrame(ownerPacketId, keyframe, catalogTrackName)) {
2709
+ droppedBeforeSync++;
2710
+ continue;
2711
+ }
2712
+ const recoveredTiming = this.#resolveRecoveredTiming(
2713
+ ownerPacketId,
2714
+ sym.fragment.mpuSequenceNumber,
2715
+ sym.fragment.movieFragmentSequenceNumber,
2716
+ sym.fragment.sampleNumber,
2717
+ sym.fragment.timestamp,
2718
+ catalogTrackName,
2719
+ );
2720
+ if (!recoveredTiming) continue;
2721
+ frames.push({
2722
+ data: sym.fragment.data,
2723
+ timestamp: recoveredTiming.timestamp,
2724
+ mmtpTimestamp: recoveredTiming.mmtpTimestamp,
2725
+ keyframe,
2726
+ authoredTiming: recoveredTiming.authoredTiming,
2727
+ packetSequenceNumber: sym.packetSequenceNumber,
2728
+ packetSequenceNumbers: [sym.packetSequenceNumber],
2729
+ repairedPacketSequenceNumbers: [sym.packetSequenceNumber],
2730
+ mpuSequenceNumber: sym.fragment.mpuSequenceNumber,
2731
+ ...(sym.fragment.movieFragmentSequenceNumber === undefined
2732
+ ? {}
2733
+ : { movieFragmentSequenceNumber: sym.fragment.movieFragmentSequenceNumber }),
2734
+ ...(sym.fragment.sampleNumber === undefined
2735
+ ? {}
2736
+ : { sampleNumber: sym.fragment.sampleNumber }),
2737
+ ...(catalogTrackName === undefined ? {} : { catalogTrackName }),
2738
+ // A single-packet MFU needs the block verdict just as much as a
2739
+ // reassembled one. The multi-fragment branch below gets it via the
2740
+ // reassembler fold (`assembled.deliveryProvenance`); this branch
2741
+ // never touches the reassembler, so it must carry the block-level
2742
+ // verdict directly or the sample reaches MSE unattributed.
2743
+ deliveryProvenance,
2744
+ ...(sourceDiscontinuity ? { sourceDiscontinuity: true } : {}),
2745
+ });
2746
+ this.#noteEmittedKeyframe(ownerPacketId, keyframe);
2747
+ } else {
2748
+ // Multi-packet MFU. Feed to reassembler; emit only when complete.
2749
+ // A decoded block contains every source symbol, including fragments
2750
+ // already accepted on the direct path. Re-feeding those fragments as
2751
+ // recovered would replace their original attribution in the shared
2752
+ // reassembler and overstate repairedPacketSequenceNumbers.
2753
+ if (this.#sourcePktSeqs.has(
2754
+ this.#packetSeqKey(ownerPacketId, sym.packetSequenceNumber, catalogTrackName),
2755
+ )) {
2756
+ dedupedAgainstSource++;
2757
+ continue;
2758
+ }
2759
+ let assembled;
2760
+ try {
2761
+ assembled = this.#parser.reassembler.addFragment({
2762
+ fragmentationIndicator: fi,
2763
+ mpuSequenceNumber: sym.fragment.mpuSequenceNumber,
2764
+ fragmentCounter: sym.fragment.fragmentCounter,
2765
+ data: sym.fragment.data,
2766
+ timestamp: sym.fragment.timestamp,
2767
+ rapFlag: sym.fragment.rapFlag,
2768
+ sampleNumber: sym.fragment.sampleNumber,
2769
+ movieFragmentSequenceNumber: sym.fragment.movieFragmentSequenceNumber,
2770
+ sampleOffset: sym.fragment.sampleOffset,
2771
+ priority: sym.fragment.priority,
2772
+ packetSequenceNumber: sym.packetSequenceNumber,
2773
+ recovered: true,
2774
+ // Without this the fold sees a fragment with neither `source`
2775
+ // nor a verdict, returns 'unknown', and the block-level finding
2776
+ // computed above is lost before the frame reaches any consumer.
2777
+ deliveryProvenance,
2778
+ packetId: ownerPacketId,
2779
+ ...(catalogTrackName === undefined ? {} : { catalogTrackName }),
2780
+ });
2781
+ } catch (error) {
2782
+ if (!isMfuReassemblyError(error)) throw error;
2783
+ console.warn(
2784
+ `[MmtpRouter] dropped rejected recovered MFU fragment: block=${blockId} ` +
2785
+ `packetId=${ownerPacketId} code=${error.code}`,
2786
+ );
2787
+ continue;
2788
+ }
2789
+ if (assembled) {
2790
+ const keyframe = isVideoTrack
2791
+ ? this.#detectKeyframe(
2792
+ ownerPacketId,
2793
+ assembled.data,
2794
+ isMfuKeyframeMetadata(assembled.rapFlag, assembled.priority),
2795
+ )
2796
+ : assembled.rapFlag;
2797
+ const recoveredCatalogTrackName = assembled.catalogTrackName ?? catalogTrackName;
2798
+ const sourceDiscontinuity = this.#opensFecSync(
2799
+ ownerPacketId,
2800
+ keyframe,
2801
+ recoveredCatalogTrackName,
2802
+ );
2803
+ if (!this.#shouldEmitFrame(
2804
+ ownerPacketId,
2805
+ keyframe,
2806
+ recoveredCatalogTrackName,
2807
+ )) {
2808
+ droppedBeforeSync++;
2809
+ continue;
2810
+ }
2811
+ const recoveredTiming = this.#resolveRecoveredTiming(
2812
+ ownerPacketId,
2813
+ assembled.mpuSequenceNumber,
2814
+ assembled.movieFragmentSequenceNumber,
2815
+ assembled.sampleNumber,
2816
+ assembled.timestamp,
2817
+ recoveredCatalogTrackName,
2818
+ );
2819
+ if (!recoveredTiming) continue;
2820
+ frames.push({
2821
+ data: assembled.data,
2822
+ timestamp: recoveredTiming.timestamp,
2823
+ mmtpTimestamp: recoveredTiming.mmtpTimestamp,
2824
+ keyframe,
2825
+ authoredTiming: recoveredTiming.authoredTiming,
2826
+ // The singular delivery identity is the fragment that completed
2827
+ // and delivered the MFU, matching the callback/parser path.
2828
+ packetSequenceNumber: sym.packetSequenceNumber,
2829
+ ...(assembled.packetSequenceNumbers === undefined
2830
+ ? {}
2831
+ : { packetSequenceNumbers: assembled.packetSequenceNumbers }),
2832
+ ...(assembled.repairedPacketSequenceNumbers === undefined
2833
+ ? {}
2834
+ : {
2835
+ repairedPacketSequenceNumbers:
2836
+ assembled.repairedPacketSequenceNumbers,
2837
+ }),
2838
+ mpuSequenceNumber: assembled.mpuSequenceNumber,
2839
+ ...(assembled.movieFragmentSequenceNumber === undefined
2840
+ ? {}
2841
+ : { movieFragmentSequenceNumber: assembled.movieFragmentSequenceNumber }),
2842
+ ...(assembled.sampleNumber === undefined
2843
+ ? {}
2844
+ : { sampleNumber: assembled.sampleNumber }),
2845
+ ...(assembled.catalogTrackName === undefined
2846
+ ? {}
2847
+ : { catalogTrackName: assembled.catalogTrackName }),
2848
+ deliveryProvenance: assembled.deliveryProvenance,
2849
+ ...(sourceDiscontinuity ? { sourceDiscontinuity: true } : {}),
2850
+ });
2851
+ this.#noteEmittedKeyframe(ownerPacketId, keyframe);
2852
+ }
2853
+ }
2854
+ }
2855
+ if (frames.length > 0) {
2856
+ assembler.addRecoveredBlock(blockId, frames);
2857
+ // The assembler owns MSE writes, but the normal frame callback still
2858
+ // carries transport admission: MulticastManager uses it to confirm
2859
+ // video/audio and MediaRouter uses that confirmation to complete the
2860
+ // takeover. Recovered frames bypass parser.onFrame, so without this
2861
+ // callback MSE can receive samples forever while takeover never fires.
2862
+ const cb = this.#getFrameCallback(ownerPacketId);
2863
+ for (const frame of frames) {
2864
+ cb?.(frame.data, {
2865
+ timestamp: frame.timestamp,
2866
+ ...(frame.mmtpTimestamp === undefined ? {} : { mmtpTimestamp: frame.mmtpTimestamp }),
2867
+ keyframe: frame.keyframe,
2868
+ ...(frame.authoredTiming === undefined ? {} : { authoredTiming: frame.authoredTiming }),
2869
+ packetId: ownerPacketId,
2870
+ ...(frame.packetSequenceNumber === undefined
2871
+ ? {}
2872
+ : { packetSequenceNumber: frame.packetSequenceNumber }),
2873
+ ...(frame.packetSequenceNumbers === undefined
2874
+ ? {}
2875
+ : { packetSequenceNumbers: frame.packetSequenceNumbers }),
2876
+ ...(frame.repairedPacketSequenceNumbers === undefined
2877
+ ? {}
2878
+ : { repairedPacketSequenceNumbers: frame.repairedPacketSequenceNumbers }),
2879
+ ...(frame.mpuSequenceNumber === undefined ? {} : { mpuSequenceNumber: frame.mpuSequenceNumber }),
2880
+ ...(frame.movieFragmentSequenceNumber === undefined
2881
+ ? {}
2882
+ : { movieFragmentSequenceNumber: frame.movieFragmentSequenceNumber }),
2883
+ ...(frame.sampleNumber === undefined ? {} : { sampleNumber: frame.sampleNumber }),
2884
+ ...(frame.catalogTrackName === undefined ? {} : { catalogTrackName: frame.catalogTrackName }),
2885
+ recovered: true,
2886
+ deliveryProvenance: frame.deliveryProvenance,
2887
+ ...(frame.sourceDiscontinuity ? { sourceDiscontinuity: true } : {}),
2888
+ });
2889
+ }
2890
+ debugRouter(
2891
+ `[MmtpRouter] Block ${blockId}: ${result.symbols.length} symbols → ${frames.length} reassembled frames → MSE assembler (packetId=${ownerPacketId}, dedupedAgainstSource=${dedupedAgainstSource}, droppedBeforeSync=${droppedBeforeSync})`,
2892
+ );
2893
+ } else if (result.symbols.length > 0) {
2894
+ // Recovery yielded symbols but no complete frames emerged — either all
2895
+ // FI=0 were source-path duplicates, or all FI=1/2/3 are still mid-
2896
+ // reassembly. Surface this so an operator debugging "FEC recovered N
2897
+ // symbols but no new frames rendered" has a signal.
2898
+ debugRouter(
2899
+ `[MmtpRouter] Block ${blockId}: ${result.symbols.length} symbols recovered → 0 emitted (dedupedAgainstSource=${dedupedAgainstSource}, droppedBeforeSync=${droppedBeforeSync}, pending-reassembly=${result.symbols.length - dedupedAgainstSource - droppedBeforeSync}, packetId=${ownerPacketId})`,
2900
+ );
2901
+ }
2902
+ return;
2903
+ }
2904
+
2905
+ const cb = this.#getFrameCallback(ownerPacketId);
2906
+ const isVideo = this.#classifyPacketId(ownerPacketId) === true;
2907
+ let droppedBeforeSync = 0;
2908
+ let droppedStale = 0;
2909
+ let dedupedAgainstSource = 0;
2910
+
2911
+ for (const sym of result.symbols) {
2912
+ // FT=0 init already delivered above — skip data routing
2913
+ if (sym.fragmentType === 0 || sym.fragmentType === 1) continue;
2914
+ // FT=0/FT=1 are metadata, not decoder samples. The metadata pass above
2915
+ // has already used them to establish the timed-MFU contract.
2916
+
2917
+ if (isVideo) {
2918
+ const fi = sym.fragment.fragmentationIndicator;
2919
+ if (fi === 0) {
2920
+ // Dedup: skip FI=0 frames already received via direct source path.
2921
+ // The reassembler deduplicates FI=1/2/3 fragments via #emitted, but
2922
+ // FI=0 (single-packet MFU) bypasses it. Without this check, recovery
2923
+ // can re-emit a source frame that is only held in the reorder buffer,
2924
+ // producing duplicate timestamps when the source path later flushes.
2925
+ if (this.#sourcePktSeqs.has(
2926
+ this.#packetSeqKey(ownerPacketId, sym.packetSequenceNumber, catalogTrackName),
2927
+ )) {
2928
+ dedupedAgainstSource++;
2929
+ continue;
2930
+ }
2931
+ if (this.#isStaleRecoveredVideo(
2932
+ ownerPacketId,
2933
+ sym.packetSequenceNumber,
2934
+ catalogTrackName,
2935
+ )) {
2936
+ droppedStale++;
2937
+ continue;
2938
+ }
2939
+
2940
+ const d = sym.fragment.data;
2941
+ const keyframe = this.#detectKeyframe(
2942
+ ownerPacketId,
2943
+ d,
2944
+ isMfuKeyframeMetadata(sym.fragment.rapFlag, sym.fragment.priority),
2945
+ );
2946
+ const recoveredTiming = this.#resolveRecoveredTiming(
2947
+ ownerPacketId,
2948
+ sym.fragment.mpuSequenceNumber,
2949
+ sym.fragment.movieFragmentSequenceNumber,
2950
+ sym.fragment.sampleNumber,
2951
+ sym.fragment.timestamp,
2952
+ catalogTrackName,
2953
+ );
2954
+ if (!recoveredTiming) continue;
2955
+ this.#parser.routeFrame(d, {
2956
+ timestamp: recoveredTiming.timestamp,
2957
+ mmtpTimestamp: recoveredTiming.mmtpTimestamp,
2958
+ authoredTiming: recoveredTiming.authoredTiming,
2959
+ keyframe,
2960
+ packetId: ownerPacketId,
2961
+ trackType: "video",
2962
+ packetSequenceNumber: sym.packetSequenceNumber,
2963
+ packetSequenceNumbers: [sym.packetSequenceNumber],
2964
+ repairedPacketSequenceNumbers: [sym.packetSequenceNumber],
2965
+ mpuSequenceNumber: sym.fragment.mpuSequenceNumber,
2966
+ ...(sym.fragment.movieFragmentSequenceNumber === undefined
2967
+ ? {}
2968
+ : { movieFragmentSequenceNumber: sym.fragment.movieFragmentSequenceNumber }),
2969
+ ...(sym.fragment.sampleNumber === undefined
2970
+ ? {}
2971
+ : { sampleNumber: sym.fragment.sampleNumber }),
2972
+ recovered: true,
2973
+ deliveryProvenance,
2974
+ ...(catalogTrackName === undefined ? {} : { catalogTrackName }),
2975
+ });
2976
+ } else {
2977
+ // Fragment of a multi-packet MFU — must go through reassembler
2978
+ if (this.#sourcePktSeqs.has(
2979
+ this.#packetSeqKey(ownerPacketId, sym.packetSequenceNumber, catalogTrackName),
2980
+ )) {
2981
+ dedupedAgainstSource++;
2982
+ continue;
2983
+ }
2984
+ let assembled;
2985
+ try {
2986
+ assembled = this.#parser.reassembler.addFragment({
2987
+ fragmentationIndicator: fi,
2988
+ mpuSequenceNumber: sym.fragment.mpuSequenceNumber,
2989
+ fragmentCounter: sym.fragment.fragmentCounter,
2990
+ data: sym.fragment.data,
2991
+ timestamp: sym.fragment.timestamp,
2992
+ rapFlag: sym.fragment.rapFlag,
2993
+ sampleNumber: sym.fragment.sampleNumber,
2994
+ movieFragmentSequenceNumber: sym.fragment.movieFragmentSequenceNumber,
2995
+ sampleOffset: sym.fragment.sampleOffset,
2996
+ priority: sym.fragment.priority,
2997
+ packetSequenceNumber: sym.packetSequenceNumber,
2998
+ recovered: true,
2999
+ // Without this the fold sees a fragment with neither `source`
3000
+ // nor a verdict, returns 'unknown', and the block-level finding
3001
+ // computed above is lost before the frame reaches any consumer.
3002
+ deliveryProvenance,
3003
+ packetId: ownerPacketId,
3004
+ ...(catalogTrackName === undefined ? {} : { catalogTrackName }),
3005
+ });
3006
+ } catch (error) {
3007
+ if (!isMfuReassemblyError(error)) throw error;
3008
+ console.warn(
3009
+ `[MmtpRouter] dropped rejected recovered MFU fragment: block=${blockId} ` +
3010
+ `packetId=${ownerPacketId} code=${error.code}`,
3011
+ );
3012
+ continue;
3013
+ }
3014
+ if (assembled) {
3015
+ if (this.#isStaleRecoveredVideo(
3016
+ ownerPacketId,
3017
+ sym.packetSequenceNumber,
3018
+ assembled.catalogTrackName,
3019
+ )) {
3020
+ droppedStale++;
3021
+ continue;
3022
+ }
3023
+ const keyframe = this.#detectKeyframe(
3024
+ ownerPacketId,
3025
+ assembled.data,
3026
+ isMfuKeyframeMetadata(assembled.rapFlag, assembled.priority),
3027
+ );
3028
+ const recoveredCatalogTrackName = assembled.catalogTrackName ?? catalogTrackName;
3029
+ const recoveredTiming = this.#resolveRecoveredTiming(
3030
+ ownerPacketId,
3031
+ assembled.mpuSequenceNumber,
3032
+ assembled.movieFragmentSequenceNumber,
3033
+ assembled.sampleNumber,
3034
+ assembled.timestamp,
3035
+ recoveredCatalogTrackName,
3036
+ );
3037
+ if (!recoveredTiming) continue;
3038
+ this.#parser.routeFrame(assembled.data, {
3039
+ timestamp: recoveredTiming.timestamp,
3040
+ mmtpTimestamp: recoveredTiming.mmtpTimestamp,
3041
+ authoredTiming: recoveredTiming.authoredTiming,
3042
+ keyframe,
3043
+ packetId: ownerPacketId,
3044
+ trackType: "video",
3045
+ packetSequenceNumber: sym.packetSequenceNumber,
3046
+ ...(assembled.packetSequenceNumbers === undefined
3047
+ ? {}
3048
+ : { packetSequenceNumbers: assembled.packetSequenceNumbers }),
3049
+ ...(assembled.repairedPacketSequenceNumbers === undefined
3050
+ ? {}
3051
+ : {
3052
+ repairedPacketSequenceNumbers:
3053
+ assembled.repairedPacketSequenceNumbers,
3054
+ }),
3055
+ mpuSequenceNumber: assembled.mpuSequenceNumber,
3056
+ ...(assembled.movieFragmentSequenceNumber === undefined
3057
+ ? {}
3058
+ : { movieFragmentSequenceNumber: assembled.movieFragmentSequenceNumber }),
3059
+ ...(assembled.sampleNumber === undefined
3060
+ ? {}
3061
+ : { sampleNumber: assembled.sampleNumber }),
3062
+ recovered: true,
3063
+ deliveryProvenance,
3064
+ ...(assembled.catalogTrackName === undefined
3065
+ ? {}
3066
+ : { catalogTrackName: assembled.catalogTrackName }),
3067
+ });
3068
+ }
3069
+ }
3070
+ } else {
3071
+ // Audio recovery: dedup single-packet MFUs already emitted via direct source path
3072
+ // (parity with video FI=0 path at line 993). Prevents re-delivering audio frames
3073
+ // with duplicate timestamps — doubled samples in the FIFO ring buffer corrupt the
3074
+ // 1kHz test tone (pitch/energy instability in FFT spectrum).
3075
+ if (this.#sourcePktSeqs.has(
3076
+ this.#packetSeqKey(ownerPacketId, sym.packetSequenceNumber, catalogTrackName),
3077
+ )) {
3078
+ dedupedAgainstSource++;
3079
+ continue;
3080
+ }
3081
+ const recoveredTiming = this.#resolveRecoveredTiming(
3082
+ ownerPacketId,
3083
+ sym.fragment.mpuSequenceNumber,
3084
+ sym.fragment.movieFragmentSequenceNumber,
3085
+ sym.fragment.sampleNumber,
3086
+ sym.fragment.timestamp,
3087
+ catalogTrackName,
3088
+ );
3089
+ if (recoveredTiming === null) continue;
3090
+ const meta = {
3091
+ timestamp: recoveredTiming.timestamp,
3092
+ mmtpTimestamp: recoveredTiming.mmtpTimestamp,
3093
+ authoredTiming: recoveredTiming.authoredTiming,
3094
+ keyframe: true,
3095
+ packetId: ownerPacketId,
3096
+ packetSequenceNumber: sym.packetSequenceNumber,
3097
+ packetSequenceNumbers: [sym.packetSequenceNumber],
3098
+ repairedPacketSequenceNumbers: [sym.packetSequenceNumber],
3099
+ mpuSequenceNumber: sym.fragment.mpuSequenceNumber,
3100
+ ...(sym.fragment.movieFragmentSequenceNumber === undefined
3101
+ ? {}
3102
+ : { movieFragmentSequenceNumber: sym.fragment.movieFragmentSequenceNumber }),
3103
+ ...(sym.fragment.sampleNumber === undefined
3104
+ ? {}
3105
+ : { sampleNumber: sym.fragment.sampleNumber }),
3106
+ recovered: true,
3107
+ ...(catalogTrackName === undefined ? {} : { catalogTrackName }),
3108
+ };
3109
+ this.#routeDirectRecoveredAudio(
3110
+ ownerPacketId,
3111
+ sym.fragment.data,
3112
+ meta,
3113
+ cb,
3114
+ );
3115
+ }
3116
+ }
3117
+ if (result.symbols.length > 0) {
3118
+ debugRouter(
3119
+ `[MmtpRouter] Block ${blockId}: ${result.symbols.length} symbols recovered (packetId=${ownerPacketId}, dedupedAgainstSource=${dedupedAgainstSource}, droppedBeforeSync=${droppedBeforeSync}, droppedStale=${droppedStale})`,
3120
+ );
3121
+ }
3122
+ }
3123
+
3124
+ // ── Private: keyframe detection ─────────────────────────────────
3125
+
3126
+ /** Codec-aware keyframe detection from bitstream data.
3127
+ * H.264 IDR (NAL type 5/7), AV1 OBU_SEQUENCE_HEADER (type 1), VP9 frame_type bit. */
3128
+ #detectKeyframe(
3129
+ packetId: number,
3130
+ data: Uint8Array,
3131
+ rapFlag = false,
3132
+ ): boolean {
3133
+ const codec = this.#trackCodecs.get(packetId) ?? "";
3134
+ return MmtpContainerParser.isKeyframe(data, rapFlag, codec);
3135
+ }
3136
+ }