@blockcast/mmt-container 0.1.0-main.1fa0d55ca4da → 0.1.0-main.21c14c2ed355

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 (282) hide show
  1. package/README.md +92 -5
  2. package/dist/align.d.ts +6 -2
  3. package/dist/align.d.ts.map +1 -1
  4. package/dist/align.js.map +2 -2
  5. package/dist/audio-ring-buffer.js +23 -36
  6. package/dist/audio-ring-buffer.js.map +1 -1
  7. package/dist/block-policy.d.ts +314 -0
  8. package/dist/block-policy.d.ts.map +1 -0
  9. package/dist/block-policy.js +466 -0
  10. package/dist/block-policy.js.map +7 -0
  11. package/dist/browser-runtime.js +15283 -8334
  12. package/dist/browser-runtime.js.map +4 -4
  13. package/dist/carp-catalog.d.ts +73 -6
  14. package/dist/carp-catalog.d.ts.map +1 -1
  15. package/dist/carp-catalog.js +403 -35
  16. package/dist/carp-catalog.js.map +4 -4
  17. package/dist/catalog-fec-params.d.ts +110 -4
  18. package/dist/catalog-fec-params.d.ts.map +1 -1
  19. package/dist/catalog-fec-params.js +512 -6
  20. package/dist/catalog-fec-params.js.map +4 -4
  21. package/dist/catalog-network-source.d.ts +25 -2
  22. package/dist/catalog-network-source.d.ts.map +1 -1
  23. package/dist/catalog-network-source.js +21 -2
  24. package/dist/catalog-network-source.js.map +4 -4
  25. package/dist/cmaf-block-assembler.d.ts +99 -11
  26. package/dist/cmaf-block-assembler.d.ts.map +1 -1
  27. package/dist/cmaf-block-assembler.js +600 -612
  28. package/dist/cmaf-block-assembler.js.map +3 -3
  29. package/dist/cmaf-group-batcher.js +27 -46
  30. package/dist/cmaf-group-batcher.js.map +2 -2
  31. package/dist/cmcd-producer.js +45 -69
  32. package/dist/cmcd-producer.js.map +2 -2
  33. package/dist/conceal-timeline.d.ts +146 -0
  34. package/dist/conceal-timeline.d.ts.map +1 -0
  35. package/dist/conceal-timeline.js +137 -0
  36. package/dist/conceal-timeline.js.map +7 -0
  37. package/dist/delivery-gap.d.ts +75 -0
  38. package/dist/delivery-gap.d.ts.map +1 -0
  39. package/dist/delivery-gap.js +88 -0
  40. package/dist/delivery-gap.js.map +7 -0
  41. package/dist/duplicate-ledger.d.ts +197 -0
  42. package/dist/duplicate-ledger.d.ts.map +1 -0
  43. package/dist/duplicate-ledger.js +226 -0
  44. package/dist/duplicate-ledger.js.map +7 -0
  45. package/dist/fec-manager.d.ts +219 -21
  46. package/dist/fec-manager.d.ts.map +1 -1
  47. package/dist/fec-manager.js +2635 -926
  48. package/dist/fec-manager.js.map +4 -4
  49. package/dist/fec-starvation-detector.js +25 -40
  50. package/dist/fec-starvation-detector.js.map +1 -1
  51. package/dist/fec-state.d.ts +99 -0
  52. package/dist/fec-state.d.ts.map +1 -0
  53. package/dist/fec-state.js +54 -0
  54. package/dist/fec-state.js.map +7 -0
  55. package/dist/fec-worker-setup.d.ts +8 -0
  56. package/dist/fec-worker-setup.d.ts.map +1 -1
  57. package/dist/fec-worker-setup.js +256 -235
  58. package/dist/fec-worker-setup.js.map +3 -3
  59. package/dist/index.d.ts +17 -13
  60. package/dist/index.d.ts.map +1 -1
  61. package/dist/index.js +81 -5
  62. package/dist/index.js.map +2 -2
  63. package/dist/iso-bmff-timing.d.ts +12 -6
  64. package/dist/iso-bmff-timing.d.ts.map +1 -1
  65. package/dist/iso-bmff-timing.js +79 -72
  66. package/dist/iso-bmff-timing.js.map +2 -2
  67. package/dist/loc.js +7 -4
  68. package/dist/loc.js.map +1 -1
  69. package/dist/manifest-auth-plan.js +293 -294
  70. package/dist/manifest-auth-plan.js.map +2 -2
  71. package/dist/media-router.d.ts +14 -102
  72. package/dist/media-router.d.ts.map +1 -1
  73. package/dist/media-router.js +300 -504
  74. package/dist/media-router.js.map +3 -3
  75. package/dist/mfu-reassembler.d.ts +11 -7
  76. package/dist/mfu-reassembler.d.ts.map +1 -1
  77. package/dist/mfu-reassembler.js +658 -611
  78. package/dist/mfu-reassembler.js.map +4 -4
  79. package/dist/mmtp-container-parser.d.ts +235 -0
  80. package/dist/mmtp-container-parser.d.ts.map +1 -1
  81. package/dist/mmtp-container-parser.js +1819 -1312
  82. package/dist/mmtp-container-parser.js.map +4 -4
  83. package/dist/mmtp-container-parser.no-modules.js +1351 -357
  84. package/dist/mmtp-container-parser.no-modules.js.map +4 -4
  85. package/dist/mmtp-decode-pipeline.d.ts +7 -1
  86. package/dist/mmtp-decode-pipeline.d.ts.map +1 -1
  87. package/dist/mmtp-decode-pipeline.js +1932 -1440
  88. package/dist/mmtp-decode-pipeline.js.map +4 -4
  89. package/dist/mmtp-router.d.ts +267 -16
  90. package/dist/mmtp-router.d.ts.map +1 -1
  91. package/dist/mmtp-router.js +5139 -3327
  92. package/dist/mmtp-router.js.map +4 -4
  93. package/dist/mmtp-to-cmaf.d.ts +18 -15
  94. package/dist/mmtp-to-cmaf.d.ts.map +1 -1
  95. package/dist/mmtp-to-cmaf.js +125 -200
  96. package/dist/mmtp-to-cmaf.js.map +2 -2
  97. package/dist/multicast-input.d.ts +70 -36
  98. package/dist/multicast-input.d.ts.map +1 -1
  99. package/dist/multicast-input.js +144 -16
  100. package/dist/multicast-input.js.map +4 -4
  101. package/dist/multicast-manager.d.ts +320 -24
  102. package/dist/multicast-manager.d.ts.map +1 -1
  103. package/dist/multicast-manager.js +13242 -7196
  104. package/dist/multicast-manager.js.map +4 -4
  105. package/dist/multicast-transport.d.ts +130 -3
  106. package/dist/multicast-transport.d.ts.map +1 -1
  107. package/dist/multicast-transport.js +1974 -226
  108. package/dist/multicast-transport.js.map +4 -4
  109. package/dist/multicast-unwrap.js +58 -30
  110. package/dist/multicast-unwrap.js.map +3 -3
  111. package/dist/no-modules.d.ts +1 -0
  112. package/dist/no-modules.d.ts.map +1 -1
  113. package/dist/no-modules.js +2791 -1879
  114. package/dist/no-modules.js.map +4 -4
  115. package/dist/repair-fetch.d.ts +221 -23
  116. package/dist/repair-fetch.d.ts.map +1 -1
  117. package/dist/repair-fetch.js +308 -85
  118. package/dist/repair-fetch.js.map +4 -4
  119. package/dist/repair-layers.d.ts +141 -0
  120. package/dist/repair-layers.d.ts.map +1 -0
  121. package/dist/repair-layers.js +303 -0
  122. package/dist/repair-layers.js.map +7 -0
  123. package/dist/sample-combiner.js +118 -139
  124. package/dist/sample-combiner.js.map +1 -1
  125. package/dist/signaling.js +5 -0
  126. package/dist/signaling.js.map +3 -3
  127. package/dist/source-auth.js +355 -366
  128. package/dist/source-auth.js.map +2 -2
  129. package/dist/switching-set-alignment.js +14 -29
  130. package/dist/switching-set-alignment.js.map +1 -1
  131. package/dist/timestamp-codec.js +22 -11
  132. package/dist/timestamp-codec.js.map +3 -3
  133. package/dist/track-pipeline-setup.d.ts +11 -0
  134. package/dist/track-pipeline-setup.d.ts.map +1 -1
  135. package/dist/track-pipeline-setup.js +113 -128
  136. package/dist/track-pipeline-setup.js.map +2 -2
  137. package/dist/track-pipeline.js +107 -127
  138. package/dist/track-pipeline.js.map +1 -1
  139. package/dist/track-session.d.ts +107 -2
  140. package/dist/track-session.d.ts.map +1 -1
  141. package/dist/track-session.js +299 -214
  142. package/dist/track-session.js.map +2 -2
  143. package/dist/transport-pool.js +24 -38
  144. package/dist/transport-pool.js.map +1 -1
  145. package/dist/wasm-mfu-reassembler.d.ts +6 -1
  146. package/dist/wasm-mfu-reassembler.d.ts.map +1 -1
  147. package/dist/wasm-mfu-reassembler.js +188 -144
  148. package/dist/wasm-mfu-reassembler.js.map +3 -3
  149. package/dist/wasm-mmtp-container-parser.js +1945 -1438
  150. package/dist/wasm-mmtp-container-parser.js.map +4 -4
  151. package/node_modules/@blockcast/fec-worker/README.md +14 -6
  152. package/node_modules/@blockcast/fec-worker/dist/fec-worker-client.d.ts +10 -6
  153. package/node_modules/@blockcast/fec-worker/dist/fec-worker-client.d.ts.map +1 -1
  154. package/node_modules/@blockcast/fec-worker/dist/fec-worker-client.js +90 -25
  155. package/node_modules/@blockcast/fec-worker/dist/fec-worker-client.js.map +1 -1
  156. package/node_modules/@blockcast/fec-worker/dist/fec-worker-types.d.ts +95 -15
  157. package/node_modules/@blockcast/fec-worker/dist/fec-worker-types.d.ts.map +1 -1
  158. package/node_modules/@blockcast/fec-worker/dist/fec-worker.js +83 -47
  159. package/node_modules/@blockcast/fec-worker/dist/fec-worker.js.map +2 -2
  160. package/node_modules/@blockcast/fec-worker/dist/index.d.ts +1 -1
  161. package/node_modules/@blockcast/fec-worker/dist/index.d.ts.map +1 -1
  162. package/node_modules/@blockcast/fec-worker/dist/index.js.map +1 -1
  163. package/node_modules/@blockcast/fec-worker/src/fec-worker-client.ts +97 -26
  164. package/node_modules/@blockcast/fec-worker/src/fec-worker-types.ts +93 -11
  165. package/node_modules/@blockcast/fec-worker/src/fec-worker.ts +145 -70
  166. package/node_modules/@blockcast/fec-worker/src/index.ts +2 -0
  167. package/node_modules/@blockcast/mmt-base/README.md +1 -0
  168. package/node_modules/@blockcast/mmt-base/dist/index.d.ts +2 -0
  169. package/node_modules/@blockcast/mmt-base/dist/index.d.ts.map +1 -1
  170. package/node_modules/@blockcast/mmt-base/dist/index.js +2 -0
  171. package/node_modules/@blockcast/mmt-base/dist/index.js.map +1 -1
  172. package/node_modules/@blockcast/mmt-base/dist/mmtp.d.ts +58 -5
  173. package/node_modules/@blockcast/mmt-base/dist/mmtp.d.ts.map +1 -1
  174. package/node_modules/@blockcast/mmt-base/dist/mmtp.js +147 -41
  175. package/node_modules/@blockcast/mmt-base/dist/mmtp.js.map +1 -1
  176. package/node_modules/@blockcast/mmt-base/dist/net-constants.d.ts +18 -0
  177. package/node_modules/@blockcast/mmt-base/dist/net-constants.d.ts.map +1 -0
  178. package/node_modules/@blockcast/mmt-base/dist/net-constants.js +18 -0
  179. package/node_modules/@blockcast/mmt-base/dist/net-constants.js.map +1 -0
  180. package/node_modules/@blockcast/mmt-base/dist/ntp-short.d.ts +47 -0
  181. package/node_modules/@blockcast/mmt-base/dist/ntp-short.d.ts.map +1 -0
  182. package/node_modules/@blockcast/mmt-base/dist/ntp-short.js +54 -0
  183. package/node_modules/@blockcast/mmt-base/dist/ntp-short.js.map +1 -0
  184. package/node_modules/@blockcast/mmt-base/package.json +1 -1
  185. package/node_modules/@blockcast/mmt-base/src/index.ts +2 -0
  186. package/node_modules/@blockcast/mmt-base/src/mmtp.ts +164 -42
  187. package/node_modules/@blockcast/mmt-base/src/net-constants.ts +17 -0
  188. package/node_modules/@blockcast/mmt-base/src/ntp-short.ts +67 -0
  189. package/node_modules/@blockcast/mmt-fec/dist/block-manager.d.ts +3 -1
  190. package/node_modules/@blockcast/mmt-fec/dist/block-manager.d.ts.map +1 -1
  191. package/node_modules/@blockcast/mmt-fec/dist/block-manager.js +3 -1
  192. package/node_modules/@blockcast/mmt-fec/dist/block-manager.js.map +1 -1
  193. package/node_modules/@blockcast/mmt-fec/dist/fec-block-mapper.d.ts +2 -2
  194. package/node_modules/@blockcast/mmt-fec/dist/fec-block-mapper.js +2 -2
  195. package/node_modules/@blockcast/mmt-fec/dist/index.d.ts +1 -1
  196. package/node_modules/@blockcast/mmt-fec/dist/index.d.ts.map +1 -1
  197. package/node_modules/@blockcast/mmt-fec/dist/index.js +1 -1
  198. package/node_modules/@blockcast/mmt-fec/dist/index.js.map +1 -1
  199. package/node_modules/@blockcast/mmt-fec/dist/oti.d.ts +94 -10
  200. package/node_modules/@blockcast/mmt-fec/dist/oti.d.ts.map +1 -1
  201. package/node_modules/@blockcast/mmt-fec/dist/oti.js +135 -9
  202. package/node_modules/@blockcast/mmt-fec/dist/oti.js.map +1 -1
  203. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/README.md +1 -0
  204. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/index.d.ts +2 -0
  205. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/index.d.ts.map +1 -1
  206. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/index.js +2 -0
  207. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/index.js.map +1 -1
  208. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/mmtp.d.ts +58 -5
  209. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/mmtp.d.ts.map +1 -1
  210. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/mmtp.js +147 -41
  211. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/mmtp.js.map +1 -1
  212. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/net-constants.d.ts +18 -0
  213. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/net-constants.d.ts.map +1 -0
  214. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/net-constants.js +18 -0
  215. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/net-constants.js.map +1 -0
  216. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/ntp-short.d.ts +47 -0
  217. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/ntp-short.d.ts.map +1 -0
  218. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/ntp-short.js +54 -0
  219. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/ntp-short.js.map +1 -0
  220. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/package.json +1 -1
  221. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/src/index.ts +2 -0
  222. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/src/mmtp.ts +164 -42
  223. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/src/net-constants.ts +17 -0
  224. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/src/ntp-short.ts +67 -0
  225. package/node_modules/@blockcast/mmt-fec/src/block-manager.ts +3 -1
  226. package/node_modules/@blockcast/mmt-fec/src/fec-block-mapper.ts +2 -2
  227. package/node_modules/@blockcast/mmt-fec/src/index.ts +3 -0
  228. package/node_modules/@blockcast/mmt-fec/src/oti.ts +157 -13
  229. package/node_modules/@blockcast/mmt-manifest-verify/dist/verifier.d.ts +10 -1
  230. package/node_modules/@blockcast/mmt-manifest-verify/dist/verifier.d.ts.map +1 -1
  231. package/node_modules/@blockcast/mmt-manifest-verify/dist/verifier.js +40 -10
  232. package/node_modules/@blockcast/mmt-manifest-verify/dist/verifier.js.map +1 -1
  233. package/node_modules/@blockcast/mmt-manifest-verify/src/verifier.ts +41 -8
  234. package/package.json +19 -6
  235. package/schemas/catalog.schema.json +80 -28
  236. package/src/align.ts +6 -2
  237. package/src/block-policy.ts +903 -0
  238. package/src/carp-catalog.ts +192 -38
  239. package/src/catalog-fec-params.ts +378 -8
  240. package/src/catalog-network-source.ts +29 -4
  241. package/src/cmaf-block-assembler.ts +260 -229
  242. package/src/cmcd-producer.ts +5 -5
  243. package/src/conceal-timeline.ts +293 -0
  244. package/src/delivery-gap.ts +147 -0
  245. package/src/duplicate-ledger.ts +372 -0
  246. package/src/fec-manager.ts +2093 -326
  247. package/src/fec-state.ts +173 -0
  248. package/src/fec-worker-setup.ts +74 -6
  249. package/src/index.ts +123 -3
  250. package/src/iso-bmff-timing.ts +62 -7
  251. package/src/media-router.ts +54 -405
  252. package/src/mfu-reassembler.ts +136 -29
  253. package/src/mmtp-container-parser.ts +845 -67
  254. package/src/mmtp-decode-pipeline.ts +23 -6
  255. package/src/mmtp-router.ts +1778 -322
  256. package/src/mmtp-to-cmaf.ts +85 -112
  257. package/src/multicast-input.ts +375 -21
  258. package/src/multicast-manager.ts +1289 -137
  259. package/src/multicast-transport.ts +601 -61
  260. package/src/no-modules.ts +1 -0
  261. package/src/repair-fetch.ts +654 -154
  262. package/src/repair-layers.ts +490 -0
  263. package/src/track-pipeline-setup.ts +18 -2
  264. package/src/track-session.ts +161 -8
  265. package/src/wasm-mfu-reassembler.ts +40 -24
  266. package/test-vectors/catalog/README.md +46 -3
  267. package/test-vectors/catalog/negative/fec-enhancement-repair-removed.json +27 -0
  268. package/test-vectors/catalog/negative/fec-repair-layer-geometry.json +32 -0
  269. package/test-vectors/catalog/negative/fec-repair-layer-missing-symbols.json +30 -0
  270. package/test-vectors/catalog/negative/fec-repair-multiple-depends.json +31 -0
  271. package/test-vectors/catalog/negative/fec-repair-overlay-with-layer.json +33 -0
  272. package/test-vectors/catalog/negative/fec-repair-priority-out-of-band.json +23 -0
  273. package/test-vectors/catalog/negative/fec-repair-source-symbols-without-scope.json +32 -0
  274. package/test-vectors/catalog/negative/media-track-repair-layer.json +16 -0
  275. package/test-vectors/catalog/negative/multicast-network-source-empty.json +43 -0
  276. package/test-vectors/catalog/negative/multicast-packet-id-signaling.json +26 -0
  277. package/test-vectors/catalog/negative/raptorq-source-symbols-over-max.json +23 -0
  278. package/test-vectors/catalog/negative/repair-track-legacy-shape.json +2 -1
  279. package/test-vectors/catalog/positive/fec-layered-repair.json +104 -0
  280. package/test-vectors/catalog/positive/multicast-native-ssm-no-network-source.json +42 -0
  281. package/test-vectors/catalog-validation.json +478 -23
  282. package/test-vectors/source-contiguity-v1.json +131 -0
