@blockcast/mmt-container 0.1.0 → 0.3.0-main.ef68de90edb3

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