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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (694) hide show
  1. package/README.md +432 -0
  2. package/dist/.build-stamp +0 -0
  3. package/dist/__tests__/test-utils.d.ts +71 -0
  4. package/dist/__tests__/test-utils.d.ts.map +1 -0
  5. package/dist/align.d.ts +152 -0
  6. package/dist/align.d.ts.map +1 -0
  7. package/dist/align.js +88 -0
  8. package/dist/align.js.map +7 -0
  9. package/dist/audio-init-parser.d.ts +66 -0
  10. package/dist/audio-init-parser.d.ts.map +1 -0
  11. package/dist/audio-init-parser.js +118 -0
  12. package/dist/audio-init-parser.js.map +7 -0
  13. package/dist/audio-ring-buffer.d.ts +50 -0
  14. package/dist/audio-ring-buffer.d.ts.map +1 -0
  15. package/dist/audio-ring-buffer.js +93 -0
  16. package/dist/audio-ring-buffer.js.map +7 -0
  17. package/dist/authored-timing.d.ts +32 -0
  18. package/dist/authored-timing.d.ts.map +1 -0
  19. package/dist/authored-timing.js +32 -0
  20. package/dist/authored-timing.js.map +7 -0
  21. package/dist/browser-runtime.js +20087 -0
  22. package/dist/browser-runtime.js.map +7 -0
  23. package/dist/carp-catalog.d.ts +318 -0
  24. package/dist/carp-catalog.d.ts.map +1 -0
  25. package/dist/carp-catalog.js +873 -0
  26. package/dist/carp-catalog.js.map +7 -0
  27. package/dist/catalog-fec-params.d.ts +202 -0
  28. package/dist/catalog-fec-params.d.ts.map +1 -0
  29. package/dist/catalog-fec-params.js +568 -0
  30. package/dist/catalog-fec-params.js.map +7 -0
  31. package/dist/catalog-network-source.d.ts +19 -0
  32. package/dist/catalog-network-source.d.ts.map +1 -0
  33. package/dist/catalog-network-source.js +22 -0
  34. package/dist/catalog-network-source.js.map +7 -0
  35. package/dist/catalog-validator.d.ts +18 -0
  36. package/dist/catalog-validator.d.ts.map +1 -0
  37. package/dist/catalog-validator.js +7747 -0
  38. package/dist/catalog-validator.js.map +7 -0
  39. package/dist/cmaf-block-assembler.d.ts +217 -0
  40. package/dist/cmaf-block-assembler.d.ts.map +1 -0
  41. package/dist/cmaf-block-assembler.js +1769 -0
  42. package/dist/cmaf-block-assembler.js.map +7 -0
  43. package/dist/cmaf-group-batcher.d.ts +38 -0
  44. package/dist/cmaf-group-batcher.d.ts.map +1 -0
  45. package/dist/cmaf-group-batcher.js +195 -0
  46. package/dist/cmaf-group-batcher.js.map +7 -0
  47. package/dist/cmcd-producer.d.ts +104 -0
  48. package/dist/cmcd-producer.d.ts.map +1 -0
  49. package/dist/cmcd-producer.js +207 -0
  50. package/dist/cmcd-producer.js.map +7 -0
  51. package/dist/codec-utils.d.ts +248 -0
  52. package/dist/codec-utils.d.ts.map +1 -0
  53. package/dist/codec-utils.js +1268 -0
  54. package/dist/codec-utils.js.map +7 -0
  55. package/dist/fec-codec.d.ts +32 -0
  56. package/dist/fec-codec.d.ts.map +1 -0
  57. package/dist/fec-codec.js +47 -0
  58. package/dist/fec-codec.js.map +7 -0
  59. package/dist/fec-manager.d.ts +307 -0
  60. package/dist/fec-manager.d.ts.map +1 -0
  61. package/dist/fec-manager.js +1530 -0
  62. package/dist/fec-manager.js.map +7 -0
  63. package/dist/fec-starvation-detector.d.ts +59 -0
  64. package/dist/fec-starvation-detector.d.ts.map +1 -0
  65. package/dist/fec-starvation-detector.js +75 -0
  66. package/dist/fec-starvation-detector.js.map +7 -0
  67. package/dist/fec-worker-setup.d.ts +88 -0
  68. package/dist/fec-worker-setup.d.ts.map +1 -0
  69. package/dist/fec-worker-setup.js +614 -0
  70. package/dist/fec-worker-setup.js.map +7 -0
  71. package/dist/fmp4-completion.d.ts +30 -0
  72. package/dist/fmp4-completion.d.ts.map +1 -0
  73. package/dist/fmp4-completion.js +21 -0
  74. package/dist/fmp4-completion.js.map +7 -0
  75. package/dist/fmp4-reader.d.ts +37 -0
  76. package/dist/fmp4-reader.d.ts.map +1 -0
  77. package/dist/fmp4-reader.js +100 -0
  78. package/dist/fmp4-reader.js.map +7 -0
  79. package/dist/index.d.ts +47 -0
  80. package/dist/index.d.ts.map +1 -0
  81. package/dist/index.js +380 -0
  82. package/dist/index.js.map +7 -0
  83. package/dist/iso-bmff-timing.d.ts +78 -0
  84. package/dist/iso-bmff-timing.d.ts.map +1 -0
  85. package/dist/iso-bmff-timing.js +618 -0
  86. package/dist/iso-bmff-timing.js.map +7 -0
  87. package/dist/loc.d.ts +68 -0
  88. package/dist/loc.d.ts.map +1 -0
  89. package/dist/loc.js +143 -0
  90. package/dist/loc.js.map +7 -0
  91. package/dist/manifest-auth-plan.d.ts +30 -0
  92. package/dist/manifest-auth-plan.d.ts.map +1 -0
  93. package/dist/manifest-auth-plan.js +1285 -0
  94. package/dist/manifest-auth-plan.js.map +7 -0
  95. package/dist/media-router.d.ts +251 -0
  96. package/dist/media-router.d.ts.map +1 -0
  97. package/dist/media-router.js +624 -0
  98. package/dist/media-router.js.map +7 -0
  99. package/dist/mfu-reassembler.d.ts +396 -0
  100. package/dist/mfu-reassembler.d.ts.map +1 -0
  101. package/dist/mfu-reassembler.js +3085 -0
  102. package/dist/mfu-reassembler.js.map +7 -0
  103. package/dist/mfu-reassembly-policy.d.ts +24 -0
  104. package/dist/mfu-reassembly-policy.d.ts.map +1 -0
  105. package/dist/mfu-reassembly-policy.js +99 -0
  106. package/dist/mfu-reassembly-policy.js.map +7 -0
  107. package/dist/mmtp-container-parser.d.ts +434 -0
  108. package/dist/mmtp-container-parser.d.ts.map +1 -0
  109. package/dist/mmtp-container-parser.js +6164 -0
  110. package/dist/mmtp-container-parser.js.map +7 -0
  111. package/dist/mmtp-container-parser.no-modules.js +8999 -0
  112. package/dist/mmtp-container-parser.no-modules.js.map +7 -0
  113. package/dist/mmtp-decode-pipeline.d.ts +132 -0
  114. package/dist/mmtp-decode-pipeline.d.ts.map +1 -0
  115. package/dist/mmtp-decode-pipeline.js +6413 -0
  116. package/dist/mmtp-decode-pipeline.js.map +7 -0
  117. package/dist/mmtp-router.d.ts +390 -0
  118. package/dist/mmtp-router.d.ts.map +1 -0
  119. package/dist/mmtp-router.js +9242 -0
  120. package/dist/mmtp-router.js.map +7 -0
  121. package/dist/mmtp-to-cmaf.d.ts +181 -0
  122. package/dist/mmtp-to-cmaf.d.ts.map +1 -0
  123. package/dist/mmtp-to-cmaf.js +802 -0
  124. package/dist/mmtp-to-cmaf.js.map +7 -0
  125. package/dist/multicast-auth-catalog.d.ts +115 -0
  126. package/dist/multicast-auth-catalog.d.ts.map +1 -0
  127. package/dist/multicast-auth-catalog.js +202 -0
  128. package/dist/multicast-auth-catalog.js.map +7 -0
  129. package/dist/multicast-input.d.ts +107 -0
  130. package/dist/multicast-input.d.ts.map +1 -0
  131. package/dist/multicast-input.js +256 -0
  132. package/dist/multicast-input.js.map +7 -0
  133. package/dist/multicast-manager.d.ts +369 -0
  134. package/dist/multicast-manager.d.ts.map +1 -0
  135. package/dist/multicast-manager.js +13400 -0
  136. package/dist/multicast-manager.js.map +7 -0
  137. package/dist/multicast-transport.d.ts +61 -0
  138. package/dist/multicast-transport.d.ts.map +1 -0
  139. package/dist/multicast-transport.js +1665 -0
  140. package/dist/multicast-transport.js.map +7 -0
  141. package/dist/multicast-unwrap.d.ts +57 -0
  142. package/dist/multicast-unwrap.d.ts.map +1 -0
  143. package/dist/multicast-unwrap.js +384 -0
  144. package/dist/multicast-unwrap.js.map +7 -0
  145. package/dist/no-modules.d.ts +9 -0
  146. package/dist/no-modules.d.ts.map +1 -0
  147. package/dist/no-modules.js +8965 -0
  148. package/dist/no-modules.js.map +7 -0
  149. package/dist/phase14-catalog-override.d.ts +126 -0
  150. package/dist/phase14-catalog-override.d.ts.map +1 -0
  151. package/dist/phase14-catalog-override.js +155 -0
  152. package/dist/phase14-catalog-override.js.map +7 -0
  153. package/dist/repair-fetch.d.ts +58 -0
  154. package/dist/repair-fetch.d.ts.map +1 -0
  155. package/dist/repair-fetch.js +127 -0
  156. package/dist/repair-fetch.js.map +7 -0
  157. package/dist/repair-layers.d.ts +141 -0
  158. package/dist/repair-layers.d.ts.map +1 -0
  159. package/dist/repair-layers.js +303 -0
  160. package/dist/repair-layers.js.map +7 -0
  161. package/dist/sample-combiner.d.ts +212 -0
  162. package/dist/sample-combiner.d.ts.map +1 -0
  163. package/dist/sample-combiner.js +616 -0
  164. package/dist/sample-combiner.js.map +7 -0
  165. package/dist/signaling.d.ts +29 -0
  166. package/dist/signaling.d.ts.map +1 -0
  167. package/dist/signaling.js +232 -0
  168. package/dist/signaling.js.map +7 -0
  169. package/dist/source-auth.d.ts +2 -0
  170. package/dist/source-auth.d.ts.map +1 -0
  171. package/dist/source-auth.js +1238 -0
  172. package/dist/source-auth.js.map +7 -0
  173. package/dist/switching-set-alignment.d.ts +61 -0
  174. package/dist/switching-set-alignment.d.ts.map +1 -0
  175. package/dist/switching-set-alignment.js +85 -0
  176. package/dist/switching-set-alignment.js.map +7 -0
  177. package/dist/timestamp-codec.d.ts +34 -0
  178. package/dist/timestamp-codec.d.ts.map +1 -0
  179. package/dist/timestamp-codec.js +456 -0
  180. package/dist/timestamp-codec.js.map +7 -0
  181. package/dist/track-pipeline-setup.d.ts +72 -0
  182. package/dist/track-pipeline-setup.d.ts.map +1 -0
  183. package/dist/track-pipeline-setup.js +306 -0
  184. package/dist/track-pipeline-setup.js.map +7 -0
  185. package/dist/track-pipeline.d.ts +121 -0
  186. package/dist/track-pipeline.d.ts.map +1 -0
  187. package/dist/track-pipeline.js +229 -0
  188. package/dist/track-pipeline.js.map +7 -0
  189. package/dist/track-session.d.ts +398 -0
  190. package/dist/track-session.d.ts.map +1 -0
  191. package/dist/track-session.js +767 -0
  192. package/dist/track-session.js.map +7 -0
  193. package/dist/transport-pool.d.ts +69 -0
  194. package/dist/transport-pool.d.ts.map +1 -0
  195. package/dist/transport-pool.js +88 -0
  196. package/dist/transport-pool.js.map +7 -0
  197. package/dist/wasm-mfu-reassembler.d.ts +92 -0
  198. package/dist/wasm-mfu-reassembler.d.ts.map +1 -0
  199. package/dist/wasm-mfu-reassembler.js +2139 -0
  200. package/dist/wasm-mfu-reassembler.js.map +7 -0
  201. package/dist/wasm-mmtp-container-parser.d.ts +10 -0
  202. package/dist/wasm-mmtp-container-parser.d.ts.map +1 -0
  203. package/dist/wasm-mmtp-container-parser.js +6418 -0
  204. package/dist/wasm-mmtp-container-parser.js.map +7 -0
  205. package/node_modules/@blockcast/fec-worker/README.md +213 -0
  206. package/node_modules/@blockcast/fec-worker/dist/.build-stamp +0 -0
  207. package/node_modules/@blockcast/fec-worker/dist/fec-worker-client.d.ts +87 -0
  208. package/node_modules/@blockcast/fec-worker/dist/fec-worker-client.d.ts.map +1 -0
  209. package/node_modules/@blockcast/fec-worker/dist/fec-worker-client.js +279 -0
  210. package/node_modules/@blockcast/fec-worker/dist/fec-worker-client.js.map +1 -0
  211. package/node_modules/@blockcast/fec-worker/dist/fec-worker-types.d.ts +252 -0
  212. package/node_modules/@blockcast/fec-worker/dist/fec-worker-types.d.ts.map +1 -0
  213. package/node_modules/@blockcast/fec-worker/dist/fec-worker-types.js +11 -0
  214. package/node_modules/@blockcast/fec-worker/dist/fec-worker-types.js.map +1 -0
  215. package/node_modules/@blockcast/fec-worker/dist/fec-worker.d.ts +14 -0
  216. package/node_modules/@blockcast/fec-worker/dist/fec-worker.d.ts.map +1 -0
  217. package/node_modules/@blockcast/fec-worker/dist/fec-worker.js +565 -0
  218. package/node_modules/@blockcast/fec-worker/dist/fec-worker.js.map +7 -0
  219. package/node_modules/@blockcast/fec-worker/dist/index.d.ts +10 -0
  220. package/node_modules/@blockcast/fec-worker/dist/index.d.ts.map +1 -0
  221. package/node_modules/@blockcast/fec-worker/dist/index.js +8 -0
  222. package/node_modules/@blockcast/fec-worker/dist/index.js.map +1 -0
  223. package/node_modules/@blockcast/fec-worker/dist/shred-fec-worker.d.ts +20 -0
  224. package/node_modules/@blockcast/fec-worker/dist/shred-fec-worker.d.ts.map +1 -0
  225. package/node_modules/@blockcast/fec-worker/dist/shred-fec-worker.js +349 -0
  226. package/node_modules/@blockcast/fec-worker/dist/shred-fec-worker.js.map +7 -0
  227. package/node_modules/@blockcast/fec-worker/dist/shred-worker-client.d.ts +104 -0
  228. package/node_modules/@blockcast/fec-worker/dist/shred-worker-client.d.ts.map +1 -0
  229. package/node_modules/@blockcast/fec-worker/dist/shred-worker-client.js +181 -0
  230. package/node_modules/@blockcast/fec-worker/dist/shred-worker-client.js.map +1 -0
  231. package/node_modules/@blockcast/fec-worker/dist/shred-worker-types.d.ts +179 -0
  232. package/node_modules/@blockcast/fec-worker/dist/shred-worker-types.d.ts.map +1 -0
  233. package/node_modules/@blockcast/fec-worker/dist/shred-worker-types.js +47 -0
  234. package/node_modules/@blockcast/fec-worker/dist/shred-worker-types.js.map +1 -0
  235. package/node_modules/@blockcast/fec-worker/dist/transfer.d.ts +9 -0
  236. package/node_modules/@blockcast/fec-worker/dist/transfer.d.ts.map +1 -0
  237. package/node_modules/@blockcast/fec-worker/dist/transfer.js +13 -0
  238. package/node_modules/@blockcast/fec-worker/dist/transfer.js.map +1 -0
  239. package/node_modules/@blockcast/fec-worker/package.json +59 -0
  240. package/node_modules/@blockcast/fec-worker/src/fec-worker-client.ts +319 -0
  241. package/node_modules/@blockcast/fec-worker/src/fec-worker-types.ts +268 -0
  242. package/node_modules/@blockcast/fec-worker/src/fec-worker.ts +1048 -0
  243. package/node_modules/@blockcast/fec-worker/src/index.ts +32 -0
  244. package/node_modules/@blockcast/fec-worker/src/shred-fec-worker.ts +558 -0
  245. package/node_modules/@blockcast/fec-worker/src/shred-worker-client.ts +222 -0
  246. package/node_modules/@blockcast/fec-worker/src/shred-worker-types.ts +194 -0
  247. package/node_modules/@blockcast/fec-worker/src/transfer.ts +12 -0
  248. package/node_modules/@blockcast/mmt-alta-parse/README.md +181 -0
  249. package/node_modules/@blockcast/mmt-alta-parse/dist/.build-stamp +0 -0
  250. package/node_modules/@blockcast/mmt-alta-parse/dist/index.d.ts +57 -0
  251. package/node_modules/@blockcast/mmt-alta-parse/dist/index.d.ts.map +1 -0
  252. package/node_modules/@blockcast/mmt-alta-parse/dist/index.js +115 -0
  253. package/node_modules/@blockcast/mmt-alta-parse/dist/index.js.map +1 -0
  254. package/node_modules/@blockcast/mmt-alta-parse/package.json +50 -0
  255. package/node_modules/@blockcast/mmt-alta-parse/src/index.ts +136 -0
  256. package/node_modules/@blockcast/mmt-base/README.md +120 -0
  257. package/node_modules/@blockcast/mmt-base/dist/.build-stamp +0 -0
  258. package/node_modules/@blockcast/mmt-base/dist/index.d.ts +2 -0
  259. package/node_modules/@blockcast/mmt-base/dist/index.d.ts.map +1 -0
  260. package/node_modules/@blockcast/mmt-base/dist/index.js +2 -0
  261. package/node_modules/@blockcast/mmt-base/dist/index.js.map +1 -0
  262. package/node_modules/@blockcast/mmt-base/dist/mmtp.d.ts +276 -0
  263. package/node_modules/@blockcast/mmt-base/dist/mmtp.d.ts.map +1 -0
  264. package/node_modules/@blockcast/mmt-base/dist/mmtp.js +645 -0
  265. package/node_modules/@blockcast/mmt-base/dist/mmtp.js.map +1 -0
  266. package/node_modules/@blockcast/mmt-base/package.json +49 -0
  267. package/node_modules/@blockcast/mmt-base/src/index.ts +1 -0
  268. package/node_modules/@blockcast/mmt-base/src/mmtp.ts +899 -0
  269. package/node_modules/@blockcast/mmt-fec/README.md +270 -0
  270. package/node_modules/@blockcast/mmt-fec/dist/.build-stamp +0 -0
  271. package/node_modules/@blockcast/mmt-fec/dist/block-manager.d.ts +82 -0
  272. package/node_modules/@blockcast/mmt-fec/dist/block-manager.d.ts.map +1 -0
  273. package/node_modules/@blockcast/mmt-fec/dist/block-manager.js +296 -0
  274. package/node_modules/@blockcast/mmt-fec/dist/block-manager.js.map +1 -0
  275. package/node_modules/@blockcast/mmt-fec/dist/block-processor.d.ts +72 -0
  276. package/node_modules/@blockcast/mmt-fec/dist/block-processor.d.ts.map +1 -0
  277. package/node_modules/@blockcast/mmt-fec/dist/block-processor.js +192 -0
  278. package/node_modules/@blockcast/mmt-fec/dist/block-processor.js.map +1 -0
  279. package/node_modules/@blockcast/mmt-fec/dist/cmaf-recovery.d.ts +46 -0
  280. package/node_modules/@blockcast/mmt-fec/dist/cmaf-recovery.d.ts.map +1 -0
  281. package/node_modules/@blockcast/mmt-fec/dist/cmaf-recovery.js +96 -0
  282. package/node_modules/@blockcast/mmt-fec/dist/cmaf-recovery.js.map +1 -0
  283. package/node_modules/@blockcast/mmt-fec/dist/cmaf-symbols.d.ts +33 -0
  284. package/node_modules/@blockcast/mmt-fec/dist/cmaf-symbols.d.ts.map +1 -0
  285. package/node_modules/@blockcast/mmt-fec/dist/cmaf-symbols.js +57 -0
  286. package/node_modules/@blockcast/mmt-fec/dist/cmaf-symbols.js.map +1 -0
  287. package/node_modules/@blockcast/mmt-fec/dist/fec-block-mapper.d.ts +185 -0
  288. package/node_modules/@blockcast/mmt-fec/dist/fec-block-mapper.d.ts.map +1 -0
  289. package/node_modules/@blockcast/mmt-fec/dist/fec-block-mapper.js +273 -0
  290. package/node_modules/@blockcast/mmt-fec/dist/fec-block-mapper.js.map +1 -0
  291. package/node_modules/@blockcast/mmt-fec/dist/index.d.ts +22 -0
  292. package/node_modules/@blockcast/mmt-fec/dist/index.d.ts.map +1 -0
  293. package/node_modules/@blockcast/mmt-fec/dist/index.js +22 -0
  294. package/node_modules/@blockcast/mmt-fec/dist/index.js.map +1 -0
  295. package/node_modules/@blockcast/mmt-fec/dist/loc-symbols.d.ts +20 -0
  296. package/node_modules/@blockcast/mmt-fec/dist/loc-symbols.d.ts.map +1 -0
  297. package/node_modules/@blockcast/mmt-fec/dist/loc-symbols.js +28 -0
  298. package/node_modules/@blockcast/mmt-fec/dist/loc-symbols.js.map +1 -0
  299. package/node_modules/@blockcast/mmt-fec/dist/oti.d.ts +172 -0
  300. package/node_modules/@blockcast/mmt-fec/dist/oti.d.ts.map +1 -0
  301. package/node_modules/@blockcast/mmt-fec/dist/oti.js +254 -0
  302. package/node_modules/@blockcast/mmt-fec/dist/oti.js.map +1 -0
  303. package/node_modules/@blockcast/mmt-fec/dist/reorder-buffer.d.ts +41 -0
  304. package/node_modules/@blockcast/mmt-fec/dist/reorder-buffer.d.ts.map +1 -0
  305. package/node_modules/@blockcast/mmt-fec/dist/reorder-buffer.js +81 -0
  306. package/node_modules/@blockcast/mmt-fec/dist/reorder-buffer.js.map +1 -0
  307. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-alta-parse/README.md +181 -0
  308. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-alta-parse/dist/.build-stamp +0 -0
  309. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-alta-parse/dist/index.d.ts +57 -0
  310. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-alta-parse/dist/index.d.ts.map +1 -0
  311. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-alta-parse/dist/index.js +115 -0
  312. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-alta-parse/dist/index.js.map +1 -0
  313. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-alta-parse/package.json +50 -0
  314. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-alta-parse/src/index.ts +136 -0
  315. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/README.md +120 -0
  316. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/.build-stamp +0 -0
  317. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/index.d.ts +2 -0
  318. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/index.d.ts.map +1 -0
  319. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/index.js +2 -0
  320. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/index.js.map +1 -0
  321. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/mmtp.d.ts +276 -0
  322. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/mmtp.d.ts.map +1 -0
  323. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/mmtp.js +645 -0
  324. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/mmtp.js.map +1 -0
  325. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/package.json +49 -0
  326. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/src/index.ts +1 -0
  327. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/src/mmtp.ts +899 -0
  328. package/node_modules/@blockcast/mmt-fec/package.json +79 -0
  329. package/node_modules/@blockcast/mmt-fec/src/block-manager.ts +371 -0
  330. package/node_modules/@blockcast/mmt-fec/src/block-processor.ts +277 -0
  331. package/node_modules/@blockcast/mmt-fec/src/cmaf-recovery.ts +127 -0
  332. package/node_modules/@blockcast/mmt-fec/src/cmaf-symbols.ts +68 -0
  333. package/node_modules/@blockcast/mmt-fec/src/fec-block-mapper.ts +332 -0
  334. package/node_modules/@blockcast/mmt-fec/src/index.ts +77 -0
  335. package/node_modules/@blockcast/mmt-fec/src/loc-symbols.ts +34 -0
  336. package/node_modules/@blockcast/mmt-fec/src/oti.ts +317 -0
  337. package/node_modules/@blockcast/mmt-fec/src/reorder-buffer.ts +101 -0
  338. package/node_modules/@blockcast/mmt-manifest-verify/dist/.build-stamp +0 -0
  339. package/node_modules/@blockcast/mmt-manifest-verify/dist/admission.d.ts +28 -0
  340. package/node_modules/@blockcast/mmt-manifest-verify/dist/admission.d.ts.map +1 -0
  341. package/node_modules/@blockcast/mmt-manifest-verify/dist/admission.js +128 -0
  342. package/node_modules/@blockcast/mmt-manifest-verify/dist/admission.js.map +1 -0
  343. package/node_modules/@blockcast/mmt-manifest-verify/dist/index.d.ts +4 -0
  344. package/node_modules/@blockcast/mmt-manifest-verify/dist/index.d.ts.map +1 -0
  345. package/node_modules/@blockcast/mmt-manifest-verify/dist/index.js +4 -0
  346. package/node_modules/@blockcast/mmt-manifest-verify/dist/index.js.map +1 -0
  347. package/node_modules/@blockcast/mmt-manifest-verify/dist/source-auth.d.ts +37 -0
  348. package/node_modules/@blockcast/mmt-manifest-verify/dist/source-auth.d.ts.map +1 -0
  349. package/node_modules/@blockcast/mmt-manifest-verify/dist/source-auth.js +179 -0
  350. package/node_modules/@blockcast/mmt-manifest-verify/dist/source-auth.js.map +1 -0
  351. package/node_modules/@blockcast/mmt-manifest-verify/dist/verifier.d.ts +99 -0
  352. package/node_modules/@blockcast/mmt-manifest-verify/dist/verifier.d.ts.map +1 -0
  353. package/node_modules/@blockcast/mmt-manifest-verify/dist/verifier.js +492 -0
  354. package/node_modules/@blockcast/mmt-manifest-verify/dist/verifier.js.map +1 -0
  355. package/node_modules/@blockcast/mmt-manifest-verify/package.json +45 -0
  356. package/node_modules/@blockcast/mmt-manifest-verify/src/admission.ts +171 -0
  357. package/node_modules/@blockcast/mmt-manifest-verify/src/index.ts +3 -0
  358. package/node_modules/@blockcast/mmt-manifest-verify/src/source-auth.ts +230 -0
  359. package/node_modules/@blockcast/mmt-manifest-verify/src/verifier.ts +669 -0
  360. package/node_modules/@noble/hashes/LICENSE +21 -0
  361. package/node_modules/@noble/hashes/README.md +521 -0
  362. package/node_modules/@noble/hashes/_assert.d.ts +17 -0
  363. package/node_modules/@noble/hashes/_assert.d.ts.map +1 -0
  364. package/node_modules/@noble/hashes/_assert.js +18 -0
  365. package/node_modules/@noble/hashes/_assert.js.map +1 -0
  366. package/node_modules/@noble/hashes/_blake.d.ts +14 -0
  367. package/node_modules/@noble/hashes/_blake.d.ts.map +1 -0
  368. package/node_modules/@noble/hashes/_blake.js +50 -0
  369. package/node_modules/@noble/hashes/_blake.js.map +1 -0
  370. package/node_modules/@noble/hashes/_md.d.ts +51 -0
  371. package/node_modules/@noble/hashes/_md.d.ts.map +1 -0
  372. package/node_modules/@noble/hashes/_md.js +162 -0
  373. package/node_modules/@noble/hashes/_md.js.map +1 -0
  374. package/node_modules/@noble/hashes/_u64.d.ts +55 -0
  375. package/node_modules/@noble/hashes/_u64.d.ts.map +1 -0
  376. package/node_modules/@noble/hashes/_u64.js +90 -0
  377. package/node_modules/@noble/hashes/_u64.js.map +1 -0
  378. package/node_modules/@noble/hashes/argon2.d.ts +32 -0
  379. package/node_modules/@noble/hashes/argon2.d.ts.map +1 -0
  380. package/node_modules/@noble/hashes/argon2.js +401 -0
  381. package/node_modules/@noble/hashes/argon2.js.map +1 -0
  382. package/node_modules/@noble/hashes/blake1.d.ts +106 -0
  383. package/node_modules/@noble/hashes/blake1.d.ts.map +1 -0
  384. package/node_modules/@noble/hashes/blake1.js +459 -0
  385. package/node_modules/@noble/hashes/blake1.js.map +1 -0
  386. package/node_modules/@noble/hashes/blake2.d.ts +116 -0
  387. package/node_modules/@noble/hashes/blake2.d.ts.map +1 -0
  388. package/node_modules/@noble/hashes/blake2.js +420 -0
  389. package/node_modules/@noble/hashes/blake2.js.map +1 -0
  390. package/node_modules/@noble/hashes/blake2b.d.ts +11 -0
  391. package/node_modules/@noble/hashes/blake2b.d.ts.map +1 -0
  392. package/node_modules/@noble/hashes/blake2b.js +14 -0
  393. package/node_modules/@noble/hashes/blake2b.js.map +1 -0
  394. package/node_modules/@noble/hashes/blake2s.d.ts +20 -0
  395. package/node_modules/@noble/hashes/blake2s.d.ts.map +1 -0
  396. package/node_modules/@noble/hashes/blake2s.js +24 -0
  397. package/node_modules/@noble/hashes/blake2s.js.map +1 -0
  398. package/node_modules/@noble/hashes/blake3.d.ts +54 -0
  399. package/node_modules/@noble/hashes/blake3.d.ts.map +1 -0
  400. package/node_modules/@noble/hashes/blake3.js +255 -0
  401. package/node_modules/@noble/hashes/blake3.js.map +1 -0
  402. package/node_modules/@noble/hashes/crypto.d.ts +2 -0
  403. package/node_modules/@noble/hashes/crypto.d.ts.map +1 -0
  404. package/node_modules/@noble/hashes/crypto.js +5 -0
  405. package/node_modules/@noble/hashes/crypto.js.map +1 -0
  406. package/node_modules/@noble/hashes/cryptoNode.d.ts +2 -0
  407. package/node_modules/@noble/hashes/cryptoNode.d.ts.map +1 -0
  408. package/node_modules/@noble/hashes/cryptoNode.js +18 -0
  409. package/node_modules/@noble/hashes/cryptoNode.js.map +1 -0
  410. package/node_modules/@noble/hashes/eskdf.d.ts +47 -0
  411. package/node_modules/@noble/hashes/eskdf.d.ts.map +1 -0
  412. package/node_modules/@noble/hashes/eskdf.js +166 -0
  413. package/node_modules/@noble/hashes/eskdf.js.map +1 -0
  414. package/node_modules/@noble/hashes/esm/_assert.d.ts +17 -0
  415. package/node_modules/@noble/hashes/esm/_assert.d.ts.map +1 -0
  416. package/node_modules/@noble/hashes/esm/_assert.js +15 -0
  417. package/node_modules/@noble/hashes/esm/_assert.js.map +1 -0
  418. package/node_modules/@noble/hashes/esm/_blake.d.ts +14 -0
  419. package/node_modules/@noble/hashes/esm/_blake.d.ts.map +1 -0
  420. package/node_modules/@noble/hashes/esm/_blake.js +45 -0
  421. package/node_modules/@noble/hashes/esm/_blake.js.map +1 -0
  422. package/node_modules/@noble/hashes/esm/_md.d.ts +51 -0
  423. package/node_modules/@noble/hashes/esm/_md.d.ts.map +1 -0
  424. package/node_modules/@noble/hashes/esm/_md.js +155 -0
  425. package/node_modules/@noble/hashes/esm/_md.js.map +1 -0
  426. package/node_modules/@noble/hashes/esm/_u64.d.ts +55 -0
  427. package/node_modules/@noble/hashes/esm/_u64.d.ts.map +1 -0
  428. package/node_modules/@noble/hashes/esm/_u64.js +67 -0
  429. package/node_modules/@noble/hashes/esm/_u64.js.map +1 -0
  430. package/node_modules/@noble/hashes/esm/argon2.d.ts +32 -0
  431. package/node_modules/@noble/hashes/esm/argon2.d.ts.map +1 -0
  432. package/node_modules/@noble/hashes/esm/argon2.js +392 -0
  433. package/node_modules/@noble/hashes/esm/argon2.js.map +1 -0
  434. package/node_modules/@noble/hashes/esm/blake1.d.ts +106 -0
  435. package/node_modules/@noble/hashes/esm/blake1.d.ts.map +1 -0
  436. package/node_modules/@noble/hashes/esm/blake1.js +452 -0
  437. package/node_modules/@noble/hashes/esm/blake1.js.map +1 -0
  438. package/node_modules/@noble/hashes/esm/blake2.d.ts +116 -0
  439. package/node_modules/@noble/hashes/esm/blake2.d.ts.map +1 -0
  440. package/node_modules/@noble/hashes/esm/blake2.js +413 -0
  441. package/node_modules/@noble/hashes/esm/blake2.js.map +1 -0
  442. package/node_modules/@noble/hashes/esm/blake2b.d.ts +11 -0
  443. package/node_modules/@noble/hashes/esm/blake2b.d.ts.map +1 -0
  444. package/node_modules/@noble/hashes/esm/blake2b.js +11 -0
  445. package/node_modules/@noble/hashes/esm/blake2b.js.map +1 -0
  446. package/node_modules/@noble/hashes/esm/blake2s.d.ts +20 -0
  447. package/node_modules/@noble/hashes/esm/blake2s.d.ts.map +1 -0
  448. package/node_modules/@noble/hashes/esm/blake2s.js +21 -0
  449. package/node_modules/@noble/hashes/esm/blake2s.js.map +1 -0
  450. package/node_modules/@noble/hashes/esm/blake3.d.ts +54 -0
  451. package/node_modules/@noble/hashes/esm/blake3.d.ts.map +1 -0
  452. package/node_modules/@noble/hashes/esm/blake3.js +251 -0
  453. package/node_modules/@noble/hashes/esm/blake3.js.map +1 -0
  454. package/node_modules/@noble/hashes/esm/crypto.d.ts +2 -0
  455. package/node_modules/@noble/hashes/esm/crypto.d.ts.map +1 -0
  456. package/node_modules/@noble/hashes/esm/crypto.js +2 -0
  457. package/node_modules/@noble/hashes/esm/crypto.js.map +1 -0
  458. package/node_modules/@noble/hashes/esm/cryptoNode.d.ts +2 -0
  459. package/node_modules/@noble/hashes/esm/cryptoNode.d.ts.map +1 -0
  460. package/node_modules/@noble/hashes/esm/cryptoNode.js +15 -0
  461. package/node_modules/@noble/hashes/esm/cryptoNode.js.map +1 -0
  462. package/node_modules/@noble/hashes/esm/eskdf.d.ts +47 -0
  463. package/node_modules/@noble/hashes/esm/eskdf.d.ts.map +1 -0
  464. package/node_modules/@noble/hashes/esm/eskdf.js +160 -0
  465. package/node_modules/@noble/hashes/esm/eskdf.js.map +1 -0
  466. package/node_modules/@noble/hashes/esm/hkdf.d.ts +36 -0
  467. package/node_modules/@noble/hashes/esm/hkdf.d.ts.map +1 -0
  468. package/node_modules/@noble/hashes/esm/hkdf.js +82 -0
  469. package/node_modules/@noble/hashes/esm/hkdf.js.map +1 -0
  470. package/node_modules/@noble/hashes/esm/hmac.d.ts +35 -0
  471. package/node_modules/@noble/hashes/esm/hmac.d.ts.map +1 -0
  472. package/node_modules/@noble/hashes/esm/hmac.js +86 -0
  473. package/node_modules/@noble/hashes/esm/hmac.js.map +1 -0
  474. package/node_modules/@noble/hashes/esm/index.d.ts +2 -0
  475. package/node_modules/@noble/hashes/esm/index.d.ts.map +1 -0
  476. package/node_modules/@noble/hashes/esm/index.js +33 -0
  477. package/node_modules/@noble/hashes/esm/index.js.map +1 -0
  478. package/node_modules/@noble/hashes/esm/legacy.d.ts +71 -0
  479. package/node_modules/@noble/hashes/esm/legacy.d.ts.map +1 -0
  480. package/node_modules/@noble/hashes/esm/legacy.js +281 -0
  481. package/node_modules/@noble/hashes/esm/legacy.js.map +1 -0
  482. package/node_modules/@noble/hashes/esm/package.json +10 -0
  483. package/node_modules/@noble/hashes/esm/pbkdf2.d.ts +23 -0
  484. package/node_modules/@noble/hashes/esm/pbkdf2.d.ts.map +1 -0
  485. package/node_modules/@noble/hashes/esm/pbkdf2.js +97 -0
  486. package/node_modules/@noble/hashes/esm/pbkdf2.js.map +1 -0
  487. package/node_modules/@noble/hashes/esm/ripemd160.d.ts +13 -0
  488. package/node_modules/@noble/hashes/esm/ripemd160.d.ts.map +1 -0
  489. package/node_modules/@noble/hashes/esm/ripemd160.js +13 -0
  490. package/node_modules/@noble/hashes/esm/ripemd160.js.map +1 -0
  491. package/node_modules/@noble/hashes/esm/scrypt.d.ts +34 -0
  492. package/node_modules/@noble/hashes/esm/scrypt.d.ts.map +1 -0
  493. package/node_modules/@noble/hashes/esm/scrypt.js +228 -0
  494. package/node_modules/@noble/hashes/esm/scrypt.js.map +1 -0
  495. package/node_modules/@noble/hashes/esm/sha1.d.ts +11 -0
  496. package/node_modules/@noble/hashes/esm/sha1.d.ts.map +1 -0
  497. package/node_modules/@noble/hashes/esm/sha1.js +11 -0
  498. package/node_modules/@noble/hashes/esm/sha1.js.map +1 -0
  499. package/node_modules/@noble/hashes/esm/sha2.d.ts +159 -0
  500. package/node_modules/@noble/hashes/esm/sha2.d.ts.map +1 -0
  501. package/node_modules/@noble/hashes/esm/sha2.js +375 -0
  502. package/node_modules/@noble/hashes/esm/sha2.js.map +1 -0
  503. package/node_modules/@noble/hashes/esm/sha256.d.ts +20 -0
  504. package/node_modules/@noble/hashes/esm/sha256.d.ts.map +1 -0
  505. package/node_modules/@noble/hashes/esm/sha256.js +20 -0
  506. package/node_modules/@noble/hashes/esm/sha256.js.map +1 -0
  507. package/node_modules/@noble/hashes/esm/sha3-addons.d.ts +142 -0
  508. package/node_modules/@noble/hashes/esm/sha3-addons.d.ts.map +1 -0
  509. package/node_modules/@noble/hashes/esm/sha3-addons.js +393 -0
  510. package/node_modules/@noble/hashes/esm/sha3-addons.js.map +1 -0
  511. package/node_modules/@noble/hashes/esm/sha3.d.ts +53 -0
  512. package/node_modules/@noble/hashes/esm/sha3.d.ts.map +1 -0
  513. package/node_modules/@noble/hashes/esm/sha3.js +234 -0
  514. package/node_modules/@noble/hashes/esm/sha3.js.map +1 -0
  515. package/node_modules/@noble/hashes/esm/sha512.d.ts +26 -0
  516. package/node_modules/@noble/hashes/esm/sha512.d.ts.map +1 -0
  517. package/node_modules/@noble/hashes/esm/sha512.js +26 -0
  518. package/node_modules/@noble/hashes/esm/sha512.js.map +1 -0
  519. package/node_modules/@noble/hashes/esm/utils.d.ts +161 -0
  520. package/node_modules/@noble/hashes/esm/utils.d.ts.map +1 -0
  521. package/node_modules/@noble/hashes/esm/utils.js +281 -0
  522. package/node_modules/@noble/hashes/esm/utils.js.map +1 -0
  523. package/node_modules/@noble/hashes/hkdf.d.ts +36 -0
  524. package/node_modules/@noble/hashes/hkdf.d.ts.map +1 -0
  525. package/node_modules/@noble/hashes/hkdf.js +88 -0
  526. package/node_modules/@noble/hashes/hkdf.js.map +1 -0
  527. package/node_modules/@noble/hashes/hmac.d.ts +35 -0
  528. package/node_modules/@noble/hashes/hmac.d.ts.map +1 -0
  529. package/node_modules/@noble/hashes/hmac.js +91 -0
  530. package/node_modules/@noble/hashes/hmac.js.map +1 -0
  531. package/node_modules/@noble/hashes/index.d.ts +1 -0
  532. package/node_modules/@noble/hashes/index.d.ts.map +1 -0
  533. package/node_modules/@noble/hashes/index.js +33 -0
  534. package/node_modules/@noble/hashes/index.js.map +1 -0
  535. package/node_modules/@noble/hashes/legacy.d.ts +71 -0
  536. package/node_modules/@noble/hashes/legacy.d.ts.map +1 -0
  537. package/node_modules/@noble/hashes/legacy.js +287 -0
  538. package/node_modules/@noble/hashes/legacy.js.map +1 -0
  539. package/node_modules/@noble/hashes/package.json +266 -0
  540. package/node_modules/@noble/hashes/pbkdf2.d.ts +23 -0
  541. package/node_modules/@noble/hashes/pbkdf2.d.ts.map +1 -0
  542. package/node_modules/@noble/hashes/pbkdf2.js +101 -0
  543. package/node_modules/@noble/hashes/pbkdf2.js.map +1 -0
  544. package/node_modules/@noble/hashes/ripemd160.d.ts +13 -0
  545. package/node_modules/@noble/hashes/ripemd160.d.ts.map +1 -0
  546. package/node_modules/@noble/hashes/ripemd160.js +16 -0
  547. package/node_modules/@noble/hashes/ripemd160.js.map +1 -0
  548. package/node_modules/@noble/hashes/scrypt.d.ts +34 -0
  549. package/node_modules/@noble/hashes/scrypt.d.ts.map +1 -0
  550. package/node_modules/@noble/hashes/scrypt.js +232 -0
  551. package/node_modules/@noble/hashes/scrypt.js.map +1 -0
  552. package/node_modules/@noble/hashes/sha1.d.ts +11 -0
  553. package/node_modules/@noble/hashes/sha1.d.ts.map +1 -0
  554. package/node_modules/@noble/hashes/sha1.js +14 -0
  555. package/node_modules/@noble/hashes/sha1.js.map +1 -0
  556. package/node_modules/@noble/hashes/sha2.d.ts +159 -0
  557. package/node_modules/@noble/hashes/sha2.d.ts.map +1 -0
  558. package/node_modules/@noble/hashes/sha2.js +384 -0
  559. package/node_modules/@noble/hashes/sha2.js.map +1 -0
  560. package/node_modules/@noble/hashes/sha256.d.ts +20 -0
  561. package/node_modules/@noble/hashes/sha256.d.ts.map +1 -0
  562. package/node_modules/@noble/hashes/sha256.js +23 -0
  563. package/node_modules/@noble/hashes/sha256.js.map +1 -0
  564. package/node_modules/@noble/hashes/sha3-addons.d.ts +142 -0
  565. package/node_modules/@noble/hashes/sha3-addons.d.ts.map +1 -0
  566. package/node_modules/@noble/hashes/sha3-addons.js +402 -0
  567. package/node_modules/@noble/hashes/sha3-addons.js.map +1 -0
  568. package/node_modules/@noble/hashes/sha3.d.ts +53 -0
  569. package/node_modules/@noble/hashes/sha3.d.ts.map +1 -0
  570. package/node_modules/@noble/hashes/sha3.js +239 -0
  571. package/node_modules/@noble/hashes/sha3.js.map +1 -0
  572. package/node_modules/@noble/hashes/sha512.d.ts +26 -0
  573. package/node_modules/@noble/hashes/sha512.d.ts.map +1 -0
  574. package/node_modules/@noble/hashes/sha512.js +29 -0
  575. package/node_modules/@noble/hashes/sha512.js.map +1 -0
  576. package/node_modules/@noble/hashes/src/_assert.ts +22 -0
  577. package/node_modules/@noble/hashes/src/_blake.ts +50 -0
  578. package/node_modules/@noble/hashes/src/_md.ts +176 -0
  579. package/node_modules/@noble/hashes/src/_u64.ts +91 -0
  580. package/node_modules/@noble/hashes/src/argon2.ts +497 -0
  581. package/node_modules/@noble/hashes/src/blake1.ts +534 -0
  582. package/node_modules/@noble/hashes/src/blake2.ts +486 -0
  583. package/node_modules/@noble/hashes/src/blake2b.ts +10 -0
  584. package/node_modules/@noble/hashes/src/blake2s.ts +20 -0
  585. package/node_modules/@noble/hashes/src/blake3.ts +272 -0
  586. package/node_modules/@noble/hashes/src/crypto.ts +9 -0
  587. package/node_modules/@noble/hashes/src/cryptoNode.ts +15 -0
  588. package/node_modules/@noble/hashes/src/eskdf.ts +187 -0
  589. package/node_modules/@noble/hashes/src/hkdf.ts +88 -0
  590. package/node_modules/@noble/hashes/src/hmac.ts +94 -0
  591. package/node_modules/@noble/hashes/src/index.ts +31 -0
  592. package/node_modules/@noble/hashes/src/legacy.ts +293 -0
  593. package/node_modules/@noble/hashes/src/pbkdf2.ts +122 -0
  594. package/node_modules/@noble/hashes/src/ripemd160.ts +12 -0
  595. package/node_modules/@noble/hashes/src/scrypt.ts +257 -0
  596. package/node_modules/@noble/hashes/src/sha1.ts +10 -0
  597. package/node_modules/@noble/hashes/src/sha2.ts +402 -0
  598. package/node_modules/@noble/hashes/src/sha256.ts +24 -0
  599. package/node_modules/@noble/hashes/src/sha3-addons.ts +499 -0
  600. package/node_modules/@noble/hashes/src/sha3.ts +258 -0
  601. package/node_modules/@noble/hashes/src/sha512.ts +34 -0
  602. package/node_modules/@noble/hashes/src/utils.ts +395 -0
  603. package/node_modules/@noble/hashes/utils.d.ts +161 -0
  604. package/node_modules/@noble/hashes/utils.d.ts.map +1 -0
  605. package/node_modules/@noble/hashes/utils.js +313 -0
  606. package/node_modules/@noble/hashes/utils.js.map +1 -0
  607. package/package.json +203 -0
  608. package/schemas/catalog.schema.json +360 -0
  609. package/src/align.ts +239 -0
  610. package/src/audio-init-parser.ts +189 -0
  611. package/src/audio-ring-buffer.ts +120 -0
  612. package/src/authored-timing.ts +63 -0
  613. package/src/carp-catalog.ts +845 -0
  614. package/src/catalog-fec-params.ts +535 -0
  615. package/src/catalog-network-source.ts +49 -0
  616. package/src/catalog-validator.ts +34 -0
  617. package/src/cmaf-block-assembler.ts +1167 -0
  618. package/src/cmaf-group-batcher.ts +104 -0
  619. package/src/cmcd-producer.ts +334 -0
  620. package/src/codec-utils.ts +1611 -0
  621. package/src/fec-codec.ts +61 -0
  622. package/src/fec-manager.ts +2081 -0
  623. package/src/fec-starvation-detector.ts +128 -0
  624. package/src/fec-worker-setup.ts +213 -0
  625. package/src/fmp4-completion.ts +47 -0
  626. package/src/fmp4-reader.ts +112 -0
  627. package/src/index.ts +486 -0
  628. package/src/iso-bmff-timing.ts +813 -0
  629. package/src/loc.ts +181 -0
  630. package/src/manifest-auth-plan.ts +107 -0
  631. package/src/media-router.ts +795 -0
  632. package/src/mfu-reassembler.ts +3741 -0
  633. package/src/mfu-reassembly-policy.ts +134 -0
  634. package/src/mmtp-container-parser.ts +1836 -0
  635. package/src/mmtp-decode-pipeline.ts +389 -0
  636. package/src/mmtp-router.ts +3136 -0
  637. package/src/mmtp-to-cmaf.ts +1024 -0
  638. package/src/multicast-auth-catalog.ts +340 -0
  639. package/src/multicast-input.ts +439 -0
  640. package/src/multicast-manager.ts +1720 -0
  641. package/src/multicast-transport.ts +321 -0
  642. package/src/multicast-unwrap.ts +132 -0
  643. package/src/no-modules.ts +20 -0
  644. package/src/phase14-catalog-override.ts +336 -0
  645. package/src/repair-fetch.ts +238 -0
  646. package/src/repair-layers.ts +490 -0
  647. package/src/sample-combiner.ts +938 -0
  648. package/src/signaling.ts +78 -0
  649. package/src/source-auth.ts +1 -0
  650. package/src/switching-set-alignment.ts +109 -0
  651. package/src/timestamp-codec.ts +265 -0
  652. package/src/track-pipeline-setup.ts +189 -0
  653. package/src/track-pipeline.ts +332 -0
  654. package/src/track-session.ts +994 -0
  655. package/src/transport-pool.ts +128 -0
  656. package/src/wasm-mfu-reassembler.ts +389 -0
  657. package/src/wasm-mmtp-container-parser.ts +23 -0
  658. package/test-vectors/align.json +199 -0
  659. package/test-vectors/catalog/README.md +40 -0
  660. package/test-vectors/catalog/negative/fec-enhancement-repair-removed.json +27 -0
  661. package/test-vectors/catalog/negative/fec-repair-layer-geometry.json +32 -0
  662. package/test-vectors/catalog/negative/fec-repair-layer-missing-symbols.json +30 -0
  663. package/test-vectors/catalog/negative/fec-repair-multiple-depends.json +31 -0
  664. package/test-vectors/catalog/negative/fec-repair-overlay-with-layer.json +33 -0
  665. package/test-vectors/catalog/negative/fec-repair-priority-out-of-band.json +23 -0
  666. package/test-vectors/catalog/negative/fec-repair-source-symbols-without-scope.json +32 -0
  667. package/test-vectors/catalog/negative/legacy-selection-params.json +15 -0
  668. package/test-vectors/catalog/negative/media-track-repair-layer.json +16 -0
  669. package/test-vectors/catalog/negative/missing-mmtp-fields.json +12 -0
  670. package/test-vectors/catalog/negative/multicast-auth-padded-key.json +37 -0
  671. package/test-vectors/catalog/negative/multicast-auth-reused-track-format.json +53 -0
  672. package/test-vectors/catalog/negative/multicast-auth-trailing-pad-bits.json +37 -0
  673. package/test-vectors/catalog/negative/multicast-endpoint-missing-protocol-and-source.json +24 -0
  674. package/test-vectors/catalog/negative/network-source-object.json +25 -0
  675. package/test-vectors/catalog/negative/raptorq-source-symbols-over-max.json +23 -0
  676. package/test-vectors/catalog/negative/raptorq-unaligned-symbol.json +26 -0
  677. package/test-vectors/catalog/negative/repair-track-legacy-shape.json +19 -0
  678. package/test-vectors/catalog/negative/track-role-empty.json +16 -0
  679. package/test-vectors/catalog/negative/track-role-non-string.json +16 -0
  680. package/test-vectors/catalog/positive/fec-layered-repair.json +104 -0
  681. package/test-vectors/catalog/positive/fec-multicast.json +51 -0
  682. package/test-vectors/catalog/positive/flat-av.json +30 -0
  683. package/test-vectors/catalog/positive/multicast-auth-rotation.json +55 -0
  684. package/test-vectors/catalog/positive/ticks-and-mixed-timescales.json +39 -0
  685. package/test-vectors/catalog/positive/track-role.json +49 -0
  686. package/test-vectors/catalog/releases/moqcast-native-v16-v1/catalog.json +65 -0
  687. package/test-vectors/catalog/releases/moqcast-native-v16-v1.archive.sha256 +1 -0
  688. package/test-vectors/catalog-multicast-auth-v1.json +26 -0
  689. package/test-vectors/catalog-validation.json +148 -0
  690. package/test-vectors/catalog-validator-public-api.mjs +25 -0
  691. package/test-vectors/generate-source-auth-fixture.mjs +114 -0
  692. package/test-vectors/source-auth-v1.json +61 -0
  693. package/test-vectors/source-contiguity-v1.json +131 -0
  694. package/test-vectors/subsample-placement-v1.json +230 -0
