@svta/cml-cmcd 2.6.0 → 2.7.0

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.
package/dist/index.js CHANGED
@@ -1,6 +1,23 @@
1
1
  import { SfItem, SfToken, decodeSfDict, encodeSfDict, symbolToStr } from "@svta/cml-structured-field-values";
2
2
  import { getBaseUrl, urlToRelativePath, uuid } from "@svta/cml-utils";
3
3
 
4
+ //#region src/CMCD_AGGREGATE_BITRATE_KEYS.ts
5
+ /**
6
+ * Maps each aggregate bitrate key to the exact bitrate key that supersedes it.
7
+ *
8
+ * Per CTA-5004-B, `ab` MUST NOT be sent when the encoded bitrate `br` is
9
+ * known. `lab` and `tab` follow the same rule for the lowest bitrate `lb`
10
+ * and the top bitrate `tb`.
11
+ *
12
+ * @internal
13
+ */
14
+ const CMCD_AGGREGATE_BITRATE_KEYS = {
15
+ ab: "br",
16
+ lab: "lb",
17
+ tab: "tb"
18
+ };
19
+
20
+ //#endregion
4
21
  //#region src/CMCD_FORMATTER_MAP.ts
5
22
  const roundValue = (value) => {
6
23
  if (value instanceof SfItem) return new SfItem(Math.round(value.value), value.params);
@@ -50,6 +67,34 @@ const CMCD_FORMATTER_MAP = {
50
67
  tb: toRounded
51
68
  };
52
69
 
70
+ //#endregion
71
+ //#region src/CMCD_KEY_OBJECT_TYPES.ts
72
+ /**
73
+ * Maps a key to the object types (`ot`) for which CTA-5004-B allows the key.
74
+ *
75
+ * A key that is absent from this map is allowed for every object type.
76
+ * `d` (Object duration) and `tpb` (Top playable bitrate) MUST NOT be sent for
77
+ * other object types.
78
+ *
79
+ * @internal
80
+ */
81
+ const CMCD_KEY_OBJECT_TYPES = {
82
+ d: [
83
+ "a",
84
+ "v",
85
+ "av",
86
+ "tt",
87
+ "c",
88
+ "o"
89
+ ],
90
+ tpb: [
91
+ "a",
92
+ "v",
93
+ "av",
94
+ "c"
95
+ ]
96
+ };
97
+
53
98
  //#endregion
54
99
  //#region src/CMCD_V2.ts
55
100
  /**
@@ -62,109 +107,109 @@ const CMCD_V2 = 2;
62
107
  //#endregion
63
108
  //#region src/CmcdEventType.ts
64
109
  /**
65
- * CMCD event type for the 'bc' key (bitrate change).
110
+ * CMCD event type 'bc' (bitrate change).
66
111
  *
67
112
  * @public
68
113
  */
69
114
  const CMCD_EVENT_BITRATE_CHANGE = "bc";
70
115
  /**
71
- * CMCD event type for the 'ps' key (play state change).
116
+ * CMCD event type 'ps' (play state change).
72
117
  *
73
118
  * @public
74
119
  */
75
120
  const CMCD_EVENT_PLAY_STATE = "ps";
76
121
  /**
77
- * CMCD event type for the 'pr' key (playback rate change).
122
+ * CMCD event type 'pr' (playback rate change).
78
123
  *
79
124
  * @public
80
125
  */
81
126
  const CMCD_EVENT_PLAYBACK_RATE = "pr";
82
127
  /**
83
- * CMCD event type for the 'e' key (error).
128
+ * CMCD event type 'e' (error).
84
129
  *
85
130
  * @public
86
131
  */
87
132
  const CMCD_EVENT_ERROR = "e";
88
133
  /**
89
- * CMCD event type for the 't' key (time interval).
134
+ * CMCD event type 't' (time interval).
90
135
  *
91
136
  * @public
92
137
  */
93
138
  const CMCD_EVENT_TIME_INTERVAL = "t";
94
139
  /**
95
- * CMCD event type for the 'c' key (content ID).
140
+ * CMCD event type 'c' (content ID).
96
141
  *
97
142
  * @public
98
143
  */
99
144
  const CMCD_EVENT_CONTENT_ID = "c";
100
145
  /**
101
- * CMCD event type for the 'b' key (backgrounded mode).
146
+ * CMCD event type 'b' (backgrounded mode).
102
147
  *
103
148
  * @public
104
149
  */
105
150
  const CMCD_EVENT_BACKGROUNDED_MODE = "b";
106
151
  /**
107
- * CMCD event type for the 'm' key (mute).
152
+ * CMCD event type 'm' (mute).
108
153
  *
109
154
  * @public
110
155
  */
111
156
  const CMCD_EVENT_MUTE = "m";
112
157
  /**
113
- * CMCD event type for the 'um' key (unmute).
158
+ * CMCD event type 'um' (unmute).
114
159
  *
115
160
  * @public
116
161
  */
117
162
  const CMCD_EVENT_UNMUTE = "um";
118
163
  /**
119
- * CMCD event type for the 'pe' key (player expand).
164
+ * CMCD event type 'pe' (player expand).
120
165
  *
121
166
  * @public
122
167
  */
123
168
  const CMCD_EVENT_PLAYER_EXPAND = "pe";
124
169
  /**
125
- * CMCD event type for the 'pc' key (player collapse).
170
+ * CMCD event type 'pc' (player collapse).
126
171
  *
127
172
  * @public
128
173
  */
129
174
  const CMCD_EVENT_PLAYER_COLLAPSE = "pc";
130
175
  /**
131
- * CMCD event type for the 'rr' key (response received).
176
+ * CMCD event type 'rr' (response received).
132
177
  *
133
178
  * @public
134
179
  */
135
180
  const CMCD_EVENT_RESPONSE_RECEIVED = "rr";
136
181
  /**
137
- * CMCD event type for the 'as' key (ad start).
182
+ * CMCD event type 'as' (ad start).
138
183
  *
139
184
  * @public
140
185
  */
141
186
  const CMCD_EVENT_AD_START = "as";
142
187
  /**
143
- * CMCD event type for the 'ae' key (ad end).
188
+ * CMCD event type 'ae' (ad end).
144
189
  *
145
190
  * @public
146
191
  */
147
192
  const CMCD_EVENT_AD_END = "ae";
148
193
  /**
149
- * CMCD event type for the 'abs' key (ad break start).
194
+ * CMCD event type 'abs' (ad break start).
150
195
  *
151
196
  * @public
152
197
  */
153
198
  const CMCD_EVENT_AD_BREAK_START = "abs";
154
199
  /**
155
- * CMCD event type for the 'abe' key (ad break end).
200
+ * CMCD event type 'abe' (ad break end).
156
201
  *
157
202
  * @public
158
203
  */
159
204
  const CMCD_EVENT_AD_BREAK_END = "abe";
160
205
  /**
161
- * CMCD event type for the 'sk' key (skip).
206
+ * CMCD event type 'sk' (skip).
162
207
  *
163
208
  * @public
164
209
  */
165
210
  const CMCD_EVENT_SKIP = "sk";
166
211
  /**
167
- * CMCD event type for the 'ce' key (custom event).
212
+ * CMCD event type 'ce' (custom event).
168
213
  *
169
214
  * @public
170
215
  */
@@ -206,13 +251,13 @@ const CmcdEventType = {
206
251
  * the event signals.
207
252
  *
208
253
  * Per CTA-5004-B, the state-change events `ps`, `pr`, `c`, `b`, `bc` are
209
- * state-transition markers and must carry the field whose value they signal.
210
- * Consumers force-include the field post-filter (`prepareCmcdData`), dedup
211
- * against its value (`CmcdReporter`), and check its presence in payloads
212
- * (`validateCmcdStructure`).
254
+ * state-transition markers. Each event must include the field whose value
255
+ * it signals. Callers force-include the field after filtering
256
+ * (`prepareCmcdData`), deduplicate against its value (`CmcdReporter`), and
257
+ * check its presence in payloads (`validateCmcdStructure`).
213
258
  *
214
- * Iteration order is load-bearing: `CmcdReporter.update()` fires state-change
215
- * events in map order when multiple tracked fields change in a single call.
259
+ * Iteration order matters: `CmcdReporter.update()` records state-change
260
+ * events in map order when several tracked fields change in one call.
216
261
  * Do not reorder entries without auditing reporter behavior.
217
262
  *
218
263
  * @internal
@@ -318,9 +363,9 @@ const CMCD_REQUEST_KEYS = [
318
363
  const CUSTOM_KEY_REGEX = /^[a-z][a-z0-9.-]*$/;
319
364
  /**
320
365
  * Check if a key is a valid custom key: a lowercase first letter, then
321
- * characters from `a-z 0-9 . -`, with a hyphen that is neither the first
322
- * nor the last character. These are the CTA-5004-B custom-key rules
323
- * restricted to names that survive RFC 8941 key serialization.
366
+ * characters from `a-z 0-9 . -`. The key needs a hyphen, not at the start
367
+ * or end. This check applies the CTA-5004-B custom-key rules, restricted
368
+ * to names that RFC 8941 key serialization accepts.
324
369
  *
325
370
  * @param key - The key to check.
326
371
  *
@@ -358,7 +403,7 @@ function isCmcdRequestKey(key) {
358
403
  //#endregion
359
404
  //#region src/CMCD_RESPONSE_KEYS.ts
360
405
  /**
361
- * CMCD v2 - Response-only and timing keys.
406
+ * Response-only and timing keys for version 2.
362
407
  *
363
408
  * @public
364
409
  */
@@ -381,7 +426,7 @@ const CMCD_RESPONSE_KEY_SET = /* @__PURE__ */ new Set(CMCD_RESPONSE_KEYS);
381
426
  *
382
427
  * @param key - The key to check.
383
428
  *
384
- * @returns `true` if the key is a valid CMCD request key, `false` otherwise.
429
+ * @returns `true` if the key is a valid CMCD response key, `false` otherwise.
385
430
  *
386
431
  * @public
387
432
  *
@@ -414,7 +459,7 @@ function isCmcdEventKey(key) {
414
459
  //#endregion
415
460
  //#region src/CMCD_INNER_LIST_KEYS.ts
416
461
  /**
417
- * Keys that are inner lists in V2 but plain scalars in V1.
462
+ * Inner-list keys in version 2 that are scalars in version 1.
418
463
  *
419
464
  * Used by both encoding (down-conversion) and decoding (up-conversion).
420
465
  *
@@ -469,7 +514,7 @@ const CMCD_V1_KEYS = [
469
514
  //#region src/isCmcdV1Key.ts
470
515
  const CMCD_V1_KEY_SET$1 = /* @__PURE__ */ new Set(CMCD_V1_KEYS);
471
516
  /**
472
- * Filter function for CMCD v1 keys.
517
+ * Filter for CMCD version 1 keys.
473
518
  *
474
519
  * @param key - The CMCD key to filter.
475
520
  *
@@ -509,32 +554,58 @@ function isTokenField(key) {
509
554
  //#endregion
510
555
  //#region src/isValid.ts
511
556
  /**
512
- * Checks if the given value is valid
557
+ * Checks if the given value is valid to send as a CMCD value.
558
+ *
559
+ * `undefined`, `null`, an empty string, `false`, an empty array, and a
560
+ * number that is not finite are not valid. CTA-5004-B requires omitting
561
+ * the key when its value is unknown.
513
562
  *
514
563
  * @param value - The value to check.
515
564
  *
516
- * @returns `true` if the key is a value is valid.
565
+ * @returns `true` if the value is valid.
517
566
  *
518
567
  * @internal
519
568
  */
520
569
  function isValid(value) {
521
570
  if (typeof value === "number") return Number.isFinite(value);
571
+ if (Array.isArray(value)) return value.length > 0;
522
572
  return value != null && value !== "" && value !== false;
523
573
  }
524
574
 
575
+ //#endregion
576
+ //#region src/toTokenString.ts
577
+ /**
578
+ * Resolve the text of a token value.
579
+ *
580
+ * A token may be a plain string, a registry `Symbol`, an `SfToken`, or an
581
+ * `SfItem` that wraps such a value.
582
+ *
583
+ * @param value - The value to resolve.
584
+ *
585
+ * @returns The token text, or `undefined` when the value is not a token.
586
+ *
587
+ * @internal
588
+ */
589
+ function toTokenString(value) {
590
+ if (value instanceof SfItem) return toTokenString(value.value);
591
+ if (typeof value === "string") return value;
592
+ if (typeof value === "symbol" || value instanceof SfToken) return symbolToStr(value);
593
+ }
594
+
525
595
  //#endregion
526
596
  //#region src/prepareCmcdData.ts
527
597
  const filterMap = {
528
- [CMCD_EVENT_MODE]: isCmcdEventKey,
529
- [CMCD_REQUEST_MODE]: isCmcdRequestKey
598
+ event: isCmcdEventKey,
599
+ request: isCmcdRequestKey
530
600
  };
531
601
  /**
532
- * Unwrap an inner list or SfItem value to a plain scalar.
602
+ * Unwrap an inner list or SfItem value to a scalar.
533
603
  */
534
604
  function unwrapValue(value, ot) {
535
605
  if (Array.isArray(value)) {
536
606
  let item;
537
- if (ot) item = value.find((item$1) => item$1.params?.ot === ot);
607
+ const otText = toTokenString(ot);
608
+ if (otText) item = value.find((item$1) => toTokenString(item$1.params?.ot) === otText);
538
609
  if (!item) item = value[0];
539
610
  return unwrapValue(item);
540
611
  }
@@ -542,10 +613,10 @@ function unwrapValue(value, ot) {
542
613
  return value;
543
614
  }
544
615
  /**
545
- * Down-convert V2 CMCD data to V1 format.
616
+ * Down-convert version 2 CMCD data to version 1.
546
617
  *
547
- * - Extracts `nrr` from `nor` SfItem `r` parameter.
548
- * - Unwraps inner-list values to plain scalars.
618
+ * - Extracts `nrr` from the `nor` SfItem's `r` parameter.
619
+ * - Unwraps inner-list values to scalars.
549
620
  */
550
621
  function downConvertToV1(obj) {
551
622
  const result = {};
@@ -565,6 +636,10 @@ function downConvertToV1(obj) {
565
636
  }
566
637
  return result;
567
638
  }
639
+ function formatValue(key, value, options, formatterOptions) {
640
+ const formatter = options.formatters?.[key] ?? CMCD_FORMATTER_MAP[key];
641
+ return typeof formatter === "function" && isValid(value) ? formatter(value, formatterOptions) : value;
642
+ }
568
643
  /**
569
644
  * Convert a generic object to CMCD data.
570
645
  *
@@ -582,15 +657,15 @@ function prepareCmcdData(obj, options = {}) {
582
657
  const version = options.version || obj["v"] || CMCD_V2;
583
658
  const reportingMode = options.reportingMode || CMCD_REQUEST_MODE;
584
659
  const data = version === 1 ? downConvertToV1(obj) : obj;
660
+ const eventType = toTokenString(data["e"]);
585
661
  const keyFilter = version === 1 ? isCmcdV1Key : filterMap[reportingMode];
586
662
  let keys = Object.keys(data).filter(keyFilter);
587
- if (data["e"] && data["e"] !== CMCD_EVENT_RESPONSE_RECEIVED) keys = keys.filter((key) => !isCmcdResponseReceivedKey(key));
663
+ if (data["e"] && eventType !== CMCD_EVENT_RESPONSE_RECEIVED) keys = keys.filter((key) => !isCmcdResponseReceivedKey(key));
588
664
  const filter = options.filter;
589
665
  if (typeof filter === "function") keys = keys.filter(filter);
590
666
  const isEventMode = reportingMode === CMCD_EVENT_MODE;
591
667
  if (isEventMode) {
592
- const eventType = data["e"];
593
- if (!keys.includes("e") && eventType != null) keys.push("e");
668
+ if (!keys.includes("e") && data["e"] != null) keys.push("e");
594
669
  if (!keys.includes("ts")) keys.push("ts");
595
670
  if (!keys.includes("cen") && data["cen"] != null && eventType === CMCD_EVENT_CUSTOM_EVENT) keys.push("cen");
596
671
  if (!keys.includes("ec") && data["ec"] != null && eventType === CMCD_EVENT_ERROR) keys.push("ec");
@@ -606,17 +681,33 @@ function prepareCmcdData(obj, options = {}) {
606
681
  baseUrl: options.baseUrl
607
682
  };
608
683
  keys.sort();
684
+ let objectType;
685
+ let objectTypeResolved = false;
609
686
  for (const key of keys) {
610
687
  let value = data[key];
611
- const formatter = options.formatters?.[key] ?? CMCD_FORMATTER_MAP[key];
612
- if (typeof formatter === "function") value = formatter(value, formatterOptions);
688
+ const exactKey = CMCD_AGGREGATE_BITRATE_KEYS[key];
689
+ if (exactKey && keys.includes(exactKey) && isValid(formatValue(exactKey, data[exactKey], options, formatterOptions))) continue;
690
+ const objectTypes = version > 1 ? CMCD_KEY_OBJECT_TYPES[key] : void 0;
691
+ if (objectTypes) {
692
+ if (!objectTypeResolved) {
693
+ const ot = formatValue("ot", data["ot"], options, formatterOptions);
694
+ objectType = isValid(ot) ? toTokenString(ot) : void 0;
695
+ objectTypeResolved = true;
696
+ }
697
+ if (objectType !== void 0 && !objectTypes.includes(objectType)) continue;
698
+ }
699
+ if (key === "cen" && eventType !== CMCD_EVENT_CUSTOM_EVENT) continue;
700
+ value = formatValue(key, value, options, formatterOptions);
613
701
  if (key === "v") if (version === 1) continue;
614
702
  else value = version;
615
- if (key === "pr" && value === 1 && !(isEventMode && data["e"] === CMCD_EVENT_PLAYBACK_RATE)) continue;
703
+ if (key === "pr" && value === 1 && !(isEventMode && eventType === CMCD_EVENT_PLAYBACK_RATE)) continue;
616
704
  if (isEventMode && key === "ts" && !Number.isFinite(value)) value = Date.now();
617
- const isBgFalseTransition = isEventMode && value === false && key === "bg" && data["e"] === CMCD_EVENT_BACKGROUNDED_MODE;
705
+ const isBgFalseTransition = isEventMode && value === false && key === "bg" && eventType === CMCD_EVENT_BACKGROUNDED_MODE;
618
706
  if (!isValid(value) && !isBgFalseTransition) continue;
619
- if (isTokenField(key) && typeof value === "string") value = new SfToken(value);
707
+ if (isTokenField(key)) {
708
+ if (typeof value === "string") value = new SfToken(value);
709
+ else if (value instanceof SfItem && typeof value.value === "string") value = new SfItem(new SfToken(value.value), value.params);
710
+ }
620
711
  results[key] = value;
621
712
  }
622
713
  return results;
@@ -679,9 +770,9 @@ const CMCD_HEADER_FIELDS = [
679
770
  /**
680
771
  * The map of CMCD keys to their appropriate header shard.
681
772
  *
682
- * Note: Event-only keys (e, ts, cen, h) and response-received keys
683
- * (rc, ttfb, ttlb, url, etc.) are intentionally absent. They are
684
- * transmitted via the event-mode POST body, not HTTP headers.
773
+ * Event-only keys (e, ts, cen, h) and response-received keys (rc, ttfb,
774
+ * ttlb, url, and others) are intentionally absent. They are transmitted
775
+ * in the event-mode POST body, not HTTP headers.
685
776
  *
686
777
  * @public
687
778
  */
@@ -736,9 +827,9 @@ function createHeaderMap(headerMap) {
736
827
  }, {});
737
828
  }
738
829
  /**
739
- * Group a CMCD data object into header shards
830
+ * Group a CMCD object into header shards
740
831
  *
741
- * @param cmcd - The CMCD data object to convert.
832
+ * @param cmcd - The CMCD object to convert.
742
833
  * @param customHeaderMap - A map of CMCD header fields to custom CMCD keys.
743
834
  *
744
835
  * @returns The CMCD header shards.
@@ -784,10 +875,10 @@ function toPreparedCmcdHeaders(data, customHeaderMap) {
784
875
  //#endregion
785
876
  //#region src/toCmcdHeaders.ts
786
877
  /**
787
- * Convert a CMCD data object to request headers
878
+ * Convert a CMCD object to request headers
788
879
  *
789
- * @param cmcd - The CMCD data object to convert.
790
- * @param options - Options for encoding the CMCD object.
880
+ * @param cmcd - The CMCD object to convert.
881
+ * @param options - Options for encoding.
791
882
  *
792
883
  * @returns The CMCD header shards.
793
884
  *
@@ -806,13 +897,13 @@ function toCmcdHeaders(cmcd, options = {}) {
806
897
  //#endregion
807
898
  //#region src/appendCmcdHeaders.ts
808
899
  /**
809
- * Append CMCD query args to a header object.
900
+ * Append CMCD header shards to a header object.
810
901
  *
811
902
  * @param headers - The headers to append to.
812
903
  * @param cmcd - The CMCD object to append.
813
- * @param options - Encode options.
904
+ * @param options - Options for encoding.
814
905
  *
815
- * @returns The headers with the CMCD header shards appended.
906
+ * @returns The headers with CMCD header shards appended.
816
907
  *
817
908
  * @public
818
909
  *
@@ -873,10 +964,10 @@ function encodeCmcd(cmcd, options = {}) {
873
964
  //#endregion
874
965
  //#region src/toCmcdUrl.ts
875
966
  /**
876
- * Convert a CMCD data object to a URL encoded string.
967
+ * Convert a CMCD object to a URL encoded string.
877
968
  *
878
969
  * @param cmcd - The CMCD object to convert.
879
- * @param options - Options for encoding the CMCD object.
970
+ * @param options - Options for encoding.
880
971
  *
881
972
  * @returns The URL encoded CMCD data.
882
973
  *
@@ -893,10 +984,10 @@ function toCmcdUrl(cmcd, options = {}) {
893
984
  //#endregion
894
985
  //#region src/toCmcdQuery.ts
895
986
  /**
896
- * Convert a CMCD data object to a query arg.
987
+ * Convert a CMCD object to a query arg.
897
988
  *
898
989
  * @param cmcd - The CMCD object to convert.
899
- * @param options - Options for encoding the CMCD object.
990
+ * @param options - Options for encoding.
900
991
  *
901
992
  * @returns The CMCD query arg.
902
993
  *
@@ -920,7 +1011,7 @@ const REGEX = /CMCD=[^&#]+/;
920
1011
  *
921
1012
  * @param url - The URL to append to.
922
1013
  * @param cmcd - The CMCD object to append.
923
- * @param options - Options for encoding the CMCD object.
1014
+ * @param options - Options for encoding.
924
1015
  *
925
1016
  * @returns The URL with the CMCD query args appended.
926
1017
  *
@@ -941,7 +1032,7 @@ function appendCmcdQuery(url, cmcd, options) {
941
1032
  //#endregion
942
1033
  //#region src/CMCD_DEFAULT_TIME_INTERVAL.ts
943
1034
  /**
944
- * The default time interval in seconds when using using event mode
1035
+ * The default time interval in seconds when using event mode
945
1036
  *
946
1037
  * @public
947
1038
  */
@@ -973,25 +1064,26 @@ const CMCD_MIME_TYPE = "application/cmcd";
973
1064
  //#endregion
974
1065
  //#region src/CMCD_REQUEST_PROVENANCE.ts
975
1066
  /**
976
- * The `customData` key under which `CmcdReporter.createRequestReport()`
977
- * stamps session provenance on every request it returns.
1067
+ * The `customData` property where `CmcdReporter.createRequestReport()`
1068
+ * writes session provenance on every request it returns.
978
1069
  *
979
- * The value is a frozen `CmcdRequestProvenance` record: `sid` names the
980
- * session that issued the request, `cid` is the content id in effect when
981
- * the request was issued, and `data` carries the request's per-call data
982
- * as an encoded CMCD string. `CmcdReporter.recordResponseReceived()` reads
983
- * the record to attribute a late response to its session and to rebuild
984
- * the request-time report keys.
985
- * Spread and `Object.assign` carry the record through ordinary request
986
- * clones, but `JSON.stringify` and structured clone drop symbol-keyed
987
- * properties: read the value before such a boundary and restore it
988
- * afterward. The record itself survives JSON, and attribution is by the
989
- * `sid` value rather than object identity, so a revived plain-object copy
990
- * attributes exactly. A record naming a `sid` the reporter does not retain
991
- * drops the response rather than relabeling it.
1070
+ * The value is a frozen `CmcdRequestProvenance` record. `sid` names the
1071
+ * session that issued the request. `cid` is the content id at the time the
1072
+ * request was issued. `data` is the request's per-call data as an encoded
1073
+ * CMCD string. `CmcdReporter.recordResponseReceived()` reads the record to
1074
+ * attribute a late response to its session and to rebuild the request-time
1075
+ * report keys.
1076
+ * Spread and `Object.assign` copy the record into ordinary request clones.
1077
+ * `JSON.stringify` and structured clone drop symbol-keyed properties. Before
1078
+ * either operation, read the value and restore it afterward. JSON preserves
1079
+ * the record's fields. Attribution uses the `sid` value rather than object
1080
+ * identity, so a parsed plain-object copy attributes correctly. For a record
1081
+ * naming a `sid` the reporter does not retain, the reporter drops the
1082
+ * response rather than reattributing it.
992
1083
  *
993
- * Backed by the symbol registry so duplicated copies of this library in
994
- * one bundle interoperate. The registry key is stable across versions.
1084
+ * The symbol registry provides the symbol, so duplicated copies of this
1085
+ * library in one bundle interoperate. The registry key is stable across
1086
+ * versions.
995
1087
  *
996
1088
  * @public
997
1089
  */
@@ -1032,7 +1124,7 @@ const CmcdObjectType = {
1032
1124
  //#endregion
1033
1125
  //#region src/CmcdPlayerState.ts
1034
1126
  /**
1035
- * CMCD v2 player states for the 'sta' key.
1127
+ * CMCD version 2 player states for the 'sta' key.
1036
1128
  *
1037
1129
  *
1038
1130
  * @enum
@@ -1057,29 +1149,29 @@ const CmcdPlayerState = {
1057
1149
  //#endregion
1058
1150
  //#region src/CmcdRecordedReportMode.ts
1059
1151
  /**
1060
- * CMCD query reporting mode — CMCD data carried as a `CMCD=` URL
1152
+ * CMCD query reporting mode: CMCD data transmitted as a `CMCD=` URL
1061
1153
  * query parameter.
1062
1154
  *
1063
1155
  * @public
1064
1156
  */
1065
1157
  const CMCD_RECORDED_REPORT_MODE_QUERY = "query";
1066
1158
  /**
1067
- * CMCD header reporting mode — CMCD data carried as `Cmcd-*` HTTP
1159
+ * CMCD header reporting mode: CMCD data transmitted as `CMCD-*` HTTP
1068
1160
  * headers.
1069
1161
  *
1070
1162
  * @public
1071
1163
  */
1072
1164
  const CMCD_RECORDED_REPORT_MODE_HEADER = "header";
1073
1165
  /**
1074
- * CMCD event reporting mode — CMCD data carried as a POST body to an
1166
+ * CMCD event reporting mode: CMCD data transmitted as a POST body to an
1075
1167
  * event-target URL.
1076
1168
  *
1077
1169
  * @public
1078
1170
  */
1079
1171
  const CMCD_RECORDED_REPORT_MODE_EVENT = "event";
1080
1172
  /**
1081
- * Reporting mode under which a captured report was observed by
1082
- * `CmcdReportRecorder`.
1173
+ * Reporting mode in which `CmcdReportRecorder` observed a captured
1174
+ * report.
1083
1175
  *
1084
1176
  * @enum
1085
1177
  *
@@ -1113,7 +1205,7 @@ const CMCD_RECORDED_REQUEST_TYPE_SEGMENT = "segment";
1113
1205
  */
1114
1206
  const CMCD_RECORDED_REQUEST_TYPE_EVENT = "event";
1115
1207
  /**
1116
- * Fallback classification for requests the recorder did not recognize
1208
+ * Default classification for requests the recorder did not recognize
1117
1209
  * as manifest, segment, or event.
1118
1210
  *
1119
1211
  * @public
@@ -1155,17 +1247,17 @@ async function toHttpRequest(request) {
1155
1247
  };
1156
1248
  }
1157
1249
  /**
1158
- * Create a transport adapter that patches `globalThis.fetch` to capture
1159
- * CMCD-bearing requests, normalizing each to `HttpRequest` (lowercase
1160
- * headers, body read as UTF-8 string). Returns the adapter object
1161
- * expected by `CmcdReportRecorder`.
1162
- *
1163
- * Bodies are read once via `Request.text()` for inspection. This is
1164
- * safe for the body types CMCD reports use in practice (`string`,
1165
- * `Blob`, `ArrayBuffer`, `FormData`, `URLSearchParams`), which can be
1166
- * read by the wrapper and re-read by the underlying `fetch`. Passing
1167
- * a `ReadableStream` as `init.body` is not supported — the stream is
1168
- * consumed by the wrapper and the underlying `fetch` will receive an
1250
+ * Create a transport adapter for `CmcdReportRecorder` that patches
1251
+ * `globalThis.fetch` to capture requests with CMCD data. The patched
1252
+ * `fetch` normalizes each request to `HttpRequest` (lowercase headers,
1253
+ * body read as a UTF-8 string).
1254
+ *
1255
+ * The patched `fetch` reads each body once with `Request.text()` for
1256
+ * inspection. This read is safe for the body types that CMCD reports use
1257
+ * in practice: `string`, `Blob`, `ArrayBuffer`, `FormData`, and
1258
+ * `URLSearchParams`. The underlying `fetch` can read those bodies again.
1259
+ * Do not pass a `ReadableStream` as `init.body`. The patched `fetch`
1260
+ * consumes the stream, and the underlying `fetch` receives an
1169
1261
  * already-disturbed stream.
1170
1262
  *
1171
1263
  * @public
@@ -1231,9 +1323,8 @@ async function completeXhrWith(xhr, response) {
1231
1323
  });
1232
1324
  }
1233
1325
  /**
1234
- * Create a transport adapter that patches `XMLHttpRequest.prototype` to
1235
- * capture CMCD-bearing requests. Returns the adapter object expected by
1236
- * `CmcdReportRecorder`.
1326
+ * Create a transport adapter for `CmcdReportRecorder` that patches
1327
+ * `XMLHttpRequest.prototype` to capture requests with CMCD data.
1237
1328
  *
1238
1329
  * @public
1239
1330
  */
@@ -1296,10 +1387,10 @@ function detectReportingMode(url, headers, isEventTarget) {
1296
1387
  return url.includes(`${CMCD_PARAM}=`) ? "query" : null;
1297
1388
  }
1298
1389
  /**
1299
- * Test helper that records CMCD-bearing reports across XHR and fetch
1300
- * transports for assertion in e2e tests. Each captured request is
1301
- * normalized to {@link @svta/cml-utils!HttpRequest | HttpRequest} so
1302
- * tests are identical regardless of which transport the player uses.
1390
+ * Test helper that records CMCD reports from the XHR and fetch
1391
+ * transports for assertions in e2e tests. The recorder normalizes each
1392
+ * captured request to {@link @svta/cml-utils!HttpRequest | HttpRequest},
1393
+ * so tests are identical regardless of which transport the player uses.
1303
1394
  *
1304
1395
  * @example
1305
1396
  * {@includeCode ../test/CmcdReportRecorder.test.ts#example}
@@ -1367,11 +1458,11 @@ var CmcdReportRecorder = class {
1367
1458
  });
1368
1459
  }
1369
1460
  /**
1370
- * Install transport patches and begin recording CMCD reports.
1371
- * No-op if already attached — the previously supplied options are
1372
- * retained and the new `options` argument is silently ignored.
1373
- * Call `detach()` first if you need to re-attach with different
1374
- * options.
1461
+ * Install transport patches and start recording CMCD reports.
1462
+ * If the recorder is already attached, this call does nothing. The
1463
+ * recorder keeps the previous options and silently ignores the new
1464
+ * `options` argument. To re-attach with different options, call
1465
+ * `detach()` first.
1375
1466
  *
1376
1467
  * @public
1377
1468
  */
@@ -1412,7 +1503,7 @@ var CmcdReportRecorder = class {
1412
1503
  this.#reports = [];
1413
1504
  }
1414
1505
  /**
1415
- * Return a defensive copy of all recorded reports.
1506
+ * Return a copy of all recorded reports.
1416
1507
  *
1417
1508
  * @public
1418
1509
  */
@@ -1421,8 +1512,8 @@ var CmcdReportRecorder = class {
1421
1512
  }
1422
1513
  /**
1423
1514
  * Wait until at least `count` reports of any type are recorded.
1424
- * Resolves with all matching reports; rejects with a diagnostic error
1425
- * on timeout. `count` defaults to 1; `timeout` falls back to the
1515
+ * Resolves with all matching reports. Rejects with a diagnostic error
1516
+ * on timeout. `count` defaults to 1. `timeout` defaults to the
1426
1517
  * recorder's `waitTimeout` option (default 15000 ms).
1427
1518
  *
1428
1519
  * @public
@@ -1432,8 +1523,8 @@ var CmcdReportRecorder = class {
1432
1523
  }
1433
1524
  /**
1434
1525
  * Wait until at least `count` manifest reports are recorded.
1435
- * Resolves with all matching reports; rejects with a diagnostic error
1436
- * on timeout. `count` defaults to 1; `timeout` falls back to the
1526
+ * Resolves with all matching reports. Rejects with a diagnostic error
1527
+ * on timeout. `count` defaults to 1. `timeout` defaults to the
1437
1528
  * recorder's `waitTimeout` option (default 15000 ms).
1438
1529
  *
1439
1530
  * @public
@@ -1443,8 +1534,8 @@ var CmcdReportRecorder = class {
1443
1534
  }
1444
1535
  /**
1445
1536
  * Wait until at least `count` segment reports are recorded.
1446
- * Resolves with all matching reports; rejects with a diagnostic error
1447
- * on timeout. `count` defaults to 1; `timeout` falls back to the
1537
+ * Resolves with all matching reports. Rejects with a diagnostic error
1538
+ * on timeout. `count` defaults to 1. `timeout` defaults to the
1448
1539
  * recorder's `waitTimeout` option (default 15000 ms).
1449
1540
  *
1450
1541
  * @public
@@ -1454,8 +1545,8 @@ var CmcdReportRecorder = class {
1454
1545
  }
1455
1546
  /**
1456
1547
  * Wait until at least `count` event-mode reports are recorded.
1457
- * Resolves with all matching reports; rejects with a diagnostic error
1458
- * on timeout. `count` defaults to 1; `timeout` falls back to the
1548
+ * Resolves with all matching reports. Rejects with a diagnostic error
1549
+ * on timeout. `count` defaults to 1. `timeout` defaults to the
1459
1550
  * recorder's `waitTimeout` option (default 15000 ms).
1460
1551
  *
1461
1552
  * @public
@@ -1503,12 +1594,12 @@ const CmcdTransmissionMode = {
1503
1594
  //#endregion
1504
1595
  //#region src/upConvertToV2.ts
1505
1596
  /**
1506
- * Up-convert V1 CMCD data to V2 format.
1597
+ * Up-convert version 1 CMCD data to version 2.
1507
1598
  *
1508
- * - Wraps plain scalar values in arrays for inner-list keys.
1599
+ * - Wraps scalar values in arrays for inner-list keys.
1509
1600
  * - Wraps `nor` string in an array.
1510
1601
  *
1511
- * If the data is already V2 (has `v: 2`), it is returned unchanged.
1602
+ * The function returns version 2 data (has `v: 2`) unchanged.
1512
1603
  *
1513
1604
  * @internal
1514
1605
  */
@@ -1557,12 +1648,11 @@ function createEncodingOptions(reportingMode, config, baseUrl) {
1557
1648
  };
1558
1649
  }
1559
1650
  /**
1560
- * Deep equality for CmcdObjectTypeList (used for `br` dedup).
1651
+ * Deep equality for CmcdObjectTypeList (used for `br` deduplication).
1561
1652
  *
1562
- * Order-sensitive: arrays with the same elements in different positions
1563
- * are treated as different. Players that construct `br` consistently
1564
- * get correct dedup; shuffling produces spurious emits, which is the
1565
- * safer failure mode.
1653
+ * Arrays with the same elements in different positions count as different.
1654
+ * Players that construct `br` consistently get correct deduplication.
1655
+ * Reordered arrays produce extra events, which is the safer failure.
1566
1656
  */
1567
1657
  function cmcdObjectTypeListEqual(a, b) {
1568
1658
  if (a === b) return true;
@@ -1603,26 +1693,30 @@ const STATE_FIELDS = /* @__PURE__ */ Array.from(CMCD_STATE_EVENT_FIELDS, ([event
1603
1693
  };
1604
1694
  });
1605
1695
  const STATE_FIELDS_BY_EVENT = /* @__PURE__ */ new Map(/* @__PURE__ */ STATE_FIELDS.map((e) => [e.event, e]));
1696
+ function buildRequiredEventKeys() {
1697
+ return new Map([
1698
+ ...CMCD_STATE_EVENT_FIELDS,
1699
+ [CMCD_EVENT_CUSTOM_EVENT, "cen"],
1700
+ [CMCD_EVENT_ERROR, "ec"],
1701
+ [CMCD_EVENT_RESPONSE_RECEIVED, "url"]
1702
+ ]);
1703
+ }
1606
1704
  /**
1607
1705
  * Maps each event type to the key CTA-5004-B requires beyond `e` and `ts`.
1608
1706
  * Built from the state-change table plus the three event types whose
1609
- * required key rides the caller's per-event data.
1707
+ * required key is event data, not player state.
1610
1708
  */
1611
- const CMCD_REQUIRED_EVENT_KEYS = /* @__PURE__ */ new Map([
1612
- ...CMCD_STATE_EVENT_FIELDS,
1613
- [CMCD_EVENT_CUSTOM_EVENT, "cen"],
1614
- [CMCD_EVENT_ERROR, "ec"],
1615
- [CMCD_EVENT_RESPONSE_RECEIVED, "url"]
1616
- ]);
1709
+ const CMCD_REQUIRED_EVENT_KEYS = /* @__PURE__ */ buildRequiredEventKeys();
1617
1710
  /**
1618
1711
  * Whether a required key's value will survive report preparation.
1619
1712
  *
1620
- * This is `isValid` minus its `false` exclusion, plus an empty-array check.
1621
- * `false` must count as usable because `bg: false` is a legitimate value on a
1622
- * backgrounded-mode event, which the encoder emits as `?0`; treating it as
1623
- * unusable would let restoration silently revert a transform that cleared it.
1624
- * Empty strings, empty lists and non-finite numbers are dropped downstream, so
1625
- * a transform substituting one leaves the report short a required key.
1713
+ * This is `isValid` without its `false` exclusion. `false` must count as
1714
+ * usable because `bg: false` is a valid value on a backgrounded-mode event,
1715
+ * which the encoder writes as `?0`. If `false` counted as unusable,
1716
+ * restoration would silently revert a transform that cleared the key.
1717
+ * Later processing drops empty strings, empty lists, and non-finite numbers.
1718
+ * A transform that substitutes one of them leaves the report without a
1719
+ * required key.
1626
1720
  */
1627
1721
  function isUsableRequiredValue(value) {
1628
1722
  if (value == null || value === "") return false;
@@ -1630,12 +1724,12 @@ function isUsableRequiredValue(value) {
1630
1724
  return !Array.isArray(value) || value.length > 0;
1631
1725
  }
1632
1726
  /**
1633
- * Copies an `SfItem`-shaped value and its `params` record.
1727
+ * Copies a value with the `SfItem` structure, including its `params` record.
1634
1728
  *
1635
- * The prototype is preserved because `prepareCmcdData`, the formatter map,
1729
+ * The copy keeps the prototype because `prepareCmcdData`, the formatter map,
1636
1730
  * validation, and the structured-field encoder all branch on
1637
1731
  * `instanceof SfItem`. A plain spread (and `structuredClone`) would return a
1638
- * prototype-less object and silently change what goes on the wire.
1732
+ * prototype-less object and silently change the encoded output.
1639
1733
  */
1640
1734
  function copyItemValue(value) {
1641
1735
  if (value === null || typeof value !== "object") return value;
@@ -1644,16 +1738,16 @@ function copyItemValue(value) {
1644
1738
  return copy;
1645
1739
  }
1646
1740
  /**
1647
- * Copies the nested values of a report in place so a transform cannot mutate
1648
- * the reporter's persistent data, or another target's report for the same
1649
- * event, by mutating an array or an `SfItem` it was handed.
1741
+ * Copies the nested values of a report in place. A transform cannot mutate
1742
+ * the reporter's data store, or another target's report for the same event,
1743
+ * through an array or an `SfItem` it received.
1650
1744
  *
1651
- * Complete for the CMCD value space rather than best-effort: `CmcdValue` and
1745
+ * The copy is complete for the CMCD value space. `CmcdValue` and
1652
1746
  * `CmcdCustomValue` admit only primitives, `SfItem<primitive>`, and arrays of
1653
- * those, and `SfItem.params` is a flat record. Called where a transform is
1654
- * configured, on the ended session's store at archival (detaching the frozen
1655
- * snapshot from caller-held references), and on the request's stored
1656
- * player-facing view.
1747
+ * those. `SfItem.params` is a flat record. The reporter calls this function
1748
+ * where a transform is configured, at session end on the ended session's
1749
+ * store, and on the request's stored player-facing view. The session-end
1750
+ * copy detaches the frozen snapshot from caller-held references.
1657
1751
  */
1658
1752
  function copyReportValues(data) {
1659
1753
  const record = data;
@@ -1668,14 +1762,13 @@ function copyReportValues(data) {
1668
1762
  return data;
1669
1763
  }
1670
1764
  /**
1671
- * Decodes the per-call data snapshot a provenance record carries into
1672
- * fresh, encodable report data: tokens revive as `SfToken` and
1673
- * params-bearing items as `SfItem` (`useSymbol: false`), so every value
1674
- * keeps its wire type through re-encoding. Only called in
1675
- * `recordResponseReceived()`. Returns an empty object when the record is
1676
- * absent, carries no snapshot, or the snapshot does not parse (a value
1677
- * this reporter did not write); the response then reports its derived keys
1678
- * over the session's data alone.
1765
+ * Decodes the per-call data snapshot of a provenance record into new,
1766
+ * encodable report data. Tokens decode as `SfToken` and items with params
1767
+ * as `SfItem` (`useSymbol: false`), so every value re-encodes with its
1768
+ * original type. Only `recordResponseReceived()` calls this function.
1769
+ * Returns an empty object when the record is absent, has no snapshot, or
1770
+ * the snapshot does not parse (a value this reporter did not write). The
1771
+ * response then reports its derived keys over the session's data alone.
1679
1772
  */
1680
1773
  function decodeSnapshot(provenance) {
1681
1774
  const encoded = provenance !== null && typeof provenance === "object" ? provenance.data : void 0;
@@ -1687,10 +1780,10 @@ function decodeSnapshot(provenance) {
1687
1780
  }
1688
1781
  }
1689
1782
  /**
1690
- * Mints a session's frozen base provenance record: the issuing `sid`, and
1691
- * the `cid` in effect at mint time. `update()` re-mints on every `cid`
1692
- * change, so requests issued before a mid-session content change keep the
1693
- * `cid` they were issued under while later requests carry the new one.
1783
+ * Creates a session's frozen base provenance record: the issuing `sid`, and
1784
+ * the `cid` that is current at creation time. `update()` creates a new record
1785
+ * on every `cid` change. Requests issued before a mid-session content change
1786
+ * keep their original `cid`, while later requests get the new one.
1694
1787
  */
1695
1788
  function mintProvenance(sid, cid) {
1696
1789
  return Object.freeze(typeof cid === "string" && cid ? {
@@ -1729,12 +1822,12 @@ function createCmcdReporterConfig(config) {
1729
1822
  /**
1730
1823
  * The CMCD reporter.
1731
1824
  *
1732
- * `C` describes the player's own `customData`, which the reporter passes
1733
- * through to each configured `transform`. It is inferred from the
1734
- * configuration, so annotating a single `transform` types the request in every
1735
- * other one; the default leaves `customData` values `unknown`.
1825
+ * `C` describes the player's own `customData`, which the reporter passes to
1826
+ * each configured `transform`. TypeScript infers `C` from the configuration,
1827
+ * so an annotation on one `transform` types the request in every other
1828
+ * `transform`. The default leaves `customData` values `unknown`.
1736
1829
  *
1737
- * @typeParam C - The shape of the player's `customData`. Defaults to
1830
+ * @typeParam C - The type of the player's `customData`. Defaults to
1738
1831
  * `Record<string, unknown>`.
1739
1832
  *
1740
1833
  * @see {@link https://cta-wave.github.io/Resources/common-media-client-data--cta-5004-b.html#reporting-modes-when-we-send-data | CTA-5004-B Reporting Modes}
@@ -1746,7 +1839,7 @@ var CmcdReporter = class {
1746
1839
  * Creates a new CMCD reporter.
1747
1840
  *
1748
1841
  * @param config - The configuration for the CMCD reporter.
1749
- * @param requester - The function to use to send the request.
1842
+ * @param requester - The function that sends the request.
1750
1843
  * The default is a simple wrapper around the
1751
1844
  * native `fetch` API.
1752
1845
  */
@@ -1764,8 +1857,8 @@ var CmcdReporter = class {
1764
1857
  this.requester = requester;
1765
1858
  }
1766
1859
  /**
1767
- * Creates the state for a new session: fresh counters, gates, queues and
1768
- * dedup baseline for every configured target.
1860
+ * Creates the state for a new session: initial counters, flags, queues, and
1861
+ * deduplication baseline for every configured target.
1769
1862
  */
1770
1863
  createSession(sid, data) {
1771
1864
  const eventTargets = /* @__PURE__ */ new Map();
@@ -1789,11 +1882,12 @@ var CmcdReporter = class {
1789
1882
  };
1790
1883
  }
1791
1884
  /**
1792
- * Starts the CMCD reporter. Called by the player when the reporter is enabled.
1885
+ * Starts the CMCD reporter. The player calls this method when it enables the
1886
+ * reporter.
1793
1887
  *
1794
- * Note: This fires an initial time-interval event immediately (synchronously)
1795
- * before the first interval elapses. Ensure CMCD data (sid, cid, etc.) is
1796
- * populated before calling start().
1888
+ * This method fires an initial time-interval event immediately (synchronously),
1889
+ * before the first interval elapses. Populate the CMCD data (sid, cid, and
1890
+ * others) before calling start().
1797
1891
  */
1798
1892
  start() {
1799
1893
  this.started = true;
@@ -1812,9 +1906,9 @@ var CmcdReporter = class {
1812
1906
  if (failure) throw failure.error;
1813
1907
  }
1814
1908
  /**
1815
- * Arms the time-interval timer for an event target when its config calls
1816
- * for one. Returns whether a timer was armed. The callback reports into
1817
- * whichever session is current when it fires.
1909
+ * Starts the time-interval timer for an event target when its config
1910
+ * requires one. Returns whether a timer started. The callback reports into
1911
+ * whichever session is current when the timer fires.
1818
1912
  */
1819
1913
  armInterval(config) {
1820
1914
  if (config.interval === 0 || !config.events.includes(CMCD_EVENT_TIME_INTERVAL)) return false;
@@ -1829,7 +1923,8 @@ var CmcdReporter = class {
1829
1923
  return true;
1830
1924
  }
1831
1925
  /**
1832
- * Stops the CMCD reporter. Called by the player when the reporter is disabled.
1926
+ * Stops the CMCD reporter. The player calls this method when it disables the
1927
+ * reporter.
1833
1928
  *
1834
1929
  * @param flush - Whether to flush the event targets.
1835
1930
  */
@@ -1839,8 +1934,8 @@ var CmcdReporter = class {
1839
1934
  for (const config of this.config.eventTargets) this.disarmInterval(config);
1840
1935
  }
1841
1936
  /**
1842
- * Forces the sending of all event reports, regardless of the batch size or interval.
1843
- * Useful for sending outstanding reports when the player is destroyed or a playback
1937
+ * Sends all outstanding event reports, regardless of the batch size or
1938
+ * interval. Use this method when the player is destroyed or a playback
1844
1939
  * session ends.
1845
1940
  */
1846
1941
  flush() {
@@ -1849,40 +1944,38 @@ var CmcdReporter = class {
1849
1944
  /**
1850
1945
  * Updates the CMCD data.
1851
1946
  *
1852
- * Called by the player when data changes. For tracked state fields
1853
- * (`sta`, `pr`, `cid`, `bg`, `br`), if the new value differs from the
1854
- * last-reported value (the value most recently emitted on the wire for
1855
- * that field), the corresponding state-change event is automatically
1856
- * fired. Comparing against the last-reported value (rather than the
1857
- * previous persisted value) ensures the first state-change event in a
1858
- * new session always emits, even when the persisted value didn't change
1859
- * across the `sid` boundary.
1947
+ * The player calls this method when data changes. The tracked state fields
1948
+ * are `sta`, `pr`, `cid`, `bg`, and `br`. If a new value for one of them
1949
+ * differs from the last reported value for that field, the reporter fires
1950
+ * the matching state-change event. The comparison uses the last reported
1951
+ * value, not the previous persisted value. The first state-change event in
1952
+ * a new session therefore always fires, even when the persisted value did
1953
+ * not change across the `sid` boundary.
1860
1954
  *
1861
- * Multi-field updates fire multiple events in the order: `sta` → `pr` →
1862
- * `cid` → `bg` → `br`. The order of keys in the input object does not
1863
- * affect the firing order.
1955
+ * Multi-field updates fire the events in this order: `sta`, `pr`, `cid`,
1956
+ * `bg`, `br`. The order of keys in the input object does not affect the
1957
+ * firing order.
1864
1958
  *
1865
- * To attach snapshot context (e.g., `bl`, `mtp`, `pt`, `ltc`) to a
1866
- * state-change event, ensure those fields are in the reporter's data
1867
- * before the state field changes. Either include them alongside the
1868
- * state field in the same `update()` call, or persist them via earlier
1869
- * `update()` calls — auto-fired events emit whatever is currently in
1870
- * the persistent data store. This is also how to keep `TIME_INTERVAL`
1871
- * reports useful — those events draw from the persistent data store
1872
- * with no caller hook for per-event data, so fields the player wants
1873
- * in periodic reports must be kept fresh here.
1959
+ * A state-change event can include context fields, for example `bl`, `mtp`,
1960
+ * `pt`, `ltc`. Those fields must be in the data store before the state field
1961
+ * changes. Either pass them with the state field in the same `update()`
1962
+ * call, or persist them with earlier `update()` calls. An auto-fired event
1963
+ * reports the current content of the data store. `TIME_INTERVAL` events also
1964
+ * read the data store, and the caller cannot add per-call data to them. The
1965
+ * player must therefore update the fields it wants in periodic reports with
1966
+ * this method.
1874
1967
  *
1875
- * A `sid` change resets the dedup baseline.
1968
+ * A `sid` change resets the deduplication baseline.
1876
1969
  *
1877
- * `sid` and `msd` are session-owned: the reporter tracks them itself and
1878
- * stamps them onto every outgoing report, so this method is the only way
1970
+ * `sid` and `msd` are session-owned. The reporter tracks them itself and
1971
+ * writes them into every outgoing report, so this method is the only way
1879
1972
  * to change them. A per-call value on {@link CmcdReporter.recordEvent},
1880
1973
  * {@link CmcdReporter.createRequestReport}, or
1881
- * {@link CmcdReporter.recordResponseReceived} has no effect; response
1882
- * attribution is by the provenance record alone.
1974
+ * {@link CmcdReporter.recordResponseReceived} has no effect. Response
1975
+ * attribution uses the provenance record alone.
1883
1976
  * `msd` must be a finite number of milliseconds between `0` and
1884
- * `999_999_999_999_999` (the RFC 8941 integer maximum); it is rounded to
1885
- * the nearest integer, and invalid values are ignored.
1977
+ * `999_999_999_999_999` (the RFC 8941 integer maximum). The reporter
1978
+ * rounds `msd` to the nearest integer and ignores invalid values.
1886
1979
  *
1887
1980
  * @param data - The data to update.
1888
1981
  */
@@ -1904,19 +1997,20 @@ var CmcdReporter = class {
1904
1997
  for (const entry of STATE_FIELDS) if (entry.field in data && !entry.equal(session.data[entry.field], session.lastEmitted[entry.field])) this.recordEvent(entry.event);
1905
1998
  }
1906
1999
  /**
1907
- * Starts a new session: the current one is archived in place for stale
1908
- * attribution, a fresh one takes over, ended sessions are drained, and
1909
- * the oldest are evicted beyond the configured retention.
2000
+ * Starts a new session. The reporter retains the current session for late
2001
+ * responses and creates a new current session. It then sends the ended
2002
+ * sessions' queued reports and removes the oldest sessions beyond the
2003
+ * configured retention.
1910
2004
  *
1911
- * The data store is carried over, and the archived snapshot is frozen:
1912
- * the ended session's value graph is detached first, so nested values
1913
- * the caller handed to `update()` can no longer mutate it, and the new
1914
- * session starts from a shallow copy of the detached graph, so `cid`,
1915
- * `br`, custom keys and the rest survive a `sid` change as they always
1916
- * have. Sessions are keyed by `sid`, which CTA-5004-B expects to be
1917
- * unique per playback session: reusing one replaces the retained
1918
- * namesake, whose remaining state dies as if evicted and whose late
1919
- * responses relabel onto the replacement.
2005
+ * The ended session's snapshot is frozen: the reporter detaches its value
2006
+ * graph, so nested values from earlier `update()` calls cannot mutate the
2007
+ * snapshot. The new session starts from a shallow copy of that graph, which
2008
+ * preserves the data store. All fields, including `cid`, `br`, and custom
2009
+ * keys, survive a `sid` change as before. CTA-5004-B expects a `sid` to be
2010
+ * unique per playback session, so a reused `sid` replaces the retained
2011
+ * session with that `sid`. The replaced session's remaining state is lost,
2012
+ * as if removed, and the reporter attributes its late responses to the
2013
+ * replacement.
1920
2014
  */
1921
2015
  startSession(sid) {
1922
2016
  this.session.data = copyReportValues({ ...this.session.data });
@@ -1933,59 +2027,57 @@ var CmcdReporter = class {
1933
2027
  }
1934
2028
  }
1935
2029
  /**
1936
- * Records an event. Called by the player when an event occurs.
2030
+ * Records an event. The player calls this method when an event occurs.
1937
2031
  *
1938
2032
  * For state-change events (`PLAY_STATE`, `PLAYBACK_RATE`, `CONTENT_ID`,
1939
2033
  * `BACKGROUNDED_MODE`, `BITRATE_CHANGE`), this method:
1940
- * 1. Persists the dedup field from `data` (if present) into the reporter's
1941
- * persistent data store — equivalent to a write-through `update()`.
1942
- * 2. Drops the event entirely if the dedup field has no value after the
1943
- * write-through (never set, or cleared via `update({ field: undefined })`).
1944
- * State-change events without their required field would violate CTA-5004-B.
1945
- * 3. Suppresses the event if the field's current value matches the
1946
- * last-emitted value (no state transition).
2034
+ * 1. Writes the state field from `data` (if present) into the reporter's
2035
+ * data store, as `update()` would.
2036
+ * 2. Discards the event if the state field has no value after that write
2037
+ * (never set, or cleared with `update({ field: undefined })`).
2038
+ * A state-change event without its required field would violate CTA-5004-B.
2039
+ * 3. Suppresses the event if the field's current value equals the
2040
+ * last reported value (no state transition).
1947
2041
  *
1948
- * For all other event types, the event is always emitted.
2042
+ * The reporter always records all other event types.
1949
2043
  *
1950
- * For state-change events, prefer {@link CmcdReporter.update} — including
1951
- * for snapshot enrichment via a combined call like
1952
- * `update({ sta: 'p', bl: [3000], mtp: [8500] })`. Calling `recordEvent()`
1953
- * for a state-change event after `update()` has already auto-fired it
1954
- * silently drops the second call's `data`, because dedup suppresses the
1955
- * second emission.
2044
+ * For state-change events, prefer {@link CmcdReporter.update}, also for
2045
+ * context fields in a combined call like
2046
+ * `update({ sta: 'p', bl: [3000], mtp: [8500] })`. After `update()`
2047
+ * auto-fires a state-change event, deduplication suppresses a
2048
+ * `recordEvent()` call for the same event, and that call's `data` is
2049
+ * silently lost.
1956
2050
  *
1957
- * Use `recordEvent()` directly for events whose payload is intrinsic to
1958
- * the event call — `CUSTOM_EVENT` with `cen`, `ERROR` with `ec`,
1959
- * ad-lifecycle events, `MUTE`/`UNMUTE`, `PLAYER_EXPAND`/`PLAYER_COLLAPSE`,
1960
- * `SKIP`. For `RESPONSE_RECEIVED`, prefer
1961
- * {@link CmcdReporter.recordResponseReceived}, which derives the
1962
- * per-response fields automatically.
2051
+ * Call `recordEvent()` directly for events whose data comes with the event
2052
+ * call. Examples: `CUSTOM_EVENT` with `cen`, `ERROR` with `ec`, ad lifecycle
2053
+ * events, `MUTE`/`UNMUTE`, `PLAYER_EXPAND`/`PLAYER_COLLAPSE`, `SKIP`. For
2054
+ * `RESPONSE_RECEIVED`, prefer {@link CmcdReporter.recordResponseReceived},
2055
+ * which derives the per-response fields automatically.
1963
2056
  *
1964
2057
  * @param type - The type of event to record.
1965
- * @param data - Additional data to record with the event. This data
1966
- * only applies to this event report, except for the dedup
1967
- * field of a state-change event, which is also persisted
1968
- * into the reporter's data store. Session-owned keys
1969
- * (`sid`, `msd`) supplied here are ignored; the reporter
1970
- * stamps its own values.
2058
+ * @param data - Additional data to record with the event. This data applies
2059
+ * only to this event report. The exception is the state field
2060
+ * of a state-change event, which the reporter also writes into
2061
+ * its data store. The reporter ignores session-owned keys
2062
+ * (`sid`, `msd`) supplied here and writes its own values.
1971
2063
  */
1972
2064
  recordEvent(type, data = {}) {
1973
2065
  this.emitEvent(this.session, type, data);
1974
2066
  }
1975
2067
  /**
1976
- * Records an event across every configured target of a session, applying
1977
- * state-change dedup once before per-target fan-out.
2068
+ * Records an event for every configured target of a session. State-change
2069
+ * deduplication runs once, before the per-target loop.
1978
2070
  *
1979
2071
  * @param session - The session the event belongs to. Everything the event
1980
- * touches (data store, dedup baseline, counters, queues)
1981
- * is that session's; only `recordResponseReceived()` ever
1982
- * passes an archived one.
2072
+ * touches (data store, deduplication baseline, counters,
2073
+ * queues) is that session's. Only `recordResponseReceived()`
2074
+ * ever passes an ended session.
1983
2075
  * @param type - The type of event to record.
1984
2076
  * @param data - Additional data to record with the event.
1985
2077
  * @param request - The media request that triggered the event, when
1986
- * one exists. Only `recordResponseReceived()`
1987
- * populates this; it is threaded through to each
1988
- * target's `transform`.
2078
+ * one exists. Only `recordResponseReceived()` sets this
2079
+ * parameter, which the reporter passes to each target's
2080
+ * `transform`.
1989
2081
  */
1990
2082
  emitEvent(session, type, data, request) {
1991
2083
  const entry = STATE_FIELDS_BY_EVENT.get(type);
@@ -2010,16 +2102,17 @@ var CmcdReporter = class {
2010
2102
  if (failure) throw failure.error;
2011
2103
  }
2012
2104
  /**
2013
- * Records an event for a target. Called by the reporter when an event occurs.
2105
+ * Records an event for a target. The reporter calls this method when an
2106
+ * event occurs.
2014
2107
  *
2015
2108
  * @param target - The target to record the event for.
2016
2109
  * @param config - The configuration for the target.
2017
2110
  * @param type - The type of event to record.
2018
2111
  * @param data - Additional data to record with the event. This data
2019
- * only applies to this event report. Persistent data should
2020
- * be updated using `update()`.
2112
+ * only applies to this event report. Use `update()` for
2113
+ * persistent data.
2021
2114
  * @param request - The media request that triggered the event, when
2022
- * one exists. Passed to the target's `transform`.
2115
+ * one exists. The target's `transform` receives it.
2023
2116
  */
2024
2117
  recordTargetEvent(session, target, config, type, data = {}, request) {
2025
2118
  if (target.disposed || !config.events.includes(type)) return;
@@ -2046,15 +2139,14 @@ var CmcdReporter = class {
2046
2139
  this.queueTargetEvent(session, target, config, report, type);
2047
2140
  }
2048
2141
  /**
2049
- * Stamps the reporter-owned fields on a finished event report, encodes it,
2050
- * and pushes the wire line to the target's queue.
2142
+ * Writes the reporter-owned fields into a finished event report, encodes
2143
+ * the report, and pushes the encoded line to the target's queue.
2051
2144
  *
2052
- * Called after any transform has run, so a transform cannot bypass the
2053
- * target's `events` filter via `e`, break `sn` continuity, or substitute
2054
- * the session identity carried by `sid` and `msd`. Encoding here means a
2055
- * report that cannot serialize throws inside the recording call that
2056
- * produced it, instead of rejecting the batch send and re-queueing
2057
- * forever.
2145
+ * This method runs after any transform. A transform cannot bypass the
2146
+ * target's `events` filter through `e`, break `sn` continuity, or replace
2147
+ * the session identity in `sid` and `msd`. Encoding here makes a report
2148
+ * that cannot serialize throw inside the recording call, instead of failing
2149
+ * the batch send and re-queueing forever.
2058
2150
  *
2059
2151
  * @param target - The target to queue the report for.
2060
2152
  * @param config - The configuration for the target.
@@ -2072,10 +2164,10 @@ var CmcdReporter = class {
2072
2164
  target.queue.push(encodeCmcd(report, createEncodingOptions(CMCD_EVENT_MODE, config)));
2073
2165
  }
2074
2166
  /**
2075
- * Records a response-received event. Called by the player when a media
2076
- * request response has been fully received.
2167
+ * Records a response-received event. The player calls this method when a
2168
+ * media request response has fully arrived.
2077
2169
  *
2078
- * This method automatically derives the `rr` event keys from the
2170
+ * This method derives these `rr` event keys automatically:
2079
2171
  *
2080
2172
  * - `url` - the original requested URL (before any redirects)
2081
2173
  * - `rc` - the HTTP response status code
@@ -2083,50 +2175,50 @@ var CmcdReporter = class {
2083
2175
  * - `ttfb` - time to first byte (from `resourceTiming.responseStart`)
2084
2176
  * - `ttlb` - time to last byte (from `resourceTiming.duration`)
2085
2177
  *
2086
- * Additional keys like `ttfbb`, `cmsdd`, `cmsds`, and `smrt` can be
2087
- * supplied via the `data` parameter if the player has access to them.
2178
+ * If the player has additional keys like `ttfbb`, `cmsdd`, `cmsds`, and
2179
+ * `smrt`, pass them in the `data` parameter.
2088
2180
  *
2089
- * The request's `customData` is generic, so a player can pass a request
2090
- * carrying its own taxonomy (e.g. `{ requestType: 'segment' }`) without a
2091
- * cast. The reporter reads only the provenance record that
2092
- * {@link CmcdReporter.createRequestReport} writes there; every other key
2093
- * is left untouched and stays visible to an event target's `transform`
2094
- * via its `request` argument.
2181
+ * The request's `customData` is generic, so a player can pass a request with
2182
+ * its own keys (for example `{ requestType: 'segment' }`) without a cast. The
2183
+ * reporter reads only the provenance record that
2184
+ * {@link CmcdReporter.createRequestReport} writes there. Every other key
2185
+ * stays unchanged and visible to an event target's `transform` through its
2186
+ * `request` argument.
2095
2187
  *
2096
- * A reporter given a concrete `C` requires the request to satisfy it, so a
2097
- * request the configured transforms could not read is rejected here rather
2098
- * than reaching them; see {@link CmcdReporterCustomData}.
2188
+ * A reporter with a concrete `C` requires the request to satisfy that type.
2189
+ * TypeScript therefore rejects a call whose request the configured
2190
+ * transforms could not read. See {@link CmcdReporterCustomData}.
2099
2191
  *
2100
- * The event is attributed to the session that issued the request, and to
2101
- * nothing else: the provenance record that
2192
+ * The reporter attributes the event only to the session that issued the
2193
+ * request. The provenance record that
2102
2194
  * {@link CmcdReporter.createRequestReport} stored on the request's
2103
- * `customData` (under {@link CMCD_REQUEST_PROVENANCE}) selects the
2104
- * session by its `sid`, or the response is dropped rather than
2105
- * relabeled. There is no other key: a record lost to a serialization
2106
- * boundary (and not restored; see the bridge notes on
2107
- * {@link CMCD_REQUEST_PROVENANCE}) and a session no longer retained
2108
- * (see `CmcdReporterConfig.sessionRetention`) both drop, and a per-call
2109
- * `data.sid` cannot substitute. The record is honored wherever its
2110
- * `sid` resolves: a request decorated by another reporter configured
2111
- * with the same session attributes here, as does a hand-built record.
2112
- * A response that completes after a `sid` change reports under its own
2113
- * retained session, with that session's data snapshot and sequence
2114
- * numbers.
2195
+ * `customData` (under {@link CMCD_REQUEST_PROVENANCE}) selects the session
2196
+ * by `sid`. Without a match, the reporter discards the response rather than
2197
+ * attributing it elsewhere. There is no other key. The reporter discards the
2198
+ * response when a serialization boundary lost the record and nothing
2199
+ * restored it (see {@link CMCD_REQUEST_PROVENANCE}). It also discards the
2200
+ * response when the session is no longer retained (see
2201
+ * `CmcdReporterConfig.sessionRetention`). A per-call `data.sid` cannot
2202
+ * substitute. The reporter accepts any record whose `sid` names a retained
2203
+ * session. The record may come from another reporter with the same session
2204
+ * attributes, or be hand-built. A response that completes after a `sid`
2205
+ * change reports under its own retained session, with that session's data
2206
+ * snapshot and sequence numbers.
2115
2207
  *
2116
- * Request-time report keys come from the record's encoded per-call
2117
- * `data` snapshot, decoded fresh per response, and the record's `cid`
2118
- * reports in place of the session's current one, so a response that
2119
- * completes after a mid-session content change keeps the meaning it
2120
- * had when its request was issued.
2208
+ * Request-time report keys come from the record's encoded per-call `data`
2209
+ * snapshot, which the reporter decodes for each response. The record's `cid`
2210
+ * replaces the session's current `cid` in the report. A response that
2211
+ * completes after a mid-session content change therefore reports its
2212
+ * request-time values.
2121
2213
  *
2122
- * @typeParam RD - The `customData` this request carries. Defaults to the
2214
+ * @typeParam RD - The `customData` of this request. Defaults to the
2123
2215
  * reporter's own `C`.
2124
2216
  *
2125
2217
  * @param response - The HTTP response received.
2126
2218
  * @param data - Additional CMCD data to include with the event.
2127
- * Values provided here override any auto-derived values.
2128
- * Session-owned keys (`sid`, `msd`) supplied here are
2129
- * ignored; attribution is by the provenance record alone.
2219
+ * Values provided here override the derived values.
2220
+ * The reporter ignores session-owned keys (`sid`, `msd`)
2221
+ * supplied here. Attribution uses the provenance record alone.
2130
2222
  */
2131
2223
  recordResponseReceived(response, data = {}) {
2132
2224
  const { request } = response;
@@ -2159,13 +2251,13 @@ var CmcdReporter = class {
2159
2251
  }, request);
2160
2252
  }
2161
2253
  /**
2162
- * Resolves the session a response belongs to: the provenance record's
2163
- * `sid` names one of this reporter's retained sessions, or the response
2164
- * is dropped. There is no other key — a record that was lost, or that
2165
- * names an evicted or never-seen `sid`, resolves nothing, and
2166
- * attributing it anywhere else would relabel it. The `sid` is read
2167
- * structurally, so a JSON-revived copy of a record attributes exactly,
2168
- * and a hand-built record naming a retained session is honored.
2254
+ * Resolves the session a response belongs to. The provenance record's `sid`
2255
+ * must name one of this reporter's retained sessions, or the reporter
2256
+ * discards the response. There is no other key. A lost record, or one that
2257
+ * names a removed or unknown `sid`, resolves nothing. Any other attribution
2258
+ * would be wrong. The reporter reads the `sid` as a plain property. A record
2259
+ * copied through JSON therefore resolves the same session, and a hand-built
2260
+ * record that names a retained session is accepted.
2169
2261
  */
2170
2262
  resolveSession(provenance) {
2171
2263
  if (provenance === null || typeof provenance !== "object") return;
@@ -2173,8 +2265,8 @@ var CmcdReporter = class {
2173
2265
  return typeof sid === "string" ? this.sessions.get(sid) : void 0;
2174
2266
  }
2175
2267
  /**
2176
- * Applies the CMCD request report data to the request. Called by the player
2177
- * before sending the request.
2268
+ * Applies the CMCD request report data to the request. The player calls this
2269
+ * method before sending the request.
2178
2270
  *
2179
2271
  * @param req - The request to apply the CMCD request report to.
2180
2272
  * @returns The request with the CMCD request report applied.
@@ -2185,23 +2277,22 @@ var CmcdReporter = class {
2185
2277
  return this.createRequestReport(req) ?? req;
2186
2278
  }
2187
2279
  /**
2188
- * Checks if the request reporting is enabled.
2280
+ * Checks whether request reporting is enabled.
2189
2281
  *
2190
- * @returns `true` if the request reporting is enabled, `false` otherwise.
2282
+ * @returns `true` if request reporting is enabled, `false` otherwise.
2191
2283
  */
2192
2284
  isRequestReportingEnabled() {
2193
2285
  return !!this.config.enabledKeys?.length;
2194
2286
  }
2195
2287
  /**
2196
- * The provenance record to stamp on a returned request: the session's
2197
- * frozen base record, extended with the caller's per-call data encoded
2198
- * as a CMCD string when there is any. Requests without per-call data
2199
- * share the base record, so decoration stays a single property write.
2200
- * The snapshot never contains reporter-stamped fields, and a value that
2201
- * cannot encode contributes no snapshot rather than failing the
2202
- * request; its response still attributes and reports derived keys over
2203
- * session data, matching `decodeSnapshot()`'s tolerance on the read
2204
- * side.
2288
+ * The provenance record to write on a returned request. It is the session's
2289
+ * frozen base record, extended with the caller's per-call data encoded as a
2290
+ * CMCD string when there is any. Requests without per-call data share the
2291
+ * base record, so the reporter writes only one property. The snapshot never
2292
+ * contains reporter-written fields. A value that cannot encode contributes
2293
+ * no snapshot and does not fail the request. The response still attributes
2294
+ * and reports derived keys over session data, matching the tolerance of
2295
+ * `decodeSnapshot()` when reading.
2205
2296
  */
2206
2297
  createRequestProvenance(data, baseUrl) {
2207
2298
  const base = this.session.provenance;
@@ -2221,21 +2312,21 @@ var CmcdReporter = class {
2221
2312
  }
2222
2313
  }
2223
2314
  /**
2224
- * Creates a new request with the CMCD request report data applied. Called by the player
2225
- * before sending the request.
2315
+ * Creates a new request with the CMCD request report data applied. The
2316
+ * player calls this method before sending the request.
2226
2317
  *
2227
- * A reporter given a concrete `C` requires the request's `customData` to
2228
- * satisfy it, so a request the configured transform could not read is
2229
- * rejected here rather than reaching it; see
2318
+ * A reporter with a concrete `C` requires the request's `customData` to
2319
+ * satisfy that type. TypeScript therefore rejects a call whose request the
2320
+ * configured transform could not read. See
2230
2321
  * {@link CmcdReporterCustomData}.
2231
2322
  *
2232
- * @typeParam R - The request being decorated. Its `customData` must satisfy
2323
+ * @typeParam R - The type of the request. Its `customData` must satisfy
2233
2324
  * the reporter's `C`.
2234
2325
  *
2235
2326
  * @param request - The request to apply the CMCD request report to.
2236
2327
  * @param data - The data to apply to the request. This data only
2237
- * applies to this request report. Persistent data
2238
- * should be updated using `update()`.
2328
+ * applies to this request report. Use `update()` for
2329
+ * persistent data.
2239
2330
  * @returns The request with the CMCD request report applied.
2240
2331
  */
2241
2332
  createRequestReport(request, data) {
@@ -2287,7 +2378,8 @@ var CmcdReporter = class {
2287
2378
  return report;
2288
2379
  }
2289
2380
  /**
2290
- * Processes the event targets. Called by the reporter when an event occurs.
2381
+ * Processes the event targets. The reporter calls this method when an event
2382
+ * occurs.
2291
2383
  *
2292
2384
  * @param flush - Whether to flush the event targets.
2293
2385
  */
@@ -2310,7 +2402,8 @@ var CmcdReporter = class {
2310
2402
  if (reprocess) this.processEventTargets();
2311
2403
  }
2312
2404
  /**
2313
- * Sends an event report. Called by the reporter when a batch is ready to be sent.
2405
+ * Sends an event report. The reporter calls this method when a batch is
2406
+ * ready.
2314
2407
  *
2315
2408
  * @param config - The target config to send the event report to.
2316
2409
  * @param data - The encoded report lines to send in the event report.
@@ -2320,27 +2413,27 @@ var CmcdReporter = class {
2320
2413
  url: config.url,
2321
2414
  method: "POST",
2322
2415
  headers: { "Content-Type": CMCD_MIME_TYPE },
2323
- body: data.join("\n") + "\n"
2416
+ body: data.join("\n")
2324
2417
  });
2325
2418
  if (status === 410) this.disposeEventTarget(session, config);
2326
2419
  else if (status === 429 || status > 499 && status < 600) throw new Error(`Event report failed with status ${status}`);
2327
2420
  }
2328
2421
  /**
2329
2422
  * Cancels the time-interval timer for an event target config and clears the
2330
- * stored id. Safe to call when no timer is armed (clearInterval(undefined)
2331
- * is a no-op).
2423
+ * stored id. Safe to call when no timer is active (clearInterval(undefined)
2424
+ * does nothing).
2332
2425
  */
2333
2426
  disarmInterval(config) {
2334
2427
  clearInterval(this.intervals.get(config));
2335
2428
  this.intervals.delete(config);
2336
2429
  }
2337
2430
  /**
2338
- * Silences an event target URL for the remainder of its session: drops the
2339
- * queues and blocks further enqueues for every config in the session that
2340
- * reports to the URL, per CTA-5004-B's per-target-URL suppression scope,
2341
- * and cancels their timers when the session is the live one. Used when the
2342
- * collector signals the target is gone (HTTP 410). A session started after
2343
- * the disposal is unaffected, since it gets fresh target state.
2431
+ * Silences an event target URL for the remainder of its session after the
2432
+ * collector signals that the target is gone (HTTP 410). The method clears
2433
+ * the queues and blocks further enqueues for every config in the session
2434
+ * that reports to the URL (the CTA-5004-B suppression scope). For the
2435
+ * current session, the method also cancels those configs' timers. A session
2436
+ * started after the disposal gets new target state and is unaffected.
2344
2437
  */
2345
2438
  disposeEventTarget(session, config) {
2346
2439
  session.eventTargets.forEach((target, sibling) => {
@@ -2493,7 +2586,7 @@ function isCmcdV2Data(data) {
2493
2586
  * Convert a value to a CMCD value.
2494
2587
  *
2495
2588
  * @param value - The value to convert to a CMCD value.
2496
- * @param params - The parameters to convert to a CMCD value.
2589
+ * @param params - The parameters of the CMCD value.
2497
2590
  * @returns The CMCD value.
2498
2591
  *
2499
2592
  * @public
@@ -2520,7 +2613,7 @@ function mergeValidationResults(...results) {
2520
2613
  //#endregion
2521
2614
  //#region src/resolveVersion.ts
2522
2615
  /**
2523
- * Resolves the CMCD version from explicit options, the payload's `v` key, or the default (v1).
2616
+ * Resolves the CMCD version from options, the payload's `v` key, or the default (version 1).
2524
2617
  *
2525
2618
  * @internal
2526
2619
  */
@@ -2611,7 +2704,8 @@ function validateCmcdStructure(data, options) {
2611
2704
  });
2612
2705
  }
2613
2706
  if ("e" in data) {
2614
- if (data["e"] === CMCD_EVENT_CUSTOM_EVENT) {
2707
+ const eventType = toTokenString(data["e"]);
2708
+ if (eventType === CMCD_EVENT_CUSTOM_EVENT) {
2615
2709
  if (!("cen" in data)) issues.push({
2616
2710
  key: "cen",
2617
2711
  message: "Custom event (e=\"ce\") requires the \"cen\" key to be present.",
@@ -2622,7 +2716,7 @@ function validateCmcdStructure(data, options) {
2622
2716
  message: "The \"cen\" key must only be present when e=\"ce\".",
2623
2717
  severity: CMCD_VALIDATION_SEVERITY_ERROR
2624
2718
  });
2625
- if (data["e"] === CMCD_EVENT_RESPONSE_RECEIVED) {
2719
+ if (eventType === CMCD_EVENT_RESPONSE_RECEIVED) {
2626
2720
  if (!("url" in data)) issues.push({
2627
2721
  key: "url",
2628
2722
  message: "Response received event (e=\"rr\") requires the \"url\" key to be present.",
@@ -2633,17 +2727,34 @@ function validateCmcdStructure(data, options) {
2633
2727
  message: `Response key "${key}" must only be present when e="rr".`,
2634
2728
  severity: CMCD_VALIDATION_SEVERITY_ERROR
2635
2729
  });
2636
- for (const [eventType, requiredField] of CMCD_STATE_EVENT_FIELDS) if (data["e"] === eventType && !(requiredField in data)) issues.push({
2730
+ for (const [stateEventType, requiredField] of CMCD_STATE_EVENT_FIELDS) if (eventType === stateEventType && !(requiredField in data)) issues.push({
2637
2731
  key: requiredField,
2638
- message: `State-change event (e="${eventType}") requires the "${requiredField}" key to be present.`,
2732
+ message: `State-change event (e="${stateEventType}") requires the "${requiredField}" key to be present.`,
2639
2733
  severity: CMCD_VALIDATION_SEVERITY_ERROR
2640
2734
  });
2641
- if (data["e"] === CMCD_EVENT_ERROR && !("ec" in data)) issues.push({
2735
+ if (eventType === CMCD_EVENT_ERROR && !("ec" in data)) issues.push({
2642
2736
  key: "ec",
2643
2737
  message: "Error event (e=\"e\") requires the \"ec\" key to be present.",
2644
2738
  severity: CMCD_VALIDATION_SEVERITY_ERROR
2645
2739
  });
2646
2740
  }
2741
+ const ot = version > 1 ? toTokenString(data["ot"]) : void 0;
2742
+ if (ot !== void 0) for (const key in CMCD_KEY_OBJECT_TYPES) {
2743
+ const objectTypes = CMCD_KEY_OBJECT_TYPES[key];
2744
+ if (key in data && !objectTypes.includes(ot)) issues.push({
2745
+ key,
2746
+ message: `Key "${key}" must not be present when "ot" is "${ot}". Expected "ot" to be one of: ${objectTypes.join(", ")}.`,
2747
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
2748
+ });
2749
+ }
2750
+ if (version > 1) for (const key in CMCD_AGGREGATE_BITRATE_KEYS) {
2751
+ const exactKey = CMCD_AGGREGATE_BITRATE_KEYS[key];
2752
+ if (key in data && exactKey in data) issues.push({
2753
+ key,
2754
+ message: `Key "${key}" must not be present when "${exactKey}" is present.`,
2755
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
2756
+ });
2757
+ }
2647
2758
  if ("v" in data && data["v"] !== 1 && data["v"] !== 2) issues.push({
2648
2759
  key: "v",
2649
2760
  message: `Unsupported CMCD version "${String(data["v"])}". Expected 1 or 2.`,
@@ -2710,8 +2821,8 @@ const CMCD_KEY_TYPE_STRING = "string";
2710
2821
  */
2711
2822
  const CMCD_KEY_TYPE_TOKEN = "token";
2712
2823
  /**
2713
- * Maps each CMCD spec key to its expected value type for v2.
2714
- * Keys that differ between v1 and v2 are handled by CMCD_V1_KEY_TYPE_OVERRIDES.
2824
+ * Maps each CMCD key to its value type for version 2.
2825
+ * CMCD_V1_KEY_TYPE_OVERRIDES handles the keys that differ between version 1 and version 2.
2715
2826
  *
2716
2827
  * @internal
2717
2828
  */
@@ -2769,7 +2880,7 @@ const CMCD_KEY_TYPES = {
2769
2880
  sta: CMCD_KEY_TYPE_TOKEN
2770
2881
  };
2771
2882
  /**
2772
- * Maps keys to their v1-specific types when they differ from v2.
2883
+ * Version 1 types of the keys that differ from version 2.
2773
2884
  *
2774
2885
  * @internal
2775
2886
  */
@@ -3024,9 +3135,10 @@ function validateCmcdValues(data, options) {
3024
3135
  break;
3025
3136
  case CMCD_KEY_TYPE_TOKEN: {
3026
3137
  const validValues = CMCD_TOKEN_VALUES[key];
3027
- if (validValues && !validValues.includes(value)) issues.push({
3138
+ const token = toTokenString(value);
3139
+ if (validValues && (token === void 0 || !validValues.includes(token))) issues.push({
3028
3140
  key,
3029
- message: `Key "${key}" has invalid token value "${String(value)}". Expected one of: ${validValues.join(", ")}.`,
3141
+ message: `Key "${key}" has invalid token value "${token ?? String(value)}". Expected one of: ${validValues.join(", ")}.`,
3030
3142
  severity: CMCD_VALIDATION_SEVERITY_ERROR
3031
3143
  });
3032
3144
  break;
@@ -3072,8 +3184,8 @@ function validateCmcd(data, options) {
3072
3184
  *
3073
3185
  * This function decodes the string internally and validates it with
3074
3186
  * `reportingMode` set to `'event'`. The input may contain multiple
3075
- * newline-separated events (e.g. an `application/cmcd` POST body), in which
3076
- * case each line is validated independently and the results are merged.
3187
+ * newline-separated events, for example an `application/cmcd` POST body.
3188
+ * The function validates each line independently and merges the results.
3077
3189
  *
3078
3190
  * @param cmcd - The raw CMCD-encoded string to validate. May contain
3079
3191
  * multiple newline-separated event lines.
@@ -3101,6 +3213,11 @@ function validateCmcdEvents(cmcd, options) {
3101
3213
  }],
3102
3214
  data: []
3103
3215
  };
3216
+ const bodyIssues = [];
3217
+ if (cmcd.endsWith("\n")) bodyIssues.push({
3218
+ message: "Event report body must not end with a line feed.",
3219
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
3220
+ });
3104
3221
  const decodedLines = [];
3105
3222
  const lineResults = [];
3106
3223
  for (let i = 0; i < lines.length; i++) try {
@@ -3118,7 +3235,10 @@ function validateCmcdEvents(cmcd, options) {
3118
3235
  });
3119
3236
  }
3120
3237
  return {
3121
- ...mergeValidationResults(...lineResults),
3238
+ ...mergeValidationResults({
3239
+ valid: true,
3240
+ issues: bodyIssues
3241
+ }, ...lineResults),
3122
3242
  data: decodedLines
3123
3243
  };
3124
3244
  }
@@ -3131,8 +3251,8 @@ function validateCmcdEvents(cmcd, options) {
3131
3251
  * Accepts an {@link @svta/cml-utils!HttpRequest | HttpRequest} object.
3132
3252
  *
3133
3253
  * This function validates that the request uses the POST method and has
3134
- * the correct `Content-Type` header (`application/cmcd`) in addition to
3135
- * validating the body content via {@link validateCmcdEvents}.
3254
+ * the correct `Content-Type` header (`application/cmcd`). It also
3255
+ * validates the body content with {@link validateCmcdEvents}.
3136
3256
  *
3137
3257
  * @param request - An `HttpRequest` to validate.
3138
3258
  * @param options - Validation options (excluding `reportingMode`).
@@ -3198,15 +3318,15 @@ function validateCmcdEventReport(request, options) {
3198
3318
  /**
3199
3319
  * Validates CMCD HTTP headers by checking shard placement and payload validity.
3200
3320
  *
3201
- * This function accepts raw CMCD header strings, decodes each shard
3202
- * internally, verifies that each key is placed in its correct header shard,
3203
- * then merges all shards and runs full payload validation (keys, values, and
3204
- * structure) on the merged data.
3321
+ * This function accepts raw CMCD header strings and decodes each shard
3322
+ * internally. It verifies that each key is in its correct header shard.
3323
+ * The function then merges all shards and runs payload validation (keys,
3324
+ * values, and structure) on the merged data.
3205
3325
  *
3206
3326
  * @example
3207
3327
  * {@includeCode ../test/validateCmcdHeaders.test.ts#example}
3208
3328
  *
3209
- * @param headers - A `Headers` instance or a record of CMCD header fields to their raw encoded string values.
3329
+ * @param headers - A `Headers` instance or a record of CMCD header fields to raw encoded strings.
3210
3330
  * @param options - Validation options (excluding `reportingMode`).
3211
3331
  * @returns The validation result including decoded data.
3212
3332
  *
@@ -3266,10 +3386,12 @@ function validateCmcdHeaders(headers, options) {
3266
3386
  * {@link https://developer.mozilla.org/en-US/docs/Web/API/Request | Request}
3267
3387
  * object or an {@link @svta/cml-utils!HttpRequest | HttpRequest} object.
3268
3388
  *
3269
- * The function checks for CMCD data in the HTTP headers first. If CMCD
3270
- * headers are found, validation includes shard-placement checks via
3271
- * {@link validateCmcdHeaders}. Otherwise, the CMCD query parameter is
3272
- * extracted from the URL and validated.
3389
+ * The function checks the HTTP headers for CMCD data first. If the
3390
+ * request has CMCD headers, {@link validateCmcdHeaders} validates them,
3391
+ * including shard placement. Otherwise, the function validates the CMCD
3392
+ * query parameter of the URL. A request with CMCD data in both the
3393
+ * headers and the `CMCD` query parameter is an error. The function still
3394
+ * validates the headers and returns their data.
3273
3395
  *
3274
3396
  * @param request - A `Request` or `HttpRequest` to validate.
3275
3397
  * @param options - Validation options (excluding `reportingMode`).
@@ -3284,12 +3406,25 @@ function validateCmcdHeaders(headers, options) {
3284
3406
  */
3285
3407
  function validateCmcdRequest(request, options) {
3286
3408
  const headers = extractHeaderRecord(request.headers);
3287
- if (headers) return validateCmcdHeaders(headers, options);
3288
- const param = new URL(request.url).searchParams.get(CMCD_PARAM);
3409
+ const param = getCmcdQueryParam(request.url);
3410
+ if (headers) {
3411
+ const result = validateCmcdHeaders(headers, options);
3412
+ if (!param) return result;
3413
+ return {
3414
+ ...mergeValidationResults({
3415
+ valid: false,
3416
+ issues: [{
3417
+ message: "CMCD data found in both request headers and the \"CMCD\" query parameter. A request must use only one transmission mode.",
3418
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
3419
+ }]
3420
+ }, result),
3421
+ data: result.data
3422
+ };
3423
+ }
3289
3424
  if (!param) return {
3290
3425
  valid: false,
3291
3426
  issues: [{
3292
- message: "No CMCD data found in request headers or query parameters.",
3427
+ message: hasCmcdInFragment(request.url) ? "No CMCD data found in request headers or query parameters. The URL fragment contains a \"CMCD\" parameter. A server never receives the fragment." : "No CMCD data found in request headers or query parameters.",
3293
3428
  severity: CMCD_VALIDATION_SEVERITY_ERROR
3294
3429
  }],
3295
3430
  data: {}
@@ -3315,6 +3450,18 @@ function validateCmcdRequest(request, options) {
3315
3450
  data
3316
3451
  };
3317
3452
  }
3453
+ const CMCD_IN_FRAGMENT = /(?:^|[?&])CMCD=/;
3454
+ function getCmcdQueryParam(url) {
3455
+ const hash = url.indexOf("#");
3456
+ const path = hash < 0 ? url : url.slice(0, hash);
3457
+ const start = path.indexOf("?");
3458
+ if (start < 0) return null;
3459
+ return new URLSearchParams(path.slice(start + 1)).get(CMCD_PARAM);
3460
+ }
3461
+ function hasCmcdInFragment(url) {
3462
+ const hash = url.indexOf("#");
3463
+ return hash >= 0 && CMCD_IN_FRAGMENT.test(url.slice(hash + 1));
3464
+ }
3318
3465
  function extractHeaderRecord(headers) {
3319
3466
  if (!headers) return;
3320
3467
  const h = ensureHeaders(headers);