package/README.md CHANGED
@@ -65,6 +65,8 @@ The package exposes a wide subpath surface so consumers can tree-shake aggressiv
65
65
  | `./loc` | LOC FEC extension headers (`FecSourceInfo`, `AuthTag`) |
66
66
  | `./codec-utils` | H.264 helpers — Annex-B ↔ AVCC, SPS/PPS extraction, `mmtpTimestampToMicros`, ADTS wrap |
67
67
  | `./fec-manager` | `FECManager` — per-track FEC decoder lifecycle (sync WASM + Worker variants) |
68
+ | `./block-policy` | Keyframe-protected repair policy — `BlockClassifier`, `repairPriorityForClass`, `decideGap` |
69
+ | `./conceal-timeline` | Sink concealment — `videoConcealModeForCodec`, `HoldLastTimeline`, `AudioGapConcealer` |
68
70
  | `./mmtp-router` | `MmtpRouter` — packet routing with two-phase FEC feed |
69
71
  | `./multicast-transport` | `MulticastTransport` — `window.multicast` gateway `subscribe({ consume: 'bytes' })` wrapper |
70
72
  | `./multicast-manager` | `MulticastManager` — backwards-compat facade composing transport + router + FEC |
@@ -82,7 +84,7 @@ The package exposes a wide subpath surface so consumers can tree-shake aggressiv
82
84
  | `./phase14-catalog-override` | Test-harness catalog override hook (`globalThis.__PHASE14_CATALOG_OVERRIDE`) |
