@blockcast/mmt-container 2.2.0-main.c4fa878f4ff4 → 2.2.0-main.da2e7cd1dd5d

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 (62) hide show
  1. package/README.md +29 -17
  2. package/dist/browser-runtime.js +241 -149
  3. package/dist/browser-runtime.js.map +3 -3
  4. package/dist/catalog-fec-params.d.ts +17 -1
  5. package/dist/catalog-fec-params.d.ts.map +1 -1
  6. package/dist/catalog-fec-params.js +15 -0
  7. package/dist/catalog-fec-params.js.map +2 -2
  8. package/dist/cmcd-producer.d.ts +60 -47
  9. package/dist/cmcd-producer.d.ts.map +1 -1
  10. package/dist/cmcd-producer.js +138 -109
  11. package/dist/cmcd-producer.js.map +2 -2
  12. package/dist/delivery-catalog.js +24 -15
  13. package/dist/delivery-catalog.js.map +2 -2
  14. package/dist/delta-fallback-config.js.map +1 -1
  15. package/dist/fec-manager.d.ts +3 -1
  16. package/dist/fec-manager.d.ts.map +1 -1
  17. package/dist/fec-manager.js +26 -4
  18. package/dist/fec-manager.js.map +2 -2
  19. package/dist/fec-worker-setup.d.ts +1 -1
  20. package/dist/fec-worker-setup.js +1 -1
  21. package/dist/fec-worker-setup.js.map +1 -1
  22. package/dist/index.d.ts +2 -2
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +5 -1
  25. package/dist/index.js.map +2 -2
  26. package/dist/media-router.js.map +1 -1
  27. package/dist/mmtp-container-parser.d.ts +2 -0
  28. package/dist/mmtp-container-parser.d.ts.map +1 -1
  29. package/dist/mmtp-container-parser.js +4 -0
  30. package/dist/mmtp-container-parser.js.map +2 -2
  31. package/dist/mmtp-container-parser.no-modules.js +4 -0
  32. package/dist/mmtp-container-parser.no-modules.js.map +2 -2
  33. package/dist/mmtp-decode-pipeline.js +4 -0
  34. package/dist/mmtp-decode-pipeline.js.map +2 -2
  35. package/dist/mmtp-router.d.ts.map +1 -1
  36. package/dist/mmtp-router.js +38 -15
  37. package/dist/mmtp-router.js.map +2 -2
  38. package/dist/multicast-manager.js +64 -19
  39. package/dist/multicast-manager.js.map +2 -2
  40. package/dist/multicast-transport.js +24 -15
  41. package/dist/multicast-transport.js.map +2 -2
  42. package/dist/no-modules.js +4 -0
  43. package/dist/no-modules.js.map +2 -2
  44. package/dist/repair-layer-subscriptions.d.ts +6 -2
  45. package/dist/repair-layer-subscriptions.d.ts.map +1 -1
  46. package/dist/repair-layer-subscriptions.js +6 -3
  47. package/dist/repair-layer-subscriptions.js.map +2 -2
  48. package/dist/track-session.d.ts +1 -1
  49. package/dist/track-session.js.map +1 -1
  50. package/dist/wasm-mmtp-container-parser.js +4 -0
  51. package/dist/wasm-mmtp-container-parser.js.map +2 -2
  52. package/package.json +8 -5
  53. package/schemas/catalog.schema.json +4 -0
  54. package/src/catalog-fec-params.ts +36 -1
  55. package/src/cmcd-producer.ts +189 -179
  56. package/src/fec-manager.ts +26 -4
  57. package/src/fec-worker-setup.ts +2 -2
  58. package/src/index.ts +6 -2
  59. package/src/mmtp-container-parser.ts +12 -0
  60. package/src/mmtp-router.ts +21 -0
  61. package/src/repair-layer-subscriptions.ts +13 -5
  62. package/src/track-session.ts +1 -1
@@ -1,20 +1,30 @@
1
1
  /**
2
- * CmcdProducer — transport-agnostic CMCD telemetry producer.
2
+ * CmcdProducer — CMCD v2 Event-mode reports (CTA-5004-B §2.2) from a player.
3
3
  *
4
- * Maps FecTrackStats snapshots and TransportSnapshot state to x-bc-*
5
- * CMCD vendor keys (proto tags 70-99). Fires callbacks for two delivery
6
- * channels: MoQ SUBSCRIBE params and multicast HTTP POST.
4
+ * Maps FecTrackStats snapshots, repair lifecycle events and transport state to
5
+ * newline-separated `application/cmcd` records (CTA-5004-B §3.3), each an
6
+ * RFC 8941 dictionary carrying `v=2`, `ts`, `e`, `sn` and `sid`:
7
7
  *
8
- * Does NOT call fetch() or write to QUIC directly — the player wiring
9
- * layer is responsible for attaching params to SUBSCRIBE or POSTing
10
- * the payload to the CDN CMCD endpoint.
8
+ * - `e=t` — time-interval heartbeat, Table 1 keys only;
9
+ * - `e=ce` — custom event `cen="com.blockcast-fec"` carrying the
10
+ * `com.blockcast-*` counters at the heartbeat cadence. Custom keys ride
11
+ * only on `e=ce`: §2.2 admits on Event reports only Table 1 keys, and the
12
+ * Table 1 `cen` row admits a custom key-value pair with a custom event;
13
+ * - `e=e` — FEC failed closed, with `ec`.
11
14
  *
12
- * @see docs/superpowers/specs/2026-04-15-iwa-multicast-stack-design.md §3.3
13
- * @see CDN+ Track D BLO-1044
15
+ * Custom values are STRING or TOKEN of at most 64 characters (§4.1): numbers
16
+ * go out as strings. A value that is unknown is omitted, never sent as zero
17
+ * (§3). Counters are absolute since session start, so a lost report loses
18
+ * nothing.
19
+ *
20
+ * Does NOT call fetch(): the player POSTs the body handed to `onCmcdPost` with
21
+ * `Content-Type: application/cmcd`. The heartbeat is driven by the snapshots
22
+ * the player already feeds; there is no timer here.
14
23
  */
15
24
  import type { FecTrackStats } from '@blockcast/fec-worker';
16
25
  import type { FecFailClosedReason } from './fec-manager.js';
