@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,2842 @@
1
+ /**
2
+ * @mmt/container - MFU Fragment Reassembler
3
+ *
4
+ * Handles authored fragmented MFUs (Media Fragment Units) according to
5
+ * ISO/IEC 23008-1:2023 Section 9.2.3.3. Fragmented runs are admitted in the
6
+ * exact FI/fragment_counter countdown carried on the wire. Transport-run
7
+ * association additionally requires the repository's contiguous-source
8
+ * producer profile; ISO-permitted interleaving remains incomplete and fails
9
+ * closed.
10
+ *
11
+ * Fragmentation Indicator (FI) values:
12
+ * - 0: Complete MFU (single packet, no fragmentation)
13
+ * - 1: First fragment
14
+ * - 2: Middle fragment
15
+ * - 3: Last fragment
16
+ *
17
+ * @packageDocumentation
18
+ */
19
+
20
+ import {
21
+ extractMFUPayload,
22
+ isFecRepairPacket,
23
+ } from '@blockcast/mmt-base'
24
+
25
+ /** Source transport admitted by the MMTP router. */
26
+ export type MmtpSourcePath = 'multicast' | 'moq'
27
+
28
+ /**
29
+ * Strict whole-frame delivery provenance, folded over EVERY contributing
30
+ * fragment rather than the head fragment alone.
31
+ *
32
+ * This is deliberately NOT `source`. `source` answers a routing question —
33
+ * "is this the reliable-MoQ copy that must not confirm multicast takeover?" —
34
+ * and head-fragment-wins is the documented, deterministic answer to it.
35
+ * Provenance answers an evidence question: "did every byte of this frame
36
+ * physically arrive over multicast?" A frame with a multicast head and a
37
+ * QUIC-delivered tail is a legitimate routing win for `source: 'multicast'`
38
+ * and is emphatically NOT multicast-exclusive delivery.
39
+ *
40
+ * - 'multicast' — every contributing fragment was explicitly multicast.
41
+ * The ONLY value that may be counted as physical-delivery evidence.
42
+ * - 'mixed' — at least one contributing fragment arrived over MoQ.
43
+ * - 'unknown' — no fragment arrived over MoQ, but at least one carried no
44
+ * attribution at all. Absence is preserved as its own state rather than
45
+ * defaulted, so a consumer cannot mistake "never recorded" for "proven".
46
+ */
47
+ export type MmtpDeliveryProvenance = 'multicast' | 'mixed' | 'unknown'
48
+
49
+ /**
50
+ * Fold one fragment's `source` into a running provenance verdict.
51
+ *
52
+ * Fail-closed, order-independent and associative: 'mixed' absorbs everything,
53
+ * a missing attribution degrades 'multicast' to 'unknown', and nothing ever
54
+ * upgrades a verdict. Fold over an empty set is 'unknown' — no fragment ever
55
+ * proved anything.
56
+ */
57
+ export function foldDeliveryProvenance(
58
+ verdict: MmtpDeliveryProvenance,
59
+ source: MmtpSourcePath | undefined,
60
+ ): MmtpDeliveryProvenance {
61
+ if (verdict === 'mixed') return 'mixed'
62
+ if (source === 'moq') return 'mixed'
63
+ if (source === undefined) return 'unknown'
64
+ return verdict
65
+ }
66
+
67
+ /**
68
+ * One fragment's own provenance.
69
+ *
70
+ * An explicit `deliveryProvenance` wins: FEC-recovered fragments carry no
71
+ * `source` (they were never delivered as themselves) but their block's symbol
72
+ * provenance IS known, and the router supplies it. Falling back to `source`
73
+ * keeps directly-delivered fragments working unchanged.
74
+ */
75
+ function fragmentProvenance(fragment: {
76
+ source?: MmtpSourcePath
77
+ deliveryProvenance?: MmtpDeliveryProvenance
78
+ }): MmtpDeliveryProvenance {
79
+ if (fragment.deliveryProvenance !== undefined) return fragment.deliveryProvenance
80
+ if (fragment.source === 'moq') return 'mixed'
81
+ if (fragment.source === 'multicast') return 'multicast'
82
+ return 'unknown'
83
+ }
84
+
85
+ /** Strict provenance for a complete fragment set. */
86
+ export function deliveryProvenanceOf(
87
+ fragments: readonly {
88
+ source?: MmtpSourcePath
89
+ deliveryProvenance?: MmtpDeliveryProvenance
90
+ }[],
91
+ ): MmtpDeliveryProvenance {
92
+ if (fragments.length === 0) return 'unknown'
93
+ let verdict: MmtpDeliveryProvenance = 'multicast'
94
+ for (const fragment of fragments) {
95
+ const own = fragmentProvenance(fragment)
96
+ verdict =
97
+ verdict === 'mixed' || own === 'mixed'
98
+ ? 'mixed'
99
+ : verdict === 'unknown' || own === 'unknown'
100
+ ? 'unknown'
101
+ : 'multicast'
102
+ }
103
+ return verdict
104
+ }
105
+
106
+ /**
107
+ * MFU Fragment for reassembly
108
+ */
109
+ export interface MFUFragment {
110
+ fragmentationIndicator: number
111
+ mpuSequenceNumber: number
112
+ fragmentCounter: number
113
+ data: Uint8Array
114
+ timestamp: bigint
115
+ /** Random Access Point flag from MMTP header - indicates keyframe */
116
+ rapFlag: boolean
117
+ /** MFU DU priority. FFmpeg marks random-access samples with high priority. */
118
+ priority?: number
119
+ sampleNumber?: number
120
+ movieFragmentSequenceNumber?: number
121
+ sampleOffset?: number
122
+ /** Non-timed MFU Item_ID from the 4-byte DU header. */
123
+ itemId?: number
124
+ /** Authored u32 MMTP packet_sequence_number, including on recovered packets. */
125
+ packetSequenceNumber?: number
126
+ /** True when this accepted source packet was reconstructed by FEC. */
127
+ recovered?: boolean
128
+ /** Transport that supplied this accepted fragment. Absent for recovered data. */
129
+ source?: MmtpSourcePath
130
+ /**
131
+ * Explicit provenance for this fragment, overriding what `source` implies.
132
+ * Set by the router for FEC-recovered fragments, which have no `source` of
133
+ * their own but whose FEC block's symbol provenance is known.
134
+ */
135
+ deliveryProvenance?: MmtpDeliveryProvenance
136
+ /** Immutable catalog identity supplied by the owning ingress subscription. */
137
+ catalogTrackName?: string
138
+ /** Physical MMTP stream identity when no catalog identity is available. */
139
+ packetId?: number
140
+ }
141
+
142
+ /**
143
+ * Reassembled MFU result
144
+ */
145
+ export interface ReassembledMFU {
146
+ mpuSequenceNumber: number
147
+ /** MMTP packet_sequence_number of the first fragment in delivery order. */
148
+ packetSequenceNumber?: number
149
+ /** Every authoritative MMTP source-packet sequence that formed this MFU. */
150
+ packetSequenceNumbers?: number[]
151
+ /** Subset of packetSequenceNumbers reconstructed by FEC. */
152
+ repairedPacketSequenceNumbers?: number[]
153
+ data: Uint8Array
154
+ timestamp: bigint
155
+ fragmentCount: number
156
+ /** Random Access Point flag - indicates keyframe (from first fragment) */
157
+ rapFlag: boolean
158
+ /** MFU DU priority from the first fragment that carried it. */
159
+ priority?: number
160
+ sampleNumber?: number
161
+ movieFragmentSequenceNumber?: number
162
+ /**
163
+ * This DU's authored base byte offset within its sample (moqcast #37). 0
164
+ * for a whole-sample DU; nonzero identifies one subsample DU among
165
+ * siblings partitioning an oversized sample. Constant across the DU's own
166
+ * fragments — feed into a sample combiner to reassemble the full sample.
167
+ */
168
+ sampleOffset?: number
169
+ /**
170
+ * Transport that supplied the accepted FI=1 head fragment (or the complete
171
+ * FI=0 frame). This is the deterministic winner for mixed-path assembly.
172
+ *
173
+ * ROUTING ONLY. It describes one fragment, so it is not evidence about the
174
+ * whole frame — read {@link ReassembledMFU.deliveryProvenance} for that.
175
+ */
176
+ source?: MmtpSourcePath
177
+ /**
178
+ * Strict provenance folded over every contributing fragment. Only
179
+ * 'multicast' proves multicast-exclusive delivery of this frame.
180
+ */
181
+ deliveryProvenance: MmtpDeliveryProvenance
182
+ /** Catalog identity from the accepted FI=1 head fragment, or FI=0 frame. */
183
+ catalogTrackName?: string
184
+ }
185
+
186
+ /**
187
+ * Reassembler statistics
188
+ */
189
+ export interface ReassemblerStats {
190
+ totalFragments: number
191
+ reassembledMFUs: number
192
+ /** Fragments rejected by strict authored DU-state validation. */
193
+ rejectedFragments: number
194
+ /** Incomplete MFUs discarded after their configured deadline. */
195
+ timedOutMFUs: number
196
+ /** Incomplete MFUs discarded to enforce the configured memory bound. */
197
+ evictedMemoryLimit: number
198
+ outOfOrderFragments: number
199
+ }
200
+
201
+ export const MFU_REASSEMBLY_ERROR_CODES = [
202
+ 'invalid-fragment-state',
203
+ 'missing-data-unit-identity',
204
+ 'missing-packet-sequence',
205
+ 'conflicting-data-unit-identity',
206
+ 'overlapping-data-unit',
207
+ 'fragment-counter-mismatch',
208
+ 'data-unit-identity-mismatch',
209
+ 'offset-overflow',
210
+ 'active-data-unit-limit',
211
+ 'incomplete-terminal-fragment',
212
+ ] as const
213
+
214
+ export type MfuReassemblyErrorCode = typeof MFU_REASSEMBLY_ERROR_CODES[number]
215
+
216
+ const mfuReassemblyErrorCodes = new Set<string>(MFU_REASSEMBLY_ERROR_CODES)
217
+
218
+ export function isMfuReassemblyErrorCode(value: unknown): value is MfuReassemblyErrorCode {
219
+ return typeof value === 'string' && mfuReassemblyErrorCodes.has(value)
220
+ }
221
+
222
+ export class MfuReassemblyError extends Error {
223
+ readonly name = 'MfuReassemblyError'
224
+
225
+ constructor(readonly code: MfuReassemblyErrorCode, message: string) {
226
+ super(message)
227
+ }
228
+ }
229
+
230
+ function assembledPacketSequences(fragments: readonly MFUFragment[]): {
231
+ packetSequenceNumbers?: number[]
232
+ repairedPacketSequenceNumbers?: number[]
233
+ } {
234
+ const packetSequenceNumbers: number[] = []
235
+ const repairedPacketSequenceNumbers: number[] = []
236
+ const seen = new Set<number>()
237
+ const repaired = new Set<number>()
238
+ for (const fragment of fragments) {
239
+ const sequence = fragment.packetSequenceNumber
240
+ if (typeof sequence !== 'number' || !Number.isSafeInteger(sequence) ||
241
+ sequence < 0 || sequence > 0xffffffff) {
242
+ return {}
243
+ }
244
+ if (!seen.has(sequence)) {
245
+ seen.add(sequence)
246
+ packetSequenceNumbers.push(sequence)
247
+ }
248
+ if (fragment.recovered === true && !repaired.has(sequence)) {
249
+ repaired.add(sequence)
250
+ repairedPacketSequenceNumbers.push(sequence)
251
+ }
252
+ }
253
+ return {packetSequenceNumbers, repairedPacketSequenceNumbers}
254
+ }
255
+
256
+ export interface PendingMfuDebug {
257
+ key: string
258
+ fragments: number
259
+ fi0: number
260
+ fi1: number
261
+ fi2: number
262
+ fi3: number
263
+ firstFi: number
264
+ lastFi: number
265
+ firstOrder: number
266
+ lastOrder: number
267
+ minCounter: number
268
+ maxCounter: number
269
+ minOffset?: number
270
+ maxOffset?: number
271
+ movieFragmentSequenceNumber?: number
272
+ sampleNumber?: number
273
+ mpuSequenceNumber?: number
274
+ sortedHead: Array<{ fi: number; counter: number; order: number; seq?: number; offset?: number }>
275
+ sortedTail: Array<{ fi: number; counter: number; order: number; seq?: number; offset?: number }>
276
+ }
277
+
278
+ export interface MFUReassemblerOptions {
279
+ /** Timeout (ms) for incomplete MFUs. Derive from frame duration or FEC interleaveDepthMs.
280
+ * Pass null for live clock-free operation; memory remains bounded by maxBuffered. */
281
+ timeoutMs: number | null
282
+ /** Max concurrent incomplete MFUs, including continuations awaiting FI=1. */
283
+ maxBuffered?: number
284
+ }
285
+
286
+ /** Reassembly surface consumed by MmtpContainerParser and MmtpRouter.
287
+ * Implemented by both the TypeScript engine and the shared
288
+ * Rust/WASM IFC adapter. */
289
+ export interface MfuReassemblerBackend {
290
+ timeoutMs: number | null
291
+ readonly stats: ReassemblerStats
292
+ onDeferredAssembly?: (result: ReassembledMFU) => void
293
+ /** Returns null only for a valid incomplete DU; strict rejection throws
294
+ * `MfuReassemblyError`. */
295
+ addFragment(fragment: MFUFragment): ReassembledMFU | null
296
+ flush(): void
297
+ resetStream(catalogTrackName: string): void
298
+ reset(): void
299
+ readonly pendingCount: number
300
+ getStatus(): {
301
+ bufferedMFUs: number
302
+ pendingFragments: number
303
+ pendingSamples: PendingMfuDebug[]
304
+ stats: ReassemblerStats
305
+ }
306
+ }
307
+
308
+ function streamScope(fragment: MFUFragment): readonly [kind: string, identity: string | number | null] {
309
+ if (fragment.catalogTrackName !== undefined) return ['catalog', fragment.catalogTrackName]
310
+ if (fragment.packetId !== undefined) return ['packet', fragment.packetId]
311
+ return ['unspecified', null]
312
+ }
313
+
314
+ function isU32(value: unknown): value is number {
315
+ return typeof value === 'number' && Number.isInteger(value) && value >= 0 && value <= 0xffffffff
316
+ }
317
+
318
+ /**
319
+ * Producer-profile transport coordinate shared by every contiguous source
320
+ * fragment in one data-unit run. This is association state only; objectKey
321
+ * remains the intrinsic DU identity. ISO-permitted interleaving creates a
322
+ * different key and therefore cannot cross-associate samples.
323
+ */
324
+ function fragmentRunKey(fragment: MFUFragment): string {
325
+ if (!isU32(fragment.packetSequenceNumber)) {
326
+ throw new Error('[MFUReassembler] fragmented value has no validated packet sequence')
327
+ }
328
+ const terminalPacketSequence =
329
+ (fragment.packetSequenceNumber + fragment.fragmentCounter) % 0x1_0000_0000
330
+ return JSON.stringify([
331
+ ...streamScope(fragment),
332
+ fragment.mpuSequenceNumber,
333
+ terminalPacketSequence,
334
+ ])
335
+ }
336
+
337
+ function hasAnySampleIdentityField(fragment: MFUFragment): boolean {
338
+ return fragment.movieFragmentSequenceNumber !== undefined ||
339
+ fragment.sampleNumber !== undefined || fragment.sampleOffset !== undefined
340
+ }
341
+
342
+ type IdentifiedSampleFragment = MFUFragment & Required<Pick<
343
+ MFUFragment,
344
+ 'movieFragmentSequenceNumber' | 'sampleNumber' | 'sampleOffset'
345
+ >>
346
+
347
+ function hasSampleIdentity(fragment: MFUFragment): fragment is IdentifiedSampleFragment {
348
+ return isU32(fragment.movieFragmentSequenceNumber) && fragment.movieFragmentSequenceNumber > 0 &&
349
+ isU32(fragment.sampleNumber) && fragment.sampleNumber > 0 && isU32(fragment.sampleOffset)
350
+ }
351
+
352
+ function hasValidFragmentHeader(fragment: MFUFragment): boolean {
353
+ const { fragmentationIndicator: fi, fragmentCounter: counter } = fragment
354
+ if (!Number.isInteger(fi) || fi < 0 || fi > 3) return false
355
+ if (!Number.isInteger(counter) || counter < 0 || counter > 0xff) return false
356
+ return fi === 0 || fi === 3 ? counter === 0 : counter > 0
357
+ }
358
+
359
+ function fragmentCounterOrder(fragment: MFUFragment): number {
360
+ return 0xff - fragment.fragmentCounter
361
+ }
362
+
363
+ function objectKey(fragment: MFUFragment): string {
364
+ const scope = streamScope(fragment)
365
+ if (fragment.itemId !== undefined) {
366
+ return JSON.stringify([
367
+ ...scope,
368
+ fragment.mpuSequenceNumber,
369
+ 'item',
370
+ fragment.itemId,
371
+ ])
372
+ }
373
+ if (hasSampleIdentity(fragment)) {
374
+ return JSON.stringify([
375
+ ...scope,
376
+ fragment.mpuSequenceNumber,
377
+ 'sample',
378
+ fragment.movieFragmentSequenceNumber,
379
+ fragment.sampleNumber,
380
+ // The DU's authored base offset (moqcast #37): sibling subsample DUs
381
+ // of one sample get distinct keys instead of colliding. Only ever
382
+ // called with the unmutated FI=1 (or FI=0) fragment, whose
383
+ // sampleOffset is that authored base -- never the accumulating
384
+ // per-continuation value written by #associateDataUnit.
385
+ fragment.sampleOffset,
386
+ ])
387
+ }
388
+ return JSON.stringify([
389
+ ...scope,
390
+ fragment.mpuSequenceNumber,
391
+ 'unidentified',
392
+ ])
393
+ }
394
+
395
+ function ordersByOffset(fragments: MFUFragment[]): boolean {
396
+ return fragments.every((fragment) => hasSampleIdentity(fragment))
397
+ }
398
+
399
+ type ActiveDataUnit = {
400
+ bufferKey: string
401
+ nextFragmentCounter: number
402
+ catalogTrackName?: string
403
+ } & (
404
+ | {
405
+ kind: 'sample'
406
+ movieFragmentSequenceNumber: number
407
+ sampleNumber: number
408
+ nextOffset: number
409
+ }
410
+ | {
411
+ kind: 'item'
412
+ itemId: number
413
+ }
414
+ )
415
+
416
+ interface DataUnitAssociation {
417
+ bufferKey: string
418
+ first: boolean
419
+ terminal: boolean
420
+ }
421
+
422
+ interface PendingContinuationRun {
423
+ fragments: Map<number, MFUFragment>
424
+ startedAt: number
425
+ timeoutId?: ReturnType<typeof setTimeout>
426
+ }
427
+
428
+ interface BufferTimeoutState {
429
+ startedAt: number
430
+ timeoutId?: ReturnType<typeof setTimeout>
431
+ }
432
+
433
+ function checkedSampleEnd(offset: number, byteLength: number): number | null {
434
+ const end = offset + byteLength
435
+ return Number.isSafeInteger(end) && end <= 0xffffffff ? end : null
436
+ }
437
+
438
+ /**
439
+ * MFU Reassembler with bounded reordered-fragment association
440
+ * Based on ISO/IEC 23008-1:2023 Section 9.2.3.3
441
+ */
442
+ export class MFUReassembler {
443
+ // Fragment buffers use only the authored MFU DU identity. Continuations are
444
+ // admitted through bounded producer-profile runs keyed by physical stream,
445
+ // MPU, and the wrapping terminal packet sequence. This requires contiguous
446
+ // source-packet sequences within a fragmented DU; interleaving stays
447
+ // separated and incomplete. Continuations may wait in a counter-indexed queue
448
+ // until FI=1 supplies intrinsic identity; MMTP packet send time is never an
449
+ // association key.
450
+ #buffers: Map<string, MFUFragment[]> = new Map()
451
+ #timeouts: Map<string, BufferTimeoutState> = new Map()
452
+ #activeDataUnits: Map<string, ActiveDataUnit> = new Map()
453
+ #pendingContinuations: Map<string, PendingContinuationRun> = new Map()
454
+ #emitted: Set<string> = new Set() // same sample keys — reject FEC-recovered duplicates
455
+ #emittedOrder: string[] = []
456
+ #timeoutMs: number | null
457
+ #maxBuffered: number
458
+
459
+ /** Statistics for debugging */
460
+ readonly stats: ReassemblerStats = {
461
+ totalFragments: 0,
462
+ reassembledMFUs: 0,
463
+ rejectedFragments: 0,
464
+ timedOutMFUs: 0,
465
+ evictedMemoryLimit: 0,
466
+ outOfOrderFragments: 0,
467
+ }
468
+
469
+ constructor(options: MFUReassemblerOptions) {
470
+ if (options.timeoutMs !== null && (!Number.isFinite(options.timeoutMs) || options.timeoutMs <= 0)) {
471
+ throw new Error('[MFUReassembler] timeoutMs must be > 0 (derive from frame duration or FEC config)')
472
+ }
473
+ const maxBuffered = options.maxBuffered ?? 100
474
+ if (!Number.isInteger(maxBuffered) || maxBuffered <= 0) {
475
+ throw new Error('[MFUReassembler] maxBuffered must be a positive integer')
476
+ }
477
+ this.#timeoutMs = options.timeoutMs
478
+ this.#maxBuffered = maxBuffered
479
+ }
480
+
481
+ /** Update the expiry policy for all retained and future incomplete MFUs. */
482
+ set timeoutMs(ms: number | null) {
483
+ if (ms !== null && (!Number.isFinite(ms) || ms <= 0)) {
484
+ throw new Error('[MFUReassembler] timeoutMs must be > 0 (derive from frame duration or FEC config)')
485
+ }
486
+ this.#timeoutMs = ms
487
+ const now = Date.now()
488
+ for (const [bufferKey, state] of this.#timeouts) {
489
+ if (state.timeoutId !== undefined) clearTimeout(state.timeoutId)
490
+ delete state.timeoutId
491
+ if (ms !== null) {
492
+ state.timeoutId = setTimeout(
493
+ () => this.#onTimeout(bufferKey),
494
+ Math.max(0, state.startedAt + ms - now),
495
+ )
496
+ }
497
+ }
498
+ for (const [runKey, pending] of this.#pendingContinuations) {
499
+ if (pending.timeoutId !== undefined) clearTimeout(pending.timeoutId)
500
+ delete pending.timeoutId
501
+ if (ms !== null && !this.#activeDataUnits.has(runKey)) {
502
+ pending.timeoutId = setTimeout(
503
+ () => this.#onPendingTimeout(runKey),
504
+ Math.max(0, pending.startedAt + ms - now),
505
+ )
506
+ }
507
+ }
508
+ }
509
+ get timeoutMs(): number | null { return this.#timeoutMs }
510
+
511
+ /**
512
+ * Add a fragment and return reassembled MFU if complete
513
+ */
514
+ addFragment(fragment: MFUFragment): ReassembledMFU | null {
515
+ this.stats.totalFragments++
516
+
517
+ if (!hasValidFragmentHeader(fragment)) {
518
+ this.#reject('invalid-fragment-state', 'invalid MFU fragmentation indicator or counter')
519
+ }
520
+ if (fragment.itemId !== undefined && hasAnySampleIdentityField(fragment)) {
521
+ this.#reject(
522
+ 'conflicting-data-unit-identity',
523
+ 'MFU carries timed and non-timed DU identities',
524
+ )
525
+ }
526
+
527
+ const { mpuSequenceNumber, fragmentationIndicator } = fragment
528
+
529
+ // A complete FT=2 data unit still requires its intrinsic DU identity.
530
+ // The transport parser strips the deterministic timed/non-timed DU header
531
+ // before calling this layer; direct callers must provide the same identity.
532
+ // Don't check or update #emitted: in MMTP over MoQ, multiple complete MFUs
533
+ // share the same mpuSequenceNumber (all frames in a GOP have the same mpuSeq).
534
+ // Blocking FI=0 would prevent keyframe fragment assembly at the same mpuSeq.
535
+ if (fragmentationIndicator === 0) {
536
+ const sampleIdentity = hasSampleIdentity(fragment)
537
+ const hasSampleFields = hasAnySampleIdentityField(fragment)
538
+ const hasItemIdentity = fragment.itemId !== undefined
539
+ if (!sampleIdentity && !hasItemIdentity) {
540
+ this.#reject('missing-data-unit-identity', 'complete MFU has no intrinsic DU identity')
541
+ }
542
+ if (hasSampleFields && !sampleIdentity) {
543
+ this.#reject('missing-data-unit-identity', 'complete MFU has an incomplete sample identity')
544
+ }
545
+ if (hasItemIdentity && !isU32(fragment.itemId)) {
546
+ this.#reject('missing-data-unit-identity', 'complete MFU has an invalid Item_ID')
547
+ }
548
+ this.stats.reassembledMFUs++
549
+ return {
550
+ mpuSequenceNumber,
551
+ packetSequenceNumber: fragment.packetSequenceNumber,
552
+ ...assembledPacketSequences([fragment]),
553
+ data: fragment.data,
554
+ timestamp: fragment.timestamp,
555
+ fragmentCount: 1,
556
+ rapFlag: fragment.rapFlag,
557
+ priority: fragment.priority,
558
+ sampleNumber: fragment.sampleNumber,
559
+ movieFragmentSequenceNumber: fragment.movieFragmentSequenceNumber,
560
+ sampleOffset: fragment.sampleOffset,
561
+ source: fragment.source,
562
+ deliveryProvenance: deliveryProvenanceOf([fragment]),
563
+ catalogTrackName: fragment.catalogTrackName,
564
+ }
565
+ }
566
+
567
+ if (!isU32(fragment.packetSequenceNumber)) {
568
+ this.#reject(
569
+ 'missing-packet-sequence',
570
+ 'fragmented MFU has no MMTP packet sequence number',
571
+ )
572
+ }
573
+ const runKey = fragmentRunKey(fragment)
574
+ if (
575
+ fragmentationIndicator === 1 &&
576
+ (hasSampleIdentity(fragment) || isU32(fragment.itemId)) &&
577
+ this.#emitted.has(objectKey(fragment))
578
+ ) {
579
+ this.#deletePendingRun(runKey)
580
+ return null
581
+ }
582
+
583
+ if (fragmentationIndicator !== 1) {
584
+ const active = this.#activeDataUnits.get(runKey)
585
+ if (active !== undefined && fragment.fragmentCounter > active.nextFragmentCounter) {
586
+ this.#reject(
587
+ 'fragment-counter-mismatch',
588
+ 'MFU continuation is behind the authored fragment-counter countdown',
589
+ )
590
+ }
591
+ if (active === undefined || fragment.fragmentCounter < active.nextFragmentCounter) {
592
+ this.#parkContinuation(runKey, fragment)
593
+ return null
594
+ }
595
+ } else if (this.#activeDataUnits.has(runKey)) {
596
+ this.#reject('overlapping-data-unit', 'fragmented MFU overlaps an active data unit')
597
+ } else {
598
+ const pending = this.#pendingContinuations.get(runKey)
599
+ if (pending !== undefined && [...pending.fragments.keys()].some(
600
+ counter => counter >= fragment.fragmentCounter,
601
+ )) {
602
+ this.#reject(
603
+ 'fragment-counter-mismatch',
604
+ 'queued continuation counter is not below the authored first-fragment counter',
605
+ )
606
+ }
607
+ }
608
+
609
+ const pendingStartedAt = fragmentationIndicator === 1
610
+ ? this.#pendingContinuations.get(runKey)?.startedAt
611
+ : undefined
612
+ const result = this.#addAssociatedFragment(fragment, runKey, pendingStartedAt)
613
+ if (fragmentationIndicator === 1) this.#clearPendingTimeout(runKey)
614
+ if (result !== null) return result
615
+ return this.#drainPendingContinuations(runKey)
616
+ }
617
+
618
+ #addAssociatedFragment(
619
+ fragment: MFUFragment,
620
+ runKey: string,
621
+ startedAt?: number,
622
+ ): ReassembledMFU | null {
623
+ const association = this.#associateDataUnit(fragment, runKey)
624
+ const { bufferKey, first, terminal } = association
625
+
626
+ let fragments = this.#buffers.get(bufferKey)
627
+ if (first) {
628
+ if (fragments !== undefined) {
629
+ this.#reject('overlapping-data-unit', 'intrinsic data-unit buffer already exists')
630
+ }
631
+ fragments = []
632
+ this.#buffers.set(bufferKey, fragments)
633
+ } else if (fragments === undefined) {
634
+ throw new Error('[MFUReassembler] active data unit has no fragment buffer')
635
+ }
636
+
637
+ // A lossy-path deadline is measured from the first received fragment,
638
+ // including a continuation that arrived before FI=1. Reliable paths are
639
+ // clock-free and never install this timer.
640
+ if (first) this.#setTimeout(bufferKey, startedAt)
641
+ fragments.push(fragment)
642
+
643
+ if (!terminal) return null
644
+ const result = this.#tryReassemble(bufferKey)
645
+ this.#activeDataUnits.delete(runKey)
646
+ if (result === null) {
647
+ this.#clearTimeout(bufferKey)
648
+ this.#buffers.delete(bufferKey)
649
+ this.#reject(
650
+ 'incomplete-terminal-fragment',
651
+ 'validated terminal fragment did not complete its data unit',
652
+ )
653
+ }
654
+ return result
655
+ }
656
+
657
+ #parkContinuation(runKey: string, fragment: MFUFragment): void {
658
+ let pending = this.#pendingContinuations.get(runKey)
659
+ if (pending === undefined) {
660
+ const awaitingFirst = !this.#activeDataUnits.has(runKey)
661
+ if (awaitingFirst && this.#incompleteRunCount() >= this.#maxBuffered) {
662
+ this.#reject('active-data-unit-limit', 'active MFU data-unit limit reached')
663
+ }
664
+ const startedAt = Date.now()
665
+ pending = { fragments: new Map(), startedAt }
666
+ if (awaitingFirst && this.#timeoutMs !== null) {
667
+ pending.timeoutId = setTimeout(() => this.#onPendingTimeout(runKey), this.#timeoutMs)
668
+ }
669
+ this.#pendingContinuations.set(runKey, pending)
670
+ }
671
+ if (pending.fragments.has(fragment.fragmentCounter)) {
672
+ this.#reject(
673
+ 'fragment-counter-mismatch',
674
+ 'duplicate queued continuation counter would replace retained media',
675
+ )
676
+ }
677
+ pending.fragments.set(fragment.fragmentCounter, fragment)
678
+ }
679
+
680
+ #drainPendingContinuations(runKey: string): ReassembledMFU | null {
681
+ while (true) {
682
+ const active = this.#activeDataUnits.get(runKey)
683
+ if (active === undefined) {
684
+ this.#deletePendingRun(runKey)
685
+ return null
686
+ }
687
+ const pending = this.#pendingContinuations.get(runKey)
688
+ const fragment = pending?.fragments.get(active.nextFragmentCounter)
689
+ if (fragment === undefined) return null
690
+ pending!.fragments.delete(active.nextFragmentCounter)
691
+ if (pending!.fragments.size === 0) this.#deletePendingRun(runKey)
692
+ const result = this.#addAssociatedFragment(fragment, runKey)
693
+ if (result !== null) {
694
+ this.#deletePendingRun(runKey)
695
+ return result
696
+ }
697
+ }
698
+ }
699
+
700
+ #incompleteRunCount(): number {
701
+ let count = this.#activeDataUnits.size
702
+ for (const runKey of this.#pendingContinuations.keys()) {
703
+ if (!this.#activeDataUnits.has(runKey)) count++
704
+ }
705
+ return count
706
+ }
707
+
708
+ #reject(code: MfuReassemblyErrorCode, message: string): never {
709
+ this.stats.rejectedFragments++
710
+ throw new MfuReassemblyError(code, message)
711
+ }
712
+
713
+ #associateDataUnit(fragment: MFUFragment, runKey: string): DataUnitAssociation {
714
+ const fi = fragment.fragmentationIndicator
715
+ if (fi === 1) {
716
+ if (this.#activeDataUnits.has(runKey)) {
717
+ this.#reject('overlapping-data-unit', 'fragmented MFU overlaps an active data unit')
718
+ }
719
+ if (!this.#pendingContinuations.has(runKey) &&
720
+ this.#incompleteRunCount() >= this.#maxBuffered) {
721
+ this.#reject('active-data-unit-limit', 'active MFU data-unit limit reached')
722
+ }
723
+
724
+ const sampleIdentity = hasSampleIdentity(fragment)
725
+ const hasSampleFields = hasAnySampleIdentityField(fragment)
726
+ const hasItemIdentity = fragment.itemId !== undefined
727
+ if (sampleIdentity && hasItemIdentity) {
728
+ this.#reject(
729
+ 'conflicting-data-unit-identity',
730
+ 'fragmented MFU carries timed and non-timed DU identities',
731
+ )
732
+ }
733
+ if (!sampleIdentity && !hasItemIdentity) {
734
+ this.#reject('missing-data-unit-identity', 'first fragment has no intrinsic DU identity')
735
+ }
736
+ if (hasSampleFields && !sampleIdentity) {
737
+ this.#reject('missing-data-unit-identity', 'first fragment has an incomplete sample identity')
738
+ }
739
+ if (hasItemIdentity && !isU32(fragment.itemId)) {
740
+ this.#reject('missing-data-unit-identity', 'first fragment has an invalid Item_ID')
741
+ }
742
+
743
+ const bufferKey = objectKey(fragment)
744
+ if (this.#buffers.has(bufferKey)) {
745
+ this.#reject('overlapping-data-unit', 'intrinsic data-unit buffer already exists')
746
+ }
747
+
748
+ let active: ActiveDataUnit
749
+ if (sampleIdentity) {
750
+ const nextOffset = checkedSampleEnd(fragment.sampleOffset, fragment.data.byteLength)
751
+ if (nextOffset === null) this.#reject('offset-overflow', 'sample byte offset overflow')
752
+ active = {
753
+ kind: 'sample',
754
+ bufferKey,
755
+ nextFragmentCounter: fragment.fragmentCounter - 1,
756
+ movieFragmentSequenceNumber: fragment.movieFragmentSequenceNumber,
757
+ sampleNumber: fragment.sampleNumber,
758
+ nextOffset,
759
+ ...(fragment.catalogTrackName === undefined
760
+ ? {}
761
+ : { catalogTrackName: fragment.catalogTrackName }),
762
+ }
763
+ } else {
764
+ active = {
765
+ kind: 'item',
766
+ bufferKey,
767
+ nextFragmentCounter: fragment.fragmentCounter - 1,
768
+ itemId: fragment.itemId!,
769
+ ...(fragment.catalogTrackName === undefined
770
+ ? {}
771
+ : { catalogTrackName: fragment.catalogTrackName }),
772
+ }
773
+ }
774
+ this.#activeDataUnits.set(runKey, active)
775
+ return { bufferKey, first: true, terminal: false }
776
+ }
777
+
778
+ const active = this.#activeDataUnits.get(runKey)
779
+ if (active === undefined) {
780
+ throw new Error('[MFUReassembler] continuation was not parked before association')
781
+ }
782
+ if (fragment.fragmentCounter !== active.nextFragmentCounter) {
783
+ this.#reject(
784
+ 'fragment-counter-mismatch',
785
+ 'MFU continuation does not follow the authored fragment-counter countdown',
786
+ )
787
+ }
788
+
789
+ if (active.kind === 'sample') {
790
+ if (fragment.itemId !== undefined) {
791
+ this.#reject('data-unit-identity-mismatch', 'sample continuation carries an Item_ID')
792
+ }
793
+ if (hasAnySampleIdentityField(fragment) && (
794
+ !hasSampleIdentity(fragment) ||
795
+ fragment.movieFragmentSequenceNumber !== active.movieFragmentSequenceNumber ||
796
+ fragment.sampleNumber !== active.sampleNumber || fragment.sampleOffset !== active.nextOffset
797
+ )) {
798
+ this.#reject('data-unit-identity-mismatch', 'sample continuation identity does not match')
799
+ }
800
+ const nextOffset = checkedSampleEnd(active.nextOffset, fragment.data.byteLength)
801
+ if (nextOffset === null) this.#reject('offset-overflow', 'sample byte offset overflow')
802
+ fragment.movieFragmentSequenceNumber = active.movieFragmentSequenceNumber
803
+ fragment.sampleNumber = active.sampleNumber
804
+ fragment.sampleOffset = active.nextOffset
805
+ if (fi === 2) {
806
+ active.nextOffset = nextOffset
807
+ active.nextFragmentCounter = fragment.fragmentCounter - 1
808
+ }
809
+ } else {
810
+ if (hasAnySampleIdentityField(fragment) ||
811
+ (fragment.itemId !== undefined && fragment.itemId !== active.itemId)) {
812
+ this.#reject('data-unit-identity-mismatch', 'Item_ID continuation identity does not match')
813
+ }
814
+ fragment.itemId = active.itemId
815
+ if (fi === 2) active.nextFragmentCounter = fragment.fragmentCounter - 1
816
+ }
817
+
818
+ return {
819
+ bufferKey: active.bufferKey,
820
+ first: false,
821
+ terminal: fi === 3,
822
+ }
823
+ }
824
+
825
+ #tryReassemble(bufferKey: string): ReassembledMFU | null {
826
+ const fragments = this.#buffers.get(bufferKey)
827
+ if (!fragments || fragments.length === 0) {
828
+ return null
829
+ }
830
+
831
+ const sorted = [...fragments].sort((a, b) => this.#orderValue(a) - this.#orderValue(b))
832
+
833
+ if (
834
+ sorted[0]!.fragmentationIndicator !== 1 ||
835
+ sorted[sorted.length - 1]!.fragmentationIndicator !== 3
836
+ ) {
837
+ return null
838
+ }
839
+ if (sorted.slice(1, -1).some((fragment) => fragment.fragmentationIndicator !== 2)) {
840
+ return null
841
+ }
842
+
843
+ // ISO fragment_counter is the number of succeeding payloads. After
844
+ // normalization (`255 - counter`), a complete non-wrapping chain is strictly
845
+ // consecutive. This also caps a data unit at 256 payloads.
846
+ for (let i = 1; i < sorted.length; i++) {
847
+ if (this.#orderValue(sorted[i]!) !== this.#orderValue(sorted[i - 1]!) + 1) {
848
+ return null
849
+ }
850
+ }
851
+
852
+ // When every fragment carries a timed-MFU byte offset, independently prove
853
+ // byte adjacency within this data unit. Counter order remains authoritative;
854
+ // conflicting offsets fail closed instead of changing the fragment order.
855
+ // sorted[0]'s offset is this DU's authored base (moqcast #37: zero for a
856
+ // whole-sample DU, nonzero for one subsample DU of a partitioned sample) --
857
+ // never asserted against a literal zero.
858
+ if (ordersByOffset(sorted)) {
859
+ for (let i = 1; i < sorted.length; i++) {
860
+ const previous = sorted[i - 1]!
861
+ if (
862
+ sorted[i]!.sampleOffset !==
863
+ previous.sampleOffset! + previous.data.byteLength
864
+ ) {
865
+ return null
866
+ }
867
+ }
868
+ }
869
+
870
+ fragments.splice(0, fragments.length, ...sorted)
871
+ return this.#assemble(bufferKey, fragments)
872
+ }
873
+
874
+ #assemble(bufferKey: string, fragments: MFUFragment[]): ReassembledMFU {
875
+ // Reassemble: concatenate all fragment data
876
+ const totalSize = fragments.reduce((sum, f) => sum + f.data.length, 0)
877
+ const data = new Uint8Array(totalSize)
878
+
879
+ let offset = 0
880
+ for (const fragment of fragments) {
881
+ data.set(fragment.data, offset)
882
+ offset += fragment.data.length
883
+ }
884
+
885
+ // Cleanup + mark the intrinsic identity as emitted to prevent a recovered
886
+ // duplicate from starting another fragmented run.
887
+ this.#clearTimeout(bufferKey)
888
+ this.#buffers.delete(bufferKey)
889
+ this.#markEmitted(bufferKey)
890
+ this.stats.reassembledMFUs++
891
+
892
+ return {
893
+ mpuSequenceNumber: fragments[0]!.mpuSequenceNumber,
894
+ packetSequenceNumber: fragments[0]!.packetSequenceNumber,
895
+ ...assembledPacketSequences(fragments),
896
+ data,
897
+ timestamp: fragments[0]!.timestamp,
898
+ fragmentCount: fragments.length,
899
+ rapFlag: fragments[0]!.rapFlag,
900
+ priority: fragments.find((f) => f.priority !== undefined)?.priority,
901
+ sampleNumber: fragments[0]!.sampleNumber,
902
+ movieFragmentSequenceNumber: fragments[0]!.movieFragmentSequenceNumber,
903
+ sampleOffset: fragments[0]!.sampleOffset,
904
+ source: fragments[0]!.source,
905
+ deliveryProvenance: deliveryProvenanceOf(fragments),
906
+ catalogTrackName: fragments[0]!.catalogTrackName,
907
+ }
908
+ }
909
+
910
+ #setTimeout(bufferKey: string, startedAt = Date.now()): void {
911
+ const state: BufferTimeoutState = { startedAt }
912
+ if (this.#timeoutMs !== null) {
913
+ state.timeoutId = setTimeout(
914
+ () => this.#onTimeout(bufferKey),
915
+ Math.max(0, startedAt + this.#timeoutMs - Date.now()),
916
+ )
917
+ }
918
+ this.#timeouts.set(bufferKey, state)
919
+ }
920
+
921
+ #clearPendingTimeout(runKey: string): void {
922
+ const pending = this.#pendingContinuations.get(runKey)
923
+ if (pending?.timeoutId !== undefined) clearTimeout(pending.timeoutId)
924
+ if (pending !== undefined) {
925
+ delete pending.timeoutId
926
+ }
927
+ }
928
+
929
+ #deletePendingRun(runKey: string): void {
930
+ this.#clearPendingTimeout(runKey)
931
+ this.#pendingContinuations.delete(runKey)
932
+ }
933
+
934
+ #onPendingTimeout(runKey: string): void {
935
+ const pending = this.#pendingContinuations.get(runKey)
936
+ if (pending === undefined) return
937
+ this.stats.timedOutMFUs++
938
+ console.warn(
939
+ `[MfuReassembler] timeout: run=${runKey} fragments=${pending.fragments.size} ` +
940
+ '(continuations awaiting first fragment, discarded)',
941
+ )
942
+ this.#pendingContinuations.delete(runKey)
943
+ }
944
+
945
+ #clearTimeout(bufferKey: string): void {
946
+ const state = this.#timeouts.get(bufferKey)
947
+ if (state !== undefined) {
948
+ if (state.timeoutId !== undefined) clearTimeout(state.timeoutId)
949
+ this.#timeouts.delete(bufferKey)
950
+ }
951
+ }
952
+
953
+ /** Callback for deferred assembly results (set by caller for async frame delivery) */
954
+ onDeferredAssembly?: (result: ReassembledMFU) => void
955
+
956
+ #markEmitted(bufferKey: string): void {
957
+ if (this.#emitted.has(bufferKey)) return
958
+ this.#emitted.add(bufferKey)
959
+ this.#emittedOrder.push(bufferKey)
960
+ while (this.#emittedOrder.length > this.#maxBuffered) {
961
+ const oldest = this.#emittedOrder.shift()
962
+ if (oldest !== undefined) this.#emitted.delete(oldest)
963
+ }
964
+ }
965
+
966
+ #orderValue(fragment: MFUFragment): number {
967
+ return fragmentCounterOrder(fragment)
968
+ }
969
+
970
+ #removeActiveDataUnitForBuffer(bufferKey: string): void {
971
+ for (const [runKey, active] of this.#activeDataUnits) {
972
+ if (active.bufferKey === bufferKey) {
973
+ this.#activeDataUnits.delete(runKey)
974
+ this.#deletePendingRun(runKey)
975
+ }
976
+ }
977
+ }
978
+
979
+ #onTimeout(bufferKey: string): void {
980
+ const fragments = this.#buffers.get(bufferKey)
981
+ if (!fragments || fragments.length === 0) {
982
+ this.#buffers.delete(bufferKey)
983
+ this.#timeouts.delete(bufferKey)
984
+ this.#removeActiveDataUnitForBuffer(bufferKey)
985
+ this.stats.timedOutMFUs++
986
+ console.warn(`[MfuReassembler] timeout: key=${bufferKey} fragments=0 (no data buffered)`)
987
+ return
988
+ }
989
+
990
+ // Discard partial MFUs — incomplete AVCC data crashes WebCodecs with
991
+ // "Unable to determine size of bitstream buffer". FEC recovery handles
992
+ // loss; partial assembly only produces garbage frames.
993
+ this.stats.timedOutMFUs++
994
+ console.warn(
995
+ `[MfuReassembler] timeout: key=${bufferKey} fragments=${fragments.length} (partial, discarded)`,
996
+ )
997
+
998
+ this.#buffers.delete(bufferKey)
999
+ this.#timeouts.delete(bufferKey)
1000
+ this.#removeActiveDataUnitForBuffer(bufferKey)
1001
+ }
1002
+
1003
+ /**
1004
+ * Flush all buffered fragments
1005
+ */
1006
+ flush(): void {
1007
+ for (const key of this.#buffers.keys()) {
1008
+ this.#clearTimeout(key)
1009
+ }
1010
+ this.#buffers.clear()
1011
+ this.#timeouts.clear()
1012
+ this.#activeDataUnits.clear()
1013
+ for (const runKey of this.#pendingContinuations.keys()) {
1014
+ this.#clearPendingTimeout(runKey)
1015
+ }
1016
+ this.#pendingContinuations.clear()
1017
+ this.#emitted.clear()
1018
+ this.#emittedOrder.length = 0
1019
+ }
1020
+
1021
+ /** Drop one catalog stream's incomplete and dedup state. */
1022
+ resetStream(catalogTrackName: string): void {
1023
+ const belongsToStream = (key: string): boolean => {
1024
+ try {
1025
+ const decoded = JSON.parse(key)
1026
+ return Array.isArray(decoded) && decoded[0] === 'catalog' && decoded[1] === catalogTrackName
1027
+ } catch {
1028
+ return false
1029
+ }
1030
+ }
1031
+ const discarded = new Set<string>()
1032
+ for (const [key, fragments] of this.#buffers) {
1033
+ if (fragments.some((fragment) => fragment.catalogTrackName === catalogTrackName)) {
1034
+ discarded.add(key)
1035
+ }
1036
+ }
1037
+ for (const [runKey, active] of this.#activeDataUnits) {
1038
+ if (active.catalogTrackName === catalogTrackName) {
1039
+ discarded.add(active.bufferKey)
1040
+ this.#activeDataUnits.delete(runKey)
1041
+ }
1042
+ }
1043
+ for (const [runKey, pending] of this.#pendingContinuations) {
1044
+ if ([...pending.fragments.values()].some(
1045
+ fragment => fragment.catalogTrackName === catalogTrackName,
1046
+ )) {
1047
+ this.#deletePendingRun(runKey)
1048
+ }
1049
+ }
1050
+ for (const key of this.#emitted) {
1051
+ if (belongsToStream(key)) discarded.add(key)
1052
+ }
1053
+ for (const key of discarded) {
1054
+ this.#clearTimeout(key)
1055
+ this.#buffers.delete(key)
1056
+ this.#emitted.delete(key)
1057
+ }
1058
+ this.#emittedOrder = this.#emittedOrder.filter((key) => !discarded.has(key))
1059
+ }
1060
+
1061
+ /** Reset all state — flush buffers and zero stats */
1062
+ reset(): void {
1063
+ this.flush()
1064
+ this.stats.totalFragments = 0
1065
+ this.stats.reassembledMFUs = 0
1066
+ this.stats.rejectedFragments = 0
1067
+ this.stats.timedOutMFUs = 0
1068
+ this.stats.evictedMemoryLimit = 0
1069
+ this.stats.outOfOrderFragments = 0
1070
+ }
1071
+
1072
+ /**
1073
+ * Get number of pending MFUs
1074
+ */
1075
+ get pendingCount(): number {
1076
+ let count = this.#buffers.size
1077
+ for (const runKey of this.#pendingContinuations.keys()) {
1078
+ if (!this.#activeDataUnits.has(runKey)) count++
1079
+ }
1080
+ return count
1081
+ }
1082
+
1083
+ /**
1084
+ * Get current buffer status
1085
+ */
1086
+ getStatus(): {
1087
+ bufferedMFUs: number
1088
+ pendingFragments: number
1089
+ pendingSamples: PendingMfuDebug[]
1090
+ stats: ReassemblerStats
1091
+ } {
1092
+ let pendingFragments = 0
1093
+ for (const fragments of this.#buffers.values()) {
1094
+ pendingFragments += fragments.length
1095
+ }
1096
+ for (const pending of this.#pendingContinuations.values()) {
1097
+ pendingFragments += pending.fragments.size
1098
+ }
1099
+
1100
+ return {
1101
+ bufferedMFUs: this.pendingCount,
1102
+ pendingFragments,
1103
+ pendingSamples: this.#pendingDebugSamples(),
1104
+ stats: { ...this.stats },
1105
+ }
1106
+ }
1107
+
1108
+ #pendingDebugSamples(): PendingMfuDebug[] {
1109
+ const out: PendingMfuDebug[] = []
1110
+ for (const [key, fragments] of this.#buffers) {
1111
+ if (fragments.length === 0) continue
1112
+ const sorted = [...fragments].sort((a, b) => this.#orderValue(a) - this.#orderValue(b))
1113
+ const fiCounts = [0, 0, 0, 0]
1114
+ let minCounter = Infinity
1115
+ let maxCounter = -Infinity
1116
+ let minOffset = Infinity
1117
+ let maxOffset = -Infinity
1118
+ for (const fragment of sorted) {
1119
+ fiCounts[fragment.fragmentationIndicator] =
1120
+ (fiCounts[fragment.fragmentationIndicator] ?? 0) + 1
1121
+ minCounter = Math.min(minCounter, fragment.fragmentCounter)
1122
+ maxCounter = Math.max(maxCounter, fragment.fragmentCounter)
1123
+ if (typeof fragment.sampleOffset === 'number') {
1124
+ minOffset = Math.min(minOffset, fragment.sampleOffset)
1125
+ maxOffset = Math.max(maxOffset, fragment.sampleOffset)
1126
+ }
1127
+ }
1128
+ const point = (fragment: MFUFragment) => ({
1129
+ fi: fragment.fragmentationIndicator,
1130
+ counter: fragment.fragmentCounter,
1131
+ order: this.#orderValue(fragment),
1132
+ ...(typeof fragment.packetSequenceNumber === 'number' ? { seq: fragment.packetSequenceNumber } : {}),
1133
+ ...(typeof fragment.sampleOffset === 'number' ? { offset: fragment.sampleOffset } : {}),
1134
+ })
1135
+ const summary: PendingMfuDebug = {
1136
+ key,
1137
+ fragments: sorted.length,
1138
+ fi0: fiCounts[0] ?? 0,
1139
+ fi1: fiCounts[1] ?? 0,
1140
+ fi2: fiCounts[2] ?? 0,
1141
+ fi3: fiCounts[3] ?? 0,
1142
+ firstFi: sorted[0]!.fragmentationIndicator,
1143
+ lastFi: sorted[sorted.length - 1]!.fragmentationIndicator,
1144
+ firstOrder: this.#orderValue(sorted[0]!),
1145
+ lastOrder: this.#orderValue(sorted[sorted.length - 1]!),
1146
+ minCounter,
1147
+ maxCounter,
1148
+ movieFragmentSequenceNumber: sorted.find((f) => f.movieFragmentSequenceNumber !== undefined)
1149
+ ?.movieFragmentSequenceNumber,
1150
+ sampleNumber: sorted.find((f) => f.sampleNumber !== undefined)?.sampleNumber,
1151
+ mpuSequenceNumber: sorted[0]!.mpuSequenceNumber,
1152
+ sortedHead: sorted.slice(0, 6).map(point),
1153
+ sortedTail: sorted.slice(-6).map(point),
1154
+ }
1155
+ if (minOffset !== Infinity) summary.minOffset = minOffset
1156
+ if (maxOffset !== -Infinity) summary.maxOffset = maxOffset
1157
+ out.push(summary)
1158
+ if (out.length >= 12) break
1159
+ }
1160
+ return out
1161
+ }
1162
+ }
1163
+
1164
+ /**
1165
+ * Create MFU fragment from MMTP packet for reassembly
1166
+ */
1167
+ export function createFragment(mmtpPacket: Uint8Array): MFUFragment {
1168
+ const extraction = extractMFUPayload(mmtpPacket)
1169
+ if (!extraction) throw new Error('Unable to extract MFU payload from MMTP packet')
1170
+ const { header, mpu, data, mfuDuHeader, itemId } = extraction
1171
+
1172
+ return {
1173
+ fragmentationIndicator: mpu.fragmentationIndicator,
1174
+ mpuSequenceNumber: mpu.mpuSequenceNumber,
1175
+ fragmentCounter: mpu.fragmentCounter,
1176
+ data,
1177
+ timestamp: header.timestamp,
1178
+ rapFlag: header.rapFlag,
1179
+ sampleNumber: mfuDuHeader?.sampleNumber,
1180
+ movieFragmentSequenceNumber: mfuDuHeader?.movieFragmentSequenceNumber,
1181
+ sampleOffset: mfuDuHeader?.offset,
1182
+ priority: mfuDuHeader?.priority,
1183
+ itemId: itemId ?? undefined,
1184
+ packetSequenceNumber: header.sequenceNumber,
1185
+ packetId: header.packetId,
1186
+ }
1187
+ }
1188
+
1189
+ /**
1190
+ * Process MMTP packet with fragment reassembly
1191
+ */
1192
+ export function processMMTPWithReassembly(
1193
+ mmtpPacket: Uint8Array,
1194
+ reassembler: MFUReassembler,
1195
+ ): ReassembledMFU | null {
1196
+ // Skip FEC repair packets
1197
+ if (isFecRepairPacket(mmtpPacket)) {
1198
+ return null
1199
+ }
1200
+
1201
+ const fragment = createFragment(mmtpPacket)
1202
+ return reassembler.addFragment(fragment)
1203
+ }
1204
+
1205
+ // =============================================================================
1206
+ // Inline Tests (vitest)
1207
+ // =============================================================================
1208
+
1209
+ if (import.meta.vitest) {
1210
+ const { describe, test, expect, vi } = import.meta.vitest
1211
+
1212
+ const expectRejection = (call: () => unknown, code: MfuReassemblyErrorCode): void => {
1213
+ let rejection: unknown
1214
+ try {
1215
+ call()
1216
+ } catch (error) {
1217
+ rejection = error
1218
+ }
1219
+ expect(rejection).toBeInstanceOf(MfuReassemblyError)
1220
+ expect((rejection as MfuReassemblyError).code).toBe(code)
1221
+ }
1222
+
1223
+ const testPacketSequence = (mpu: number, counter: number, run = 0): number =>
1224
+ (mpu * 1000 + run - counter + 0x1_0000_0000) % 0x1_0000_0000
1225
+
1226
+ describe('MFUReassembler', () => {
1227
+ test('should return complete MFU for FI=0', () => {
1228
+ const reassembler = new MFUReassembler({ timeoutMs: 1000 })
1229
+ const result = reassembler.addFragment({
1230
+ fragmentationIndicator: 0,
1231
+ mpuSequenceNumber: 1,
1232
+ fragmentCounter: 0,
1233
+ itemId: 1,
1234
+ data: new Uint8Array([1, 2, 3, 4]),
1235
+ timestamp: 1000n,
1236
+
1237
+ rapFlag: true,
1238
+ })
1239
+ expect(result).not.toBeNull()
1240
+ expect(result?.data).toEqual(new Uint8Array([1, 2, 3, 4]))
1241
+ expect(result?.fragmentCount).toBe(1)
1242
+ expect(result?.rapFlag).toBe(true)
1243
+ })
1244
+
1245
+ test('rejects a complete MFU without a valid sample identity', () => {
1246
+ const reassembler = new MFUReassembler({ timeoutMs: 1000 })
1247
+ const complete = (identity: Partial<MFUFragment>): MFUFragment => ({
1248
+ fragmentationIndicator: 0,
1249
+ mpuSequenceNumber: 1,
1250
+ fragmentCounter: 0,
1251
+ data: new Uint8Array([1]),
1252
+ timestamp: 1000n,
1253
+ rapFlag: true,
1254
+ ...identity,
1255
+ })
1256
+ expectRejection(
1257
+ () => reassembler.addFragment(complete({})),
1258
+ 'missing-data-unit-identity',
1259
+ )
1260
+ expectRejection(
1261
+ () => reassembler.addFragment(complete({
1262
+ movieFragmentSequenceNumber: 2,
1263
+ sampleNumber: 1,
1264
+ })),
1265
+ 'missing-data-unit-identity',
1266
+ )
1267
+ })
1268
+
1269
+ test('accepts a complete subsample MFU with a nonzero authored offset', () => {
1270
+ // moqcast #37: a subsample DU's offset is its authored base byte
1271
+ // position within the sample -- nonzero identifies one subsample DU
1272
+ // among siblings partitioning an oversized sample.
1273
+ const reassembler = new MFUReassembler({ timeoutMs: 1000 })
1274
+ const result = reassembler.addFragment({
1275
+ fragmentationIndicator: 0,
1276
+ mpuSequenceNumber: 1,
1277
+ fragmentCounter: 0,
1278
+ movieFragmentSequenceNumber: 2,
1279
+ sampleNumber: 1,
1280
+ sampleOffset: 4,
1281
+ data: new Uint8Array([1]),
1282
+ timestamp: 1000n,
1283
+ rapFlag: true,
1284
+ })
1285
+ expect(result).not.toBeNull()
1286
+ expect(result?.sampleOffset).toBe(4)
1287
+ })
1288
+
1289
+ test('accepts a fragmented subsample DU whose first fragment has a nonzero authored offset', () => {
1290
+ const reassembler = new MFUReassembler({ timeoutMs: 1000 })
1291
+
1292
+ expect(reassembler.addFragment({
1293
+ fragmentationIndicator: 1,
1294
+ mpuSequenceNumber: 2,
1295
+ fragmentCounter: 1,
1296
+ movieFragmentSequenceNumber: 2,
1297
+ sampleNumber: 1,
1298
+ sampleOffset: 4,
1299
+ packetSequenceNumber: testPacketSequence(2, 1),
1300
+ data: new Uint8Array([0xa1]),
1301
+ timestamp: 1000n,
1302
+ rapFlag: true,
1303
+ })).toBeNull()
1304
+ expect(reassembler.pendingCount).toBe(1)
1305
+ expect(reassembler.getStatus().pendingFragments).toBe(1)
1306
+
1307
+ const out = reassembler.addFragment({
1308
+ fragmentationIndicator: 3,
1309
+ mpuSequenceNumber: 2,
1310
+ fragmentCounter: 0,
1311
+ data: new Uint8Array([0xa2]),
1312
+ timestamp: 1001n,
1313
+ rapFlag: false,
1314
+ packetSequenceNumber: testPacketSequence(2, 0),
1315
+ })
1316
+ expect(out?.data).toEqual(new Uint8Array([0xa1, 0xa2]))
1317
+ expect(out?.sampleOffset).toBe(4)
1318
+ })
1319
+
1320
+ test('sibling subsample DUs of one sample reassemble independently by offset', () => {
1321
+ // moqcast #37: each subsample DU of a partitioned sample runs its own
1322
+ // FI chain and gets its own buffer, keyed in part by its authored base
1323
+ // offset -- distinct from a sibling DU sharing (movie, sample).
1324
+ const reassembler = new MFUReassembler({ timeoutMs: 1000 })
1325
+
1326
+ expect(reassembler.addFragment({
1327
+ mpuSequenceNumber: 3,
1328
+ movieFragmentSequenceNumber: 5,
1329
+ sampleNumber: 9,
1330
+ fragmentationIndicator: 1,
1331
+ fragmentCounter: 1,
1332
+ sampleOffset: 0,
1333
+ data: new Uint8Array([0xa1]),
1334
+ timestamp: 2000n,
1335
+ rapFlag: true,
1336
+ packetSequenceNumber: testPacketSequence(3, 1, 0),
1337
+ })).toBeNull()
1338
+ expect(reassembler.addFragment({
1339
+ mpuSequenceNumber: 3,
1340
+ movieFragmentSequenceNumber: 5,
1341
+ sampleNumber: 9,
1342
+ fragmentationIndicator: 1,
1343
+ fragmentCounter: 1,
1344
+ sampleOffset: 3,
1345
+ data: new Uint8Array([0xb1]),
1346
+ timestamp: 2001n,
1347
+ rapFlag: false,
1348
+ packetSequenceNumber: testPacketSequence(3, 1, 1),
1349
+ })).toBeNull()
1350
+ expect(reassembler.pendingCount).toBe(2)
1351
+
1352
+ const first = reassembler.addFragment({
1353
+ mpuSequenceNumber: 3,
1354
+ fragmentationIndicator: 3,
1355
+ fragmentCounter: 0,
1356
+ data: new Uint8Array([0xa2]),
1357
+ timestamp: 2000n,
1358
+ rapFlag: false,
1359
+ packetSequenceNumber: testPacketSequence(3, 0, 0),
1360
+ })
1361
+ expect(first?.data).toEqual(new Uint8Array([0xa1, 0xa2]))
1362
+ expect(first?.sampleOffset).toBe(0)
1363
+
1364
+ const second = reassembler.addFragment({
1365
+ mpuSequenceNumber: 3,
1366
+ fragmentationIndicator: 3,
1367
+ fragmentCounter: 0,
1368
+ data: new Uint8Array([0xb2]),
1369
+ timestamp: 2001n,
1370
+ rapFlag: false,
1371
+ packetSequenceNumber: testPacketSequence(3, 0, 1),
1372
+ })
1373
+ expect(second?.data).toEqual(new Uint8Array([0xb1, 0xb2]))
1374
+ expect(second?.sampleOffset).toBe(3)
1375
+ expect(reassembler.pendingCount).toBe(0)
1376
+ })
1377
+
1378
+ test('isolates colliding fragment coordinates by catalog track identity', () => {
1379
+ const reassembler = new MFUReassembler({ timeoutMs: 1000 })
1380
+ const fragment = (
1381
+ catalogTrackName: string,
1382
+ fragmentationIndicator: number,
1383
+ fragmentCounter: number,
1384
+ byte: number,
1385
+ ): MFUFragment => ({
1386
+ itemId: 1,
1387
+ fragmentationIndicator,
1388
+ fragmentCounter,
1389
+ mpuSequenceNumber: 1,
1390
+ data: new Uint8Array([byte]),
1391
+ timestamp: 1000n,
1392
+ rapFlag: fragmentationIndicator === 1,
1393
+ catalogTrackName,
1394
+ packetSequenceNumber: testPacketSequence(1, fragmentCounter),
1395
+ })
1396
+
1397
+ expect(reassembler.addFragment(fragment('video/720p', 1, 1, 0x71))).toBeNull()
1398
+ expect(reassembler.addFragment(fragment('video/540p', 1, 1, 0x51))).toBeNull()
1399
+ const high = reassembler.addFragment(fragment('video/720p', 3, 0, 0x72))
1400
+ const low = reassembler.addFragment(fragment('video/540p', 3, 0, 0x52))
1401
+
1402
+ expect(high?.data).toEqual(new Uint8Array([0x71, 0x72]))
1403
+ expect(high?.catalogTrackName).toBe('video/720p')
1404
+ expect(low?.data).toEqual(new Uint8Array([0x51, 0x52]))
1405
+ expect(low?.catalogTrackName).toBe('video/540p')
1406
+ })
1407
+
1408
+ test('rejects an overlapping non-timed item without replacing the active run', () => {
1409
+ const reassembler = new MFUReassembler({ timeoutMs: 1000 })
1410
+ const fragment = (
1411
+ itemId: number,
1412
+ fragmentationIndicator: number,
1413
+ fragmentCounter: number,
1414
+ byte: number,
1415
+ ): MFUFragment => ({
1416
+ itemId,
1417
+ fragmentationIndicator,
1418
+ fragmentCounter,
1419
+ mpuSequenceNumber: 1,
1420
+ data: new Uint8Array([byte]),
1421
+ timestamp: 1000n,
1422
+ rapFlag: false,
1423
+ packetSequenceNumber: testPacketSequence(1, fragmentCounter),
1424
+ })
1425
+
1426
+ expect(reassembler.addFragment(fragment(10, 1, 1, 0x10))).toBeNull()
1427
+ expectRejection(
1428
+ () => reassembler.addFragment(fragment(20, 1, 1, 0x20)),
1429
+ 'overlapping-data-unit',
1430
+ )
1431
+ expect(reassembler.pendingCount).toBe(1)
1432
+ expect(reassembler.addFragment(fragment(10, 3, 0, 0x11))?.data)
1433
+ .toEqual(new Uint8Array([0x10, 0x11]))
1434
+ expect(reassembler.pendingCount).toBe(0)
1435
+ })
1436
+
1437
+ test('associates continuations by authored state when packet timestamps change', () => {
1438
+ const reassembler = new MFUReassembler({ timeoutMs: 1000 })
1439
+ const fragment = (
1440
+ itemId: number | undefined,
1441
+ fragmentationIndicator: number,
1442
+ fragmentCounter: number,
1443
+ byte: number,
1444
+ timestamp: bigint,
1445
+ ): MFUFragment => ({
1446
+ ...(itemId === undefined ? {} : { itemId }),
1447
+ fragmentationIndicator,
1448
+ fragmentCounter,
1449
+ mpuSequenceNumber: 1,
1450
+ data: new Uint8Array([byte]),
1451
+ timestamp,
1452
+ rapFlag: false,
1453
+ packetSequenceNumber: testPacketSequence(1, fragmentCounter),
1454
+ })
1455
+
1456
+ expect(reassembler.addFragment(fragment(10, 1, 2, 0x10, 1000n))).toBeNull()
1457
+ expect(reassembler.addFragment(fragment(undefined, 2, 1, 0x11, 1001n))).toBeNull()
1458
+ const out = reassembler.addFragment(fragment(undefined, 3, 0, 0x12, 1002n))
1459
+ expect(out?.data).toEqual(new Uint8Array([0x10, 0x11, 0x12]))
1460
+ expect(out?.timestamp).toBe(1000n)
1461
+ })
1462
+
1463
+ test('should buffer and reassemble 2 fragments', () => {
1464
+ const reassembler = new MFUReassembler({ timeoutMs: 1000 })
1465
+
1466
+ // First fragment (FI=1)
1467
+ const result1 = reassembler.addFragment({
1468
+ fragmentationIndicator: 1,
1469
+ mpuSequenceNumber: 1,
1470
+ fragmentCounter: 1,
1471
+ itemId: 1,
1472
+ data: new Uint8Array([1, 2]),
1473
+ timestamp: 1000n,
1474
+ rapFlag: true,
1475
+ packetSequenceNumber: testPacketSequence(1, 1),
1476
+ })
1477
+ expect(result1).toBeNull()
1478
+
1479
+ // Last fragment (FI=3)
1480
+ const result2 = reassembler.addFragment({
1481
+ fragmentationIndicator: 3,
1482
+ mpuSequenceNumber: 1,
1483
+ fragmentCounter: 0,
1484
+ data: new Uint8Array([3, 4]),
1485
+ timestamp: 1000n,
1486
+ rapFlag: false,
1487
+ packetSequenceNumber: testPacketSequence(1, 0),
1488
+ })
1489
+ expect(result2).not.toBeNull()
1490
+ expect(result2?.data).toEqual(new Uint8Array([1, 2, 3, 4]))
1491
+ expect(result2?.fragmentCount).toBe(2)
1492
+ expect(result2?.rapFlag).toBe(true) // Should use first fragment's rapFlag
1493
+ })
1494
+
1495
+ test('buffers a terminal continuation until its authored first fragment arrives', () => {
1496
+ const reassembler = new MFUReassembler({ timeoutMs: 1000 })
1497
+
1498
+ // Last fragment first (FI=3)
1499
+ expect(reassembler.addFragment({
1500
+ fragmentationIndicator: 3,
1501
+ mpuSequenceNumber: 1,
1502
+ fragmentCounter: 0,
1503
+ data: new Uint8Array([3, 4]),
1504
+ timestamp: 1000n,
1505
+ rapFlag: false,
1506
+ packetSequenceNumber: testPacketSequence(1, 0),
1507
+ })).toBeNull()
1508
+ expect(reassembler.pendingCount).toBe(1)
1509
+
1510
+ // Then first fragment (FI=1)
1511
+ const completed = reassembler.addFragment({
1512
+ fragmentationIndicator: 1,
1513
+ mpuSequenceNumber: 1,
1514
+ fragmentCounter: 1,
1515
+ itemId: 1,
1516
+ data: new Uint8Array([1, 2]),
1517
+ timestamp: 1000n,
1518
+ rapFlag: true,
1519
+ packetSequenceNumber: testPacketSequence(1, 1),
1520
+ })
1521
+
1522
+ expect(completed?.data).toEqual(new Uint8Array([1, 2, 3, 4]))
1523
+ expect(completed?.rapFlag).toBe(true)
1524
+ expect(reassembler.pendingCount).toBe(0)
1525
+ })
1526
+
1527
+ test('clock-free mode keeps an admitted MFU until completion', async () => {
1528
+ const reassembler = new MFUReassembler({ timeoutMs: null })
1529
+
1530
+ reassembler.addFragment({
1531
+ fragmentationIndicator: 1,
1532
+ mpuSequenceNumber: 1,
1533
+ fragmentCounter: 1,
1534
+ itemId: 1,
1535
+ data: new Uint8Array([1, 2]),
1536
+ timestamp: 1000n,
1537
+ rapFlag: true,
1538
+ packetSequenceNumber: testPacketSequence(1, 1),
1539
+ })
1540
+
1541
+ await new Promise(resolve => setTimeout(resolve, 5))
1542
+
1543
+ const result = reassembler.addFragment({
1544
+ fragmentationIndicator: 3,
1545
+ mpuSequenceNumber: 1,
1546
+ fragmentCounter: 0,
1547
+ data: new Uint8Array([3, 4]),
1548
+ timestamp: 1000n,
1549
+ rapFlag: false,
1550
+ packetSequenceNumber: testPacketSequence(1, 0),
1551
+ })
1552
+
1553
+ expect(result).not.toBeNull()
1554
+ expect(result?.data).toEqual(new Uint8Array([1, 2, 3, 4]))
1555
+ expect(reassembler.stats.timedOutMFUs).toBe(0)
1556
+ })
1557
+
1558
+ test('should track pending count', () => {
1559
+ const reassembler = new MFUReassembler({ timeoutMs: 1000 })
1560
+ expect(reassembler.pendingCount).toBe(0)
1561
+
1562
+ reassembler.addFragment({
1563
+ fragmentationIndicator: 1,
1564
+ mpuSequenceNumber: 1,
1565
+ fragmentCounter: 1,
1566
+ itemId: 1,
1567
+ data: new Uint8Array([1]),
1568
+ timestamp: 1000n,
1569
+ rapFlag: false,
1570
+ packetSequenceNumber: testPacketSequence(1, 1),
1571
+ })
1572
+ expect(reassembler.pendingCount).toBe(1)
1573
+
1574
+ reassembler.flush()
1575
+ expect(reassembler.pendingCount).toBe(0)
1576
+ })
1577
+
1578
+ test('should reassemble 3 fragments (FI=1, FI=2, FI=3)', () => {
1579
+ const reassembler = new MFUReassembler({ timeoutMs: 1000 })
1580
+
1581
+ // First fragment
1582
+ expect(reassembler.addFragment({
1583
+ fragmentationIndicator: 1, mpuSequenceNumber: 10,
1584
+ fragmentCounter: 2, itemId: 1, data: new Uint8Array([0xAA, 0xBB]),
1585
+ timestamp: 5000n, rapFlag: true,
1586
+ packetSequenceNumber: testPacketSequence(10, 2),
1587
+ })).toBeNull()
1588
+
1589
+ // Middle fragment
1590
+ expect(reassembler.addFragment({
1591
+ fragmentationIndicator: 2, mpuSequenceNumber: 10,
1592
+ fragmentCounter: 1, data: new Uint8Array([0xCC, 0xDD]),
1593
+ timestamp: 5000n, rapFlag: false,
1594
+ packetSequenceNumber: testPacketSequence(10, 1),
1595
+ })).toBeNull()
1596
+
1597
+ // Last fragment — triggers reassembly
1598
+ const result = reassembler.addFragment({
1599
+ fragmentationIndicator: 3, mpuSequenceNumber: 10,
1600
+ fragmentCounter: 0, data: new Uint8Array([0xEE, 0xFF]),
1601
+ timestamp: 5000n, rapFlag: false,
1602
+ packetSequenceNumber: testPacketSequence(10, 0),
1603
+ })
1604
+
1605
+ expect(result).not.toBeNull()
1606
+ expect(result!.data).toEqual(new Uint8Array([0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]))
1607
+ expect(result!.fragmentCount).toBe(3)
1608
+ expect(result!.rapFlag).toBe(true) // from first fragment
1609
+ expect(result!.mpuSequenceNumber).toBe(10)
1610
+ })
1611
+
1612
+ test('should handle interleaved MPU sequences', () => {
1613
+ const reassembler = new MFUReassembler({ timeoutMs: 1000 })
1614
+
1615
+ // MPU 1: first fragment
1616
+ reassembler.addFragment({
1617
+ fragmentationIndicator: 1, mpuSequenceNumber: 1,
1618
+ fragmentCounter: 1, itemId: 1, data: new Uint8Array([0x01]),
1619
+ timestamp: 100n, rapFlag: false,
1620
+ packetSequenceNumber: testPacketSequence(1, 1),
1621
+ })
1622
+ // MPU 2: complete (FI=0)
1623
+ const r2 = reassembler.addFragment({
1624
+ fragmentationIndicator: 0, mpuSequenceNumber: 2,
1625
+ fragmentCounter: 0, itemId: 2, data: new Uint8Array([0x02]),
1626
+ timestamp: 200n, rapFlag: false,
1627
+ })
1628
+ expect(r2).not.toBeNull()
1629
+ expect(r2!.mpuSequenceNumber).toBe(2)
1630
+
1631
+ // MPU 1: last fragment
1632
+ const r1 = reassembler.addFragment({
1633
+ fragmentationIndicator: 3, mpuSequenceNumber: 1,
1634
+ fragmentCounter: 0, data: new Uint8Array([0x03]),
1635
+ timestamp: 100n, rapFlag: false,
1636
+ packetSequenceNumber: testPacketSequence(1, 0),
1637
+ })
1638
+ expect(r1).not.toBeNull()
1639
+ expect(r1!.data).toEqual(new Uint8Array([0x01, 0x03]))
1640
+ })
1641
+
1642
+ test('rejects overlapping fragmented MFUs sharing one stream and MPU', () => {
1643
+ const reassembler = new MFUReassembler({ timeoutMs: 1000 })
1644
+ const mpuSeq = 5
1645
+
1646
+ expect(
1647
+ reassembler.addFragment({
1648
+ fragmentationIndicator: 1, mpuSequenceNumber: mpuSeq,
1649
+ fragmentCounter: 1, itemId: 10,
1650
+ data: new Uint8Array([0xa1]), timestamp: 100n, rapFlag: true,
1651
+ packetSequenceNumber: testPacketSequence(mpuSeq, 1),
1652
+ }),
1653
+ ).toBeNull()
1654
+ expectRejection(
1655
+ () => reassembler.addFragment({
1656
+ fragmentationIndicator: 1, mpuSequenceNumber: mpuSeq,
1657
+ fragmentCounter: 1, itemId: 20,
1658
+ data: new Uint8Array([0xb1]), timestamp: 200n, rapFlag: false,
1659
+ packetSequenceNumber: testPacketSequence(mpuSeq, 1),
1660
+ }),
1661
+ 'overlapping-data-unit',
1662
+ )
1663
+ expect(reassembler.pendingCount).toBe(1)
1664
+
1665
+ const a = reassembler.addFragment({
1666
+ fragmentationIndicator: 3, mpuSequenceNumber: mpuSeq,
1667
+ fragmentCounter: 0, data: new Uint8Array([0xa2]), timestamp: 101n, rapFlag: false,
1668
+ packetSequenceNumber: testPacketSequence(mpuSeq, 0),
1669
+ })
1670
+ expect(a).not.toBeNull()
1671
+ expect(a!.data).toEqual(new Uint8Array([0xa1, 0xa2]))
1672
+ expect(a!.rapFlag).toBe(true) // from A's first fragment
1673
+
1674
+ expect(reassembler.addFragment({
1675
+ fragmentationIndicator: 1, mpuSequenceNumber: mpuSeq,
1676
+ fragmentCounter: 1, itemId: 20,
1677
+ data: new Uint8Array([0xb1]), timestamp: 200n, rapFlag: false,
1678
+ packetSequenceNumber: testPacketSequence(mpuSeq, 1, 10),
1679
+ })).toBeNull()
1680
+ const b = reassembler.addFragment({
1681
+ fragmentationIndicator: 3, mpuSequenceNumber: mpuSeq,
1682
+ fragmentCounter: 0, data: new Uint8Array([0xb2]), timestamp: 201n, rapFlag: false,
1683
+ packetSequenceNumber: testPacketSequence(mpuSeq, 0, 10),
1684
+ })
1685
+ expect(b).not.toBeNull()
1686
+ expect(b!.data).toEqual(new Uint8Array([0xb1, 0xb2]))
1687
+ })
1688
+
1689
+ test('reassembles FFmpeg timed MFU fragments by sample number and byte offset', () => {
1690
+ const reassembler = new MFUReassembler({ timeoutMs: 1000 })
1691
+ const common = {
1692
+ mpuSequenceNumber: 0,
1693
+ movieFragmentSequenceNumber: 9,
1694
+ sampleNumber: 17,
1695
+ }
1696
+
1697
+ expect(reassembler.addFragment({
1698
+ ...common,
1699
+ fragmentationIndicator: 1,
1700
+ fragmentCounter: 2,
1701
+ sampleOffset: 0,
1702
+ data: new Uint8Array([0xa1]),
1703
+ timestamp: 1000n,
1704
+ rapFlag: true,
1705
+ packetSequenceNumber: testPacketSequence(0, 2),
1706
+ })).toBeNull()
1707
+ expect(reassembler.addFragment({
1708
+ ...common,
1709
+ fragmentationIndicator: 2,
1710
+ fragmentCounter: 1,
1711
+ sampleOffset: 1,
1712
+ data: new Uint8Array([0xb2]),
1713
+ timestamp: 1001n,
1714
+ rapFlag: false,
1715
+ packetSequenceNumber: testPacketSequence(0, 1),
1716
+ })).toBeNull()
1717
+ const out = reassembler.addFragment({
1718
+ ...common,
1719
+ fragmentationIndicator: 3,
1720
+ fragmentCounter: 0,
1721
+ sampleOffset: 2,
1722
+ data: new Uint8Array([0xc3]),
1723
+ timestamp: 1002n,
1724
+ rapFlag: false,
1725
+ packetSequenceNumber: testPacketSequence(0, 0),
1726
+ })
1727
+
1728
+ expect(out).not.toBeNull()
1729
+ expect(out!.data).toEqual(new Uint8Array([0xa1, 0xb2, 0xc3]))
1730
+ expect(out!.timestamp).toBe(1000n)
1731
+ expect(out!.sampleNumber).toBe(17)
1732
+ expect(out!.movieFragmentSequenceNumber).toBe(9)
1733
+ })
1734
+
1735
+ test('buffers a counter gap without advancing the active data unit', () => {
1736
+ const reassembler = new MFUReassembler({ timeoutMs: 1000 })
1737
+ const common = {
1738
+ mpuSequenceNumber: 0,
1739
+ movieFragmentSequenceNumber: 9,
1740
+ sampleNumber: 17,
1741
+ timestamp: 1000n,
1742
+ rapFlag: true,
1743
+ }
1744
+
1745
+ expect(reassembler.addFragment({
1746
+ ...common,
1747
+ fragmentationIndicator: 1,
1748
+ fragmentCounter: 2,
1749
+ sampleOffset: 0,
1750
+ data: new Uint8Array([0xa1, 0xa2]),
1751
+ packetSequenceNumber: testPacketSequence(0, 2),
1752
+ })).toBeNull()
1753
+ expect(reassembler.addFragment({
1754
+ ...common,
1755
+ fragmentationIndicator: 3,
1756
+ fragmentCounter: 0,
1757
+ sampleOffset: 4,
1758
+ data: new Uint8Array([0xc1, 0xc2]),
1759
+ packetSequenceNumber: testPacketSequence(0, 0),
1760
+ })).toBeNull()
1761
+ expect(reassembler.pendingCount).toBe(1)
1762
+
1763
+ const out = reassembler.addFragment({
1764
+ ...common,
1765
+ fragmentationIndicator: 2,
1766
+ fragmentCounter: 1,
1767
+ sampleOffset: 2,
1768
+ data: new Uint8Array([0xb1, 0xb2]),
1769
+ packetSequenceNumber: testPacketSequence(0, 1),
1770
+ })
1771
+ expect(out?.data).toEqual(
1772
+ new Uint8Array([0xa1, 0xa2, 0xb1, 0xb2, 0xc1, 0xc2]),
1773
+ )
1774
+ })
1775
+
1776
+ test('reassembles first-only DU-header fragments across changing packet timestamps', () => {
1777
+ const reassembler = new MFUReassembler({ timeoutMs: 1000 })
1778
+
1779
+ expect(reassembler.addFragment({
1780
+ fragmentationIndicator: 1,
1781
+ mpuSequenceNumber: 44,
1782
+ fragmentCounter: 2,
1783
+ movieFragmentSequenceNumber: 9,
1784
+ sampleNumber: 17,
1785
+ sampleOffset: 0,
1786
+ data: new Uint8Array([0xa1]),
1787
+ timestamp: 12345n,
1788
+ rapFlag: true,
1789
+ packetSequenceNumber: testPacketSequence(44, 2),
1790
+ })).toBeNull()
1791
+ expect(reassembler.addFragment({
1792
+ fragmentationIndicator: 2,
1793
+ mpuSequenceNumber: 44,
1794
+ fragmentCounter: 1,
1795
+ data: new Uint8Array([0xb2]),
1796
+ timestamp: 12346n,
1797
+ rapFlag: false,
1798
+ packetSequenceNumber: testPacketSequence(44, 1),
1799
+ })).toBeNull()
1800
+ const out = reassembler.addFragment({
1801
+ fragmentationIndicator: 3,
1802
+ mpuSequenceNumber: 44,
1803
+ fragmentCounter: 0,
1804
+ data: new Uint8Array([0xc3]),
1805
+ timestamp: 12347n,
1806
+ rapFlag: false,
1807
+ packetSequenceNumber: testPacketSequence(44, 0),
1808
+ })
1809
+
1810
+ expect(out).not.toBeNull()
1811
+ expect(out!.data).toEqual(new Uint8Array([0xa1, 0xb2, 0xc3]))
1812
+ expect(out!.timestamp).toBe(12345n)
1813
+ expect(out!.sampleNumber).toBe(17)
1814
+ expect(out!.movieFragmentSequenceNumber).toBe(9)
1815
+ })
1816
+
1817
+ test('reassembles first-only DU-header fragments with an ISO countdown', () => {
1818
+ const reassembler = new MFUReassembler({ timeoutMs: 1000 })
1819
+ const timestamp = 12345n
1820
+
1821
+ expect(reassembler.addFragment({
1822
+ fragmentationIndicator: 1,
1823
+ mpuSequenceNumber: 44,
1824
+ fragmentCounter: 3,
1825
+ movieFragmentSequenceNumber: 9,
1826
+ sampleNumber: 17,
1827
+ sampleOffset: 0,
1828
+ data: new Uint8Array([0xa1]),
1829
+ timestamp,
1830
+ rapFlag: true,
1831
+ packetSequenceNumber: testPacketSequence(44, 3),
1832
+ })).toBeNull()
1833
+ expect(reassembler.addFragment({
1834
+ fragmentationIndicator: 2,
1835
+ mpuSequenceNumber: 44,
1836
+ fragmentCounter: 2,
1837
+ data: new Uint8Array([0xb2]),
1838
+ timestamp,
1839
+ rapFlag: false,
1840
+ packetSequenceNumber: testPacketSequence(44, 2),
1841
+ })).toBeNull()
1842
+ expect(reassembler.addFragment({
1843
+ fragmentationIndicator: 2,
1844
+ mpuSequenceNumber: 44,
1845
+ fragmentCounter: 1,
1846
+ data: new Uint8Array([0xc3]),
1847
+ timestamp,
1848
+ rapFlag: false,
1849
+ packetSequenceNumber: testPacketSequence(44, 1),
1850
+ })).toBeNull()
1851
+ const out = reassembler.addFragment({
1852
+ fragmentationIndicator: 3,
1853
+ mpuSequenceNumber: 44,
1854
+ fragmentCounter: 0,
1855
+ data: new Uint8Array([0xd4]),
1856
+ timestamp,
1857
+ rapFlag: false,
1858
+ packetSequenceNumber: testPacketSequence(44, 0),
1859
+ })
1860
+
1861
+ expect(out).not.toBeNull()
1862
+ expect(out!.data).toEqual(new Uint8Array([0xa1, 0xb2, 0xc3, 0xd4]))
1863
+ expect(out!.sampleNumber).toBe(17)
1864
+ expect(out!.movieFragmentSequenceNumber).toBe(9)
1865
+ })
1866
+
1867
+ test('rejects fragmented MFUs without an authored DU identity', () => {
1868
+ const reassembler = new MFUReassembler({ timeoutMs: 1000 })
1869
+ const timestamp = 22222n
1870
+
1871
+ expectRejection(() => reassembler.addFragment({
1872
+ fragmentationIndicator: 1,
1873
+ mpuSequenceNumber: 55,
1874
+ fragmentCounter: 2,
1875
+ data: new Uint8Array([0xa1]),
1876
+ timestamp,
1877
+ rapFlag: true,
1878
+ packetSequenceNumber: testPacketSequence(55, 2),
1879
+ }), 'missing-data-unit-identity')
1880
+ expect(reassembler.pendingCount).toBe(0)
1881
+ })
1882
+
1883
+ test('rejects fragmented MFUs without an authored packet sequence', () => {
1884
+ const reassembler = new MFUReassembler({ timeoutMs: 1000 })
1885
+ expectRejection(() => reassembler.addFragment({
1886
+ fragmentationIndicator: 1,
1887
+ mpuSequenceNumber: 55,
1888
+ fragmentCounter: 1,
1889
+ itemId: 1,
1890
+ data: new Uint8Array([0xa1]),
1891
+ timestamp: 22222n,
1892
+ rapFlag: true,
1893
+ }), 'missing-packet-sequence')
1894
+ expect(reassembler.pendingCount).toBe(0)
1895
+ })
1896
+
1897
+ test('rejects fragmented MFUs whose authored packet sequence is not u32', () => {
1898
+ const reassembler = new MFUReassembler({ timeoutMs: 1000 })
1899
+ for (const packetSequenceNumber of [-1, 0x1_0000_0000, 1.5, Number.NaN]) {
1900
+ expectRejection(() => reassembler.addFragment({
1901
+ fragmentationIndicator: 1,
1902
+ mpuSequenceNumber: 55,
1903
+ fragmentCounter: 1,
1904
+ itemId: 1,
1905
+ data: new Uint8Array([0xa1]),
1906
+ timestamp: 22222n,
1907
+ rapFlag: true,
1908
+ packetSequenceNumber,
1909
+ }), 'missing-packet-sequence')
1910
+ }
1911
+ expect(reassembler.pendingCount).toBe(0)
1912
+ })
1913
+
1914
+ test('fails closed when one fragmented DU has interleaved packet sequence gaps', () => {
1915
+ const reassembler = new MFUReassembler({ timeoutMs: 1000 })
1916
+ const timestamp = 66666n
1917
+
1918
+ expect(reassembler.addFragment({
1919
+ fragmentationIndicator: 1,
1920
+ mpuSequenceNumber: 80,
1921
+ fragmentCounter: 17,
1922
+ movieFragmentSequenceNumber: 241,
1923
+ sampleNumber: 1,
1924
+ sampleOffset: 0,
1925
+ packetSequenceNumber: 1000,
1926
+ data: new Uint8Array([0xa0]),
1927
+ timestamp,
1928
+ rapFlag: true,
1929
+ })).toBeNull()
1930
+
1931
+ for (let i = 1; i < 17; i++) {
1932
+ expect(reassembler.addFragment({
1933
+ fragmentationIndicator: 2,
1934
+ mpuSequenceNumber: 80,
1935
+ fragmentCounter: 17 - i,
1936
+ packetSequenceNumber: 1000 + i * 2,
1937
+ data: new Uint8Array([0xa0 + i]),
1938
+ timestamp,
1939
+ rapFlag: false,
1940
+ })).toBeNull()
1941
+ }
1942
+
1943
+ const out = reassembler.addFragment({
1944
+ fragmentationIndicator: 3,
1945
+ mpuSequenceNumber: 80,
1946
+ fragmentCounter: 0,
1947
+ packetSequenceNumber: 1040,
1948
+ data: new Uint8Array([0xb1]),
1949
+ timestamp,
1950
+ rapFlag: false,
1951
+ })
1952
+
1953
+ expect(out).toBeNull()
1954
+ expect(reassembler.pendingCount).toBe(18)
1955
+ expect(reassembler.stats.reassembledMFUs).toBe(0)
1956
+ })
1957
+
1958
+ test('does not combine adjacent reordered MFUs in one MPU', () => {
1959
+ const reassembler = new MFUReassembler({ timeoutMs: 1000 })
1960
+ const common = { mpuSequenceNumber: 80, packetId: 7, timestamp: 66666n }
1961
+
1962
+ expect(reassembler.addFragment({
1963
+ ...common,
1964
+ fragmentationIndicator: 3,
1965
+ fragmentCounter: 0,
1966
+ packetSequenceNumber: 103,
1967
+ data: new Uint8Array([0xb2]),
1968
+ rapFlag: false,
1969
+ })).toBeNull()
1970
+ expect(reassembler.addFragment({
1971
+ ...common,
1972
+ fragmentationIndicator: 1,
1973
+ fragmentCounter: 1,
1974
+ packetSequenceNumber: 100,
1975
+ movieFragmentSequenceNumber: 241,
1976
+ sampleNumber: 1,
1977
+ sampleOffset: 0,
1978
+ data: new Uint8Array([0xa1]),
1979
+ rapFlag: true,
1980
+ })).toBeNull()
1981
+
1982
+ const earlier = reassembler.addFragment({
1983
+ ...common,
1984
+ fragmentationIndicator: 3,
1985
+ fragmentCounter: 0,
1986
+ packetSequenceNumber: 101,
1987
+ data: new Uint8Array([0xa2]),
1988
+ rapFlag: false,
1989
+ })
1990
+ expect(earlier?.data).toEqual(new Uint8Array([0xa1, 0xa2]))
1991
+
1992
+ const later = reassembler.addFragment({
1993
+ ...common,
1994
+ fragmentationIndicator: 1,
1995
+ fragmentCounter: 1,
1996
+ packetSequenceNumber: 102,
1997
+ movieFragmentSequenceNumber: 241,
1998
+ sampleNumber: 2,
1999
+ sampleOffset: 0,
2000
+ data: new Uint8Array([0xb1]),
2001
+ rapFlag: false,
2002
+ })
2003
+ expect(later?.data).toEqual(new Uint8Array([0xb1, 0xb2]))
2004
+ })
2005
+
2006
+ test('wraps the fragmented run coordinate at u32 packet sequence', () => {
2007
+ const reassembler = new MFUReassembler({ timeoutMs: 1000 })
2008
+ expect(reassembler.addFragment({
2009
+ fragmentationIndicator: 1,
2010
+ mpuSequenceNumber: 81,
2011
+ fragmentCounter: 1,
2012
+ packetSequenceNumber: 0xffffffff,
2013
+ itemId: 1,
2014
+ data: new Uint8Array([0xa1]),
2015
+ timestamp: 66666n,
2016
+ rapFlag: true,
2017
+ })).toBeNull()
2018
+ expect(reassembler.addFragment({
2019
+ fragmentationIndicator: 3,
2020
+ mpuSequenceNumber: 81,
2021
+ fragmentCounter: 0,
2022
+ packetSequenceNumber: 0,
2023
+ data: new Uint8Array([0xa2]),
2024
+ timestamp: 66667n,
2025
+ rapFlag: false,
2026
+ })?.data).toEqual(new Uint8Array([0xa1, 0xa2]))
2027
+ })
2028
+
2029
+ test('dedupes recovered copies by ISO fragment-counter position', () => {
2030
+ const reassembler = new MFUReassembler({ timeoutMs: 1000 })
2031
+ const timestamp = 44444n
2032
+
2033
+ expect(reassembler.addFragment({
2034
+ fragmentationIndicator: 1,
2035
+ mpuSequenceNumber: 77,
2036
+ fragmentCounter: 2,
2037
+ movieFragmentSequenceNumber: 12,
2038
+ sampleNumber: 1,
2039
+ sampleOffset: 0,
2040
+ packetSequenceNumber: 2000,
2041
+ data: new Uint8Array([0xa1]),
2042
+ timestamp,
2043
+ rapFlag: true,
2044
+ })).toBeNull()
2045
+ expect(reassembler.addFragment({
2046
+ fragmentationIndicator: 2,
2047
+ mpuSequenceNumber: 77,
2048
+ fragmentCounter: 1,
2049
+ packetSequenceNumber: 2001,
2050
+ data: new Uint8Array([0xb2]),
2051
+ timestamp,
2052
+ rapFlag: false,
2053
+ })).toBeNull()
2054
+ expect(reassembler.addFragment({
2055
+ fragmentationIndicator: 3,
2056
+ mpuSequenceNumber: 77,
2057
+ fragmentCounter: 0,
2058
+ packetSequenceNumber: 2002,
2059
+ data: new Uint8Array([0xc3]),
2060
+ timestamp,
2061
+ rapFlag: false,
2062
+ })).not.toBeNull()
2063
+
2064
+ reassembler.flush()
2065
+
2066
+ expect(reassembler.addFragment({
2067
+ fragmentationIndicator: 1,
2068
+ mpuSequenceNumber: 78,
2069
+ fragmentCounter: 2,
2070
+ movieFragmentSequenceNumber: 13,
2071
+ sampleNumber: 1,
2072
+ sampleOffset: 0,
2073
+ packetSequenceNumber: 3000,
2074
+ data: new Uint8Array([0xd4]),
2075
+ timestamp,
2076
+ rapFlag: true,
2077
+ })).toBeNull()
2078
+ expect(reassembler.addFragment({
2079
+ fragmentationIndicator: 2,
2080
+ mpuSequenceNumber: 78,
2081
+ fragmentCounter: 1,
2082
+ packetSequenceNumber: 3001,
2083
+ data: new Uint8Array([0xe5]),
2084
+ timestamp,
2085
+ rapFlag: false,
2086
+ })).toBeNull()
2087
+ expectRejection(() => reassembler.addFragment({
2088
+ fragmentationIndicator: 2,
2089
+ mpuSequenceNumber: 78,
2090
+ fragmentCounter: 1,
2091
+ packetSequenceNumber: 3001,
2092
+ data: new Uint8Array([0xe5]),
2093
+ timestamp,
2094
+ rapFlag: false,
2095
+ }), 'fragment-counter-mismatch')
2096
+ const out = reassembler.addFragment({
2097
+ fragmentationIndicator: 3,
2098
+ mpuSequenceNumber: 78,
2099
+ fragmentCounter: 0,
2100
+ packetSequenceNumber: 3002,
2101
+ data: new Uint8Array([0xf6]),
2102
+ timestamp,
2103
+ rapFlag: false,
2104
+ })
2105
+
2106
+ expect(out).not.toBeNull()
2107
+ expect(out!.data).toEqual(new Uint8Array([0xd4, 0xe5, 0xf6]))
2108
+ })
2109
+
2110
+ test('reassembles middle and terminal continuations that arrive before first', () => {
2111
+ const reassembler = new MFUReassembler({ timeoutMs: 1000 })
2112
+ const timestamp = 54321n
2113
+
2114
+ expect(reassembler.addFragment({
2115
+ fragmentationIndicator: 2,
2116
+ mpuSequenceNumber: 45,
2117
+ fragmentCounter: 1,
2118
+ data: new Uint8Array([0xb2]),
2119
+ timestamp,
2120
+ rapFlag: false,
2121
+ packetSequenceNumber: testPacketSequence(45, 1),
2122
+ })).toBeNull()
2123
+ expect(reassembler.addFragment({
2124
+ fragmentationIndicator: 3,
2125
+ mpuSequenceNumber: 45,
2126
+ fragmentCounter: 0,
2127
+ data: new Uint8Array([0xc3]),
2128
+ timestamp: timestamp + 1n,
2129
+ rapFlag: false,
2130
+ packetSequenceNumber: testPacketSequence(45, 0),
2131
+ })).toBeNull()
2132
+ expect(reassembler.pendingCount).toBe(1)
2133
+ const out = reassembler.addFragment({
2134
+ fragmentationIndicator: 1,
2135
+ mpuSequenceNumber: 45,
2136
+ fragmentCounter: 2,
2137
+ movieFragmentSequenceNumber: 10,
2138
+ sampleNumber: 18,
2139
+ sampleOffset: 0,
2140
+ data: new Uint8Array([0xa1]),
2141
+ timestamp,
2142
+ rapFlag: true,
2143
+ packetSequenceNumber: testPacketSequence(45, 2),
2144
+ })
2145
+
2146
+ expect(out).not.toBeNull()
2147
+ expect(out!.data).toEqual(new Uint8Array([0xa1, 0xb2, 0xc3]))
2148
+ expect(out!.sampleNumber).toBe(18)
2149
+ expect(out!.movieFragmentSequenceNumber).toBe(10)
2150
+ expect(reassembler.pendingCount).toBe(0)
2151
+ })
2152
+
2153
+ test('rejects a duplicate continuation before first without replacing retained media', () => {
2154
+ const reassembler = new MFUReassembler({ timeoutMs: 1000 })
2155
+ const timestamp = 54322n
2156
+
2157
+ expect(reassembler.addFragment({
2158
+ fragmentationIndicator: 3,
2159
+ mpuSequenceNumber: 46,
2160
+ fragmentCounter: 0,
2161
+ data: new Uint8Array([0xc3]),
2162
+ timestamp,
2163
+ rapFlag: false,
2164
+ packetSequenceNumber: testPacketSequence(46, 0),
2165
+ })).toBeNull()
2166
+ expectRejection(() => reassembler.addFragment({
2167
+ fragmentationIndicator: 3,
2168
+ mpuSequenceNumber: 46,
2169
+ fragmentCounter: 0,
2170
+ data: new Uint8Array([0xff]),
2171
+ timestamp: timestamp + 1n,
2172
+ rapFlag: false,
2173
+ packetSequenceNumber: testPacketSequence(46, 0),
2174
+ }), 'fragment-counter-mismatch')
2175
+ expect(reassembler.addFragment({
2176
+ fragmentationIndicator: 2,
2177
+ mpuSequenceNumber: 46,
2178
+ fragmentCounter: 1,
2179
+ data: new Uint8Array([0xb2]),
2180
+ timestamp: timestamp + 2n,
2181
+ rapFlag: false,
2182
+ packetSequenceNumber: testPacketSequence(46, 1),
2183
+ })).toBeNull()
2184
+
2185
+ const out = reassembler.addFragment({
2186
+ fragmentationIndicator: 1,
2187
+ mpuSequenceNumber: 46,
2188
+ fragmentCounter: 2,
2189
+ movieFragmentSequenceNumber: 10,
2190
+ sampleNumber: 19,
2191
+ sampleOffset: 0,
2192
+ data: new Uint8Array([0xa1]),
2193
+ timestamp: timestamp + 3n,
2194
+ rapFlag: true,
2195
+ packetSequenceNumber: testPacketSequence(46, 2),
2196
+ })
2197
+
2198
+ expect(out).not.toBeNull()
2199
+ expect(out!.data).toEqual(new Uint8Array([0xa1, 0xb2, 0xc3]))
2200
+ expect(reassembler.pendingCount).toBe(0)
2201
+ })
2202
+
2203
+ test('rejects invalid ISO fragmentation-indicator and counter combinations', () => {
2204
+ const reassembler = new MFUReassembler({ timeoutMs: 1000 })
2205
+ const invalidHeaders = [
2206
+ { fragmentationIndicator: 0, fragmentCounter: 1 },
2207
+ { fragmentationIndicator: 3, fragmentCounter: 1 },
2208
+ { fragmentationIndicator: 1, fragmentCounter: 0 },
2209
+ { fragmentationIndicator: 2, fragmentCounter: 0 },
2210
+ { fragmentationIndicator: -1, fragmentCounter: 1 },
2211
+ { fragmentationIndicator: 4, fragmentCounter: 1 },
2212
+ { fragmentationIndicator: 1.5, fragmentCounter: 1 },
2213
+ { fragmentationIndicator: 1, fragmentCounter: -1 },
2214
+ { fragmentationIndicator: 1, fragmentCounter: 256 },
2215
+ { fragmentationIndicator: 1, fragmentCounter: 1.5 },
2216
+ { fragmentationIndicator: 1, fragmentCounter: Number.NaN },
2217
+ ]
2218
+
2219
+ for (const header of invalidHeaders) {
2220
+ expectRejection(() => reassembler.addFragment({
2221
+ ...header,
2222
+ mpuSequenceNumber: 48,
2223
+ data: new Uint8Array([0xa1]),
2224
+ timestamp: 54324n,
2225
+ rapFlag: false,
2226
+ }), 'invalid-fragment-state')
2227
+ }
2228
+
2229
+ expect(reassembler.pendingCount).toBe(0)
2230
+ expect(reassembler.stats.reassembledMFUs).toBe(0)
2231
+ })
2232
+
2233
+ test('reassembles the maximum ISO chain of 256 payloads', () => {
2234
+ const reassembler = new MFUReassembler({ timeoutMs: null })
2235
+ const common = {
2236
+ mpuSequenceNumber: 47,
2237
+ timestamp: 54323n,
2238
+ rapFlag: false,
2239
+ }
2240
+
2241
+ expect(reassembler.addFragment({
2242
+ ...common,
2243
+ fragmentationIndicator: 1,
2244
+ fragmentCounter: 255,
2245
+ itemId: 1,
2246
+ data: new Uint8Array([0]),
2247
+ rapFlag: true,
2248
+ packetSequenceNumber: testPacketSequence(47, 255),
2249
+ })).toBeNull()
2250
+ for (let counter = 254; counter > 0; counter--) {
2251
+ expect(reassembler.addFragment({
2252
+ ...common,
2253
+ fragmentationIndicator: 2,
2254
+ fragmentCounter: counter,
2255
+ data: new Uint8Array([255 - counter]),
2256
+ packetSequenceNumber: testPacketSequence(47, counter),
2257
+ })).toBeNull()
2258
+ }
2259
+ const out = reassembler.addFragment({
2260
+ ...common,
2261
+ fragmentationIndicator: 3,
2262
+ fragmentCounter: 0,
2263
+ data: new Uint8Array([255]),
2264
+ packetSequenceNumber: testPacketSequence(47, 0),
2265
+ })
2266
+
2267
+ expect(out?.fragmentCount).toBe(256)
2268
+ expect(out?.data).toEqual(Uint8Array.from({ length: 256 }, (_, index) => index))
2269
+ })
2270
+
2271
+ test('should not reassemble without last fragment (FI=3)', () => {
2272
+ const reassembler = new MFUReassembler({ timeoutMs: 1000 })
2273
+ reassembler.addFragment({
2274
+ fragmentationIndicator: 1, mpuSequenceNumber: 1,
2275
+ fragmentCounter: 2, itemId: 1, data: new Uint8Array([1]),
2276
+ timestamp: 1000n, rapFlag: false,
2277
+ packetSequenceNumber: testPacketSequence(1, 2),
2278
+ })
2279
+ const result = reassembler.addFragment({
2280
+ fragmentationIndicator: 2, mpuSequenceNumber: 1,
2281
+ fragmentCounter: 1, data: new Uint8Array([2]),
2282
+ timestamp: 1000n, rapFlag: false,
2283
+ packetSequenceNumber: testPacketSequence(1, 1),
2284
+ })
2285
+ expect(result).toBeNull()
2286
+ expect(reassembler.pendingCount).toBe(1)
2287
+ })
2288
+
2289
+ test('bounds continuations that arrive before their first fragments', () => {
2290
+ const reassembler = new MFUReassembler({ timeoutMs: null, maxBuffered: 1 })
2291
+ expect(reassembler.addFragment({
2292
+ fragmentationIndicator: 3, mpuSequenceNumber: 1,
2293
+ fragmentCounter: 0, data: new Uint8Array([1]),
2294
+ timestamp: 1000n, rapFlag: false,
2295
+ packetSequenceNumber: testPacketSequence(1, 0),
2296
+ })).toBeNull()
2297
+ expectRejection(() => reassembler.addFragment({
2298
+ fragmentationIndicator: 3, mpuSequenceNumber: 2,
2299
+ fragmentCounter: 0, data: new Uint8Array([2]),
2300
+ timestamp: 1000n, rapFlag: false,
2301
+ packetSequenceNumber: testPacketSequence(2, 0),
2302
+ }), 'active-data-unit-limit')
2303
+ expectRejection(() => reassembler.addFragment({
2304
+ fragmentationIndicator: 1, mpuSequenceNumber: 2,
2305
+ fragmentCounter: 1, itemId: 2, data: new Uint8Array([2]),
2306
+ timestamp: 1000n, rapFlag: true,
2307
+ packetSequenceNumber: testPacketSequence(2, 1),
2308
+ }), 'active-data-unit-limit')
2309
+ expect(reassembler.pendingCount).toBe(1)
2310
+ expect(reassembler.getStatus().pendingFragments).toBe(1)
2311
+
2312
+ const result = reassembler.addFragment({
2313
+ fragmentationIndicator: 1, mpuSequenceNumber: 1,
2314
+ fragmentCounter: 1, itemId: 1, data: new Uint8Array([3]),
2315
+ timestamp: 1000n, rapFlag: true,
2316
+ packetSequenceNumber: testPacketSequence(1, 1),
2317
+ })
2318
+ expect(result?.data).toEqual(new Uint8Array([3, 1]))
2319
+ })
2320
+
2321
+ test('expires a continuation that never receives its first fragment', () => {
2322
+ vi.useFakeTimers()
2323
+ try {
2324
+ const reassembler = new MFUReassembler({ timeoutMs: 50, maxBuffered: 1 })
2325
+ expect(reassembler.addFragment({
2326
+ fragmentationIndicator: 3, mpuSequenceNumber: 1,
2327
+ fragmentCounter: 0, data: new Uint8Array([1]),
2328
+ timestamp: 1000n, rapFlag: false,
2329
+ packetSequenceNumber: testPacketSequence(1, 0),
2330
+ })).toBeNull()
2331
+ vi.advanceTimersByTime(50)
2332
+ expect(reassembler.pendingCount).toBe(0)
2333
+ expect(reassembler.stats.timedOutMFUs).toBe(1)
2334
+ } finally {
2335
+ vi.useRealTimers()
2336
+ }
2337
+ })
2338
+
2339
+ test('retimes retained continuations when the timeout policy changes', () => {
2340
+ vi.useFakeTimers()
2341
+ try {
2342
+ const enabled = new MFUReassembler({ timeoutMs: null, maxBuffered: 1 })
2343
+ expect(enabled.addFragment({
2344
+ fragmentationIndicator: 3, mpuSequenceNumber: 1,
2345
+ fragmentCounter: 0, data: new Uint8Array([1]),
2346
+ timestamp: 1000n, rapFlag: false,
2347
+ packetSequenceNumber: testPacketSequence(1, 0),
2348
+ })).toBeNull()
2349
+ enabled.timeoutMs = 10
2350
+ vi.advanceTimersByTime(10)
2351
+ expect(enabled.pendingCount).toBe(0)
2352
+
2353
+ const disabled = new MFUReassembler({ timeoutMs: 10, maxBuffered: 1 })
2354
+ expect(disabled.addFragment({
2355
+ fragmentationIndicator: 3, mpuSequenceNumber: 2,
2356
+ fragmentCounter: 0, data: new Uint8Array([2]),
2357
+ timestamp: 1000n, rapFlag: false,
2358
+ packetSequenceNumber: testPacketSequence(2, 0),
2359
+ })).toBeNull()
2360
+ disabled.timeoutMs = null
2361
+ vi.advanceTimersByTime(10)
2362
+ expect(disabled.pendingCount).toBe(1)
2363
+ disabled.flush()
2364
+ } finally {
2365
+ vi.useRealTimers()
2366
+ }
2367
+ })
2368
+
2369
+ test('bounds active data units and rejects a new first at maxBuffered', () => {
2370
+ const reassembler = new MFUReassembler({ timeoutMs: 1000, maxBuffered: 2 })
2371
+
2372
+ // Two runs remain active; the third first fragment is rejected.
2373
+ for (let i = 0; i < 2; i++) {
2374
+ reassembler.addFragment({
2375
+ fragmentationIndicator: 1, mpuSequenceNumber: i,
2376
+ fragmentCounter: 1, itemId: i, data: new Uint8Array([i]),
2377
+ timestamp: BigInt(i * 100), rapFlag: false,
2378
+ packetSequenceNumber: testPacketSequence(i, 1),
2379
+ })
2380
+ }
2381
+ expectRejection(() => reassembler.addFragment({
2382
+ fragmentationIndicator: 1, mpuSequenceNumber: 2,
2383
+ fragmentCounter: 1, itemId: 2, data: new Uint8Array([2]),
2384
+ timestamp: 200n, rapFlag: false,
2385
+ packetSequenceNumber: testPacketSequence(2, 1),
2386
+ }), 'active-data-unit-limit')
2387
+ expect(reassembler.pendingCount).toBe(2)
2388
+ expect(reassembler.stats.timedOutMFUs).toBe(0)
2389
+ expect(reassembler.stats.evictedMemoryLimit).toBe(0)
2390
+ })
2391
+
2392
+ test('should report stats correctly', () => {
2393
+ const reassembler = new MFUReassembler({ timeoutMs: 1000 })
2394
+
2395
+ // One complete MFU
2396
+ reassembler.addFragment({
2397
+ fragmentationIndicator: 0, mpuSequenceNumber: 1,
2398
+ fragmentCounter: 0, itemId: 1, data: new Uint8Array([1]),
2399
+ timestamp: 1000n, rapFlag: false,
2400
+ })
2401
+
2402
+ const status = reassembler.getStatus()
2403
+ expect(status.bufferedMFUs).toBe(0)
2404
+ expect(status.stats.totalFragments).toBe(1)
2405
+ expect(status.stats.reassembledMFUs).toBe(1)
2406
+ })
2407
+
2408
+ test('rejects an overlapping first without replacing the original fragment', () => {
2409
+ const reassembler = new MFUReassembler({ timeoutMs: 1000 })
2410
+
2411
+ reassembler.addFragment({
2412
+ fragmentationIndicator: 1, mpuSequenceNumber: 1,
2413
+ fragmentCounter: 1, itemId: 1, data: new Uint8Array([0xAA]),
2414
+ timestamp: 1000n, rapFlag: true, source: 'multicast',
2415
+ packetSequenceNumber: testPacketSequence(1, 1),
2416
+ })
2417
+ // Duplicate of fragmentCounter=1
2418
+ expectRejection(() => reassembler.addFragment({
2419
+ fragmentationIndicator: 1, mpuSequenceNumber: 1,
2420
+ fragmentCounter: 1, itemId: 2, data: new Uint8Array([0xBB]),
2421
+ timestamp: 1000n, rapFlag: true, source: 'moq',
2422
+ packetSequenceNumber: testPacketSequence(1, 1),
2423
+ }), 'overlapping-data-unit')
2424
+
2425
+ const result = reassembler.addFragment({
2426
+ fragmentationIndicator: 3, mpuSequenceNumber: 1,
2427
+ fragmentCounter: 0, data: new Uint8Array([0xCC]),
2428
+ timestamp: 1000n, rapFlag: false,
2429
+ packetSequenceNumber: testPacketSequence(1, 0),
2430
+ })
2431
+ expect(result).not.toBeNull()
2432
+ expect(result!.data).toEqual(new Uint8Array([0xAA, 0xCC]))
2433
+ expect(result!.fragmentCount).toBe(2)
2434
+ expect(result!.source).toBe('multicast')
2435
+ })
2436
+ })
2437
+
2438
+ test('should retain an assembly with a gap in fragment counters', () => {
2439
+ const reassembler = new MFUReassembler({ timeoutMs: 1000 })
2440
+
2441
+ // First fragment declares three succeeding payloads.
2442
+ reassembler.addFragment({
2443
+ fragmentationIndicator: 1, mpuSequenceNumber: 1,
2444
+ fragmentCounter: 3, itemId: 1, data: new Uint8Array([0xAA]),
2445
+ timestamp: 1000n, rapFlag: true,
2446
+ packetSequenceNumber: testPacketSequence(1, 3),
2447
+ })
2448
+ // Counter 1 arrives while counter 2 is missing.
2449
+ expect(reassembler.addFragment({
2450
+ fragmentationIndicator: 2, mpuSequenceNumber: 1,
2451
+ fragmentCounter: 1, data: new Uint8Array([0xCC]),
2452
+ timestamp: 1000n, rapFlag: false,
2453
+ packetSequenceNumber: testPacketSequence(1, 1),
2454
+ })).toBeNull()
2455
+ // Last fragment has the required zero counter.
2456
+ expect(reassembler.addFragment({
2457
+ fragmentationIndicator: 3, mpuSequenceNumber: 1,
2458
+ fragmentCounter: 0, data: new Uint8Array([0xDD]),
2459
+ timestamp: 1000n, rapFlag: false,
2460
+ packetSequenceNumber: testPacketSequence(1, 0),
2461
+ })).toBeNull()
2462
+ expect(reassembler.pendingCount).toBe(1)
2463
+ expect(reassembler.getStatus().pendingFragments).toBe(3)
2464
+ })
2465
+
2466
+ test('accepts a reordered countdown and rejects a stale duplicate', () => {
2467
+ const reassembler = new MFUReassembler({ timeoutMs: 1000 })
2468
+
2469
+ // Counters 3, 1, 0 arrive (gap at 2).
2470
+ reassembler.addFragment({
2471
+ fragmentationIndicator: 1, mpuSequenceNumber: 1,
2472
+ fragmentCounter: 3, itemId: 1, data: new Uint8Array([0xAA]),
2473
+ timestamp: 1000n, rapFlag: true,
2474
+ packetSequenceNumber: testPacketSequence(1, 3),
2475
+ })
2476
+ expect(reassembler.addFragment({
2477
+ fragmentationIndicator: 2, mpuSequenceNumber: 1,
2478
+ fragmentCounter: 1, data: new Uint8Array([0xCC]),
2479
+ timestamp: 1000n, rapFlag: false,
2480
+ packetSequenceNumber: testPacketSequence(1, 1),
2481
+ })).toBeNull()
2482
+ expect(reassembler.addFragment({
2483
+ fragmentationIndicator: 2, mpuSequenceNumber: 1,
2484
+ fragmentCounter: 2, data: new Uint8Array([0xBB]),
2485
+ timestamp: 1000n, rapFlag: false,
2486
+ packetSequenceNumber: testPacketSequence(1, 2),
2487
+ })).toBeNull()
2488
+ expectRejection(() => reassembler.addFragment({
2489
+ fragmentationIndicator: 2, mpuSequenceNumber: 1,
2490
+ fragmentCounter: 1, data: new Uint8Array([0xCC]),
2491
+ timestamp: 1001n, rapFlag: false,
2492
+ packetSequenceNumber: testPacketSequence(1, 1),
2493
+ }), 'fragment-counter-mismatch')
2494
+ const result = reassembler.addFragment({
2495
+ fragmentationIndicator: 3, mpuSequenceNumber: 1,
2496
+ fragmentCounter: 0, data: new Uint8Array([0xDD]),
2497
+ timestamp: 1002n, rapFlag: false,
2498
+ packetSequenceNumber: testPacketSequence(1, 0),
2499
+ })
2500
+ expect(result).not.toBeNull()
2501
+ expect(result!.data).toEqual(new Uint8Array([0xAA, 0xBB, 0xCC, 0xDD]))
2502
+ expect(result!.fragmentCount).toBe(4)
2503
+ })
2504
+
2505
+ test('rejects an overlapping first without discarding queued continuations', () => {
2506
+ const reassembler = new MFUReassembler({ timeoutMs: 1000 })
2507
+ const fragment = (
2508
+ fragmentationIndicator: number,
2509
+ fragmentCounter: number,
2510
+ byte: number,
2511
+ ): MFUFragment => ({
2512
+ fragmentationIndicator,
2513
+ mpuSequenceNumber: 2,
2514
+ fragmentCounter,
2515
+ data: new Uint8Array([byte]),
2516
+ timestamp: 1000n,
2517
+ rapFlag: fragmentationIndicator === 1,
2518
+ packetSequenceNumber: testPacketSequence(2, fragmentCounter),
2519
+ ...(fragmentationIndicator === 1 ? { itemId: 9 } : {}),
2520
+ })
2521
+
2522
+ expect(reassembler.addFragment(fragment(1, 3, 0xAA))).toBeNull()
2523
+ expect(reassembler.addFragment(fragment(2, 1, 0xCC))).toBeNull()
2524
+ expectRejection(
2525
+ () => reassembler.addFragment(fragment(1, 1, 0xEE)),
2526
+ 'overlapping-data-unit',
2527
+ )
2528
+ expect(reassembler.addFragment(fragment(2, 2, 0xBB))).toBeNull()
2529
+ expect(reassembler.addFragment(fragment(3, 0, 0xDD))?.data).toEqual(
2530
+ new Uint8Array([0xAA, 0xBB, 0xCC, 0xDD]),
2531
+ )
2532
+ })
2533
+
2534
+ test('rejects a duplicate emitted intrinsic identity', () => {
2535
+ const reassembler = new MFUReassembler({ timeoutMs: 1000 })
2536
+
2537
+ expect(reassembler.addFragment({
2538
+ fragmentationIndicator: 1, mpuSequenceNumber: 42,
2539
+ fragmentCounter: 1, itemId: 1, data: new Uint8Array([0xAA]),
2540
+ timestamp: 1000n, rapFlag: true,
2541
+ packetSequenceNumber: testPacketSequence(42, 1),
2542
+ })).toBeNull()
2543
+ expect(reassembler.addFragment({
2544
+ fragmentationIndicator: 3, mpuSequenceNumber: 42,
2545
+ fragmentCounter: 0, data: new Uint8Array([0xAB]),
2546
+ timestamp: 1001n, rapFlag: false,
2547
+ packetSequenceNumber: testPacketSequence(42, 0),
2548
+ })).not.toBeNull()
2549
+ expect(reassembler.addFragment({
2550
+ fragmentationIndicator: 3, mpuSequenceNumber: 42,
2551
+ fragmentCounter: 0, data: new Uint8Array([0xBC]),
2552
+ timestamp: 1002n, rapFlag: false,
2553
+ packetSequenceNumber: testPacketSequence(42, 0, 10),
2554
+ })).toBeNull()
2555
+ expect(reassembler.pendingCount).toBe(1)
2556
+ expect(reassembler.addFragment({
2557
+ fragmentationIndicator: 1, mpuSequenceNumber: 42,
2558
+ fragmentCounter: 1, itemId: 1, data: new Uint8Array([0xBB]),
2559
+ timestamp: 2000n, rapFlag: true,
2560
+ packetSequenceNumber: testPacketSequence(42, 1, 10),
2561
+ })).toBeNull()
2562
+ expect(reassembler.pendingCount).toBe(0)
2563
+
2564
+ const result = reassembler.addFragment({
2565
+ fragmentationIndicator: 1, mpuSequenceNumber: 43,
2566
+ fragmentCounter: 1, itemId: 1, data: new Uint8Array([0xCC]),
2567
+ timestamp: 2000n, rapFlag: false,
2568
+ packetSequenceNumber: testPacketSequence(43, 1),
2569
+ })
2570
+ expect(result).toBeNull()
2571
+ expect(reassembler.addFragment({
2572
+ fragmentationIndicator: 3, mpuSequenceNumber: 43,
2573
+ fragmentCounter: 0, data: new Uint8Array([0xDD]),
2574
+ timestamp: 2001n, rapFlag: false,
2575
+ packetSequenceNumber: testPacketSequence(43, 0),
2576
+ })?.data).toEqual(new Uint8Array([0xCC, 0xDD]))
2577
+ })
2578
+
2579
+ test('does not create active state for an emitted identity duplicate', () => {
2580
+ const reassembler = new MFUReassembler({ timeoutMs: 1000 })
2581
+
2582
+ expect(reassembler.addFragment({
2583
+ fragmentationIndicator: 1,
2584
+ mpuSequenceNumber: 50,
2585
+ fragmentCounter: 1,
2586
+ movieFragmentSequenceNumber: 10,
2587
+ sampleNumber: 18,
2588
+ sampleOffset: 0,
2589
+ data: new Uint8Array([0xa1]),
2590
+ timestamp: 1000n,
2591
+ rapFlag: true,
2592
+ packetSequenceNumber: testPacketSequence(50, 1),
2593
+ })).toBeNull()
2594
+ expect(reassembler.addFragment({
2595
+ fragmentationIndicator: 3,
2596
+ mpuSequenceNumber: 50,
2597
+ fragmentCounter: 0,
2598
+ data: new Uint8Array([0xb2]),
2599
+ timestamp: 1000n,
2600
+ rapFlag: false,
2601
+ packetSequenceNumber: testPacketSequence(50, 0),
2602
+ })?.data).toEqual(new Uint8Array([0xa1, 0xb2]))
2603
+ expect(reassembler.pendingCount).toBe(0)
2604
+
2605
+ expect(reassembler.addFragment({
2606
+ fragmentationIndicator: 1,
2607
+ mpuSequenceNumber: 50,
2608
+ fragmentCounter: 1,
2609
+ movieFragmentSequenceNumber: 10,
2610
+ sampleNumber: 18,
2611
+ sampleOffset: 0,
2612
+ data: new Uint8Array([0xa1]),
2613
+ timestamp: 2000n,
2614
+ rapFlag: true,
2615
+ packetSequenceNumber: testPacketSequence(50, 1, 10),
2616
+ })).toBeNull()
2617
+ expect(reassembler.pendingCount).toBe(0)
2618
+ })
2619
+
2620
+ test('flush should clear emitted set and allow re-assembly', () => {
2621
+ const reassembler = new MFUReassembler({ timeoutMs: 1000 })
2622
+
2623
+ // Use fragmented MFU (FI=1 start + FI=3 end) — only fragmented MFUs
2624
+ // go through the #emitted dedup set. FI=0 (complete) bypasses it.
2625
+ const first = reassembler.addFragment({
2626
+ fragmentationIndicator: 1, mpuSequenceNumber: 42,
2627
+ fragmentCounter: 1, itemId: 1, data: new Uint8Array([0xAA]),
2628
+ timestamp: 1000n, rapFlag: true,
2629
+ packetSequenceNumber: testPacketSequence(42, 1),
2630
+ })
2631
+ expect(first).toBeNull() // first fragment, not yet complete
2632
+ const assembled = reassembler.addFragment({
2633
+ fragmentationIndicator: 3, mpuSequenceNumber: 42,
2634
+ fragmentCounter: 0, data: new Uint8Array([0xBB]),
2635
+ timestamp: 1000n, rapFlag: true,
2636
+ packetSequenceNumber: testPacketSequence(42, 0),
2637
+ })
2638
+ expect(assembled).not.toBeNull() // complete — mpuSeq 42 now in emitted set
2639
+
2640
+ // Rejected before flush — same mpuSeq already emitted
2641
+ expect(reassembler.addFragment({
2642
+ fragmentationIndicator: 1, mpuSequenceNumber: 42,
2643
+ fragmentCounter: 1, itemId: 1, data: new Uint8Array([0xCC]),
2644
+ timestamp: 1000n, rapFlag: true,
2645
+ packetSequenceNumber: testPacketSequence(42, 1, 10),
2646
+ })).toBeNull()
2647
+
2648
+ // After flush, same mpuSeq should work again
2649
+ reassembler.flush()
2650
+ const r2 = reassembler.addFragment({
2651
+ fragmentationIndicator: 1, mpuSequenceNumber: 42,
2652
+ fragmentCounter: 1, itemId: 1, data: new Uint8Array([0xDD]),
2653
+ timestamp: 1000n, rapFlag: true,
2654
+ packetSequenceNumber: testPacketSequence(42, 1),
2655
+ })
2656
+ expect(r2).toBeNull() // first fragment of new assembly
2657
+ const r3 = reassembler.addFragment({
2658
+ fragmentationIndicator: 3, mpuSequenceNumber: 42,
2659
+ fragmentCounter: 0, data: new Uint8Array([0xEE]),
2660
+ timestamp: 1000n, rapFlag: true,
2661
+ packetSequenceNumber: testPacketSequence(42, 0),
2662
+ })
2663
+ expect(r3).not.toBeNull()
2664
+ expect(r3!.data).toEqual(new Uint8Array([0xDD, 0xEE]))
2665
+ })
2666
+
2667
+ describe('createFragment', () => {
2668
+ test('should create fragment from complete timed MMTP packet', () => {
2669
+ // Build: MMTP(12) + MPU(8, T=1 FI=0 payloadLength=25) + MFU_DU(14) + data(5)
2670
+ const packet = new Uint8Array(12 + 8 + 14 + 5)
2671
+ // MMTP header: byte0 = rap=1, byte1 = MPU type
2672
+ packet[0] = 0x01 // RAP flag set
2673
+ packet[1] = 0x00 // packet_type = MPU
2674
+ // packetId = 1
2675
+ const view = new DataView(packet.buffer)
2676
+ view.setUint16(2, 1, false)
2677
+ view.setUint32(4, 42, false) // timestamp
2678
+ view.setUint32(8, 100, false) // sequence number
2679
+
2680
+ // MPU header at offset 12
2681
+ view.setUint16(12, 25, false) // 6 remaining MPU bytes + 14 DU + 5 media
2682
+ packet[14] = 0x28 // FT=2, T=1, FI=0, A=0
2683
+ packet[15] = 0 // fragment_counter
2684
+ view.setUint32(16, 999, false) // mpuSequenceNumber
2685
+
2686
+ // MFU DU header (14 bytes) at offset 20.
2687
+ view.setUint32(20, 7, false) // movie_fragment_sequence_number
2688
+ view.setUint32(24, 1, false) // sample_number
2689
+ // Video data at offset 34
2690
+ packet[34] = 0xDE; packet[35] = 0xAD; packet[36] = 0xBE
2691
+ packet[37] = 0xEF; packet[38] = 0x01
2692
+
2693
+ const frag = createFragment(packet)
2694
+ expect(frag.mpuSequenceNumber).toBe(999)
2695
+ expect(frag.fragmentationIndicator).toBe(0)
2696
+ expect(frag.rapFlag).toBe(true)
2697
+ expect(frag.movieFragmentSequenceNumber).toBe(7)
2698
+ expect(frag.sampleNumber).toBe(1)
2699
+ expect(frag.sampleOffset).toBe(0)
2700
+ expect(frag.data.length).toBe(5) // 5 bytes of video data
2701
+ })
2702
+
2703
+ test('reads Item_ID from a non-timed MFU DU header', () => {
2704
+ const packet = new Uint8Array(12 + 8 + 4 + 2)
2705
+ const view = new DataView(packet.buffer)
2706
+ view.setUint16(2, 1, false)
2707
+ view.setUint32(4, 42, false)
2708
+ view.setUint32(8, 100, false)
2709
+ view.setUint16(12, 12, false) // 6 remaining MPU bytes + 4 DU + 2 media
2710
+ packet[14] = 0x20 // FT=2, T=0, FI=0
2711
+ view.setUint32(16, 999, false)
2712
+ view.setUint32(20, 0x01020304, false)
2713
+ packet.set([0xde, 0xad], 24)
2714
+
2715
+ const fragment = createFragment(packet)
2716
+ expect(fragment.itemId).toBe(0x01020304)
2717
+ expect(fragment.data).toEqual(new Uint8Array([0xde, 0xad]))
2718
+ })
2719
+ })
2720
+
2721
+ describe('processMMTPWithReassembly', () => {
2722
+ test('should skip FEC repair packets', () => {
2723
+ const reassembler = new MFUReassembler({ timeoutMs: 1000 })
2724
+ const packet = new Uint8Array(12 + 8 + 14 + 4)
2725
+ packet[0] = 0x10 // fecType=2 (repair)
2726
+ const result = processMMTPWithReassembly(packet, reassembler)
2727
+ expect(result).toBeNull()
2728
+ })
2729
+
2730
+ test('should return reassembled MFU for complete packet', () => {
2731
+ const reassembler = new MFUReassembler({ timeoutMs: 1000 })
2732
+ const packet = new Uint8Array(12 + 8 + 14 + 4)
2733
+ packet[0] = 0x01 // RAP
2734
+ packet[1] = 0x00 // MPU
2735
+ const view = new DataView(packet.buffer)
2736
+ view.setUint16(2, 1, false) // packetId
2737
+ view.setUint32(4, 100, false) // timestamp
2738
+ view.setUint32(8, 1, false) // seq
2739
+ // MPU: payloadLength=24 (6 + 14 + 4), FT=2 T=1 FI=0 A=0
2740
+ view.setUint16(12, 24, false)
2741
+ packet[14] = 0x28
2742
+ view.setUint32(16, 42, false) // mpuSeq
2743
+ view.setUint32(20, 7, false) // movie_fragment_sequence_number
2744
+ view.setUint32(24, 1, false) // sample_number
2745
+
2746
+ packet[34] = 0x00; packet[35] = 0x00; packet[36] = 0x00; packet[37] = 0x01
2747
+
2748
+ const result = processMMTPWithReassembly(packet, reassembler)
2749
+ expect(result).not.toBeNull()
2750
+ expect(result!.mpuSequenceNumber).toBe(42)
2751
+ expect(result!.rapFlag).toBe(true)
2752
+ })
2753
+ })
2754
+
2755
+ describe('strict delivery provenance', () => {
2756
+ test('folds to multicast only when every fragment is explicitly multicast', () => {
2757
+ expect(deliveryProvenanceOf([{ source: 'multicast' }, { source: 'multicast' }]))
2758
+ .toBe('multicast')
2759
+ })
2760
+
2761
+ test('a single moq fragment makes the whole frame mixed', () => {
2762
+ expect(deliveryProvenanceOf([{ source: 'multicast' }, { source: 'moq' }]))
2763
+ .toBe('mixed')
2764
+ // ...and it does not matter where in the run it lands.
2765
+ expect(deliveryProvenanceOf([{ source: 'moq' }, { source: 'multicast' }]))
2766
+ .toBe('mixed')
2767
+ })
2768
+
2769
+ test('an unattributed fragment degrades multicast to unknown, never to proven', () => {
2770
+ expect(deliveryProvenanceOf([{ source: 'multicast' }, {}])).toBe('unknown')
2771
+ expect(deliveryProvenanceOf([{}])).toBe('unknown')
2772
+ })
2773
+
2774
+ test('mixed absorbs unknown — a moq byte is worse news than a missing label', () => {
2775
+ expect(deliveryProvenanceOf([{ source: 'moq' }, {}])).toBe('mixed')
2776
+ expect(deliveryProvenanceOf([{}, { source: 'moq' }])).toBe('mixed')
2777
+ })
2778
+
2779
+ test('an empty fragment set proves nothing', () => {
2780
+ expect(deliveryProvenanceOf([])).toBe('unknown')
2781
+ })
2782
+
2783
+ test('the fold never upgrades a verdict', () => {
2784
+ expect(foldDeliveryProvenance('mixed', 'multicast')).toBe('mixed')
2785
+ expect(foldDeliveryProvenance('unknown', 'multicast')).toBe('unknown')
2786
+ expect(foldDeliveryProvenance('unknown', 'moq')).toBe('mixed')
2787
+ })
2788
+
2789
+ test('a multicast head with a non-multicast tail is NOT multicast-exclusive', () => {
2790
+ // This is the leak this field exists to close. `source` legitimately
2791
+ // reports 'multicast' — head-fragment-wins is the routing contract —
2792
+ // but the frame contains bytes that never crossed a multicast socket,
2793
+ // so it must never be counted as physical-delivery evidence.
2794
+ const reassembler = new MFUReassembler({ timeoutMs: 1000 })
2795
+ reassembler.addFragment({
2796
+ fragmentationIndicator: 1, mpuSequenceNumber: 1,
2797
+ fragmentCounter: 1, itemId: 1, data: new Uint8Array([0xAA]),
2798
+ timestamp: 1000n, rapFlag: true, source: 'multicast',
2799
+ packetSequenceNumber: testPacketSequence(1, 1),
2800
+ })
2801
+ const result = reassembler.addFragment({
2802
+ fragmentationIndicator: 3, mpuSequenceNumber: 1,
2803
+ fragmentCounter: 0, data: new Uint8Array([0xCC]),
2804
+ timestamp: 1000n, rapFlag: false, source: 'moq',
2805
+ packetSequenceNumber: testPacketSequence(1, 0),
2806
+ })
2807
+ expect(result).not.toBeNull()
2808
+ expect(result!.fragmentCount).toBe(2)
2809
+ expect(result!.source).toBe('multicast')
2810
+ expect(result!.deliveryProvenance).toBe('mixed')
2811
+ })
2812
+
2813
+ test('an all-multicast fragmented frame is multicast-exclusive', () => {
2814
+ const reassembler = new MFUReassembler({ timeoutMs: 1000 })
2815
+ reassembler.addFragment({
2816
+ fragmentationIndicator: 1, mpuSequenceNumber: 2,
2817
+ fragmentCounter: 1, itemId: 1, data: new Uint8Array([0xAA]),
2818
+ timestamp: 2000n, rapFlag: true, source: 'multicast',
2819
+ packetSequenceNumber: testPacketSequence(2, 1),
2820
+ })
2821
+ const result = reassembler.addFragment({
2822
+ fragmentationIndicator: 3, mpuSequenceNumber: 2,
2823
+ fragmentCounter: 0, data: new Uint8Array([0xCC]),
2824
+ timestamp: 2000n, rapFlag: false, source: 'multicast',
2825
+ packetSequenceNumber: testPacketSequence(2, 0),
2826
+ })
2827
+ expect(result).not.toBeNull()
2828
+ expect(result!.deliveryProvenance).toBe('multicast')
2829
+ })
2830
+
2831
+ test('an unlabelled complete frame proves nothing', () => {
2832
+ const reassembler = new MFUReassembler({ timeoutMs: 1000 })
2833
+ const result = reassembler.addFragment({
2834
+ fragmentationIndicator: 0, mpuSequenceNumber: 3,
2835
+ fragmentCounter: 0, itemId: 1, data: new Uint8Array([1]),
2836
+ timestamp: 3000n, rapFlag: true,
2837
+ })
2838
+ expect(result).not.toBeNull()
2839
+ expect(result!.deliveryProvenance).toBe('unknown')
2840
+ })
2841
+ })
2842
+ }