83
85
  | `./align` | Group-id derivation (`groupIdForTicks`, `groupIdFor`, `driftCheck`) — shared with Go sender + Rust relay |
84
86
  | `./mmtp-decode-pipeline` | `MmtpDecodePipeline` — unified MoQ + multicast decode path |
85
- | `./mmtp-to-cmaf` | `MmtpToCmaf` + standalone segment builders (`encodeMultiSampleSegment`, `buildVideoInitSegment`, `generateGreenFillNAL`, `generateSilenceFrame`) |
87
+ | `./mmtp-to-cmaf` | `MmtpToCmaf` + standalone segment builders (`encodeMultiSampleSegment`, `buildVideoInitSegment`, `generateSilenceFrame`) |
86
88
  | `./track-pipeline-setup` | `setupTrackPipeline` — shared wiring for both reference players |
87
89
  | `./track-session` | `TrackSession` + `SessionManager` — per-track state machine (Patent Claim 9) |
88
90
 
@@ -190,14 +192,21 @@ interface FecCatalogConfig {
190
192
  }
191
193
 
192
194
  interface MulticastEndpoint {
193
- protocol: "ssm" | "asm" | "amt"
195
+ protocol: "ssm" | "asm" | "amt" | "atsc3"
194
196
  sourceAddress?: string // REQUIRED for SSM
195
197
  groupAddress: string
196
198
  port: number
197
199
  tracks: string[]
198
- relayAddress?: string // REQUIRED for AMT
200
+ // relayAddress?: string — deprecated, read-only (see Breaking, below)
199
201
  }
200
202
 