17
- /** Source-auth counters for CMCD: records verified, and drops per reason. */
26
+ import type { RepairLifecycleEvent } from './multicast-manager.js';
27
+ /** Source-auth counters: records verified, and drops per reason. */
18
28
  export interface ManifestVerificationStats {
19
29
  verified: number;
20
30
  failures: Record<string, number>;
@@ -34,23 +44,33 @@ export interface TransportSnapshot {
34
44
  /** AMT relay address (omit when undefined) */
35
45
  amtRelay?: string;
36
46
  }
37
- /** Transport transition event identifiers for x-bc-ct. */
47
+ /** Transport transition event identifiers for `com.blockcast-ct`. */
38
48
  export type TransportTransitionEvent = 'mc-start' | 'mc-to-uc' | 'amt-start';
39
49
  /**
40
- * Phase 14 play-mode state (D-12, x-bc-play-mode):
50
+ * Play-mode state (`com.blockcast-pm`):
41
51
  * - `full` — base + delta both active
42
52
  * - `base-only` — delta fell back; base carries playback
43
53
  * - `single-track` — stream is not a multi-track switching set (legacy)
44
54
  */
45
55
  export type PlayMode = 'full' | 'base-only' | 'single-track';
46
- /** CMCD vendor key map. Values are string, number, or boolean. */
47
- export type CmcdParams = Record<string, string | number | boolean>;
56
+ /** An RFC 8941 token (bare, unquoted). */
57
+ export interface CmcdToken {
58
+ readonly token: string;
59
+ }
60
+ /** A record value: integer, string, token, or inner list of strings. */
61
+ export type CmcdValue = number | string | CmcdToken | readonly string[];
62
+ /** One CMCD record, before encoding. */
63
+ export type CmcdRecord = Record<string, CmcdValue>;
48
64
  /** Options for CmcdProducer construction. */
49
65
  export interface CmcdProducerOptions {
50
- /** Fired on each snapshot and on transport transitions. Params for MoQ SUBSCRIBE. */
51
- onCmcdParams?: (params: CmcdParams) => void;
52
- /** Fired on each snapshot and on transport transitions. RFC 9559 query-string for HTTP POST body. */
53
- onCmcdPost?: (payload: string) => void;
66
+ /** Fired with an `application/cmcd` body: one record per line. */
67
+ onCmcdPost?: (body: string) => void;
68
+ /** Heartbeat period for the `e=t` and counter reports. Default 30 s (CTA-5004-B §5). */
69
+ intervalMs?: number;
70
+ /** CTA-5004-B `sid`. Default: a random UUID per producer. */
71
+ sessionId?: string;
72
+ /** Wall clock in ms since the epoch, for `ts` and the heartbeat. Default `Date.now`. */
73
+ now?: () => number;
54
74
  }
55
75
  /** Snapshot data passed to onSnapshot(). */
56
76
  export interface SnapshotData {
@@ -64,58 +84,51 @@ export interface TransportOnlySnapshotData {
64
84
  playMode?: PlayMode;
65
85
  switchMiscount?: number;
66
86
  }
87
+ /** Encode one record as an RFC 8941 dictionary with keys in alphabetical order. */
88
+ export declare function encodeCmcdRecord(record: CmcdRecord): string;
67
89
  export declare class CmcdProducer {
68
90
  #private;
69
91
  constructor(options: CmcdProducerOptions);
70
92
  /** Update the stored transport state. Called by player wiring layer. */
71
93
  updateTransport(snapshot: TransportSnapshot): void;
72
94
  /**
73
- * Set the current play-mode. Emitted as `x-bc-play-mode` on subsequent
74
- * snapshots. Throws on any value outside {'full','base-only','single-track'}
75
- * (CLAUDE.md: error on bad init).
95
+ * Set the current play-mode, reported as `com.blockcast-pm`. Throws on any
96
+ * value outside {'full','base-only','single-track'} (CLAUDE.md: error on bad init).
76
97
  */
77
98
  updatePlayMode(mode: PlayMode): void;
78
99
  /**
79
- * Set the delta-track FEC block-failure rate (0..1 inclusive). Emitted as
80
- * `x-bc-delta-fail-rate` on snapshots when play-mode is 'full' or 'base-only'.
81
- * Omitted on single-track streams.
100
+ * Set the delta-track FEC block-failure rate (0..1 inclusive), reported as
101
+ * `com.blockcast-dfr` when play-mode is 'full' or 'base-only'.
82
102
  */
83
103
  updateDeltaFailRate(rate: number): void;
84
- /**
85
- * Bump the cumulative switching-set misalignment counter. Emitted as
86
- * `x-bc-switch-miscount` on snapshots when the counter is > 0.
87
- */
104
+ /** Bump the cumulative switching-set misalignment counter (`com.blockcast-sm`). */
88
105
  incrementSwitchMiscount(): void;
89
106
  /**
90
- * Record that FEC failed closed at runtime (fec-01 6.3.9). Every later
91
- * snapshot carries `x-bc-fec-fail-closed` (the reason code) and
92
- * `x-bc-fec-fail-layer` (the held layer i whose total B_i was undercut), so a
93
- * stream playing without FEC is told apart from one with no loss.
107
+ * Record that FEC failed closed at runtime (fec-01 6.3.9). Reported at once
108
+ * as `e=e` with the reason code in `ec`, and on every later counter report
109
+ * as `com.blockcast-ffc` / `com.blockcast-ffl` (the held layer whose total
110
+ * B_i was undercut), so a stream playing without FEC is told apart from one
111
+ * with no loss.
94
112
  */
95
113
  updateFecFailClosed(reason: FecFailClosedReason): void;
96
114
  updateManifestVerification(stats: ManifestVerificationStats): void;
97
115
  /**
98
- * One-shot emit of `x-bc-switch-drift-us` for a single misalignment event.
99
- * Does NOT mutate snapshot state — subsequent onSnapshot() calls will not
100
- * include the drift key until the next emitSwitchDrift(). Mirrors the
101
- * existing onTransition() pattern.
116
+ * Count a video block's resolution from the repair lifecycle: recovered in
117
+ * band (`com.blockcast-fbr`), recovered by repair FETCH (`-fbq`), or failed
118
+ * as a keyframe-class block (`-fbfk`). Audio blocks are not counted, matching
119
+ * the video-track `-fbf`. Until the player feeds this, the three are absent.
102
120
  */
121
+ onRepairLifecycle(event: RepairLifecycleEvent): void;
122
+ /** One-shot custom event `com.blockcast-switch-drift` for a single misalignment. */
103
123
  emitSwitchDrift(driftUs: number): void;
104
- /**
105
- * Process a FecTrackStats snapshot. Finds the video track, builds
106
- * x-bc-* vendor keys, and fires both delivery callbacks.
107
- */
124
+ /** Take a FecTrackStats snapshot (first video track) and send the heartbeat when due. */
108
125
  onSnapshot(data: SnapshotData): void;
109
126
  /**
110
- * Emit transport state without inventing FEC metrics. Cadence is owned by
111
- * the caller because only the transport integration knows when state changed.
127
+ * Take transport state for receivers that do not own FEC stats, and send the
128
+ * heartbeat when due.
112
129
  */
113
130
  onTransportSnapshot(data: TransportOnlySnapshotData): void;
114
- /**
115
- * Signal a transport transition event. Fires an immediate out-of-band
116
- * emit with x-bc-ct set. The subsequent onSnapshot() will NOT include
117
- * x-bc-ct (one-shot).
118
- */
131
+ /** Report a transport transition at once, as custom event `com.blockcast-ct`. */
119
132
  onTransition(event: TransportTransitionEvent): void;
120
133
  }
121
134
  //# sourceMappingURL=cmcd-producer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"cmcd-producer.d.ts","sourceRoot":"","sources":["../src/cmcd-producer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAA;AAC1D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAA;AAM3D,6EAA6E;AAC7E,MAAM,WAAW,yBAAyB;IACxC,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACjC;AAED,kFAAkF;AAClF,MAAM,WAAW,iBAAiB;IAChC;;;;OAIG;IACH,SAAS,EAAE,QAAQ,GAAG,KAAK,GAAG,KAAK,CAAA;IACnC,wCAAwC;IACxC,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,yBAAyB;IACzB,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,8CAA8C;IAC9C,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED,0DAA0D;AAC1D,MAAM,MAAM,wBAAwB,GAAG,UAAU,GAAG,UAAU,GAAG,WAAW,CAAA;AAE5E;;;;;GAKG;AACH,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,WAAW,GAAG,cAAc,CAAA;AAI5D,kEAAkE;AAClE,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,CAAA;AAElE,6CAA6C;AAC7C,MAAM,WAAW,mBAAmB;IAClC,qFAAqF;IACrF,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,IAAI,CAAA;IAC3C,qGAAqG;IACrG,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;CACvC;AAED,4CAA4C;AAC5C,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,aAAa,EAAE,CAAA;IACvB,WAAW,EAAE,MAAM,CAAA;CACpB;AAED,8EAA8E;AAC9E,MAAM,WAAW,yBAAyB;IACxC,SAAS,EAAE,iBAAiB,CAAA;IAC5B,WAAW,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,EAAE,QAAQ,CAAA;IACnB,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB;AA4ED,qBAAa,YAAY;;gBAYX,OAAO,EAAE,mBAAmB;IAKxC,wEAAwE;IACxE,eAAe,CAAC,QAAQ,EAAE,iBAAiB,GAAG,IAAI;IAIlD;;;;OAIG;IACH,cAAc,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI;IASpC;;;;OAIG;IACH,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IASvC;;;OAGG;IACH,uBAAuB,IAAI,IAAI;IAI/B;;;;;OAKG;IACH,mBAAmB,CAAC,MAAM,EAAE,mBAAmB,GAAG,IAAI;IAItD,0BAA0B,CAAC,KAAK,EAAE,yBAAyB,GAAG,IAAI;IAIlE;;;;;OAKG;IACH,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAkBtC;;;OAGG;IACH,UAAU,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI;IA0CpC;;;OAGG;IACH,mBAAmB,CAAC,IAAI,EAAE,yBAAyB,GAAG,IAAI;IAoB1D;;;;OAIG;IACH,YAAY,CAAC,KAAK,EAAE,wBAAwB,GAAG,IAAI;CAsBpD"}
1
+ {"version":3,"file":"cmcd-producer.d.ts","sourceRoot":"","sources":["../src/cmcd-producer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAA;AAC1D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAA;AAC3D,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAA;AAMlE,oEAAoE;AACpE,MAAM,WAAW,yBAAyB;IACxC,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACjC;AAED,kFAAkF;AAClF,MAAM,WAAW,iBAAiB;IAChC;;;;OAIG;IACH,SAAS,EAAE,QAAQ,GAAG,KAAK,GAAG,KAAK,CAAA;IACnC,wCAAwC;IACxC,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,yBAAyB;IACzB,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,8CAA8C;IAC9C,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED,qEAAqE;AACrE,MAAM,MAAM,wBAAwB,GAAG,UAAU,GAAG,UAAU,GAAG,WAAW,CAAA;AAE5E;;;;;GAKG;AACH,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,WAAW,GAAG,cAAc,CAAA;AAI5D,0CAA0C;AAC1C,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CACvB;AAED,wEAAwE;AACxE,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,GAAG,SAAS,MAAM,EAAE,CAAA;AAEvE,wCAAwC;AACxC,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAA;AAElD,6CAA6C;AAC7C,MAAM,WAAW,mBAAmB;IAClC,kEAAkE;IAClE,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IACnC,wFAAwF;IACxF,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,6DAA6D;IAC7D,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,wFAAwF;IACxF,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CACnB;AAED,4CAA4C;AAC5C,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,aAAa,EAAE,CAAA;IACvB,WAAW,EAAE,MAAM,CAAA;CACpB;AAED,8EAA8E;AAC9E,MAAM,WAAW,yBAAyB;IACxC,SAAS,EAAE,iBAAiB,CAAA;IAC5B,WAAW,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,EAAE,QAAQ,CAAA;IACnB,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB;AAsBD,mFAAmF;AACnF,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,CAW3D;AAgBD,qBAAa,YAAY;;gBAkBX,OAAO,EAAE,mBAAmB;IAUxC,wEAAwE;IACxE,eAAe,CAAC,QAAQ,EAAE,iBAAiB,GAAG,IAAI;IAIlD;;;OAGG;IACH,cAAc,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI;IASpC;;;OAGG;IACH,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IASvC,mFAAmF;IACnF,uBAAuB,IAAI,IAAI;IAI/B;;;;;;OAMG;IACH,mBAAmB,CAAC,MAAM,EAAE,mBAAmB,GAAG,IAAI;IAMtD,0BAA0B,CAAC,KAAK,EAAE,yBAAyB,GAAG,IAAI;IAIlE;;;;;OAKG;IACH,iBAAiB,CAAC,KAAK,EAAE,oBAAoB,GAAG,IAAI;IAUpD,oFAAoF;IACpF,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAWtC,yFAAyF;IACzF,UAAU,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI;IAOpC;;;OAGG;IACH,mBAAmB,CAAC,IAAI,EAAE,yBAAyB,GAAG,IAAI;IAe1D,iFAAiF;IACjF,YAAY,CAAC,KAAK,EAAE,wBAAwB,GAAG,IAAI;CAsEpD"}
@@ -1,62 +1,64 @@
1
1
  // src/cmcd-producer.ts
2
2
  var PLAY_MODES = ["full", "base-only", "single-track"];
3
- function sanitize(value) {
4
- return value.replace(/[,\r\n]/g, "");
5
- }
6
- function buildFecParams(stats) {
7
- const params = {};
8
- const cfg = stats.config;
9
- params["x-bc-fec-algo"] = cfg.algorithm;
10
- params["x-bc-fec-k"] = cfg.k;
11
- params["x-bc-fec-p"] = cfg.repairCount;
12
- params["x-bc-fec-recovery"] = parseFloat((stats.recoveryRate / 100).toFixed(4));
13
- return params;
14
- }
15
- function buildTransportParams(snap) {
16
- if (!snap) return {};
17
- const params = {};
18
- params["x-bc-transport"] = snap.transport;
19
- if (snap.multicastGroup != null) {
20
- params["x-bc-multicast-group"] = snap.multicastGroup;
21
- }
22
- if (snap.multicastSource != null) {
23
- params["x-bc-multicast-source"] = snap.multicastSource;
24
- }
25
- if (snap.amtRelay != null) {
26
- params["x-bc-amt-relay"] = snap.amtRelay;
27
- }
28
- return params;
3
+ var DEFAULT_INTERVAL_MS = 3e4;
4
+ var CUSTOM_PREFIX = "com.blockcast-";
5
+ var MAX_CUSTOM_VALUE = 64;
6
+ var TRANSPORT_TOKEN = {
7
+ native: "ssm",
8
+ amt: "amt",
9
+ moq: "moq"
10
+ };
11
+ function sfString(value) {
12
+ return `"${value.replace(/[^\x20-\x7e]/g, "").replace(/[\\"]/g, "\\$&")}"`;
29
13
  }
30
- function formatQueryString(params) {
31
- return Object.keys(params).sort().map((key) => {
32
- const v = params[key];
33
- if (typeof v === "string") return `${key}=${sanitize(v)}`;
34
- return `${key}=${v}`;
14
+ function encodeCmcdRecord(record) {
15
+ return Object.keys(record).sort().map((key) => {
16
+ const value = record[key];
17
+ if (typeof value === "number") return `${key}=${value}`;
18
+ if (typeof value === "string") return `${key}=${sfString(value)}`;
19
+ if (Array.isArray(value)) return `${key}=(${value.map(sfString).join(" ")})`;
20
+ return `${key}=${value.token}`;
35
21
  }).join(",");
36
22
  }
23
+ var token = (value) => ({ token: value });
24
+ function putCustom(record, name, value) {
25
+ if (value === void 0) return;
26
+ if (typeof value === "number") value = String(value);
27
+ if (typeof value === "string" && value.length > MAX_CUSTOM_VALUE) return;
28
+ record[CUSTOM_PREFIX + name] = value;
29
+ }
37
30
  var CmcdProducer = class {
38
- #onCmcdParams;
39
31
  #onCmcdPost;
32
+ #intervalMs;
33
+ #sessionId;
34
+ #now;
35
+ #sn = 0;
36
+ #lastHeartbeat = null;
40
37
  #transport;
41
38
  #lastStats;
42
- // Phase 14 state — Plan 03 Task 2 (D-12, D-15).
43
39
  #playMode = null;
44
40
  #deltaFailRate = null;
45
41
  #switchMiscount = 0;
46
42
  #manifestStats = null;
47
43
  #fecFailClosed = null;
44
+ /** Video-block resolutions from the repair lifecycle; null until the player feeds one. */
45
+ #lifecycle = null;
48
46
  constructor(options) {
49
- this.#onCmcdParams = options.onCmcdParams;
50
47
  this.#onCmcdPost = options.onCmcdPost;
48
+ this.#intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
49
+ if (!Number.isFinite(this.#intervalMs) || this.#intervalMs <= 0) {
50
+ throw new Error(`[CmcdProducer] invalid intervalMs "${String(options.intervalMs)}"; expected a positive number`);
51
+ }
52
+ this.#sessionId = options.sessionId ?? globalThis.crypto.randomUUID();
53
+ this.#now = options.now ?? Date.now;
51
54
  }
52
55
  /** Update the stored transport state. Called by player wiring layer. */
53
56
  updateTransport(snapshot) {
54
57
  this.#transport = snapshot;
55
58
  }
56
59
  /**
57
- * Set the current play-mode. Emitted as `x-bc-play-mode` on subsequent
58
- * snapshots. Throws on any value outside {'full','base-only','single-track'}
59
- * (CLAUDE.md: error on bad init).
60
+ * Set the current play-mode, reported as `com.blockcast-pm`. Throws on any
61
+ * value outside {'full','base-only','single-track'} (CLAUDE.md: error on bad init).
60
62
  */
61
63
  updatePlayMode(mode) {
62
64
  if (typeof mode !== "string" || !PLAY_MODES.includes(mode)) {
@@ -67,9 +69,8 @@ var CmcdProducer = class {
67
69
  this.#playMode = mode;
68
70
  }
69
71
  /**
70
- * Set the delta-track FEC block-failure rate (0..1 inclusive). Emitted as
71
- * `x-bc-delta-fail-rate` on snapshots when play-mode is 'full' or 'base-only'.
72
- * Omitted on single-track streams.
72
+ * Set the delta-track FEC block-failure rate (0..1 inclusive), reported as
73
+ * `com.blockcast-dfr` when play-mode is 'full' or 'base-only'.
73
74
  */
74
75
  updateDeltaFailRate(rate) {
75
76
  if (typeof rate !== "number" || !Number.isFinite(rate) || rate < 0 || rate > 1) {
@@ -79,115 +80,143 @@ var CmcdProducer = class {
79
80
  }
80
81
  this.#deltaFailRate = rate;
81
82
  }
82
- /**
83
- * Bump the cumulative switching-set misalignment counter. Emitted as
84
- * `x-bc-switch-miscount` on snapshots when the counter is > 0.
85
- */
83
+ /** Bump the cumulative switching-set misalignment counter (`com.blockcast-sm`). */
86
84
  incrementSwitchMiscount() {
87
85
  this.#switchMiscount += 1;
88
86
  }
89
87
  /**
90
- * Record that FEC failed closed at runtime (fec-01 6.3.9). Every later
91
- * snapshot carries `x-bc-fec-fail-closed` (the reason code) and
92
- * `x-bc-fec-fail-layer` (the held layer i whose total B_i was undercut), so a
93
- * stream playing without FEC is told apart from one with no loss.
88
+ * Record that FEC failed closed at runtime (fec-01 6.3.9). Reported at once
89
+ * as `e=e` with the reason code in `ec`, and on every later counter report
90
+ * as `com.blockcast-ffc` / `com.blockcast-ffl` (the held layer whose total
91
+ * B_i was undercut), so a stream playing without FEC is told apart from one
92
+ * with no loss.
94
93
  */
95
94
  updateFecFailClosed(reason) {
95
+ const changed = this.#fecFailClosed?.code !== reason.code || this.#fecFailClosed?.layer !== reason.layer;
96
96
  this.#fecFailClosed = reason;
97
+ if (changed) this.#post([{ ...this.#base("e"), ec: [reason.code] }]);
97
98
  }
98
99
  updateManifestVerification(stats) {
99
100
  this.#manifestStats = stats;
100
101
  }
101
102
  /**
102
- * One-shot emit of `x-bc-switch-drift-us` for a single misalignment event.
103
- * Does NOT mutate snapshot state — subsequent onSnapshot() calls will not
104
- * include the drift key until the next emitSwitchDrift(). Mirrors the
105
- * existing onTransition() pattern.
103
+ * Count a video block's resolution from the repair lifecycle: recovered in
104
+ * band (`com.blockcast-fbr`), recovered by repair FETCH (`-fbq`), or failed
105
+ * as a keyframe-class block (`-fbfk`). Audio blocks are not counted, matching
106
+ * the video-track `-fbf`. Until the player feeds this, the three are absent.
106
107
  */
108
+ onRepairLifecycle(event) {
109
+ this.#lifecycle ??= { inband: 0, fetch: 0, failedKeyframe: 0 };
110
+ if (event.type === "recovered" && event.blockClass !== "audio") {
111
+ if (event.via === "inband") this.#lifecycle.inband++;
112
+ else this.#lifecycle.fetch++;
113
+ } else if (event.type === "failed" && event.blockClass === "keyframe") {
114
+ this.#lifecycle.failedKeyframe++;
115
+ }
116
+ }
117
+ /** One-shot custom event `com.blockcast-switch-drift` for a single misalignment. */
107
118
  emitSwitchDrift(driftUs) {
108
119
  if (typeof driftUs !== "number" || !Number.isFinite(driftUs) || driftUs < 0) {
109
120
  throw new Error(
110
121
  `[CmcdProducer] invalid drift-us "${String(driftUs)}"; expected a non-negative finite number`
111
122
  );
112
123
  }
113
- const params = {};
114
- if (this.#lastStats) Object.assign(params, buildFecParams(this.#lastStats));
115
- Object.assign(params, buildTransportParams(this.#transport));
116
- if (this.#switchMiscount > 0) params["x-bc-switch-miscount"] = this.#switchMiscount;
117
- params["x-bc-switch-drift-us"] = driftUs;
118
- this.#emit(params);
124
+ const record = { ...this.#base("ce"), cen: CUSTOM_PREFIX + "switch-drift" };
125
+ putCustom(record, "sdu", Math.round(driftUs));
126
+ this.#post([record]);
119
127
  }
120
- /**
121
- * Process a FecTrackStats snapshot. Finds the video track, builds
122
- * x-bc-* vendor keys, and fires both delivery callbacks.
123
- */
128
+ /** Take a FecTrackStats snapshot (first video track) and send the heartbeat when due. */
124
129
  onSnapshot(data) {
125
130
  const videoTrack = data.tracks.find((t) => t.config.trackType === "video");
126
131
  if (!videoTrack) return;
127
132
  this.#lastStats = videoTrack;
128
- const params = {
129
- ...buildFecParams(videoTrack),
130
- ...buildTransportParams(this.#transport)
131
- };
132
- if (this.#playMode !== null) {
133
- params["x-bc-play-mode"] = this.#playMode;
134
- }
135
- if (this.#fecFailClosed) {
136
- params["x-bc-fec-fail-closed"] = this.#fecFailClosed.code;
137
- params["x-bc-fec-fail-layer"] = this.#fecFailClosed.layer;
138
- }
139
- if (this.#playMode !== null && this.#playMode !== "single-track" && this.#deltaFailRate !== null) {
140
- params["x-bc-delta-fail-rate"] = parseFloat(this.#deltaFailRate.toFixed(4));
141
- }
142
- if (this.#switchMiscount > 0) {
143
- params["x-bc-switch-miscount"] = this.#switchMiscount;
144
- }
145
- if (this.#manifestStats) {
146
- params["x-bc-manifest-verified"] = this.#manifestStats.verified > 0;
147
- params["x-bc-manifest-failures"] = Object.values(this.#manifestStats.failures).reduce((total, count) => total + count, 0);
148
- }
149
- this.#emit(params);
133
+ this.#heartbeat();
150
134
  }
151
135
  /**
152
- * Emit transport state without inventing FEC metrics. Cadence is owned by
153
- * the caller because only the transport integration knows when state changed.
136
+ * Take transport state for receivers that do not own FEC stats, and send the
137
+ * heartbeat when due.
154
138
  */
155
139
  onTransportSnapshot(data) {
156
140
  if (!Number.isFinite(data.timestampMs) || data.timestampMs < 0) {
157
141
  throw new Error("[CmcdProducer] transport snapshot timestamp must be a non-negative finite number");
158
142
  }
159
143
  if (data.playMode !== void 0) this.updatePlayMode(data.playMode);
160
- if (data.switchMiscount !== void 0 && (!Number.isSafeInteger(data.switchMiscount) || data.switchMiscount < 0)) {
161
- throw new Error("[CmcdProducer] transport snapshot switch-miscount must be a non-negative safe integer");
144
+ if (data.switchMiscount !== void 0) {
145
+ if (!Number.isSafeInteger(data.switchMiscount) || data.switchMiscount < 0) {
146
+ throw new Error("[CmcdProducer] transport snapshot switch-miscount must be a non-negative safe integer");
147
+ }
148
+ this.#switchMiscount = data.switchMiscount;
162
149
  }
163
150
  this.#transport = data.transport;
164
- const params = buildTransportParams(data.transport);
165
- if (this.#playMode !== null) params["x-bc-play-mode"] = this.#playMode;
166
- const switchMiscount = data.switchMiscount ?? this.#switchMiscount;
167
- if (switchMiscount > 0) params["x-bc-switch-miscount"] = switchMiscount;
168
- this.#emit(params);
151
+ this.#heartbeat();
169
152
  }
170
- /**
171
- * Signal a transport transition event. Fires an immediate out-of-band
172
- * emit with x-bc-ct set. The subsequent onSnapshot() will NOT include
173
- * x-bc-ct (one-shot).
174
- */
153
+ /** Report a transport transition at once, as custom event `com.blockcast-ct`. */
175
154
  onTransition(event) {
176
- const params = {};
177
- if (this.#lastStats) {
178
- Object.assign(params, buildFecParams(this.#lastStats));
155
+ const record = { ...this.#base("ce"), cen: CUSTOM_PREFIX + "ct" };
156
+ putCustom(record, "ct", token(event));
157
+ this.#putTransport(record);
158
+ this.#post([record]);
159
+ }
160
+ #base(event) {
161
+ return { e: token(event), sid: this.#sessionId, sn: this.#sn++, ts: Math.floor(this.#now()), v: 2 };
162
+ }
163
+ #heartbeat() {
164
+ const now = this.#now();
165
+ if (this.#lastHeartbeat !== null && now - this.#lastHeartbeat < this.#intervalMs) return;
166
+ this.#lastHeartbeat = now;
167
+ this.#post([this.#base("t"), this.#counters()]);
168
+ }
169
+ #putTransport(record) {
170
+ const snap = this.#transport;
171
+ if (!snap) return;
172
+ putCustom(record, "tr", token(TRANSPORT_TOKEN[snap.transport]));
173
+ if (snap.multicastGroup != null && snap.multicastSource != null) {
174
+ putCustom(record, "sg", `${snap.multicastGroup}@${snap.multicastSource}`);
175
+ }
176
+ putCustom(record, "ar", snap.amtRelay);
177
+ }
178
+ /** The `e=ce` counter report. */
179
+ #counters() {
180
+ const record = { ...this.#base("ce"), cen: CUSTOM_PREFIX + "fec" };
181
+ const stats = this.#lastStats;
182
+ if (stats) {
183
+ putCustom(record, "fa", stats.config.algorithm);
184
+ putCustom(record, "fk", stats.config.k);
185
+ putCustom(record, "fp", stats.config.repairCount);
186
+ putCustom(record, "pr", stats.sourceSymbols);
187
+ putCustom(record, "pl", stats.sourcePacketsMissing);
188
+ putCustom(record, "pu", stats.sourcePacketsUnrecovered);
189
+ putCustom(record, "fbf", stats.blocksFailed);
190
+ }
191
+ if (this.#lifecycle) {
192
+ putCustom(record, "fbr", this.#lifecycle.inband);
193
+ putCustom(record, "fbq", this.#lifecycle.fetch);
194
+ putCustom(record, "fbfk", this.#lifecycle.failedKeyframe);
195
+ }
196
+ this.#putTransport(record);
197
+ if (this.#playMode !== null) {
198
+ putCustom(record, "pm", token(this.#playMode));
199
+ if (this.#playMode !== "single-track" && this.#deltaFailRate !== null) {
200
+ putCustom(record, "dfr", this.#deltaFailRate.toFixed(3));
201
+ }
202
+ }
203
+ if (this.#fecFailClosed) {
204
+ putCustom(record, "ffc", token(this.#fecFailClosed.code));
205
+ putCustom(record, "ffl", this.#fecFailClosed.layer);
206
+ }
207
+ if (this.#switchMiscount > 0) putCustom(record, "sm", this.#switchMiscount);
208
+ if (this.#manifestStats) {
209
+ putCustom(record, "mv", this.#manifestStats.verified);
210
+ putCustom(record, "mf", Object.values(this.#manifestStats.failures).reduce((a, b) => a + b, 0));
179
211
  }
180
- Object.assign(params, buildTransportParams(this.#transport));
181
- params["x-bc-ct"] = event;
182
- this.#emit(params);
212
+ return record;
183
213
  }
184
- /** Fire both delivery callbacks with the given params. */
185
- #emit(params) {
186
- this.#onCmcdParams?.(params);
187
- this.#onCmcdPost?.(formatQueryString(params));
214
+ #post(records) {
215
+ this.#onCmcdPost?.(records.map(encodeCmcdRecord).join("\n"));
188
216
  }
189
217
  };
190
218
  export {
191
- CmcdProducer
219
+ CmcdProducer,
220
+ encodeCmcdRecord
192
221
  };
193
222
  //# sourceMappingURL=cmcd-producer.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/cmcd-producer.ts"],
4
- "sourcesContent": ["/**\n * CmcdProducer \u2014 transport-agnostic CMCD telemetry producer.\n *\n * Maps FecTrackStats snapshots and TransportSnapshot state to x-bc-*\n * CMCD vendor keys (proto tags 70-99). Fires callbacks for two delivery\n * channels: MoQ SUBSCRIBE params and multicast HTTP POST.\n *\n * Does NOT call fetch() or write to QUIC directly \u2014 the player wiring\n * layer is responsible for attaching params to SUBSCRIBE or POSTing\n * the payload to the CDN CMCD endpoint.\n *\n * @see docs/superpowers/specs/2026-04-15-iwa-multicast-stack-design.md \u00A73.3\n * @see CDN+ Track D BLO-1044\n */\n\nimport type { FecTrackStats } from '@blockcast/fec-worker'\nimport type { FecFailClosedReason } from './fec-manager.js'\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Source-auth counters for CMCD: records verified, and drops per reason. */\nexport interface ManifestVerificationStats {\n verified: number\n failures: Record<string, number>\n}\n\n/** Transport state snapshot, built by the player wiring layer from SocketPool. */\nexport interface TransportSnapshot {\n /**\n * CMCD has no user-agent value: report a `'user-agent'` join\n * (`ResolvedMulticastTransport`, the browser's own MulticastReceiver) as\n * `'native'`, since the browser does not say which leg it chose.\n */\n transport: 'native' | 'amt' | 'moq'\n /** IPv4/IPv6 multicast group address */\n multicastGroup?: string\n /** SSM source address */\n multicastSource?: string\n /** AMT relay address (omit when undefined) */\n amtRelay?: string\n}\n\n/** Transport transition event identifiers for x-bc-ct. */\nexport type TransportTransitionEvent = 'mc-start' | 'mc-to-uc' | 'amt-start'\n\n/**\n * Phase 14 play-mode state (D-12, x-bc-play-mode):\n * - `full` \u2014 base + delta both active\n * - `base-only` \u2014 delta fell back; base carries playback\n * - `single-track` \u2014 stream is not a multi-track switching set (legacy)\n */\nexport type PlayMode = 'full' | 'base-only' | 'single-track'\n\nconst PLAY_MODES: readonly PlayMode[] = ['full', 'base-only', 'single-track']\n\n/** CMCD vendor key map. Values are string, number, or boolean. */\nexport type CmcdParams = Record<string, string | number | boolean>\n\n/** Options for CmcdProducer construction. */\nexport interface CmcdProducerOptions {\n /** Fired on each snapshot and on transport transitions. Params for MoQ SUBSCRIBE. */\n onCmcdParams?: (params: CmcdParams) => void\n /** Fired on each snapshot and on transport transitions. RFC 9559 query-string for HTTP POST body. */\n onCmcdPost?: (payload: string) => void\n}\n\n/** Snapshot data passed to onSnapshot(). */\nexport interface SnapshotData {\n tracks: FecTrackStats[]\n timestampMs: number\n}\n\n/** Transport-only telemetry for receivers that do not own FEC track stats. */\nexport interface TransportOnlySnapshotData {\n transport: TransportSnapshot\n timestampMs: number\n playMode?: PlayMode\n switchMiscount?: number\n}\n\n// ---------------------------------------------------------------------------\n// Value sanitization (T-06-01: header injection mitigation)\n// ---------------------------------------------------------------------------\n\n/** Strip commas, newlines, and carriage returns from string values. */\nfunction sanitize(value: string): string {\n return value.replace(/[,\\r\\n]/g, '')\n}\n\n// ---------------------------------------------------------------------------\n// Key building (pure functions, no side effects)\n// ---------------------------------------------------------------------------\n\n/** Build FEC vendor keys from a FecTrackStats snapshot. */\nfunction buildFecParams(stats: FecTrackStats): CmcdParams {\n const params: CmcdParams = {}\n const cfg = stats.config\n\n params['x-bc-fec-algo'] = cfg.algorithm\n params['x-bc-fec-k'] = cfg.k\n // P as this receiver JOINED it: the worker is configured from\n // TrackSessionConfig.joinedRepairCount, the sum of repairSymbols over the\n // joined base layers. Not the catalog total, which a receiver on a clean path\n // does not receive, and never including a keyframe overlay (separate FEC\n // instance with its own RSB_length).\n params['x-bc-fec-p'] = cfg.repairCount\n params['x-bc-fec-recovery'] = parseFloat((stats.recoveryRate / 100).toFixed(4))\n\n return params\n}\n\n/** Build transport vendor keys from a TransportSnapshot. */\nfunction buildTransportParams(snap: TransportSnapshot | undefined): CmcdParams {\n if (!snap) return {}\n\n const params: CmcdParams = {}\n params['x-bc-transport'] = snap.transport\n\n if (snap.multicastGroup != null) {\n params['x-bc-multicast-group'] = snap.multicastGroup\n }\n if (snap.multicastSource != null) {\n params['x-bc-multicast-source'] = snap.multicastSource\n }\n if (snap.amtRelay != null) {\n params['x-bc-amt-relay'] = snap.amtRelay\n }\n\n return params\n}\n\n// ---------------------------------------------------------------------------\n// Query-string serialization (RFC 9559 \u00A72)\n// ---------------------------------------------------------------------------\n\n/**\n * Serialize CmcdParams to RFC 9559 query-string format.\n * Keys sorted for deterministic output. String values sanitized.\n */\nfunction formatQueryString(params: CmcdParams): string {\n return Object.keys(params)\n .sort()\n .map(key => {\n const v = params[key]\n if (typeof v === 'string') return `${key}=${sanitize(v)}`\n return `${key}=${v}`\n })\n .join(',')\n}\n\n// ---------------------------------------------------------------------------\n// CmcdProducer\n// ---------------------------------------------------------------------------\n\nexport class CmcdProducer {\n #onCmcdParams: ((params: CmcdParams) => void) | undefined\n #onCmcdPost: ((payload: string) => void) | undefined\n #transport: TransportSnapshot | undefined\n #lastStats: FecTrackStats | undefined\n // Phase 14 state \u2014 Plan 03 Task 2 (D-12, D-15).\n #playMode: PlayMode | null = null\n #deltaFailRate: number | null = null\n #switchMiscount = 0\n #manifestStats: ManifestVerificationStats | null = null\n #fecFailClosed: FecFailClosedReason | null = null\n\n constructor(options: CmcdProducerOptions) {\n this.#onCmcdParams = options.onCmcdParams\n this.#onCmcdPost = options.onCmcdPost\n }\n\n /** Update the stored transport state. Called by player wiring layer. */\n updateTransport(snapshot: TransportSnapshot): void {\n this.#transport = snapshot\n }\n\n /**\n * Set the current play-mode. Emitted as `x-bc-play-mode` on subsequent\n * snapshots. Throws on any value outside {'full','base-only','single-track'}\n * (CLAUDE.md: error on bad init).\n */\n updatePlayMode(mode: PlayMode): void {\n if (typeof mode !== 'string' || !PLAY_MODES.includes(mode as PlayMode)) {\n throw new Error(\n `[CmcdProducer] invalid play-mode \"${String(mode)}\"; expected one of: ${PLAY_MODES.join(', ')}`,\n )\n }\n this.#playMode = mode\n }\n\n /**\n * Set the delta-track FEC block-failure rate (0..1 inclusive). Emitted as\n * `x-bc-delta-fail-rate` on snapshots when play-mode is 'full' or 'base-only'.\n * Omitted on single-track streams.\n */\n updateDeltaFailRate(rate: number): void {\n if (typeof rate !== 'number' || !Number.isFinite(rate) || rate < 0 || rate > 1) {\n throw new Error(\n `[CmcdProducer] invalid delta-fail-rate \"${String(rate)}\"; expected a finite number in [0, 1]`,\n )\n }\n this.#deltaFailRate = rate\n }\n\n /**\n * Bump the cumulative switching-set misalignment counter. Emitted as\n * `x-bc-switch-miscount` on snapshots when the counter is > 0.\n */\n incrementSwitchMiscount(): void {\n this.#switchMiscount += 1\n }\n\n /**\n * Record that FEC failed closed at runtime (fec-01 6.3.9). Every later\n * snapshot carries `x-bc-fec-fail-closed` (the reason code) and\n * `x-bc-fec-fail-layer` (the held layer i whose total B_i was undercut), so a\n * stream playing without FEC is told apart from one with no loss.\n */\n updateFecFailClosed(reason: FecFailClosedReason): void {\n this.#fecFailClosed = reason\n }\n\n updateManifestVerification(stats: ManifestVerificationStats): void {\n this.#manifestStats = stats\n }\n\n /**\n * One-shot emit of `x-bc-switch-drift-us` for a single misalignment event.\n * Does NOT mutate snapshot state \u2014 subsequent onSnapshot() calls will not\n * include the drift key until the next emitSwitchDrift(). Mirrors the\n * existing onTransition() pattern.\n */\n emitSwitchDrift(driftUs: number): void {\n if (\n typeof driftUs !== 'number' ||\n !Number.isFinite(driftUs) ||\n driftUs < 0\n ) {\n throw new Error(\n `[CmcdProducer] invalid drift-us \"${String(driftUs)}\"; expected a non-negative finite number`,\n )\n }\n const params: CmcdParams = {}\n if (this.#lastStats) Object.assign(params, buildFecParams(this.#lastStats))\n Object.assign(params, buildTransportParams(this.#transport))\n if (this.#switchMiscount > 0) params['x-bc-switch-miscount'] = this.#switchMiscount\n params['x-bc-switch-drift-us'] = driftUs\n this.#emit(params)\n }\n\n /**\n * Process a FecTrackStats snapshot. Finds the video track, builds\n * x-bc-* vendor keys, and fires both delivery callbacks.\n */\n onSnapshot(data: SnapshotData): void {\n // Find first video track; skip if none\n const videoTrack = data.tracks.find(t => t.config.trackType === 'video')\n if (!videoTrack) return\n\n this.#lastStats = videoTrack\n\n const params: CmcdParams = {\n ...buildFecParams(videoTrack),\n ...buildTransportParams(this.#transport),\n }\n\n if (this.#playMode !== null) {\n params['x-bc-play-mode'] = this.#playMode\n }\n\n if (this.#fecFailClosed) {\n params['x-bc-fec-fail-closed'] = this.#fecFailClosed.code\n params['x-bc-fec-fail-layer'] = this.#fecFailClosed.layer\n }\n\n // Delta-fail-rate is only meaningful when a delta track exists.\n if (\n this.#playMode !== null &&\n this.#playMode !== 'single-track' &&\n this.#deltaFailRate !== null\n ) {\n params['x-bc-delta-fail-rate'] = parseFloat(this.#deltaFailRate.toFixed(4))\n }\n\n if (this.#switchMiscount > 0) {\n params['x-bc-switch-miscount'] = this.#switchMiscount\n }\n if (this.#manifestStats) {\n params['x-bc-manifest-verified'] = this.#manifestStats.verified > 0\n params['x-bc-manifest-failures'] = Object.values(this.#manifestStats.failures)\n .reduce((total, count) => total + count, 0)\n }\n\n this.#emit(params)\n }\n\n /**\n * Emit transport state without inventing FEC metrics. Cadence is owned by\n * the caller because only the transport integration knows when state changed.\n */\n onTransportSnapshot(data: TransportOnlySnapshotData): void {\n if (!Number.isFinite(data.timestampMs) || data.timestampMs < 0) {\n throw new Error('[CmcdProducer] transport snapshot timestamp must be a non-negative finite number')\n }\n if (data.playMode !== undefined) this.updatePlayMode(data.playMode)\n if (\n data.switchMiscount !== undefined &&\n (!Number.isSafeInteger(data.switchMiscount) || data.switchMiscount < 0)\n ) {\n throw new Error('[CmcdProducer] transport snapshot switch-miscount must be a non-negative safe integer')\n }\n\n this.#transport = data.transport\n const params = buildTransportParams(data.transport)\n if (this.#playMode !== null) params['x-bc-play-mode'] = this.#playMode\n const switchMiscount = data.switchMiscount ?? this.#switchMiscount\n if (switchMiscount > 0) params['x-bc-switch-miscount'] = switchMiscount\n this.#emit(params)\n }\n\n /**\n * Signal a transport transition event. Fires an immediate out-of-band\n * emit with x-bc-ct set. The subsequent onSnapshot() will NOT include\n * x-bc-ct (one-shot).\n */\n onTransition(event: TransportTransitionEvent): void {\n const params: CmcdParams = {}\n\n // Include last known FEC stats if available\n if (this.#lastStats) {\n Object.assign(params, buildFecParams(this.#lastStats))\n }\n\n // Include transport state if available\n Object.assign(params, buildTransportParams(this.#transport))\n\n // Add the transition event key (one-shot)\n params['x-bc-ct'] = event\n\n this.#emit(params)\n }\n\n /** Fire both delivery callbacks with the given params. */\n #emit(params: CmcdParams): void {\n this.#onCmcdParams?.(params)\n this.#onCmcdPost?.(formatQueryString(params))\n }\n}\n"],
5
- "mappings": ";AAuDA,IAAM,aAAkC,CAAC,QAAQ,aAAa,cAAc;AAgC5E,SAAS,SAAS,OAAuB;AACvC,SAAO,MAAM,QAAQ,YAAY,EAAE;AACrC;AAOA,SAAS,eAAe,OAAkC;AACxD,QAAM,SAAqB,CAAC;AAC5B,QAAM,MAAM,MAAM;AAElB,SAAO,eAAe,IAAI,IAAI;AAC9B,SAAO,YAAY,IAAI,IAAI;AAM3B,SAAO,YAAY,IAAI,IAAI;AAC3B,SAAO,mBAAmB,IAAI,YAAY,MAAM,eAAe,KAAK,QAAQ,CAAC,CAAC;AAE9E,SAAO;AACT;AAGA,SAAS,qBAAqB,MAAiD;AAC7E,MAAI,CAAC,KAAM,QAAO,CAAC;AAEnB,QAAM,SAAqB,CAAC;AAC5B,SAAO,gBAAgB,IAAI,KAAK;AAEhC,MAAI,KAAK,kBAAkB,MAAM;AAC/B,WAAO,sBAAsB,IAAI,KAAK;AAAA,EACxC;AACA,MAAI,KAAK,mBAAmB,MAAM;AAChC,WAAO,uBAAuB,IAAI,KAAK;AAAA,EACzC;AACA,MAAI,KAAK,YAAY,MAAM;AACzB,WAAO,gBAAgB,IAAI,KAAK;AAAA,EAClC;AAEA,SAAO;AACT;AAUA,SAAS,kBAAkB,QAA4B;AACrD,SAAO,OAAO,KAAK,MAAM,EACtB,KAAK,EACL,IAAI,SAAO;AACV,UAAM,IAAI,OAAO,GAAG;AACpB,QAAI,OAAO,MAAM,SAAU,QAAO,GAAG,GAAG,IAAI,SAAS,CAAC,CAAC;AACvD,WAAO,GAAG,GAAG,IAAI,CAAC;AAAA,EACpB,CAAC,EACA,KAAK,GAAG;AACb;AAMO,IAAM,eAAN,MAAmB;AAAA,EACxB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA,YAA6B;AAAA,EAC7B,iBAAgC;AAAA,EAChC,kBAAkB;AAAA,EAClB,iBAAmD;AAAA,EACnD,iBAA6C;AAAA,EAE7C,YAAY,SAA8B;AACxC,SAAK,gBAAgB,QAAQ;AAC7B,SAAK,cAAc,QAAQ;AAAA,EAC7B;AAAA;AAAA,EAGA,gBAAgB,UAAmC;AACjD,SAAK,aAAa;AAAA,EACpB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,eAAe,MAAsB;AACnC,QAAI,OAAO,SAAS,YAAY,CAAC,WAAW,SAAS,IAAgB,GAAG;AACtE,YAAM,IAAI;AAAA,QACR,qCAAqC,OAAO,IAAI,CAAC,uBAAuB,WAAW,KAAK,IAAI,CAAC;AAAA,MAC/F;AAAA,IACF;AACA,SAAK,YAAY;AAAA,EACnB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,oBAAoB,MAAoB;AACtC,QAAI,OAAO,SAAS,YAAY,CAAC,OAAO,SAAS,IAAI,KAAK,OAAO,KAAK,OAAO,GAAG;AAC9E,YAAM,IAAI;AAAA,QACR,2CAA2C,OAAO,IAAI,CAAC;AAAA,MACzD;AAAA,IACF;AACA,SAAK,iBAAiB;AAAA,EACxB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,0BAAgC;AAC9B,SAAK,mBAAmB;AAAA,EAC1B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,oBAAoB,QAAmC;AACrD,SAAK,iBAAiB;AAAA,EACxB;AAAA,EAEA,2BAA2B,OAAwC;AACjE,SAAK,iBAAiB;AAAA,EACxB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,gBAAgB,SAAuB;AACrC,QACE,OAAO,YAAY,YACnB,CAAC,OAAO,SAAS,OAAO,KACxB,UAAU,GACV;AACA,YAAM,IAAI;AAAA,QACR,oCAAoC,OAAO,OAAO,CAAC;AAAA,MACrD;AAAA,IACF;AACA,UAAM,SAAqB,CAAC;AAC5B,QAAI,KAAK,WAAY,QAAO,OAAO,QAAQ,eAAe,KAAK,UAAU,CAAC;AAC1E,WAAO,OAAO,QAAQ,qBAAqB,KAAK,UAAU,CAAC;AAC3D,QAAI,KAAK,kBAAkB,EAAG,QAAO,sBAAsB,IAAI,KAAK;AACpE,WAAO,sBAAsB,IAAI;AACjC,SAAK,MAAM,MAAM;AAAA,EACnB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,WAAW,MAA0B;AAEnC,UAAM,aAAa,KAAK,OAAO,KAAK,OAAK,EAAE,OAAO,cAAc,OAAO;AACvE,QAAI,CAAC,WAAY;AAEjB,SAAK,aAAa;AAElB,UAAM,SAAqB;AAAA,MACzB,GAAG,eAAe,UAAU;AAAA,MAC5B,GAAG,qBAAqB,KAAK,UAAU;AAAA,IACzC;AAEA,QAAI,KAAK,cAAc,MAAM;AAC3B,aAAO,gBAAgB,IAAI,KAAK;AAAA,IAClC;AAEA,QAAI,KAAK,gBAAgB;AACvB,aAAO,sBAAsB,IAAI,KAAK,eAAe;AACrD,aAAO,qBAAqB,IAAI,KAAK,eAAe;AAAA,IACtD;AAGA,QACE,KAAK,cAAc,QACnB,KAAK,cAAc,kBACnB,KAAK,mBAAmB,MACxB;AACA,aAAO,sBAAsB,IAAI,WAAW,KAAK,eAAe,QAAQ,CAAC,CAAC;AAAA,IAC5E;AAEA,QAAI,KAAK,kBAAkB,GAAG;AAC5B,aAAO,sBAAsB,IAAI,KAAK;AAAA,IACxC;AACA,QAAI,KAAK,gBAAgB;AACvB,aAAO,wBAAwB,IAAI,KAAK,eAAe,WAAW;AAClE,aAAO,wBAAwB,IAAI,OAAO,OAAO,KAAK,eAAe,QAAQ,EAC1E,OAAO,CAAC,OAAO,UAAU,QAAQ,OAAO,CAAC;AAAA,IAC9C;AAEA,SAAK,MAAM,MAAM;AAAA,EACnB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,oBAAoB,MAAuC;AACzD,QAAI,CAAC,OAAO,SAAS,KAAK,WAAW,KAAK,KAAK,cAAc,GAAG;AAC9D,YAAM,IAAI,MAAM,kFAAkF;AAAA,IACpG;AACA,QAAI,KAAK,aAAa,OAAW,MAAK,eAAe,KAAK,QAAQ;AAClE,QACE,KAAK,mBAAmB,WACvB,CAAC,OAAO,cAAc,KAAK,cAAc,KAAK,KAAK,iBAAiB,IACrE;AACA,YAAM,IAAI,MAAM,uFAAuF;AAAA,IACzG;AAEA,SAAK,aAAa,KAAK;AACvB,UAAM,SAAS,qBAAqB,KAAK,SAAS;AAClD,QAAI,KAAK,cAAc,KAAM,QAAO,gBAAgB,IAAI,KAAK;AAC7D,UAAM,iBAAiB,KAAK,kBAAkB,KAAK;AACnD,QAAI,iBAAiB,EAAG,QAAO,sBAAsB,IAAI;AACzD,SAAK,MAAM,MAAM;AAAA,EACnB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,aAAa,OAAuC;AAClD,UAAM,SAAqB,CAAC;AAG5B,QAAI,KAAK,YAAY;AACnB,aAAO,OAAO,QAAQ,eAAe,KAAK,UAAU,CAAC;AAAA,IACvD;AAGA,WAAO,OAAO,QAAQ,qBAAqB,KAAK,UAAU,CAAC;AAG3D,WAAO,SAAS,IAAI;AAEpB,SAAK,MAAM,MAAM;AAAA,EACnB;AAAA;AAAA,EAGA,MAAM,QAA0B;AAC9B,SAAK,gBAAgB,MAAM;AAC3B,SAAK,cAAc,kBAAkB,MAAM,CAAC;AAAA,EAC9C;AACF;",
4
+ "sourcesContent": ["/**\n * CmcdProducer \u2014 CMCD v2 Event-mode reports (CTA-5004-B \u00A72.2) from a player.\n *\n * Maps FecTrackStats snapshots, repair lifecycle events and transport state to\n * newline-separated `application/cmcd` records (CTA-5004-B \u00A73.3), each an\n * RFC 8941 dictionary carrying `v=2`, `ts`, `e`, `sn` and `sid`:\n *\n * - `e=t` \u2014 time-interval heartbeat, Table 1 keys only;\n * - `e=ce` \u2014 custom event `cen=\"com.blockcast-fec\"` carrying the\n * `com.blockcast-*` counters at the heartbeat cadence. Custom keys ride\n * only on `e=ce`: \u00A72.2 admits on Event reports only Table 1 keys, and the\n * Table 1 `cen` row admits a custom key-value pair with a custom event;\n * - `e=e` \u2014 FEC failed closed, with `ec`.\n *\n * Custom values are STRING or TOKEN of at most 64 characters (\u00A74.1): numbers\n * go out as strings. A value that is unknown is omitted, never sent as zero\n * (\u00A73). Counters are absolute since session start, so a lost report loses\n * nothing.\n *\n * Does NOT call fetch(): the player POSTs the body handed to `onCmcdPost` with\n * `Content-Type: application/cmcd`. The heartbeat is driven by the snapshots\n * the player already feeds; there is no timer here.\n */\n\nimport type { FecTrackStats } from '@blockcast/fec-worker'\nimport type { FecFailClosedReason } from './fec-manager.js'\nimport type { RepairLifecycleEvent } from './multicast-manager.js'\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Source-auth counters: records verified, and drops per reason. */\nexport interface ManifestVerificationStats {\n verified: number\n failures: Record<string, number>\n}\n\n/** Transport state snapshot, built by the player wiring layer from SocketPool. */\nexport interface TransportSnapshot {\n /**\n * CMCD has no user-agent value: report a `'user-agent'` join\n * (`ResolvedMulticastTransport`, the browser's own MulticastReceiver) as\n * `'native'`, since the browser does not say which leg it chose.\n */\n transport: 'native' | 'amt' | 'moq'\n /** IPv4/IPv6 multicast group address */\n multicastGroup?: string\n /** SSM source address */\n multicastSource?: string\n /** AMT relay address (omit when undefined) */\n amtRelay?: string\n}\n\n/** Transport transition event identifiers for `com.blockcast-ct`. */\nexport type TransportTransitionEvent = 'mc-start' | 'mc-to-uc' | 'amt-start'\n\n/**\n * Play-mode state (`com.blockcast-pm`):\n * - `full` \u2014 base + delta both active\n * - `base-only` \u2014 delta fell back; base carries playback\n * - `single-track` \u2014 stream is not a multi-track switching set (legacy)\n */\nexport type PlayMode = 'full' | 'base-only' | 'single-track'\n\nconst PLAY_MODES: readonly PlayMode[] = ['full', 'base-only', 'single-track']\n\n/** An RFC 8941 token (bare, unquoted). */\nexport interface CmcdToken {\n readonly token: string\n}\n\n/** A record value: integer, string, token, or inner list of strings. */\nexport type CmcdValue = number | string | CmcdToken | readonly string[]\n\n/** One CMCD record, before encoding. */\nexport type CmcdRecord = Record<string, CmcdValue>\n\n/** Options for CmcdProducer construction. */\nexport interface CmcdProducerOptions {\n /** Fired with an `application/cmcd` body: one record per line. */\n onCmcdPost?: (body: string) => void\n /** Heartbeat period for the `e=t` and counter reports. Default 30 s (CTA-5004-B \u00A75). */\n intervalMs?: number\n /** CTA-5004-B `sid`. Default: a random UUID per producer. */\n sessionId?: string\n /** Wall clock in ms since the epoch, for `ts` and the heartbeat. Default `Date.now`. */\n now?: () => number\n}\n\n/** Snapshot data passed to onSnapshot(). */\nexport interface SnapshotData {\n tracks: FecTrackStats[]\n timestampMs: number\n}\n\n/** Transport-only telemetry for receivers that do not own FEC track stats. */\nexport interface TransportOnlySnapshotData {\n transport: TransportSnapshot\n timestampMs: number\n playMode?: PlayMode\n switchMiscount?: number\n}\n\nconst DEFAULT_INTERVAL_MS = 30_000\nconst CUSTOM_PREFIX = 'com.blockcast-'\n/** CTA-5004-B \u00A74.1: a custom value is at most 64 characters. */\nconst MAX_CUSTOM_VALUE = 64\n\nconst TRANSPORT_TOKEN: Record<TransportSnapshot['transport'], string> = {\n native: 'ssm',\n amt: 'amt',\n moq: 'moq',\n}\n\n// ---------------------------------------------------------------------------\n// Encoding (RFC 8941 dictionary, keys sorted \u2014 CTA-5004-B \u00A73)\n// ---------------------------------------------------------------------------\n\n/** RFC 8941 \u00A73.3.3 string: printable ASCII, `\\` and `\"` escaped. */\nfunction sfString(value: string): string {\n return `\"${value.replace(/[^\\x20-\\x7e]/g, '').replace(/[\\\\\"]/g, '\\\\$&')}\"`\n}\n\n/** Encode one record as an RFC 8941 dictionary with keys in alphabetical order. */\nexport function encodeCmcdRecord(record: CmcdRecord): string {\n return Object.keys(record)\n .sort()\n .map(key => {\n const value = record[key]\n if (typeof value === 'number') return `${key}=${value}`\n if (typeof value === 'string') return `${key}=${sfString(value)}`\n if (Array.isArray(value)) return `${key}=(${value.map(sfString).join(' ')})`\n return `${key}=${(value as CmcdToken).token}`\n })\n .join(',')\n}\n\nconst token = (value: string): CmcdToken => ({ token: value })\n\n/** Set a custom key, numbers as strings; drop anything absent or over 64 characters. */\nfunction putCustom(record: CmcdRecord, name: string, value: string | number | CmcdToken | undefined): void {\n if (value === undefined) return\n if (typeof value === 'number') value = String(value)\n if (typeof value === 'string' && value.length > MAX_CUSTOM_VALUE) return\n record[CUSTOM_PREFIX + name] = value\n}\n\n// ---------------------------------------------------------------------------\n// CmcdProducer\n// ---------------------------------------------------------------------------\n\nexport class CmcdProducer {\n #onCmcdPost: ((body: string) => void) | undefined\n #intervalMs: number\n #sessionId: string\n #now: () => number\n #sn = 0\n #lastHeartbeat: number | null = null\n\n #transport: TransportSnapshot | undefined\n #lastStats: FecTrackStats | undefined\n #playMode: PlayMode | null = null\n #deltaFailRate: number | null = null\n #switchMiscount = 0\n #manifestStats: ManifestVerificationStats | null = null\n #fecFailClosed: FecFailClosedReason | null = null\n /** Video-block resolutions from the repair lifecycle; null until the player feeds one. */\n #lifecycle: { inband: number; fetch: number; failedKeyframe: number } | null = null\n\n constructor(options: CmcdProducerOptions) {\n this.#onCmcdPost = options.onCmcdPost\n this.#intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS\n if (!Number.isFinite(this.#intervalMs) || this.#intervalMs <= 0) {\n throw new Error(`[CmcdProducer] invalid intervalMs \"${String(options.intervalMs)}\"; expected a positive number`)\n }\n this.#sessionId = options.sessionId ?? globalThis.crypto.randomUUID()\n this.#now = options.now ?? Date.now\n }\n\n /** Update the stored transport state. Called by player wiring layer. */\n updateTransport(snapshot: TransportSnapshot): void {\n this.#transport = snapshot\n }\n\n /**\n * Set the current play-mode, reported as `com.blockcast-pm`. Throws on any\n * value outside {'full','base-only','single-track'} (CLAUDE.md: error on bad init).\n */\n updatePlayMode(mode: PlayMode): void {\n if (typeof mode !== 'string' || !PLAY_MODES.includes(mode as PlayMode)) {\n throw new Error(\n `[CmcdProducer] invalid play-mode \"${String(mode)}\"; expected one of: ${PLAY_MODES.join(', ')}`,\n )\n }\n this.#playMode = mode\n }\n\n /**\n * Set the delta-track FEC block-failure rate (0..1 inclusive), reported as\n * `com.blockcast-dfr` when play-mode is 'full' or 'base-only'.\n */\n updateDeltaFailRate(rate: number): void {\n if (typeof rate !== 'number' || !Number.isFinite(rate) || rate < 0 || rate > 1) {\n throw new Error(\n `[CmcdProducer] invalid delta-fail-rate \"${String(rate)}\"; expected a finite number in [0, 1]`,\n )\n }\n this.#deltaFailRate = rate\n }\n\n /** Bump the cumulative switching-set misalignment counter (`com.blockcast-sm`). */\n incrementSwitchMiscount(): void {\n this.#switchMiscount += 1\n }\n\n /**\n * Record that FEC failed closed at runtime (fec-01 6.3.9). Reported at once\n * as `e=e` with the reason code in `ec`, and on every later counter report\n * as `com.blockcast-ffc` / `com.blockcast-ffl` (the held layer whose total\n * B_i was undercut), so a stream playing without FEC is told apart from one\n * with no loss.\n */\n updateFecFailClosed(reason: FecFailClosedReason): void {\n const changed = this.#fecFailClosed?.code !== reason.code || this.#fecFailClosed?.layer !== reason.layer\n this.#fecFailClosed = reason\n if (changed) this.#post([{ ...this.#base('e'), ec: [reason.code] }])\n }\n\n updateManifestVerification(stats: ManifestVerificationStats): void {\n this.#manifestStats = stats\n }\n\n /**\n * Count a video block's resolution from the repair lifecycle: recovered in\n * band (`com.blockcast-fbr`), recovered by repair FETCH (`-fbq`), or failed\n * as a keyframe-class block (`-fbfk`). Audio blocks are not counted, matching\n * the video-track `-fbf`. Until the player feeds this, the three are absent.\n */\n onRepairLifecycle(event: RepairLifecycleEvent): void {\n this.#lifecycle ??= { inband: 0, fetch: 0, failedKeyframe: 0 }\n if (event.type === 'recovered' && event.blockClass !== 'audio') {\n if (event.via === 'inband') this.#lifecycle.inband++\n else this.#lifecycle.fetch++\n } else if (event.type === 'failed' && event.blockClass === 'keyframe') {\n this.#lifecycle.failedKeyframe++\n }\n }\n\n /** One-shot custom event `com.blockcast-switch-drift` for a single misalignment. */\n emitSwitchDrift(driftUs: number): void {\n if (typeof driftUs !== 'number' || !Number.isFinite(driftUs) || driftUs < 0) {\n throw new Error(\n `[CmcdProducer] invalid drift-us \"${String(driftUs)}\"; expected a non-negative finite number`,\n )\n }\n const record: CmcdRecord = { ...this.#base('ce'), cen: CUSTOM_PREFIX + 'switch-drift' }\n putCustom(record, 'sdu', Math.round(driftUs))\n this.#post([record])\n }\n\n /** Take a FecTrackStats snapshot (first video track) and send the heartbeat when due. */\n onSnapshot(data: SnapshotData): void {\n const videoTrack = data.tracks.find(t => t.config.trackType === 'video')\n if (!videoTrack) return\n this.#lastStats = videoTrack\n this.#heartbeat()\n }\n\n /**\n * Take transport state for receivers that do not own FEC stats, and send the\n * heartbeat when due.\n */\n onTransportSnapshot(data: TransportOnlySnapshotData): void {\n if (!Number.isFinite(data.timestampMs) || data.timestampMs < 0) {\n throw new Error('[CmcdProducer] transport snapshot timestamp must be a non-negative finite number')\n }\n if (data.playMode !== undefined) this.updatePlayMode(data.playMode)\n if (data.switchMiscount !== undefined) {\n if (!Number.isSafeInteger(data.switchMiscount) || data.switchMiscount < 0) {\n throw new Error('[CmcdProducer] transport snapshot switch-miscount must be a non-negative safe integer')\n }\n this.#switchMiscount = data.switchMiscount\n }\n this.#transport = data.transport\n this.#heartbeat()\n }\n\n /** Report a transport transition at once, as custom event `com.blockcast-ct`. */\n onTransition(event: TransportTransitionEvent): void {\n const record: CmcdRecord = { ...this.#base('ce'), cen: CUSTOM_PREFIX + 'ct' }\n putCustom(record, 'ct', token(event))\n this.#putTransport(record)\n this.#post([record])\n }\n\n #base(event: string): CmcdRecord {\n return { e: token(event), sid: this.#sessionId, sn: this.#sn++, ts: Math.floor(this.#now()), v: 2 }\n }\n\n #heartbeat(): void {\n const now = this.#now()\n if (this.#lastHeartbeat !== null && now - this.#lastHeartbeat < this.#intervalMs) return\n this.#lastHeartbeat = now\n this.#post([this.#base('t'), this.#counters()])\n }\n\n #putTransport(record: CmcdRecord): void {\n const snap = this.#transport\n if (!snap) return\n putCustom(record, 'tr', token(TRANSPORT_TOKEN[snap.transport]))\n if (snap.multicastGroup != null && snap.multicastSource != null) {\n putCustom(record, 'sg', `${snap.multicastGroup}@${snap.multicastSource}`)\n }\n putCustom(record, 'ar', snap.amtRelay)\n }\n\n /** The `e=ce` counter report. */\n #counters(): CmcdRecord {\n const record: CmcdRecord = { ...this.#base('ce'), cen: CUSTOM_PREFIX + 'fec' }\n const stats = this.#lastStats\n if (stats) {\n putCustom(record, 'fa', stats.config.algorithm)\n putCustom(record, 'fk', stats.config.k)\n // P as this receiver JOINED it: the sum of repairSymbols over the joined\n // base layers, never including a keyframe overlay.\n putCustom(record, 'fp', stats.config.repairCount)\n putCustom(record, 'pr', stats.sourceSymbols)\n putCustom(record, 'pl', stats.sourcePacketsMissing)\n putCustom(record, 'pu', stats.sourcePacketsUnrecovered)\n putCustom(record, 'fbf', stats.blocksFailed)\n }\n if (this.#lifecycle) {\n putCustom(record, 'fbr', this.#lifecycle.inband)\n putCustom(record, 'fbq', this.#lifecycle.fetch)\n putCustom(record, 'fbfk', this.#lifecycle.failedKeyframe)\n }\n this.#putTransport(record)\n if (this.#playMode !== null) {\n putCustom(record, 'pm', token(this.#playMode))\n if (this.#playMode !== 'single-track' && this.#deltaFailRate !== null) {\n putCustom(record, 'dfr', this.#deltaFailRate.toFixed(3))\n }\n }\n if (this.#fecFailClosed) {\n putCustom(record, 'ffc', token(this.#fecFailClosed.code))\n putCustom(record, 'ffl', this.#fecFailClosed.layer)\n }\n if (this.#switchMiscount > 0) putCustom(record, 'sm', this.#switchMiscount)\n if (this.#manifestStats) {\n putCustom(record, 'mv', this.#manifestStats.verified)\n putCustom(record, 'mf', Object.values(this.#manifestStats.failures).reduce((a, b) => a + b, 0))\n }\n return record\n }\n\n #post(records: CmcdRecord[]): void {\n this.#onCmcdPost?.(records.map(encodeCmcdRecord).join('\\n'))\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }