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

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