203
+ // Relay/tuning discovery is catalog-level, not per-endpoint:
204
+ // multicast.networkSource: [{ type: "amt", discovery: "driad" | "manual",
205
+ // relay?: string, port?: number }]
206
+ // `relay` is REQUIRED for discovery: "manual"; "driad" resolves it at runtime.
207
+ // An endpoint-level `relayAddress` is retired — the schema and moq-rs both
208
+ // reject it.
209
+
201
210
  interface TrackTimingConfig {
202
211
  groupDurationMs: number // REQUIRED, positive integer
203
212
  groupDurationTicks?: number // override when ms × timescale isn't integer
@@ -209,6 +218,45 @@ function resolveGroupDurationTicks(timing: TrackTimingConfig, timescale: number)
209
218
  function validateSwitchingSetTiming(...): { ok: true } | { ok: false; ... }
210
219
  ```
211
220
 
221
+ #### Breaking: multicast emitter output (BLO-37834)
222
+
223
+ `multicastFromConfig` / `multicastFromEnv` no longer emit
224
+ `endpoints[].relayAddress` or `amtFallback`. Both are lifted into catalog-level
225
+ `multicast.networkSource` (`{type: "amt", discovery, relay?}`), and a native
226
+ multicast catalog omits `networkSource` entirely (BLO-37926). A reader that
227
+ still looks for `relayAddress` or `amtFallback` reads `undefined`: read
228
+ `networkSource` instead (`catalogAmtNetworkSource` in `./catalog-network-source`
229
+ resolves a dialable AMT relay from it, skipping `discovery: "driad"` entries
230
+ whose `relay` is a DNS name for a runtime lookup rather than an address), and
231
+ keep accepting `relayAddress` from catalogs published before this change until
232
+ every emitter is on it.
233
+
234
+ Inputs are unchanged: `relayAddress` and `amtFallback` are still accepted and
235
+ lifted. An input relay that would not survive into the emitted `networkSource`
236
+ (two endpoints with different relays, one overridden by
237
+ `amtFallback.relayDns` or an explicit `networkSource`, or one matched only by a
238
+ `discovery: "driad"` entry, whose relay is a runtime discovery hint) now throws
239
+ `conflicting_amt_relay` instead of being dropped. For that last case — wanting
240
+ DRIAD *and* a named relay — emit both entries, `[{type: "amt", discovery:
241
+ "manual", relay}, {type: "amt", discovery: "driad"}]`: readers join the first
242
+ non-DRIAD AMT entry, so the manual one carries the relay while the DRIAD one
243
+ keeps the discovery request. An `atsc3` endpoint now
244
+ requires a `{type: "atsc3", …}` entry and throws `atsc3_requires_tuning`
245
+ without one: that entry carries the frequency, PLP, service id and SLS URI,
246
+ which is the only tuning information an ATSC3 receiver has, and the schema
247
+ cannot enforce it because `networkSource` is optional there (a native join has
248
+ none). `version`, `validUntil` and `enabled` are still emitted and still
249
+ outside `$defs/multicast` (the BLO-3323 freshness model, tracked separately).
250
+
251
+ `MulticastInputErrorKind` gains two members, `atsc3_requires_tuning` and
252
+ `invalid_endpoints`. The union is exported, so a consumer that `switch`es on it
253
+ exhaustively needs the two new arms to keep compiling.
254
+
255
+ The package version stays `0.1.0`: consumers pin a libmmt commit (`file:` or the
256
+ per-commit `0.1.0-main.<sha>` prerelease from `stamp-prerelease.mjs`), and a
257
+ `0.2.0` would fall outside the `^0.1.0` range that siblings such as
258
+ `mmt-render` declare.
259
+
212
260
  #### Track-role convention (Phase 14)
213
261
 
214
262
  This is **project-specific**, not in any IETF draft yet. The container's switching-set parser derives its internal role from the last `/`-segment of a track name. MMTP catalogs may also carry the publisher-owned `trackRole` dispatch tag; the canonical schema validates it as a non-empty string. `parseTrackRole` remains the name-based helper for base/delta/repair switching sets (`carp-catalog.ts:41-79`):
@@ -263,6 +311,47 @@ class FECManager {
263
311
 
264
312
  `interleaveDepthMs` is the full FEC block span in **milliseconds** per CARP §5.1, valid range 33 ms (interactive / gaming, `MIN_INTERLEAVE_DEPTH_MS`) to 10 000 ms (broadcast ATSC 3.0). `configure()` warns if a positive value below `MIN_INTERLEAVE_DEPTH_MS` (33) is passed — that is almost certainly a raw frame count, not milliseconds — and such sub-floor values never drive the block timeout (the established timeout is preserved instead of collapsing to ~0).
265
313
 
314
+ ### Keyframe-protected repair (`./block-policy`, `./conceal-timeline`)
315
+
316
+ Every FEC block still gets in-band FEC decode. On top of that:
317
+
318
+ - **Classification.** Each block is `keyframe | delta | unknown | audio`, from
319
+ first-arrival network source evidence only (MMTP R bit, MPU number, MFU
320
+ DU-header sample position) against catalog geometry (framerate,
321
+ `interleaveDepthMs`, keyframe interval). Geometry that cannot be derived
322
+ exactly, or any violated invariant, runs the track `all-protected` (every
323
+ video block `unknown`). Vanished blocks are materialised from SBN evidence.
324
+ A frame split across ADJACENT blocks (a K-close) is expected; only source
325
+ packets whose authored order and SBN order disagree (a round-robin
326
+ interleave) trip `block-not-contiguous`. Pass the catalog track name to
327
+ `configureTrackFec(packetId, config, catalogTrackName)` so ABR rungs that
328
+ share a packetId each classify with their own framerate and GOP; a rung on
329
+ such a packetId without its own geometry runs `stream-geometry-absent`.
330
+ - **Repair priority.** `wireRepairFetch` dispatches P0 (`keyframe`/`unknown`)
331
+ and P1 (`audio`) immediately; P2 (`delta`) only while no P0/P1 is queued or
332
+ in flight, earliest deadline first, preempted by any new P0/P1. No timers.
333
+ - **FETCH priority (draft-ramadan-moq-fec §11.4.5).** The transport gets one
334
+ `RepairFetchRequest { repairTrack, sbn, packetId, signal,
335
+ subscriberPriority }` and puts `subscriberPriority` on the FETCH
336
+ (`undefined` = omit the parameter; the publisher then uses 128,
337
+ draft-ietf-moq-transport-16 §9.2.2.3). P0 requests carry
338
+ `min(sourcePriority, 191)`; P1/P2 carry `max(repairPriority, 192)`. Both
339
+ priorities come from `ResolveRepairTarget` (`catalogRepairTarget` reads
340
+ them from a catalog). A missing source priority omits the parameter; a
341
+ missing repair priority declines the delta/audio FETCH; each emits a
342
+ `repair-priority` lifecycle event. Abort MUST send FETCH_CANCEL, from the
343
+ abort handler.
344
+ - **Stall vs conceal.** FEC tracks release frames in authored order; a frame
345
+ after a gap waits until every block that could hold the missing frames
346
+ resolved. When media was lost, the frame carries `gapBefore`
347
+ (`decideGap`): video stalls only when a RAP may be inside the gap; audio
348
+ never stalls. The same object is the manager's `gap` lifecycle event.
349
+ - **Concealment** is hold-last-good-frame (`HoldLastTimeline`) plus exact
350
+ audio sizing (`AudioGapConcealer`). No bitstream is ever synthesised.
351
+
352
+ Typed evidence is `RepairLifecycleEvent` version 2 and
353
+ `MulticastManager.getBlockPolicyStats()`.
354
+
266
355
  This package depends on the FEC primitives from:
267
356
  - **`@blockcast/mmt-fec`** — RaptorQ WASM decoder + OTI codec + `computeFecTimeout`
268
357
  - **`@blockcast/fec-worker`** — Web Worker host, `FecWorkerClient`, `FecTrackStats`
@@ -293,7 +382,6 @@ Standalone helpers exported from the same module:
293
382
  | `encodeMultiSampleSegment(...)` | Multi-sample moof+mdat (one per FEC block — used by `CmafBlockAssembler`) |
294
383
  | `buildVideoInitSegment(avcC, w, h)` | ftyp + moov for AVC |
295
384
  | `buildAudioInitSegment(sampleRate, channels, asc?, timescale?)` | ftyp + moov for AAC / Opus |
296
- | `generateGreenFillNAL(avcC, w, h)` | Green-fill NAL when a FEC block fails (loss visible) |
297
385
  | `generateSilenceFrame(codec, sampleRate, channels)` | Audio silence frame for FEC-failed audio blocks |
298
386
 
299
387
  Per-track timestamp rebase (`#videoBaseTs` / `#audioBaseTs` at `mmtp-to-cmaf.ts:550-562`) prevents the negative-uint64 `tfdt` bug that kills Chromium MediaSource (`CHUNK_DEMUXER_ERROR_APPEND_FAILED`).
@@ -322,7 +410,6 @@ Vendor keys emitted (BLO-1044):
322
410
  | `x-bc-fec-algo` | FEC config | `none` / `raptorq` / `reed-solomon` |
323
411
  | `x-bc-fec-k`, `x-bc-fec-p` | FEC config | source / repair counts |
324
412
  | `x-bc-fec-recovery` | `recoveryRate` | Block-level recovery, 4 decimal places |
325
- | `x-bc-scu` | green-fill count | Omitted when 0 |
326
413
  | `x-bc-alta-verified`, `x-bc-alta-failures` | ALTA stats | Omitted when ALTA disabled (D-09) |
327
414
  | `x-bc-transport` | `TransportSnapshot.transport` | `native` / `amt` / `moq` |
328
415
  | `x-bc-multicast-group`, `x-bc-multicast-source`, `x-bc-amt-relay` | endpoint | Omitted when null |
package/dist/align.d.ts CHANGED
@@ -101,8 +101,12 @@ export declare function groupIdForTicks(ticks: number, groupDurationTicks: numbe
101
101
  * the quantized duration under-counts and `expected` slips a whole group.
102
102
  * See the divergence fixture in `__tests__/align.test.ts`.
103
103
  *
104
- * No non-test caller remains in this package: `MediaRouter` now takes
105
- * `GroupBoundaryTicks` (PCL-2211). Removal is tracked by BLO-21176.
104
+ * No non-test caller remains in this package: `MediaRouter` took tick-space
105
+ * inputs from PCL-2211, and its group-alignment observer was then removed
106
+ * outright as unwired telemetry (BLO-34141). Removal of this wrapper is
107
+ * tracked by BLO-21176; the exported `groupIdForTicks` / `driftCheck` stay
108
+ * regardless — they are the formula shared with the Go sender and the Rust
109
+ * relay, not internals of the deleted observer.
106
110
  */
107
111
  export declare function groupIdFor(mediaTimeSec: Seconds | number, groupDurationSec: number): number;
108
112
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"align.d.ts","sourceRoot":"","sources":["../src/align.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAEH;;;;;GAKG;AACH,MAAM,MAAM,OAAO,GAAG,MAAM,GAAG;IAAE,QAAQ,CAAC,OAAO,EAAE,SAAS,CAAA;CAAE,CAAA;AAE9D;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAU1E;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAK5C;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,kBAAkB,EAAE,MAAM,GAAG,MAAM,CAajF;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,UAAU,CACxB,YAAY,EAAE,OAAO,GAAG,MAAM,EAC9B,gBAAgB,EAAE,MAAM,GACvB,MAAM,CA+BR;AAED;;;;;;GAMG;AACH,MAAM,MAAM,gBAAgB,GACxB;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,WAAW,EAAE,CAAC,CAAA;CAAE,GACrD;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;CAAE,CAAA;AAE9D;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,gBAAgB,CAS7E;AAED,MAAM,MAAM,oBAAoB,GAC5B;IAAE,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAA;CAAE,GACjC;IACE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;IACtB,QAAQ,CAAC,2BAA2B,EAAE,MAAM,CAAA;IAC5C,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;CAC9B,GACD;IAAE,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAA;AAE3D;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,oBAAoB,GAAG,MAAM,CAgBlE"}
1
+ {"version":3,"file":"align.d.ts","sourceRoot":"","sources":["../src/align.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAEH;;;;;GAKG;AACH,MAAM,MAAM,OAAO,GAAG,MAAM,GAAG;IAAE,QAAQ,CAAC,OAAO,EAAE,SAAS,CAAA;CAAE,CAAA;AAE9D;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAU1E;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAK5C;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,kBAAkB,EAAE,MAAM,GAAG,MAAM,CAajF;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,UAAU,CACxB,YAAY,EAAE,OAAO,GAAG,MAAM,EAC9B,gBAAgB,EAAE,MAAM,GACvB,MAAM,CA+BR;AAED;;;;;;GAMG;AACH,MAAM,MAAM,gBAAgB,GACxB;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,WAAW,EAAE,CAAC,CAAA;CAAE,GACrD;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;CAAE,CAAA;AAE9D;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,gBAAgB,CAS7E;AAED,MAAM,MAAM,oBAAoB,GAC5B;IAAE,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAA;CAAE,GACjC;IACE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;IACtB,QAAQ,CAAC,2BAA2B,EAAE,MAAM,CAAA;IAC5C,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;CAC9B,GACD;IAAE,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAA;AAE3D;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,oBAAoB,GAAG,MAAM,CAgBlE"}
package/dist/align.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/align.ts"],
4
- "sourcesContent": ["/**\n * Shared group-alignment primitive for MMT over MoQ switching sets.\n *\n * Pure math \u2014 no MMT wire parsing, no CMCD, no I/O. Callers wire the output\n * into their own drift-observation plumbing (see media-router.ts for the\n * libmmt wiring).\n *\n * \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n * Contract (integer-ticks form, authoritative):\n *\n * groupId = ticks < 0 ? 0 : floor(ticks / groupDurationTicks)\n *\n * Where `ticks` is a media timestamp expressed at the catalog's `timescale`\n * (Hz), and `groupDurationTicks = groupDurationSec * timescale`.\n *\n * Integer math means there is no epsilon and no float rounding to reason\n * about. The seconds-form wrapper `groupIdFor(Seconds, seconds)` exists for\n * legacy callers but converts to ticks internally.\n *\n * Every sender/receiver that emits or consumes MoQ groups from an MMT or\n * CMAF switching set MUST derive groupId from this formula so that base,\n * delta, and repair tracks agree on the group boundary for a given media\n * timestamp across languages and processes.\n *\n * Test vectors that exercise the contract live at\n * `packages/container/test-vectors/align.json`; the Go and Rust siblings\n * load the same fixture so cross-language agreement is executable, not\n * prose. **Formula changes require updating the fixture AND all three\n * sibling implementations in the same release** \u2014 divergence between\n * sender/relay/receiver is worse than any formula change.\n *\n * Canonical implementations:\n * Go: Blockcast/multicast `cmd/caddy/sender/group_align.go`\n * (commit 104eeff) \u2014 sender-side groupId stamping on egress.\n * Rust: moqtail/moqtail `apps/relay/src/server/abr.rs`\n * (commit 39accbf; experimental branch, may be rebased \u2014 rely\n * on the SHA, not the branch name) \u2014 relay-side check.\n * TS: THIS FILE (libmmt) \u2014 consumed by moq-lib + moqtail-ts via\n * @blockcast/mmt-container.\n * \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n */\n\n/**\n * Branded seconds type. Construct only via `secondsFromTicks(ticks, timescale)`\n * or `asSeconds(n)` (the latter exists solely for tests and bring-up). The\n * brand prevents accidentally passing a raw tick count \u2014 or a Date.now()\n * millisecond \u2014 to an API that expects a media-time in seconds.\n */\nexport type Seconds = number & { readonly __brand: 'Seconds' }\n\n/**\n * Explicit conversion constructor: ticks at `timescale` Hz \u2192 Seconds.\n *\n * Call this at the wire/catalog boundary so the brand flows with the value\n * through the rest of the system. Throws on non-finite inputs or\n * non-positive timescale (CLAUDE.md \u00A7Architectural Principles: error on\n * bad init).\n */\nexport function secondsFromTicks(ticks: number, timescale: number): Seconds {\n if (!Number.isFinite(ticks)) {\n throw new Error(`[align] secondsFromTicks: ticks must be finite, got ${ticks}`)\n }\n if (!Number.isFinite(timescale) || timescale <= 0) {\n throw new Error(\n `[align] secondsFromTicks: timescale must be positive and finite, got ${timescale}`,\n )\n }\n return (ticks / timescale) as Seconds\n}\n\n/**\n * Escape hatch for tests / bring-up code that holds a raw seconds number\n * (e.g. a catalog field that ships as float seconds). Production code\n * should prefer `secondsFromTicks` so the conversion site is explicit.\n */\nexport function asSeconds(n: number): Seconds {\n if (!Number.isFinite(n)) {\n throw new Error(`[align] asSeconds: value must be finite, got ${n}`)\n }\n return n as Seconds\n}\n\n/**\n * Deterministic groupId from integer ticks. Authoritative form of the\n * contract (see module docstring).\n *\n * @param ticks \u2014 media presentation time in ticks (integer).\n * Negative values clamp to group 0 (matches\n * the Go sibling's base-clamp).\n * @param groupDurationTicks \u2014 group duration in ticks (positive integer).\n * @returns non-negative integer groupId.\n *\n * Throws on non-integer/non-finite inputs or non-positive duration. These\n * are init-time errors, not packet-hot-path errors \u2014 callers ingesting\n * wire data should validate once at catalog load, not once per packet.\n */\nexport function groupIdForTicks(ticks: number, groupDurationTicks: number): number {\n if (!Number.isInteger(ticks)) {\n throw new Error(\n `[align] groupIdForTicks: ticks must be an integer, got ${ticks}`,\n )\n }\n if (!Number.isInteger(groupDurationTicks) || groupDurationTicks <= 0) {\n throw new Error(\n `[align] groupIdForTicks: groupDurationTicks must be a positive integer, got ${groupDurationTicks}`,\n )\n }\n if (ticks < 0) return 0\n return Math.floor(ticks / groupDurationTicks)\n}\n\n/**\n * Seconds-form wrapper around `groupIdForTicks`. Kept for backwards\n * compatibility with existing callers that hold `Seconds` (or a raw\n * `number` that semantically represents seconds).\n *\n * Internally converts to ticks using a synthetic 1e9 Hz timescale so the\n * integer-math contract is the single source of truth. The `| number`\n * branch of the parameter type is a migration ramp \u2014 new code should\n * pass `Seconds` explicitly (via `secondsFromTicks` or `asSeconds`).\n *\n * @param mediaTimeSec \u2014 finite number of seconds; negatives clamp to 0.\n * @param groupDurationSec \u2014 positive finite seconds.\n *\n * @deprecated Use `groupIdForTicks(ticks, groupDurationTicks)` with\n * tick-space inputs from the catalog. The seconds wrapper rounds to the\n * nearest nanosecond and throws above ~104 days of media time; the\n * integer-ticks form has no rounding and a much wider safe range.\n *\n * The rounding is not benign: when `groupDurationTicks / timescale` is not\n * ns-exact (33333@90k, 1001@30k, 1024@44.1k \u2014 all real catalog cadences)\n * the quantized duration under-counts and `expected` slips a whole group.\n * See the divergence fixture in `__tests__/align.test.ts`.\n *\n * No non-test caller remains in this package: `MediaRouter` now takes\n * `GroupBoundaryTicks` (PCL-2211). Removal is tracked by BLO-21176.\n */\nexport function groupIdFor(\n mediaTimeSec: Seconds | number,\n groupDurationSec: number,\n): number {\n if (!Number.isFinite(mediaTimeSec as number)) {\n throw new Error(\n `[align] groupIdFor: mediaTimeSec must be a finite number, got ${mediaTimeSec}`,\n )\n }\n if (!Number.isFinite(groupDurationSec) || groupDurationSec <= 0) {\n throw new Error(\n `[align] groupIdFor: groupDurationSec must be a positive finite number, got ${groupDurationSec}`,\n )\n }\n if ((mediaTimeSec as number) < 0) return 0\n // 1e9 Hz keeps both operands inside Number.MAX_SAFE_INTEGER (2^53-1)\n // for ~104 days of media time. Beyond that, Math.round returns a\n // non-safe-integer float that Number.isInteger still accepts \u2014 throw\n // explicitly so the caller picks a narrower timescale rather than\n // silently quantizing.\n const TS = 1_000_000_000\n const ticks = Math.round((mediaTimeSec as number) * TS)\n const durTicks = Math.round(groupDurationSec * TS)\n if (durTicks <= 0) {\n throw new Error(\n `[align] groupIdFor: groupDurationSec=${groupDurationSec} rounds to <1 tick at 1e9 Hz; pass groupIdForTicks directly for sub-nanosecond durations`,\n )\n }\n if (!Number.isSafeInteger(ticks)) {\n throw new Error(\n `[align] groupIdFor: mediaTimeSec=${mediaTimeSec} overflows safe-integer ticks at 1e9 Hz (>~104 days); use groupIdForTicks with a narrower timescale`,\n )\n }\n return groupIdForTicks(ticks, durTicks)\n}\n\n/**\n * Result of a drift check between expected and actual groupIds.\n *\n * Discriminated on `kind` so callers must handle both outcomes explicitly\n * (the previous `{ drifted: boolean, deltaGroups: number }` shape let a\n * drifted result slip through with a silent `if (result.drifted)` gate).\n */\nexport type DriftCheckResult =\n | { readonly kind: 'aligned'; readonly deltaGroups: 0 }\n | { readonly kind: 'drifted'; readonly deltaGroups: number }\n\n/**\n * Compare a packet-stamped groupId against the formula-derived expectation.\n *\n * Pure observer \u2014 does not throw on drift, does not mutate anything.\n * Callers decide how to react (e.g. bump a CMCD counter, log, ignore).\n *\n * @param expected \u2014 groupId as derived via `groupIdForTicks` or `groupIdFor`.\n * @param actual \u2014 groupId as stamped on the incoming packet/group.\n */\nexport function driftCheck(expected: number, actual: number): DriftCheckResult {\n if (!Number.isInteger(expected) || !Number.isInteger(actual)) {\n throw new Error(\n `[align] driftCheck: both arguments must be integer groupIds, got expected=${expected} actual=${actual}`,\n )\n }\n const deltaGroups = actual - expected\n if (deltaGroups === 0) return { kind: 'aligned', deltaGroups: 0 }\n return { kind: 'drifted', deltaGroups }\n}\n\nexport type MmtpSubgroupKeyInput =\n | { readonly kind: 'mpu-metadata' }\n | {\n readonly kind: 'timed'\n readonly movieFragmentSequenceNumber: number\n readonly sampleNumber: number\n }\n | { readonly kind: 'non-timed'; readonly itemId: number }\n\n/**\n * Canonical, domain-separated key for MMTP payloads sharing one MoQ subgroup.\n * This is a map/storage key, not the scalar MoQ subgroup ID. MPU metadata is\n * carried in subgroup 0; media identity remains explicit and collision-free.\n */\nexport function subgroupKeyFor(input: MmtpSubgroupKeyInput): string {\n if (input.kind === 'mpu-metadata') return 'metadata:0'\n\n const values =\n input.kind === 'timed'\n ? [input.movieFragmentSequenceNumber, input.sampleNumber]\n : [input.itemId]\n for (const value of values) {\n if (!Number.isInteger(value) || value < 0 || value > 0xffff_ffff) {\n throw new Error(`[align] subgroup key coordinates must be uint32, got ${value}`)\n }\n }\n\n return input.kind === 'timed'\n ? `timed:${input.movieFragmentSequenceNumber}:${input.sampleNumber}`\n : `non-timed:${input.itemId}`\n}\n"],
5
- "mappings": ";AA0DO,SAAS,iBAAiB,OAAe,WAA4B;AAC1E,MAAI,CAAC,OAAO,SAAS,KAAK,GAAG;AAC3B,UAAM,IAAI,MAAM,uDAAuD,KAAK,EAAE;AAAA,EAChF;AACA,MAAI,CAAC,OAAO,SAAS,SAAS,KAAK,aAAa,GAAG;AACjD,UAAM,IAAI;AAAA,MACR,wEAAwE,SAAS;AAAA,IACnF;AAAA,EACF;AACA,SAAQ,QAAQ;AAClB;AAOO,SAAS,UAAU,GAAoB;AAC5C,MAAI,CAAC,OAAO,SAAS,CAAC,GAAG;AACvB,UAAM,IAAI,MAAM,gDAAgD,CAAC,EAAE;AAAA,EACrE;AACA,SAAO;AACT;AAgBO,SAAS,gBAAgB,OAAe,oBAAoC;AACjF,MAAI,CAAC,OAAO,UAAU,KAAK,GAAG;AAC5B,UAAM,IAAI;AAAA,MACR,0DAA0D,KAAK;AAAA,IACjE;AAAA,EACF;AACA,MAAI,CAAC,OAAO,UAAU,kBAAkB,KAAK,sBAAsB,GAAG;AACpE,UAAM,IAAI;AAAA,MACR,+EAA+E,kBAAkB;AAAA,IACnG;AAAA,EACF;AACA,MAAI,QAAQ,EAAG,QAAO;AACtB,SAAO,KAAK,MAAM,QAAQ,kBAAkB;AAC9C;AA4BO,SAAS,WACd,cACA,kBACQ;AACR,MAAI,CAAC,OAAO,SAAS,YAAsB,GAAG;AAC5C,UAAM,IAAI;AAAA,MACR,iEAAiE,YAAY;AAAA,IAC/E;AAAA,EACF;AACA,MAAI,CAAC,OAAO,SAAS,gBAAgB,KAAK,oBAAoB,GAAG;AAC/D,UAAM,IAAI;AAAA,MACR,8EAA8E,gBAAgB;AAAA,IAChG;AAAA,EACF;AACA,MAAK,eAA0B,EAAG,QAAO;AAMzC,QAAM,KAAK;AACX,QAAM,QAAQ,KAAK,MAAO,eAA0B,EAAE;AACtD,QAAM,WAAW,KAAK,MAAM,mBAAmB,EAAE;AACjD,MAAI,YAAY,GAAG;AACjB,UAAM,IAAI;AAAA,MACR,wCAAwC,gBAAgB;AAAA,IAC1D;AAAA,EACF;AACA,MAAI,CAAC,OAAO,cAAc,KAAK,GAAG;AAChC,UAAM,IAAI;AAAA,MACR,oCAAoC,YAAY;AAAA,IAClD;AAAA,EACF;AACA,SAAO,gBAAgB,OAAO,QAAQ;AACxC;AAsBO,SAAS,WAAW,UAAkB,QAAkC;AAC7E,MAAI,CAAC,OAAO,UAAU,QAAQ,KAAK,CAAC,OAAO,UAAU,MAAM,GAAG;AAC5D,UAAM,IAAI;AAAA,MACR,6EAA6E,QAAQ,WAAW,MAAM;AAAA,IACxG;AAAA,EACF;AACA,QAAM,cAAc,SAAS;AAC7B,MAAI,gBAAgB,EAAG,QAAO,EAAE,MAAM,WAAW,aAAa,EAAE;AAChE,SAAO,EAAE,MAAM,WAAW,YAAY;AACxC;AAgBO,SAAS,eAAe,OAAqC;AAClE,MAAI,MAAM,SAAS,eAAgB,QAAO;AAE1C,QAAM,SACJ,MAAM,SAAS,UACX,CAAC,MAAM,6BAA6B,MAAM,YAAY,IACtD,CAAC,MAAM,MAAM;AACnB,aAAW,SAAS,QAAQ;AAC1B,QAAI,CAAC,OAAO,UAAU,KAAK,KAAK,QAAQ,KAAK,QAAQ,YAAa;AAChE,YAAM,IAAI,MAAM,wDAAwD,KAAK,EAAE;AAAA,IACjF;AAAA,EACF;AAEA,SAAO,MAAM,SAAS,UAClB,SAAS,MAAM,2BAA2B,IAAI,MAAM,YAAY,KAChE,aAAa,MAAM,MAAM;AAC/B;",
4
+ "sourcesContent": ["/**\n * Shared group-alignment primitive for MMT over MoQ switching sets.\n *\n * Pure math \u2014 no MMT wire parsing, no CMCD, no I/O. Callers wire the output\n * into their own drift-observation plumbing (see media-router.ts for the\n * libmmt wiring).\n *\n * \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n * Contract (integer-ticks form, authoritative):\n *\n * groupId = ticks < 0 ? 0 : floor(ticks / groupDurationTicks)\n *\n * Where `ticks` is a media timestamp expressed at the catalog's `timescale`\n * (Hz), and `groupDurationTicks = groupDurationSec * timescale`.\n *\n * Integer math means there is no epsilon and no float rounding to reason\n * about. The seconds-form wrapper `groupIdFor(Seconds, seconds)` exists for\n * legacy callers but converts to ticks internally.\n *\n * Every sender/receiver that emits or consumes MoQ groups from an MMT or\n * CMAF switching set MUST derive groupId from this formula so that base,\n * delta, and repair tracks agree on the group boundary for a given media\n * timestamp across languages and processes.\n *\n * Test vectors that exercise the contract live at\n * `packages/container/test-vectors/align.json`; the Go and Rust siblings\n * load the same fixture so cross-language agreement is executable, not\n * prose. **Formula changes require updating the fixture AND all three\n * sibling implementations in the same release** \u2014 divergence between\n * sender/relay/receiver is worse than any formula change.\n *\n * Canonical implementations:\n * Go: Blockcast/multicast `cmd/caddy/sender/group_align.go`\n * (commit 104eeff) \u2014 sender-side groupId stamping on egress.\n * Rust: moqtail/moqtail `apps/relay/src/server/abr.rs`\n * (commit 39accbf; experimental branch, may be rebased \u2014 rely\n * on the SHA, not the branch name) \u2014 relay-side check.\n * TS: THIS FILE (libmmt) \u2014 consumed by moq-lib + moqtail-ts via\n * @blockcast/mmt-container.\n * \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n */\n\n/**\n * Branded seconds type. Construct only via `secondsFromTicks(ticks, timescale)`\n * or `asSeconds(n)` (the latter exists solely for tests and bring-up). The\n * brand prevents accidentally passing a raw tick count \u2014 or a Date.now()\n * millisecond \u2014 to an API that expects a media-time in seconds.\n */\nexport type Seconds = number & { readonly __brand: 'Seconds' }\n\n/**\n * Explicit conversion constructor: ticks at `timescale` Hz \u2192 Seconds.\n *\n * Call this at the wire/catalog boundary so the brand flows with the value\n * through the rest of the system. Throws on non-finite inputs or\n * non-positive timescale (CLAUDE.md \u00A7Architectural Principles: error on\n * bad init).\n */\nexport function secondsFromTicks(ticks: number, timescale: number): Seconds {\n if (!Number.isFinite(ticks)) {\n throw new Error(`[align] secondsFromTicks: ticks must be finite, got ${ticks}`)\n }\n if (!Number.isFinite(timescale) || timescale <= 0) {\n throw new Error(\n `[align] secondsFromTicks: timescale must be positive and finite, got ${timescale}`,\n )\n }\n return (ticks / timescale) as Seconds\n}\n\n/**\n * Escape hatch for tests / bring-up code that holds a raw seconds number\n * (e.g. a catalog field that ships as float seconds). Production code\n * should prefer `secondsFromTicks` so the conversion site is explicit.\n */\nexport function asSeconds(n: number): Seconds {\n if (!Number.isFinite(n)) {\n throw new Error(`[align] asSeconds: value must be finite, got ${n}`)\n }\n return n as Seconds\n}\n\n/**\n * Deterministic groupId from integer ticks. Authoritative form of the\n * contract (see module docstring).\n *\n * @param ticks \u2014 media presentation time in ticks (integer).\n * Negative values clamp to group 0 (matches\n * the Go sibling's base-clamp).\n * @param groupDurationTicks \u2014 group duration in ticks (positive integer).\n * @returns non-negative integer groupId.\n *\n * Throws on non-integer/non-finite inputs or non-positive duration. These\n * are init-time errors, not packet-hot-path errors \u2014 callers ingesting\n * wire data should validate once at catalog load, not once per packet.\n */\nexport function groupIdForTicks(ticks: number, groupDurationTicks: number): number {\n if (!Number.isInteger(ticks)) {\n throw new Error(\n `[align] groupIdForTicks: ticks must be an integer, got ${ticks}`,\n )\n }\n if (!Number.isInteger(groupDurationTicks) || groupDurationTicks <= 0) {\n throw new Error(\n `[align] groupIdForTicks: groupDurationTicks must be a positive integer, got ${groupDurationTicks}`,\n )\n }\n if (ticks < 0) return 0\n return Math.floor(ticks / groupDurationTicks)\n}\n\n/**\n * Seconds-form wrapper around `groupIdForTicks`. Kept for backwards\n * compatibility with existing callers that hold `Seconds` (or a raw\n * `number` that semantically represents seconds).\n *\n * Internally converts to ticks using a synthetic 1e9 Hz timescale so the\n * integer-math contract is the single source of truth. The `| number`\n * branch of the parameter type is a migration ramp \u2014 new code should\n * pass `Seconds` explicitly (via `secondsFromTicks` or `asSeconds`).\n *\n * @param mediaTimeSec \u2014 finite number of seconds; negatives clamp to 0.\n * @param groupDurationSec \u2014 positive finite seconds.\n *\n * @deprecated Use `groupIdForTicks(ticks, groupDurationTicks)` with\n * tick-space inputs from the catalog. The seconds wrapper rounds to the\n * nearest nanosecond and throws above ~104 days of media time; the\n * integer-ticks form has no rounding and a much wider safe range.\n *\n * The rounding is not benign: when `groupDurationTicks / timescale` is not\n * ns-exact (33333@90k, 1001@30k, 1024@44.1k \u2014 all real catalog cadences)\n * the quantized duration under-counts and `expected` slips a whole group.\n * See the divergence fixture in `__tests__/align.test.ts`.\n *\n * No non-test caller remains in this package: `MediaRouter` took tick-space\n * inputs from PCL-2211, and its group-alignment observer was then removed\n * outright as unwired telemetry (BLO-34141). Removal of this wrapper is\n * tracked by BLO-21176; the exported `groupIdForTicks` / `driftCheck` stay\n * regardless \u2014 they are the formula shared with the Go sender and the Rust\n * relay, not internals of the deleted observer.\n */\nexport function groupIdFor(\n mediaTimeSec: Seconds | number,\n groupDurationSec: number,\n): number {\n if (!Number.isFinite(mediaTimeSec as number)) {\n throw new Error(\n `[align] groupIdFor: mediaTimeSec must be a finite number, got ${mediaTimeSec}`,\n )\n }\n if (!Number.isFinite(groupDurationSec) || groupDurationSec <= 0) {\n throw new Error(\n `[align] groupIdFor: groupDurationSec must be a positive finite number, got ${groupDurationSec}`,\n )\n }\n if ((mediaTimeSec as number) < 0) return 0\n // 1e9 Hz keeps both operands inside Number.MAX_SAFE_INTEGER (2^53-1)\n // for ~104 days of media time. Beyond that, Math.round returns a\n // non-safe-integer float that Number.isInteger still accepts \u2014 throw\n // explicitly so the caller picks a narrower timescale rather than\n // silently quantizing.\n const TS = 1_000_000_000\n const ticks = Math.round((mediaTimeSec as number) * TS)\n const durTicks = Math.round(groupDurationSec * TS)\n if (durTicks <= 0) {\n throw new Error(\n `[align] groupIdFor: groupDurationSec=${groupDurationSec} rounds to <1 tick at 1e9 Hz; pass groupIdForTicks directly for sub-nanosecond durations`,\n )\n }\n if (!Number.isSafeInteger(ticks)) {\n throw new Error(\n `[align] groupIdFor: mediaTimeSec=${mediaTimeSec} overflows safe-integer ticks at 1e9 Hz (>~104 days); use groupIdForTicks with a narrower timescale`,\n )\n }\n return groupIdForTicks(ticks, durTicks)\n}\n\n/**\n * Result of a drift check between expected and actual groupIds.\n *\n * Discriminated on `kind` so callers must handle both outcomes explicitly\n * (the previous `{ drifted: boolean, deltaGroups: number }` shape let a\n * drifted result slip through with a silent `if (result.drifted)` gate).\n */\nexport type DriftCheckResult =\n | { readonly kind: 'aligned'; readonly deltaGroups: 0 }\n | { readonly kind: 'drifted'; readonly deltaGroups: number }\n\n/**\n * Compare a packet-stamped groupId against the formula-derived expectation.\n *\n * Pure observer \u2014 does not throw on drift, does not mutate anything.\n * Callers decide how to react (e.g. bump a CMCD counter, log, ignore).\n *\n * @param expected \u2014 groupId as derived via `groupIdForTicks` or `groupIdFor`.\n * @param actual \u2014 groupId as stamped on the incoming packet/group.\n */\nexport function driftCheck(expected: number, actual: number): DriftCheckResult {\n if (!Number.isInteger(expected) || !Number.isInteger(actual)) {\n throw new Error(\n `[align] driftCheck: both arguments must be integer groupIds, got expected=${expected} actual=${actual}`,\n )\n }\n const deltaGroups = actual - expected\n if (deltaGroups === 0) return { kind: 'aligned', deltaGroups: 0 }\n return { kind: 'drifted', deltaGroups }\n}\n\nexport type MmtpSubgroupKeyInput =\n | { readonly kind: 'mpu-metadata' }\n | {\n readonly kind: 'timed'\n readonly movieFragmentSequenceNumber: number\n readonly sampleNumber: number\n }\n | { readonly kind: 'non-timed'; readonly itemId: number }\n\n/**\n * Canonical, domain-separated key for MMTP payloads sharing one MoQ subgroup.\n * This is a map/storage key, not the scalar MoQ subgroup ID. MPU metadata is\n * carried in subgroup 0; media identity remains explicit and collision-free.\n */\nexport function subgroupKeyFor(input: MmtpSubgroupKeyInput): string {\n if (input.kind === 'mpu-metadata') return 'metadata:0'\n\n const values =\n input.kind === 'timed'\n ? [input.movieFragmentSequenceNumber, input.sampleNumber]\n : [input.itemId]\n for (const value of values) {\n if (!Number.isInteger(value) || value < 0 || value > 0xffff_ffff) {\n throw new Error(`[align] subgroup key coordinates must be uint32, got ${value}`)\n }\n }\n\n return input.kind === 'timed'\n ? `timed:${input.movieFragmentSequenceNumber}:${input.sampleNumber}`\n : `non-timed:${input.itemId}`\n}\n"],
5
+ "mappings": ";AA0DO,SAAS,iBAAiB,OAAe,WAA4B;AAC1E,MAAI,CAAC,OAAO,SAAS,KAAK,GAAG;AAC3B,UAAM,IAAI,MAAM,uDAAuD,KAAK,EAAE;AAAA,EAChF;AACA,MAAI,CAAC,OAAO,SAAS,SAAS,KAAK,aAAa,GAAG;AACjD,UAAM,IAAI;AAAA,MACR,wEAAwE,SAAS;AAAA,IACnF;AAAA,EACF;AACA,SAAQ,QAAQ;AAClB;AAOO,SAAS,UAAU,GAAoB;AAC5C,MAAI,CAAC,OAAO,SAAS,CAAC,GAAG;AACvB,UAAM,IAAI,MAAM,gDAAgD,CAAC,EAAE;AAAA,EACrE;AACA,SAAO;AACT;AAgBO,SAAS,gBAAgB,OAAe,oBAAoC;AACjF,MAAI,CAAC,OAAO,UAAU,KAAK,GAAG;AAC5B,UAAM,IAAI;AAAA,MACR,0DAA0D,KAAK;AAAA,IACjE;AAAA,EACF;AACA,MAAI,CAAC,OAAO,UAAU,kBAAkB,KAAK,sBAAsB,GAAG;AACpE,UAAM,IAAI;AAAA,MACR,+EAA+E,kBAAkB;AAAA,IACnG;AAAA,EACF;AACA,MAAI,QAAQ,EAAG,QAAO;AACtB,SAAO,KAAK,MAAM,QAAQ,kBAAkB;AAC9C;AAgCO,SAAS,WACd,cACA,kBACQ;AACR,MAAI,CAAC,OAAO,SAAS,YAAsB,GAAG;AAC5C,UAAM,IAAI;AAAA,MACR,iEAAiE,YAAY;AAAA,IAC/E;AAAA,EACF;AACA,MAAI,CAAC,OAAO,SAAS,gBAAgB,KAAK,oBAAoB,GAAG;AAC/D,UAAM,IAAI;AAAA,MACR,8EAA8E,gBAAgB;AAAA,IAChG;AAAA,EACF;AACA,MAAK,eAA0B,EAAG,QAAO;AAMzC,QAAM,KAAK;AACX,QAAM,QAAQ,KAAK,MAAO,eAA0B,EAAE;AACtD,QAAM,WAAW,KAAK,MAAM,mBAAmB,EAAE;AACjD,MAAI,YAAY,GAAG;AACjB,UAAM,IAAI;AAAA,MACR,wCAAwC,gBAAgB;AAAA,IAC1D;AAAA,EACF;AACA,MAAI,CAAC,OAAO,cAAc,KAAK,GAAG;AAChC,UAAM,IAAI;AAAA,MACR,oCAAoC,YAAY;AAAA,IAClD;AAAA,EACF;AACA,SAAO,gBAAgB,OAAO,QAAQ;AACxC;AAsBO,SAAS,WAAW,UAAkB,QAAkC;AAC7E,MAAI,CAAC,OAAO,UAAU,QAAQ,KAAK,CAAC,OAAO,UAAU,MAAM,GAAG;AAC5D,UAAM,IAAI;AAAA,MACR,6EAA6E,QAAQ,WAAW,MAAM;AAAA,IACxG;AAAA,EACF;AACA,QAAM,cAAc,SAAS;AAC7B,MAAI,gBAAgB,EAAG,QAAO,EAAE,MAAM,WAAW,aAAa,EAAE;AAChE,SAAO,EAAE,MAAM,WAAW,YAAY;AACxC;AAgBO,SAAS,eAAe,OAAqC;AAClE,MAAI,MAAM,SAAS,eAAgB,QAAO;AAE1C,QAAM,SACJ,MAAM,SAAS,UACX,CAAC,MAAM,6BAA6B,MAAM,YAAY,IACtD,CAAC,MAAM,MAAM;AACnB,aAAW,SAAS,QAAQ;AAC1B,QAAI,CAAC,OAAO,UAAU,KAAK,KAAK,QAAQ,KAAK,QAAQ,YAAa;AAChE,YAAM,IAAI,MAAM,wDAAwD,KAAK,EAAE;AAAA,IACjF;AAAA,EACF;AAEA,SAAO,MAAM,SAAS,UAClB,SAAS,MAAM,2BAA2B,IAAI,MAAM,YAAY,KAChE,aAAa,MAAM,MAAM;AAC/B;",
6
6
  "names": []
7
7
  }
@@ -1,36 +1,27 @@
1
- var __typeError = (msg) => {
2
- throw TypeError(msg);
3
- };
4
- var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
5
- var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
6
- var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
7
- var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
8
-
9
1
  // src/audio-ring-buffer.ts
10
- var _buffer, _writeIndex, _readIndex, _stalled;
11
2
  var AudioRingBuffer = class {
3
+ #buffer;
4
+ #writeIndex = 0;
5
+ #readIndex = 0;
6
+ #stalled;
12
7
  constructor(config) {
13
- __privateAdd(this, _buffer);
14
- __privateAdd(this, _writeIndex, 0);
15
- __privateAdd(this, _readIndex, 0);
16
- __privateAdd(this, _stalled);
17
8
  this.channels = config.channels;
18
9
  this.rate = config.rate;
19
- __privateSet(this, _stalled, !(config.startUnstalled ?? false));
10
+ this.#stalled = !(config.startUnstalled ?? false);
20
11
  const samples = Math.ceil(config.rate * (config.latencyMs / 1e3));
21
- __privateSet(this, _buffer, []);
12
+ this.#buffer = [];
22
13
  for (let i = 0; i < config.channels; i++) {
23
- __privateGet(this, _buffer)[i] = new Float32Array(samples);
14
+ this.#buffer[i] = new Float32Array(samples);
24
15
  }
25
16
  }
26
17
  get stalled() {
27
- return __privateGet(this, _stalled);
18
+ return this.#stalled;
28
19
  }
29
20
  get capacity() {
30
- return __privateGet(this, _buffer)[0].length;
21
+ return this.#buffer[0].length;
31
22
  }
32
23
  get length() {
33
- return __privateGet(this, _writeIndex) - __privateGet(this, _readIndex);
24
+ return this.#writeIndex - this.#readIndex;
34
25
  }
35
26
  /**
36
27
  * Append decoded audio samples to the FIFO.
@@ -39,24 +30,24 @@ var AudioRingBuffer = class {
39
30
  */
40
31
  write(_timestampUs, data) {
41
32
  const samples = data[0].length;
42
- const start = __privateGet(this, _writeIndex);
33
+ const start = this.#writeIndex;
43
34
  const end = start + samples;
44
- const overflow = end - __privateGet(this, _readIndex) - __privateGet(this, _buffer)[0].length;
35
+ const overflow = end - this.#readIndex - this.#buffer[0].length;
45
36
  if (overflow >= 0) {
46
- __privateSet(this, _stalled, false);
47
- __privateSet(this, _readIndex, __privateGet(this, _readIndex) + overflow);
37
+ this.#stalled = false;
38
+ this.#readIndex += overflow;
48
39
  }
49
- if (__privateGet(this, _stalled) && end - __privateGet(this, _readIndex) >= Math.floor(__privateGet(this, _buffer)[0].length / 2)) {
50
- __privateSet(this, _stalled, false);
40
+ if (this.#stalled && end - this.#readIndex >= Math.floor(this.#buffer[0].length / 2)) {
41
+ this.#stalled = false;
51
42
  }
52
43
  for (let ch = 0; ch < this.channels; ch++) {
53
44
  const src = data[ch];
54
- const dst = __privateGet(this, _buffer)[ch];
45
+ const dst = this.#buffer[ch];
55
46
  for (let i = 0; i < samples; i++) {
56
47
  dst[(start + i) % dst.length] = src[i];
57
48
  }
58
49
  }
59
- __privateSet(this, _writeIndex, end);
50
+ this.#writeIndex = end;
60
51
  }
61
52
  /**
62
53
  * Read samples into output buffers — plain FIFO, no clock recovery.
@@ -64,29 +55,25 @@ var AudioRingBuffer = class {
64
55
  * Underrun → zero-fill.
65
56
  */
66
57
  read(output) {
67
- if (__privateGet(this, _stalled)) return 0;
68
- const available = __privateGet(this, _writeIndex) - __privateGet(this, _readIndex);
58
+ if (this.#stalled) return 0;
59
+ const available = this.#writeIndex - this.#readIndex;
69
60
  if (available === 0) return 0;
70
61
  const requested = output[0].length;
71
62
  const readSamples = Math.min(available, requested);
72
63
  for (let ch = 0; ch < this.channels; ch++) {
73
64
  const dst = output[ch];
74
- const src = __privateGet(this, _buffer)[ch];
65
+ const src = this.#buffer[ch];
75
66
  for (let i = 0; i < readSamples; i++) {
76
- dst[i] = src[(__privateGet(this, _readIndex) + i) % src.length];
67
+ dst[i] = src[(this.#readIndex + i) % src.length];
77
68
  }
78
69
  for (let i = readSamples; i < requested; i++) {
79
70
  dst[i] = 0;
80
71
  }
81
72
  }
82
- __privateSet(this, _readIndex, __privateGet(this, _readIndex) + readSamples);
73
+ this.#readIndex += readSamples;
83
74
  return readSamples;
84
75
  }
85
76
  };
86
- _buffer = new WeakMap();
87
- _writeIndex = new WeakMap();
88
- _readIndex = new WeakMap();
89
- _stalled = new WeakMap();
90
77
  export {
91
78
  AudioRingBuffer
92
79
  };
@@ -2,6 +2,6 @@
2
2
  "version": 3,
3
3
  "sources": ["../src/audio-ring-buffer.ts"],
4
4
  "sourcesContent": ["/**\n * AudioRingBuffer \u2014 plain FIFO audio buffer (no clock recovery).\n *\n * Source of truth for the audio buffer algorithm used in both moq-lib and\n * moqtail AudioWorklet processors. AudioWorklet isolation requires the\n * worklet copy to be self-contained (no imports), so changes here must be\n * manually synced to:\n * - moqtail-private/apps/client-js/src/lib/audio-worklet-processor.js\n * - hang-mmt-fec/js/watch/src/audio/ring-buffer.ts (uses @moq/lite Time types)\n *\n * Rationale: FEC guarantees gap-free, ordered audio on multicast; MoQ\n * delivers ordered audio without drops. The decoder output rate matches\n * the AudioContext destination rate closely enough that skip/dup clock\n * recovery is unnecessary and harmful \u2014 a single sample skip at 48 kHz is\n * a ~21 \u00B5s phase glitch, and repeated skip/dup produces audible pitch\n * wobble on pure tones (measured: 16 Hz offset + \u00B111 Hz spread on a 1 kHz\n * test tone before this refactor).\n */\n\nexport interface AudioRingBufferConfig {\n /** Number of audio channels */\n channels: number;\n /** Sample rate in Hz */\n rate: number;\n /** Buffer latency in milliseconds */\n latencyMs: number;\n /** Start in unstalled state (for source switching) */\n startUnstalled?: boolean;\n}\n\nexport class AudioRingBuffer {\n readonly channels: number;\n readonly rate: number;\n\n #buffer: Float32Array[];\n #writeIndex = 0;\n #readIndex = 0;\n #stalled: boolean;\n\n constructor(config: AudioRingBufferConfig) {\n this.channels = config.channels;\n this.rate = config.rate;\n this.#stalled = !(config.startUnstalled ?? false);\n\n const samples = Math.ceil(config.rate * (config.latencyMs / 1000));\n this.#buffer = [];\n for (let i = 0; i < config.channels; i++) {\n this.#buffer[i] = new Float32Array(samples);\n }\n }\n\n get stalled(): boolean { return this.#stalled; }\n get capacity(): number { return this.#buffer[0].length; }\n get length(): number { return this.#writeIndex - this.#readIndex; }\n\n /**\n * Append decoded audio samples to the FIFO.\n * Timestamp parameter retained for API compatibility but is ignored \u2014\n * FIFO is sequence-based, not time-based.\n */\n write(_timestampUs: number, data: Float32Array[]): void {\n const samples = data[0].length;\n\n const start = this.#writeIndex;\n const end = start + samples;\n\n // Overflow: discard oldest samples (protects against encoder/AudioContext\n // clock drift when one runs faster than the other).\n const overflow = end - this.#readIndex - this.#buffer[0].length;\n if (overflow >= 0) {\n this.#stalled = false;\n this.#readIndex += overflow;\n }\n\n // Unstall at half capacity \u2014 enough prebuffer for bounded jitter.\n if (this.#stalled && (end - this.#readIndex) >= Math.floor(this.#buffer[0].length / 2)) {\n this.#stalled = false;\n }\n\n // Append samples sequentially\n for (let ch = 0; ch < this.channels; ch++) {\n const src = data[ch];\n const dst = this.#buffer[ch];\n for (let i = 0; i < samples; i++) {\n dst[(start + i) % dst.length] = src[i];\n }\n }\n this.#writeIndex = end;\n }\n\n /**\n * Read samples into output buffers \u2014 plain FIFO, no clock recovery.\n * FEC + MoQ delivery guarantees no gaps, so skip/dup is unnecessary.\n * Underrun \u2192 zero-fill.\n */\n read(output: Float32Array[]): number {\n if (this.#stalled) return 0;\n\n const available = this.#writeIndex - this.#readIndex;\n if (available === 0) return 0;\n\n const requested = output[0].length;\n const readSamples = Math.min(available, requested);\n\n for (let ch = 0; ch < this.channels; ch++) {\n const dst = output[ch];\n const src = this.#buffer[ch];\n for (let i = 0; i < readSamples; i++) {\n dst[i] = src[(this.#readIndex + i) % src.length];\n }\n // Zero-fill remainder on underrun\n for (let i = readSamples; i < requested; i++) {\n dst[i] = 0;\n }\n }\n\n this.#readIndex += readSamples;\n return readSamples;\n }\n}\n"],
5
- "mappings": ";;;;;;;;;AAAA;AA8BO,IAAM,kBAAN,MAAsB;AAAA,EAS3B,YAAY,QAA+B;AAL3C;AACA,oCAAc;AACd,mCAAa;AACb;AAGE,SAAK,WAAW,OAAO;AACvB,SAAK,OAAO,OAAO;AACnB,uBAAK,UAAW,EAAE,OAAO,kBAAkB;AAE3C,UAAM,UAAU,KAAK,KAAK,OAAO,QAAQ,OAAO,YAAY,IAAK;AACjE,uBAAK,SAAU,CAAC;AAChB,aAAS,IAAI,GAAG,IAAI,OAAO,UAAU,KAAK;AACxC,yBAAK,SAAQ,CAAC,IAAI,IAAI,aAAa,OAAO;AAAA,IAC5C;AAAA,EACF;AAAA,EAEA,IAAI,UAAmB;AAAE,WAAO,mBAAK;AAAA,EAAU;AAAA,EAC/C,IAAI,WAAmB;AAAE,WAAO,mBAAK,SAAQ,CAAC,EAAE;AAAA,EAAQ;AAAA,EACxD,IAAI,SAAiB;AAAE,WAAO,mBAAK,eAAc,mBAAK;AAAA,EAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOlE,MAAM,cAAsB,MAA4B;AACtD,UAAM,UAAU,KAAK,CAAC,EAAE;AAExB,UAAM,QAAQ,mBAAK;AACnB,UAAM,MAAM,QAAQ;AAIpB,UAAM,WAAW,MAAM,mBAAK,cAAa,mBAAK,SAAQ,CAAC,EAAE;AACzD,QAAI,YAAY,GAAG;AACjB,yBAAK,UAAW;AAChB,yBAAK,YAAL,mBAAK,cAAc;AAAA,IACrB;AAGA,QAAI,mBAAK,aAAa,MAAM,mBAAK,eAAe,KAAK,MAAM,mBAAK,SAAQ,CAAC,EAAE,SAAS,CAAC,GAAG;AACtF,yBAAK,UAAW;AAAA,IAClB;AAGA,aAAS,KAAK,GAAG,KAAK,KAAK,UAAU,MAAM;AACzC,YAAM,MAAM,KAAK,EAAE;AACnB,YAAM,MAAM,mBAAK,SAAQ,EAAE;AAC3B,eAAS,IAAI,GAAG,IAAI,SAAS,KAAK;AAChC,aAAK,QAAQ,KAAK,IAAI,MAAM,IAAI,IAAI,CAAC;AAAA,MACvC;AAAA,IACF;AACA,uBAAK,aAAc;AAAA,EACrB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,KAAK,QAAgC;AACnC,QAAI,mBAAK,UAAU,QAAO;AAE1B,UAAM,YAAY,mBAAK,eAAc,mBAAK;AAC1C,QAAI,cAAc,EAAG,QAAO;AAE5B,UAAM,YAAY,OAAO,CAAC,EAAE;AAC5B,UAAM,cAAc,KAAK,IAAI,WAAW,SAAS;AAEjD,aAAS,KAAK,GAAG,KAAK,KAAK,UAAU,MAAM;AACzC,YAAM,MAAM,OAAO,EAAE;AACrB,YAAM,MAAM,mBAAK,SAAQ,EAAE;AAC3B,eAAS,IAAI,GAAG,IAAI,aAAa,KAAK;AACpC,YAAI,CAAC,IAAI,KAAK,mBAAK,cAAa,KAAK,IAAI,MAAM;AAAA,MACjD;AAEA,eAAS,IAAI,aAAa,IAAI,WAAW,KAAK;AAC5C,YAAI,CAAC,IAAI;AAAA,MACX;AAAA,IACF;AAEA,uBAAK,YAAL,mBAAK,cAAc;AACnB,WAAO;AAAA,EACT;AACF;AArFE;AACA;AACA;AACA;",
5
+ "mappings": ";AA8BO,IAAM,kBAAN,MAAsB;AAAA,EAI3B;AAAA,EACA,cAAc;AAAA,EACd,aAAa;AAAA,EACb;AAAA,EAEA,YAAY,QAA+B;AACzC,SAAK,WAAW,OAAO;AACvB,SAAK,OAAO,OAAO;AACnB,SAAK,WAAW,EAAE,OAAO,kBAAkB;AAE3C,UAAM,UAAU,KAAK,KAAK,OAAO,QAAQ,OAAO,YAAY,IAAK;AACjE,SAAK,UAAU,CAAC;AAChB,aAAS,IAAI,GAAG,IAAI,OAAO,UAAU,KAAK;AACxC,WAAK,QAAQ,CAAC,IAAI,IAAI,aAAa,OAAO;AAAA,IAC5C;AAAA,EACF;AAAA,EAEA,IAAI,UAAmB;AAAE,WAAO,KAAK;AAAA,EAAU;AAAA,EAC/C,IAAI,WAAmB;AAAE,WAAO,KAAK,QAAQ,CAAC,EAAE;AAAA,EAAQ;AAAA,EACxD,IAAI,SAAiB;AAAE,WAAO,KAAK,cAAc,KAAK;AAAA,EAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOlE,MAAM,cAAsB,MAA4B;AACtD,UAAM,UAAU,KAAK,CAAC,EAAE;AAExB,UAAM,QAAQ,KAAK;AACnB,UAAM,MAAM,QAAQ;AAIpB,UAAM,WAAW,MAAM,KAAK,aAAa,KAAK,QAAQ,CAAC,EAAE;AACzD,QAAI,YAAY,GAAG;AACjB,WAAK,WAAW;AAChB,WAAK,cAAc;AAAA,IACrB;AAGA,QAAI,KAAK,YAAa,MAAM,KAAK,cAAe,KAAK,MAAM,KAAK,QAAQ,CAAC,EAAE,SAAS,CAAC,GAAG;AACtF,WAAK,WAAW;AAAA,IAClB;AAGA,aAAS,KAAK,GAAG,KAAK,KAAK,UAAU,MAAM;AACzC,YAAM,MAAM,KAAK,EAAE;AACnB,YAAM,MAAM,KAAK,QAAQ,EAAE;AAC3B,eAAS,IAAI,GAAG,IAAI,SAAS,KAAK;AAChC,aAAK,QAAQ,KAAK,IAAI,MAAM,IAAI,IAAI,CAAC;AAAA,MACvC;AAAA,IACF;AACA,SAAK,cAAc;AAAA,EACrB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,KAAK,QAAgC;AACnC,QAAI,KAAK,SAAU,QAAO;AAE1B,UAAM,YAAY,KAAK,cAAc,KAAK;AAC1C,QAAI,cAAc,EAAG,QAAO;AAE5B,UAAM,YAAY,OAAO,CAAC,EAAE;AAC5B,UAAM,cAAc,KAAK,IAAI,WAAW,SAAS;AAEjD,aAAS,KAAK,GAAG,KAAK,KAAK,UAAU,MAAM;AACzC,YAAM,MAAM,OAAO,EAAE;AACrB,YAAM,MAAM,KAAK,QAAQ,EAAE;AAC3B,eAAS,IAAI,GAAG,IAAI,aAAa,KAAK;AACpC,YAAI,CAAC,IAAI,KAAK,KAAK,aAAa,KAAK,IAAI,MAAM;AAAA,MACjD;AAEA,eAAS,IAAI,aAAa,IAAI,WAAW,KAAK;AAC5C,YAAI,CAAC,IAAI;AAAA,MACX;AAAA,IACF;AAEA,SAAK,cAAc;AACnB,WAAO;AAAA,EACT;AACF;",
6
6
  "names": []
7
7
  }