@@ -0,0 +1,1836 @@
1
+ import {
2
+ parseMMTPHeader,
3
+ extractMFUPayload,
4
+ extractFecPayloadId,
5
+ hasSourceFecPayloadId,
6
+ extractAltaTrailer,
7
+ PACKET_TYPE_MPU,
8
+ PACKET_TYPE_REPAIR,
9
+ } from '@blockcast/mmt-base'
10
+ import {
11
+ MFUReassembler,
12
+ MfuReassemblyError,
13
+ isMfuReassemblyError,
14
+ type MfuReassemblerBackend,
15
+ type MmtpSourcePath,
16
+ type MmtpDeliveryProvenance,
17
+ type ReassembledMFU,
18
+ } from './mfu-reassembler.js'
19
+ import {
20
+ SampleCombiner,
21
+ SampleCombineError,
22
+ type AbandonedSample,
23
+ type CombinedSample,
24
+ type SampleCombinerOptions,
25
+ type SampleCombinerStats,
26
+ } from './sample-combiner.js'
27
+ import {
28
+ classifyH264Keyframe,
29
+ mmtpTimestampToMicros,
30
+ normalizeMmtpInitSegment,
31
+ } from './codec-utils.js'
32
+ import {
33
+ mmtpFrameReorderWindowFrames,
34
+ mfuReassemblyTimeoutMs,
35
+ type MfuReassemblyPolicy,
36
+ } from './mfu-reassembly-policy.js'
37
+ import {
38
+ IsoBmffTimingResolver,
39
+ mediaTicksToMicroseconds,
40
+ type IsoBmffSampleTiming,
41
+ } from './iso-bmff-timing.js'
42
+ import type { AuthoredTiming } from './authored-timing.js'
43
+
44
+ // ── Types ───────────────────────────────────────────────────────────
45
+
46
+ export interface TrackInfo {
47
+ type: 'video' | 'audio'
48
+ codec: string
49
+ /** Catalog/in-band frame cadence available to downstream media stages. */
50
+ framerate?: number
51
+ /** Catalog MMTP group duration available to downstream media stages. */
52
+ groupDurationMs?: number
53
+ }
54
+
55
+ export interface MmtpParserConfig {
56
+ tracks?: Map<number, TrackInfo>
57
+ reorderWindow?: number
58
+ /** MFU reassembly timeout (ms). Derive from FEC interleaveDepthMs or frame duration.
59
+ * Pass null for live clock-free operation; memory remains bounded by maxBuffered.
60
+ * Defaults to 1000ms if not provided. */
61
+ mfuTimeoutMs?: number | null
62
+ /** Explicit transport policy. Reliable MoQ is clock-free; datagrams require
63
+ * a catalog/in-band-derived deadline. Mutually exclusive with mfuTimeoutMs. */
64
+ reassemblyPolicy?: MfuReassemblyPolicy
65
+ /** Max concurrent active MFUs. A new first fragment is rejected at the bound. */
66
+ maxBufferedMfus?: number
67
+ /**
68
+ * Max subsample data units held for one partitioned sample (moqcast #37).
69
+ * A sample needs `ceil(sampleSize / (256 * maxPayload))` data units; at the
70
+ * ~1300-byte payloads this producer emits, one data unit spans ~327 KB, so
71
+ * the default admits a ~5 MB sample — beyond any 8K intra frame — while
72
+ * still bounding the per-insert overlap scan. Raise it only alongside a
73
+ * larger authored frame size, never to paper over a lost data unit.
74
+ */
75
+ maxSubsampleSegments?: number
76
+ /** Alternate backend. The TypeScript and Rust/WASM implementations enforce
77
+ * the same authored active-DU contract. */
78
+ reassembler?: MfuReassemblerBackend
79
+ /** Default track info for auto-discovered packetIds.
80
+ * When the parser receives an MMTP packet with an unregistered packetId,
81
+ * it creates a new track entry using this info. Required for correct
82
+ * keyframe detection with HEVC/AV1/VP9 (without it, defaults to H.264). */
83
+ defaultTrack?: TrackInfo
84
+ }
85
+
86
+ /** Structured result from parseHeader(). Used by MmtpRouter for two-phase parsing:
87
+ * 1. parseHeader() → get packetId + fecPayloadId for FEC feeding
88
+ * 2. feed() → MFU extraction, reassembly, keyframe detection, emit */
89
+ export interface PacketInfo {
90
+ packetId: number
91
+ packetType: number
92
+ timestamp: bigint
93
+ /** MMTP timestamp converted to monotonic microseconds for pacing/sync users. */
94
+ timestampUs: number
95
+ sequenceNumber: number
96
+ rapFlag: boolean
97
+ fecType: number
98
+ /** FEC source symbol ID (from 4-byte FEC Payload ID at packet end). Null if no FEC. */
99
+ fecPayloadId: { ssId: number } | null
100
+ /** True if this is a repair packet (packetType=0x03). */
101
+ isRepair: boolean
102
+ }
103
+
104
+ export interface FrameMeta {
105
+ /** Authored ISO-BMFF presentation time converted to microseconds. */
106
+ timestamp: number
107
+ /** Authored ISO-BMFF decode time converted to microseconds. */
108
+ decodeTimestamp?: number
109
+ /** Authored ISO-BMFF sample duration converted to microseconds. */
110
+ duration?: number
111
+ /** Exact authored decode time in mediaTimescale units. */
112
+ decodeTime?: bigint
113
+ /** Exact authored presentation time in mediaTimescale units. */
114
+ presentationTime?: bigint
115
+ /** Exact authored duration in mediaTimescale units. */
116
+ durationTicks?: number
117
+ /** mdhd media timescale for the exact timing fields. */
118
+ mediaTimescale?: number
119
+ /** Exact authored ISO-BMFF timing carried without narrowing to microseconds. */
120
+ authoredTiming?: AuthoredTiming
121
+ keyframe: boolean
122
+ packetId: number
123
+ trackType: 'video' | 'audio' | null
124
+ packetSequenceNumber?: number
125
+ /** Every authoritative MMTP source-packet sequence that formed this frame. */
126
+ packetSequenceNumbers?: number[]
127
+ /** Subset of packetSequenceNumbers reconstructed by FEC. */
128
+ repairedPacketSequenceNumbers?: number[]
129
+ /** Immutable catalog identity supplied by the owning ingress subscription. */
130
+ catalogTrackName?: string
131
+ /** Raw MMTP short-NTP timestamp retained for adapters that own the timeline. */
132
+ mmtpTimestamp?: bigint
133
+ mpuSequenceNumber?: number
134
+ movieFragmentSequenceNumber?: number
135
+ sampleNumber?: number
136
+ fragmentCount?: number
137
+ rapFlag?: boolean
138
+ priority?: number
139
+ recovered?: boolean
140
+ /** Starts a new publisher/source epoch for ordering and timestamp state. */
141
+ sourceDiscontinuity?: boolean
142
+ /** Source of the accepted FI=1/FI=0 frame path. Absent for recovery. */
143
+ source?: MmtpSourcePath
144
+ /**
145
+ * Strict whole-frame delivery provenance. Unlike `source`, which names the
146
+ * head fragment's transport for routing, this is folded over every
147
+ * contributing fragment (and, for FEC-recovered frames, over the transports
148
+ * that supplied the block's symbols). Only 'multicast' is evidence of
149
+ * multicast-exclusive delivery. Absent means no producer on this path
150
+ * recorded provenance — consumers MUST treat that as unproven, never as
151
+ * multicast.
152
+ */
153
+ deliveryProvenance?: MmtpDeliveryProvenance
154
+ }
155
+
156
+ /** Typed ISO-BMFF timing-sample budget counters. See `timingBudgetStats`. */
157
+ export interface MmtpTimingBudgetStats {
158
+ /** Slots currently charged to declared-but-undelivered samples. */
159
+ pending: number
160
+ /** Total slots available; `pending === capacity` rejects every new moof. */
161
+ capacity: number
162
+ /** Slots returned by samples the combiner abandoned (evicted or timed out). */
163
+ reclaimed: number
164
+ /**
165
+ * Slots reclaimed by window displacement — samples no component ever saw,
166
+ * so no abandonment event could be raised for them. Separate from
167
+ * `reclaimed` because the owners differ: `reclaimed` is the combiner giving
168
+ * up, `evicted` is loss that was never observable at all.
169
+ */
170
+ evicted: number
171
+ /** Movie fragments refused because the budget had no room for their samples. */
172
+ rejectedMovieFragments: number
173
+ }
174
+
175
+ export interface MmtpReorderStats {
176
+ droppedBeforeStartupKeyframe: number
177
+ droppedIncompleteStartupInterval: number
178
+ droppedStale: number
179
+ forcedWindowFlushes: number
180
+ droppedOnLiveDispose: number
181
+ publisherResets: number
182
+ droppedOnPublisherReset: number
183
+ bufferedFrames: number
184
+ }
185
+
186
+ export interface MmtpReorderFrameDiagnostics {
187
+ timestampUs: number
188
+ keyframe: boolean
189
+ sequenceNumber: number
190
+ packetSequenceNumber?: number
191
+ movieFragmentSequenceNumber?: number
192
+ sampleNumber?: number
193
+ fragmentCount?: number
194
+ }
195
+
196
+ export interface MmtpReorderStreamDiagnostics {
197
+ packetId: number
198
+ bufferedFrames: number
199
+ lastEmitted: MmtpReorderFrameDiagnostics | null
200
+ startupKeyframe: MmtpReorderFrameDiagnostics | null
201
+ head: MmtpReorderFrameDiagnostics[]
202
+ tail: MmtpReorderFrameDiagnostics[]
203
+ keyframes: MmtpReorderFrameDiagnostics[]
204
+ }
205
+
206
+ export type MmtpParserRecord =
207
+ | {
208
+ kind: 'repair' | 'non-media'
209
+ packet: PacketInfo
210
+ byteLength: number
211
+ }
212
+ | {
213
+ kind: 'init' | 'fragment'
214
+ packet: PacketInfo
215
+ data: Uint8Array
216
+ mpuSequenceNumber: number
217
+ fragmentationIndicator: number
218
+ fragmentCounter: number
219
+ }
220
+ | {
221
+ kind: 'pending'
222
+ packet: PacketInfo
223
+ mpuSequenceNumber: number
224
+ fragmentationIndicator: number
225
+ fragmentCounter: number
226
+ }
227
+ | {
228
+ kind: 'rejected'
229
+ packet: PacketInfo
230
+ mpuSequenceNumber: number
231
+ fragmentationIndicator: number
232
+ fragmentCounter: number
233
+ error: {
234
+ /**
235
+ * Either a single data unit's own fragment chain was rejected, or
236
+ * combining reassembled data units into a sample was (moqcast #37).
237
+ * Both are fail-closed drops of the same packet, so they share this
238
+ * record; the code says which class it came from.
239
+ */
240
+ code: MfuReassemblyError['code'] | SampleCombineError['code']
241
+ message: string
242
+ }
243
+ }
244
+ | {
245
+ kind: 'frame'
246
+ packet: PacketInfo
247
+ data: Uint8Array
248
+ meta: FrameMeta
249
+ mpuSequenceNumber: number
250
+ fragmentCount: number
251
+ rapFlag: boolean
252
+ priority?: number
253
+ }
254
+
255
+ /**
256
+ * Why feed() returned no record. Every `return null` inside feed() names one
257
+ * of these, so a packet that produced nothing is attributable to the stage
258
+ * that rejected it rather than to one collapsed failure bucket.
259
+ *
260
+ * The distinction is what makes a wedged receive path readable: a media track
261
+ * that stops producing records looks identical from outside whether the header
262
+ * failed to parse, MFU extraction failed, or the timing resolver refused the
263
+ * sample — and those name three different components.
264
+ */
265
+ export type MmtpFeedDropReason =
266
+ /** feed() after dispose(); the host is still pushing at a torn-down parser. */
267
+ | 'disposed'
268
+ | 'header-parse-failed'
269
+ | 'mfu-extract-failed'
270
+ /** MPU present but not a timed MFU, so no media payload to reassemble. */
271
+ | 'untimed-mpu'
272
+ | 'init-unexpected-fragmentation'
273
+ | 'init-normalize-failed'
274
+ /** No timing resolver, or it refused the initialization segment. */
275
+ | 'init-timing-refused'
276
+ | 'fragment-unexpected-fragmentation'
277
+ | 'fragment-no-resolver'
278
+ /** addMovieFragment refused — includes the pending-timing-sample budget. */
279
+ | 'fragment-timing-refused'
280
+ | 'unknown-fragment-type'
281
+ | 'mfu-no-resolver'
282
+ | 'mfu-empty-payload'
283
+ | 'mfu-du-header-missing'
284
+ | 'mfu-sample-timing-missing'
285
+ | 'mfu-offset-out-of-range'
286
+ | 'mfu-size-mismatch'
287
+ | 'assembled-identity-missing'
288
+ | 'assembled-timing-missing'
289
+ | 'assembled-mpu-mismatch'
290
+ /**
291
+ * Refusals inside the combined-sample delivery step, which runs after the
292
+ * combiner has already produced a whole sample. These are distinct from
293
+ * their `assembled-*` counterparts: those reject a data unit on the way in,
294
+ * these reject a sample that was fully reassembled and then found
295
+ * undeliverable, so collapsing the two would hide a combiner that completes
296
+ * samples nothing can present.
297
+ */
298
+ | 'combined-timing-missing'
299
+ | 'combined-size-mismatch'
300
+ /** Frame metadata could not be built — an fMP4 box arrived as a timed MFU
301
+ * payload, or the authored timing converts to a non-positive duration. */
302
+ | 'combined-meta-build-failed'
303
+ | 'combined-timing-consume-refused'
304
+ /** feed() threw. Counted so the tally still sums to fedObjects; the error
305
+ * is rethrown, never swallowed. */
306
+ | 'threw'
307
+
308
+ /**
309
+ * Every key `outcomes` can carry: one per record kind, one per drop reason.
310
+ *
311
+ * Derived from the two unions rather than written out, so a new record kind or
312
+ * drop reason cannot be counted under a key the type does not admit, and a
313
+ * consumer can enumerate the vocabulary from the type instead of from prose.
314
+ */
315
+ export type MmtpFeedOutcomeKey =
316
+ | MmtpParserRecord['kind']
317
+ | `drop:${MmtpFeedDropReason}`
318
+
319
+ /**
320
+ * Feed tallies owned by the parser.
321
+ *
322
+ * These live here rather than on MmtpDecodePipeline because the two players
323
+ * sit at different layers of this library: hang-mmt-fec drives the pipeline,
324
+ * while moqtail constructs MmtpContainerParser directly and never builds the
325
+ * wrapper. feed() is the one chokepoint both cross, so a tally maintained here
326
+ * instruments both by construction.
327
+ *
328
+ * Logs are diagnostic output, not an acceptance API — this is the typed
329
+ * counter a harness should read instead of scraping console histograms.
330
+ */
331
+ export interface MmtpParserFeedStats {
332
+ /** Every packet handed to feed(), including post-dispose calls. */
333
+ fedObjects: number
334
+ /**
335
+ * Mutually exclusive outcome tally: record kinds under their own `kind`,
336
+ * drops under `drop:<reason>`. The values sum to exactly `fedObjects`, which
337
+ * is the property that makes a frozen media bucket beside a still-climbing
338
+ * non-media bucket a readable signal rather than two unrelated numbers.
339
+ *
340
+ * Partial because a key appears only once it has been counted; absent and
341
+ * zero mean the same thing to a reader.
342
+ */
343
+ outcomes: Partial<Record<MmtpFeedOutcomeKey, number>>
344
+ /**
345
+ * Refusals from a completed older sample delivered while registering a
346
+ * later packet's authored size. Kept separate because that side effect is
347
+ * not the packet's own feed outcome.
348
+ */
349
+ deferredOutcomes: Partial<Record<MmtpFeedDropReason, number>>
350
+ }
351
+
352
+ type FrameCallback = (data: Uint8Array, meta: FrameMeta) => void
353
+ type InitCallback = (data: Uint8Array, packetId: number, catalogTrackName?: string) => void
354
+
355
+ /**
356
+ * Result of one combined-sample delivery attempt: either the record to return
357
+ * or the reason the sample could not be delivered.
358
+ *
359
+ * Internal to the parser. A bare `null` would be indistinguishable from the
360
+ * other refusals `feed()` can take, and the whole point of the drop-reason
361
+ * vocabulary is that a stage which stops producing frames says which stage it
362
+ * was.
363
+ */
364
+ type CombinedSampleOutcome =
365
+ | { readonly record: MmtpParserRecord; readonly reason?: undefined }
366
+ | { readonly record?: undefined; readonly reason: MmtpFeedDropReason }
367
+
368
+ export function isMfuKeyframeMetadata(rapFlag: boolean, priority?: number): boolean {
369
+ return rapFlag || (priority ?? 0) > 0x80
370
+ }
371
+
372
+ /** Per-packet detached ALTA authenticator trailer (08-07 Path 2).
373
+ * - `auth`: the authenticator bytes (length-prefixed at packet end).
374
+ * - `payloadEnd`: byte offset within the source packet where signed
375
+ * payload ends — consumers reconstruct `payload = packet[0..payloadEnd]`
376
+ * for verify_detached. */
377
+ export interface ParsedAltaTrailer {
378
+ auth: Uint8Array
379
+ payloadEnd: number
380
+ }
381
+
382
+ /** Source-symbol exposure for FEC consumers.
383
+ *
384
+ * Fires from {@link MmtpContainerParser.feed} once per FEC-protected source
385
+ * packet — after parseHeader confirms fec_payload_id is present, before MFU
386
+ * extraction.
387
+ *
388
+ * Arguments:
389
+ * - `packetId` – MMTP header `packet_id` (track identifier)
390
+ * - `ssId` – flat Source Symbol ID from the 4-byte fec_payload_id trailer
391
+ * - `rawSymbol` – packet bytes minus the 4-byte fec_payload_id trailer.
392
+ * NOT zero-padded to FEC symbol size — consumer pads if needed
393
+ * (FEC symbol size is FEC-config, not parser-config).
394
+ * - `alta` – detached ALTA authenticator if present, else undefined
395
+ *
396
+ * Designed so consumers (player, Shaka shim, upstream moqtail) can wire
397
+ * source-symbol delivery to whatever FEC pipeline they own, without depending
398
+ * on libmmt's MulticastManager/MmtpRouter/FECManager stack. */
399
+ type SourceSymbolCallback = (
400
+ packetId: number,
401
+ ssId: number,
402
+ rawSymbol: Uint8Array,
403
+ alta: ParsedAltaTrailer | undefined,
404
+ ) => void
405
+
406
+ export interface ParseHeaderOptions {
407
+ /** Recovered source symbols already had the trailing Source FEC Payload ID stripped. */
408
+ skipFecPayloadId?: boolean
409
+ /** Mark frames emitted from this packet as FEC-recovered. */
410
+ recovered?: boolean
411
+ /** Exact catalog track bound to this packet by its ingress subscription. */
412
+ catalogTrackName?: string
413
+ /** Source transport for parser-owned frame attribution. */
414
+ source?: MmtpSourcePath
415
+ /**
416
+ * Explicit strict provenance for the fragments this packet produces.
417
+ * The router sets it for FEC-recovered packets, which carry no `source`
418
+ * but whose FEC block's symbol provenance is known.
419
+ */
420
+ deliveryProvenance?: MmtpDeliveryProvenance
421
+ }
422
+
423
+ // ── FrameSequencer ──────────────────────────────────────────────────
424
+
425
+ interface PendingFrame {
426
+ data: Uint8Array
427
+ meta: FrameMeta
428
+ seq: number
429
+ }
430
+
431
+ interface SequencerStream {
432
+ buf: PendingFrame[]
433
+ lastEmitted: PendingFrame | null
434
+ }
435
+
436
+ function reorderFrameDiagnostics(frame: PendingFrame): MmtpReorderFrameDiagnostics {
437
+ return {
438
+ timestampUs: frame.meta.timestamp,
439
+ keyframe: frame.meta.keyframe,
440
+ sequenceNumber: frame.seq,
441
+ ...(frame.meta.packetSequenceNumber === undefined
442
+ ? {}
443
+ : { packetSequenceNumber: frame.meta.packetSequenceNumber }),
444
+ ...(frame.meta.movieFragmentSequenceNumber === undefined
445
+ ? {}
446
+ : { movieFragmentSequenceNumber: frame.meta.movieFragmentSequenceNumber }),
447
+ ...(frame.meta.sampleNumber === undefined ? {} : { sampleNumber: frame.meta.sampleNumber }),
448
+ ...(frame.meta.fragmentCount === undefined
449
+ ? {}
450
+ : { fragmentCount: frame.meta.fragmentCount }),
451
+ }
452
+ }
453
+
454
+ type SequencerStat = Exclude<keyof MmtpReorderStats, 'bufferedFrames'>
455
+
456
+ function serialSequenceDelta(a: number, b: number): number {
457
+ return (a - b) | 0
458
+ }
459
+
460
+ function compareFrameOrder(
461
+ a: PendingFrame,
462
+ b: PendingFrame,
463
+ ): number {
464
+ if (a.meta.decodeTime !== undefined && b.meta.decodeTime !== undefined &&
465
+ a.meta.mediaTimescale === b.meta.mediaTimescale) {
466
+ if (a.meta.decodeTime < b.meta.decodeTime) return -1
467
+ if (a.meta.decodeTime > b.meta.decodeTime) return 1
468
+ } else {
469
+ const decodeDelta = (a.meta.decodeTimestamp ?? a.meta.timestamp) -
470
+ (b.meta.decodeTimestamp ?? b.meta.timestamp)
471
+ if (decodeDelta !== 0) return decodeDelta
472
+ }
473
+ const movieDelta = serialSequenceDelta(
474
+ a.meta.movieFragmentSequenceNumber ?? 0,
475
+ b.meta.movieFragmentSequenceNumber ?? 0,
476
+ )
477
+ if (movieDelta !== 0) return movieDelta
478
+ const sampleDelta = (a.meta.sampleNumber ?? 0) - (b.meta.sampleNumber ?? 0)
479
+ return sampleDelta !== 0 ? sampleDelta : serialSequenceDelta(a.seq, b.seq)
480
+ }
481
+
482
+ class FrameSequencer {
483
+ #streams = new Map<number, SequencerStream>()
484
+ #windowSize: number
485
+ #emit: (data: Uint8Array, meta: FrameMeta) => void
486
+ #recordStat: (stat: SequencerStat, count?: number) => void
487
+
488
+ constructor(
489
+ windowSize: number,
490
+ emit: (data: Uint8Array, meta: FrameMeta) => void,
491
+ recordStat: (stat: SequencerStat, count?: number) => void = () => {},
492
+ ) {
493
+ this.#windowSize = windowSize
494
+ this.#emit = emit
495
+ this.#recordStat = recordStat
496
+ }
497
+
498
+ push(data: Uint8Array, meta: FrameMeta, seq: number): void {
499
+ if (this.#windowSize <= 0) {
500
+ this.#emit(data, meta)
501
+ return
502
+ }
503
+ const frame: PendingFrame = { data, meta: { ...meta }, seq }
504
+ const stream = this.#streamFor(meta.packetId)
505
+ if (meta.sourceDiscontinuity) {
506
+ this.#recordStat('publisherResets')
507
+ this.#recordStat('droppedOnPublisherReset', stream.buf.length)
508
+ stream.buf = []
509
+ stream.lastEmitted = null
510
+ frame.meta.sourceDiscontinuity = true
511
+ }
512
+ if (this.#isStale(stream, frame)) {
513
+ this.#recordStat('droppedStale')
514
+ return
515
+ }
516
+ if (meta.keyframe && stream.lastEmitted === null) {
517
+ // The first RAP establishes decoder sync. Discard any pre-RAP media and
518
+ // emit it immediately so startup does not pay the reorder-window delay.
519
+ this.#recordStat('droppedBeforeStartupKeyframe', stream.buf.length)
520
+ stream.buf = []
521
+ this.#emitFrame(stream, frame)
522
+ return
523
+ }
524
+ let lo = 0, hi = stream.buf.length
525
+ while (lo < hi) {
526
+ const mid = (lo + hi) >>> 1
527
+ if (compareFrameOrder(stream.buf[mid]!, frame) < 0) lo = mid + 1
528
+ else hi = mid
529
+ }
530
+ stream.buf.splice(lo, 0, frame)
531
+ while (stream.buf.length > this.#windowSize) this.#flushHead(stream)
532
+ }
533
+
534
+ flush(): void {
535
+ for (const packetId of [...this.#streams.keys()].sort((a, b) => a - b)) {
536
+ const stream = this.#streams.get(packetId)
537
+ while (stream && stream.buf.length > 0) this.#flushHead(stream)
538
+ }
539
+ }
540
+
541
+ get buffered(): number {
542
+ let total = 0
543
+ for (const stream of this.#streams.values()) total += stream.buf.length
544
+ return total
545
+ }
546
+
547
+ get diagnostics(): Omit<MmtpReorderStreamDiagnostics, 'packetId'>[] {
548
+ return [...this.#streams.values()].map(stream => ({
549
+ bufferedFrames: stream.buf.length,
550
+ lastEmitted: stream.lastEmitted ? reorderFrameDiagnostics(stream.lastEmitted) : null,
551
+ startupKeyframe: null,
552
+ head: stream.buf.slice(0, 4).map(reorderFrameDiagnostics),
553
+ tail: stream.buf.slice(-4).map(reorderFrameDiagnostics),
554
+ keyframes: stream.buf.filter(frame => frame.meta.keyframe).slice(0, 8)
555
+ .map(reorderFrameDiagnostics),
556
+ }))
557
+ }
558
+
559
+ discard(): void {
560
+ for (const stream of this.#streams.values()) {
561
+ this.#recordStat('droppedOnLiveDispose', stream.buf.length)
562
+ stream.buf = []
563
+ }
564
+ this.#streams.clear()
565
+ }
566
+
567
+ #streamFor(packetId: number): SequencerStream {
568
+ let stream = this.#streams.get(packetId)
569
+ if (!stream) {
570
+ stream = { buf: [], lastEmitted: null }
571
+ this.#streams.set(packetId, stream)
572
+ }
573
+ return stream
574
+ }
575
+
576
+ #flushHead(stream: SequencerStream): boolean {
577
+ const f = stream.buf.shift()
578
+ if (!f) return false
579
+ return this.#emitFrame(stream, f)
580
+ }
581
+
582
+ #isStale(stream: SequencerStream, frame: PendingFrame): boolean {
583
+ return stream.lastEmitted !== null &&
584
+ compareFrameOrder(frame, stream.lastEmitted) <= 0
585
+ }
586
+
587
+ #emitFrame(stream: SequencerStream, frame: PendingFrame): boolean {
588
+ if (this.#isStale(stream, frame)) {
589
+ this.#recordStat('droppedStale')
590
+ return false
591
+ }
592
+ stream.lastEmitted = frame
593
+ this.#emit(frame.data, frame.meta)
594
+ return true
595
+ }
596
+ }
597
+
598
+ // ── fMP4 box detection ──────────────────────────────────────────────
599
+
600
+ const FMPU_BOX_TYPES = new Set(['ftyp', 'moov', 'moof'])
601
+
602
+ function isFmp4Box(data: Uint8Array): boolean {
603
+ if (data.length < 8) return false
604
+ const type = String.fromCharCode(data[4]!, data[5]!, data[6]!, data[7]!)
605
+ return FMPU_BOX_TYPES.has(type)
606
+ }
607
+
608
+ interface TimingStreamState {
609
+ packetId: number
610
+ resolver: IsoBmffTimingResolver
611
+ }
612
+
613
+ // ── MmtpContainerParser ─────────────────────────────────────────────
614
+
615
+ export class MmtpContainerParser {
616
+ #tracks = new Map<number, TrackInfo>()
617
+ #defaultTrack: TrackInfo | null = null
618
+ #reassembler: MfuReassemblerBackend
619
+ /**
620
+ * Combines subsample data units back into whole samples (moqcast #37), one
621
+ * combiner per parser stream. Per-stream is required, not an optimization:
622
+ * {@link SampleCombiner} identifies a sample by
623
+ * `(catalogTrackName, mpu, movieFragment, sampleNumber)`, so two streams
624
+ * that carry no catalog identity would collide on every sample number and
625
+ * each would see the other's data units as duplicates of its own. The
626
+ * parser already isolates timing and reordering by this same stream key.
627
+ *
628
+ * Inert until a producer partitions — which it does the moment a frame
629
+ * exceeds one data unit's 256-payload capacity (~327 KB here, routinely
630
+ * exceeded by 4K/8K intra frames).
631
+ */
632
+ #sampleCombiners = new Map<string, SampleCombiner>()
633
+ #sampleCombinerOptions: SampleCombinerOptions
634
+ /** Counters folded in from combiners dropped by a track reset, so the
635
+ * totals below stay monotonic across a publisher change. */
636
+ #retiredSampleCombinerStats: SampleCombinerStats = {
637
+ combinedSamples: 0,
638
+ passthroughSamples: 0,
639
+ timedOutSamples: 0,
640
+ evictedSamples: 0,
641
+ }
642
+ #timingStreams = new Map<string, TimingStreamState>()
643
+ #maxPendingTimingSamples: number
644
+ #pendingTimingSamples = 0
645
+ #reclaimedTimingSamples = 0
646
+ #evictedTimingSamples = 0
647
+ #rejectedMovieFragments = 0
648
+ #sequencers = new Map<string, { packetId: number; sequencer: FrameSequencer }>()
649
+ #reorderWindow = 0
650
+ #reassemblyPolicy: MfuReassemblyPolicy | null = null
651
+ #frameCb: FrameCallback | null = null
652
+ #initCb: InitCallback | null = null
653
+ #sourceSymbolCb: SourceSymbolCallback | null = null
654
+ #trackFrameCbs = new Map<number, FrameCallback>()
655
+ #reorderStats: Omit<MmtpReorderStats, 'bufferedFrames'> = {
656
+ droppedBeforeStartupKeyframe: 0,
657
+ droppedIncompleteStartupInterval: 0,
658
+ droppedStale: 0,
659
+ forcedWindowFlushes: 0,
660
+ droppedOnLiveDispose: 0,
661
+ publisherResets: 0,
662
+ droppedOnPublisherReset: 0,
663
+ }
664
+ /** Streams (keyed by physical parser stream, value is packetId) that have
665
+ * observed their first RAP under a reliable-moq policy. Reliable MoQ
666
+ * resolves to a zero parser reorder window (transport ordering is
667
+ * protocol-owned), so #sequencerFor returns null and its own startup gate
668
+ * never runs; this map preserves an order-independent "discard delta
669
+ * frames before the first RAP" gate for that path without reintroducing
670
+ * reordering into the parser. */
671
+ #reliableMoqStartupGate = new Map<string, number>()
672
+ #disposed = false
673
+ #fedObjects = 0
674
+ #feedOutcomes = new Map<MmtpFeedOutcomeKey, number>()
675
+ #deferredFeedOutcomes = new Map<MmtpFeedDropReason, number>()
676
+ #lastOutcome: MmtpFeedOutcomeKey | null = null
677
+ #lastOutcomeCount = 0
678
+
679
+ #streamKey(packetId: number, catalogTrackName?: string): string {
680
+ return JSON.stringify([packetId, catalogTrackName ?? null])
681
+ }
682
+
683
+ #sampleCombinerFor(streamKey: string): SampleCombiner {
684
+ let combiner = this.#sampleCombiners.get(streamKey)
685
+ if (combiner === undefined) {
686
+ combiner = new SampleCombiner({
687
+ ...this.#sampleCombinerOptions,
688
+ onAbandon: sample => this.#releaseTimingSlot(streamKey, sample),
689
+ })
690
+ this.#sampleCombiners.set(streamKey, combiner)
691
+ }
692
+ return combiner
693
+ }
694
+
695
+ /**
696
+ * Return the timing budget slot of a sample the combiner abandoned.
697
+ *
698
+ * Without this, `#pendingTimingSamples` is charged for every sample a moof
699
+ * declares but credited only when that sample is delivered, so each sample
700
+ * lost to eviction or timeout burns a slot until the window displaces it.
701
+ * Window displacement bounds that leak but only reclaims the OLDEST entries;
702
+ * this path returns a slot the moment the combiner knows the sample is gone,
703
+ * which keeps the live window as wide as the stream actually needs.
704
+ *
705
+ * The parser total is decremented only when the resolver reports it
706
+ * actually freed a slot, so a resolver already dropped by a track reset, or
707
+ * a sample released twice, cannot drive the accounting negative.
708
+ */
709
+ #releaseTimingSlot(streamKey: string, sample: AbandonedSample): void {
710
+ const resolver = this.#timingStreams.get(streamKey)?.resolver
711
+ if (!resolver) return
712
+ if (!resolver.releaseSample(
713
+ sample.mpuSequenceNumber,
714
+ sample.movieFragmentSequenceNumber,
715
+ sample.sampleNumber,
716
+ )) return
717
+ this.#pendingTimingSamples--
718
+ this.#reclaimedTimingSamples++
719
+ if (this.#pendingTimingSamples < 0) {
720
+ throw new Error('[MmtpContainerParser] pending timing sample accounting underflow')
721
+ }
722
+ }
723
+
724
+ /** Drop one stream's combiner, folding its counters into the retained
725
+ * totals so a reset cannot make the exposed stats run backwards. */
726
+ #dropSampleCombiner(streamKey: string): void {
727
+ const combiner = this.#sampleCombiners.get(streamKey)
728
+ if (combiner === undefined) return
729
+ const stats = combiner.stats
730
+ this.#retiredSampleCombinerStats.combinedSamples += stats.combinedSamples
731
+ this.#retiredSampleCombinerStats.passthroughSamples += stats.passthroughSamples
732
+ this.#retiredSampleCombinerStats.timedOutSamples += stats.timedOutSamples
733
+ this.#retiredSampleCombinerStats.evictedSamples += stats.evictedSamples
734
+ this.#sampleCombiners.delete(streamKey)
735
+ }
736
+
737
+ constructor(config?: MmtpParserConfig) {
738
+ if (config?.reassemblyPolicy && config.mfuTimeoutMs !== undefined) {
739
+ throw new Error('[MmtpContainerParser] use reassemblyPolicy or mfuTimeoutMs, not both')
740
+ }
741
+ const reassemblyPolicy = config?.reassemblyPolicy ?? null
742
+ const requestedReorderWindow = config?.reorderWindow ?? 0
743
+ if (!Number.isFinite(requestedReorderWindow) || requestedReorderWindow < 0) {
744
+ throw new Error('[MmtpContainerParser] reorderWindow must be a finite number >= 0')
745
+ }
746
+ const normalizedReorderWindow = Math.ceil(requestedReorderWindow)
747
+ const requestedMfuTimeout = reassemblyPolicy
748
+ ? mfuReassemblyTimeoutMs(reassemblyPolicy)
749
+ : config?.mfuTimeoutMs
750
+ if (reassemblyPolicy) {
751
+ mmtpFrameReorderWindowFrames(reassemblyPolicy, normalizedReorderWindow)
752
+ }
753
+ if (
754
+ requestedMfuTimeout !== undefined &&
755
+ requestedMfuTimeout !== null &&
756
+ requestedMfuTimeout <= 0
757
+ ) {
758
+ throw new Error('[MmtpContainerParser] mfuTimeoutMs must be > 0')
759
+ }
760
+ const maxPendingTimingSamples = config?.maxBufferedMfus ?? 100
761
+ if (!Number.isInteger(maxPendingTimingSamples) || maxPendingTimingSamples <= 0) {
762
+ throw new Error('[MmtpContainerParser] maxBufferedMfus must be a positive integer')
763
+ }
764
+ this.#maxPendingTimingSamples = maxPendingTimingSamples
765
+
766
+ const maxSubsampleSegments = config?.maxSubsampleSegments ?? 16
767
+ if (!Number.isInteger(maxSubsampleSegments) || maxSubsampleSegments <= 0) {
768
+ throw new Error('[MmtpContainerParser] maxSubsampleSegments must be a positive integer')
769
+ }
770
+
771
+ // Everything above only reads `config` — the full policy/window
772
+ // combination is validated before this point, so it is now safe to
773
+ // mutate this instance or an injected, caller-owned reassembler.
774
+ this.#reassemblyPolicy = reassemblyPolicy
775
+ if (config?.reassembler) {
776
+ this.#reassembler = config.reassembler
777
+ // An injected backend owns its existing transport policy unless this
778
+ // parser was given an explicit policy/legacy timeout override.
779
+ if (requestedMfuTimeout !== undefined) {
780
+ this.#reassembler.timeoutMs = requestedMfuTimeout
781
+ }
782
+ } else {
783
+ this.#reassembler = new MFUReassembler({
784
+ timeoutMs: requestedMfuTimeout === undefined ? 1000 : requestedMfuTimeout,
785
+ ...(config?.maxBufferedMfus === undefined ? {} : { maxBuffered: config.maxBufferedMfus }),
786
+ })
787
+ }
788
+ this.#reorderWindow = normalizedReorderWindow
789
+ this.#sampleCombinerOptions = {
790
+ maxBuffered: maxPendingTimingSamples,
791
+ // The parser resolves every timed sample's authored size from FT=1
792
+ // before its data units reach the combiner, so "no size yet" always
793
+ // means the size has not arrived -- never that the host declines to
794
+ // supply one. Waiting is therefore correct: treating an offset-zero
795
+ // data unit as a whole sample in that window would truncate a
796
+ // partitioned sample instead of completing it.
797
+ requireAuthoredSize: true,
798
+ maxSegmentsPerSample: maxSubsampleSegments,
799
+ }
800
+ this.#defaultTrack = config?.defaultTrack ?? null
801
+ if (this.#defaultTrack) this.#validateReassemblyPolicy()
802
+ if (config?.tracks) {
803
+ for (const [id, info] of config.tracks) {
804
+ this.#validateReassemblyPolicy()
805
+ this.#tracks.set(id, info)
806
+ }
807
+ }
808
+ }
809
+
810
+ /** The MFU reassembler that buffers fragmented MFUs fed via feed().
811
+ * Exposed so a co-operating FEC-recovery layer (MmtpRouter) feeds recovered
812
+ * fragments into the SAME reassembler: a frame split between source-received
813
+ * and FEC-recovered fragments must share one buffer to complete (BLO-8151).
814
+ * Before this was shared, recovered video symbols reported "0 emitted,
815
+ * pending-reassembly=N" because they landed in a separate instance. */
816
+ get reassembler(): MfuReassemblerBackend {
817
+ return this.#reassembler
818
+ }
819
+
820
+ /**
821
+ * Subsample-combining counters (moqcast #37). `combinedSamples` counts
822
+ * samples reassembled from two or more data units and `passthroughSamples`
823
+ * those that arrived whole; the two are deliberately separate, so a
824
+ * producer that silently stopped partitioning cannot hide behind a single
825
+ * "samples delivered" total. This is the typed acceptance signal for
826
+ * large-frame playback — never parse the console for it.
827
+ */
828
+ get sampleCombinerStats(): Readonly<SampleCombinerStats> {
829
+ const total = { ...this.#retiredSampleCombinerStats }
830
+ for (const combiner of this.#sampleCombiners.values()) {
831
+ const stats = combiner.stats
832
+ total.combinedSamples += stats.combinedSamples
833
+ total.passthroughSamples += stats.passthroughSamples
834
+ total.timedOutSamples += stats.timedOutSamples
835
+ total.evictedSamples += stats.evictedSamples
836
+ }
837
+ return total
838
+ }
839
+
840
+ /**
841
+ * Typed view of the ISO-BMFF timing-sample budget.
842
+ *
843
+ * This is the acceptance signal for the budget ratchet: `pending` walking
844
+ * to `capacity` while `rejectedMovieFragments` climbs is the stream wedging,
845
+ * and it is observable here rather than only as media silently stopping.
846
+ * Per repo policy this is a producer-owned counter -- never infer budget
847
+ * state by parsing logs.
848
+ */
849
+ get timingBudgetStats(): Readonly<MmtpTimingBudgetStats> {
850
+ return {
851
+ pending: this.#pendingTimingSamples,
852
+ capacity: this.#maxPendingTimingSamples,
853
+ reclaimed: this.#reclaimedTimingSamples,
854
+ evicted: this.#evictedTimingSamples,
855
+ rejectedMovieFragments: this.#rejectedMovieFragments,
856
+ }
857
+ }
858
+
859
+ get reorderStats(): Readonly<MmtpReorderStats> {
860
+ let bufferedFrames = 0
861
+ for (const { sequencer } of this.#sequencers.values()) bufferedFrames += sequencer.buffered
862
+ return { ...this.#reorderStats, bufferedFrames }
863
+ }
864
+
865
+ /**
866
+ * Feed tallies for this parser. A fresh snapshot per call, not a live view,
867
+ * so a caller can hold one and compare it against a later read.
868
+ *
869
+ * Survives dispose() — the counters are the parser's own state and are not
870
+ * cleared by teardown, because a wedged receive path is usually read after
871
+ * the fact.
872
+ */
873
+ get feedStats(): Readonly<MmtpParserFeedStats> {
874
+ return {
875
+ fedObjects: this.#fedObjects,
876
+ outcomes: Object.fromEntries(this.#feedOutcomes),
877
+ deferredOutcomes: Object.fromEntries(this.#deferredFeedOutcomes),
878
+ }
879
+ }
880
+
881
+ /**
882
+ * Outcome token of the most recent feed(), or null before the first call.
883
+ *
884
+ * Exposed alongside `lastFeedOutcomeCount` as two scalars rather than one
885
+ * object because a wrapper reads them on every packet, and feedStats()
886
+ * allocates a fresh snapshot per call.
887
+ */
888
+ get lastFeedOutcome(): MmtpFeedOutcomeKey | null {
889
+ return this.#lastOutcome
890
+ }
891
+
892
+ /** How many times `lastFeedOutcome` has now been seen. 1 on its first
893
+ * occurrence, which is what a caller logging once per novel outcome needs. */
894
+ get lastFeedOutcomeCount(): number {
895
+ return this.#lastOutcomeCount
896
+ }
897
+
898
+ #countOutcome(token: MmtpFeedOutcomeKey): void {
899
+ const count = (this.#feedOutcomes.get(token) ?? 0) + 1
900
+ this.#feedOutcomes.set(token, count)
901
+ this.#lastOutcome = token
902
+ this.#lastOutcomeCount = count
903
+ }
904
+
905
+ #countDeferredOutcome(reason: MmtpFeedDropReason): void {
906
+ this.#deferredFeedOutcomes.set(
907
+ reason,
908
+ (this.#deferredFeedOutcomes.get(reason) ?? 0) + 1,
909
+ )
910
+ }
911
+
912
+ /**
913
+ * Count a feed() drop and return null in one expression, so every early
914
+ * return inside feed() states the reason it took instead of collapsing into
915
+ * a single failure bucket. Returns null so call sites stay `return this.#drop(...)`.
916
+ */
917
+ #drop(reason: MmtpFeedDropReason): null {
918
+ this.#countOutcome(`drop:${reason}`)
919
+ return null
920
+ }
921
+
922
+ get reorderDiagnostics(): ReadonlyArray<MmtpReorderStreamDiagnostics> {
923
+ const streams: MmtpReorderStreamDiagnostics[] = []
924
+ for (const { packetId, sequencer } of this.#sequencers.values()) {
925
+ for (const diagnostics of sequencer.diagnostics) streams.push({ packetId, ...diagnostics })
926
+ }
927
+ return streams
928
+ }
929
+
930
+ /**
931
+ * Reclaim samples abandoned by the sample combiner's protocol deadline.
932
+ *
933
+ * This is deliberately driven by the parser's existing packet/lifecycle
934
+ * cleanup path rather than by another timer. The reassembler timeout is the
935
+ * same catalog/FEC-derived deadline that bounds each constituent MFU.
936
+ */
937
+ cleanupTimeouts(): void {
938
+ const timeoutMs = this.#reassembler.timeoutMs
939
+ if (timeoutMs === null) return
940
+ for (const combiner of this.#sampleCombiners.values()) {
941
+ combiner.cleanupTimeouts(timeoutMs)
942
+ }
943
+ }
944
+
945
+ /**
946
+ * Phase 1 of two-phase parsing. Returns structured header info for FEC feeding.
947
+ * Call this before feed() when you need to extract FEC payload ID and feed
948
+ * symbols to FECManager before MFU extraction.
949
+ * Returns null for malformed packets.
950
+ */
951
+ parseHeader(packet: Uint8Array, options?: ParseHeaderOptions): PacketInfo | null {
952
+ try {
953
+ const header = parseMMTPHeader(packet)
954
+ const isRepair = header.packetType === PACKET_TYPE_REPAIR
955
+ const fecPayloadId =
956
+ !options?.skipFecPayloadId &&
957
+ !isRepair && hasSourceFecPayloadId(packet)
958
+ ? extractFecPayloadId(packet)
959
+ : null
960
+ return {
961
+ packetId: header.packetId,
962
+ packetType: header.packetType,
963
+ timestamp: header.timestamp,
964
+ timestampUs: mmtpTimestampToMicros(header.timestamp),
965
+ sequenceNumber: header.sequenceNumber,
966
+ rapFlag: header.rapFlag,
967
+ fecType: header.fecType,
968
+ fecPayloadId,
969
+ isRepair,
970
+ }
971
+ } catch {
972
+ return null
973
+ }
974
+ }
975
+
976
+ /** Feed a complete MMTP packet. For two-phase use, call parseHeader() first. */
977
+ feed(packet: Uint8Array, options?: ParseHeaderOptions): MmtpParserRecord | null {
978
+ this.#fedObjects += 1
979
+ let record: MmtpParserRecord | null
980
+ try {
981
+ record = this.#feed(packet, options)
982
+ } catch (error) {
983
+ // Count before rethrowing: a throw is an outcome, and leaving it
984
+ // uncounted would silently break the sum-to-fedObjects invariant that
985
+ // makes this tally readable.
986
+ this.#countOutcome('drop:threw')
987
+ throw error
988
+ }
989
+ // Drops are counted at their own return sites via #drop(), which is what
990
+ // preserves the reason. Only successful records are counted here.
991
+ if (record) this.#countOutcome(record.kind)
992
+ return record
993
+ }
994
+
995
+ #feed(packet: Uint8Array, options?: ParseHeaderOptions): MmtpParserRecord | null {
996
+ if (this.#disposed) return this.#drop('disposed')
997
+
998
+ this.cleanupTimeouts()
999
+
1000
+ const info = this.parseHeader(packet, options)
1001
+ if (!info) return this.#drop('header-parse-failed')
1002
+
1003
+ if (info.packetType !== PACKET_TYPE_MPU) {
1004
+ return {
1005
+ kind: info.isRepair ? 'repair' : 'non-media',
1006
+ packet: info,
1007
+ byteLength: packet.byteLength,
1008
+ }
1009
+ }
1010
+
1011
+ // ── FEC source-symbol exposure ──
1012
+ // Fire onSourceSymbol BEFORE MFU extraction so the consumer-side FEC
1013
+ // pipeline (e.g. FecWorkerClient.feedSource) starts as early as possible.
1014
+ // Source-only: parseHeader already gates fecPayloadId on !isRepair.
1015
+ if (info.fecPayloadId !== null && this.#sourceSymbolCb) {
1016
+ // Strip the 4-byte fec_payload_id trailer per ISO 23008-1 §C.5.2.
1017
+ // Symbol-size zero-padding is the consumer's responsibility — FEC
1018
+ // symbol size is a FEC-config concern, not a parser concern.
1019
+ const rawSymbol = packet.subarray(0, packet.length - 4)
1020
+ const altaTrailer = extractAltaTrailer(packet)
1021
+ const alta = altaTrailer
1022
+ ? { auth: altaTrailer.auth, payloadEnd: altaTrailer.signedEnd }
1023
+ : undefined
1024
+ try {
1025
+ this.#sourceSymbolCb(info.packetId, info.fecPayloadId.ssId, rawSymbol, alta)
1026
+ } catch {
1027
+ // Source-symbol consumer threw — never block MFU extraction on
1028
+ // FEC-pipeline errors. The frame path stays viable; the consumer
1029
+ // is responsible for its own error surfacing.
1030
+ }
1031
+ }
1032
+
1033
+ const extraction = extractMFUPayload(packet, {
1034
+ skipFecId: options?.skipFecPayloadId,
1035
+ })
1036
+ if (!extraction) return this.#drop('mfu-extract-failed')
1037
+
1038
+ const { mpu, fragmentType, data, mfuDuHeader } = extraction
1039
+ if (mpu.timedFlag !== 1) return this.#drop('untimed-mpu')
1040
+ const streamKey = this.#streamKey(info.packetId, options?.catalogTrackName)
1041
+
1042
+ if (fragmentType === 0) {
1043
+ if (mpu.fragmentationIndicator !== 0 || mpu.fragmentCounter !== 0)
1044
+ return this.#drop('init-unexpected-fragmentation')
1045
+ // ISO MMTP MPU metadata may carry an `mmpu` box between the ftyp and
1046
+ // moov boxes. That box is valid MMTP metadata but is not a legal
1047
+ // top-level MSE initialization box. Normalize at the parser boundary so
1048
+ // every container consumer (including MSE and rendition handoff paths)
1049
+ // receives append-safe ftyp+moov bytes rather than having to remember
1050
+ // this wire-format detail independently.
1051
+ const normalizedInit = normalizeMmtpInitSegment(data)
1052
+ if (!normalizedInit) return this.#drop('init-normalize-failed')
1053
+ const resolver = this.#timingResolver(streamKey, info.packetId, true)
1054
+ if (!resolver?.addInitialization(normalizedInit)) return this.#drop('init-timing-refused')
1055
+ this.#initCb?.(normalizedInit, info.packetId, options?.catalogTrackName)
1056
+ return {
1057
+ kind: 'init',
1058
+ packet: info,
1059
+ data: normalizedInit,
1060
+ mpuSequenceNumber: mpu.mpuSequenceNumber,
1061
+ fragmentationIndicator: mpu.fragmentationIndicator,
1062
+ fragmentCounter: mpu.fragmentCounter,
1063
+ }
1064
+ }
1065
+
1066
+ if (fragmentType === 1) {
1067
+ if (mpu.fragmentationIndicator !== 0 || mpu.fragmentCounter !== 0)
1068
+ return this.#drop('fragment-unexpected-fragmentation')
1069
+ const resolver = this.#timingResolver(streamKey, info.packetId, false)
1070
+ if (!resolver) return this.#drop('fragment-no-resolver')
1071
+ const pendingBefore = resolver.pendingSampleCount
1072
+ const evictedBefore = resolver.evictedSampleCount
1073
+ const remaining = this.#maxPendingTimingSamples - this.#pendingTimingSamples
1074
+ const admitted = resolver.addMovieFragment(mpu.mpuSequenceNumber, data, remaining)
1075
+ // Sync on BOTH paths: the resolver can displace stale timing and still
1076
+ // refuse the fragment in hand, and a global count that missed those
1077
+ // freed slots would keep refusing on a budget that is no longer full.
1078
+ this.#pendingTimingSamples += resolver.pendingSampleCount - pendingBefore
1079
+ this.#evictedTimingSamples += resolver.evictedSampleCount - evictedBefore
1080
+ if (!admitted) {
1081
+ if (resolver.lastMovieFragmentRejection === 'budget') {
1082
+ this.#rejectedMovieFragments++
1083
+ }
1084
+ return this.#drop('fragment-timing-refused')
1085
+ }
1086
+ return {
1087
+ kind: 'fragment',
1088
+ packet: info,
1089
+ data,
1090
+ mpuSequenceNumber: mpu.mpuSequenceNumber,
1091
+ fragmentationIndicator: mpu.fragmentationIndicator,
1092
+ fragmentCounter: mpu.fragmentCounter,
1093
+ }
1094
+ }
1095
+ if (fragmentType !== 2) return this.#drop('unknown-fragment-type')
1096
+
1097
+ const resolver = this.#timingResolver(streamKey, info.packetId, false)
1098
+ if (!resolver) return this.#drop('mfu-no-resolver')
1099
+ if (!Number.isSafeInteger(data.byteLength) || data.byteLength <= 0)
1100
+ return this.#drop('mfu-empty-payload')
1101
+ if (mpu.fragmentationIndicator === 0 || mpu.fragmentationIndicator === 1) {
1102
+ if (!mfuDuHeader || mfuDuHeader.movieFragmentSequenceNumber === 0 ||
1103
+ mfuDuHeader.sampleNumber === 0) {
1104
+ return this.#drop('mfu-du-header-missing')
1105
+ }
1106
+ const headTiming = resolver.sampleTiming(
1107
+ mpu.mpuSequenceNumber,
1108
+ mfuDuHeader.movieFragmentSequenceNumber,
1109
+ mfuDuHeader.sampleNumber,
1110
+ )
1111
+ if (!headTiming) return this.#drop('mfu-sample-timing-missing')
1112
+ // A data unit's authored base offset is its position within the sample
1113
+ // (moqcast #37): 0 for a whole-sample DU, nonzero for one subsample DU
1114
+ // among siblings. Sizes below are therefore relative to that base, not
1115
+ // to the sample's start -- comparing a subsample DU against the whole
1116
+ // sample's size is what dropped every partitioned 4K/8K frame.
1117
+ const offset = mfuDuHeader.offset
1118
+ if (!Number.isSafeInteger(offset) || offset < 0 || offset >= headTiming.size)
1119
+ return this.#drop('mfu-offset-out-of-range')
1120
+ const remaining = headTiming.size - offset
1121
+ // FI=0 carries its data unit whole, in a single payload. Only a
1122
+ // partitioned sample's FINAL data unit can be shorter than a full data
1123
+ // unit -- every earlier one spans exactly 256 payloads and so is always
1124
+ // fragmented -- which makes a whole-carried DU exactly the bytes left
1125
+ // from its base. At offset 0 this is the pre-amendment check unchanged.
1126
+ if (mpu.fragmentationIndicator === 0 && data.byteLength !== remaining)
1127
+ return this.#drop('mfu-size-mismatch')
1128
+ if (mpu.fragmentationIndicator === 1 && data.byteLength >= remaining)
1129
+ return this.#drop('mfu-size-mismatch')
1130
+
1131
+ // Publish the authored size before this data unit can complete, so the
1132
+ // combiner never has to guess whether an offset-zero DU is a whole
1133
+ // sample. Registration can complete a previously buffered sample; its
1134
+ // refusal is a side effect of this feed, not this packet's own outcome,
1135
+ // so expose it in deferredOutcomes without double-counting outcomes.
1136
+ const readyOnRegister = this.#sampleCombinerFor(streamKey).registerAuthoredSize(
1137
+ options?.catalogTrackName,
1138
+ mpu.mpuSequenceNumber,
1139
+ mfuDuHeader.movieFragmentSequenceNumber,
1140
+ mfuDuHeader.sampleNumber,
1141
+ headTiming.size,
1142
+ )
1143
+ if (readyOnRegister) {
1144
+ const deferred = this.#emitCombinedSample(
1145
+ readyOnRegister,
1146
+ info,
1147
+ resolver,
1148
+ options?.recovered === true,
1149
+ )
1150
+ if (deferred.record === undefined) this.#countDeferredOutcome(deferred.reason)
1151
+ }
1152
+ }
1153
+
1154
+ let assembled: ReassembledMFU | null
1155
+ try {
1156
+ assembled = this.#reassembler.addFragment({
1157
+ fragmentationIndicator: mpu.fragmentationIndicator,
1158
+ mpuSequenceNumber: mpu.mpuSequenceNumber,
1159
+ fragmentCounter: mpu.fragmentCounter,
1160
+ data,
1161
+ timestamp: info.timestamp,
1162
+ rapFlag: info.rapFlag,
1163
+ priority: mfuDuHeader?.priority,
1164
+ sampleNumber: mfuDuHeader?.sampleNumber,
1165
+ movieFragmentSequenceNumber: mfuDuHeader?.movieFragmentSequenceNumber,
1166
+ sampleOffset: mfuDuHeader?.offset,
1167
+ packetSequenceNumber: info.sequenceNumber,
1168
+ recovered: options?.recovered === true,
1169
+ source: options?.source,
1170
+ deliveryProvenance: options?.deliveryProvenance,
1171
+ catalogTrackName: options?.catalogTrackName,
1172
+ packetId: info.packetId,
1173
+ })
1174
+ } catch (error) {
1175
+ if (!isMfuReassemblyError(error)) throw error
1176
+ console.warn(
1177
+ `[MmtpContainerParser] rejected MFU fragment: code=${error.code} ` +
1178
+ `mpu=${mpu.mpuSequenceNumber} fi=${mpu.fragmentationIndicator} ` +
1179
+ `counter=${mpu.fragmentCounter}`,
1180
+ )
1181
+ return {
1182
+ kind: 'rejected',
1183
+ packet: info,
1184
+ mpuSequenceNumber: mpu.mpuSequenceNumber,
1185
+ fragmentationIndicator: mpu.fragmentationIndicator,
1186
+ fragmentCounter: mpu.fragmentCounter,
1187
+ error: { code: error.code, message: error.message },
1188
+ }
1189
+ }
1190
+
1191
+ if (!assembled) {
1192
+ return {
1193
+ kind: 'pending',
1194
+ packet: info,
1195
+ mpuSequenceNumber: mpu.mpuSequenceNumber,
1196
+ fragmentationIndicator: mpu.fragmentationIndicator,
1197
+ fragmentCounter: mpu.fragmentCounter,
1198
+ }
1199
+ }
1200
+
1201
+ const movieFragmentSequenceNumber = assembled.movieFragmentSequenceNumber
1202
+ const sampleNumber = assembled.sampleNumber
1203
+ if (movieFragmentSequenceNumber === undefined || sampleNumber === undefined)
1204
+ return this.#drop('assembled-identity-missing')
1205
+ // Resolve authored timing BEFORE the combiner sees this data unit. An
1206
+ // assembled data unit whose sample identity has no authored timing is not
1207
+ // a sample this stream declared, so it must be dropped outright -- letting
1208
+ // it buffer would occupy a combiner slot and could evict a legitimate
1209
+ // partitioned sample waiting on a sibling.
1210
+ if (!resolver.sampleTiming(
1211
+ assembled.mpuSequenceNumber,
1212
+ movieFragmentSequenceNumber,
1213
+ sampleNumber,
1214
+ )) return this.#drop('assembled-timing-missing')
1215
+ if (assembled.mpuSequenceNumber !== mpu.mpuSequenceNumber)
1216
+ return this.#drop('assembled-mpu-mismatch')
1217
+
1218
+ // One reassembled data unit is not necessarily one whole sample. A
1219
+ // partitioned sample completes only once every sibling data unit has
1220
+ // arrived and they are byte-adjacent from zero to the authored size; an
1221
+ // unpartitioned sample passes straight through with no buffering.
1222
+ let combined: CombinedSample | null
1223
+ try {
1224
+ combined = this.#sampleCombinerFor(streamKey).addMfu(assembled)
1225
+ } catch (error) {
1226
+ if (!(error instanceof SampleCombineError)) throw error
1227
+ console.warn(
1228
+ `[MmtpContainerParser] rejected subsample data unit: code=${error.code} ` +
1229
+ `mpu=${assembled.mpuSequenceNumber} sample=${sampleNumber} ` +
1230
+ `offset=${assembled.sampleOffset ?? 0}`,
1231
+ )
1232
+ return {
1233
+ kind: 'rejected',
1234
+ packet: info,
1235
+ mpuSequenceNumber: mpu.mpuSequenceNumber,
1236
+ fragmentationIndicator: mpu.fragmentationIndicator,
1237
+ fragmentCounter: mpu.fragmentCounter,
1238
+ error: { code: error.code, message: error.message },
1239
+ }
1240
+ }
1241
+ if (!combined) {
1242
+ return {
1243
+ kind: 'pending',
1244
+ packet: info,
1245
+ mpuSequenceNumber: mpu.mpuSequenceNumber,
1246
+ fragmentationIndicator: mpu.fragmentationIndicator,
1247
+ fragmentCounter: mpu.fragmentCounter,
1248
+ }
1249
+ }
1250
+
1251
+ const outcome = this.#emitCombinedSample(
1252
+ combined,
1253
+ info,
1254
+ resolver,
1255
+ options?.recovered === true,
1256
+ )
1257
+ if (outcome.record !== undefined) return outcome.record
1258
+ return this.#drop(outcome.reason)
1259
+ }
1260
+
1261
+ /**
1262
+ * Deliver one combined sample: resolve its authored timing, build frame
1263
+ * metadata, retire the timing entry, and route it. Shared by the ordinary
1264
+ * completion path and by the rarer completion that falls out of registering
1265
+ * an authored size for data units that arrived before it.
1266
+ *
1267
+ * Reports a refusal as a reason rather than counting it, because the two
1268
+ * call sites owe different accounting. On `#feed()`'s tail return the
1269
+ * refusal *is* that feed()'s outcome and must be attributed; on the
1270
+ * register-completion path it belongs to an earlier sample while this
1271
+ * packet's own fragment continues to its own outcome, so counting it there
1272
+ * would push `outcomes` past `fedObjects` — the very invariant the
1273
+ * attribution exists to keep readable. Leaving the choice at the call site
1274
+ * is what keeps both true.
1275
+ */
1276
+ #emitCombinedSample(
1277
+ combined: CombinedSample,
1278
+ info: PacketInfo,
1279
+ resolver: IsoBmffTimingResolver,
1280
+ recovered: boolean,
1281
+ ): CombinedSampleOutcome {
1282
+ const timing = resolver.sampleTiming(
1283
+ combined.mpuSequenceNumber,
1284
+ combined.movieFragmentSequenceNumber,
1285
+ combined.sampleNumber,
1286
+ )
1287
+ if (!timing) return { reason: 'combined-timing-missing' }
1288
+ // The combiner already completes against the registered authored size;
1289
+ // re-checking here keeps the unpartitioned passthrough path fail-closed
1290
+ // on the same invariant rather than trusting it by construction.
1291
+ if (combined.data.byteLength !== timing.size) return { reason: 'combined-size-mismatch' }
1292
+
1293
+ const meta = this.#buildFrameMeta(
1294
+ combined,
1295
+ info.packetId,
1296
+ info.sequenceNumber,
1297
+ timing,
1298
+ recovered,
1299
+ )
1300
+ if (!meta) return { reason: 'combined-meta-build-failed' }
1301
+ if (!resolver.consumeSample(
1302
+ combined.mpuSequenceNumber,
1303
+ combined.movieFragmentSequenceNumber,
1304
+ combined.sampleNumber,
1305
+ timing,
1306
+ )) return { reason: 'combined-timing-consume-refused' }
1307
+ this.#pendingTimingSamples--
1308
+ this.routeFrame(combined.data, meta)
1309
+ return {
1310
+ record: {
1311
+ kind: 'frame',
1312
+ packet: info,
1313
+ data: combined.data,
1314
+ meta,
1315
+ mpuSequenceNumber: combined.mpuSequenceNumber,
1316
+ fragmentCount: combined.fragmentCount,
1317
+ rapFlag: combined.rapFlag,
1318
+ ...(combined.priority === undefined ? {} : { priority: combined.priority }),
1319
+ },
1320
+ }
1321
+ }
1322
+
1323
+ onInit(cb: InitCallback): void {
1324
+ this.#initCb = cb
1325
+ }
1326
+
1327
+ onFrame(cb: FrameCallback): void {
1328
+ this.#frameCb = cb
1329
+ }
1330
+
1331
+ /** Register a per-packet FEC source-symbol callback. Fires for each MMTP
1332
+ * source MPU packet that carries a Source FEC Payload ID (ISO 23008-1 §C.5.2).
1333
+ *
1334
+ * Consumer responsibilities:
1335
+ * - Zero-pad `rawSymbol` to the FEC `symbolSize` if shorter (FEC config is
1336
+ * out-of-band — typically from the CMSF catalog's `fec.symbolSize`).
1337
+ * - Decide oversized handling (rawSymbol.length > symbolSize is not FEC-
1338
+ * decodable; consumer typically skips that block's source-feed).
1339
+ * - Look up the right FEC consumer (e.g. FecWorkerClient) by packetId.
1340
+ *
1341
+ * Designed for spec-clean reuse: Shaka, upstream moqtail, or any consumer
1342
+ * that uses MmtpContainerParser can wire FEC source delivery without
1343
+ * pulling in MulticastManager/MmtpRouter/FECManager. */
1344
+ onSourceSymbol(cb: SourceSymbolCallback): void {
1345
+ this.#sourceSymbolCb = cb
1346
+ }
1347
+
1348
+ onTrackFrame(packetId: number, cb: FrameCallback): void {
1349
+ this.#trackFrameCbs.set(packetId, cb)
1350
+ }
1351
+
1352
+ setTrack(packetId: number, info: TrackInfo): void {
1353
+ this.#validateReassemblyPolicy()
1354
+ for (const [key, entry] of this.#sequencers) {
1355
+ if (entry.packetId !== packetId) continue
1356
+ entry.sequencer.flush()
1357
+ this.#sequencers.delete(key)
1358
+ }
1359
+ for (const [key, state] of this.#timingStreams) {
1360
+ if (state.packetId === packetId) this.#dropTimingStream(key)
1361
+ }
1362
+ for (const key of [...this.#sampleCombiners.keys()]) {
1363
+ if ((JSON.parse(key) as [number, string | null])[0] === packetId) {
1364
+ this.#dropSampleCombiner(key)
1365
+ }
1366
+ }
1367
+ for (const [key, gatedPacketId] of this.#reliableMoqStartupGate) {
1368
+ if (gatedPacketId === packetId) this.#reliableMoqStartupGate.delete(key)
1369
+ }
1370
+ this.#tracks.set(packetId, info)
1371
+ }
1372
+
1373
+ /** Drop one physical stream's ordering and MFU state for retained replay. */
1374
+ resetTrack(packetId: number, catalogTrackName: string): void {
1375
+ if (!catalogTrackName) {
1376
+ throw new Error('[MmtpContainerParser] catalogTrackName is required for track reset')
1377
+ }
1378
+ for (const [key, entry] of this.#sequencers) {
1379
+ if (key !== this.#streamKey(packetId, catalogTrackName)) continue
1380
+ entry.sequencer.discard()
1381
+ this.#sequencers.delete(key)
1382
+ }
1383
+ const streamKey = this.#streamKey(packetId, catalogTrackName)
1384
+ this.#dropTimingStream(streamKey)
1385
+ this.#reliableMoqStartupGate.delete(streamKey)
1386
+ this.#reassembler.resetStream(catalogTrackName)
1387
+ // Drop half-combined samples too: a partitioned sample whose siblings
1388
+ // straddle the reset would otherwise complete against a stale authored
1389
+ // size and hand the decoder bytes from two different publisher epochs.
1390
+ this.#dropSampleCombiner(streamKey)
1391
+ }
1392
+
1393
+ setMfuTimeoutMs(ms: number | null): void {
1394
+ if (ms !== null && ms <= 0) throw new Error('[MmtpContainerParser] mfuTimeoutMs must be > 0')
1395
+ this.#reassembler.timeoutMs = ms
1396
+ }
1397
+
1398
+ setReassemblyPolicy(policy: MfuReassemblyPolicy): void {
1399
+ const timeoutMs = mfuReassemblyTimeoutMs(policy)
1400
+ this.#validateReassemblyPolicy(policy)
1401
+ this.#flushSequencers()
1402
+ this.#reassemblyPolicy = policy
1403
+ this.#reliableMoqStartupGate.clear()
1404
+ this.setMfuTimeoutMs(timeoutMs)
1405
+ }
1406
+
1407
+ setReorderWindow(windowSize: number): void {
1408
+ if (!Number.isFinite(windowSize) || windowSize < 0) {
1409
+ throw new Error('[MmtpContainerParser] reorderWindow must be a finite number >= 0')
1410
+ }
1411
+ const nextWindow = Math.ceil(windowSize)
1412
+ this.#validateReassemblyPolicy(this.#reassemblyPolicy, nextWindow)
1413
+ this.#flushSequencers()
1414
+ this.#reorderWindow = nextWindow
1415
+ if (this.#reorderWindow <= 0) this.#sequencers.clear()
1416
+ }
1417
+
1418
+ /** Route an already-assembled frame through the same reorder/callback path
1419
+ * used by source packets. FEC recovery uses this to merge recovered frames
1420
+ * with direct source frames before the decoder sees them. */
1421
+ routeFrame(data: Uint8Array, meta: FrameMeta): void {
1422
+ const seq = meta.packetSequenceNumber
1423
+ const sequencer = this.#sequencerFor(meta.packetId, meta.trackType, meta.catalogTrackName)
1424
+ if (sequencer && seq !== undefined && Number.isFinite(seq)) {
1425
+ sequencer.push(data, meta, seq)
1426
+ return
1427
+ }
1428
+ if (this.#reassemblyPolicy?.transport === 'reliable-moq' &&
1429
+ meta.trackType === 'video' &&
1430
+ !this.#passReliableMoqStartupGate(meta)) {
1431
+ return
1432
+ }
1433
+ this.#emitFrame(data, meta)
1434
+ }
1435
+
1436
+ /**
1437
+ * Claim and return the authored timing for a recovered sample. Recovery does
1438
+ * not pass through the ordinary `feed()` completion path, so it must retire
1439
+ * the same bounded resolver entry once the router accepts the recovered
1440
+ * sample. A missing entry is a fail-closed signal: callers must not replace
1441
+ * it with an MMTP send timestamp or cadence guess.
1442
+ */
1443
+ consumeAuthoredTimingFor(
1444
+ packetId: number,
1445
+ mpuSequenceNumber: number,
1446
+ movieFragmentSequenceNumber: number | undefined,
1447
+ sampleNumber: number | undefined,
1448
+ catalogTrackName?: string,
1449
+ ): AuthoredTiming | null {
1450
+ if (movieFragmentSequenceNumber === undefined || sampleNumber === undefined) return null
1451
+ const resolver = this.#timingResolver(
1452
+ this.#streamKey(packetId, catalogTrackName),
1453
+ packetId,
1454
+ false,
1455
+ )
1456
+ const timing = resolver?.sampleTiming(
1457
+ mpuSequenceNumber,
1458
+ movieFragmentSequenceNumber,
1459
+ sampleNumber,
1460
+ )
1461
+ if (!timing || !resolver || !resolver.consumeSample(
1462
+ mpuSequenceNumber,
1463
+ movieFragmentSequenceNumber,
1464
+ sampleNumber,
1465
+ timing,
1466
+ )) return null
1467
+ this.#pendingTimingSamples--
1468
+ if (this.#pendingTimingSamples < 0) {
1469
+ throw new Error('[MmtpContainerParser] pending timing sample accounting underflow')
1470
+ }
1471
+ return {
1472
+ decodeTime: timing.decodeTime,
1473
+ presentationTime: timing.presentationTime,
1474
+ durationTicks: timing.duration,
1475
+ mediaTimescale: timing.timescale,
1476
+ }
1477
+ }
1478
+
1479
+ /** Order-independent reliable-moq startup gate: discard delta frames for a
1480
+ * stream until its first RAP is observed, without buffering/reordering. */
1481
+ #passReliableMoqStartupGate(meta: FrameMeta): boolean {
1482
+ const streamKey = this.#streamKey(meta.packetId, meta.catalogTrackName)
1483
+ if (meta.sourceDiscontinuity) this.#reliableMoqStartupGate.delete(streamKey)
1484
+ if (this.#reliableMoqStartupGate.has(streamKey)) return true
1485
+ if (!meta.keyframe) {
1486
+ this.#reorderStats.droppedBeforeStartupKeyframe++
1487
+ return false
1488
+ }
1489
+ this.#reliableMoqStartupGate.set(streamKey, meta.packetId)
1490
+ return true
1491
+ }
1492
+
1493
+ /** Emit all ordered frames currently buffered by the parser. Finite-stream
1494
+ * consumers may call this before dispose(); live cancellation should not. */
1495
+ flush(): void {
1496
+ if (this.#disposed) return
1497
+ this.#flushSequencers()
1498
+ }
1499
+
1500
+ dispose(): void {
1501
+ this.#disposed = true
1502
+ for (const { sequencer } of this.#sequencers.values()) sequencer.discard()
1503
+ this.#sequencers.clear()
1504
+ this.#timingStreams.clear()
1505
+ this.#pendingTimingSamples = 0
1506
+ this.#reliableMoqStartupGate.clear()
1507
+ this.#reassembler.reset()
1508
+ this.#frameCb = null
1509
+ this.#initCb = null
1510
+ this.#trackFrameCbs.clear()
1511
+ }
1512
+
1513
+ // ── Private ─────────────────────────────────────────────────────
1514
+
1515
+ #timingResolver(
1516
+ streamKey: string,
1517
+ packetId: number,
1518
+ create: boolean,
1519
+ ): IsoBmffTimingResolver | null {
1520
+ const existing = this.#timingStreams.get(streamKey)
1521
+ if (existing) return existing.resolver
1522
+ if (!create) return null
1523
+ const resolver = new IsoBmffTimingResolver(this.#maxPendingTimingSamples)
1524
+ this.#timingStreams.set(streamKey, { packetId, resolver })
1525
+ return resolver
1526
+ }
1527
+
1528
+ #dropTimingStream(streamKey: string): void {
1529
+ const state = this.#timingStreams.get(streamKey)
1530
+ if (!state) return
1531
+ this.#timingStreams.delete(streamKey)
1532
+ this.#pendingTimingSamples -= state.resolver.pendingSampleCount
1533
+ if (this.#pendingTimingSamples < 0) {
1534
+ throw new Error('[MmtpContainerParser] pending timing sample accounting underflow')
1535
+ }
1536
+ }
1537
+
1538
+ #validateReassemblyPolicy(
1539
+ policy: MfuReassemblyPolicy | null = this.#reassemblyPolicy,
1540
+ reorderWindow = this.#reorderWindow,
1541
+ ): void {
1542
+ if (!policy) return
1543
+ mmtpFrameReorderWindowFrames(policy, reorderWindow)
1544
+ }
1545
+
1546
+ #buildFrameMeta(
1547
+ assembled: CombinedSample,
1548
+ packetId: number,
1549
+ seq: number,
1550
+ timing: IsoBmffSampleTiming,
1551
+ recovered = false,
1552
+ ): FrameMeta | null {
1553
+ const { data, rapFlag, priority } = assembled
1554
+
1555
+ if (isFmp4Box(data)) return null
1556
+ const presentationTimestamp = mediaTicksToMicroseconds(
1557
+ timing.presentationTime,
1558
+ timing.timescale,
1559
+ )
1560
+ const decodeTimestamp = mediaTicksToMicroseconds(timing.decodeTime, timing.timescale)
1561
+ const duration = mediaTicksToMicroseconds(BigInt(timing.duration), timing.timescale)
1562
+ if (presentationTimestamp === null || decodeTimestamp === null || duration === null ||
1563
+ duration <= 0) {
1564
+ return null
1565
+ }
1566
+
1567
+ let track = this.#tracks.get(packetId)
1568
+ if (!track && this.#defaultTrack) {
1569
+ track = this.#defaultTrack
1570
+ this.#tracks.set(packetId, track)
1571
+ }
1572
+ const codec = track?.codec ?? ''
1573
+
1574
+ const keyframe = MmtpContainerParser.isKeyframe(data, isMfuKeyframeMetadata(rapFlag, priority), codec)
1575
+
1576
+ const meta: FrameMeta = {
1577
+ timestamp: presentationTimestamp,
1578
+ decodeTimestamp,
1579
+ duration,
1580
+ decodeTime: timing.decodeTime,
1581
+ presentationTime: timing.presentationTime,
1582
+ durationTicks: timing.duration,
1583
+ mediaTimescale: timing.timescale,
1584
+ authoredTiming: {
1585
+ decodeTime: timing.decodeTime,
1586
+ presentationTime: timing.presentationTime,
1587
+ durationTicks: timing.duration,
1588
+ mediaTimescale: timing.timescale,
1589
+ },
1590
+ keyframe,
1591
+ packetId,
1592
+ trackType: track?.type ?? null,
1593
+ packetSequenceNumber: seq,
1594
+ ...(assembled.packetSequenceNumbers === undefined
1595
+ ? {}
1596
+ : { packetSequenceNumbers: assembled.packetSequenceNumbers }),
1597
+ ...(assembled.repairedPacketSequenceNumbers === undefined
1598
+ ? {}
1599
+ : { repairedPacketSequenceNumbers: assembled.repairedPacketSequenceNumbers }),
1600
+ ...(assembled.catalogTrackName === undefined
1601
+ ? {}
1602
+ : { catalogTrackName: assembled.catalogTrackName }),
1603
+ ...(assembled.mmtpTimestamp === undefined
1604
+ ? {}
1605
+ : { mmtpTimestamp: assembled.mmtpTimestamp }),
1606
+ mpuSequenceNumber: assembled.mpuSequenceNumber,
1607
+ ...(assembled.movieFragmentSequenceNumber === undefined
1608
+ ? {}
1609
+ : { movieFragmentSequenceNumber: assembled.movieFragmentSequenceNumber }),
1610
+ ...(assembled.sampleNumber === undefined ? {} : { sampleNumber: assembled.sampleNumber }),
1611
+ fragmentCount: assembled.fragmentCount,
1612
+ rapFlag,
1613
+ ...(priority === undefined ? {} : { priority }),
1614
+ ...(assembled.source === undefined ? {} : { source: assembled.source }),
1615
+ deliveryProvenance: assembled.deliveryProvenance,
1616
+ }
1617
+ if (recovered || (assembled.repairedPacketSequenceNumbers?.length ?? 0) > 0) {
1618
+ meta.recovered = true
1619
+ }
1620
+
1621
+ return meta
1622
+ }
1623
+
1624
+ #sequencerFor(
1625
+ packetId: number,
1626
+ _trackType: FrameMeta['trackType'],
1627
+ catalogTrackName?: string,
1628
+ ): FrameSequencer | null {
1629
+ const windowSize = this.#reassemblyPolicy
1630
+ ? mmtpFrameReorderWindowFrames(this.#reassemblyPolicy, this.#reorderWindow)
1631
+ : this.#reorderWindow
1632
+ if (windowSize <= 0) return null
1633
+ const streamKey = this.#streamKey(packetId, catalogTrackName)
1634
+ let sequencer = this.#sequencers.get(streamKey)?.sequencer
1635
+ if (!sequencer) {
1636
+ sequencer = new FrameSequencer(windowSize, (data, meta) => {
1637
+ this.#emitFrame(data, meta)
1638
+ }, (stat, count = 1) => {
1639
+ this.#reorderStats[stat] += count
1640
+ })
1641
+ this.#sequencers.set(streamKey, { packetId, sequencer })
1642
+ }
1643
+ return sequencer
1644
+ }
1645
+
1646
+ #flushSequencers(): void {
1647
+ for (const { sequencer } of this.#sequencers.values()) sequencer.flush()
1648
+ this.#sequencers.clear()
1649
+ }
1650
+
1651
+ /** Authored presentation timestamp of the last emitted frame, in microseconds. */
1652
+ lastFrameTimestamp: number | undefined = undefined
1653
+
1654
+ #emitFrame(data: Uint8Array, meta: FrameMeta): void {
1655
+ this.lastFrameTimestamp = meta.timestamp
1656
+ const trackCb = this.#trackFrameCbs.get(meta.packetId)
1657
+ trackCb?.(data, meta)
1658
+ this.#frameCb?.(data, meta)
1659
+ }
1660
+
1661
+ // ── Static utilities ──────────────────────────────────────────
1662
+
1663
+ static isKeyframe(data: Uint8Array, rapFlag: boolean, codec = ''): boolean {
1664
+ const payloadKeyframe = MmtpContainerParser.#scanForKeyframe(data, codec)
1665
+ if (payloadKeyframe !== undefined) return payloadKeyframe
1666
+ return rapFlag
1667
+ }
1668
+
1669
+ static #scanForKeyframe(data: Uint8Array, codec: string): boolean | undefined {
1670
+ const c = codec.toLowerCase()
1671
+ if (data.length === 0) return undefined
1672
+
1673
+ if (c.startsWith('av01')) {
1674
+ return scanAv1ObusForKeyframe(data)
1675
+ }
1676
+
1677
+ if (c.startsWith('vp09') || c.startsWith('vp8')) {
1678
+ return (data[0]! & 0x04) === 0
1679
+ }
1680
+
1681
+ const isHevc = c.startsWith('hev1') || c.startsWith('hvc1')
1682
+ if (isHevc) {
1683
+ if (data.length < 5) return undefined
1684
+ if (startsWithAnnexB(data)) {
1685
+ return MmtpContainerParser.#scanAnnexBForKeyframe(data, true)
1686
+ }
1687
+ return MmtpContainerParser.#scanLengthPrefixedHevcForKeyframe(data)
1688
+ }
1689
+
1690
+ if (c === '' || c.startsWith('avc')) {
1691
+ return classifyH264Keyframe(data, { allowPartialAvcc: true })
1692
+ }
1693
+
1694
+ return undefined
1695
+ }
1696
+
1697
+ static #scanLengthPrefixedHevcForKeyframe(data: Uint8Array): boolean | undefined {
1698
+ let sawNal = false
1699
+ let offset = 0
1700
+ while (offset + 4 < data.length) {
1701
+ const nalLen =
1702
+ (data[offset]! << 24) |
1703
+ (data[offset + 1]! << 16) |
1704
+ (data[offset + 2]! << 8) |
1705
+ data[offset + 3]!
1706
+ offset += 4
1707
+ if (nalLen <= 0) return sawNal ? false : undefined
1708
+ // The router's sync probe may see only the first fragment of a large
1709
+ // length-prefixed NAL. The length can exceed this packet, but the NAL
1710
+ // header is still enough to classify IRAP/IDR and open the sync gate.
1711
+ if (offset >= data.length) return sawNal ? false : undefined
1712
+ sawNal = true
1713
+ const hevcType = (data[offset]! >> 1) & 0x3f
1714
+ if (hevcType >= 16 && hevcType <= 21) return true
1715
+ if (offset + nalLen > data.length) break
1716
+ offset += nalLen
1717
+ }
1718
+ return sawNal ? false : undefined
1719
+ }
1720
+
1721
+ static #scanAnnexBForKeyframe(data: Uint8Array, isHevc: boolean): boolean | undefined {
1722
+ let offset = 0
1723
+ let sawNal = false
1724
+ while (offset < data.length) {
1725
+ const start = findAnnexBStart(data, offset)
1726
+ if (!start) return sawNal ? false : undefined
1727
+ const headerOffset = start.offset + start.length
1728
+ if (headerOffset >= data.length) return sawNal ? false : undefined
1729
+ sawNal = true
1730
+ if (isHevc) {
1731
+ if (headerOffset + 1 >= data.length) return sawNal ? false : undefined
1732
+ const hevcType = (data[headerOffset]! >> 1) & 0x3f
1733
+ if (hevcType >= 16 && hevcType <= 21) return true
1734
+ } else {
1735
+ const nalType = data[headerOffset]! & 0x1f
1736
+ if (nalType === 5) return true
1737
+ }
1738
+ offset = headerOffset + 1
1739
+ }
1740
+ return sawNal ? false : undefined
1741
+ }
1742
+ }
1743
+
1744
+ function scanAv1ObusForKeyframe(data: Uint8Array): boolean | undefined {
1745
+ let offset = 0
1746
+ let sawSequenceHeader = false
1747
+ let sawFrame = false
1748
+
1749
+ while (offset < data.length) {
1750
+ const obu = readAv1Obu(data, offset)
1751
+ if (!obu) return sawFrame ? false : undefined
1752
+ offset = obu.nextOffset
1753
+
1754
+ if (obu.type === 1) {
1755
+ sawSequenceHeader = true
1756
+ continue
1757
+ }
1758
+ if (obu.type !== 3 && obu.type !== 6) continue
1759
+
1760
+ sawFrame = true
1761
+ if (!sawSequenceHeader) {
1762
+ continue
1763
+ }
1764
+
1765
+ const keyframe = readAv1FrameHeaderKeyframe(data.subarray(obu.payloadOffset, obu.payloadEnd))
1766
+ if (keyframe !== undefined) return keyframe
1767
+ }
1768
+
1769
+ return sawFrame ? false : undefined
1770
+ }
1771
+
1772
+ function readAv1Obu(
1773
+ data: Uint8Array,
1774
+ offset: number,
1775
+ ): { type: number; payloadOffset: number; payloadEnd: number; nextOffset: number } | null {
1776
+ if (offset >= data.length) return null
1777
+ const header = data[offset]!
1778
+ if ((header & 0x80) !== 0 || (header & 0x01) !== 0) return null
1779
+
1780
+ const type = (header >> 3) & 0x0f
1781
+ const extensionFlag = (header & 0x04) !== 0
1782
+ const hasSizeField = (header & 0x02) !== 0
1783
+ let cursor = offset + 1
1784
+ if (extensionFlag) cursor += 1
1785
+ if (cursor > data.length) return null
1786
+
1787
+ let payloadSize = data.length - cursor
1788
+ if (hasSizeField) {
1789
+ const leb = readLeb128(data, cursor)
1790
+ if (!leb) return null
1791
+ payloadSize = leb.value
1792
+ cursor = leb.nextOffset
1793
+ }
1794
+
1795
+ const payloadEnd = cursor + payloadSize
1796
+ if (payloadEnd > data.length) return null
1797
+ return { type, payloadOffset: cursor, payloadEnd, nextOffset: payloadEnd }
1798
+ }
1799
+
1800
+ function readLeb128(data: Uint8Array, offset: number): { value: number; nextOffset: number } | null {
1801
+ let value = 0
1802
+ for (let i = 0; i < 8; i++) {
1803
+ if (offset + i >= data.length) return null
1804
+ const byte = data[offset + i]!
1805
+ value += (byte & 0x7f) * (2 ** (i * 7))
1806
+ if ((byte & 0x80) === 0) return { value, nextOffset: offset + i + 1 }
1807
+ }
1808
+ return null
1809
+ }
1810
+
1811
+ function readAv1FrameHeaderKeyframe(payload: Uint8Array): boolean | undefined {
1812
+ if (payload.length === 0) return undefined
1813
+ const first = payload[0]!
1814
+ const showExistingFrame = (first & 0x80) !== 0
1815
+ if (showExistingFrame) return false
1816
+ const frameType = (first >> 5) & 0x03
1817
+ return frameType === 0
1818
+ }
1819
+
1820
+ function startsWithAnnexB(data: Uint8Array): boolean {
1821
+ return (
1822
+ (data.length >= 4 && data[0] === 0 && data[1] === 0 && data[2] === 1) ||
1823
+ (data.length >= 5 && data[0] === 0 && data[1] === 0 && data[2] === 0 && data[3] === 1)
1824
+ )
1825
+ }
1826
+
1827
+ function findAnnexBStart(data: Uint8Array, offset: number): { offset: number; length: 3 | 4 } | null {
1828
+ for (let i = offset; i + 3 <= data.length; i++) {
1829
+ if (data[i] !== 0 || data[i + 1] !== 0) continue
1830
+ if (data[i + 2] === 1) return { offset: i, length: 3 }
1831
+ if (i + 3 < data.length && data[i + 2] === 0 && data[i + 3] === 1) {
1832
+ return { offset: i, length: 4 }
1833
+ }
1834
+ }
1835
+ return null
1836
+ }