@blockcast/mmt-container 0.1.0-main.c71423b8a4e4 → 0.1.0-main.ce3c8aeb7f68

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 (148) hide show
  1. package/dist/align.d.ts +6 -2
  2. package/dist/align.d.ts.map +1 -1
  3. package/dist/align.js.map +2 -2
  4. package/dist/audio-ring-buffer.js +23 -36
  5. package/dist/audio-ring-buffer.js.map +1 -1
  6. package/dist/browser-runtime.js +6865 -6485
  7. package/dist/browser-runtime.js.map +4 -4
  8. package/dist/carp-catalog.d.ts +3 -1
  9. package/dist/carp-catalog.d.ts.map +1 -1
  10. package/dist/carp-catalog.js +21 -2
  11. package/dist/carp-catalog.js.map +4 -4
  12. package/dist/catalog-fec-params.js +10 -0
  13. package/dist/catalog-fec-params.js.map +4 -4
  14. package/dist/cmaf-block-assembler.d.ts +42 -0
  15. package/dist/cmaf-block-assembler.d.ts.map +1 -1
  16. package/dist/cmaf-block-assembler.js +542 -535
  17. package/dist/cmaf-block-assembler.js.map +2 -2
  18. package/dist/cmaf-group-batcher.js +27 -46
  19. package/dist/cmaf-group-batcher.js.map +2 -2
  20. package/dist/cmcd-producer.js +45 -66
  21. package/dist/cmcd-producer.js.map +1 -1
  22. package/dist/fec-manager.d.ts +13 -1
  23. package/dist/fec-manager.d.ts.map +1 -1
  24. package/dist/fec-manager.js +1151 -871
  25. package/dist/fec-manager.js.map +2 -2
  26. package/dist/fec-starvation-detector.js +25 -40
  27. package/dist/fec-starvation-detector.js.map +1 -1
  28. package/dist/fec-worker-setup.js +242 -225
  29. package/dist/fec-worker-setup.js.map +3 -3
  30. package/dist/index.d.ts +3 -3
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js.map +2 -2
  33. package/dist/iso-bmff-timing.js +63 -88
  34. package/dist/iso-bmff-timing.js.map +1 -1
  35. package/dist/loc.js +7 -4
  36. package/dist/loc.js.map +1 -1
  37. package/dist/manifest-auth-plan.js +292 -311
  38. package/dist/manifest-auth-plan.js.map +1 -1
  39. package/dist/media-router.d.ts +0 -99
  40. package/dist/media-router.d.ts.map +1 -1
  41. package/dist/media-router.js +270 -504
  42. package/dist/media-router.js.map +3 -3
  43. package/dist/mfu-reassembler.js +597 -607
  44. package/dist/mfu-reassembler.js.map +3 -3
  45. package/dist/mmtp-container-parser.d.ts +37 -0
  46. package/dist/mmtp-container-parser.d.ts.map +1 -1
  47. package/dist/mmtp-container-parser.js +1531 -1522
  48. package/dist/mmtp-container-parser.js.map +3 -3
  49. package/dist/mmtp-container-parser.no-modules.js +179 -102
  50. package/dist/mmtp-container-parser.no-modules.js.map +3 -3
  51. package/dist/mmtp-decode-pipeline.js +1641 -1651
  52. package/dist/mmtp-decode-pipeline.js.map +3 -3
  53. package/dist/mmtp-router.d.ts +34 -0
  54. package/dist/mmtp-router.d.ts.map +1 -1
  55. package/dist/mmtp-router.js +3676 -3390
  56. package/dist/mmtp-router.js.map +3 -3
  57. package/dist/mmtp-to-cmaf.d.ts +7 -1
  58. package/dist/mmtp-to-cmaf.d.ts.map +1 -1
  59. package/dist/mmtp-to-cmaf.js +109 -134
  60. package/dist/mmtp-to-cmaf.js.map +2 -2
  61. package/dist/multicast-input.d.ts +7 -35
  62. package/dist/multicast-input.d.ts.map +1 -1
  63. package/dist/multicast-input.js +21 -2
  64. package/dist/multicast-input.js.map +4 -4
  65. package/dist/multicast-manager.d.ts +10 -1
  66. package/dist/multicast-manager.d.ts.map +1 -1
  67. package/dist/multicast-manager.js +5584 -4961
  68. package/dist/multicast-manager.js.map +4 -4
  69. package/dist/multicast-transport.d.ts +7 -0
  70. package/dist/multicast-transport.d.ts.map +1 -1
  71. package/dist/multicast-transport.js +369 -421
  72. package/dist/multicast-transport.js.map +4 -4
  73. package/dist/multicast-unwrap.js +53 -30
  74. package/dist/multicast-unwrap.js.map +2 -2
  75. package/dist/no-modules.js +2096 -2097
  76. package/dist/no-modules.js.map +3 -3
  77. package/dist/sample-combiner.js +118 -139
  78. package/dist/sample-combiner.js.map +1 -1
  79. package/dist/signaling.js.map +2 -2
  80. package/dist/source-auth.js +353 -382
  81. package/dist/source-auth.js.map +1 -1
  82. package/dist/switching-set-alignment.js +14 -29
  83. package/dist/switching-set-alignment.js.map +1 -1
  84. package/dist/timestamp-codec.js +17 -11
  85. package/dist/timestamp-codec.js.map +2 -2
  86. package/dist/track-pipeline-setup.js +105 -125
  87. package/dist/track-pipeline-setup.js.map +1 -1
  88. package/dist/track-pipeline.js +107 -127
  89. package/dist/track-pipeline.js.map +1 -1
  90. package/dist/track-session.d.ts +16 -0
  91. package/dist/track-session.d.ts.map +1 -1
  92. package/dist/track-session.js +230 -243
  93. package/dist/track-session.js.map +2 -2
  94. package/dist/transport-pool.js +24 -38
  95. package/dist/transport-pool.js.map +1 -1
  96. package/dist/wasm-mfu-reassembler.d.ts +6 -1
  97. package/dist/wasm-mfu-reassembler.d.ts.map +1 -1
  98. package/dist/wasm-mfu-reassembler.js +127 -144
  99. package/dist/wasm-mfu-reassembler.js.map +2 -2
  100. package/dist/wasm-mmtp-container-parser.js +1659 -1650
  101. package/dist/wasm-mmtp-container-parser.js.map +3 -3
  102. package/node_modules/@blockcast/fec-worker/README.md +5 -1
  103. package/node_modules/@blockcast/fec-worker/dist/fec-worker-client.d.ts +8 -2
  104. package/node_modules/@blockcast/fec-worker/dist/fec-worker-client.d.ts.map +1 -1
  105. package/node_modules/@blockcast/fec-worker/dist/fec-worker-client.js +89 -16
  106. package/node_modules/@blockcast/fec-worker/dist/fec-worker-client.js.map +1 -1
  107. package/node_modules/@blockcast/fec-worker/dist/fec-worker-types.d.ts +48 -3
  108. package/node_modules/@blockcast/fec-worker/dist/fec-worker-types.d.ts.map +1 -1
  109. package/node_modules/@blockcast/fec-worker/dist/fec-worker.js +55 -8
  110. package/node_modules/@blockcast/fec-worker/dist/fec-worker.js.map +2 -2
  111. package/node_modules/@blockcast/fec-worker/dist/index.d.ts +1 -1
  112. package/node_modules/@blockcast/fec-worker/dist/index.d.ts.map +1 -1
  113. package/node_modules/@blockcast/fec-worker/dist/index.js.map +1 -1
  114. package/node_modules/@blockcast/fec-worker/src/fec-worker-client.ts +97 -17
  115. package/node_modules/@blockcast/fec-worker/src/fec-worker-types.ts +46 -2
  116. package/node_modules/@blockcast/fec-worker/src/fec-worker.ts +78 -10
  117. package/node_modules/@blockcast/fec-worker/src/index.ts +2 -0
  118. package/node_modules/@blockcast/mmt-base/README.md +1 -0
  119. package/node_modules/@blockcast/mmt-base/dist/mmtp.d.ts +52 -0
  120. package/node_modules/@blockcast/mmt-base/dist/mmtp.d.ts.map +1 -1
  121. package/node_modules/@blockcast/mmt-base/dist/mmtp.js +142 -37
  122. package/node_modules/@blockcast/mmt-base/dist/mmtp.js.map +1 -1
  123. package/node_modules/@blockcast/mmt-base/src/mmtp.ts +158 -37
  124. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/README.md +1 -0
  125. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/mmtp.d.ts +52 -0
  126. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/mmtp.d.ts.map +1 -1
  127. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/mmtp.js +142 -37
  128. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/mmtp.js.map +1 -1
  129. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/src/mmtp.ts +158 -37
  130. package/package.json +7 -6
  131. package/schemas/catalog.schema.json +1 -1
  132. package/src/align.ts +6 -2
  133. package/src/carp-catalog.ts +13 -3
  134. package/src/cmaf-block-assembler.ts +90 -81
  135. package/src/fec-manager.ts +530 -146
  136. package/src/index.ts +2 -2
  137. package/src/media-router.ts +0 -397
  138. package/src/mmtp-container-parser.ts +97 -21
  139. package/src/mmtp-router.ts +302 -125
  140. package/src/mmtp-to-cmaf.ts +21 -7
  141. package/src/multicast-input.ts +23 -3
  142. package/src/multicast-manager.ts +83 -11
  143. package/src/multicast-transport.ts +56 -17
  144. package/src/track-session.ts +16 -0
  145. package/src/wasm-mfu-reassembler.ts +40 -24
  146. package/test-vectors/catalog/README.md +2 -0
  147. package/test-vectors/catalog/negative/multicast-packet-id-signaling.json +26 -0
  148. package/test-vectors/catalog-validation.json +11 -0
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
  }